@anyforge/control 0.13.0 → 0.13.1

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var Lc=Object.defineProperty;var Nc=(e,t,n)=>()=>{if(n)throw n[0];try{return e&&(t=e(e=0)),t}catch(r){throw n=[r],r}};var Fc=(e,t)=>{for(var n in t)Lc(e,n,{get:t[n],enumerable:!0})};var hr={};Fc(hr,{default:()=>Cp});var Cp,gr=Nc(()=>{Cp={name:"@anyforge/control",version:"0.13.0",description:"AnyForge CLI \u2014 hosted Oracle conversations, a governed LLM proxy, and a scriptable client for 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 && esbuild src/opencodePlugin/plugin.ts --bundle --platform=node --format=esm --outfile=dist/opencode-plugin.js","type-check":"tsc --noEmit",test:"npm run build && node scripts/test.mjs",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.19.0"},dependencies:{"@agentclientprotocol/sdk":"^1.9.0","@earendil-works/pi-tui":"^1.0.2",kleur:"^4.1.5",open:"^11.0.4",prompts:"^2.4.2","proper-lockfile":"^4.1.2"},devDependencies:{"@types/node":"^22.20.0","@types/prompts":"^2.4.9","@types/proper-lockfile":"^4.1.4",esbuild:"^0.28.2","node-pty":"^1.1.0",tsx:"^4.7.0",typescript:"^7.0.2"}}});import{red as hc,yellow as wf,dim as Ze,bold as G}from"kleur/colors";import{execFileSync as Cr}from"child_process";import Vc from"open";import ot from"prompts";import{green as it,dim as $,bold as se,yellow as Ae,cyan as on}from"kleur/colors";import{promises as Ln}from"fs";import{homedir as qc}from"os";import{join as Zo}from"path";var ei=Zo(qc(),".anyforge"),xr=Zo(ei,"credentials");async function ti(e){await Ln.mkdir(ei,{recursive:!0,mode:448}),await Ln.writeFile(xr,JSON.stringify(e,null,2),{mode:384})}async function _t(){try{let e=await Ln.readFile(xr,"utf8");return JSON.parse(e)}catch(e){if(e.code==="ENOENT")return null;throw e}}async function ni(){try{return await Ln.unlink(xr),!0}catch(e){if(e.code==="ENOENT")return!1;throw e}}import{promises as B}from"fs";import{execFileSync as Dc}from"child_process";import{homedir as Te}from"os";import{join as X}from"path";import Et from"prompts";import{dim as H,green as Nn,yellow as nt}from"kleur/colors";import{promises as fe}from"node:fs";import{execFileSync as Mc}from"node:child_process";import{homedir as Ce}from"node:os";import{dirname as Sr,join as me}from"node:path";import{fileURLToPath as Uc}from"node:url";function Zt(e=Ce(),t=process.env){return me(t.XDG_CONFIG_HOME||me(e,".config"),"opencode")}function oi(e=Ce(),t=process.env){return me(t.XDG_DATA_HOME||me(e,".local","share"),"opencode")}async function It(e=Ce()){if(await ri(Zt(e))||await ri(oi(e)))return!0;try{return Mc("opencode",["--version"],{stdio:["ignore","pipe","pipe"],timeout:5e3}),!0}catch{return!1}}async function ii(e=Ce()){try{return JSON.parse(await fe.readFile(me(oi(e),"auth.json"),"utf8")).anthropic?.type==="oauth"}catch{return!1}}async function en(e=Ce()){let t=Zt(e);for(let n of["opencode.json","opencode.jsonc"]){let r=me(t,n),o;try{o=await fe.readFile(r,"utf8")}catch{continue}if(!o.trim())return{path:r,config:{}};try{let i=JSON.parse(o);return{path:r,config:T(i)?i:null}}catch{return{path:r,config:null}}}return{path:me(t,"opencode.json"),config:{}}}async function tn(e,t){await fe.mkdir(me(e,".."),{recursive:!0});let n=t.$schema?t:{$schema:"https://opencode.ai/config.json",...t};await fe.writeFile(e,JSON.stringify(n,null,2)+`
2
+ var Lc=Object.defineProperty;var Nc=(e,t,n)=>()=>{if(n)throw n[0];try{return e&&(t=e(e=0)),t}catch(r){throw n=[r],r}};var Fc=(e,t)=>{for(var n in t)Lc(e,n,{get:t[n],enumerable:!0})};var hr={};Fc(hr,{default:()=>Cp});var Cp,gr=Nc(()=>{Cp={name:"@anyforge/control",version:"0.13.1",description:"AnyForge CLI \u2014 hosted Oracle conversations, a governed LLM proxy, and a scriptable client for 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 && esbuild src/opencodePlugin/plugin.ts --bundle --platform=node --format=esm --outfile=dist/opencode-plugin.js","type-check":"tsc --noEmit",test:"npm run build && node scripts/test.mjs",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.19.0"},dependencies:{"@agentclientprotocol/sdk":"^1.9.0","@earendil-works/pi-tui":"^1.0.2",kleur:"^4.1.5",open:"^11.0.4",prompts:"^2.4.2","proper-lockfile":"^4.1.2"},devDependencies:{"@types/node":"^22.20.0","@types/prompts":"^2.4.9","@types/proper-lockfile":"^4.1.4",esbuild:"^0.28.2","node-pty":"^1.1.0",tsx:"^4.7.0",typescript:"^7.0.2"}}});import{red as hc,yellow as wf,dim as Ze,bold as G}from"kleur/colors";import{execFileSync as Cr}from"child_process";import Vc from"open";import ot from"prompts";import{green as it,dim as $,bold as se,yellow as Ae,cyan as on}from"kleur/colors";import{promises as Ln}from"fs";import{homedir as qc}from"os";import{join as Zo}from"path";var ei=Zo(qc(),".anyforge"),xr=Zo(ei,"credentials");async function ti(e){await Ln.mkdir(ei,{recursive:!0,mode:448}),await Ln.writeFile(xr,JSON.stringify(e,null,2),{mode:384})}async function _t(){try{let e=await Ln.readFile(xr,"utf8");return JSON.parse(e)}catch(e){if(e.code==="ENOENT")return null;throw e}}async function ni(){try{return await Ln.unlink(xr),!0}catch(e){if(e.code==="ENOENT")return!1;throw e}}import{promises as B}from"fs";import{execFileSync as Dc}from"child_process";import{homedir as Te}from"os";import{join as X}from"path";import Et from"prompts";import{dim as H,green as Nn,yellow as nt}from"kleur/colors";import{promises as fe}from"node:fs";import{execFileSync as Mc}from"node:child_process";import{homedir as Ce}from"node:os";import{dirname as Sr,join as me}from"node:path";import{fileURLToPath as Uc}from"node:url";function Zt(e=Ce(),t=process.env){return me(t.XDG_CONFIG_HOME||me(e,".config"),"opencode")}function oi(e=Ce(),t=process.env){return me(t.XDG_DATA_HOME||me(e,".local","share"),"opencode")}async function It(e=Ce()){if(await ri(Zt(e))||await ri(oi(e)))return!0;try{return Mc("opencode",["--version"],{stdio:["ignore","pipe","pipe"],timeout:5e3}),!0}catch{return!1}}async function ii(e=Ce()){try{return JSON.parse(await fe.readFile(me(oi(e),"auth.json"),"utf8")).anthropic?.type==="oauth"}catch{return!1}}async function en(e=Ce()){let t=Zt(e);for(let n of["opencode.json","opencode.jsonc"]){let r=me(t,n),o;try{o=await fe.readFile(r,"utf8")}catch{continue}if(!o.trim())return{path:r,config:{}};try{let i=JSON.parse(o);return{path:r,config:T(i)?i:null}}catch{return{path:r,config:null}}}return{path:me(t,"opencode.json"),config:{}}}async function tn(e,t){await fe.mkdir(me(e,".."),{recursive:!0});let n=t.$schema?t:{$schema:"https://opencode.ai/config.json",...t};await fe.writeFile(e,JSON.stringify(n,null,2)+`
3
3
  `,{mode:384}),await fe.chmod(e,384).catch(()=>{})}function si(e){if(T(e.providers))return!0;let t=e.mcp;return T(t)&&T(t.servers)}function Rr(e){return`${e}/v1/anthropic/v1`}function ai(e,t,n){let r=Rr(t),o=`Bearer ${n}`;if(si(e)){let l=T(e.providers)?e.providers:{},d=T(l.anthropic)?l.anthropic:{},u=T(d.settings)?d.settings:{},y=T(d.headers)?d.headers:{};return{...e,providers:{...l,anthropic:{...d,settings:{...u,baseURL:r,apiKey:n},headers:{...y,Authorization:o}}}}}let i=T(e.provider)?e.provider:{},s=T(i.anthropic)?i.anthropic:{},a=T(s.options)?s.options:{},c=T(a.headers)?a.headers:{};return{...e,provider:{...i,anthropic:{...s,options:{...a,baseURL:r,apiKey:n,headers:{...c,Authorization:o}}}}}}function _r(e){let t=T(e.providers)&&T(e.providers.anthropic)&&T(e.providers.anthropic.settings)?e.providers.anthropic.settings.baseURL:void 0,n=T(e.provider)&&T(e.provider.anthropic)&&T(e.provider.anthropic.options)?e.provider.anthropic.options.baseURL:void 0,r=t??n;return typeof r=="string"?r:null}function Ir(e){let t=e.mcp;return T(t)?T(t.servers)&&T(t.servers.anyforge)||T(t.anyforge):!1}function ci(e,t){if(Ir(e))return e;let n=`${t}/api/mcp`,r=T(e.mcp)?e.mcp:{};if(si(e)){let o=T(r.servers)?r.servers:{};return{...e,mcp:{...r,servers:{...o,anyforge:{type:"remote",url:n}}}}}return{...e,mcp:{...r,anyforge:{type:"remote",url:n,enabled:!0}}}}function li(e,t){let n=_r(e);if(!n||!t(n))return null;let r={...e},o=(i,s)=>{let a=r[i];if(!T(a)||!T(a.anthropic))return;let c={...a.anthropic};if(T(c[s])){let d={...c[s]};if(typeof d.baseURL=="string"&&t(d.baseURL)&&(delete d.baseURL,delete d.apiKey,T(d.headers))){let u={...d.headers};delete u.Authorization,Object.keys(u).length?d.headers=u:delete d.headers}Object.keys(d).length?c[s]=d:delete c[s]}if(i==="providers"&&T(c.headers)){let d={...c.headers};typeof d.Authorization=="string"&&d.Authorization.startsWith("Bearer sk_anyforge")&&delete d.Authorization,Object.keys(d).length?c.headers=d:delete c.headers}let l={...a};Object.keys(c).length?l.anthropic=c:delete l.anthropic,Object.keys(l).length?r[i]=l:delete r[i]};return o("providers","settings"),o("provider","options"),r}function T(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}async function ri(e){try{return await fe.access(e),!0}catch{return!1}}function di(e=Ce()){return me(Zt(e),"plugins","anyforge-telemetry.js")}function ui(e=Ce()){return me(e,".anyforge","opencode-telemetry.json")}async function jc(){let e=Sr(Uc(import.meta.url));for(let t of[me(e,"opencode-plugin.js"),me(e,"..","..","dist","opencode-plugin.js")])try{return await fe.readFile(t,"utf8")}catch{}throw new Error("The OpenCode plugin is missing from this CLI build (dist/opencode-plugin.js).")}async function pi(e){let t=e.home??Ce(),n=di(t);await fe.mkdir(Sr(n),{recursive:!0}),await fe.writeFile(n,await jc());let r=ui(t);return await fe.mkdir(Sr(r),{recursive:!0,mode:448}),await fe.writeFile(r,JSON.stringify({sendNames:e.sendNames},null,2)+`
4
4
  `,{mode:384}),n}async function fi(e=Ce()){let t=[];for(let n of[di(e),ui(e)])try{await fe.unlink(n),t.push(n)}catch{}return t}var He="# === AnyForge Control \u2014 added by `anyforge init` ===",nn="# === end AnyForge Control ===",rt=".anyforge-bak",Bc=/claude_(max|pro|team|enterprise)/i,Wc=["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 yi(e){let t=[],n=await Gc(e);if(n&&t.push(n),e.scope==="repo"){let o=await zc(e);return o&&t.push(o),await It()&&(console.log(H(" OpenCode is patched only globally (a project opencode.json is usually committed).")),console.log(H(' Re-run outside a repo, or choose "global", to route OpenCode through AnyForge.'))),t}let r=await Hc(e);r&&t.push(r);for(let o of Jc){let i=await o.detect();if(!(!i||!(await Et({type:"confirm",name:"go",message:`Patch ${o.label} via shell rc (${H(i.path)}) ?`,initial:!0})).go))try{await o.patch(i,e),t.push({label:o.label,path:i.path}),console.log(Nn(" \u2714")+" "+o.label)}catch(a){console.warn(" \u26A0 Skipped "+o.label+": "+a.message)}}return t}async function Gc(e){let t=e.scope==="repo"&&e.repoRoot?{dir:X(e.repoRoot,".claude"),file:"settings.local.json"}:{dir:X(Te(),".claude"),file:"settings.json"},n=X(t.dir,t.file);if(!(await Et({type:"confirm",name:"go",message:`Patch Claude Code config at ${H(n)} ?`,initial:!0})).go)return null;await B.mkdir(t.dir,{recursive:!0});let o=await B.readFile(n,"utf8").catch(()=>"{}"),i;try{i=JSON.parse(o)}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(nt(" \u26A0 Local LLM proxy detected: ")+H(a)),console.log(H(" AnyForge only routes Anthropic Claude models. Local models (Ollama,")),console.log(H(" LM Studio, etc.) will not work in Claude Code after this patch.")),console.log(H(" To restore: remove ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN")),console.log(H(` from ${n}`)),console.log(),!(await Et({type:"confirm",name:"go",message:"Overwrite existing local LLM proxy config?",initial:!1})).go)?null:(await Oe(n)&&!await Oe(n+rt)&&await B.copyFile(n,n+rt),s.ANTHROPIC_BASE_URL=`${e.endpoint}/v1/anthropic`,s.ANTHROPIC_AUTH_TOKEN=e.apiKey,delete s.ANTHROPIC_API_KEY,i.env=s,await B.writeFile(n,JSON.stringify(i,null,2)+`
5
5
  `),console.log(Nn(" \u2714")+" Claude Code"),{label:e.scope==="repo"?"Claude Code (repo)":"Claude Code (global)",path:n})}async function Hc(e){if(!await It())return null;if(await ii())return console.log(nt(" \u26A0 OpenCode is signed in to Anthropic with a Claude subscription.")),console.log(H(" Routing it through AnyForge would replace that login, so OpenCode was not patched.")),null;let t=await en();if(t.config===null)return console.log(nt(" \u26A0 "+t.path+" is not plain JSON (comments?), so it was not changed.")),console.log(H(' To route OpenCode through AnyForge, add to its "provider" section:')),console.log(H(` "anthropic": { "options": { "baseURL": "${Rr(e.endpoint)}", "apiKey": "<your AnyForge key>",`)),console.log(H(' "headers": { "Authorization": "Bearer <your AnyForge key>" } } }')),null;let n=_r(t.config);return n&&!rn(n,Er(e.endpoint))&&(console.log(nt(" \u26A0 OpenCode already sends Anthropic requests to ")+H(n)),!(await Et({type:"confirm",name:"go",message:"Replace it with the AnyForge proxy?",initial:!1})).go)||!(await Et({type:"confirm",name:"go",message:`Patch OpenCode config at ${H(t.path)} ?`,initial:!0})).go?null:(await tn(t.path,ai(t.config,e.endpoint,e.apiKey)),console.log(Nn(" \u2714")+" OpenCode"),{label:"OpenCode (global)",path:t.path})}async function vi(e){let t=X(Te(),".claude"),n=X(t,"settings.json");await B.mkdir(t,{recursive:!0});let r=await B.readFile(n,"utf8").catch(()=>"{}"),o;try{o=JSON.parse(r)}catch{o={}}let i=o.env??{},s=i.OTEL_EXPORTER_OTLP_ENDPOINT;return s&&!rn(s,Er(e.endpoint))?(console.log(nt(" \u26A0 An OpenTelemetry endpoint is already configured: ")+H(s)),console.log(H(" Leaving it untouched. To send Claude Code usage to AnyForge, point")),console.log(H(" OTEL_EXPORTER_OTLP_ENDPOINT at "+e.endpoint+"/api/otel")),null):(await Oe(n)&&!await Oe(n+rt)&&await B.copyFile(n,n+rt),i.CLAUDE_CODE_ENABLE_TELEMETRY="1",i.OTEL_METRICS_EXPORTER="otlp",i.OTEL_LOGS_EXPORTER===void 0&&i.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT===void 0&&(i.OTEL_LOGS_EXPORTER="otlp",e.toolDetails&&i.OTEL_LOG_TOOL_DETAILS===void 0&&(i.OTEL_LOG_TOOL_DETAILS="1")),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",o.env=i,await B.writeFile(n,JSON.stringify(o,null,2)+`
@@ -150,19 +150,55 @@ function attr(key, value) {
150
150
  function sanitize(name) {
151
151
  return name.replace(/[^a-zA-Z0-9_-]/g, "_");
152
152
  }
153
+ function mcpPrefixesOf(server2) {
154
+ return [.../* @__PURE__ */ new Set([`${sanitize(server2)}_`, `${server2}_`, `${server2}.`, `${server2}/`, `${server2}:`])];
155
+ }
156
+ var BUILTIN_TOOLS = /* @__PURE__ */ new Set([
157
+ "bash",
158
+ "read",
159
+ "write",
160
+ "edit",
161
+ "multiedit",
162
+ "patch",
163
+ "apply_patch",
164
+ "glob",
165
+ "grep",
166
+ "list",
167
+ "ls",
168
+ "webfetch",
169
+ "websearch",
170
+ "codesearch",
171
+ "todowrite",
172
+ "todoread",
173
+ "task",
174
+ "skill",
175
+ "lsp",
176
+ "question",
177
+ "batch",
178
+ "invalid",
179
+ "external_directory",
180
+ "doom_loop"
181
+ ]);
182
+ var UNNAMED_TOOL = "other_tool";
183
+ var STOPPED_ERROR = /abort|cancel|interrupt/i;
153
184
  var TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$/;
154
185
  var OpencodeTelemetry = class {
155
186
  constructor(options) {
156
187
  this.options = options;
157
- this.mcpPrefixes = options.mcpServers.map((server) => ({ server, prefix: `${sanitize(server)}_` })).sort((a, b) => b.prefix.length - a.prefix.length);
188
+ this.setMcpServers(options.mcpServers);
158
189
  }
159
190
  options;
160
- mcpPrefixes;
191
+ mcpPrefixes = [];
161
192
  models = new Remembered();
162
193
  callTools = new Remembered();
163
194
  permissionTools = new Remembered();
164
195
  reported = new Remembered();
196
+ toolStarts = new Remembered();
165
197
  proxiedProviders = /* @__PURE__ */ new Set();
198
+ /** The configured MCP servers, which V2 reports after the plugin has started. */
199
+ setMcpServers(servers) {
200
+ this.mcpPrefixes = servers.flatMap((server2) => mcpPrefixesOf(server2).map((prefix) => ({ server: server2, prefix }))).sort((a, b) => b.prefix.length - a.prefix.length);
201
+ }
166
202
  /** `chat.message`: the member sent a prompt. Its text is never read. */
167
203
  prompt(nowMs) {
168
204
  return { logs: [this.record("user_prompt", nowMs, [])], usage: [] };
@@ -225,14 +261,24 @@ var OpencodeTelemetry = class {
225
261
  if (part.type === "tool") return this.tool(part, id, nowMs);
226
262
  return EMPTY;
227
263
  }
228
- /** One finished model call: an `api_request`, and its tokens unless the proxy metered them. */
264
+ /** One finished model call (V1 `step-finish` part). */
229
265
  step(part, id, nowMs) {
230
- if (this.reported.has(`step:${id}`)) return EMPTY;
231
- this.reported.set(`step:${id}`, true);
232
- const model = this.models.get(str(part.messageID) ?? "");
233
- const logs = [this.record("api_request", nowMs, model ? [attr("model", model.modelID)] : [])];
234
- if (model && this.proxiedProviders.has(model.providerID)) return { logs, usage: [] };
235
- const tokens = isRecord(part.tokens) ? part.tokens : {};
266
+ return this.modelCall(`step:${id}`, str(part.messageID), part.tokens, part.cost, "api_request", nowMs);
267
+ }
268
+ /**
269
+ * One model call: an `api_request` (or `api_error`) and its tokens, unless
270
+ * the AnyForge proxy already metered them. Reported once per key.
271
+ */
272
+ modelCall(key, messageID, rawTokens, cost, event, nowMs) {
273
+ if (this.reported.has(key)) return EMPTY;
274
+ this.reported.set(key, true);
275
+ const model = this.models.get(messageID ?? "");
276
+ const logs = [this.record(event, nowMs, model ? [attr("model", model.modelID)] : [])];
277
+ return { logs, usage: this.usage(model, rawTokens, cost) };
278
+ }
279
+ usage(model, rawTokens, cost) {
280
+ if (model && this.proxiedProviders.has(model.providerID)) return [];
281
+ const tokens = isRecord(rawTokens) ? rawTokens : {};
236
282
  const cache = isRecord(tokens.cache) ? tokens.cache : {};
237
283
  const reading = {
238
284
  model: model?.modelID ?? "unknown",
@@ -241,10 +287,85 @@ var OpencodeTelemetry = class {
241
287
  output: num(tokens.output) + num(tokens.reasoning),
242
288
  cacheRead: num(cache.read),
243
289
  cacheWrite: num(cache.write),
244
- costUsd: num(part.cost)
290
+ costUsd: num(cost)
245
291
  };
246
292
  const any = reading.input + reading.output + reading.cacheRead + reading.cacheWrite + reading.costUsd > 0;
247
- return { logs, usage: any ? [reading] : [] };
293
+ return any ? [reading] : [];
294
+ }
295
+ // ── OpenCode V2 ──────────────────────────────────────────────────────────
296
+ /** One V2 session or permission event, `{ id, type, data }`. */
297
+ eventV2(event, nowMs) {
298
+ if (!isRecord(event) || !isRecord(event.data)) return EMPTY;
299
+ const data = event.data;
300
+ const eventId = str(event.id);
301
+ switch (event.type) {
302
+ case "session.inbox.enqueued": {
303
+ const inboxID = str(data.inboxID);
304
+ if (!isRecord(data.item) || data.item.type !== "user" || !inboxID || this.reported.has(`prompt:${inboxID}`)) return EMPTY;
305
+ this.reported.set(`prompt:${inboxID}`, true);
306
+ return this.prompt(nowMs);
307
+ }
308
+ case "session.step.started": {
309
+ const messageID = str(data.assistantMessageID);
310
+ const model = isRecord(data.model) ? data.model : {};
311
+ const modelID = str(model.id);
312
+ const providerID = str(model.providerID);
313
+ if (messageID && modelID && providerID) this.models.set(messageID, { modelID, providerID });
314
+ return EMPTY;
315
+ }
316
+ case "session.step.ended":
317
+ if (!eventId) return EMPTY;
318
+ return this.modelCall(`step2:${eventId}`, str(data.assistantMessageID), data.tokens, data.cost, "api_request", nowMs);
319
+ case "session.step.failed": {
320
+ if (!eventId) return EMPTY;
321
+ const error = isRecord(data.error) ? data.error : {};
322
+ if (STOPPED_ERROR.test(str(error.type) ?? "")) {
323
+ this.reported.set(`step2:${eventId}`, true);
324
+ return { logs: [], usage: this.usage(this.models.get(str(data.assistantMessageID) ?? ""), data.tokens, data.cost) };
325
+ }
326
+ return this.modelCall(`step2:${eventId}`, str(data.assistantMessageID), data.tokens, data.cost, "api_error", nowMs);
327
+ }
328
+ case "session.skill.activated": {
329
+ if (!eventId || this.reported.has(`skill:${eventId}`)) return EMPTY;
330
+ this.reported.set(`skill:${eventId}`, true);
331
+ return { logs: [this.record("skill_activated", nowMs, [attr("skill.name", this.skillName({ name: data.name }))])], usage: [] };
332
+ }
333
+ case "permission.asked": {
334
+ const source = isRecord(data.source) ? data.source : {};
335
+ this.notePermission(str(data.id), str(source.id), str(data.action));
336
+ return EMPTY;
337
+ }
338
+ case "permission.replied":
339
+ return this.decision(str(data.requestID), str(data.reply), nowMs);
340
+ default:
341
+ return EMPTY;
342
+ }
343
+ }
344
+ /** V2 `tool` hook `execute.before`: remember the call's tool and start time. */
345
+ toolStarted(callID, tool, nowMs) {
346
+ const id = str(callID);
347
+ const name = str(tool);
348
+ if (!id || !name) return;
349
+ this.callTools.set(id, name);
350
+ this.toolStarts.set(id, nowMs);
351
+ }
352
+ /** V2 `tool` hook `execute.after`: one finished call. V2 reports skills as their own event. */
353
+ toolEnded(callID, tool, status, nowMs) {
354
+ const id = str(callID);
355
+ const name = str(tool) ?? (id ? this.callTools.get(id) : void 0);
356
+ if (!id || !name || status !== "completed" && status !== "error") return EMPTY;
357
+ if (this.reported.has(`tool2:${id}`)) return EMPTY;
358
+ this.reported.set(`tool2:${id}`, true);
359
+ const started = this.toolStarts.get(id);
360
+ const duration = started === void 0 ? 0 : nowMs - started;
361
+ return {
362
+ logs: [this.record("tool_result", nowMs, [
363
+ ...this.toolAttributes(name),
364
+ attr("success", status === "completed"),
365
+ ...duration > 0 && duration < 864e5 ? [attr("duration_ms", Math.round(duration))] : []
366
+ ])],
367
+ usage: []
368
+ };
248
369
  }
249
370
  tool(part, id, nowMs) {
250
371
  const tool = str(part.tool);
@@ -294,7 +415,8 @@ var OpencodeTelemetry = class {
294
415
  const name = this.options.sendNames ? `mcp__${mcp.server}__${tool.slice(mcp.prefix.length)}` : "mcp_tool";
295
416
  return [attr("tool_name", TOOL_NAME.test(name) ? name : "mcp_tool"), attr("tool_source", "mcp")];
296
417
  }
297
- return TOOL_NAME.test(tool) ? [attr("tool_name", tool), attr("tool_source", "builtin")] : [attr("tool_source", "builtin")];
418
+ const named = this.options.sendNames ? TOOL_NAME.test(tool) : BUILTIN_TOOLS.has(tool);
419
+ return [attr("tool_name", named ? tool : UNNAMED_TOOL), attr("tool_source", "builtin")];
298
420
  }
299
421
  skillName(input) {
300
422
  const name = isRecord(input) ? str(input.name) : void 0;
@@ -335,24 +457,20 @@ async function mcpServerNames(input) {
335
457
  return [];
336
458
  }
337
459
  }
338
- var AnyforgeTelemetry = async (input) => {
460
+ async function start(mcpServers) {
339
461
  const anyforgeDir = join(homedir(), ".anyforge");
340
462
  const creds = await readJson(join(anyforgeDir, "credentials"));
341
463
  const apiKey = typeof creds?.apiKey === "string" ? creds.apiKey : null;
342
464
  const endpoint = typeof creds?.endpoint === "string" ? creds.endpoint.replace(/\/+$/, "") : null;
343
- if (!apiKey || !endpoint) return {};
344
- let anyforgeHost = null;
465
+ if (!apiKey || !endpoint) return null;
466
+ let anyforgeHost;
345
467
  try {
346
468
  anyforgeHost = new URL(endpoint).host;
347
469
  } catch {
348
- return {};
470
+ return null;
349
471
  }
350
472
  const settings = await readJson(join(anyforgeDir, "opencode-telemetry.json"));
351
- const telemetry = new OpencodeTelemetry({
352
- sendNames: settings?.sendNames === true,
353
- mcpServers: await mcpServerNames(input),
354
- anyforgeHost
355
- });
473
+ const telemetry = new OpencodeTelemetry({ sendNames: settings?.sendNames === true, mcpServers, anyforgeHost });
356
474
  const exporter = new TelemetryExporter({
357
475
  endpoint,
358
476
  apiKey,
@@ -363,36 +481,87 @@ var AnyforgeTelemetry = async (input) => {
363
481
  void exporter.flush();
364
482
  }, FLUSH_INTERVAL_MS);
365
483
  timer.unref?.();
366
- const take = (t) => {
367
- exporter.addLogs(t.logs);
368
- exporter.addUsage(t.usage);
369
- };
370
484
  return {
371
- "chat.message": async () => {
372
- try {
373
- take(telemetry.prompt(Date.now()));
374
- } catch {
375
- }
376
- },
377
- "chat.params": async (params) => {
378
- try {
379
- telemetry.provider(params?.provider?.info?.id, params?.provider?.options?.baseURL);
380
- } catch {
381
- }
382
- },
383
- event: async ({ event }) => {
384
- try {
385
- take(telemetry.event(event, Date.now()));
386
- if (event?.type === "session.idle") void exporter.flush();
387
- } catch {
388
- }
485
+ telemetry,
486
+ exporter,
487
+ take: (t) => {
488
+ exporter.addLogs(t.logs);
489
+ exporter.addUsage(t.usage);
389
490
  },
390
- dispose: async () => {
491
+ stop: async () => {
391
492
  clearInterval(timer);
392
493
  await exporter.flush();
393
494
  }
394
495
  };
395
- };
496
+ }
497
+ function quietly(step) {
498
+ try {
499
+ step();
500
+ } catch {
501
+ }
502
+ }
503
+ async function server(input) {
504
+ const run = await start(await mcpServerNames(input));
505
+ if (!run) return {};
506
+ const { telemetry, take, exporter } = run;
507
+ return {
508
+ "chat.message": async () => quietly(() => take(telemetry.prompt(Date.now()))),
509
+ "chat.params": async (params) => quietly(() => telemetry.provider(params?.provider?.info?.id, params?.provider?.options?.baseURL)),
510
+ event: async ({ event }) => quietly(() => {
511
+ take(telemetry.event(event, Date.now()));
512
+ if (event?.type === "session.idle") void exporter.flush();
513
+ }),
514
+ dispose: () => run.stop()
515
+ };
516
+ }
517
+ var V2_EVENTS = [
518
+ "session.inbox.enqueued",
519
+ "session.step.started",
520
+ "session.step.ended",
521
+ "session.step.failed",
522
+ "session.skill.activated",
523
+ "session.execution.succeeded",
524
+ "session.execution.failed",
525
+ "permission.asked",
526
+ "permission.replied"
527
+ ];
528
+ async function setup(ctx) {
529
+ const run = await start([]);
530
+ if (!run) return;
531
+ const { telemetry, take, exporter } = run;
532
+ const registrations = [];
533
+ const register = async (registering) => {
534
+ try {
535
+ registrations.push(await registering());
536
+ } catch {
537
+ }
538
+ };
539
+ await register(() => ctx.mcp.transform((editor) => quietly(() => telemetry.setMcpServers(editor.list().map(([name]) => name)))));
540
+ await register(() => ctx.session.hook("model.request", (input) => quietly(() => telemetry.provider(input?.model?.providerID, input?.baseURL))));
541
+ await register(() => ctx.tool.hook("execute.before", (input) => quietly(() => telemetry.toolStarted(input?.id, input?.tool, Date.now()))));
542
+ await register(() => ctx.tool.hook("execute.after", (input) => quietly(() => take(telemetry.toolEnded(input?.id, input?.tool, input?.status, Date.now())))));
543
+ const abort = new AbortController();
544
+ void (async () => {
545
+ try {
546
+ for await (const event of ctx.event.subscribe({ types: V2_EVENTS, signal: abort.signal })) {
547
+ quietly(() => {
548
+ take(telemetry.eventV2(event, Date.now()));
549
+ const type = event?.type;
550
+ if (type === "session.execution.succeeded" || type === "session.execution.failed") void exporter.flush();
551
+ });
552
+ }
553
+ } catch {
554
+ }
555
+ })();
556
+ return async () => {
557
+ abort.abort();
558
+ await Promise.all(registrations.map((r) => r.dispose().catch(() => {
559
+ })));
560
+ await run.stop();
561
+ };
562
+ }
563
+ var AnyforgeTelemetry = { id: "anyforge-telemetry", setup, server };
564
+ var plugin_default = AnyforgeTelemetry;
396
565
  export {
397
- AnyforgeTelemetry
566
+ plugin_default as default
398
567
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyforge/control",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "AnyForge CLI — hosted Oracle conversations, a governed LLM proxy, and a scriptable client for approvals, crews, work items and workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://anyforge.ai",