@wirecat/cli-core 0.466.0 → 0.468.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 +64 -0
- package/README.md +4 -2
- package/dist/cli/bot/mcp.js +2 -1
- package/dist/cli/command-runs.d.ts +38 -10
- package/dist/cli/command-runs.js +51 -17
- package/dist/cli/config-command.d.ts +4 -0
- package/dist/cli/config-command.js +5 -5
- package/dist/cli/context.d.ts +3 -0
- package/dist/cli/context.js +15 -3
- package/dist/cli/effects.d.ts +9 -0
- package/dist/cli/effects.js +27 -0
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +2 -2
- 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 +12 -5
- package/dist/cli/settings.d.ts +24 -6
- package/dist/cli/settings.js +26 -4
- package/dist/cli/shared-command-exports.d.ts +12 -4
- package/dist/cli/skill-command.js +11 -3
- package/dist/cli/steps.d.ts +9 -0
- package/dist/cli/steps.js +16 -0
- package/dist/cli/store-settings.d.ts +6 -0
- package/dist/cli/store-settings.js +6 -0
- 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 +5 -0
- package/dist/mcp/agent.js +3 -1
- package/dist/mcp/all-profiles.d.ts +2 -0
- package/dist/mcp/all-profiles.js +16 -3
- package/dist/mcp/bot/server.d.ts +2 -0
- package/dist/mcp/bot/server.js +9 -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 +21 -8
- 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/embeddings-maintenance.js +4 -6
- package/dist/services/patience.js +5 -1
- package/package.json +1 -1
|
@@ -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
|
};
|
package/dist/runs/run.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Logger } from "@wirecat/cli-base";
|
|
2
2
|
import type { AppIdentity } from "../ports/app.js";
|
|
3
3
|
import type { RunError } from "./error-detail.js";
|
|
4
|
-
import type { RunAgent } from "./run-context.js";
|
|
4
|
+
import type { RunAgent, RunMcp, RunVia } from "./run-context.js";
|
|
5
5
|
export type RunStatus = "success" | "failed" | "partial";
|
|
6
6
|
/**
|
|
7
7
|
* What a run was, and how it ended. **It never says what was read or sent** — a command, a profile,
|
|
@@ -18,8 +18,10 @@ export interface RunMetadata {
|
|
|
18
18
|
startedAt: string;
|
|
19
19
|
completedAt?: string;
|
|
20
20
|
status: RunStatus | "running";
|
|
21
|
-
/** How many
|
|
21
|
+
/** How many calls reached the messenger: wire calls when the transport logs them, otherwise operations. */
|
|
22
22
|
requests?: number;
|
|
23
|
+
/** Events past the run's cap, counted rather than written; an `events.dropped` line says which. */
|
|
24
|
+
eventsDropped?: number;
|
|
23
25
|
durationMs?: number;
|
|
24
26
|
cliVersion: string;
|
|
25
27
|
/** `node 24.1.0` or `bun 1.3.14`, and where: what a problem report is asked for first. */
|
|
@@ -53,13 +55,17 @@ export interface RunMetadata {
|
|
|
53
55
|
argv?: string[];
|
|
54
56
|
/** The word that named no command, when that is why the run failed. */
|
|
55
57
|
unknownCommand?: string;
|
|
56
|
-
/** The coding agent that started it, when one did. */
|
|
58
|
+
/** The coding agent that started it, when one did, and the MCP client that made the call. */
|
|
57
59
|
agent?: RunAgent;
|
|
60
|
+
/** How it was asked for (`viaOf`, or the MCP server that received it). */
|
|
61
|
+
via?: RunVia;
|
|
62
|
+
/** The MCP call it answered. */
|
|
63
|
+
mcp?: RunMcp;
|
|
58
64
|
/** How a failed run failed (`errorDetailOf`). */
|
|
59
65
|
error?: RunError;
|
|
60
66
|
}
|
|
61
67
|
/** Who started a run and with what, known before it starts. */
|
|
62
|
-
export type RunContext = Pick<RunMetadata, "cliCommand" | "argv" | "unknownCommand" | "agent">;
|
|
68
|
+
export type RunContext = Pick<RunMetadata, "cliCommand" | "argv" | "unknownCommand" | "agent" | "via" | "mcp">;
|
|
63
69
|
export interface StartRunOptions {
|
|
64
70
|
runsDir: string;
|
|
65
71
|
command: string;
|
|
@@ -69,6 +75,15 @@ export interface StartRunOptions {
|
|
|
69
75
|
cliVersion: string;
|
|
70
76
|
/** Older runs are removed as this one starts. */
|
|
71
77
|
keepDays?: number;
|
|
78
|
+
/** Successful runs older than this are removed sooner than the failures `keepDays` keeps. */
|
|
79
|
+
keepSuccessDays?: number;
|
|
80
|
+
/** The runs folder's size limit in bytes: past it, the oldest successful runs go first, then the oldest days. */
|
|
81
|
+
maxBytes?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Opens the directory only when there is something to put in it: an event, or an ending other than a plain
|
|
84
|
+
* success. A success with no events is then one line in the day's index (`runs show` reads it from there).
|
|
85
|
+
*/
|
|
86
|
+
lazy?: boolean;
|
|
72
87
|
runId?: string;
|
|
73
88
|
now?: () => Date;
|
|
74
89
|
/** When the command began, if that was before the run was opened — a failure kept after the fact. */
|
|
@@ -90,6 +105,8 @@ export interface Run {
|
|
|
90
105
|
finish(status: RunStatus, extra?: Partial<RunMetadata>): Promise<void>;
|
|
91
106
|
}
|
|
92
107
|
export declare const KEEP_RUNS_FOR_DAYS = 30;
|
|
108
|
+
export declare const KEEP_SUCCESSFUL_RUNS_FOR_DAYS = 7;
|
|
109
|
+
export declare const RUNS_SIZE_LIMIT_MB = 200;
|
|
93
110
|
/** `<state dir>/runs` — never the cache directory: a cache is deleted to fix a cache, and a run cannot be rebuilt. */
|
|
94
111
|
export declare const runsDirFor: (app: AppIdentity, env?: NodeJS.ProcessEnv) => string;
|
|
95
112
|
/**
|
|
@@ -101,14 +118,19 @@ export declare const runsDirFor: (app: AppIdentity, env?: NodeJS.ProcessEnv) =>
|
|
|
101
118
|
* events.jsonl one log record per event (cli-base's `LogRecord`), no ANSI ever
|
|
102
119
|
* ```
|
|
103
120
|
*
|
|
104
|
-
* `run.json` is written
|
|
121
|
+
* `run.json` is written as the directory opens, with `status: "running"`, and again by `finish`. A directory
|
|
105
122
|
* holding events and no metadata is a special case `runs list` would carry forever.
|
|
106
123
|
*
|
|
107
124
|
* Each write also appends the metadata to the day's `index.jsonl`, which `runs list` and `search runs`
|
|
108
|
-
* read instead of every `run.json`. It is only
|
|
109
|
-
* the slow way.
|
|
125
|
+
* read instead of every `run.json`. It is the only record of a `lazy` run that succeeded without events.
|
|
110
126
|
*/
|
|
111
127
|
export declare const startRun: (options: StartRunOptions) => Run;
|
|
128
|
+
/**
|
|
129
|
+
* The test sandbox moves HOME and the XDG folders, so a run that still resolves into the owner's own data folder is a
|
|
130
|
+
* fixture that bypassed it: thousands of test runs once landed beside the owner's real ones. The passwd entry's home
|
|
131
|
+
* is read because HOME is the very thing the sandbox changed.
|
|
132
|
+
*/
|
|
133
|
+
export declare const refuseRealStateInTests: (dir: string, env?: NodeJS.ProcessEnv) => void;
|
|
112
134
|
export declare const runtime: () => string;
|
|
113
135
|
/** `20260920T084431Z-chats-list-a81f2c` — sortable, and readable without opening it. */
|
|
114
136
|
export declare const newRunId: (startedAt: Date, command: string) => string;
|
|
@@ -138,12 +160,16 @@ export declare const findRun: (runsDir: string, id: string) => {
|
|
|
138
160
|
*/
|
|
139
161
|
export declare const readEvents: (dir: string) => Record<string, unknown>[];
|
|
140
162
|
/**
|
|
141
|
-
* Removes every day older than `keepDays`,
|
|
163
|
+
* Removes every day older than `keepDays`, then the successful runs of every day older than `keepSuccessDays`, and
|
|
164
|
+
* answers how many days went. Then, once a day, the folder is measured against `maxBytes`: successful runs go first,
|
|
165
|
+
* oldest day first, then whole days, until it fits.
|
|
142
166
|
*
|
|
143
|
-
* A
|
|
144
|
-
*
|
|
167
|
+
* A day at a time, by the directory name and the day's index, so no run has to be opened to decide — and a directory
|
|
168
|
+
* that is not a date is left alone rather than guessed at.
|
|
145
169
|
*/
|
|
146
|
-
export declare const pruneRuns: (runsDir: string, { keepDays, now }: {
|
|
170
|
+
export declare const pruneRuns: (runsDir: string, { keepDays, keepSuccessDays, maxBytes, now, }: {
|
|
147
171
|
keepDays: number;
|
|
172
|
+
keepSuccessDays?: number;
|
|
173
|
+
maxBytes?: number;
|
|
148
174
|
now: Date;
|
|
149
175
|
}) => number;
|