@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +4 -2
  3. package/dist/cli/bot/mcp.js +2 -1
  4. package/dist/cli/command-runs.d.ts +38 -10
  5. package/dist/cli/command-runs.js +51 -17
  6. package/dist/cli/config-command.d.ts +4 -0
  7. package/dist/cli/config-command.js +5 -5
  8. package/dist/cli/context.d.ts +3 -0
  9. package/dist/cli/context.js +15 -3
  10. package/dist/cli/effects.d.ts +9 -0
  11. package/dist/cli/effects.js +27 -0
  12. package/dist/cli/index.d.ts +3 -3
  13. package/dist/cli/index.js +2 -2
  14. package/dist/cli/messenger/context.js +1 -1
  15. package/dist/cli/messenger/observed.js +10 -2
  16. package/dist/cli/messenger/report.js +48 -3
  17. package/dist/cli/program.d.ts +1 -1
  18. package/dist/cli/program.js +12 -5
  19. package/dist/cli/settings.d.ts +24 -6
  20. package/dist/cli/settings.js +26 -4
  21. package/dist/cli/shared-command-exports.d.ts +12 -4
  22. package/dist/cli/skill-command.js +11 -3
  23. package/dist/cli/steps.d.ts +9 -0
  24. package/dist/cli/steps.js +16 -0
  25. package/dist/cli/store-settings.d.ts +6 -0
  26. package/dist/cli/store-settings.js +6 -0
  27. package/dist/entities/index.d.ts +39 -0
  28. package/dist/entities/recorded-run.d.ts +39 -0
  29. package/dist/entities/recorded-run.js +14 -1
  30. package/dist/mcp/agent.d.ts +5 -0
  31. package/dist/mcp/agent.js +3 -1
  32. package/dist/mcp/all-profiles.d.ts +2 -0
  33. package/dist/mcp/all-profiles.js +16 -3
  34. package/dist/mcp/bot/server.d.ts +2 -0
  35. package/dist/mcp/bot/server.js +9 -3
  36. package/dist/mcp/caller.d.ts +38 -0
  37. package/dist/mcp/caller.js +95 -0
  38. package/dist/mcp/http/oauth.js +5 -1
  39. package/dist/mcp/server.js +21 -8
  40. package/dist/mcp/surface.js +11 -2
  41. package/dist/mcp/tool.d.ts +4 -1
  42. package/dist/mcp/tool.js +21 -11
  43. package/dist/runs/events.d.ts +16 -4
  44. package/dist/runs/events.js +4 -0
  45. package/dist/runs/log-attributes.js +7 -1
  46. package/dist/runs/log-events.js +5 -0
  47. package/dist/runs/recording.d.ts +10 -3
  48. package/dist/runs/recording.js +59 -15
  49. package/dist/runs/run-context.d.ts +55 -4
  50. package/dist/runs/run-context.js +34 -3
  51. package/dist/runs/run.d.ts +37 -11
  52. package/dist/runs/run.js +201 -52
  53. package/dist/runs/scope.d.ts +12 -0
  54. package/dist/runs/scope.js +6 -0
  55. package/dist/services/embeddings-maintenance.js +4 -6
  56. package/dist/services/patience.js +5 -1
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,70 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.468.0 — 27.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **An MCP server offers a command that only fetches into the store through its read tool.** A command that reads
15
+ the provider and only keeps what it read in the owner's store (`effects: { remote: "read", local: "write" }`,
16
+ `FETCH`) — `contacts sync`, `metadata sync`, `chats members fetch`, a calendar's `sync` and `calendars list` — is
17
+ now `writes: false` in the catalogue and runs with `<cli>_read`; `<cli>_write` refuses it and names `_read` (owner
18
+ ruling, 2026-10-27; WC-CMD-17). These stay writes: a local edit (`changesStore`, `tags rebuild`), an authority
19
+ change (`grantsAuthority`, `recipients add`), a call to another service (`reachesOtherService`), a command that
20
+ saves files (named `download`, or with a folder to write into: `messages download`, `models download`,
21
+ `attachments extract --output-dir`), and anything that changes the provider. Permissions do not change: a
22
+ `readonly` level still refuses a fetch. New `fetchesOnly(command)` and `fetchesOnlyAt(root)`; `toolsFromCommands`
23
+ takes `fetchOnly`, and without it every write stays one (a CLI with its own server passes
24
+ `fetchOnly: fetchesOnlyAt(program)`). A CLI's `contracts/mcp-catalogue.json` changes where it has such commands.
25
+ - **A CLI that is not a messenger (`AppIdentity.messenger: false`) no longer offers messenger settings.** `config
26
+ set|unset` lists and takes neither `sends_per_hour`, `sender_colors`, `transcribe_with`, `search_catch_up` and
27
+ `read_other_bots` (`settingsFor(...).allSettings` leaves them out; a file that has one is still read) nor the store
28
+ settings of messages — `phone_countries`, `task_*` and `account_groups.<name>` — which it would have changed for
29
+ tg and MAX. `search_stemmers.*` stays. `config show` leaves the same profile settings out.
30
+ - **A model that is not downloaded names its download as the error's `next` step, only in a CLI that has the
31
+ command.** The `model.not-downloaded` error of a text model said `` `<cli> models download <id>` `` in its message,
32
+ also in a CLI without `models` (gcal). Its message is now `<id> is not downloaded on this machine`; the step
33
+ (`models.download`, or `models.download-image` for a picture model) comes as `error.next` with its `line`, the
34
+ profile in it, and only where the program has the command. Any error's `next` is now dropped when it names a
35
+ command the program lacks.
36
+ - **`skill show` offers `follow-ups` and `link-conversations` only in a messenger CLI.** Both work on chats.
37
+
38
+ ## 0.467.0 — 27.10.2026
39
+
40
+ ### Changed — may break callers
41
+
42
+ - **Every run is now recorded by default, successes too** — before, only failures were kept unless `--record` or
43
+ `record: true`. Successful runs are kept 7 days (`keep_successful_runs_for_days`), failures 30
44
+ (`keep_runs_for_days`), and the runs folder stays under `runs_size_limit_mb` (200; the oldest successes go
45
+ first, checked once a day). **Privacy:** with `record_input` on, as by default since 0.439.0, every successful
46
+ `send` now leaves its typed words (message text, a chat typed by title) in a local, 0600 run record for 7 days.
47
+ `record: false` restores failures only; `record_input: "failed"` keeps the typed words only in a run that failed;
48
+ `record_input: false` masks them everywhere; `--no-record` keeps nothing. A success that logged no events is one
49
+ line in the day's index, with no folder; `runs show` finds it there. MCP tools keep successes too. A command that
50
+ runs until stopped (`watch`, `serve`, `mcp`) and a command WireCat Desktop runs for itself
51
+ (`WIRECAT_CALLER=desktop`) keep a success only under `--record`. Search history still follows `record`.
52
+ - **A run says who asked for it.** `run.json` gains `via` (`cli`, `mcp-stdio`, `mcp-http`, `desktop`), `mcp`
53
+ (`tool`, `request_id`, `connection_id`, `proxied`) and `agent.client` (`name`, `version`, and over HTTP `id`, the
54
+ first 8 hex of the SHA-256 of the OAuth client id). Over HTTP the agent is `mcp-client` with the
55
+ `Mcp-Session-Id` as its session, never the environment of whoever started `mcp --http`: such runs were credited to
56
+ 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
57
+ `_meta["wirecat.dev/caller"]`; a profile server trusts it only on a connection that server opened.
58
+ - **Request events gain `level`, `transport`, `attempt` and `wait_ms`**, and `requests` counts wire calls when a
59
+ transport logs them, otherwise operations. `providerRequest` takes a third argument with the call's method, path
60
+ template, transport, attempt, wait and bytes. A run writes at most 2,000 events or 1 MB, then one
61
+ `events.dropped` line and `events_dropped`. `runEvent` reaches the current run from a wire layer handed no sink. A
62
+ retry made above the transport (`patiently`, the bot APIs' rate-limit wait) marks the calls under it with
63
+ `withAttempt`, so their lines say `attempt: 2` and the wait. Events logged after a run finished — a long-running
64
+ server's connection — are dropped rather than written into it.
65
+ - **`record: false` in the config now keeps failures**; before, it kept nothing. `--no-record` still keeps nothing.
66
+
67
+ ### Fixed
68
+
69
+ - **`doctor reports create` labels the ids a run's metadata keeps** — the agent session, the OAuth client id, the
70
+ MCP connection and request, the trace and a failed chat — as it already labelled the events' ids.
71
+
8
72
  ## 0.466.0 — 27.10.2026
9
73
 
10
74
  ### 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,
@@ -3,7 +3,7 @@ import { CliError } from "@wirecat/cli-base";
3
3
  import { annotate, describeProgram } from "@wirecat/cli-base/commands";
4
4
  import { Command } from "commander";
5
5
  import { environmentOf } from "../context.js";
6
- import { changesAuthorityAt, LOCAL_READ, REMOTE_WRITE } from "../effects.js";
6
+ import { changesAuthorityAt, fetchesOnlyAt, LOCAL_READ, REMOTE_WRITE } from "../effects.js";
7
7
  import { itemOf, renderAnswer } from "../envelope.js";
8
8
  import { serverEntry } from "../messenger/mcp-command.js";
9
9
  import { nextStep, rootOf } from "../steps.js";
@@ -37,6 +37,7 @@ export const botMcpCommand = (bot) => {
37
37
  bot,
38
38
  program: { run, commands: describeProgram(rootOf(this)) },
39
39
  authority: changesAuthorityAt(rootOf(this)),
40
+ fetchOnly: fetchesOnlyAt(rootOf(this)),
40
41
  settings: context.settings,
41
42
  levels: () => bot.resolveSettings(this.optsWithGlobals(), { env: context.env, kind: "bot" }).permissions,
42
43
  env: context.env,
@@ -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();
@@ -2,6 +2,10 @@ import { Command } from "commander";
2
2
  import { type AppIdentity } from "../ports/app.js";
3
3
  import { type PermissionKeySource } from "./permission-keys.js";
4
4
  import { type Configuration } from "./settings.js";
5
+ /**
6
+ * The settings in force and where each came from, and changing them. Printed whole: no field the
7
+ * configuration accepts can hold a secret — the schema has nowhere to put one.
8
+ */
5
9
  export declare const configCommand: (app: AppIdentity, config: Configuration, { permissionKey }?: {
6
10
  permissionKey?: PermissionKeySource | undefined;
7
11
  }) => Command;
@@ -9,13 +9,11 @@ import { itemOf, renderAnswer } from "./envelope.js";
9
9
  import { knownBeside, knownPermissionKeys, unknownPermissionIssues, } from "./permission-keys.js";
10
10
  import { fromFile, NONE_NAMED } from "./settings.js";
11
11
  import { nextStep } from "./steps.js";
12
- import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
12
+ import { changeStoreSetting, isStoreSetting, offersStoreSetting, storeSettings, storeSettingsOf, } from "./store-settings.js";
13
13
  /**
14
14
  * The settings in force and where each came from, and changing them. Printed whole: no field the
15
15
  * configuration accepts can hold a secret — the schema has nowhere to put one.
16
16
  */
17
- /** `sender_colors` colours who wrote a message; a CLI without messages has nothing to colour. */
18
- const shownSettings = (app, config) => app.messenger === false ? config.allSettings.filter((setting) => setting !== "sender_colors") : config.allSettings;
19
17
  export const configCommand = (app, config, { permissionKey = config.permissions } = {}) => {
20
18
  const command = new Command("config").description("the settings in force, and where each one came from");
21
19
  annotate(command.command("show"), { effects: LOCAL_READ, examples: [`${app.command} personal config show`] })
@@ -38,7 +36,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions
38
36
  config_file: settings.configPath,
39
37
  config_found: settings.configFound,
40
38
  paths_overridden: overridden,
41
- settings: [...shownSettings(app, config), "command_timeout_ms"]
39
+ settings: [...config.allSettings, "command_timeout_ms"]
42
40
  .filter((setting) => bot || setting !== "read_other_bots")
43
41
  .map((setting) => sourced(settings, setting)),
44
42
  store_settings: await storeSettings(env),
@@ -58,7 +56,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions
58
56
  effects: LOCAL_WRITE,
59
57
  examples: [`${app.command} personal config ${action} limit${action === "set" ? " 20" : ""}`],
60
58
  }))
