@wirecat/cli-core 0.465.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +4 -2
  3. package/dist/cli/command-runs.d.ts +38 -10
  4. package/dist/cli/command-runs.js +51 -17
  5. package/dist/cli/context.d.ts +3 -0
  6. package/dist/cli/context.js +15 -3
  7. package/dist/cli/index.d.ts +3 -3
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/messenger/account-command.js +3 -0
  10. package/dist/cli/messenger/context.d.ts +9 -0
  11. package/dist/cli/messenger/context.js +6 -4
  12. package/dist/cli/messenger/mcp-command.js +8 -1
  13. package/dist/cli/messenger/observed.js +10 -2
  14. package/dist/cli/messenger/report.js +48 -3
  15. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  16. package/dist/cli/program.d.ts +1 -1
  17. package/dist/cli/program.js +7 -3
  18. package/dist/cli/settings.d.ts +19 -6
  19. package/dist/cli/settings.js +14 -3
  20. package/dist/cli/shared-command-exports.d.ts +12 -4
  21. package/dist/entities/account-profile.d.ts +2 -0
  22. package/dist/entities/account-profile.js +2 -0
  23. package/dist/entities/index.d.ts +40 -0
  24. package/dist/entities/recorded-run.d.ts +39 -0
  25. package/dist/entities/recorded-run.js +14 -1
  26. package/dist/mcp/agent.d.ts +3 -0
  27. package/dist/mcp/agent.js +2 -0
  28. package/dist/mcp/all-profiles.d.ts +12 -2
  29. package/dist/mcp/all-profiles.js +38 -8
  30. package/dist/mcp/bot/server.js +8 -3
  31. package/dist/mcp/caller.d.ts +38 -0
  32. package/dist/mcp/caller.js +95 -0
  33. package/dist/mcp/http/oauth.js +6 -2
  34. package/dist/mcp/server.js +19 -7
  35. package/dist/mcp/surface.js +11 -2
  36. package/dist/mcp/tool.d.ts +4 -1
  37. package/dist/mcp/tool.js +21 -11
  38. package/dist/runs/events.d.ts +16 -4
  39. package/dist/runs/events.js +4 -0
  40. package/dist/runs/log-attributes.js +7 -1
  41. package/dist/runs/log-events.js +5 -0
  42. package/dist/runs/recording.d.ts +10 -3
  43. package/dist/runs/recording.js +59 -15
  44. package/dist/runs/run-context.d.ts +55 -4
  45. package/dist/runs/run-context.js +34 -3
  46. package/dist/runs/run.d.ts +37 -11
  47. package/dist/runs/run.js +201 -52
  48. package/dist/runs/scope.d.ts +12 -0
  49. package/dist/runs/scope.js +6 -0
  50. package/dist/services/patience.js +5 -1
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.467.0 — 27.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **Every run is now recorded by default, successes too** — before, only failures were kept unless `--record` or
15
+ `record: true`. Successful runs are kept 7 days (`keep_successful_runs_for_days`), failures 30
16
+ (`keep_runs_for_days`), and the runs folder stays under `runs_size_limit_mb` (200; the oldest successes go
17
+ first, checked once a day). **Privacy:** with `record_input` on, as by default since 0.439.0, every successful
18
+ `send` now leaves its typed words (message text, a chat typed by title) in a local, 0600 run record for 7 days.
19
+ `record: false` restores failures only; `record_input: "failed"` keeps the typed words only in a run that failed;
20
+ `record_input: false` masks them everywhere; `--no-record` keeps nothing. A success that logged no events is one
21
+ line in the day's index, with no folder; `runs show` finds it there. MCP tools keep successes too. A command that
22
+ runs until stopped (`watch`, `serve`, `mcp`) and a command WireCat Desktop runs for itself
23
+ (`WIRECAT_CALLER=desktop`) keep a success only under `--record`. Search history still follows `record`.
24
+ - **A run says who asked for it.** `run.json` gains `via` (`cli`, `mcp-stdio`, `mcp-http`, `desktop`), `mcp`
25
+ (`tool`, `request_id`, `connection_id`, `proxied`) and `agent.client` (`name`, `version`, and over HTTP `id`, the
26
+ first 8 hex of the SHA-256 of the OAuth client id). Over HTTP the agent is `mcp-client` with the
27
+ `Mcp-Session-Id` as its session, never the environment of whoever started `mcp --http`: such runs were credited to
28
+ that agent before. The `run`, `run.details` and `run.hit` answer shapes declare the new fields. The every-profile server names the real caller to the profile server in
29
+ `_meta["wirecat.dev/caller"]`; a profile server trusts it only on a connection that server opened.
30
+ - **Request events gain `level`, `transport`, `attempt` and `wait_ms`**, and `requests` counts wire calls when a
31
+ transport logs them, otherwise operations. `providerRequest` takes a third argument with the call's method, path
32
+ template, transport, attempt, wait and bytes. A run writes at most 2,000 events or 1 MB, then one
33
+ `events.dropped` line and `events_dropped`. `runEvent` reaches the current run from a wire layer handed no sink. A
34
+ retry made above the transport (`patiently`, the bot APIs' rate-limit wait) marks the calls under it with
35
+ `withAttempt`, so their lines say `attempt: 2` and the wait. Events logged after a run finished — a long-running
36
+ server's connection — are dropped rather than written into it.
37
+ - **`record: false` in the config now keeps failures**; before, it kept nothing. `--no-record` still keeps nothing.
38
+
39
+ ### Fixed
40
+
41
+ - **`doctor reports create` labels the ids a run's metadata keeps** — the agent session, the OAuth client id, the
42
+ MCP connection and request, the trace and a failed chat — as it already labelled the events' ids.
43
+
44
+ ## 0.466.0 — 27.10.2026
45
+
46
+ ### Changed — may break callers
47
+
48
+ - **`account list` says whether each profile is signed in on this computer.** The `account_profile` entity gains an
49
+ optional `signed_in`, set when the messenger sets `Messenger.profileState`; a caller that checks items against an
50
+ older, strict copy of it refuses the new field.
51
+
52
+ ### Fixed
53
+
54
+ - **`<cli> mcp serve` with no profile marks the profiles that are not signed in.** A messenger sets the new optional
55
+ `Messenger.profileState(profile, env)` (`signed-in`, `signed-out` or `bot`), read from its own files without
56
+ connecting. The `profile` field keeps its `enum` and gains `oneOf` entries with a `title` per profile
57
+ (`default — not signed in`), as MCP's titled enums, and its description lists who is signed in. Bot profiles are left
58
+ out: they have their own server. A call for a profile that is not signed in is refused with how to sign it in
59
+ (`<cli> <profile> setup`, or WireCat Desktop) and starts nothing. Unset, every profile counts as signed in, as before.
60
+ - **The "no profile named" error marks the listed profiles that are not signed in** (`beta (not signed in)`), when the
61
+ messenger sets `profileState`.
62
+ - **The MCP login page says why a code that just worked is now wrong.** Each login code works once; after an app
63
+ logs in, the server shows a new one. The wrong-code message now says so and points to WireCat Desktop and
64
+ `<cli> mcp server status`, where the new code is.
65
+
10
66
  ## 0.465.0 — 27.10.2026
11
67
 
12
68
  ### Changed — may break callers
package/README.md CHANGED
@@ -218,8 +218,10 @@ they deliberately differ. A rule changes in its own pull request, before the cod
218
218
  - **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
219
219
  wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
220
220
  choices — even when there is only one.
221
- - **Run records keep the command as typed, never a secret.** A failure is kept by default, so `runs show` can explain
222
- it; a token, a password or a phone number is hidden, and `record_input: false` leaves the typed words out too.
221
+ - **Run records keep the command as typed, never a secret.** Every run is kept, with one line per request it made,
222
+ so `runs show` can explain it: successes for 7 days, failures for 30, within a size limit. A token, a password or
223
+ a phone number is hidden; `record_input: false` leaves the typed words out, `"failed"` keeps them only for a
224
+ failure, and `record: false` keeps failures only.
223
225
  - **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
224
226
  sleep. A coverage floor: 92% of lines, and at least half of every file.
225
227
  - **Contract cases for every adapter.** `contractCases` turns the port's promises into cases any test runner can run,
@@ -1,20 +1,28 @@
1
1
  import { type RenderFormat, type Streams } from "@wirecat/cli-base";
2
2
  import type { Command } from "commander";
3
3
  import type { AppIdentity } from "../ports/app.js";
4
- import type { RequestEvent } from "../runs/events.js";
5
- /** What the owner saved: `record` and `keep_runs_for_days`, read from the CLI's own config. */
4
+ import { type RequestEvent } from "../runs/events.js";
5
+ import { type RecordInput } from "../runs/run-context.js";
6
+ /** What the owner saved, read from the CLI's own config: `record`, `record_input` and how long runs are kept. */
6
7
  export interface SavedRunSettings {
7
8
  record?: boolean | undefined;
8
9
  keepRunsForDays?: number | undefined;
10
+ keepSuccessfulRunsForDays?: number | undefined;
11
+ runsSizeLimitMb?: number | undefined;
9
12
  /** `record_input`: left out, the typed words are kept, secrets hidden. */
10
- recordInput?: boolean | undefined;
13
+ recordInput?: RecordInput | undefined;
11
14
  }
12
15
  export interface RunSettings {
13
16
  record: boolean;
14
17
  keepFailedRuns: boolean;
15
18
  keepRunsForDays: number;
19
+ keepSuccessfulRunsForDays: number;
20
+ runsSizeLimitMb: number;
16
21
  }
17
- /** `--record`/`--no-record` first, then the config; a failure is kept unless either says no (WC-LIFE-15). */
22
+ /**
23
+ * `--record`/`--no-record` first, then the config: every run is kept unless it says `record: false`, which keeps
24
+ * failures only; `--no-record` keeps nothing (WC-LIFE-15).
25
+ */
18
26
  export declare const runSettings: (flag: boolean | undefined, saved?: SavedRunSettings) => RunSettings;
19
27
  export interface CommandRunOptions {
20
28
  app: AppIdentity;
@@ -47,17 +55,37 @@ export interface CommandRuns {
47
55
  events(event: RequestEvent): void;
48
56
  }
49
57
  export declare const commandRuns: (options: CommandRunOptions) => CommandRuns;
50
- /** For a provider client far from the program: the event lands in the current command's run, or nowhere. */
51
- export declare const providerEvent: (event: RequestEvent) => void;
52
58
  /**
53
- * Times one provider call under a fixed operation name — nothing the call carries goes into the run. The returned
54
- * function ends it; an `error` or an `errorCode` makes it a failed one.
59
+ * For a provider client far from the program: the event lands in the current command's run — a `startCli` command's,
60
+ * or the run `run()` opened — or nowhere.
55
61
  */
56
- export declare const providerRequest: (operation: string, sink?: (event: RequestEvent) => void) => ({ status, error, errorCode }?: {
62
+ export declare const providerEvent: (event: RequestEvent) => void;
63
+ /** What one wire call was, besides its operation: never the URL as sent, its query or its body. */
64
+ export interface ProviderCall {
65
+ method?: string;
66
+ /** The path with ids as `:id`, `/v2/users/:id/meetings`; `endpointTemplate` cuts anything past it. */
67
+ endpointTemplate?: string;
68
+ transport?: RequestEvent["transport"];
69
+ /** 1 for the first try; a retry passes 2 and up, with the wait before it. */
70
+ attempt?: number;
71
+ waitMs?: number;
72
+ bytes?: number;
73
+ }
74
+ export interface ProviderAnswer {
57
75
  status?: number;
58
76
  error?: unknown;
59
77
  errorCode?: string;
60
- }) => void;
78
+ /** The service's own name for a refusal, `rateLimitExceeded`; dropped unless shaped like one. */
79
+ providerError?: string;
80
+ bytes?: number;
81
+ counts?: Record<string, number>;
82
+ ids?: Record<string, string>;
83
+ }
84
+ /**
85
+ * Times one wire call under a fixed operation name — nothing the call carries goes into the run. The returned
86
+ * function ends it; an `error` or an `errorCode` makes it a failed one.
87
+ */
88
+ export declare const providerRequest: (operation: string, sink?: (event: RequestEvent) => void, call?: ProviderCall) => ({ status, error, errorCode, providerError, bytes, counts, ids }?: ProviderAnswer) => void;
61
89
  /**
62
90
  * WC-LIFE-15 for a CLI with its own commander program (`startCli`, not `run()`): adds `--record` and `--no-record`,
63
91
  * keeps every failed command as a run — a usage error before any action too — and every command when recording is
@@ -1,17 +1,25 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { isCliError } from "@wirecat/cli-base";
3
+ import { metaOf } from "@wirecat/cli-base/commands";
4
+ import { providerErrorKey } from "../runs/events.js";
3
5
  import { startRecording, wasSettled } from "../runs/recording.js";
4
- import { KEEP_RUNS_FOR_DAYS } from "../runs/run.js";
5
- import { runContextOf } from "../runs/run-context.js";
6
+ import { KEEP_RUNS_FOR_DAYS, KEEP_SUCCESSFUL_RUNS_FOR_DAYS, RUNS_SIZE_LIMIT_MB } from "../runs/run.js";
7
+ import { keepsSuccess, runContextOf } from "../runs/run-context.js";
8
+ import { currentAttempt, runEvent } from "../runs/scope.js";
6
9
  import { environmentOf } from "./context.js";
7
10
  import { rootOf } from "./profile.js";
8
- /** `--record`/`--no-record` first, then the config; a failure is kept unless either says no (WC-LIFE-15). */
11
+ /**
12
+ * `--record`/`--no-record` first, then the config: every run is kept unless it says `record: false`, which keeps
13
+ * failures only; `--no-record` keeps nothing (WC-LIFE-15).
14
+ */
9
15
  export const runSettings = (flag, saved = {}) => {
10
- const record = flag ?? saved.record ?? false;
16
+ const record = flag ?? saved.record ?? true;
11
17
  return {
12
18
  record,
13
- keepFailedRuns: record || (flag !== false && saved.record !== false),
19
+ keepFailedRuns: record || flag !== false,
14
20
  keepRunsForDays: saved.keepRunsForDays ?? KEEP_RUNS_FOR_DAYS,
21
+ keepSuccessfulRunsForDays: saved.keepSuccessfulRunsForDays ?? KEEP_SUCCESSFUL_RUNS_FOR_DAYS,
22
+ runsSizeLimitMb: saved.runsSizeLimitMb ?? RUNS_SIZE_LIMIT_MB,
15
23
  };
16
24
  };
17
25
  const SKIPPED = new Set(["runs", "help"]);
@@ -23,7 +31,7 @@ const pathOf = (command) => {
23
31
  };
24
32
  const tracker = (options, typed) => {
25
33
  let recording;
26
- const open = (program, words, flag, format) => {
34
+ const open = (program, words, flag, format, lifetime) => {
27
35
  recording = undefined;
28
36
  // Reading the log is not worth a run of its own.
29
37
  if (SKIPPED.has(words[0] ?? ""))
@@ -46,13 +54,22 @@ const tracker = (options, typed) => {
46
54
  const streams = options.streams ?? environment.streams;
47
55
  const env = options.env ?? environment.env;
48
56
  const command = `${agent ? "mcp " : ""}${words.join(" ") || options.app.command}`;
57
+ const caller = environment.agent?.caller;
49
58
  recording = startRecording({
50
59
  app: options.app,
51
60
  command,
52
61
  ...(profile === undefined ? {} : { profile }),
53
- record: settings.record,
62
+ record: keepsSuccess({
63
+ record: settings.record,
64
+ fromFlag: flag !== undefined,
65
+ env: env ?? process.env,
66
+ caller,
67
+ persistent: lifetime === "persistent",
68
+ }),
54
69
  keepFailed: settings.keepFailedRuns,
55
70
  keepDays: settings.keepRunsForDays,
71
+ keepSuccessDays: settings.keepSuccessfulRunsForDays,
72
+ maxBytes: settings.runsSizeLimitMb * 1024 * 1024,
56
73
  trace: false,
57
74
  format,
58
75
  ...(streams ? { streams } : {}),
@@ -63,7 +80,8 @@ const tracker = (options, typed) => {
63
80
  program: rootOf(program),
64
81
  ...((environment.argv ?? typed) ? { argv: environment.argv ?? typed } : {}),
65
82
  env: env ?? process.env,
66
- recordInput: saved.recordInput !== false,
83
+ recordInput: saved.recordInput ?? true,
84
+ caller,
67
85
  }),
68
86
  });
69
87
  };
@@ -76,7 +94,7 @@ const tracker = (options, typed) => {
76
94
  start(action) {
77
95
  const flags = action.optsWithGlobals();
78
96
  const json = (flags.json !== undefined && flags.json !== false) || Boolean(flags.jsonl);
79
- open(action, pathOf(action), flags.record, options.format?.() ?? (json ? "json" : "pretty"));
97
+ open(action, pathOf(action), flags.record, options.format?.() ?? (json ? "json" : "pretty"), metaOf(action).lifetime);
80
98
  },
81
99
  async succeed() {
82
100
  await finish()?.succeed();
@@ -91,24 +109,40 @@ const tracker = (options, typed) => {
91
109
  };
92
110
  export const commandRuns = (options) => tracker(options);
93
111
  const parses = new AsyncLocalStorage();
94
- /** For a provider client far from the program: the event lands in the current command's run, or nowhere. */
95
- export const providerEvent = (event) => parses.getStore()?.runs.events(event);
96
112
  /**
97
- * Times one provider call under a fixed operation name — nothing the call carries goes into the run. The returned
113
+ * For a provider client far from the program: the event lands in the current command's run — a `startCli` command's,
114
+ * or the run `run()` opened — or nowhere.
115
+ */
116
+ export const providerEvent = (event) => {
117
+ const parse = parses.getStore();
118
+ if (parse)
119
+ parse.runs.events(event);
120
+ else
121
+ runEvent(event);
122
+ };
123
+ /**
124
+ * Times one wire call under a fixed operation name — nothing the call carries goes into the run. The returned
98
125
  * function ends it; an `error` or an `errorCode` makes it a failed one.
99
126
  */
100
- export const providerRequest = (operation, sink = providerEvent) => {
101
- sink({ event: "request", operation });
127
+ export const providerRequest = (operation, sink = providerEvent, call = {}) => {
128
+ const { bytes: sent, ...shape } = call;
129
+ const wire = { operation, level: "wire", ...(call.attempt === undefined ? currentAttempt() : {}), ...shape };
130
+ sink({ event: "request", ...wire, ...(sent === undefined ? {} : { bytes: sent }) });
102
131
  const started = performance.now();
103
- return ({ status, error, errorCode } = {}) => {
132
+ return ({ status, error, errorCode, providerError, bytes, counts, ids } = {}) => {
104
133
  const failed = error !== undefined || errorCode !== undefined;
134
+ const key = providerErrorKey(providerError);
105
135
  sink({
106
136
  event: "response",
107
- operation,
137
+ ...wire,
108
138
  ...(status === undefined ? {} : { status }),
109
139
  durationMs: Math.round(performance.now() - started),
110
140
  outcome: failed ? "error" : "ok",
111
141
  ...(failed ? { errorCode: errorCode ?? (isCliError(error) ? error.code : "provider_error") } : {}),
142
+ ...(key ? { providerError: key } : {}),
143
+ ...(bytes === undefined ? {} : { bytes }),
144
+ ...(counts ? { counts } : {}),
145
+ ...(ids ? { ids } : {}),
112
146
  });
113
147
  };
114
148
  };
@@ -144,7 +178,7 @@ const isHelpExit = (error) => {
144
178
  */
145
179
  export const recordRuns = (program, options) => {
146
180
  program
147
- .option("--record", "keep this run, with the command as typed and secrets hidden; record_input false leaves the typed words out")
181
+ .option("--record", "keep this run even where the config says record false; record_input false leaves the typed words out")
148
182
  .option("--no-record", "do not keep this run, even if it fails");
149
183
  program.hook("preAction", (_root, action) => {
150
184
  const parse = parses.getStore();
@@ -6,6 +6,7 @@ import type { MessageEvent } from "../domain/models.js";
6
6
  import type { AppIdentity } from "../ports/app.js";
7
7
  import type { MessengerAdapter } from "../ports/messenger.js";
8
8
  import type { EventSink } from "../runs/events.js";
9
+ import { type RunCaller } from "../runs/run-context.js";
9
10
  import type { OpenRecognizer } from "../speech/transcribe.js";
10
11
  import type { SecretInput } from "../terminal/prompt.js";
11
12
  import { type Closeable } from "./deadline.js";
@@ -95,6 +96,8 @@ export interface AgentRun {
95
96
  authority?: boolean;
96
97
  /** The server's levels allow `bot.api`: a raw Bot API call, past the send guard, may run. */
97
98
  botApi?: boolean;
99
+ /** Who made the MCP call, as the server received it; the run records it instead of the server's environment. */
100
+ caller?: RunCaller;
98
101
  }
99
102
  export interface RelayedRun {
100
103
  /** The profile serve holds; a command for another is refused before it touches the connection. */
@@ -1,8 +1,9 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { metaOf } from "@wirecat/cli-base/commands";
2
3
  import { runBudget } from "../background/budget.js";
3
4
  import { resolveOutput } from "../output.js";
4
5
  import { recorded } from "../runs/recording.js";
5
- import { runContextOf } from "../runs/run-context.js";
6
+ import { currentCaller, keepsSuccess, runContextOf } from "../runs/run-context.js";
6
7
  import { withDeadline } from "./deadline.js";
7
8
  import { withAbort } from "./execution.js";
8
9
  import { rootOf } from "./profile.js";
@@ -101,13 +102,24 @@ export const baseContext = (command, resolveSettings) => {
101
102
  program: rootOf(command),
102
103
  ...(environment.argv ? { argv: environment.argv } : {}),
103
104
  env,
104
- recordInput: settings.recordInput !== false,
105
+ recordInput: settings.recordInput ?? true,
106
+ caller: environment.agent?.caller ?? currentCaller(),
105
107
  }),
106
108
  ...(ranFor === undefined ? {} : { profile: ranFor }),
107
- record: settings.record,
109
+ record: keepsSuccess({
110
+ record: settings.record,
111
+ fromFlag: settings.sources?.record === "flag",
112
+ env,
113
+ caller: environment.agent?.caller ?? currentCaller(),
114
+ persistent: metaOf(command).lifetime === "persistent",
115
+ }),
108
116
  keepFailed: settings.keepFailedRuns,
109
117
  trace: settings.trace,
110
118
  keepDays: settings.keepRunsForDays,
119
+ ...(settings.keepSuccessfulRunsForDays === undefined
120
+ ? {}
121
+ : { keepSuccessDays: settings.keepSuccessfulRunsForDays }),
122
+ ...(settings.runsSizeLimitMb === undefined ? {} : { maxBytes: settings.runsSizeLimitMb * 1024 * 1024 }),
111
123
  processRun: !environment.agent,
112
124
  format,
113
125
  streams,
@@ -26,7 +26,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
26
26
  export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
27
27
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
28
  export { runContextOf, secretInput } from "../runs/run-context.js";
29
- export { currentRun, inRun, type RunScope, useAsProcessRun } from "../runs/scope.js";
29
+ export { type Attempt, currentAttempt, currentRun, inRun, type RunScope, runEvent, useAsProcessRun, withAttempt, } from "../runs/scope.js";
30
30
  export { LANGUAGES } from "../search/languages.js";
31
31
  export { guardedApp, guardedPress, guardedStart, type Pressed } from "../sends/buttons.js";
32
32
  export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
@@ -52,7 +52,7 @@ export { BotTokenStore, type BotTokenStoreOptions } from "./bot/token.js";
52
52
  export { PressLog, UpdatesCursor } from "./bot/updates.js";
53
53
  export type { CommandAvailability, DiscoveredCommand } from "./command-availability.js";
54
54
  export { commandContract, type JsonSchema, SCHEMA_DIALECT } from "./command-contract.js";
55
- export { type CommandRunOptions, type CommandRuns, commandRuns, providerEvent, providerRequest, type RunSettings, recordRuns, runSettings, type SavedRunSettings, } from "./command-runs.js";
55
+ export { type CommandRunOptions, type CommandRuns, commandRuns, type ProviderAnswer, type ProviderCall, providerEvent, providerRequest, type RunSettings, recordRuns, runSettings, type SavedRunSettings, } from "./command-runs.js";
56
56
  export { CONTRACT } from "./commands-command.js";
57
57
  export { refuseUnknownKey } from "./config-command.js";
58
58
  export { type ConfigIssue, type ConfigIssueKind, checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
@@ -88,7 +88,7 @@ export { type MeetingSeriesInventoryReader, readMeetingSeriesInventory } from ".
88
88
  export { accountFileFor, profilesHere, rememberAccount } from "./messenger/accounts.js";
89
89
  export { terminalAsker } from "./messenger/ask.js";
90
90
  export type { CompletionOptions } from "./messenger/complete-command.js";
91
- export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
91
+ export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ProfileState, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
92
92
  export { COMMAND_NEEDS } from "./messenger/lacks.js";
93
93
  export { httpTokenFile, type McpEnvironment, serverEntry } from "./messenger/mcp-command.js";
94
94
  export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
package/dist/cli/index.js CHANGED
@@ -22,7 +22,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
22
22
  export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
23
23
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
24
24
  export { runContextOf, secretInput } from "../runs/run-context.js";
25
- export { currentRun, inRun, useAsProcessRun } from "../runs/scope.js";
25
+ export { currentAttempt, currentRun, inRun, runEvent, useAsProcessRun, withAttempt, } from "../runs/scope.js";
26
26
  export { LANGUAGES } from "../search/languages.js";
27
27
  export { guardedApp, guardedPress, guardedStart } from "../sends/buttons.js";
28
28
  export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
@@ -45,6 +45,9 @@ export const accountCommand = (messenger) => {
45
45
  current: profile === current,
46
46
  account: key?.account ?? null,
47
47
  name: key && store ? await store.accounts.accountName(key) : null,
48
+ ...(messenger.profileState
49
+ ? { signed_in: messenger.profileState(profile, context.env) !== "signed-out" }
50
+ : {}),
48
51
  });
49
52
  }
50
53
  renderAnswer(this, context, wholeList("account list", items));
@@ -49,6 +49,7 @@ export interface LazyStore {
49
49
  }
50
50
  export declare const lazyStore: (env: NodeJS.ProcessEnv, command: string, version?: string) => LazyStore;
51
51
  export declare const storedMessages: (provider: Provider, store: () => Promise<MessageStore>, account: () => Id | null | undefined) => StoredMessages;
52
+ export type ProfileState = "signed-in" | "signed-out" | "bot";
52
53
  /** What one messenger CLI hands the shared commands. Everything else about it stays in its own code. */
53
54
  export interface Messenger extends ServiceMessenger {
54
55
  /**
@@ -65,6 +66,12 @@ export interface Messenger extends ServiceMessenger {
65
66
  * Called inside `--timeout`; the context tracks and closes what it returns.
66
67
  */
67
68
  connect: (command: Command, context: BaseContext, options?: ConnectOptions) => Promise<MessengerAdapter>;
69
+ /**
70
+ * A profile's state, read from the messenger's own files without connecting: `signed-in` has a session, `signed-out`
71
+ * has none yet, `bot` speaks for a bot. `<tool> mcp` with no profile offers the personal ones and marks those not
72
+ * signed in; `account list` and the profile errors say so too. Unset: every profile counts as signed in.
73
+ */
74
+ profileState?: (profile: string, env: NodeJS.ProcessEnv) => ProfileState;
68
75
  /** The help for a `<chat>` argument, in this messenger's words. */
69
76
  chatArgument: string;
70
77
  /** The group settings this messenger has, as `chats update` offers them; every one when unset. */
@@ -262,6 +269,8 @@ export declare const connectedFor: (command: Command, messenger: Pick<Messenger,
262
269
  adapter: MessengerAdapter;
263
270
  close: () => Promise<void>;
264
271
  }>;
272
+ /** `default (not signed in)`: a profile as an error lists it among the choices. */
273
+ export declare const markedProfile: (messenger: Messenger, profile: string, env: NodeJS.ProcessEnv) => string;
265
274
  /**
266
275
  * A write to the local store alone. `deny` already stopped the command (`messengerContext`); `readonly`
267
276
  * stops this write too. There is no question to put to the owner here, so `ask` refuses rather than
@@ -288,9 +288,11 @@ export const connectedFor = async (command, messenger, base, options) => {
288
288
  }),
289
289
  };
290
290
  };
291
- const unrecorded = (command, messenger, profile, { offline, profilesHere }) => {
291
+ /** `default (not signed in)`: a profile as an error lists it among the choices. */
292
+ export const markedProfile = (messenger, profile, env) => messenger.profileState?.(profile, env) === "signed-out" ? `${profile} (not signed in)` : profile;
293
+ const unrecorded = (command, messenger, profile, { offline, profilesHere }, env) => {
292
294
  if (!profilesHere.includes(profile)) {
293
- return new CliError("not_found", `no profile named "${profile}" — profiles here: ${profilesHere.length > 0 ? profilesHere.join(", ") : "none"}`, { profiles: profilesHere });
295
+ return new CliError("not_found", `no profile named "${profile}" — profiles here: ${profilesHere.length > 0 ? profilesHere.map((one) => markedProfile(messenger, one, env)).join(", ") : "none"}`, { profiles: profilesHere });
294
296
  }
295
297
  const words = `${messenger.app.command} ${asFirstWord(profile)}`;
296
298
  const setup = rootOf(command).commands.some((one) => one.name() === "setup") ? `\`${words}setup\`` : undefined;
@@ -361,7 +363,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
361
363
  const accountFor = () => {
362
364
  const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
363
365
  if (!account)
364
- throw unrecorded(command, messenger, profile, base.settings);
366
+ throw unrecorded(command, messenger, profile, base.settings, base.env);
365
367
  return account;
366
368
  };
367
369
  const guard = messenger.guard?.(command, base.settings, warn) ??
@@ -411,7 +413,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
411
413
  offline: base.settings.offline,
412
414
  reads: messenger.history ?? "server",
413
415
  // Turning recording off by name (--no-record, or record: false) keeps no search history either.
414
- history: base.settings.keepFailedRuns,
416
+ history: base.settings.record,
415
417
  searchCatchUp: base.settings.searchCatchUp,
416
418
  pictures: base.settings.pictureSearch,
417
419
  reranker: base.settings.searchReranker,
@@ -222,7 +222,11 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
222
222
  const { app } = messenger;
223
223
  const { env } = context;
224
224
  const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
225
- const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
225
+ const state = (profile) => messenger.profileState?.(profile, env) ?? "signed-in";
226
+ // Read again for each session, so a profile set up while the server runs appears. A bot has its own server.
227
+ const profiles = () => messenger
228
+ .resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true })
229
+ .profilesHere.filter((profile) => state(profile) !== "bot");
226
230
  if (profiles().length === 0)
227
231
  throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
228
232
  const given = environmentOf(self).mcp ?? {};
@@ -235,6 +239,9 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
235
239
  version: app.version,
236
240
  name: messenger.name ?? app.command,
237
241
  profiles,
242
+ signedIn: (profile) => state(profile) === "signed-in",
243
+ signIn: (profile) => `profile ${profile} is not signed in — run \`${app.command} ${profile} setup\`, or sign in from WireCat Desktop, ` +
244
+ "then try again. If the owner did not choose this profile, ask which one.",
238
245
  child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
239
246
  instructions: instructions({
240
247
  command: app.command,
@@ -10,12 +10,19 @@ export const observed = (messenger, events) => {
10
10
  const timed = async (operation, named, work, after) => {
11
11
  const write = currentOperation();
12
12
  const marked = (part = {}) => write === undefined ? part : { ...part, ids: { ...part.ids, operation: write } };
13
- events({ event: "request", operation, ...marked(named) });
13
+ events({ event: "request", operation, level: "operation", ...marked(named) });
14
14
  const started = performance.now();
15
15
  const durationMs = () => Math.round(performance.now() - started);
16
16
  try {
17
17
  const answer = await work();
18
- events({ event: "response", operation, durationMs: durationMs(), outcome: "ok", ...marked(after?.(answer)) });
18
+ events({
19
+ event: "response",
20
+ operation,
21
+ level: "operation",
22
+ durationMs: durationMs(),
23
+ outcome: "ok",
24
+ ...marked(after?.(answer)),
25
+ });
19
26
  return answer;
20
27
  }
21
28
  catch (error) {
@@ -23,6 +30,7 @@ export const observed = (messenger, events) => {
23
30
  events({
24
31
  event: "response",
25
32
  operation,
33
+ level: "operation",
26
34
  durationMs: durationMs(),
27
35
  outcome: "error",
28
36
  errorCode: isCliFailure(error) ? error.code : "generic_failure",
@@ -17,6 +17,14 @@ const EVENT_FIELDS = [
17
17
  "error_code",
18
18
  "provider_error",
19
19
  "code",
20
+ "level",
21
+ "transport",
22
+ "attempt",
23
+ "wait_ms",
24
+ "method",
25
+ "endpoint_template",
26
+ "status",
27
+ "dropped",
20
28
  ];
21
29
  /**
22
30
  * **Everything a report holds is already free of content** — the doctor, the run log and the send
@@ -38,7 +46,7 @@ export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new
38
46
  },
39
47
  run: chosen
40
48
  ? {
41
- metadata: maskedRun(chosen.metadata, program),
49
+ metadata: maskedRun(chosen.metadata, label, program),
42
50
  events: readEvents(chosen.dir).map(eventOf(label, eventFields)),
43
51
  }
44
52
  : null,
@@ -46,8 +54,43 @@ export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new
46
54
  };
47
55
  return JSON.parse(hideHome(JSON.stringify(report), home));
48
56
  };
49
- // A report leaves the machine, so it masks the typed words as `record_input: false` would.
50
- const maskedRun = (metadata, program = new Command()) => metadata.argv ? { ...metadata, argv: maskArgv(program, metadata.argv).argv } : metadata;
57
+ /**
58
+ * A report leaves the machine, so it masks the typed words as `record_input: false` would, and every id that ties it
59
+ * to a session, a connection, a trace, an app or a chat becomes a label, as the events' ids do.
60
+ */
61
+ const maskedRun = (metadata, label, program = new Command()) => {
62
+ const { agent, mcp, partial, traceId } = metadata;
63
+ return {
64
+ ...metadata,
65
+ ...(metadata.argv ? { argv: maskArgv(program, metadata.argv).argv } : {}),
66
+ ...(traceId ? { traceId: label(traceId) } : {}),
67
+ ...(agent
68
+ ? {
69
+ agent: {
70
+ ...agent,
71
+ session_id: agent.session_id === null ? null : label(agent.session_id),
72
+ ...(agent.client
73
+ ? { client: { ...agent.client, ...(agent.client.id ? { id: label(agent.client.id) } : {}) } }
74
+ : {}),
75
+ },
76
+ }
77
+ : {}),
78
+ ...(mcp
79
+ ? {
80
+ mcp: {
81
+ ...mcp,
82
+ ...(mcp.request_id ? { request_id: label(mcp.request_id) } : {}),
83
+ ...(mcp.connection_id ? { connection_id: label(mcp.connection_id) } : {}),
84
+ },
85
+ }
86
+ : {}),
87
+ ...(partial
88
+ ? {
89
+ partial: { ...partial, failures: partial.failures.map((failure) => ({ ...failure, id: label(failure.id) })) },
90
+ }
91
+ : {}),
92
+ };
93
+ };
51
94
  /**
52
95
  * Every field of a journal line that names a chat, a message or a write. A send's `operationId` is
53
96
  * its `sendId`, so one left raw would undo the other's label.
@@ -105,6 +148,8 @@ const RECORDED_ID_FIELDS = new Set([
105
148
  const eventOf = (label, fields) => (event) => {
106
149
  const kept = Object.fromEntries(Object.entries(event)
107
150
  .filter(([key]) => (fields === "recorded" ? RECORDED_FIELDS.has(key) : EVENT_FIELDS.includes(key)))
151
+ // A line from before records were nested holds pino's numeric `level`, not the request's layer.
152
+ .filter(([key, value]) => key !== "level" || typeof value === "string")
108
153
  .map(([key, value]) => [
109
154
  key,
110
155
  fields === "recorded" && RECORDED_ID_FIELDS.has(key) && value !== undefined && value !== null
@@ -39,6 +39,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
39
39
  connection?: "exclusive";
40
40
  pace?: import("../../sends/pace.js").PaceRate;
41
41
  resolveSettings: (flags: import("../settings.js").GlobalFlags, options?: import("../settings.js").ResolveOptions) => import("../settings.js").Settings;
42
+ profileState?: (profile: string, env: NodeJS.ProcessEnv) => import("./context.js").ProfileState;
42
43
  chatArgument: string;
43
44
  groupSettings?: readonly import("../../domain/models.js").GroupSettingKey[];
44
45
  pollQuiz?: boolean;
@@ -13,7 +13,7 @@ export interface ProgramDefinition {
13
13
  permissionKey?: (path: readonly string[]) => string | null | undefined;
14
14
  /** A CLI's own keys (`commandPermissions`), from `settingsFor`: its commands are refused by them before they run. */
15
15
  permissions?: CommandPermissions | undefined;
16
- resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "recordInput" | "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
16
+ resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "recordInput" | "keepSuccessfulRunsForDays" | "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
17
17
  };
18
18
  configure?: (program: Command) => void;
19
19
  /** The tool's pitch and "what you can do" lines on `--help` and a bare run. */