@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 +1 -1
- package/dist/opencode-plugin.js +215 -46
- package/package.json +1 -1
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.
|
|
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)+`
|
package/dist/opencode-plugin.js
CHANGED
|
@@ -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.
|
|
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
|
|
264
|
+
/** One finished model call (V1 `step-finish` part). */
|
|
229
265
|
step(part, id, nowMs) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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(
|
|
290
|
+
costUsd: num(cost)
|
|
245
291
|
};
|
|
246
292
|
const any = reading.input + reading.output + reading.cacheRead + reading.cacheWrite + reading.costUsd > 0;
|
|
247
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|