61
- .argument("<setting>", `one of: ${[...shownSettings(app, config), ...STORE_SETTINGS, "account_groups.<name>"].join(", ")}`)
59
+ .argument("<setting>", `one of: ${[...config.allSettings, ...storeSettingsOf(app)].join(", ")}`)
62
60
  .option("--defaults", "change what every profile gets, rather than this profile")
63
61
  .option("--personal", "only for personal accounts — the personal section of the file")
64
62
  .option("--bot", "only for bots — the bot section of the file");
@@ -72,6 +70,8 @@ export const configCommand = (app, config, { permissionKey = config.permissions
72
70
  const { defaults: everyone, personal, bot, } = this.opts();
73
71
  const context = baseContext(this, (flags, options) => config.resolveSettings(flags, { ...options, needsProfile: false, withIssues: true }));
74
72
  const { settings, renderer, env } = context;
73
+ if (isStoreSetting(setting) && !offersStoreSetting(app, setting))
74
+ throw new CliError("validation_error", `no setting called "${setting}" here — it belongs to the messenger CLIs; one of: ${[...config.allSettings, ...storeSettingsOf(app)].join(", ")}`);
75
75
  if (isStoreSetting(setting)) {
76
76
  if (everyone || personal || bot)
77
77
  throw new CliError("validation_error", `${setting} is store-wide — --defaults, --personal and --bot do not apply`);
@@ -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,
@@ -41,6 +41,15 @@ export declare const isLogin: (command: Command) => boolean;
41
41
  export declare const destroysNothing: <T extends Command>(command: T) => T;
42
42
  /** A local edit remains an edit even when it first fetches metadata from the messenger. */
43
43
  export declare const changesStore: <T extends Command>(command: T) => T;
44
+ /**
45
+ * Reads the provider and only keeps what it read in the owner's store — a sync, a list that refreshes it. An MCP server
46
+ * offers it through the read tool (owner ruling, 2026-10-27; WC-CMD-17): it changes nothing at the provider or for
47
+ * other people. A local edit (`changesStore`), an authority change, a call to another service (`reachesOtherService`,
48
+ * a model download) and a command that saves files (a download, a folder to write into) stay writes.
49
+ */
50
+ export declare const fetchesOnly: (command: Command) => boolean;
51
+ /** `fetchesOnly` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
52
+ export declare const fetchesOnlyAt: (root: Command) => (path: readonly string[]) => boolean;
44
53
  /**
45
54
  * Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
46
55
  * is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
@@ -66,6 +66,33 @@ export const changesStore = (command) => {
66
66
  STORE_WRITES.add(command);
67
67
  return command;
68
68
  };
69
+ /** A download or a folder to write into: files outside the store, which the owner places, not an agent. */
70
+ const savesFiles = (command) => command.name() === "download" ||
71
+ [...command.options.map((option) => option.attributeName()), ...command.registeredArguments.map((one) => one.name())]
72
+ .map((name) => name.toLowerCase())
73
+ .some((name) => name === "dir" || name.endsWith("dir") || name === "output");
74
+ /**
75
+ * Reads the provider and only keeps what it read in the owner's store — a sync, a list that refreshes it. An MCP server
76
+ * offers it through the read tool (owner ruling, 2026-10-27; WC-CMD-17): it changes nothing at the provider or for
77
+ * other people. A local edit (`changesStore`), an authority change, a call to another service (`reachesOtherService`,
78
+ * a model download) and a command that saves files (a download, a folder to write into) stay writes.
79
+ */
80
+ export const fetchesOnly = (command) => {
81
+ const effects = metaOf(command).effects;
82
+ return (effects?.remote === "read" &&
83
+ effects.local === "write" &&
84
+ !STORE_WRITES.has(command) &&
85
+ !changesAuthority(command) &&
86
+ !OTHER_SERVICE.has(command) &&
87
+ !savesFiles(command));
88
+ };
89
+ /** `fetchesOnly` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
90
+ export const fetchesOnlyAt = (root) => (path) => {
91
+ let at = root;
92
+ for (const word of path)
93
+ at = at?.commands.find((one) => one.name() === word);
94
+ return at !== undefined && fetchesOnly(at);
95
+ };
69
96
  /**
70
97
  * Whether `<PREFIX>_READ_ONLY` refuses the command as a change to the store. Every write that stays on this machine
71
98
  * is refused unless `destroysNothing` marks it, so a new one fails closed (WC-PERM-12). A fetch (`remote: "read"`)
@@ -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";
@@ -63,7 +63,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
63
63
  export { type Closeable, withDeadline } from "./deadline.js";
64
64
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
65
65
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
66
- export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
66
+ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
67
67
  export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
68
68
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
69
69
  export { withAbort } from "./execution.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";
@@ -56,7 +56,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
56
56
  export { withDeadline } from "./deadline.js";
57
57
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
58
58
  export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
59
- export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
59
+ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, previewFlag, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, storeChangeRefusal, } from "./effects.js";
60
60
  export { startCli } from "./entry.js";
61
61
  export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
62
62
  export { withAbort } from "./execution.js";
@@ -413,7 +413,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
413
413
  offline: base.settings.offline,
414
414
  reads: messenger.history ?? "server",
415
415
  // Turning recording off by name (--no-record, or record: false) keeps no search history either.
416
- history: base.settings.keepFailedRuns,
416
+ history: base.settings.record,
417
417
  searchCatchUp: base.settings.searchCatchUp,
418
418
  pictures: base.settings.pictureSearch,
419
419
  reranker: base.settings.searchReranker,
@@ -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",