@wirecat/cli-core 0.424.0 → 0.425.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 +39 -0
- package/dist/cli/author.d.ts +5 -6
- package/dist/cli/author.js +9 -16
- package/dist/cli/config-issues.d.ts +20 -2
- package/dist/cli/config-issues.js +111 -17
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.js +17 -6
- package/dist/cli/dry-run-guard.d.ts +2 -0
- package/dist/cli/dry-run-guard.js +68 -0
- package/dist/cli/entry.js +14 -9
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -2
- package/dist/cli/list-query.d.ts +2 -0
- package/dist/cli/list-query.js +5 -2
- package/dist/cli/messenger/store-maintenance-command.js +15 -5
- package/dist/cli/preview.d.ts +44 -5
- package/dist/cli/preview.js +26 -8
- package/dist/cli/program.js +30 -18
- package/dist/cli/runs/search.js +2 -0
- package/dist/cli/settings.js +13 -12
- package/dist/cli/shared-command-exports.js +3 -1
- package/dist/entities/index.d.ts +148 -0
- package/dist/entities/recorded-run.d.ts +84 -0
- package/dist/entities/recorded-run.js +15 -0
- package/dist/entities/store-maintenance.d.ts +64 -0
- package/dist/entities/store-maintenance.js +24 -1
- package/dist/mcp/agent.js +3 -4
- package/dist/mcp/bot/server.js +1 -1
- package/dist/mcp/meeting-preparation.js +2 -2
- package/dist/mcp/meeting-project-candidates.js +2 -2
- package/dist/mcp/meeting-series-inventory.js +2 -2
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/surface.d.ts +3 -1
- package/dist/mcp/surface.js +20 -9
- package/dist/mcp/text.d.ts +1 -1
- package/dist/mcp/text.js +1 -1
- package/dist/mcp/tool.d.ts +1 -1
- package/dist/mcp/tool.js +3 -1
- package/dist/meetings/mcp/agent-text.d.ts +2 -0
- package/dist/meetings/mcp/agent-text.js +2 -0
- package/dist/meetings/mcp/index.d.ts +1 -1
- package/dist/meetings/mcp/index.js +3 -3
- package/dist/meetings/mcp/passage.js +2 -2
- package/dist/meetings/mcp/project-evidence.js +2 -2
- package/dist/meetings/mcp/search.js +2 -2
- package/dist/runs/error-detail.d.ts +42 -0
- package/dist/runs/error-detail.js +131 -0
- package/dist/runs/events.d.ts +4 -0
- package/dist/runs/events.js +2 -0
- package/dist/runs/log-attributes.js +2 -0
- package/dist/runs/recording.d.ts +5 -2
- package/dist/runs/recording.js +20 -19
- package/dist/runs/run-context.d.ts +37 -0
- package/dist/runs/run-context.js +148 -0
- package/dist/runs/run.d.ts +19 -3
- package/dist/runs/run.js +1 -0
- package/dist/store/exports.d.ts +2 -0
- package/dist/store/exports.js +2 -0
- package/dist/store/sqlite/store-contents.d.ts +47 -0
- package/dist/store/sqlite/store-contents.js +104 -0
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CliError, errorObjectOf, isCliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
2
2
|
import { meetingHitsList } from "../answers.js";
|
|
3
3
|
import { validateMeetingHitSearch } from "../search-validation.js";
|
|
4
|
-
import { agentResult,
|
|
4
|
+
import { agentResult, UNTRUSTED_NEUTRAL } from "./agent-text.js";
|
|
5
5
|
const integer = { type: "integer", minimum: 1, maximum: Number.MAX_SAFE_INTEGER };
|
|
6
6
|
const name = { type: "string", minLength: 1, maxLength: 200 };
|
|
7
7
|
export const meetingSearchTool = {
|
|
8
8
|
name: "search_meetings",
|
|
9
|
-
description: `Find words in the selected account's stored meetings, indexing new ones first; participant filters do not say who spoke a hit. ${
|
|
9
|
+
description: `Find words in the selected account's stored meetings, indexing new ones first; participant filters do not say who spoke a hit. ${UNTRUSTED_NEUTRAL}`,
|
|
10
10
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11
11
|
inputSchema: {
|
|
12
12
|
type: "object",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type RequestEvent } from "./events.js";
|
|
2
|
+
/**
|
|
3
|
+
* What went wrong, for the owner reading a failed run: the class, the code, the sentence with anything that can be
|
|
4
|
+
* somebody's words masked, where the call went, and where in our code it broke. Keys as written in run.json.
|
|
5
|
+
*/
|
|
6
|
+
export interface RunError {
|
|
7
|
+
class: string;
|
|
8
|
+
code?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
/** Ours, not the messenger's: `messages.send`. */
|
|
12
|
+
operation?: string;
|
|
13
|
+
http?: {
|
|
14
|
+
method?: string;
|
|
15
|
+
endpoint_template?: string;
|
|
16
|
+
status?: number;
|
|
17
|
+
};
|
|
18
|
+
/** A messenger protocol's refusal: `FLOOD_WAIT`, and its numeric code when it has one (MTProto's 420). */
|
|
19
|
+
provider?: {
|
|
20
|
+
key?: string;
|
|
21
|
+
code?: number;
|
|
22
|
+
};
|
|
23
|
+
causes?: {
|
|
24
|
+
class: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}[];
|
|
27
|
+
stack?: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare const errorDetailOf: (error: unknown, response?: RequestEvent) => RunError;
|
|
30
|
+
/** `function file:line`, as `command.crashed` writes them: the package's own path, never the home directory. */
|
|
31
|
+
export declare const framesOf: (error: unknown, limit?: number) => string[];
|
|
32
|
+
/**
|
|
33
|
+
* Where a request went, with nothing in it that names a person or opens an account: no query string, no user, an id
|
|
34
|
+
* or a token in the path as `:id` / `:token`. The host stays — which service refused is the point.
|
|
35
|
+
*/
|
|
36
|
+
export declare const endpointTemplate: (raw: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
|
|
39
|
+
* phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
|
|
40
|
+
* defence — errors here are written to name things, not quote them — so it errs toward masking.
|
|
41
|
+
*/
|
|
42
|
+
export declare const redactText: (text: string) => string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isCliFailure } from "../errors/failures.js";
|
|
3
|
+
import { providerErrorKey } from "./events.js";
|
|
4
|
+
const MESSAGE_AT_MOST = 500;
|
|
5
|
+
const CAUSES_AT_MOST = 5;
|
|
6
|
+
const FRAMES_AT_MOST = 15;
|
|
7
|
+
export const errorDetailOf = (error, response) => {
|
|
8
|
+
const details = isCliFailure(error) ? { ...error.details } : {};
|
|
9
|
+
const thrown = error instanceof Error ? error : undefined;
|
|
10
|
+
const code = isCliFailure(error) ? error.code : codeOf(error);
|
|
11
|
+
const reason = typeof details.reason === "string" ? details.reason : undefined;
|
|
12
|
+
const message = thrown ? redactText(thrown.message) : undefined;
|
|
13
|
+
const operation = stringOf(details.operation) ?? response?.operation;
|
|
14
|
+
const status = numberOf(details.status) ?? response?.status;
|
|
15
|
+
const method = stringOf(details.method) ?? response?.method;
|
|
16
|
+
const endpoint = stringOf(details.endpoint) ?? response?.endpointTemplate;
|
|
17
|
+
const key = providerErrorKey(details.provider_error) ?? providerErrorKey(response?.providerError);
|
|
18
|
+
const protocol = key !== undefined && method === undefined && endpoint === undefined;
|
|
19
|
+
const http = method !== undefined || endpoint !== undefined || (status !== undefined && !protocol)
|
|
20
|
+
? {
|
|
21
|
+
...(method === undefined ? {} : { method: method.toUpperCase().slice(0, 16) }),
|
|
22
|
+
...(endpoint === undefined ? {} : { endpoint_template: endpointTemplate(endpoint) }),
|
|
23
|
+
...(status === undefined ? {} : { status }),
|
|
24
|
+
}
|
|
25
|
+
: undefined;
|
|
26
|
+
const causes = causesOf(thrown?.cause ?? details.cause);
|
|
27
|
+
const stack = framesOf(thrown);
|
|
28
|
+
return {
|
|
29
|
+
class: classOf(error),
|
|
30
|
+
...(code === undefined ? {} : { code }),
|
|
31
|
+
...(reason === undefined ? {} : { reason }),
|
|
32
|
+
...(message ? { message } : {}),
|
|
33
|
+
...(operation === undefined ? {} : { operation }),
|
|
34
|
+
...(http ? { http } : {}),
|
|
35
|
+
...(key === undefined ? {} : { provider: { key, ...(protocol && status !== undefined ? { code: status } : {}) } }),
|
|
36
|
+
...(causes.length > 0 ? { causes } : {}),
|
|
37
|
+
...(stack.length > 0 ? { stack } : {}),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const stringOf = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
41
|
+
const numberOf = (value) => typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
42
|
+
const classOf = (error) => {
|
|
43
|
+
if (!(error instanceof Error))
|
|
44
|
+
return error === null ? "null" : typeof error;
|
|
45
|
+
const constructed = error.constructor?.name;
|
|
46
|
+
return constructed && constructed !== "Error" && constructed !== error.name ? constructed : error.name;
|
|
47
|
+
};
|
|
48
|
+
/** `ECONNRESET`, `commander.unknownCommand`, `ERR_INVALID_ARG_TYPE` — a code a program set, never a sentence. */
|
|
49
|
+
const codeOf = (error) => {
|
|
50
|
+
const code = error?.code;
|
|
51
|
+
return typeof code === "string" && /^[A-Za-z][A-Za-z0-9._-]{0,63}$/.test(code) ? code : undefined;
|
|
52
|
+
};
|
|
53
|
+
const causesOf = (first) => {
|
|
54
|
+
const causes = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (let cause = first; cause !== undefined && cause !== null && causes.length < CAUSES_AT_MOST;) {
|
|
57
|
+
if (seen.has(cause))
|
|
58
|
+
break;
|
|
59
|
+
seen.add(cause);
|
|
60
|
+
const message = cause instanceof Error ? redactText(cause.message) : undefined;
|
|
61
|
+
causes.push({ class: classOf(cause), ...(message ? { message } : {}) });
|
|
62
|
+
cause = cause instanceof Error ? cause.cause : undefined;
|
|
63
|
+
}
|
|
64
|
+
return causes;
|
|
65
|
+
};
|
|
66
|
+
/** `function file:line`, as `command.crashed` writes them: the package's own path, never the home directory. */
|
|
67
|
+
export const framesOf = (error, limit = FRAMES_AT_MOST) => {
|
|
68
|
+
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
|
|
69
|
+
return stack
|
|
70
|
+
.split("\n")
|
|
71
|
+
.filter((line) => line.trimStart().startsWith("at "))
|
|
72
|
+
.flatMap((line) => {
|
|
73
|
+
const match = /^\s*at (?:async )?(?:(.+?) \()?(.+?):(\d+):\d+\)?$/.exec(line);
|
|
74
|
+
if (!match)
|
|
75
|
+
return [];
|
|
76
|
+
const [, fn, path = "", lineNumber] = match;
|
|
77
|
+
if (path.startsWith("node:"))
|
|
78
|
+
return [];
|
|
79
|
+
return [`${fn ?? "<anonymous>"} ${fileOf(path)}:${lineNumber}`];
|
|
80
|
+
})
|
|
81
|
+
.slice(0, limit);
|
|
82
|
+
};
|
|
83
|
+
const fileOf = (path) => {
|
|
84
|
+
const plain = path.replace(/^file:\/\//, "");
|
|
85
|
+
const packaged = /\/node_modules\/((?:@[^/]+\/)?[^/]+\/.+)$/.exec(plain)?.[1];
|
|
86
|
+
if (packaged)
|
|
87
|
+
return packaged.replace(/\/(?:dist|src)\//, "/");
|
|
88
|
+
return /(?:^|\/)(?:dist|src)\/(.+)$/.exec(plain)?.[1] ?? plain.split("/").at(-1) ?? "?";
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Where a request went, with nothing in it that names a person or opens an account: no query string, no user, an id
|
|
92
|
+
* or a token in the path as `:id` / `:token`. The host stays — which service refused is the point.
|
|
93
|
+
*/
|
|
94
|
+
export const endpointTemplate = (raw) => {
|
|
95
|
+
const trimmed = raw.trim().split(/[?#]/)[0] ?? "";
|
|
96
|
+
const origin = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?([^/]*)/i.exec(trimmed);
|
|
97
|
+
const path = origin ? trimmed.slice(origin[0].length) : trimmed;
|
|
98
|
+
const scheme = origin ? `${trimmed.slice(0, trimmed.indexOf("://"))}://${origin[1]}` : "";
|
|
99
|
+
const segments = path.split("/").map((segment) => {
|
|
100
|
+
if (segment === "" || /^[:{].+/.test(segment))
|
|
101
|
+
return segment;
|
|
102
|
+
if (/^bot\d+:/.test(segment) || /^\d{6,}:[\w-]{20,}$/.test(segment))
|
|
103
|
+
return ":token";
|
|
104
|
+
if (/^-?\d+$/.test(segment) || /^[0-9a-f-]{16,}$/i.test(segment))
|
|
105
|
+
return ":id";
|
|
106
|
+
if (!/^[A-Za-z0-9._~-]{1,40}$/.test(segment))
|
|
107
|
+
return ":value";
|
|
108
|
+
return segment;
|
|
109
|
+
});
|
|
110
|
+
return `${scheme}${segments.join("/")}`.slice(0, 300);
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
|
|
114
|
+
* phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
|
|
115
|
+
* defence — errors here are written to name things, not quote them — so it errs toward masking.
|
|
116
|
+
*/
|
|
117
|
+
export const redactText = (text) => {
|
|
118
|
+
const home = homedir();
|
|
119
|
+
let out = home && home !== "/" ? text.split(home).join("~") : text;
|
|
120
|
+
out = out
|
|
121
|
+
.replace(/"[^"\n]*"|“[^”\n]*”|«[^»\n]*»/g, '"[redacted]"')
|
|
122
|
+
.replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]?\s+)\S+/gi, "$1$2[redacted]")
|
|
123
|
+
.replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "[email]")
|
|
124
|
+
.replace(/\+\d[\d\s()-]{6,}\d/g, "[number]")
|
|
125
|
+
.replace(/(^|[^\w-])\d{11,15}\b/g, "$1[number]")
|
|
126
|
+
.replace(/\bbot\d+:[\w-]+/g, "bot[token]")
|
|
127
|
+
.replace(/\b\d{6,}:[\w-]{20,}\b/g, "[token]")
|
|
128
|
+
.replace(/(\?|&)[^\s#]*/g, "$1[redacted]")
|
|
129
|
+
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, "[redacted]");
|
|
130
|
+
return out.length > MESSAGE_AT_MOST ? `${out.slice(0, MESSAGE_AT_MOST)}…` : out;
|
|
131
|
+
};
|
package/dist/runs/events.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface RequestEvent {
|
|
|
15
15
|
opcode?: number;
|
|
16
16
|
seq?: number;
|
|
17
17
|
status?: number;
|
|
18
|
+
/** An HTTP API's verb, `POST`. */
|
|
19
|
+
method?: string;
|
|
20
|
+
/** The path it went to with ids as `:id` and no query string — `/chats/:id/messages`. Never the URL as sent. */
|
|
21
|
+
endpointTemplate?: string;
|
|
18
22
|
/** The size of what went out or came back. */
|
|
19
23
|
bytes?: number;
|
|
20
24
|
ids?: Record<string, string>;
|
package/dist/runs/events.js
CHANGED
|
@@ -24,6 +24,8 @@ export const renderEvent = (event) => {
|
|
|
24
24
|
else {
|
|
25
25
|
if (event.opcode !== undefined)
|
|
26
26
|
parts.push(`op ${event.opcode}`, `seq ${event.seq}`);
|
|
27
|
+
if (event.method !== undefined || event.endpointTemplate !== undefined)
|
|
28
|
+
parts.push([event.method, event.endpointTemplate].filter(Boolean).join(" "));
|
|
27
29
|
if (event.status !== undefined)
|
|
28
30
|
parts.push(`${event.status}`);
|
|
29
31
|
}
|
package/dist/runs/recording.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RenderFormat, type Streams } from "@wirecat/cli-base";
|
|
2
2
|
import type { AppIdentity } from "../ports/app.js";
|
|
3
3
|
import { type EventSink } from "./events.js";
|
|
4
|
+
import { type RunContext } from "./run.js";
|
|
4
5
|
export interface RecordingOptions {
|
|
5
6
|
app: AppIdentity;
|
|
6
7
|
/** As typed, without the arguments: `chats list`. It names the run directory. */
|
|
@@ -25,6 +26,8 @@ export interface RecordingOptions {
|
|
|
25
26
|
* that answer's `meta.run_id` name the run `runs show` finds. Never in a server, where runs overlap.
|
|
26
27
|
*/
|
|
27
28
|
processRun?: boolean;
|
|
29
|
+
/** The whole command, its masked arguments and the agent that started it. */
|
|
30
|
+
context?: RunContext;
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
@@ -48,8 +51,8 @@ export declare const startRecording: (options: RecordingOptions) => Recording;
|
|
|
48
51
|
export declare const wasSettled: (error: unknown) => boolean;
|
|
49
52
|
/**
|
|
50
53
|
* Where a crash happened, not what it said: the class and up to ten frames as `function file:line`,
|
|
51
|
-
* paths cut to
|
|
52
|
-
* failing line was handling.
|
|
54
|
+
* paths cut to the package's own (`framesOf`). **Never `message`** in an event — it can hold whatever the
|
|
55
|
+
* failing line was handling; run.json's `error` keeps it, masked.
|
|
53
56
|
*/
|
|
54
57
|
export declare const crashOf: (error: unknown) => {
|
|
55
58
|
event: "command.crashed";
|
package/dist/runs/recording.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { processStreams } from "@wirecat/cli-base";
|
|
2
2
|
import { isCliFailure } from "../errors/failures.js";
|
|
3
|
+
import { endpointTemplate, errorDetailOf, framesOf } from "./error-detail.js";
|
|
3
4
|
import { providerErrorKey, renderEvent } from "./events.js";
|
|
4
5
|
import { DIAGNOSTIC_LOG_EVENTS } from "./log-events.js";
|
|
5
6
|
import { logAttributes } from "./log-keys.js";
|
|
@@ -48,6 +49,7 @@ export const startRecording = (options) => {
|
|
|
48
49
|
service: options.app.command,
|
|
49
50
|
cliVersion: options.app.version,
|
|
50
51
|
...(traceId === undefined ? {} : { traceId }),
|
|
52
|
+
...(options.context ? { context: options.context } : {}),
|
|
51
53
|
// The operation has happened or is under way, and failing the command now invites a retry
|
|
52
54
|
// that sends twice.
|
|
53
55
|
onError: (error) => {
|
|
@@ -62,9 +64,12 @@ export const startRecording = (options) => {
|
|
|
62
64
|
// Unrecorded, the newest events are held until the outcome is known, so a failure can still be kept.
|
|
63
65
|
const held = !run && options.keepFailed ? [] : undefined;
|
|
64
66
|
let requests = 0;
|
|
67
|
+
let refused;
|
|
65
68
|
const events = (event) => {
|
|
66
69
|
if (event.event === "request")
|
|
67
70
|
requests += 1;
|
|
71
|
+
if (event.event === "response" && event.outcome === "error")
|
|
72
|
+
refused = event;
|
|
68
73
|
if (options.trace)
|
|
69
74
|
streams.diagnostic(options.format === "pretty" ? renderEvent(event) : JSON.stringify(logAttributes(event)));
|
|
70
75
|
if (run)
|
|
@@ -92,6 +97,7 @@ export const startRecording = (options) => {
|
|
|
92
97
|
await failed.finish("failed", {
|
|
93
98
|
requests,
|
|
94
99
|
...outcomeOf(error),
|
|
100
|
+
error: errorDetailOf(error, refused),
|
|
95
101
|
...(run ? {} : { keptBecauseFailed: true }),
|
|
96
102
|
});
|
|
97
103
|
}
|
|
@@ -110,7 +116,13 @@ const keep = (run, events) => {
|
|
|
110
116
|
return run;
|
|
111
117
|
};
|
|
112
118
|
const logEvent = (run, event) => {
|
|
113
|
-
const record = {
|
|
119
|
+
const record = {
|
|
120
|
+
...event,
|
|
121
|
+
event: DIAGNOSTIC_LOG_EVENTS[event.event],
|
|
122
|
+
...("endpointTemplate" in event && event.endpointTemplate !== undefined
|
|
123
|
+
? { endpointTemplate: endpointTemplate(event.endpointTemplate) }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
114
126
|
if (event.event === "warning")
|
|
115
127
|
run.logger.warn(record);
|
|
116
128
|
else if (event.event !== "cache" && event.outcome === "error")
|
|
@@ -126,25 +138,14 @@ const outcomeOf = (error) => {
|
|
|
126
138
|
};
|
|
127
139
|
/**
|
|
128
140
|
* Where a crash happened, not what it said: the class and up to ten frames as `function file:line`,
|
|
129
|
-
* paths cut to
|
|
130
|
-
* failing line was handling.
|
|
141
|
+
* paths cut to the package's own (`framesOf`). **Never `message`** in an event — it can hold whatever the
|
|
142
|
+
* failing line was handling; run.json's `error` keeps it, masked.
|
|
131
143
|
*/
|
|
132
|
-
export const crashOf = (error) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.slice(0, 10)
|
|
138
|
-
.map((line) => {
|
|
139
|
-
const match = /^\s*at (?:(.+?) \()?(.+?):(\d+):\d+\)?$/.exec(line);
|
|
140
|
-
if (!match)
|
|
141
|
-
return "?";
|
|
142
|
-
const [, fn, path = "", lineNumber] = match;
|
|
143
|
-
const file = /(?:^|\/)(?:dist|src)\/(.+)$/.exec(path)?.[1] ?? path.split("/").at(-1);
|
|
144
|
-
return `${fn ?? "<anonymous>"} ${file}:${lineNumber}`;
|
|
145
|
-
});
|
|
146
|
-
return { event: "command.crashed", errorClass: error instanceof Error ? error.name : typeof error, frames };
|
|
147
|
-
};
|
|
144
|
+
export const crashOf = (error) => ({
|
|
145
|
+
event: "command.crashed",
|
|
146
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
147
|
+
frames: framesOf(error, 10),
|
|
148
|
+
});
|
|
148
149
|
export const partialOutcome = (answer) => {
|
|
149
150
|
if (!answer || typeof answer !== "object")
|
|
150
151
|
return undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { RunContext } from "./run.js";
|
|
3
|
+
export declare const REDACTED = "[redacted]";
|
|
4
|
+
/** Which coding agent started the process, and its session when the agent names one. Keys as written in run.json. */
|
|
5
|
+
export interface RunAgent {
|
|
6
|
+
kind: string;
|
|
7
|
+
session_id: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
|
|
11
|
+
* `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Gemini CLI
|
|
12
|
+
* `GEMINI_CLI`, Cursor `CURSOR_AGENT` — the last two give their commands no session id. `AI_AGENT` (Claude Code,
|
|
13
|
+
* OpenCode) carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
|
|
14
|
+
*/
|
|
15
|
+
export declare const agentOf: (env: NodeJS.ProcessEnv) => RunAgent | undefined;
|
|
16
|
+
export interface MaskedArgv {
|
|
17
|
+
/** The words after the binary, every value that may be content or a secret replaced by `[redacted]`. */
|
|
18
|
+
argv: string[];
|
|
19
|
+
/** The first word that names no command where one was expected. */
|
|
20
|
+
unknownCommand?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The command line as typed, safe to keep. Command words, flag names and the profile stay; a value stays only when
|
|
24
|
+
* its shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
|
|
25
|
+
* option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
|
|
26
|
+
* else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
|
|
27
|
+
*/
|
|
28
|
+
export declare const maskArgv: (program: Command, words: readonly string[]) => MaskedArgv;
|
|
29
|
+
/** A run's context from the parsed program: `max messages list` and its masked words, or the word that named nothing. */
|
|
30
|
+
export declare const runContextOf: ({ bin, command, program, argv, env, }: {
|
|
31
|
+
bin: string;
|
|
32
|
+
/** The run's own name, `messages list` or `mcp messages list`; empty when no command was recognised. */
|
|
33
|
+
command: string;
|
|
34
|
+
program?: Command;
|
|
35
|
+
argv?: readonly string[];
|
|
36
|
+
env: NodeJS.ProcessEnv;
|
|
37
|
+
}) => RunContext;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { isContentKey } from "@wirecat/cli-base/standards";
|
|
2
|
+
export const REDACTED = "[redacted]";
|
|
3
|
+
/**
|
|
4
|
+
* The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
|
|
5
|
+
* `CLAUDE_CODE_SESSION_ID`, Codex `CODEX_THREAD_ID`, OpenCode `OPENCODE` and `OPENCODE_SESSION_ID`, Gemini CLI
|
|
6
|
+
* `GEMINI_CLI`, Cursor `CURSOR_AGENT` — the last two give their commands no session id. `AI_AGENT` (Claude Code,
|
|
7
|
+
* OpenCode) carries a version, `claude-code_2-1-296_agent`, so only the name before it is kept.
|
|
8
|
+
*/
|
|
9
|
+
export const agentOf = (env) => {
|
|
10
|
+
const named = env.AI_AGENT?.trim().split("_")[0];
|
|
11
|
+
const kind = (named && plainWord(named) ? named.toLowerCase() : undefined) ??
|
|
12
|
+
(env.CLAUDECODE ? "claude-code" : undefined) ??
|
|
13
|
+
(env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || env.CODEX_SANDBOX ? "codex" : undefined) ??
|
|
14
|
+
(env.GEMINI_CLI ? "gemini-cli" : undefined) ??
|
|
15
|
+
(env.OPENCODE ? "opencode" : undefined) ??
|
|
16
|
+
(env.CURSOR_AGENT ? "cursor" : undefined);
|
|
17
|
+
if (!kind)
|
|
18
|
+
return undefined;
|
|
19
|
+
const session = sessionOf(kind, env);
|
|
20
|
+
return { kind, session_id: session && SESSION.test(session) ? session : null };
|
|
21
|
+
};
|
|
22
|
+
const SESSION = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
23
|
+
const sessionOf = (kind, env) => {
|
|
24
|
+
const value = kind === "claude-code"
|
|
25
|
+
? env.CLAUDE_CODE_SESSION_ID
|
|
26
|
+
: kind === "codex"
|
|
27
|
+
? env.CODEX_THREAD_ID
|
|
28
|
+
: kind === "opencode"
|
|
29
|
+
? env.OPENCODE_SESSION_ID
|
|
30
|
+
: undefined;
|
|
31
|
+
return value?.trim() || undefined;
|
|
32
|
+
};
|
|
33
|
+
const plainWord = (word) => /^[A-Za-z][A-Za-z0-9.-]{0,39}$/.test(word);
|
|
34
|
+
/**
|
|
35
|
+
* The command line as typed, safe to keep. Command words, flag names and the profile stay; a value stays only when
|
|
36
|
+
* its shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
|
|
37
|
+
* option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
|
|
38
|
+
* else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
|
|
39
|
+
*/
|
|
40
|
+
export const maskArgv = (program, words) => {
|
|
41
|
+
const argv = [];
|
|
42
|
+
let unknownCommand;
|
|
43
|
+
let current = program;
|
|
44
|
+
let positional = 0;
|
|
45
|
+
let profileSeen = false;
|
|
46
|
+
let lost = false;
|
|
47
|
+
const optionFor = (flag) => {
|
|
48
|
+
for (let at = current; at; at = at.parent) {
|
|
49
|
+
const found = at.options.find((option) => option.long === flag || option.short === flag);
|
|
50
|
+
if (found)
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
for (let at = 0; at < words.length; at++) {
|
|
56
|
+
const word = words[at];
|
|
57
|
+
if (word === "--") {
|
|
58
|
+
argv.push("--");
|
|
59
|
+
for (let after = at + 1; after < words.length; after++)
|
|
60
|
+
argv.push(REDACTED);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (word.startsWith("-") && word.length > 1 && !NUMBER.test(word)) {
|
|
64
|
+
const equals = word.indexOf("=");
|
|
65
|
+
const flag = equals > 0 ? word.slice(0, equals) : word;
|
|
66
|
+
if (!/^--?[A-Za-z0-9][A-Za-z0-9-]*$/.test(flag)) {
|
|
67
|
+
argv.push(REDACTED);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const option = optionFor(flag);
|
|
71
|
+
const name = option?.attributeName() ?? flag.replace(/^-+/, "");
|
|
72
|
+
if (equals > 0) {
|
|
73
|
+
argv.push(`${flag}=${maskedValue(name, word.slice(equals + 1), option?.argChoices)}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
argv.push(flag);
|
|
77
|
+
if (!option || !(option.required || option.optional))
|
|
78
|
+
continue;
|
|
79
|
+
const takes = (next) => next !== undefined && (option.required || !next.startsWith("-") || NUMBER.test(next));
|
|
80
|
+
if (takes(words[at + 1])) {
|
|
81
|
+
at += 1;
|
|
82
|
+
argv.push(maskedValue(name, words[at], option.argChoices));
|
|
83
|
+
while (option.variadic && words[at + 1] !== undefined && !words[at + 1].startsWith("-")) {
|
|
84
|
+
at += 1;
|
|
85
|
+
argv.push(maskedValue(name, words[at], option.argChoices));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!lost && positional === 0) {
|
|
91
|
+
const sub = current.commands.find((one) => one.name() === word || one.aliases().includes(word));
|
|
92
|
+
if (sub) {
|
|
93
|
+
argv.push(word);
|
|
94
|
+
current = sub;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (current === program && !profileSeen) {
|
|
98
|
+
profileSeen = true;
|
|
99
|
+
argv.push(PROFILE.test(word) ? word : REDACTED);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (current.commands.length > 0 && current.registeredArguments.length === 0) {
|
|
103
|
+
lost = true;
|
|
104
|
+
unknownCommand = commandLike(word);
|
|
105
|
+
argv.push(unknownCommand);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const declared = lost ? undefined : current.registeredArguments;
|
|
110
|
+
const argument = declared?.[Math.min(positional, declared.length - 1)];
|
|
111
|
+
positional += 1;
|
|
112
|
+
argv.push(argument ? maskedValue(argument.name(), word, argument.argChoices) : REDACTED);
|
|
113
|
+
}
|
|
114
|
+
return { argv, ...(unknownCommand === undefined ? {} : { unknownCommand }) };
|
|
115
|
+
};
|
|
116
|
+
const NUMBER = /^-?\d+(\.\d+)?$/;
|
|
117
|
+
const PROFILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
118
|
+
/** A mistyped command word is kept so the typo can be seen; anything shaped like a sentence or an address is not. */
|
|
119
|
+
const commandLike = (word) => (/^[a-z][a-z0-9-]{0,31}$/i.test(word) ? word : REDACTED);
|
|
120
|
+
const maskedValue = (name, value, choices) => {
|
|
121
|
+
if (isContentKey(name))
|
|
122
|
+
return REDACTED;
|
|
123
|
+
if (name === "profile")
|
|
124
|
+
return PROFILE.test(value) ? value : REDACTED;
|
|
125
|
+
if (name === "json" || name === "fields")
|
|
126
|
+
return /^[a-z0-9_.,]{1,200}$/.test(value) ? value : REDACTED;
|
|
127
|
+
if (choices?.includes(value))
|
|
128
|
+
return value;
|
|
129
|
+
return safeShape(value) ? value : REDACTED;
|
|
130
|
+
};
|
|
131
|
+
// A positive run of eleven or more digits can be a phone number written without its "+"; ids that long are negative
|
|
132
|
+
// (chats and channels) and stay.
|
|
133
|
+
const safeShape = (value) => (NUMBER.test(value) && !/^\d{11,}$/.test(value)) ||
|
|
134
|
+
/^\d+(ms|s|m|h|d|w)$/.test(value) ||
|
|
135
|
+
/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(value) ||
|
|
136
|
+
value === "true" ||
|
|
137
|
+
value === "false";
|
|
138
|
+
/** A run's context from the parsed program: `max messages list` and its masked words, or the word that named nothing. */
|
|
139
|
+
export const runContextOf = ({ bin, command, program, argv, env, }) => {
|
|
140
|
+
const masked = program && argv ? maskArgv(program, argv) : undefined;
|
|
141
|
+
const agent = agentOf(env);
|
|
142
|
+
return {
|
|
143
|
+
cliCommand: command ? `${bin} ${command}` : bin,
|
|
144
|
+
...(masked ? { argv: masked.argv } : {}),
|
|
145
|
+
...(masked?.unknownCommand === undefined ? {} : { unknownCommand: masked.unknownCommand }),
|
|
146
|
+
...(agent ? { agent } : {}),
|
|
147
|
+
};
|
|
148
|
+
};
|
package/dist/runs/run.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type Logger } from "@wirecat/cli-base";
|
|
2
2
|
import type { AppIdentity } from "../ports/app.js";
|
|
3
|
+
import type { RunError } from "./error-detail.js";
|
|
4
|
+
import type { RunAgent } from "./run-context.js";
|
|
3
5
|
export type RunStatus = "success" | "failed" | "partial";
|
|
4
6
|
/**
|
|
5
|
-
* What a run was, and how it ended. **It never says what was read or sent** — a command, a profile
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* What a run was, and how it ended. **It never says what was read or sent** — a command, a profile,
|
|
8
|
+
* a count, the arguments with every value that may be content masked, and the `errorCode` a script
|
|
9
|
+
* branches on. The sentence a person reads is kept only in `error`, masked, because a messenger's
|
|
10
|
+
* refusal can quote the payload back.
|
|
8
11
|
*/
|
|
9
12
|
export interface RunMetadata {
|
|
10
13
|
runId: string;
|
|
@@ -44,7 +47,19 @@ export interface RunMetadata {
|
|
|
44
47
|
codes?: string[];
|
|
45
48
|
/** The caller's trace: the MCP client's, or the schedule's or job's that started it. */
|
|
46
49
|
traceId?: string;
|
|
50
|
+
/** The whole command with the program's name, `max messages list`; `command` stays the run's short name. */
|
|
51
|
+
cliCommand?: string;
|
|
52
|
+
/** The words after the program's name, every value that may be content or a secret `[redacted]` (`maskArgv`). */
|
|
53
|
+
argv?: string[];
|
|
54
|
+
/** The word that named no command, when that is why the run failed. */
|
|
55
|
+
unknownCommand?: string;
|
|
56
|
+
/** The coding agent that started it, when one did. */
|
|
57
|
+
agent?: RunAgent;
|
|
58
|
+
/** How a failed run failed (`errorDetailOf`). */
|
|
59
|
+
error?: RunError;
|
|
47
60
|
}
|
|
61
|
+
/** Who started a run and with what, known before it starts. */
|
|
62
|
+
export type RunContext = Pick<RunMetadata, "cliCommand" | "argv" | "unknownCommand" | "agent">;
|
|
48
63
|
export interface StartRunOptions {
|
|
49
64
|
runsDir: string;
|
|
50
65
|
command: string;
|
|
@@ -60,6 +75,7 @@ export interface StartRunOptions {
|
|
|
60
75
|
startedAt?: Date;
|
|
61
76
|
/** The caller's trace, written on every record as `trace_id`. */
|
|
62
77
|
traceId?: string;
|
|
78
|
+
context?: RunContext;
|
|
63
79
|
/** The log file could not be opened or written; the run goes on unrecorded from here. */
|
|
64
80
|
onError?: (error: Error) => void;
|
|
65
81
|
}
|
package/dist/runs/run.js
CHANGED
package/dist/store/exports.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { OTHER_SOURCES, type OtherSource, type SourceMention } from "./sqlite/me
|
|
|
32
32
|
export { MENTION_QUERY, MENTION_STATUSES, type MentionDecision, type MentionStatus, type MentionsRebuild, type MentionsState, type StoredMention, type StoredPlace, } from "./sqlite/mentions.js";
|
|
33
33
|
export { drainNoteIndex, noteIndexState, resetNoteIndex } from "./sqlite/note-index.js";
|
|
34
34
|
export type { Link as ThingLink } from "./sqlite/notes.js";
|
|
35
|
+
export { openSqlite } from "./sqlite/open.js";
|
|
35
36
|
export type { PersonPreparationRead, PreparationFact, PreparationNote, PreparationTask, } from "./sqlite/person-preparation.js";
|
|
36
37
|
export { savedPhoneCountries, saveStorePhoneCountries } from "./sqlite/phone-countries.js";
|
|
37
38
|
export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
|
|
@@ -46,6 +47,7 @@ export { fillSearchIndex, resetSearchIndex, searchIndexState } from "./sqlite/se
|
|
|
46
47
|
export type { SearchCommand, SearchRecord, StoredSearch } from "./sqlite/searches.js";
|
|
47
48
|
export { type StemsState, savedStemmers, stemmerCache, stemmersOrigin, stemsState, } from "./sqlite/stems.js";
|
|
48
49
|
export { fillStems, resetStems } from "./sqlite/stems-fill.js";
|
|
50
|
+
export { rowCounts, storeContents } from "./sqlite/store-contents.js";
|
|
49
51
|
export type { StoredTag, TagFilter, TagTarget, TaskTagTarget } from "./sqlite/tags.js";
|
|
50
52
|
export { savedTaskCategories } from "./sqlite/task-category-settings.js";
|
|
51
53
|
export { DEFAULT_AGENT_CLOSE, savedAgentClose } from "./sqlite/task-close.js";
|
package/dist/store/exports.js
CHANGED
|
@@ -11,12 +11,14 @@ export { involvementStoreOver } from "./sqlite/involvements.js";
|
|
|
11
11
|
export { OTHER_SOURCES } from "./sqlite/mention-sources.js";
|
|
12
12
|
export { MENTION_QUERY, MENTION_STATUSES, } from "./sqlite/mentions.js";
|
|
13
13
|
export { drainNoteIndex, noteIndexState, resetNoteIndex } from "./sqlite/note-index.js";
|
|
14
|
+
export { openSqlite } from "./sqlite/open.js";
|
|
14
15
|
export { savedPhoneCountries, saveStorePhoneCountries } from "./sqlite/phone-countries.js";
|
|
15
16
|
export { queryFilter } from "./sqlite/query-filter.js";
|
|
16
17
|
export { AGENDA_COLUMNS } from "./sqlite/reads.js";
|
|
17
18
|
export { fillSearchIndex, resetSearchIndex, searchIndexState } from "./sqlite/search-index.js";
|
|
18
19
|
export { savedStemmers, stemmerCache, stemmersOrigin, stemsState, } from "./sqlite/stems.js";
|
|
19
20
|
export { fillStems, resetStems } from "./sqlite/stems-fill.js";
|
|
21
|
+
export { rowCounts, storeContents } from "./sqlite/store-contents.js";
|
|
20
22
|
export { savedTaskCategories } from "./sqlite/task-category-settings.js";
|
|
21
23
|
export { DEFAULT_AGENT_CLOSE, savedAgentClose } from "./sqlite/task-close.js";
|
|
22
24
|
export { DEFAULT_FIRST_PASS_PER_CHAT, firstPassPerChatOf, savedFirstPassPerChat } from "./sqlite/task-first-pass.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/** The tables `store info` counts; a file from before a table's migration simply lacks it. */
|
|
3
|
+
export declare const COUNTED_TABLES: readonly ["accounts", "chats", "messages", "emails", "documents", "meetings", "events", "attachments", "identities"];
|
|
4
|
+
export declare const hasTable: (database: CacheDatabase, table: string) => boolean;
|
|
5
|
+
export declare const rowCounts: (database: CacheDatabase) => Partial<Record<(typeof COUNTED_TABLES)[number], number>>;
|
|
6
|
+
/**
|
|
7
|
+
* What the store holds, grouped, over every account in it. Plain `count(*) … GROUP BY` on indexed
|
|
8
|
+
* columns only: `dbstat` would give sizes per table but reads every page of the file.
|
|
9
|
+
*/
|
|
10
|
+
export declare const storeContents: (database: CacheDatabase) => {
|
|
11
|
+
byProvider: {
|
|
12
|
+
accounts: number;
|
|
13
|
+
provider: string;
|
|
14
|
+
chats: number;
|
|
15
|
+
documents: number;
|
|
16
|
+
emails: number;
|
|
17
|
+
meetings: number;
|
|
18
|
+
messages: number;
|
|
19
|
+
}[];
|
|
20
|
+
byChatKind: {
|
|
21
|
+
provider: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
chats: number;
|
|
24
|
+
messages: number;
|
|
25
|
+
}[];
|
|
26
|
+
byAccount: {
|
|
27
|
+
provider: string;
|
|
28
|
+
accountId: number;
|
|
29
|
+
name: string | null;
|
|
30
|
+
chats: number;
|
|
31
|
+
documents: number;
|
|
32
|
+
emails: number;
|
|
33
|
+
meetings: number;
|
|
34
|
+
messages: number;
|
|
35
|
+
}[];
|
|
36
|
+
byYear: {
|
|
37
|
+
provider: string;
|
|
38
|
+
year: number;
|
|
39
|
+
messages: number;
|
|
40
|
+
}[];
|
|
41
|
+
byAttachmentKind: {
|
|
42
|
+
type: string;
|
|
43
|
+
kind: string;
|
|
44
|
+
count: number;
|
|
45
|
+
bytes: number;
|
|
46
|
+
}[];
|
|
47
|
+
} | undefined;
|