@anyforge/control 0.13.0 → 0.14.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/dist/cli.js +33 -31
- package/dist/opencode-plugin.js +215 -46
- package/package.json +1 -1
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.
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|