@anyforge/control 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -4
- package/dist/cli.js +91 -135
- package/package.json +8 -6
- package/dist/commands/applyPatch.js +0 -92
- package/dist/commands/init.js +0 -296
- package/dist/commands/logout.js +0 -34
- package/dist/commands/status.js +0 -58
- package/dist/commands/whoami.js +0 -23
- package/dist/lib/configPatch.js +0 -524
- package/dist/lib/credentials.js +0 -40
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# @anyforge/control
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two things in one binary:
|
|
4
|
+
|
|
5
|
+
1. **A governed LLM proxy** for any agent — Claude Code, Cursor, Cline, Continue, or your own SDK. Route their calls through AnyForge for cost analytics, audit log, and policy enforcement.
|
|
6
|
+
2. **A scriptable AnyForge client** — approvals, crews, work items and workflows from a shell, with no browser and no agent in the loop.
|
|
7
|
+
|
|
8
|
+
The second half is what makes AnyForge usable from CI. Every command speaks the same `/api/v1/control/*` API the MCP server does, but without needing a language model to drive it.
|
|
4
9
|
|
|
5
10
|
## Install
|
|
6
11
|
|
|
@@ -50,12 +55,55 @@ Claude Code recognises both, but they behave differently. `ANTHROPIC_API_KEY` is
|
|
|
50
55
|
|
|
51
56
|
## Commands
|
|
52
57
|
|
|
58
|
+
### Setup
|
|
59
|
+
|
|
53
60
|
| | |
|
|
54
61
|
|---|---|
|
|
55
62
|
| `anyforge init` | Authorise this device and patch local agent configs |
|
|
56
|
-
| `anyforge whoami` | Show the company
|
|
63
|
+
| `anyforge whoami` | Show the company, scopes and endpoint this key acts as (asks the server) |
|
|
57
64
|
| `anyforge status` | Verify the proxy is reachable and your BYOK key is configured |
|
|
58
65
|
| `anyforge logout` | Clear the local credential file **and revert patched configs** (does not revoke server-side) |
|
|
66
|
+
| `anyforge apply-patch` | Apply a unified diff from a file or stdin via `git apply` |
|
|
67
|
+
|
|
68
|
+
### Scriptable
|
|
69
|
+
|
|
70
|
+
| | | Scope |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `anyforge approvals` | List HIL approvals awaiting a decision | `read` |
|
|
73
|
+
| `anyforge approve <id>` | Approve one (`--notes` is recorded in the audit trail) | `approve` |
|
|
74
|
+
| `anyforge reject <id>` | Reject one | `approve` |
|
|
75
|
+
| `anyforge crews` | List crews (`--active` for non-terminal only) | `read` |
|
|
76
|
+
| `anyforge crew <id>` | Show one crew, its status and recent output | `read` |
|
|
77
|
+
| `anyforge halt <id>` / `resume <id>` | Stop or restart a crew | `approve` |
|
|
78
|
+
| `anyforge work` | List work items (`--status`, `--type`, `--priority`, `--search`, …) | `read` |
|
|
79
|
+
| `anyforge workflows` | List operator workflows | `read` |
|
|
80
|
+
| `anyforge run <id>` | Dispatch a run (`--input '{"repo":"acme/web"}'`) | `write` |
|
|
81
|
+
| `anyforge ask <question>` | Ask the Oracle one question | `read` |
|
|
82
|
+
|
|
83
|
+
Every command takes `--json`, which prints the API payload verbatim:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
anyforge approvals --json | jq 'length'
|
|
87
|
+
anyforge crews --active --json | jq -r '.[].id' | xargs -n1 anyforge halt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Exit codes
|
|
91
|
+
|
|
92
|
+
CI gates on these, so they are a contract:
|
|
93
|
+
|
|
94
|
+
| | |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `0` | Success — an empty list included |
|
|
97
|
+
| `1` | Network or unexpected failure (retryable) |
|
|
98
|
+
| `2` | Usage error |
|
|
99
|
+
| `3` | Not authenticated — run `init`, or set `ANYFORGE_API_TOKEN` |
|
|
100
|
+
| `4` | The API refused; its error code prints to stderr |
|
|
101
|
+
|
|
102
|
+
Errors and progress go to stderr, so a `--json` pipe stays parseable under a failure.
|
|
103
|
+
|
|
104
|
+
### Unattended use
|
|
105
|
+
|
|
106
|
+
Set `ANYFORGE_API_TOKEN` (and optionally `ANYFORGE_API_URL`) instead of running `init` — no browser, no credential file. Mint the key under **Settings → API keys** with the narrowest scope the job needs.
|
|
59
107
|
|
|
60
108
|
## Endpoints
|
|
61
109
|
|
|
@@ -63,8 +111,8 @@ The CLI defaults to `https://crew.anyforge.ai`. Override per-invocation with `--
|
|
|
63
111
|
|
|
64
112
|
The proxy exposes:
|
|
65
113
|
|
|
66
|
-
- `POST /v1/anthropic/messages` — drop-in for `https://api.anthropic.com/v1/messages`. Use `Authorization: Bearer sk_anyforge_…` instead of `x-api-key`.
|
|
67
|
-
- `POST /v1/openai/chat/completions` — drop-in for `https://api.openai.com/v1/chat/completions`.
|
|
114
|
+
- `POST /v1/anthropic/v1/messages` — drop-in for `https://api.anthropic.com/v1/messages`. Use `Authorization: Bearer sk_anyforge_…` instead of `x-api-key`. The `/v1` repeats because you set `ANTHROPIC_BASE_URL` to `https://crew.anyforge.ai/v1/anthropic` and the Anthropic SDK appends `/v1/messages` to it.
|
|
115
|
+
- `POST /v1/openai/v1/chat/completions` — drop-in for `https://api.openai.com/v1/chat/completions`.
|
|
68
116
|
|
|
69
117
|
Both validate your AnyForge API key, look up your company's BYOK provider key, forward the request, and stream the response back unchanged. Token counts and cost are logged to your usage dashboard.
|
|
70
118
|
|
package/dist/cli.js
CHANGED
|
@@ -1,141 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* Each subcommand is a separate module under src/commands/. The
|
|
14
|
-
* argv parsing here is deliberately tiny — no commander, no yargs.
|
|
15
|
-
* For three flags and four commands, hand-rolled is shorter than
|
|
16
|
-
* the lib + plugin chain.
|
|
17
|
-
*/
|
|
18
|
-
import { red, yellow, dim, bold } from 'kleur/colors';
|
|
19
|
-
import { runInit } from './commands/init.js';
|
|
20
|
-
import { runWhoami } from './commands/whoami.js';
|
|
21
|
-
import { runLogout } from './commands/logout.js';
|
|
22
|
-
import { runStatus } from './commands/status.js';
|
|
23
|
-
import { runApplyPatch } from './commands/applyPatch.js';
|
|
24
|
-
function parseArgs(argv) {
|
|
25
|
-
const a = { positional: [] };
|
|
26
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
-
const v = argv[i];
|
|
28
|
-
if (!a.command && !v.startsWith('-')) {
|
|
29
|
-
a.command = v;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (v === '--help' || v === '-h')
|
|
33
|
-
a.help = true;
|
|
34
|
-
else if (v === '--version' || v === '-V')
|
|
35
|
-
a.version = true;
|
|
36
|
-
else if (v === '--endpoint' && argv[i + 1]) {
|
|
37
|
-
a.endpoint = argv[i + 1];
|
|
38
|
-
i += 1;
|
|
39
|
-
}
|
|
40
|
-
else if (v === '--3way')
|
|
41
|
-
a.threeWay = true;
|
|
42
|
-
else if (v === '--dry-run')
|
|
43
|
-
a.dryRun = true;
|
|
44
|
-
else if (v === '--force' || v === '-f')
|
|
45
|
-
a.force = true;
|
|
46
|
-
else if (!v.startsWith('-'))
|
|
47
|
-
a.positional.push(v);
|
|
48
|
-
}
|
|
49
|
-
return a;
|
|
50
|
-
}
|
|
51
|
-
function printHelp() {
|
|
52
|
-
// eslint-disable-next-line no-console
|
|
53
|
-
console.log(`
|
|
54
|
-
${bold('AnyForge Control')} — governed LLM proxy for any agent.
|
|
2
|
+
var rt=Object.defineProperty;var it=(e,t)=>()=>(e&&(t=e(e=0)),t);var st=(e,t)=>{for(var o in t)rt(e,o,{get:t[o],enumerable:!0})};var Qe={};st(Qe,{default:()=>Ht});var Ht,Ze=it(()=>{Ht={name:"@anyforge/control",version:"0.3.0",description:"AnyForge Control CLI \u2014 governed LLM proxy for any agent, and a scriptable client for AnyForge approvals, crews, work items and workflows.",license:"Apache-2.0",homepage:"https://anyforge.ai",repository:{type:"git",url:"git+https://github.com/pootonline/sovereign-engineering.git",directory:"packages/control/cli"},publishConfig:{access:"public"},type:"module",main:"dist/cli.js",bin:{anyforge:"dist/cli.js",control:"dist/cli.js"},files:["dist","README.md"],scripts:{build:"esbuild src/cli.ts --bundle --minify --platform=node --format=esm --outfile=dist/cli.js --packages=external && chmod +x dist/cli.js","type-check":"tsc --noEmit",dev:"tsx src/cli.ts",prepublishOnly:"npm run build","publish:beta":"npm publish --access public --tag beta","publish:stable":"npm publish --access public"},engines:{node:">=22"},dependencies:{kleur:"^4.1.5",open:"^10.0.0",prompts:"^2.4.2"},devDependencies:{"@types/node":"^22.20.0","@types/prompts":"^2.4.9",esbuild:"^0.25.0",tsx:"^4.7.0",typescript:"^7.0.2"}}});import{red as et,yellow as Bt,dim as F,bold as P}from"kleur/colors";import{execFileSync as ne}from"child_process";import yt from"open";import oe from"prompts";import{green as $,dim as c,bold as E,yellow as I,cyan as M}from"kleur/colors";import{promises as U}from"fs";import{homedir as at}from"os";import{join as Ee,dirname as no}from"path";var Re=Ee(at(),".anyforge"),Z=Ee(Re,"credentials");async function ve(e){await U.mkdir(Re,{recursive:!0,mode:448}),await U.writeFile(Z,JSON.stringify(e,null,2),{mode:384})}async function b(){try{let e=await U.readFile(Z,"utf8");return JSON.parse(e)}catch(e){if(e.code==="ENOENT")return null;throw e}}async function Ae(){try{return await U.unlink(Z),!0}catch(e){if(e.code==="ENOENT")return!1;throw e}}import{promises as l}from"fs";import{execFileSync as lt}from"child_process";import{homedir as C}from"os";import{join as f}from"path";import D from"prompts";import{dim as w,green as te,yellow as K}from"kleur/colors";var T="# === AnyForge Control \u2014 added by `anyforge init` ===",S="# === end AnyForge Control ===",k=".anyforge-bak",ct=/claude_(max|pro|team|enterprise)/i,dt=["CLAUDE_CODE_ENABLE_TELEMETRY","OTEL_METRICS_EXPORTER","OTEL_EXPORTER_OTLP_PROTOCOL","OTEL_EXPORTER_OTLP_ENDPOINT","OTEL_EXPORTER_OTLP_HEADERS","OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"];async function Te(e){let t=[],o=await ut(e);if(o&&t.push(o),e.scope==="repo"){let n=await pt(e);return n&&t.push(n),t}for(let n of gt){let r=await n.detect();if(!(!r||!(await D({type:"confirm",name:"go",message:`Patch ${n.label} via shell rc (${w(r.path)}) ?`,initial:!0})).go))try{await n.patch(r,e),t.push({label:n.label,path:r.path}),console.log(te(" \u2714")+" "+n.label)}catch(s){console.warn(" \u26A0 Skipped "+n.label+": "+s.message)}}return t}async function ut(e){let t=e.scope==="repo"&&e.repoRoot?{dir:f(e.repoRoot,".claude"),file:"settings.local.json"}:{dir:f(C(),".claude"),file:"settings.json"},o=f(t.dir,t.file);if(!(await D({type:"confirm",name:"go",message:`Patch Claude Code config at ${w(o)} ?`,initial:!0})).go)return null;await l.mkdir(t.dir,{recursive:!0});let r=await l.readFile(o,"utf8").catch(()=>"{}"),i;try{i=JSON.parse(r)}catch{i={}}let s=i.env??{},a=s.ANTHROPIC_BASE_URL;return a&&!a.includes(e.endpoint)&&(a.includes("localhost")||a.includes("127.0.0.1")||a.includes("0.0.0.0"))&&(console.log(),console.log(K(" \u26A0 Local LLM proxy detected: ")+w(a)),console.log(w(" AnyForge only routes Anthropic Claude models. Local models (Ollama,")),console.log(w(" LM Studio, etc.) will not work in Claude Code after this patch.")),console.log(w(" To restore: remove ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN")),console.log(w(` from ${o}`)),console.log(),!(await D({type:"confirm",name:"go",message:"Overwrite existing local LLM proxy config?",initial:!1})).go)?null:(await R(o)&&!await R(o+k)&&await l.copyFile(o,o+k),s.ANTHROPIC_BASE_URL=`${e.endpoint}/v1/anthropic`,s.ANTHROPIC_AUTH_TOKEN=e.apiKey,delete s.ANTHROPIC_API_KEY,i.env=s,await l.writeFile(o,JSON.stringify(i,null,2)+`
|
|
3
|
+
`),console.log(te(" \u2714")+" Claude Code"),{label:e.scope==="repo"?"Claude Code (repo)":"Claude Code (global)",path:o})}async function xe(e){let t=f(C(),".claude"),o=f(t,"settings.json");await l.mkdir(t,{recursive:!0});let n=await l.readFile(o,"utf8").catch(()=>"{}"),r;try{r=JSON.parse(n)}catch{r={}}let i=r.env??{},s=i.OTEL_EXPORTER_OTLP_ENDPOINT;return s&&!ee(s,_e(e.endpoint))?(console.log(K(" \u26A0 An OpenTelemetry endpoint is already configured: ")+w(s)),console.log(w(" Leaving it untouched. To send Claude Code usage to AnyForge, point")),console.log(w(" OTEL_EXPORTER_OTLP_ENDPOINT at "+e.endpoint+"/api/otel")),null):(await R(o)&&!await R(o+k)&&await l.copyFile(o,o+k),i.CLAUDE_CODE_ENABLE_TELEMETRY="1",i.OTEL_METRICS_EXPORTER="otlp",i.OTEL_EXPORTER_OTLP_PROTOCOL="http/json",i.OTEL_EXPORTER_OTLP_ENDPOINT=`${e.endpoint}/api/otel`,i.OTEL_EXPORTER_OTLP_HEADERS=`Authorization=Bearer ${e.apiKey}`,i.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta",r.env=i,await l.writeFile(o,JSON.stringify(r,null,2)+`
|
|
4
|
+
`),{label:"Claude Code usage telemetry (global)",path:o})}async function pt(e){if(!e.repoRoot)return null;let t=f(e.repoRoot,".env");if(!(await D({type:"confirm",name:"go",message:`Write AnyForge env block to ${w(t)} ?`,initial:!0})).go)return null;let n=[T,`ANYFORGE_API_KEY=${e.apiKey}`,`ANTHROPIC_BASE_URL=${e.endpoint}/v1/anthropic`,`ANTHROPIC_AUTH_TOKEN=${e.apiKey}`,`OPENAI_BASE_URL=${e.endpoint}/v1/openai`,`OPENAI_API_KEY=${e.apiKey}`,S,""].join(`
|
|
5
|
+
`),r="";try{r=await l.readFile(t,"utf8")}catch{}let i;return r.includes(T)?i=r.replace(new RegExp(`${T}[\\s\\S]*?${S}\\n?`),n):r.length===0?i=n:i=r.endsWith(`
|
|
6
|
+
`)?`${r}${n}`:`${r}
|
|
7
|
+
${n}`,i!==r&&await l.writeFile(t,i),await ft(e.repoRoot),console.log(te(" \u2714")+" .env"),{label:".env (repo)",path:t}}async function ft(e){let t=f(e,".gitignore");try{(await l.readFile(t,"utf8")).split(`
|
|
8
|
+
`).map(i=>i.trim()).some(i=>i===".env"||i==="/.env"||i==="*.env")||console.log(K(" \u26A0 .env is not in .gitignore \u2014 add it before committing."))}catch{console.log(K(" \u26A0 No .gitignore found \u2014 make sure .env is excluded before committing."))}}var gt=[{label:"Cursor (shell hint)",async detect(){let e=f(C(),".cursor");return await R(e)?{path:".cursor (shell hint)"}:null},async patch(e,t){await Ce(t)}},{label:"Cline / Continue (shell hint)",async detect(){let e=await R(f(C(),".cline")),t=await R(f(C(),".continue"));return e||t?{path:"~/.cline or ~/.continue (shell hint)"}:null},async patch(e,t){await Ce(t)}}];async function Ce(e){let t=[T,`export ANYFORGE_API_KEY="${e.apiKey}"`,`export ANTHROPIC_BASE_URL="${e.endpoint}/v1/anthropic"`,`export ANTHROPIC_AUTH_TOKEN="${e.apiKey}"`,`export OPENAI_BASE_URL="${e.endpoint}/v1/openai"`,`export OPENAI_API_KEY="${e.apiKey}"`,"unset ANTHROPIC_API_KEY 2>/dev/null || true",S,""].join(`
|
|
9
|
+
`);for(let o of[".zshrc",".bashrc"]){let n=f(C(),o);if(!await R(n))continue;let r=await l.readFile(n,"utf8"),i;r.includes(T)?i=r.replace(new RegExp(`${T}[\\s\\S]*?${S}\\n?`),t):i=r.endsWith(`
|
|
10
|
+
`)?`${r}
|
|
11
|
+
${t}`:`${r}
|
|
55
12
|
|
|
56
|
-
${
|
|
13
|
+
${t}`,i!==r&&await l.writeFile(n,i)}}async function R(e){try{return await l.access(e),!0}catch{return!1}}async function ke(){try{let e=await l.readFile(f(C(),".claude.json"),"utf8"),o=JSON.parse(e).oauthAccount,n=o?.organizationType??"";if(o&&ct.test(n))return{isSubscription:!0,plan:/max/i.test(n)?"Max":/pro/i.test(n)?"Pro":/enterprise/i.test(n)?"Enterprise":/team/i.test(n)?"Team":n,email:o.emailAddress??null}}catch{}return{isSubscription:!1,plan:null,email:null}}async function Ie(e={}){let t=e.endpoint?_e(e.endpoint):null,o=[],n=f(C(),".claude","settings.json");await Oe(n,t)&&o.push(n);for(let i of[".zshrc",".bashrc"]){let s=f(C(),i);await Pe(s)&&o.push(s)}let r=e.repoRoot??mt(process.cwd());if(r){let i=f(r,".claude","settings.local.json");await Oe(i,t)&&o.push(i);let s=f(r,".env");await Pe(s)&&o.push(s)}return{reverted:o}}function ee(e,t){return e?e.includes("anyforge")?!0:t?e.includes(t):!1:!1}async function Oe(e,t){if(!await R(e))return!1;if(await R(e+k))return await l.copyFile(e+k,e),await l.unlink(e+k).catch(()=>{}),!0;let o;try{o=JSON.parse(await l.readFile(e,"utf8"))}catch{return!1}let n=o.env;if(!n)return!1;let r=!1,i=[["ANTHROPIC_BASE_URL","ANTHROPIC_AUTH_TOKEN"],["OPENAI_BASE_URL","OPENAI_API_KEY"]];for(let[s,a]of i)ee(n[s],t)&&(delete n[s],delete n[a],r=!0);if(n.ANYFORGE_API_KEY&&(delete n.ANYFORGE_API_KEY,r=!0),ee(n.OTEL_EXPORTER_OTLP_ENDPOINT,t))for(let s of dt)s in n&&(delete n[s],r=!0);return r?(Object.keys(n).length===0&&delete o.env,await l.writeFile(e,JSON.stringify(o,null,2)+`
|
|
14
|
+
`),!0):!1}async function Pe(e){let t;try{t=await l.readFile(e,"utf8")}catch{return!1}if(!t.includes(T))return!1;let o=t.replace(new RegExp(`\\n?${be(T)}[\\s\\S]*?${be(S)}\\n?`),`
|
|
15
|
+
`).replace(/\n{3,}/g,`
|
|
16
|
+
|
|
17
|
+
`);return await l.writeFile(e,o),!0}function be(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function _e(e){try{return new URL(e).host}catch{return null}}function mt(e){try{return lt("git",["rev-parse","--show-toplevel"],{cwd:e,encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return null}}var ht=2e3,wt=900*1e3;async function $e(e){let{endpoint:t}=e;console.log(E("AnyForge Control \u2014 device authorisation")),console.log(c(` endpoint: ${t}`));let o=process.cwd(),n=Se(o),r=n?Ct(n):null,s=await(await fetch(`${t}/api/cli/init`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({repo:r})})).json();if(!s.ok||!s.data)throw new Error(`Could not start the authorisation flow: ${s.error??"unknown error"}`);let{code:a,authUrl:m,pollUrl:me}=s.data;console.log(),console.log("Open this URL in your browser to authorise:"),console.log(` ${M(m)}`),console.log(c(` Code: ${a}`)),console.log();try{await yt(m)}catch{}let Q=await Et({pollUrl:me,endpoint:t});await ve(Q),console.log($("\u2714")+" API key saved to "+c("~/.anyforge/credentials"));let j=await ke();if(j.isSubscription&&!e.force){await Rt(j,t,Q.apiKey);return}let nt=process.cwd(),ye=Se(nt),he=await vt(ye),we=await Te({endpoint:t,apiKey:Q.apiKey,scope:he,repoRoot:ye});we.length===0?(console.log(I("No agent configs were patched.")),console.log(c(" Set ANTHROPIC_BASE_URL="+t+"/v1/anthropic and")),console.log(c(" ANTHROPIC_AUTH_TOKEN=<your AnyForge key> manually if needed."))):At(we),console.log(),j.isSubscription&&e.force&&(console.log(I(` \u26A0 --force: your Claude ${j.plan??""} subscription login is now shadowed`)),console.log(I(" by the AnyForge token \u2014 Claude Code will use the BYOK proxy, which")),console.log(I(" needs an Anthropic API key on your company. Run ")+E("anyforge logout")),console.log(I(" to revert and restore your subscription."))),he==="repo"&&(console.log(c(" Re-run this command in another repo to patch it too,")),console.log(c(' or run from any directory and choose "global" to install system-wide.'))),console.log(E("Done.")+" Visit "+M(`${t}/usage`)+" to see your traffic.")}async function Et(e){let t=Date.now();process.stdout.write("Waiting for browser authorisation\u2026 ");let o=0;for(;Date.now()-t<wt;){await new Promise(i=>setTimeout(i,ht)),process.stdout.write("."),o+=1,o>40&&(process.stdout.write(`
|
|
18
|
+
`),o=0);let r=await(await fetch(e.pollUrl)).json();if(r.status==="authorized"&&r.data)return process.stdout.write(" "+$("done")+`
|
|
19
|
+
`),{apiKey:r.data.apiKey,companyId:r.data.companyId,endpoint:e.endpoint,savedAt:new Date().toISOString()};if(r.status==="expired"||r.status==="not_found")throw process.stdout.write(`
|
|
20
|
+
`),new Error(`Authorisation ${r.status}. Re-run \`anyforge init\`.`)}throw new Error("Authorisation timed out (15 min). Re-run `anyforge init`.")}async function Rt(e,t,o){let n=e.email?` (${e.email})`:"";console.log(),console.log(I(` \u26A0 Claude ${e.plan??""} subscription login detected${n}.`)),console.log(),console.log(" AnyForge Control\u2019s inference proxy is "+E("BYOK-only")+" \u2014 it routes"),console.log(" Claude Code through your own Anthropic API key for metering and"),console.log(" governance. Pointing Claude Code at the proxy would "+E("override and")),console.log(" "+E("disable your subscription login")+", so we did NOT patch your config."),console.log(),console.log(" Instead, use your subscription for inference and add the "+E("AnyForge")),console.log(" "+E("MCP server")+" for control (HIL approvals, work items, crews):"),console.log();let r=`claude mcp add anyforge --env ANYFORGE_API_KEY=${o} -- npx -y @anyforge/mcp-server`;if((await oe({type:"confirm",name:"go",message:"Add the AnyForge MCP server to Claude Code now?",initial:!0})).go)try{ne("claude",["mcp","add","anyforge","--env",`ANYFORGE_API_KEY=${o}`,"--","npx","-y","@anyforge/mcp-server"],{stdio:"inherit"}),console.log($(" \u2714")+" AnyForge MCP server added to Claude Code.")}catch{console.log(I(" Could not run `claude mcp add` automatically (is the `claude` CLI on PATH?).")),console.log(c(" Add it manually:")),console.log(" "+r)}else console.log(c(" Add it later with:")),console.log(" "+r);if(console.log(),(await oe({type:"confirm",name:"go",message:"Send Claude Code usage to your AnyForge dashboard? (token counts only \u2014 no prompt content)",initial:!0})).go){let a=await xe({endpoint:t,apiKey:o});a&&(console.log($(" \u2714")+" Usage telemetry enabled \u2014 view it at "+M(`${t}/usage`)),console.log(c(" (written to "+a.path+"; `anyforge logout` removes it)")))}else console.log(c(" Skipped \u2014 your subscription usage stays local to Claude Code."));console.log(),console.log(c(" Want the metered BYOK proxy instead? Re-run with ")+E("--force")+c(" using an")),console.log(c(" Anthropic API key on your company (this disables your subscription in")),console.log(c(" Claude Code; ")+E("anyforge logout")+c(" reverts it).")),console.log(),console.log(E("Done.")+" Visit "+M(`${t}/usage`)+" to see your traffic.")}function Se(e){try{return ne("git",["rev-parse","--show-toplevel"],{cwd:e,encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return null}}async function vt(e){return e?(await oe({type:"select",name:"scope",message:"Where should AnyForge patch Claude Code?",choices:[{title:`This repo only (${e}/.claude/settings.local.json + .env)`,value:"repo"},{title:"Globally (~/.claude/settings.json)",value:"global"}],initial:0})).scope??"repo":(console.log(c(" Not a git repo root \u2014 installing globally.")),console.log(c(" Run this from a repo root if you want a per-repo install instead.")),"global")}function At(e){console.log($("\u2714")+" Patched "+e.length+" agent config"+(e.length===1?"":"s")+":");for(let t of e)console.log(` ${c("\u2022")} ${t.label} ${c("("+t.path+")")}`)}function Ct(e){try{let t=ne("git",["remote","get-url","origin"],{cwd:e,encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim(),o=t.match(/^git@[\w.-]+:([\w.-]+\/[\w.-]+?)(?:\.git)?$/);if(o)return o[1];let n=t.match(/^https?:\/\/[\w.-]+\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);return n?n[1]:null}catch{return null}}import{dim as ae,yellow as Tt,bold as xt}from"kleur/colors";var O={OK:0,FAILURE:1,USAGE:2,NO_CREDENTIALS:3,API_ERROR:4},v=class extends Error{exitCode;hint;constructor(t,o=O.FAILURE,n){super(t),this.name="CliError",this.exitCode=o,this.hint=n}},d=class extends v{constructor(t,o){super(t,O.USAGE,o),this.name="UsageError"}},G=class extends v{constructor(){super("Not authenticated \u2014 no ANYFORGE_API_TOKEN and no saved credential.",O.NO_CREDENTIALS,"Run `npx @anyforge/control init`, or set ANYFORGE_API_TOKEN for unattended use."),this.name="NotAuthenticatedError"}},W=class extends v{code;status;constructor(t,o,n){super(n?`${t}: ${n}`:t,O.API_ERROR,Ot(t,o)),this.name="ControlApiError",this.code=t,this.status=o}};function Ot(e,t){if(e==="missing_api_key"||e==="invalid_api_key"||e==="invalid_token"||t===401)return"The key was rejected. Re-run `npx @anyforge/control init`.";if(e==="insufficient_scope"||t===403)return"This key lacks the scope for that command. Run `anyforge whoami` to see its scopes.";if(t===404)return"No such id in this company. Check it with the matching list command.";if(t===429)return"Rate limited. Retry after a short pause."}var Ne="https://crew.anyforge.ai";async function Y(e){let t=process.env.ANYFORGE_API_TOKEN,o=e||process.env.ANYFORGE_API_URL;if(t)return{apiKey:t,endpoint:o||Ne};let n=await b();if(n?.apiKey)return{apiKey:n.apiKey,endpoint:o||n.endpoint||Ne};throw new G}async function u(e,t,o,n){let r=`${e.endpoint.replace(/\/$/,"")}/api/v1/control${o}`,i;try{i=await fetch(r,{method:t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`},...n!==void 0?{body:JSON.stringify(n)}:{}})}catch(a){throw new v(`Cannot reach ${e.endpoint}: ${a instanceof Error?a.message:String(a)}`,O.FAILURE,"Check your network, or pass --endpoint if this workspace is self-hosted.")}let s;try{s=await i.json()}catch{throw new v(`Non-JSON response from ${r} (HTTP ${i.status}).`,O.FAILURE)}if(!i.ok||s.ok===!1){let a=typeof s.error=="string"?s.error:`http_${i.status}`,m=typeof s.message=="string"?s.message:typeof s.detail=="string"?s.detail:void 0;throw new W(a,i.status,m)}return s.data}function _(e){let t=Object.entries(e).filter(([,o])=>o!==void 0&&o!=="").map(([o,n])=>`${encodeURIComponent(o)}=${encodeURIComponent(String(n))}`);return t.length?`?${t.join("&")}`:""}async function re(e){return u(e,"GET","/me")}function ie(e,t,o,n){let r=e?.entityLabels?.[t]?.[o];return typeof r=="string"&&r.trim()?r:n}async function Le(e){try{return await re(e)}catch{return null}}function Fe(e){return[...e].length}function se(e,t){if(t<=0)return"";let o=[...e];return o.length<=t?e:t===1?"\u2026":`${o.slice(0,t-1).join("")}\u2026`}function x(e,t){if(e.length===0)return"";let o=e.map(i=>t.map(s=>{let a=s.value(i)??"";return s.max?se(a,s.max):a})),n=t.map((i,s)=>Math.max(Fe(i.header),...o.map(a=>Fe(a[s])))),r=i=>i.map((s,a)=>a===i.length-1?s:s.padEnd(n[a]+2)).join("").replace(/\s+$/,"");return[r(t.map(i=>i.header)),...o.map(r)].join(`
|
|
21
|
+
`)}function p(e){return JSON.stringify(e,null,2)}function H(e,t=Date.now()){let o=Pt(e);if(o===null)return"\u2014";let n=Math.max(0,Math.floor((t-o)/1e3));if(n<60)return`${n}s`;let r=Math.floor(n/60);if(r<60)return`${r}m`;let i=Math.floor(r/60);return i<48?`${i}h`:`${Math.floor(i/24)}d`}function Pt(e){if(typeof e=="number")return Number.isFinite(e)?e:null;if(typeof e=="string"){let t=Date.parse(e);return Number.isNaN(t)?null:t}return null}function g(e,t,o="\u2014"){let n=e[t];return typeof n=="string"&&n.trim()?n:typeof n=="number"&&Number.isFinite(n)?String(n):o}function y(e,t,o="\u2014"){for(let n of t){let r=e[n];if(typeof r=="string"&&r.trim())return r}return o}function A(e){let t=Array.isArray(e)?e:e&&typeof e=="object"?bt(e):null;return t?t.filter(o=>!!o&&typeof o=="object"&&!Array.isArray(o)):[]}function bt(e){for(let t of["items","results","rows","data"])if(Array.isArray(e[t]))return e[t];return null}async function je(e){let t=await Y(e.endpoint),o=await re(t);if(e.json){console.log(p(o));return}console.log(`Company: ${xt(o.companyName??o.companyId)}${o.companyName?ae(` (${o.companyId})`):""}`),console.log(`Endpoint: ${t.endpoint}`),console.log(`Scopes: ${o.scopes.length?o.scopes.join(", "):Tt("none")}`),o.userId&&console.log(`User: ${o.userId}`);let n=await b();console.log(n?ae(`Key tail: \u2026${n.apiKey.slice(-4)} \xB7 saved ${new Date(n.savedAt).toLocaleString()}`):ae("Key from: ANYFORGE_API_TOKEN (no saved credential file)"))}import{green as Ue,dim as B,yellow as kt}from"kleur/colors";async function De(){let e=await b(),t=await Ae(),{reverted:o}=await Ie({endpoint:e?.endpoint??null});if(console.log(t?Ue("\u2714")+" Local credentials cleared.":kt("No credentials were saved on this machine.")),o.length){console.log(Ue("\u2714")+" Reverted patched configs:");for(let n of o)console.log(` ${B("\u2022")} ${n}`);console.log(B(" Your previous Claude Code login (if any) is restored."))}else console.log(B(" No AnyForge config patches found to revert."));console.log(B(" Revoke the key server-side at /settings \u2192 API keys."))}import{green as It,red as J,yellow as _t,dim as X}from"kleur/colors";async function Ke(e){let t=await b();if(!t){console.log(_t("Not logged in.")+" Run "+X("npx @anyforge/control init")+" first.");return}let o=e.endpoint||t.endpoint;console.log(`Endpoint: ${o}`),console.log(`Company: ${t.companyId}`);let n={model:"claude-3-5-haiku-20241022",max_tokens:1,messages:[{role:"user",content:"Hi"}]},r;try{r=await fetch(`${o}/v1/anthropic/v1/messages`,{method:"POST",headers:{"Content-Type":"application/json","anthropic-version":"2023-06-01",Authorization:`Bearer ${t.apiKey}`},body:JSON.stringify(n)})}catch(i){console.log(J("\u2717 Proxy unreachable: ")+i.message),process.exit(1)}r.status===401&&(console.log(J("\u2717 API key rejected.")+" Re-run "+X("npx @anyforge/control init")+"."),process.exit(1)),r.status===502&&(console.log(J("\u2717 No BYOK Anthropic key configured for your company.")),console.log(X(" Add one at ")+o+X("/settings \u2192 API keys.")),process.exit(1)),r.ok||(console.log(J(`\u2717 Proxy returned ${r.status}: `)+(await r.text()).slice(0,200)),process.exit(1)),console.log(It("\u2714 Proxy reachable, key valid, BYOK configured."))}import{spawn as St}from"node:child_process";import{readFile as $t}from"node:fs/promises";import{red as N,green as le,yellow as Nt,dim as L,bold as Me}from"kleur/colors";async function Ge(e){let t=await Lt(e.patchFile);t.trim()||(process.stderr.write(N(`Empty patch \u2014 nothing to apply.
|
|
22
|
+
`)),process.exit(1));let o=Ft(t);process.stderr.write(L(`Patch: ${o.files} ${o.files===1?"file":"files"}, `)+le(`+${o.added}`)+" "+N(`-${o.removed}`)+`
|
|
23
|
+
`);let n=await ce(["apply","--check"],t);if(n.code!==0){if(e.threeWay){let i=await ce(["apply","--3way"],t);i.code!==0&&(process.stderr.write(N(`3-way apply failed:
|
|
24
|
+
`)+i.stderr+`
|
|
25
|
+
`),process.exit(1)),process.stderr.write(Nt(`\u2713 Applied via 3-way merge \u2014 review conflicts before committing.
|
|
26
|
+
`));return}process.stderr.write(N(`Patch does not apply cleanly:
|
|
27
|
+
`)+n.stderr+`
|
|
28
|
+
`),process.stderr.write(L("Try ")+Me("--3way")+L(` to attempt a 3-way merge against the index.
|
|
29
|
+
`)),process.exit(1)}if(e.dryRun){process.stderr.write(le(`\u2713 Patch would apply cleanly.
|
|
30
|
+
`));return}let r=await ce(["apply"],t);r.code!==0&&(process.stderr.write(N(`git apply failed:
|
|
31
|
+
`)+r.stderr+`
|
|
32
|
+
`),process.exit(1)),process.stderr.write(le(`\u2713 Applied.
|
|
33
|
+
`)),o.files>0&&process.stderr.write(L(" Run ")+Me("git diff --stat")+L(` to review.
|
|
34
|
+
`))}async function Lt(e){if(e)return await $t(e,"utf8");let t=[];for await(let o of process.stdin)t.push(o);return Buffer.concat(t).toString("utf8")}function ce(e,t){return new Promise((o,n)=>{let r=St("git",e,{stdio:["pipe","pipe","pipe"]}),i="",s="";r.stdout.on("data",a=>{i+=a.toString()}),r.stderr.on("data",a=>{s+=a.toString()}),r.on("error",n),r.on("close",a=>o({code:a??0,stdout:i,stderr:s})),r.stdin.write(t),r.stdin.end()})}function Ft(e){let t=0,o=0,n=0;for(let r of e.split(`
|
|
35
|
+
`))if(r.startsWith("diff --git"))t+=1;else{if(r.startsWith("+++")||r.startsWith("---"))continue;r.startsWith("+")?o+=1:r.startsWith("-")&&(n+=1)}return t===0&&(o>0||n>0)&&(t=(e.match(/^\+\+\+ /gm)??[]).length),{files:t,added:o,removed:n}}import{bold as jt,dim as de,green as Ut,red as Dt}from"kleur/colors";async function We(e){let t=await u(e.creds,"GET",`/hil${_({limit:e.limit})}`);if(e.json){console.log(p(t));return}let o=A(t);if(o.length===0){console.log(de("No approvals pending."));return}console.log(x(o,[{header:"ID",value:n=>g(n,"id")},{header:"CREW",value:n=>g(n,"crewId"),max:24},{header:"AGE",value:n=>H(n.requestedAt)},{header:"SUMMARY",value:n=>y(n,["planSummary","summary","title","description"]),max:60}])),console.log(de(`
|
|
36
|
+
${o.length} pending \xB7 approve with `)+jt("anyforge approve <id>"))}async function ue(e){if(!e.approvalId)throw new d(`${e.decision==="APPROVED"?"approve":"reject"} requires an approval id.`,"List them with `anyforge approvals`.");let t=await u(e.creds,"POST",`/hil/${encodeURIComponent(e.approvalId)}`,{decision:e.decision,...e.notes?{notes:e.notes}:{}});if(e.json){console.log(p(t));return}let o=e.decision==="APPROVED"?Ut("Approved"):Dt("Rejected");console.log(`${o} ${e.approvalId}`);let n=t&&typeof t=="object"?t.signature:void 0;typeof n=="string"&&n&&console.log(de(`Audit signature: ${n}`))}import{bold as Kt,dim as pe}from"kleur/colors";async function Ye(e){let t=await u(e.creds,"GET",`/crews${_({status:e.status,activeOnly:e.activeOnly,limit:e.limit})}`);if(e.json){console.log(p(t));return}let o=A(t);if(o.length===0){console.log(pe(e.activeOnly?"No active crews.":"No crews."));return}console.log(x(o,[{header:"ID",value:n=>g(n,"id")},{header:"STATUS",value:n=>g(n,"status")},{header:"AGE",value:n=>H(n.createdAt??n.startedAt)},{header:"NAME",value:n=>y(n,["name","title","taskTitle","workItemTitle"]),max:52}]))}async function He(e){if(!e.crewId)throw new d("crew requires a crew id.","List them with `anyforge crews`.");let t=await u(e.creds,"GET",`/crews/${encodeURIComponent(e.crewId)}${_({messages:e.messages})}`);if(e.json){console.log(p(t));return}let o=t?.crew&&typeof t.crew=="object"?t.crew:t;console.log(`${Kt(y(o,["name","title","taskTitle"],g(o,"id")))}`),console.log(`Status: ${g(o,"status")}`),console.log(`Repo: ${y(o,["targetGithubRepo","repoFullName","repo"])}`);let n=g(o,"url","");n&&console.log(`Console: ${n}`);let r=A(t?.messages);if(r.length>0){console.log(pe(`
|
|
37
|
+
Last ${r.length} messages:`));for(let i of r){let s=y(i,["role","author","agent","kind"],"?"),a=y(i,["content","body","text","message"],"");console.log(`${pe(`[${s}]`)} ${se(a.replace(/\s+/g," "),160)}`)}}}async function fe(e){if(!e.crewId)throw new d(`${e.action} requires a crew id.`,"List them with `anyforge crews`.");let t=await u(e.creds,"POST",`/crews/${encodeURIComponent(e.crewId)}/${e.action}`);if(e.json){console.log(p(t));return}console.log(`${e.action==="halt"?"Halted":"Resumed"} ${e.crewId}`)}import{dim as q}from"kleur/colors";var Je=100;function Be(e){let t=e&&typeof e=="object"?e:{};return{total:typeof t.total=="number"?t.total:null,hasMore:t.hasMore===!0,cursor:typeof t.nextCursor=="string"?t.nextCursor:null}}async function Mt(e){let t=s=>`/work${_({status:e.status,type:e.type,priority:e.priority,initiative:e.initiative,program:e.program,search:e.search,assignee:e.assignee,includeArchived:e.includeArchived,limit:e.limit,cursor:s})}`,o=await u(e.creds,"GET",t()),n=A(o),r=Be(o),i=1;if(!e.all)return{rows:n,total:r.total,hasMore:r.hasMore,raw:o,pagesFetched:i};for(;r.hasMore&&r.cursor&&i<Je;){let s=await u(e.creds,"GET",t(r.cursor));n.push(...A(s)),r=Be(s),i+=1}return{rows:n,total:r.total,hasMore:r.hasMore,raw:o,pagesFetched:i}}async function Xe(e){let t=await Mt(e);if(e.json){console.log(p(e.all?{items:t.rows,total:t.total,hasMore:t.hasMore,nextCursor:null}:t.raw));return}let o=t.rows,n=await Le(e.creds),r=ie(n,"work_item","plural","Work items");if(o.length===0){console.log(q(`No ${r.toLowerCase()} match that filter.`));return}console.log(x(o,[{header:"ID",value:m=>g(m,"id")},{header:"PRI",value:m=>g(m,"priority","")},{header:"TYPE",value:m=>g(m,"type","")},{header:"STATUS",value:m=>g(m,"status")},{header:"TITLE",value:m=>y(m,["title","name","summary"]),max:58}]));let i=(o.length===1?ie(n,"work_item","singular","work item"):r).toLowerCase(),s=t.total!==null&&t.total!==o.length?`${o.length} of ${t.total} ${r.toLowerCase()}`:`${o.length} ${i}`,a=t.hasMore?e.all?q(` \u2014 stopped after ${Je} pages; narrow the filter`):q(" \u2014 pass --all for the rest"):"";console.log(`${q(`
|
|
38
|
+
${s}`)}${a}`)}import{bold as Gt,dim as V}from"kleur/colors";async function qe(e){let t=await u(e.creds,"GET","/workflows");if(e.json){console.log(p(t));return}let o=A(t);if(o.length===0){console.log(V("No workflows defined."));return}console.log(x(o,[{header:"ID",value:n=>g(n,"id")},{header:"STATUS",value:n=>y(n,["status","state"],"")},{header:"NAME",value:n=>y(n,["name","title"]),max:58}])),console.log(V(`
|
|
39
|
+
Dispatch with `)+Gt("anyforge run <id>"))}async function Ve(e){if(!e.workflowId)throw new d("run requires a workflow id.","List them with `anyforge workflows`.");let t;if(e.input!==void 0){try{t=JSON.parse(e.input)}catch(r){throw new d(`--input is not valid JSON: ${r instanceof Error?r.message:String(r)}`,`Example: --input '{"repo":"acme/web"}'`)}if(!t||typeof t!="object"||Array.isArray(t))throw new d("--input must be a JSON object.",`Example: --input '{"repo":"acme/web"}'`)}let o=await u(e.creds,"POST",`/workflows/${encodeURIComponent(e.workflowId)}/run`,t===void 0?void 0:{inputs:t});if(e.json){console.log(p(o));return}let n=y(o??{},["id","runId"],"");console.log(`Queued run ${n||V("(id not returned)")}`),console.log(V("Runs pause at gate steps for an operator decision."))}import{dim as Wt}from"kleur/colors";async function ze(e){let t=e.question?.trim();if(!t)throw new d("ask requires a question.",'Example: anyforge ask "which initiatives slipped this cycle?"');!e.json&&process.stderr.isTTY&&process.stderr.write(Wt(`Asking the Oracle (this can take a few minutes)\u2026
|
|
40
|
+
`));let o=await u(e.creds,"POST","/oracle",{question:t,...e.context?{context:e.context}:{}});if(e.json){console.log(p(o));return}let n=o&&typeof o=="object"?["answer","text","content","response"].map(r=>o[r]).find(r=>typeof r=="string"&&r.trim()):void 0;console.log(typeof n=="string"?n:p(o))}var ot="https://crew.anyforge.ai",Jt=new Set(["endpoint","limit","status","type","priority","initiative","program","search","assignee","notes","input","context","messages"]),Xt=new Set(["help","version","json","active","archived","3way","dry-run","force"]);function qt(e){let t={positional:[],flags:{}};for(let o=0;o<e.length;o+=1){let n=e[o];if(!n.startsWith("-")){t.command?t.positional.push(n):t.command=n;continue}let r=n==="-h"?"help":n==="-V"?"version":n==="-f"?"force":n.replace(/^--?/,""),[i,s]=r.includes("=")?[r.slice(0,r.indexOf("=")),r.slice(r.indexOf("=")+1)]:[r,void 0];if(Jt.has(i)){let a=s??e[++o];if(a===void 0)throw new d(`--${i} needs a value.`);t.flags[i]=a;continue}if(Xt.has(i)){t.flags[i]=s===void 0?!0:s!=="false";continue}throw new d(`Unknown option: ${n}`,"Run `anyforge --help` for the command list.")}return t}function z(e,t){if(e===void 0||typeof e=="boolean")return;let o=Number(e);if(!Number.isFinite(o)||o<=0)throw new d(`--${t} must be a positive number, got "${e}".`);return o}function h(e){return typeof e=="string"?e:void 0}function ge(e){return e===!0?!0:void 0}function Vt(){console.log(`
|
|
41
|
+
${P("AnyForge Control")} \u2014 governed LLM proxy, and a scriptable AnyForge client.
|
|
42
|
+
|
|
43
|
+
${P("USAGE")}
|
|
57
44
|
npx @anyforge/control <command> [options]
|
|
58
45
|
|
|
59
|
-
${
|
|
60
|
-
init
|
|
61
|
-
whoami
|
|
62
|
-
status
|
|
63
|
-
logout
|
|
64
|
-
apply-patch
|
|
46
|
+
${P("SETUP")}
|
|
47
|
+
init Authorise this device and patch local agent configs
|
|
48
|
+
whoami Show the company, scopes and endpoint this key acts as
|
|
49
|
+
status Check connectivity to the AnyForge proxy
|
|
50
|
+
logout Clear the saved API key and revert patched configs
|
|
51
|
+
apply-patch [file] Apply a unified diff (file or stdin) via git apply
|
|
52
|
+
|
|
53
|
+
${P("APPROVALS")} ${F("(the gate that blocks a human)")}
|
|
54
|
+
approvals List HIL approvals awaiting a decision
|
|
55
|
+
approve <id> Approve one, optionally with --notes
|
|
56
|
+
reject <id> Reject one, optionally with --notes
|
|
57
|
+
|
|
58
|
+
${P("CREWS")}
|
|
59
|
+
crews List crews (--active for running/paused only)
|
|
60
|
+
crew <id> Show one crew, its status and recent output
|
|
61
|
+
halt <id> Stop a crew
|
|
62
|
+
resume <id> Resume a paused or halted crew
|
|
63
|
+
|
|
64
|
+
${P("WORK & WORKFLOWS")}
|
|
65
|
+
work List work items (--status, --type, --priority, --search).
|
|
66
|
+
Paged: one page by default, --all walks every page
|
|
67
|
+
workflows List operator workflows
|
|
68
|
+
run <id> Dispatch a workflow run, with optional --input JSON
|
|
69
|
+
ask <question> Ask the Oracle one question
|
|
70
|
+
|
|
71
|
+
${P("OPTIONS")}
|
|
72
|
+
--json Print the raw API payload instead of a table
|
|
73
|
+
--endpoint <url> Override the base URL (default: ${F(ot)})
|
|
74
|
+
--limit <n> Page size on list commands (work: 1-200, default 50)
|
|
75
|
+
--all work: follow the cursor until the backlog is exhausted
|
|
76
|
+
--notes <text> approve/reject: reviewer notes, recorded in the audit trail
|
|
77
|
+
--input <json> run: workflow inputs, e.g. '{"repo":"acme/web"}'
|
|
78
|
+
--context <text> ask: background for the Oracle
|
|
79
|
+
--help, -h Show this message
|
|
80
|
+
--version, -V Print CLI version
|
|
65
81
|
|
|
66
|
-
${
|
|
67
|
-
|
|
68
|
-
--force, -f init: patch the BYOK proxy even if a Claude subscription
|
|
69
|
-
login is present (this disables that subscription in Claude Code)
|
|
70
|
-
--3way apply-patch: try 3-way merge if the patch doesn't apply cleanly
|
|
71
|
-
--dry-run apply-patch: only check applicability, don't write
|
|
72
|
-
--help, -h Show this message
|
|
73
|
-
--version, -V Print CLI version
|
|
82
|
+
${P("EXIT CODES")}
|
|
83
|
+
0 ok 1 failure 2 usage 3 not authenticated 4 API error
|
|
74
84
|
|
|
75
|
-
${
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log(pkg.default.version);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (args.help || !args.command) {
|
|
92
|
-
printHelp();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// The proxy lives at ${endpoint} — same host as Code Studio, Crew,
|
|
96
|
-
// and the operator console. Override via --endpoint or
|
|
97
|
-
// ANYFORGE_ENDPOINT for staging.
|
|
98
|
-
const endpoint = args.endpoint
|
|
99
|
-
?? process.env.ANYFORGE_ENDPOINT
|
|
100
|
-
?? 'https://crew.anyforge.ai';
|
|
101
|
-
try {
|
|
102
|
-
switch (args.command) {
|
|
103
|
-
case 'init':
|
|
104
|
-
await runInit({ endpoint, force: args.force });
|
|
105
|
-
break;
|
|
106
|
-
case 'whoami':
|
|
107
|
-
await runWhoami();
|
|
108
|
-
break;
|
|
109
|
-
case 'status':
|
|
110
|
-
await runStatus({ endpoint });
|
|
111
|
-
break;
|
|
112
|
-
case 'logout':
|
|
113
|
-
await runLogout();
|
|
114
|
-
break;
|
|
115
|
-
case 'apply-patch':
|
|
116
|
-
await runApplyPatch({
|
|
117
|
-
patchFile: args.positional[0],
|
|
118
|
-
threeWay: args.threeWay,
|
|
119
|
-
dryRun: args.dryRun,
|
|
120
|
-
});
|
|
121
|
-
break;
|
|
122
|
-
case 'proxy':
|
|
123
|
-
// eslint-disable-next-line no-console
|
|
124
|
-
console.log(yellow('Local proxy mode is coming in a future release.'));
|
|
125
|
-
// eslint-disable-next-line no-console
|
|
126
|
-
console.log(dim(' Until then, your agents talk directly to the AnyForge proxy.'));
|
|
127
|
-
break;
|
|
128
|
-
default:
|
|
129
|
-
// eslint-disable-next-line no-console
|
|
130
|
-
console.error(red(`Unknown command: ${args.command}`));
|
|
131
|
-
printHelp();
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
// eslint-disable-next-line no-console
|
|
137
|
-
console.error(red('Error: ') + (err instanceof Error ? err.message : String(err)));
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
void main();
|
|
85
|
+
${P("EXAMPLES")}
|
|
86
|
+
anyforge approvals
|
|
87
|
+
anyforge approve hil_123 --notes "ship it"
|
|
88
|
+
anyforge crews --active --json | jq -r '.[].id'
|
|
89
|
+
anyforge work --status IN_PROGRESS --priority P0
|
|
90
|
+
anyforge work --program prog_123 --all --json | jq -r '.items[].id'
|
|
91
|
+
anyforge run wf_nightly --input '{"repo":"acme/web"}'
|
|
92
|
+
anyforge ask "which initiatives slipped this cycle?"
|
|
93
|
+
`)}async function zt(e,t,o,n){let r=t.flags.json===!0,[i]=t.positional;switch(e){case"init":return $e({endpoint:o,force:t.flags.force===!0});case"logout":return De();case"status":return Ke({endpoint:o});case"whoami":return je({endpoint:n,json:r});case"apply-patch":return Ge({patchFile:i,threeWay:t.flags["3way"]===!0,dryRun:t.flags["dry-run"]===!0});case"proxy":console.log(Bt("Local proxy mode is coming in a future release.")),console.log(F(" Until then, your agents talk directly to the AnyForge proxy."));return}let s=await Y(n);switch(e){case"approvals":return We({creds:s,json:r,limit:z(t.flags.limit,"limit")});case"approve":return ue({creds:s,json:r,approvalId:i,decision:"APPROVED",notes:h(t.flags.notes)});case"reject":return ue({creds:s,json:r,approvalId:i,decision:"REJECTED",notes:h(t.flags.notes)});case"crews":return Ye({creds:s,json:r,status:h(t.flags.status),activeOnly:ge(t.flags.active),limit:z(t.flags.limit,"limit")});case"crew":return He({creds:s,json:r,crewId:i,messages:z(t.flags.messages,"messages")});case"halt":return fe({creds:s,json:r,crewId:i,action:"halt"});case"resume":return fe({creds:s,json:r,crewId:i,action:"resume"});case"work":return Xe({creds:s,json:r,status:h(t.flags.status),type:h(t.flags.type),priority:h(t.flags.priority),initiative:h(t.flags.initiative),program:h(t.flags.program),search:h(t.flags.search),assignee:h(t.flags.assignee),includeArchived:ge(t.flags.archived),limit:z(t.flags.limit,"limit"),all:ge(t.flags.all)});case"workflows":return qe({creds:s,json:r});case"run":return Ve({creds:s,json:r,workflowId:i,input:h(t.flags.input)});case"ask":return ze({creds:s,json:r,question:t.positional.join(" "),context:h(t.flags.context)});default:throw new d(`Unknown command: ${e}`,"Run `anyforge --help` for the command list.")}}async function Qt(){let e;try{e=qt(process.argv.slice(2))}catch(n){tt(n);return}if(e.flags.version===!0){let n=await Promise.resolve().then(()=>(Ze(),Qe));console.log(n.default.version);return}if(e.flags.help===!0||!e.command){Vt();return}let t=h(e.flags.endpoint)??process.env.ANYFORGE_ENDPOINT,o=t??ot;try{await zt(e.command,e,o,t)}catch(n){tt(n)}}function tt(e){e instanceof v&&(process.stderr.write(et("Error: ")+e.message+`
|
|
94
|
+
`),e.hint?process.stderr.write(F(e.hint)+`
|
|
95
|
+
`):e instanceof d&&process.stderr.write(F("Run `anyforge --help`.")+`
|
|
96
|
+
`),process.exit(e.exitCode)),process.stderr.write(et("Error: ")+(e instanceof Error?e.message:String(e))+`
|
|
97
|
+
`),process.exit(O.FAILURE)}Qt();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anyforge/control",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AnyForge Control CLI
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AnyForge Control CLI \u2014 governed LLM proxy for any agent, and a scriptable client for AnyForge approvals, crews, work items and workflows.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://anyforge.ai",
|
|
7
7
|
"repository": {
|
|
@@ -23,14 +23,15 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "
|
|
26
|
+
"build": "esbuild src/cli.ts --bundle --minify --platform=node --format=esm --outfile=dist/cli.js --packages=external && chmod +x dist/cli.js",
|
|
27
|
+
"type-check": "tsc --noEmit",
|
|
27
28
|
"dev": "tsx src/cli.ts",
|
|
28
29
|
"prepublishOnly": "npm run build",
|
|
29
30
|
"publish:beta": "npm publish --access public --tag beta",
|
|
30
31
|
"publish:stable": "npm publish --access public"
|
|
31
32
|
},
|
|
32
33
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
34
|
+
"node": ">=22"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"kleur": "^4.1.5",
|
|
@@ -38,9 +39,10 @@
|
|
|
38
39
|
"prompts": "^2.4.2"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^20.
|
|
42
|
+
"@types/node": "^22.20.0",
|
|
42
43
|
"@types/prompts": "^2.4.9",
|
|
44
|
+
"esbuild": "^0.25.0",
|
|
43
45
|
"tsx": "^4.7.0",
|
|
44
|
-
"typescript": "^
|
|
46
|
+
"typescript": "^7.0.2"
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { red, green, yellow, dim, bold } from 'kleur/colors';
|
|
4
|
-
export async function runApplyPatch(opts) {
|
|
5
|
-
const patchContent = await loadPatch(opts.patchFile);
|
|
6
|
-
if (!patchContent.trim()) {
|
|
7
|
-
process.stderr.write(red('Empty patch — nothing to apply.\n'));
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
const stats = parseDiffStats(patchContent);
|
|
11
|
-
process.stderr.write(dim(`Patch: ${stats.files} ${stats.files === 1 ? 'file' : 'files'}, `) +
|
|
12
|
-
green(`+${stats.added}`) + ' ' + red(`-${stats.removed}`) + '\n');
|
|
13
|
-
// git apply --check first — never make a partial mess
|
|
14
|
-
const check = await runGit(['apply', '--check'], patchContent);
|
|
15
|
-
if (check.code !== 0) {
|
|
16
|
-
if (opts.threeWay) {
|
|
17
|
-
const three = await runGit(['apply', '--3way'], patchContent);
|
|
18
|
-
if (three.code !== 0) {
|
|
19
|
-
process.stderr.write(red('3-way apply failed:\n') + three.stderr + '\n');
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
process.stderr.write(yellow('✓ Applied via 3-way merge — review conflicts before committing.\n'));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
process.stderr.write(red('Patch does not apply cleanly:\n') + check.stderr + '\n');
|
|
26
|
-
process.stderr.write(dim('Try ') + bold('--3way') + dim(' to attempt a 3-way merge against the index.\n'));
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
if (opts.dryRun) {
|
|
30
|
-
process.stderr.write(green('✓ Patch would apply cleanly.\n'));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const apply = await runGit(['apply'], patchContent);
|
|
34
|
-
if (apply.code !== 0) {
|
|
35
|
-
process.stderr.write(red('git apply failed:\n') + apply.stderr + '\n');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
process.stderr.write(green('✓ Applied.\n'));
|
|
39
|
-
if (stats.files > 0) {
|
|
40
|
-
process.stderr.write(dim(' Run ') + bold('git diff --stat') + dim(' to review.\n'));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function loadPatch(filePath) {
|
|
44
|
-
if (filePath) {
|
|
45
|
-
return await readFile(filePath, 'utf8');
|
|
46
|
-
}
|
|
47
|
-
// Read stdin to a single string
|
|
48
|
-
const chunks = [];
|
|
49
|
-
for await (const chunk of process.stdin) {
|
|
50
|
-
chunks.push(chunk);
|
|
51
|
-
}
|
|
52
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
53
|
-
}
|
|
54
|
-
/** Spawn `git <args>`, pipe `stdinData` to its stdin, return code +
|
|
55
|
-
* captured streams. No shell — args are passed as a list to spawn. */
|
|
56
|
-
function runGit(args, stdinData) {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const child = spawn('git', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
59
|
-
let stdout = '';
|
|
60
|
-
let stderr = '';
|
|
61
|
-
child.stdout.on('data', d => { stdout += d.toString(); });
|
|
62
|
-
child.stderr.on('data', d => { stderr += d.toString(); });
|
|
63
|
-
child.on('error', reject);
|
|
64
|
-
child.on('close', code => resolve({ code: code ?? 0, stdout, stderr }));
|
|
65
|
-
child.stdin.write(stdinData);
|
|
66
|
-
child.stdin.end();
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
/** Quick stats parse off a unified diff — counts file headers and
|
|
70
|
-
* +/- lines. Skips diff metadata lines (+++, ---) so the report
|
|
71
|
-
* matches `git apply --stat` shape close enough for an inline note. */
|
|
72
|
-
function parseDiffStats(patch) {
|
|
73
|
-
let files = 0;
|
|
74
|
-
let added = 0;
|
|
75
|
-
let removed = 0;
|
|
76
|
-
for (const line of patch.split('\n')) {
|
|
77
|
-
if (line.startsWith('diff --git'))
|
|
78
|
-
files += 1;
|
|
79
|
-
else if (line.startsWith('+++') || line.startsWith('---'))
|
|
80
|
-
continue;
|
|
81
|
-
else if (line.startsWith('+'))
|
|
82
|
-
added += 1;
|
|
83
|
-
else if (line.startsWith('-'))
|
|
84
|
-
removed += 1;
|
|
85
|
-
}
|
|
86
|
-
if (files === 0 && (added > 0 || removed > 0)) {
|
|
87
|
-
// Patch had no `diff --git` headers (a hand-rolled fragment);
|
|
88
|
-
// count filename headers instead.
|
|
89
|
-
files = (patch.match(/^\+\+\+ /gm) ?? []).length;
|
|
90
|
-
}
|
|
91
|
-
return { files, added, removed };
|
|
92
|
-
}
|