@wirecat/cli-core 0.466.0 → 0.467.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/CHANGELOG.md +36 -0
- package/README.md +4 -2
- package/dist/cli/command-runs.d.ts +38 -10
- package/dist/cli/command-runs.js +51 -17
- package/dist/cli/context.d.ts +3 -0
- package/dist/cli/context.js +15 -3
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/messenger/context.js +1 -1
- package/dist/cli/messenger/observed.js +10 -2
- package/dist/cli/messenger/report.js +48 -3
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +7 -3
- package/dist/cli/settings.d.ts +19 -6
- package/dist/cli/settings.js +14 -3
- package/dist/cli/shared-command-exports.d.ts +12 -4
- package/dist/entities/index.d.ts +39 -0
- package/dist/entities/recorded-run.d.ts +39 -0
- package/dist/entities/recorded-run.js +14 -1
- package/dist/mcp/agent.d.ts +3 -0
- package/dist/mcp/agent.js +2 -0
- package/dist/mcp/all-profiles.d.ts +2 -0
- package/dist/mcp/all-profiles.js +16 -3
- package/dist/mcp/bot/server.js +8 -3
- package/dist/mcp/caller.d.ts +38 -0
- package/dist/mcp/caller.js +95 -0
- package/dist/mcp/http/oauth.js +5 -1
- package/dist/mcp/server.js +19 -7
- package/dist/mcp/surface.js +11 -2
- package/dist/mcp/tool.d.ts +4 -1
- package/dist/mcp/tool.js +21 -11
- package/dist/runs/events.d.ts +16 -4
- package/dist/runs/events.js +4 -0
- package/dist/runs/log-attributes.js +7 -1
- package/dist/runs/log-events.js +5 -0
- package/dist/runs/recording.d.ts +10 -3
- package/dist/runs/recording.js +59 -15
- package/dist/runs/run-context.d.ts +55 -4
- package/dist/runs/run-context.js +34 -3
- package/dist/runs/run.d.ts +37 -11
- package/dist/runs/run.js +201 -52
- package/dist/runs/scope.d.ts +12 -0
- package/dist/runs/scope.js +6 -0
- package/dist/services/patience.js +5 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
3
|
import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
4
|
import { CliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
@@ -25,6 +26,7 @@ import { levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
|
25
26
|
import { openStore } from "../store/store.js";
|
|
26
27
|
import { runCommandForAgent, toolsFromCommands } from "./agent.js";
|
|
27
28
|
import { agentLog } from "./agent-log.js";
|
|
29
|
+
import { callerOf, withCaller } from "./caller.js";
|
|
28
30
|
import { mcpHttpMarker } from "./http/state.js";
|
|
29
31
|
import { instructions } from "./instructions.js";
|
|
30
32
|
import { registerPrompts } from "./prompts.js";
|
|
@@ -195,7 +197,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
195
197
|
* One command at a time, on the session's connection, opened only if the command asks for it. A failure that may
|
|
196
198
|
* have been the connection's drops it, as the session does for its own calls.
|
|
197
199
|
*/
|
|
198
|
-
const invoke = (one, input, env) => new Promise((resolve) => {
|
|
200
|
+
const invoke = (one, input, env, caller) => new Promise((resolve) => {
|
|
199
201
|
let finish;
|
|
200
202
|
let held;
|
|
201
203
|
const connection = () => new Promise((lend, refuse) => {
|
|
@@ -219,6 +221,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
219
221
|
stdin,
|
|
220
222
|
connection,
|
|
221
223
|
environment: agentEnvironment,
|
|
224
|
+
caller,
|
|
222
225
|
}).then(async (answer) => {
|
|
223
226
|
finish?.("error" in answer && !harmless(answer.error.error.code));
|
|
224
227
|
await held?.catch(() => undefined);
|
|
@@ -226,12 +229,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
226
229
|
});
|
|
227
230
|
});
|
|
228
231
|
let queue = Promise.resolve();
|
|
229
|
-
const queued = (one, input, env) => {
|
|
230
|
-
const next = queue.then(() => invoke(one, input, env));
|
|
232
|
+
const queued = (one, input, env, caller) => {
|
|
233
|
+
const next = queue.then(() => invoke(one, input, env, caller));
|
|
231
234
|
queue = next.catch(() => undefined);
|
|
232
235
|
return next;
|
|
233
236
|
};
|
|
234
|
-
const asCommand = (one) => ({
|
|
237
|
+
const asCommand = (one, source) => ({
|
|
235
238
|
title: one.title,
|
|
236
239
|
description: one.description,
|
|
237
240
|
writes: one.writes,
|
|
@@ -243,7 +246,9 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
243
246
|
// The caller's trace reaches the run, its send journal lines and its record, as `TRACEPARENT` does.
|
|
244
247
|
const traceparent = ctx?.mcpReq?._meta?.traceparent;
|
|
245
248
|
const env = typeof traceparent === "string" ? { ...context.env, TRACEPARENT: traceparent } : context.env;
|
|
246
|
-
const
|
|
249
|
+
const caller = callerOf(ctx, keyOfCommand(one.command), source);
|
|
250
|
+
// The connection the session opens for this call is recorded under the same caller.
|
|
251
|
+
const answer = await withCaller(caller, () => queued(one, args, env, caller));
|
|
247
252
|
const result = "error" in answer
|
|
248
253
|
? mcpErrorResult(JSON.parse(agentJson(answer.error)))
|
|
249
254
|
: (() => {
|
|
@@ -266,6 +271,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
266
271
|
},
|
|
267
272
|
});
|
|
268
273
|
const build = () => {
|
|
274
|
+
// Built once for a stdio connection; over HTTP the session or the OAuth client identifies the caller instead.
|
|
275
|
+
const callers = {
|
|
276
|
+
env: context.env,
|
|
277
|
+
client: () => server.server.getClientVersion(),
|
|
278
|
+
connectionId: randomUUID(),
|
|
279
|
+
};
|
|
269
280
|
const server = new McpServer({ name: app.command, version: app.version }, {
|
|
270
281
|
instructions: instructions({
|
|
271
282
|
command: app.command,
|
|
@@ -305,11 +316,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
305
316
|
env: context.env,
|
|
306
317
|
...(context.keyring ? { keyring: context.keyring } : {}),
|
|
307
318
|
embedders,
|
|
308
|
-
history: settings.
|
|
319
|
+
history: settings.record,
|
|
309
320
|
},
|
|
321
|
+
callers,
|
|
310
322
|
};
|
|
311
323
|
registerCommands(server, app.command, {
|
|
312
|
-
...Object.fromEntries(commands.map((one) => [one.command, asCommand(one)])),
|
|
324
|
+
...Object.fromEntries(commands.map((one) => [one.command, asCommand(one, callers)])),
|
|
313
325
|
...Object.fromEntries(Object.entries(surfaceCommands({ ...pictures, status }, registration)).map(([name, one]) => [
|
|
314
326
|
name,
|
|
315
327
|
{
|
package/dist/mcp/surface.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { toJsonSchema, toStandardJsonSchema } from "@valibot/to-json-schema";
|
|
2
3
|
import { CliError } from "@wirecat/cli-base";
|
|
3
4
|
import * as v from "valibot";
|
|
4
5
|
import { answerSchema, listOf } from "../cli/envelope.js";
|
|
5
6
|
import { ENTITIES } from "../entities/index.js";
|
|
7
|
+
import { callerOf, currentCaller, withCaller } from "./caller.js";
|
|
6
8
|
import { agentArguments } from "./text.js";
|
|
7
9
|
import { answered, commandOf, entryRunner, failed, inputOf, outputSchemaOf, READ, syncAllowedFor, UNTRUSTED, UNTRUSTED_NEUTRAL, WRITE, } from "./tool.js";
|
|
8
10
|
const MATCHES = 8;
|
|
@@ -113,6 +115,12 @@ isMessenger) => {
|
|
|
113
115
|
});
|
|
114
116
|
return answered(list);
|
|
115
117
|
});
|
|
118
|
+
// A CLI's own server runs in its own process: its environment names the agent that started it on stdio.
|
|
119
|
+
const callers = {
|
|
120
|
+
env: process.env,
|
|
121
|
+
client: () => server.server.getClientVersion(),
|
|
122
|
+
connectionId: randomUUID(),
|
|
123
|
+
};
|
|
116
124
|
const runner = (writing) => async ({ command: asked, arguments: given }, ctx) => {
|
|
117
125
|
try {
|
|
118
126
|
const command = byNormal.get(normal(asked));
|
|
@@ -125,14 +133,15 @@ isMessenger) => {
|
|
|
125
133
|
if (one.writes !== writing)
|
|
126
134
|
throw new CliError("validation_error", `"${command}" ${writing ? "only reads" : "writes"}: call it with ${prefix}_${writing ? "read" : "write"}`);
|
|
127
135
|
const args = writing ? agentArguments(given ?? {}) : (given ?? {});
|
|
136
|
+
const caller = currentCaller() ?? callerOf(ctx, command.replace(/[ -]/g, "_"), callers);
|
|
128
137
|
if (one.checked)
|
|
129
|
-
return runnableOnly(await one.run(args, ctx), (named) => byNormal.has(normal(named)));
|
|
138
|
+
return runnableOnly(await withCaller(caller, () => one.run(args, ctx)), (named) => byNormal.has(normal(named)));
|
|
130
139
|
const parsed = v.safeParse(v.strictObject(one.input.entries), args);
|
|
131
140
|
if (!parsed.success)
|
|
132
141
|
throw new CliError("validation_error", `${command}: ${v.summarize(parsed.issues)}`, {
|
|
133
142
|
arguments: toJsonSchema(one.input),
|
|
134
143
|
});
|
|
135
|
-
return runnableOnly(await one.run(parsed.output, ctx), (named) => byNormal.has(normal(named)));
|
|
144
|
+
return runnableOnly(await withCaller(caller, () => one.run(parsed.output, ctx)), (named) => byNormal.has(normal(named)));
|
|
136
145
|
}
|
|
137
146
|
catch (error) {
|
|
138
147
|
return failed(error, errors);
|
package/dist/mcp/tool.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { SendGuard } from "../sends/guard.js";
|
|
|
13
13
|
import { type PermissionKey } from "../sends/permissions.js";
|
|
14
14
|
import { type Services } from "../services/index.js";
|
|
15
15
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
16
|
+
import { type CallerSource } from "./caller.js";
|
|
16
17
|
import type { MessengerSession } from "./session.js";
|
|
17
18
|
export declare const limit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 100, undefined>, v.DescriptionAction<number, "how many">]>, undefined>;
|
|
18
19
|
export declare const page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.DescriptionAction<number, "which page, from 1">]>, undefined>;
|
|
@@ -34,7 +35,7 @@ export interface Defaults {
|
|
|
34
35
|
limit: number;
|
|
35
36
|
guard: SendGuard;
|
|
36
37
|
/** The profile's own entries, for a tool reading a setting of its own — `transcribeWith`. */
|
|
37
|
-
settings: Pick<Settings, "configured" | "shared" | "profile"> & Partial<Pick<Settings, "permissions" | "searchCatchUp" | "pictureSearch" | "searchReranker" | "offline">> & AISettings;
|
|
38
|
+
settings: Pick<Settings, "configured" | "shared" | "profile"> & Partial<Pick<Settings, "permissions" | "searchCatchUp" | "pictureSearch" | "searchReranker" | "offline" | "record" | "keepFailedRuns" | "keepRunsForDays" | "keepSuccessfulRunsForDays" | "runsSizeLimitMb">> & AISettings;
|
|
38
39
|
env: NodeJS.ProcessEnv;
|
|
39
40
|
keyring?: KeyringStore;
|
|
40
41
|
/** The server's open models, kept between `search_conversations` calls. */
|
|
@@ -123,6 +124,8 @@ export interface Registration {
|
|
|
123
124
|
log?: (call: ToolCall) => Promise<void>;
|
|
124
125
|
/** The CLI's commands, by which an answer's next steps are judged safe to run unasked. */
|
|
125
126
|
targets?: NonNullable<StepInput["targets"]>;
|
|
127
|
+
/** How a call's caller is worked out on this connection (`callerOf`); without it the environment's agent is kept. */
|
|
128
|
+
callers?: CallerSource;
|
|
126
129
|
}
|
|
127
130
|
export type ToolTier = "read" | "draft" | "write-private" | "write-public" | "destructive" | "admin";
|
|
128
131
|
export interface ToolCall {
|
package/dist/mcp/tool.js
CHANGED
|
@@ -9,13 +9,14 @@ import { MAX_BUFFERED_INPUT } from "../cli/input-policy.js";
|
|
|
9
9
|
import { refuseLacking } from "../cli/messenger/lacks.js";
|
|
10
10
|
import { ENTITIES } from "../entities/index.js";
|
|
11
11
|
import { failureContext } from "../errors/reasons.js";
|
|
12
|
-
import {
|
|
12
|
+
import { startRecording } from "../runs/recording.js";
|
|
13
13
|
import { newRunId } from "../runs/run.js";
|
|
14
|
-
import { runContextOf } from "../runs/run-context.js";
|
|
14
|
+
import { keepsSuccess, runContextOf } from "../runs/run-context.js";
|
|
15
15
|
import { inRun } from "../runs/scope.js";
|
|
16
16
|
import { keyForCommand, levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
17
17
|
import { onlineDeps, storeModeDeps } from "../services/deps.js";
|
|
18
18
|
import { servicesFor } from "../services/index.js";
|
|
19
|
+
import { callerOf, currentCaller, withCaller } from "./caller.js";
|
|
19
20
|
import { agentJson } from "./text.js";
|
|
20
21
|
export const limit = v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100), v.description("how many")));
|
|
21
22
|
export const page = v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.description("which page, from 1")));
|
|
@@ -104,19 +105,25 @@ export const entryRunner = (registration) => {
|
|
|
104
105
|
const command = `mcp ${commandOf(key, definition)}`;
|
|
105
106
|
const traceId = traceIdOf(ctx?.mcpReq?._meta?.traceparent);
|
|
106
107
|
const app = registration.messenger?.app;
|
|
107
|
-
const
|
|
108
|
+
const caller = registration.callers ? callerOf(ctx, key, registration.callers) : currentCaller();
|
|
109
|
+
const env = registration.defaults.env ?? process.env;
|
|
110
|
+
const settings = registration.defaults.settings;
|
|
111
|
+
const recording = key !== "runs_search" && settings.keepFailedRuns !== false && app?.appName
|
|
108
112
|
? startRecording({
|
|
109
113
|
app,
|
|
110
114
|
command,
|
|
111
|
-
...(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
env: registration.defaults.env,
|
|
115
|
-
record: false,
|
|
115
|
+
...(settings.profile === undefined ? {} : { profile: settings.profile }),
|
|
116
|
+
env,
|
|
117
|
+
record: keepsSuccess({ record: settings.record ?? true, fromFlag: false, env, caller }),
|
|
116
118
|
keepFailed: true,
|
|
117
119
|
trace: false,
|
|
118
120
|
format: "json",
|
|
119
|
-
context: runContextOf({ bin: app.command, command, env
|
|
121
|
+
context: runContextOf({ bin: app.command, command, env, caller }),
|
|
122
|
+
...(settings.keepRunsForDays === undefined ? {} : { keepDays: settings.keepRunsForDays }),
|
|
123
|
+
...(settings.keepSuccessfulRunsForDays === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { keepSuccessDays: settings.keepSuccessfulRunsForDays }),
|
|
126
|
+
...(settings.runsSizeLimitMb === undefined ? {} : { maxBytes: settings.runsSizeLimitMb * 1024 * 1024 }),
|
|
120
127
|
...(traceId === undefined ? {} : { traceId }),
|
|
121
128
|
})
|
|
122
129
|
: undefined;
|
|
@@ -127,8 +134,9 @@ export const entryRunner = (registration) => {
|
|
|
127
134
|
startedAt,
|
|
128
135
|
...(profile === undefined ? {} : { profile }),
|
|
129
136
|
...(traceId === undefined ? {} : { traceId }),
|
|
137
|
+
...(recording ? { events: recording.events } : {}),
|
|
130
138
|
};
|
|
131
|
-
const
|
|
139
|
+
const work = () => inRun(scope, () => {
|
|
132
140
|
const input = inputOf(definition, syncAllowedFor(key, registration.defaults));
|
|
133
141
|
const parsed = v.safeParse(input, args);
|
|
134
142
|
return parsed.success
|
|
@@ -137,7 +145,9 @@ export const entryRunner = (registration) => {
|
|
|
137
145
|
arguments: toJsonSchema(input),
|
|
138
146
|
}), app?.errors);
|
|
139
147
|
});
|
|
140
|
-
|
|
148
|
+
// The connection a session opens for this call is recorded under the same caller.
|
|
149
|
+
const result = await (caller ? withCaller(caller, work) : work());
|
|
150
|
+
if (recording) {
|
|
141
151
|
if (result.isError) {
|
|
142
152
|
const error = result.structuredContent?.error;
|
|
143
153
|
const code = error?.code && errorCodes.includes(error.code)
|
package/dist/runs/events.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* One operation against the messenger, as both sinks see it: `--trace` renders it, a recorded run
|
|
3
3
|
* writes it as a line of JSON.
|
|
4
4
|
*
|
|
5
|
-
* ⚠ **Nothing here may carry content.** An operation name, an id, a count, a duration and an
|
|
6
|
-
* code are safe
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* ⚠ **Nothing here may carry content.** An operation name, an id, a file or folder path, a count, a duration and an
|
|
6
|
+
* error code are safe: a run record stays on the owner's machine, and keeping the ids that tell runs apart was the
|
|
7
|
+
* owner's choice (NEED-110, 2026-10-27). A chat title, a name, a message body, a phone number, a token, a URL as sent
|
|
8
|
+
* or a query string are not — not truncated and not hashed. The adapter builds `ids` and `counts` from fields it names,
|
|
9
|
+
* never by copying a request, and never from what the person typed: a typed chat is often a title. Anything that
|
|
10
|
+
* leaves the machine (a bug report) strips the ids too.
|
|
9
11
|
*/
|
|
10
12
|
export interface RequestEvent {
|
|
11
13
|
event: "request" | "response";
|
|
@@ -30,6 +32,16 @@ export interface RequestEvent {
|
|
|
30
32
|
errorCode?: string;
|
|
31
33
|
/** The messenger's own name for a refusal, when it is shaped like one (`providerErrorKey`). */
|
|
32
34
|
providerError?: string;
|
|
35
|
+
/**
|
|
36
|
+
* `operation` for one adapter call (`observed`), `wire` for one call on the network under it. A run counts wire
|
|
37
|
+
* requests when it has any, so one operation over three frames is three requests, never four.
|
|
38
|
+
*/
|
|
39
|
+
level?: "operation" | "wire";
|
|
40
|
+
transport?: "http" | "mtproto" | "max-ws" | "wa-engine";
|
|
41
|
+
/** 1 for the first try, 2 and up for a retry of the same call. */
|
|
42
|
+
attempt?: number;
|
|
43
|
+
/** How long the client waited before this attempt: a flood wait or a `Retry-After`. */
|
|
44
|
+
waitMs?: number;
|
|
33
45
|
}
|
|
34
46
|
/** Something the client noticed and went on without. A code, never the sentence. */
|
|
35
47
|
export interface WarningEvent {
|
package/dist/runs/events.js
CHANGED
|
@@ -28,6 +28,10 @@ export const renderEvent = (event) => {
|
|
|
28
28
|
parts.push([event.method, event.endpointTemplate].filter(Boolean).join(" "));
|
|
29
29
|
if (event.status !== undefined)
|
|
30
30
|
parts.push(`${event.status}`);
|
|
31
|
+
if (event.attempt !== undefined && event.attempt > 1)
|
|
32
|
+
parts.push(`attempt ${event.attempt}`);
|
|
33
|
+
if (event.waitMs !== undefined)
|
|
34
|
+
parts.push(`waited ${age(event.waitMs)}`);
|
|
31
35
|
}
|
|
32
36
|
if (event.ids)
|
|
33
37
|
for (const [name, value] of Object.entries(event.ids))
|
|
@@ -16,6 +16,10 @@ const diagnostic = {
|
|
|
16
16
|
outcome: true,
|
|
17
17
|
errorCode: true,
|
|
18
18
|
providerError: true,
|
|
19
|
+
level: true,
|
|
20
|
+
transport: true,
|
|
21
|
+
attempt: true,
|
|
22
|
+
waitMs: true,
|
|
19
23
|
code: true,
|
|
20
24
|
detail: true,
|
|
21
25
|
reason: true,
|
|
@@ -63,12 +67,14 @@ const background = [
|
|
|
63
67
|
];
|
|
64
68
|
/** A crash: where it happened, never what it said. */
|
|
65
69
|
const crash = ["errorClass", "frames"];
|
|
70
|
+
/** What a capped run did not write: how many, by operation. */
|
|
71
|
+
const dropped = ["dropped"];
|
|
66
72
|
/**
|
|
67
73
|
* Every attribute key cli-core's writers log, as written: snake_case (`logAttributes`). `createRecordLog`
|
|
68
74
|
* blanks any other, cli-base's own camelCase list included.
|
|
69
75
|
*/
|
|
70
76
|
export const LOG_ATTRIBUTES = [
|
|
71
|
-
...new Set([...Object.keys(diagnostic), ...Object.keys(send), ...background, ...crash].map((key) => Object.keys(logAttributes({ [key]: true }))[0])),
|
|
77
|
+
...new Set([...Object.keys(diagnostic), ...Object.keys(send), ...background, ...crash, ...dropped].map((key) => Object.keys(logAttributes({ [key]: true }))[0])),
|
|
72
78
|
].sort();
|
|
73
79
|
/** For `cli-dev lint --schemas`, under the name `log_attributes`. */
|
|
74
80
|
export const logAttributesSchema = () => logAttributesJsonSchema(LOG_ATTRIBUTES);
|
package/dist/runs/log-events.js
CHANGED
|
@@ -10,6 +10,11 @@ export const CORE_LOG_EVENTS = defineLogEvents([
|
|
|
10
10
|
keys: ["operation", "reason"],
|
|
11
11
|
},
|
|
12
12
|
{ id: "command.crashed", meaning: "a command failed on a bug; where, never what", keys: ["error_class", "frames"] },
|
|
13
|
+
{
|
|
14
|
+
id: "events.dropped",
|
|
15
|
+
meaning: "the run reached its cap and counted the rest instead of writing them",
|
|
16
|
+
keys: ["dropped", "counts"],
|
|
17
|
+
},
|
|
13
18
|
{ id: "send.recorded", meaning: "a write the send guard saw, and how it ended", keys: ["outcome"] },
|
|
14
19
|
{ id: "job.start", meaning: "a background job started", keys: ["job"] },
|
|
15
20
|
{ id: "job.exit", meaning: "a background job ended", keys: ["job", "outcome"] },
|
package/dist/runs/recording.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface RecordingOptions {
|
|
|
18
18
|
env?: NodeJS.ProcessEnv;
|
|
19
19
|
runsDir?: string;
|
|
20
20
|
keepDays?: number;
|
|
21
|
+
keepSuccessDays?: number;
|
|
22
|
+
maxBytes?: number;
|
|
21
23
|
now?: () => Date;
|
|
22
24
|
/** The caller's trace; otherwise the enclosing run's, otherwise `TRACEPARENT` from `env`. */
|
|
23
25
|
traceId?: string;
|
|
@@ -27,13 +29,15 @@ export interface RecordingOptions {
|
|
|
27
29
|
*/
|
|
28
30
|
processRun?: boolean;
|
|
29
31
|
/** The whole command, its masked arguments and the agent that started it. */
|
|
30
|
-
context?: RunContext
|
|
32
|
+
context?: RunContext & {
|
|
33
|
+
argvIfFailed?: string[];
|
|
34
|
+
};
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
34
38
|
*
|
|
35
|
-
* `--trace` shows each event as it happens
|
|
36
|
-
*
|
|
39
|
+
* `--trace` shows each event as it happens; the run is kept as the config says — every run unless
|
|
40
|
+
* `record: false`, which keeps failures only. It is a wrapper rather than three lines in each command because the failure it prevents
|
|
37
41
|
* is the quiet one: a run directory left saying `running` because the command threw on the way out.
|
|
38
42
|
*/
|
|
39
43
|
export declare const recorded: <T>(options: RecordingOptions, body: (events: EventSink) => Promise<T>) => Promise<T>;
|
|
@@ -49,6 +53,9 @@ export interface Recording {
|
|
|
49
53
|
/** `recorded` in two halves, for a caller that starts a run before its command and ends it after (max-cli's `max bot`). */
|
|
50
54
|
export declare const startRecording: (options: RecordingOptions) => Recording;
|
|
51
55
|
export declare const wasSettled: (error: unknown) => boolean;
|
|
56
|
+
/** A run of a loop that never ends must not fill the disk: past these, events are counted by operation instead. */
|
|
57
|
+
export declare const EVENTS_AT_MOST = 2000;
|
|
58
|
+
export declare const EVENT_BYTES_AT_MOST: number;
|
|
52
59
|
/**
|
|
53
60
|
* Where a crash happened, not what it said: the class and up to ten frames as `function file:line`,
|
|
54
61
|
* paths cut to the package's own (`framesOf`). **Never `message`** in an event — it can hold whatever the
|
package/dist/runs/recording.js
CHANGED
|
@@ -9,8 +9,8 @@ import { enclosingTraceId, inRun, traceOf, useAsProcessRun } from "./scope.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
11
11
|
*
|
|
12
|
-
* `--trace` shows each event as it happens
|
|
13
|
-
*
|
|
12
|
+
* `--trace` shows each event as it happens; the run is kept as the config says — every run unless
|
|
13
|
+
* `record: false`, which keeps failures only. It is a wrapper rather than three lines in each command because the failure it prevents
|
|
14
14
|
* is the quiet one: a run directory left saying `running` because the command threw on the way out.
|
|
15
15
|
*/
|
|
16
16
|
export const recorded = async (options, body) => {
|
|
@@ -22,6 +22,7 @@ export const recorded = async (options, body) => {
|
|
|
22
22
|
startedAt: Date.now(),
|
|
23
23
|
...(options.profile === undefined ? {} : { profile: options.profile }),
|
|
24
24
|
...(recording.traceId ? { traceId: recording.traceId } : {}),
|
|
25
|
+
events: recording.events,
|
|
25
26
|
};
|
|
26
27
|
if (options.processRun)
|
|
27
28
|
useAsProcessRun(scope);
|
|
@@ -41,6 +42,7 @@ export const startRecording = (options) => {
|
|
|
41
42
|
const startedAt = now();
|
|
42
43
|
const runId = newRunId(startedAt, options.command);
|
|
43
44
|
const traceId = options.traceId ?? enclosingTraceId() ?? traceOf(options.env ?? process.env).traceId;
|
|
45
|
+
const { argvIfFailed, ...context } = options.context ?? {};
|
|
44
46
|
const open = (extra = {}) => startRun({
|
|
45
47
|
runsDir: options.runsDir ?? runsDirFor(options.app, options.env),
|
|
46
48
|
runId,
|
|
@@ -49,7 +51,7 @@ export const startRecording = (options) => {
|
|
|
49
51
|
service: options.app.command,
|
|
50
52
|
cliVersion: options.app.version,
|
|
51
53
|
...(traceId === undefined ? {} : { traceId }),
|
|
52
|
-
...(options.context ? { context
|
|
54
|
+
...(options.context ? { context } : {}),
|
|
53
55
|
// The operation has happened or is under way, and failing the command now invites a retry
|
|
54
56
|
// that sends twice.
|
|
55
57
|
onError: (error) => {
|
|
@@ -57,47 +59,65 @@ export const startRecording = (options) => {
|
|
|
57
59
|
streams.diagnostic(options.format === "pretty" ? warning : JSON.stringify({ warning }));
|
|
58
60
|
},
|
|
59
61
|
...(options.keepDays === undefined ? {} : { keepDays: options.keepDays }),
|
|
62
|
+
...(options.keepSuccessDays === undefined ? {} : { keepSuccessDays: options.keepSuccessDays }),
|
|
63
|
+
...(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }),
|
|
60
64
|
now,
|
|
61
65
|
...extra,
|
|
62
66
|
});
|
|
63
|
-
const run = options.record ? open({ startedAt }) : undefined;
|
|
67
|
+
const run = options.record ? open({ startedAt, lazy: true }) : undefined;
|
|
64
68
|
// Unrecorded, the newest events are held until the outcome is known, so a failure can still be kept.
|
|
65
69
|
const held = !run && options.keepFailed ? [] : undefined;
|
|
66
|
-
|
|
70
|
+
const counted = { wire: 0, operation: 0 };
|
|
67
71
|
let refused;
|
|
72
|
+
const cap = capped();
|
|
73
|
+
const requests = () => (counted.wire > 0 ? counted.wire : counted.operation);
|
|
68
74
|
const events = (event) => {
|
|
69
75
|
if (event.event === "request")
|
|
70
|
-
|
|
76
|
+
counted[event.level === "wire" ? "wire" : "operation"] += 1;
|
|
71
77
|
if (event.event === "response" && event.outcome === "error")
|
|
72
78
|
refused = event;
|
|
73
79
|
if (options.trace)
|
|
74
80
|
streams.diagnostic(options.format === "pretty" ? renderEvent(event) : JSON.stringify(logAttributes(event)));
|
|
75
81
|
if (run)
|
|
76
|
-
|
|
82
|
+
cap.log(run, event);
|
|
77
83
|
if (held) {
|
|
78
84
|
held.push(event);
|
|
79
85
|
if (held.length > HELD_AT_MOST)
|
|
80
86
|
held.shift();
|
|
81
87
|
}
|
|
82
88
|
};
|
|
89
|
+
const keep = (kept) => {
|
|
90
|
+
for (const event of held ?? [])
|
|
91
|
+
cap.log(kept, event);
|
|
92
|
+
return kept;
|
|
93
|
+
};
|
|
83
94
|
return {
|
|
84
95
|
runId,
|
|
85
96
|
...(traceId === undefined ? {} : { traceId }),
|
|
86
97
|
events,
|
|
87
98
|
succeed: async (answer) => {
|
|
88
99
|
const partial = partialOutcome(answer);
|
|
89
|
-
const kept = run ?? (partial && held ? keep(open({ startedAt })
|
|
90
|
-
|
|
100
|
+
const kept = run ?? (partial && held ? keep(open({ startedAt })) : undefined);
|
|
101
|
+
if (kept)
|
|
102
|
+
cap.close(kept);
|
|
103
|
+
await kept?.finish(partial ? "partial" : "success", {
|
|
104
|
+
requests: requests(),
|
|
105
|
+
...cap.dropped(),
|
|
106
|
+
...(partial ? { partial } : {}),
|
|
107
|
+
});
|
|
91
108
|
},
|
|
92
109
|
fail: async (error) => {
|
|
93
|
-
const failed = run ?? (held ? keep(open({ startedAt })
|
|
110
|
+
const failed = run ?? (held ? keep(open({ startedAt })) : undefined);
|
|
94
111
|
if (failed) {
|
|
95
112
|
if (!isCliFailure(error))
|
|
96
113
|
failed.logger.error(crashOf(error));
|
|
114
|
+
cap.close(failed);
|
|
97
115
|
await failed.finish("failed", {
|
|
98
|
-
requests,
|
|
116
|
+
requests: requests(),
|
|
117
|
+
...cap.dropped(),
|
|
99
118
|
...outcomeOf(error),
|
|
100
119
|
error: errorDetailOf(error, refused),
|
|
120
|
+
...(argvIfFailed ? { argv: argvIfFailed } : {}),
|
|
101
121
|
...(run ? {} : { keptBecauseFailed: true }),
|
|
102
122
|
});
|
|
103
123
|
}
|
|
@@ -110,10 +130,34 @@ export const startRecording = (options) => {
|
|
|
110
130
|
const settled = new WeakSet();
|
|
111
131
|
export const wasSettled = (error) => typeof error === "object" && error !== null && settled.has(error);
|
|
112
132
|
const HELD_AT_MOST = 500;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
/** A run of a loop that never ends must not fill the disk: past these, events are counted by operation instead. */
|
|
134
|
+
export const EVENTS_AT_MOST = 2000;
|
|
135
|
+
export const EVENT_BYTES_AT_MOST = 1024 * 1024;
|
|
136
|
+
const capped = () => {
|
|
137
|
+
let lines = 0;
|
|
138
|
+
let bytes = 0;
|
|
139
|
+
let dropped = 0;
|
|
140
|
+
const byOperation = {};
|
|
141
|
+
return {
|
|
142
|
+
log: (run, event) => {
|
|
143
|
+
// The record's own envelope (time, ids of the run, resource) is about 200 bytes on top of its attributes.
|
|
144
|
+
const size = JSON.stringify(logAttributes(event)).length + 200;
|
|
145
|
+
if (lines >= EVENTS_AT_MOST || bytes + size > EVENT_BYTES_AT_MOST) {
|
|
146
|
+
dropped += 1;
|
|
147
|
+
const operation = "operation" in event && event.operation ? event.operation : event.event;
|
|
148
|
+
byOperation[operation] = (byOperation[operation] ?? 0) + 1;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
lines += 1;
|
|
152
|
+
bytes += size;
|
|
153
|
+
logEvent(run, event);
|
|
154
|
+
},
|
|
155
|
+
close: (run) => {
|
|
156
|
+
if (dropped > 0)
|
|
157
|
+
run.logger.warn({ event: "events.dropped", dropped, counts: byOperation });
|
|
158
|
+
},
|
|
159
|
+
dropped: () => (dropped > 0 ? { eventsDropped: dropped } : {}),
|
|
160
|
+
};
|
|
117
161
|
};
|
|
118
162
|
const logEvent = (run, event) => {
|
|
119
163
|
const record = {
|
|
@@ -7,7 +7,54 @@ export interface RunAgent {
|
|
|
7
7
|
session_id: string | null;
|
|
8
8
|
/** Set, and only `true`, when a chat message started the agent's turn; who sent it is never kept. */
|
|
9
9
|
from_chat?: true;
|
|
10
|
+
/** The MCP client that made the call, as it named itself; `id` is `clientHash` of an OAuth client's id. */
|
|
11
|
+
client?: RunClient;
|
|
10
12
|
}
|
|
13
|
+
export interface RunClient {
|
|
14
|
+
name: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
}
|
|
18
|
+
/** `record_input`: the words typed are kept (`true`), masked (`false`), or kept only in a run that failed. */
|
|
19
|
+
export type RecordInput = boolean | "failed";
|
|
20
|
+
/**
|
|
21
|
+
* Whether a run that succeeds is kept. Every run is, unless the config says `record: false` — then only failures —
|
|
22
|
+
* or WireCat Desktop ran the command for its own pages (`WIRECAT_CALLER=desktop`), or the command runs until stopped
|
|
23
|
+
* (`watch`, `serve`, `mcp`): those two only on `--record`. An MCP call is the caller's run, not Desktop's.
|
|
24
|
+
*/
|
|
25
|
+
export declare const keepsSuccess: ({ record, fromFlag, env, caller, persistent, }: {
|
|
26
|
+
record: boolean;
|
|
27
|
+
fromFlag: boolean;
|
|
28
|
+
env: NodeJS.ProcessEnv;
|
|
29
|
+
caller?: RunCaller | undefined;
|
|
30
|
+
persistent?: boolean;
|
|
31
|
+
}) => boolean;
|
|
32
|
+
/** How the run was asked for: typed or scripted, an MCP client on stdio or over HTTP, or WireCat Desktop. */
|
|
33
|
+
export type RunVia = "cli" | "mcp-stdio" | "mcp-http" | "desktop";
|
|
34
|
+
/** The MCP call a run answered. `connection_id` groups the calls of one connection when no agent session does. */
|
|
35
|
+
export interface RunMcp {
|
|
36
|
+
tool: string;
|
|
37
|
+
request_id?: string;
|
|
38
|
+
connection_id?: string;
|
|
39
|
+
/** The every-profile server passed the call on; the caller is the one it named. */
|
|
40
|
+
proxied?: true;
|
|
41
|
+
}
|
|
42
|
+
/** Who asked for a run, worked out by whoever received the call; it wins over the process's environment. */
|
|
43
|
+
export interface RunCaller {
|
|
44
|
+
via: RunVia;
|
|
45
|
+
agent?: RunAgent;
|
|
46
|
+
mcp?: RunMcp;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Runs one MCP call with its caller known, so every run opened under it names that caller: the command's own, and
|
|
50
|
+
* the connection a server opens for it, which runs under the server's context.
|
|
51
|
+
*/
|
|
52
|
+
export declare const withCaller: <T>(caller: RunCaller, work: () => T) => T;
|
|
53
|
+
export declare const currentCaller: () => RunCaller | undefined;
|
|
54
|
+
/** An OAuth client id is a bearer of sorts: a run keeps eight hex characters of its SHA-256, enough to tell apps apart. */
|
|
55
|
+
export declare const clientHash: (id: string) => string;
|
|
56
|
+
/** `WIRECAT_CALLER=desktop` is set by WireCat Desktop on the commands it runs for its own pages. */
|
|
57
|
+
export declare const viaOf: (env: NodeJS.ProcessEnv) => RunVia;
|
|
11
58
|
/**
|
|
12
59
|
* The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
|
|
13
60
|
* `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Hermes
|
|
@@ -43,13 +90,17 @@ export declare const maskArgv: (program: Command, words: readonly string[], { ke
|
|
|
43
90
|
* that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
|
|
44
91
|
* keeps no command line and no agent.
|
|
45
92
|
*/
|
|
46
|
-
export declare const runContextOf: ({ bin, command, program, argv, env, recordInput, }: {
|
|
93
|
+
export declare const runContextOf: ({ bin, command, program, argv, env, recordInput, caller, }: {
|
|
47
94
|
bin: string;
|
|
48
95
|
/** The run's own name, `messages list` or `mcp messages list`; empty when no command was recognised. */
|
|
49
96
|
command: string;
|
|
50
97
|
program?: Command;
|
|
51
98
|
argv?: readonly string[];
|
|
52
99
|
env: NodeJS.ProcessEnv;
|
|
53
|
-
/** The owner's `record_input`; left out, every value that may be content is masked. */
|
|
54
|
-
recordInput?:
|
|
55
|
-
|
|
100
|
+
/** The owner's `record_input`; left out, every value that may be content is masked. `failed` keeps them only then. */
|
|
101
|
+
recordInput?: RecordInput | undefined;
|
|
102
|
+
/** Who asked, when the receiver knows better than the environment: an MCP call. */
|
|
103
|
+
caller?: RunCaller | undefined;
|
|
104
|
+
}) => RunContext & {
|
|
105
|
+
argvIfFailed?: string[];
|
|
106
|
+
};
|
package/dist/runs/run-context.js
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
1
3
|
import { isContentKey } from "@wirecat/cli-base/standards";
|
|
2
4
|
import { BARE_PHONE, INTERNATIONAL_PHONE } from "./error-detail.js";
|
|
3
5
|
import { secretNamed, withoutSecrets } from "./secrets.js";
|
|
4
6
|
export const REDACTED = "[redacted]";
|
|
7
|
+
/**
|
|
8
|
+
* Whether a run that succeeds is kept. Every run is, unless the config says `record: false` — then only failures —
|
|
9
|
+
* or WireCat Desktop ran the command for its own pages (`WIRECAT_CALLER=desktop`), or the command runs until stopped
|
|
10
|
+
* (`watch`, `serve`, `mcp`): those two only on `--record`. An MCP call is the caller's run, not Desktop's.
|
|
11
|
+
*/
|
|
12
|
+
export const keepsSuccess = ({ record, fromFlag, env, caller, persistent = false, }) => {
|
|
13
|
+
if (!record)
|
|
14
|
+
return false;
|
|
15
|
+
if (fromFlag)
|
|
16
|
+
return true;
|
|
17
|
+
if (persistent)
|
|
18
|
+
return false;
|
|
19
|
+
return caller !== undefined || viaOf(env) !== "desktop";
|
|
20
|
+
};
|
|
21
|
+
const calls = new AsyncLocalStorage();
|
|
22
|
+
/**
|
|
23
|
+
* Runs one MCP call with its caller known, so every run opened under it names that caller: the command's own, and
|
|
24
|
+
* the connection a server opens for it, which runs under the server's context.
|
|
25
|
+
*/
|
|
26
|
+
export const withCaller = (caller, work) => calls.run(caller, work);
|
|
27
|
+
export const currentCaller = () => calls.getStore();
|
|
28
|
+
/** An OAuth client id is a bearer of sorts: a run keeps eight hex characters of its SHA-256, enough to tell apps apart. */
|
|
29
|
+
export const clientHash = (id) => createHash("sha256").update(id).digest("hex").slice(0, 8);
|
|
30
|
+
/** `WIRECAT_CALLER=desktop` is set by WireCat Desktop on the commands it runs for its own pages. */
|
|
31
|
+
export const viaOf = (env) => (env.WIRECAT_CALLER?.trim() === "desktop" ? "desktop" : "cli");
|
|
5
32
|
/**
|
|
6
33
|
* The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
|
|
7
34
|
* `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Hermes
|
|
@@ -176,13 +203,17 @@ const safeShape = (value) => (NUMBER.test(value) && !/^\d{11,}$/.test(value)) ||
|
|
|
176
203
|
* that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
|
|
177
204
|
* keeps no command line and no agent.
|
|
178
205
|
*/
|
|
179
|
-
export const runContextOf = ({ bin, command, program, argv, env, recordInput = false, }) => {
|
|
180
|
-
const masked = program && argv ? maskArgv(program, argv, { keep: recordInput }) : undefined;
|
|
181
|
-
const
|
|
206
|
+
export const runContextOf = ({ bin, command, program, argv, env, recordInput = false, caller, }) => {
|
|
207
|
+
const masked = program && argv ? maskArgv(program, argv, { keep: recordInput === true }) : undefined;
|
|
208
|
+
const ifFailed = program && argv && recordInput === "failed" ? maskArgv(program, argv, { keep: true }) : undefined;
|
|
209
|
+
const agent = caller ? caller.agent : agentOf(env);
|
|
182
210
|
return {
|
|
183
211
|
cliCommand: command ? `${bin} ${command}` : bin,
|
|
184
212
|
...(masked ? { argv: masked.argv } : {}),
|
|
213
|
+
...(ifFailed ? { argvIfFailed: ifFailed.argv } : {}),
|
|
185
214
|
...(masked?.unknownCommand === undefined ? {} : { unknownCommand: masked.unknownCommand }),
|
|
186
215
|
...(agent ? { agent } : {}),
|
|
216
|
+
via: caller?.via ?? viaOf(env),
|
|
217
|
+
...(caller?.mcp ? { mcp: caller.mcp } : {}),
|
|
187
218
|
};
|
|
188
219
|
};
|