+188
-369
AGENTS.md
+188
-369
AGENTS.md
···-This document is a complete handoff for the next Codex instance working on the Tangled CLI (Rust). It explains what exists, what to build next, where to edit, how to call the APIs, how to persist sessions, how to print output, and how to validate success.+This document provides an overview of the Tangled CLI implementation status for AI agents or developers working on the project.-Primary focus for this session: implement authentication (auth login/status/logout) and repository listing (repo list).-- POST to `/xrpc/com.atproto.server.createSession` at the configured PDS (default `https://bsky.social`).+- `list` - List repositories using `com.atproto.repo.listRecords` with `collection=sh.tangled.repo`+- `list` - List issues via `com.atproto.repo.listRecords` with `collection=sh.tangled.repo.issue`+- `list` - List pipeline runs via `com.atproto.repo.listRecords` with `collection=sh.tangled.pipeline`+- `logs` - Stream workflow logs via WebSocket (`wss://spindle.tangled.sh/spindle/logs/{knot}/{rkey}/{name}`)-- `tangled/crates/tangled-cli/src/cli.rs` → already contains arguments and subcommands; no structural changes needed.-- `tangled/crates/tangled-config/src/session.rs` → already provides `Session` + `SessionManager` (keyring).-- `tangled/crates/tangled-config/src/config.rs` → optional use for PDS/base URL (MVP can use CLI flags/env vars).-- `tangled/crates/tangled-api/src/client.rs` → add XRPC helpers and implement `login_with_password` and `list_repos`.-- Workspace is scaffolded and compiles after wiring dependencies (network needed to fetch crates):-- Placeholder lexicons in `tangled/lexicons/sh.tangled/*` are not authoritative; use AT Protocol docs and inspect real endpoints later.+- **Tangled API Base** (server operations): Default `https://tngl.sh`, can override via `TANGLED_API_BASE`+- **Spindle Base** (CI/CD): Default `wss://spindle.tangled.sh` for WebSocket logs, can override via `TANGLED_SPINDLE_BASE`-- `async fn post_json<TReq: Serialize, TRes: DeserializeOwned>(&self, method, req, bearer) -> Result<TRes>`.-- `pub async fn login_with_password(&self, handle: &str, password: &str, pds: &str) -> Result<Session>`-- POST to `com.atproto.server.createSession` at `self.base_url` (which should be the PDS base).-- `pub async fn list_repos(&self, user: Option<&str>, knot: Option<&str>, starred: bool, bearer: Option<&str>) -> Result<Vec<Repository>>`-- Determine PDS: use `--pds` arg if provided, else default `https://bsky.social` (later from config/env).-- Load session; if absent, print `Please login first: tangled auth login` and exit 1 (or 0 with friendly message; choose one and be consistent).-- Build a client for Tangled API base (for now, default to `https://tangled.org` or allow `TANGLED_API_BASE` env var to override):-- `let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tangled.org".into());`-- Call `client.list_repos(args.user.as_deref(), args.knot.as_deref(), args.starred, Some(session.access_jwt.as_str())).await?`.-if let Some(token) = bearer { reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); }-if let Some(token) = bearer { reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token)); }-pub async fn login_with_password(&self, handle: &str, password: &str, _pds: &str) -> Result<Session> {-struct Req<'a> { #[serde(rename = "identifier")] identifier: &'a str, #[serde(rename = "password")] password: &'a str }-struct Res { #[serde(rename = "accessJwt")] access_jwt: String, #[serde(rename = "refreshJwt")] refresh_jwt: String, did: String, handle: String }-Ok(Session { access_jwt: res.access_jwt, refresh_jwt: res.refresh_jwt, did: res.did, handle: res.handle, ..Default::default() })-pub async fn list_repos(&self, user: Option<&str>, knot: Option<&str>, starred: bool, bearer: Option<&str>) -> Result<Vec<Repository>> {-pub struct Repository { pub did: Option<String>, pub rkey: Option<String>, pub name: String, pub knot: Option<String>, pub description: Option<String>, pub private: bool }-let handle: String = match args.handle.take() { Some(h) => h, None => Input::new().with_prompt("Handle").interact_text()? };-let password: String = match args.password.take() { Some(p) => p, None => Password::new().with_prompt("Password").interact()? };-if mgr.load()?.is_some() { mgr.clear()?; println!("Logged out"); } else { println!("No session found"); }-match cmd { RepoCommand::List(args) => list(args).await, _ => Ok(println!("not implemented")) }-let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tangled.org".into());-let repos = client.list_repos(args.user.as_deref(), args.knot.as_deref(), args.starred, Some(session.access_jwt.as_str())).await?;-- PDS base (auth): default `https://bsky.social`. Accept CLI flag `--pds`; later read from config.-- Tangled API base (repo list): default `https://tangled.org`; allow override via `TANGLED_API_BASE` env var.+The only remaining stub is `spindle run` for manually triggering workflows. Implementation plan:-- Client unit tests with `mockito` for `createSession` and `repo list` endpoints; simulate expected JSON.-- CLI smoke tests optional for this pass. If added, use `assert_cmd` to check printed output strings.-- Keyring errors on Linux may indicate no secret service running; suggest enabling GNOME Keyring or KWallet.-- If `repo list` returns 404, the method name or base URL may be wrong; adjust `sh.tangled.repo.list` or `TANGLED_API_BASE`.-- Repo list: `TANGLED_API_BASE=https://tangled.org cargo run -p tangled-cli -- repo list --user <handle>`-End of handoff. Implement auth login and repo list as described, keeping changes focused and testable.-This workspace often needs to peek at the Tangled monorepo to confirm XRPC endpoints and shapes. Here are concise tips and findings that informed this CLI implementation.-- Note: there is no `sh.tangled.repo.list` lexicon in the core right now; listing is done via ATproto records.-- Knotserver XRPC routes (what requires auth vs open): `../tangled-core/knotserver/xrpc/xrpc.go`-- Validates ServiceAuth; expects rkey for the `sh.tangled.repo` record that already exists on the user's PDS.-- ServiceAuth middleware (how Bearer is validated): `../tangled-core/xrpc/serviceauth/service_auth.go`-- Appview client for ServiceAuth: `../tangled-core/appview/xrpcclient/xrpc.go` (method: `ServerGetServiceAuth`).-- `ls ../tangled-core/lexicons/repo` or `rg -n "\bid\": \"sh\.tangled\..*\"" ../tangled-core/lexicons`-2) List records from the user’s PDS: `GET com.atproto.repo.listRecords` with `collection=sh.tangled.repo`.-3) Filter client-side (e.g., by `knot`). “Starred” filtering is not currently defined in core.-- `POST com.atproto.repo.createRecord` with `{ repo: <did>, collection: "sh.tangled.repo", record: { name, knot, description?, createdAt } }`.-- Obtain ServiceAuth: `GET com.atproto.server.getServiceAuth` from PDS with `aud=did:web:<tngl.sh or target-host>`.-- `POST sh.tangled.repo.create` on the Tangled API base with `{ rkey, defaultBranch?, source? }` and `Authorization: Bearer <serviceAuth>`.-- Server validates token via `xrpc/serviceauth`, confirms actor permissions, and creates the git repo.-- Tangled API base (server): default is `https://tngl.sh`. Do not use the marketing/landing site.-- PDS base (auth + record ops): default `https://bsky.social` unless a different PDS was chosen on login.-- ServiceAuth audience DID is `did:web:<host>` where `<host>` is the Tangled API base hostname.-- `InvalidToken` when listing repos: listing should use the PDS (`com.atproto.repo.listRecords`), not the Tangled API base.-- 404 on `repo.create`: verify ServiceAuth audience matches the target host and that the rkey exists on the PDS.-- Keychain issues on Linux: ensure a Secret Service (e.g., GNOME Keyring or KWallet) is running.-- `com.atproto.server.createSession` against the PDS, save `{accessJwt, refreshJwt, did, handle, pds}` in keyring.-- Unit test decoding with minimal JSON envelopes: record lists, createRecord `uri`, and repo.create (empty body or simple ack).
+184
-7
Cargo.lock
+184
-7
Cargo.lock
·························································
+5
-1
Cargo.toml
+5
-1
Cargo.toml
······
+173
-16
README.md
+173
-16
README.md
···-Status: project scaffold with CLI, config, API and git crates. Commands are stubs pending endpoint wiring.+Tangled CLI is a fully functional tool for managing repositories, issues, pull requests, and CI/CD workflows on the Tangled platform.+- `TANGLED_SPINDLE_BASE` - Override the Spindle base URL (default: `wss://spindle.tangled.sh`)
+872
-7
crates/tangled-api/src/client.rs
+872
-7
crates/tangled-api/src/client.rs
··········································
+4
crates/tangled-api/src/lib.rs
+4
crates/tangled-api/src/lib.rs
+3
crates/tangled-cli/Cargo.toml
+3
crates/tangled-cli/Cargo.toml
···
+43
-44
crates/tangled-cli/src/cli.rs
+43
-44
crates/tangled-cli/src/cli.rs
············
+7
-1
crates/tangled-cli/src/commands/auth.rs
+7
-1
crates/tangled-cli/src/commands/auth.rs
···
+267
-21
crates/tangled-cli/src/commands/issue.rs
+267
-21
crates/tangled-cli/src/commands/issue.rs
······+let mut repo_cache: std::collections::HashMap<String, String> = std::collections::HashMap::new();
+2
-44
crates/tangled-cli/src/commands/knot.rs
+2
-44
crates/tangled-cli/src/commands/knot.rs
···
+228
-19
crates/tangled-cli/src/commands/pr.rs
+228
-19
crates/tangled-cli/src/commands/pr.rs
···use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrMergeArgs, PrReviewArgs, PrShowArgs};···+let parts: Vec<&str> = target_repo.strip_prefix("at://").unwrap_or(target_repo).split('/').collect();
+12
-31
crates/tangled-cli/src/commands/repo.rs
+12
-31
crates/tangled-cli/src/commands/repo.rs
···························
+284
-9
crates/tangled-cli/src/commands/spindle.rs
+284
-9
crates/tangled-cli/src/commands/spindle.rs
·········
+1
crates/tangled-cli/src/main.rs
+1
crates/tangled-cli/src/main.rs
+55
crates/tangled-cli/src/util.rs
+55
crates/tangled-cli/src/util.rs
···
+9
-1
crates/tangled-config/Cargo.toml
+9
-1
crates/tangled-config/Cargo.toml
···
+8
-3
crates/tangled-config/src/config.rs
+8
-3
crates/tangled-config/src/config.rs
······
+2
-2
crates/tangled-config/src/keychain.rs
+2
-2
crates/tangled-config/src/keychain.rs
···
+303
-7
docs/getting-started.md
+303
-7
docs/getting-started.md
···+3. The binary will be available at `target/release/tangled-cli`. Optionally, add it to your PATH or create an alias:+You'll be prompted for your handle (e.g., `alice.bsky.social`) and password. If you're using a custom PDS, specify it with the `--pds` flag:+By default, repositories are created on the default knot (`tngl.sh`). You can specify a different knot:+tangled issue create --repo my-project --title "Add new feature" --body "We should add feature X"-This project is a scaffold of a Tangled CLI in Rust. The commands are present as stubs and will be wired to XRPC endpoints iteratively.+This command must be run from within the repository's working directory, and your working tree must be clean and pushed.+Session credentials are stored securely in your system keyring (GNOME Keyring, KWallet, macOS Keychain, or Windows Credential Manager).