diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5aeb3af..72087dd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,87 +34,42 @@ jobs: - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - - name: Are these versions already on crates.io? - id: check - shell: bash - run: | - set -euo pipefail - version="" - missing="" - for crate_name in agency-proxy-protocol agency-proxy-client agency-proxy; do - package_version=$(cargo metadata --no-deps --format-version 1 \ - | jq -r --arg name "$crate_name" '.packages[] | select(.name == $name) | .version') - if [ -n "$version" ] && [ "$package_version" != "$version" ]; then - echo "workspace package versions disagree: $version and $package_version" >&2 - exit 1 - fi - version=$package_version - published=$(curl -sS -H 'User-Agent: pathscale-ci' \ - "https://crates.io/api/v1/crates/$crate_name/versions" \ - | jq -r --arg v "$version" '.versions[]? | select(.num == $v) | .num' || true) - if [ -n "$published" ]; then - echo "$crate_name $version is already published" - else - missing="$missing $crate_name" - echo "$crate_name $version is new" - fi - done - echo "version=$version" >> "$GITHUB_OUTPUT" - if [ -n "$missing" ]; then - echo "already=false" >> "$GITHUB_OUTPUT" - else - echo "already=true" >> "$GITHUB_OUTPUT" - fi - - - name: Build - if: steps.check.outputs.already == 'false' - run: cargo build --release - - name: Test - if: steps.check.outputs.already == 'false' run: cargo test --release - - name: Package dry run - if: steps.check.outputs.already == 'false' && inputs.dry_run - run: | - cargo package -p agency-proxy-protocol --locked - cargo package -p agency-proxy-client --locked --no-verify - cargo package -p agency-proxy --locked --no-verify + - name: Package + if: inputs.dry_run + run: cargo package --workspace + # Cargo knows which crates are publishable and what order they depend on + # each other in. This used to say both again: a hand-kept list of crate + # names in dependency order, a `cargo metadata` per crate to read one + # field out of the same output, and a jq query against the crates.io API + # per crate. Every line was a second copy of something cargo already + # knew, and the copy is what goes stale when a crate is added. + # + # What cargo does not do is tolerate a version already on the registry: + # it fails the whole run with "already exists on crates.io index". A dry + # run only warns about that, so it is easy to ship this broken - it + # happened in ps-vello. The re-run of a partial release is exactly the + # case worth surviving, so that one error passes and nothing else does. + # + # No `--locked`: publishing one crate makes the next one's dependency on + # it resolvable at the new version, which changes the lockfile, and + # `--locked` would abort the sequence partway with some crates already + # permanently live. - name: Publish - if: steps.check.outputs.already == 'false' && !inputs.dry_run + if: '!inputs.dry_run' + shell: bash env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - VERSION: ${{ steps.check.outputs.version }} run: | - set -euo pipefail - if [ -z "$CARGO_REGISTRY_TOKEN" ]; then - echo "CARGO_REGISTRY_TOKEN is not set on this repository" >&2 - echo "Add it under Settings -> Secrets -> Actions." >&2 - exit 1 + set -o pipefail + if cargo publish --workspace 2>&1 | tee /tmp/publish.log; then + exit 0 fi - for crate_name in agency-proxy-protocol agency-proxy-client agency-proxy; do - published=$(curl -sS -H 'User-Agent: pathscale-ci' \ - "https://crates.io/api/v1/crates/$crate_name/versions" \ - | jq -r --arg v "$VERSION" '.versions[]? | select(.num == $v) | .num' || true) - if [ -n "$published" ]; then - echo "$crate_name $VERSION is already published; skipping" - else - cargo publish -p "$crate_name" --locked --registry crates-io - fi - done - - - name: Summary - if: always() - run: | - { - if [ '${{ steps.check.outputs.already }}' = 'true' ]; then - echo "Nothing to publish: all AgencyProxy crates at \`${{ steps.check.outputs.version }}\` are already on crates.io." - echo - echo 'Bump `version` in Cargo.toml to cut a new release.' - elif [ '${{ inputs.dry_run }}' = 'true' ]; then - echo "Dry run: packaged AgencyProxy \`${{ steps.check.outputs.version }}\` without uploading." - else - echo "Published AgencyProxy \`${{ steps.check.outputs.version }}\`." - fi - } >> "$GITHUB_STEP_SUMMARY" + if grep -q "already exists on crates.io index" /tmp/publish.log; then + echo "::notice::Some crates at this version are already published; nothing left to upload." + exit 0 + fi + exit 1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3c7929f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Working agreement — agencyproxy + +The operating contract for **any** coding agent working in this repository. Codex, Cursor +and Gemini CLI read `AGENTS.md` natively; Claude Code loads it through the `@AGENTS.md` +import in [`CLAUDE.md`](CLAUDE.md). Never fork these rules into a per-vendor file. + +**Rust** crate. + +## Invariants (do not break these) + +- **No Python.** Not a script, not `python3 -c`, not a heredoc. Reaching for it is the + tell that a step is being solved by parsing when the tool that owns the answer could + just be asked. Do not swap it for another parser either, and do not assume `jq` is + present: it does not ship with macOS. A fixed-shape field is one `sed -nE` line; + anything needing real parsing belongs in Rust, where it can be tested. If a task seems + to need Python, the approach is wrong. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7472904 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +@AGENTS.md + +# Claude Code notes — agencyproxy + +The import above is binding: [`AGENTS.md`](AGENTS.md) is the working agreement for this +repository, and every Claude Code session loads it automatically. Do not copy rules here, +one source of truth, no drift. Only genuinely Claude-specific wiring belongs below. diff --git a/Cargo.lock b/Cargo.lock index 96069b6..1274e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agency-proxy" -version = "0.1.9" +version = "0.1.10" dependencies = [ "agency-proxy-client", "agency-proxy-protocol", @@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "agency-proxy-client" -version = "0.1.9" +version = "0.1.10" dependencies = [ "agency-proxy-protocol", "endpoint-libs", @@ -37,7 +37,7 @@ dependencies = [ [[package]] name = "agency-proxy-protocol" -version = "0.1.9" +version = "0.1.10" dependencies = [ "serde", "serde_json", @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "agent-abstraction" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9610a85e179dd8575d24794f13920af19e86718fd5ed0d556367e4aa38c5c5a7" +checksum = "3ddf92d92000d84579f3344b73d62f04a4bc0ff32f90ff4ee69a8ff243f595c4" dependencies = [ "fs2", "libc", diff --git a/Cargo.toml b/Cargo.toml index 3709412..829bb8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/client", "crates/protocol", "crates/proxy"] [workspace.package] -version = "0.1.9" +version = "0.1.10" edition = "2024" license = "Apache-2.0" publish = true diff --git a/crates/proxy/Cargo.toml b/crates/proxy/Cargo.toml index 7c8c84e..bccb9b6 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true repository.workspace = true [dependencies] -agent-abstraction = "0.4.19" +agent-abstraction = "0.4.21" agency-proxy-protocol.workspace = true async-trait.workspace = true clap.workspace = true diff --git a/crates/proxy/src/runtime.rs b/crates/proxy/src/runtime.rs index 1ae4b96..c4ad994 100644 --- a/crates/proxy/src/runtime.rs +++ b/crates/proxy/src/runtime.rs @@ -114,8 +114,8 @@ pub enum RuntimeError { impl RuntimeRegistry { pub async fn account_usage(&self) -> Vec { - futures::future::join_all([Agent::Claude, Agent::Codex, Agent::Copilot].map( - |agent| async move { + futures::future::join_all( + [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { let provider = agent_name(agent).to_string(); if !agent.reports_account_usage() { return agency_proxy_protocol::ProviderAccountUsage { @@ -139,14 +139,14 @@ impl RuntimeRegistry { error: Some(error.to_string()), }, } - }, - )) + }), + ) .await } pub async fn probe_providers(&self) -> Vec { - futures::future::join_all([Agent::Claude, Agent::Codex, Agent::Copilot].map( - |agent| async move { + futures::future::join_all( + [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { let probe = Probe::run(agent).await; let auth = AuthStatus::check(agent).await; let installed = @@ -194,8 +194,8 @@ impl RuntimeRegistry { plan, login_hint, } - }, - )) + }), + ) .await } @@ -601,6 +601,7 @@ fn agent_name(agent: Agent) -> &'static str { Agent::Claude => "claude", Agent::Codex => "codex", Agent::Copilot => "copilot", + Agent::Grok => "grok", } } @@ -609,6 +610,7 @@ fn agent_for_provider(provider: &str) -> Result { "claude" => Ok(Agent::Claude), "codex" => Ok(Agent::Codex), "copilot" => Ok(Agent::Copilot), + "grok" => Ok(Agent::Grok), other => Err(RuntimeError::Provider(other.into())), } }