@wirecat/cli-core 0.438.0 → 0.440.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 CHANGED
@@ -7,6 +7,60 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.440.0 — 26.10.2026
11
+
12
+ ### Added
13
+
14
+ - **A CLI's own command can keep its command line in its run record.** A command the CLI records itself with
15
+ `recorded` or `startRecording` kept no `argv` and no agent, since only cli-core's own commands built that part of the
16
+ record. `runContextOf` and the `RunContext` type are now exported from `@wirecat/cli-core/cli`. To adopt, pass
17
+ `context: runContextOf({ bin, command, program: rootOf(command), argv: environmentOf(command).argv, env,
18
+ recordInput: settings.recordInput !== false })` to the call; a CLI with its own program passes its own root command
19
+ and the words it parsed. Left out, `recordInput` masks every value that may be content. A CLI that records through
20
+ `commandRuns` under `run()` gets the command line with no change; its `settings()` may return `recordInput`, and when
21
+ it does not, the typed words are kept with secrets hidden.
22
+ - **`secretInput` also marks a whole command**: every word typed after it that is not an option is `[redacted]` in a
23
+ run record. For a command that refuses its arguments, such as one that asks for a phone number instead of taking it,
24
+ this hides what was typed without declaring an argument, so its usage line and contract stay the same.
25
+
26
+ ### Changed — may break callers
27
+
28
+ - **`--record` help says what a run keeps now.** It read "ids and timings, never content", which stopped being true
29
+ in 0.439.0; it now reads "keep this run, with the command as typed and secrets hidden; record_input false leaves the
30
+ typed words out". Every CLI's help snapshot and `docs/commands.md` change with it. `recordRuns` no longer takes
31
+ `content`, which only that line used.
32
+
33
+ ### Security
34
+
35
+ - **A phone number no longer reaches a run record.** Since 0.439.0 a run keeps the words as typed, so a number typed
36
+ after `contacts lookup`, which the command refuses, was kept in `argv` in the clear. The command is now marked with
37
+ `secretInput`, so every word typed after it is `[redacted]`. A phone number typed anywhere else, message text included, is replaced by `[redacted]` inside the word,
38
+ the rest kept: one written with its "+" and country code, or 11 to 15 digits in a row, the shapes a kept error
39
+ already masks. A positive id of 11 to 15 digits, such as an 11-digit Zoom meeting id or a time in milliseconds, is
40
+ hidden as well; a negative chat id is kept. A number with no "+" written in groups (`8 912 345-67-89`) is not
41
+ recognised.
42
+
43
+ ## 0.439.0 — 26.10.2026
44
+
45
+ ### Changed — may break callers
46
+
47
+ - **A run record keeps the command line as typed.** `argv` in `run.json` and `runs show` now holds the words the person
48
+ typed — a chat's title, a search, message text — so the history of what was run can be read back. A secret is
49
+ still always `[redacted]`: the value of an option or argument a CLI marks with the new `secretInput(...)` (exported
50
+ from `@wirecat/cli-core/cli`), one whose name says token, password, secret, credential, cookie, authorization or
51
+ API key, a password inside a URL, and a long opaque string. `bot api … --body` is marked secret, since a field the
52
+ API marks secret can ride in it. The new per-profile setting `record_input: false` (`config set record_input false`)
53
+ brings back the previous masking exactly. A configuration that will not load also masks, since it may be the one
54
+ that turned recording off. A CLI with its own `resolveSettings` may return `recordInput`; when it does not, typed
55
+ words are kept. `doctor reports create` still masks the run's words as before, since the report is sent to other
56
+ people. `config set` and `config show` list `record_input`, so a CLI's help snapshot changes. A test or document
57
+ that says a run never holds what was typed needs updating.
58
+
59
+ ### Security
60
+
61
+ - **An error that quotes `--token=…` or `password=…` no longer keeps the value.** A kept error masked such a value
62
+ only when a space followed the `=` or `:`; Commander's `unknown option '--token=…'` kept it whole.
63
+
10
64
  ## 0.438.0 — 26.10.2026
11
65
 
12
66
  ### Security
package/README.md CHANGED
@@ -217,8 +217,8 @@ they deliberately differ. A rule changes in its own pull request, before the cod
217
217
  - **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
218
218
  wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
219
219
  choices — even when there is only one.
220
- - **Run records keep ids and timings, never content.** A failure is kept by default, so `runs show` can explain it
221
- without anyone sending a log full of messages.
220
+ - **Run records keep the command as typed, never a secret.** A failure is kept by default, so `runs show` can explain
221
+ it; a token, a password or a phone number is hidden, and `record_input: false` leaves the typed words out too.
222
222
  - **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
223
223
  sleep. A coverage floor: 92% of lines, and at least half of every file.
224
224
  - **Contract cases for every adapter.** `contractCases` turns the port's promises into cases any test runner can run,
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command, Option } from "commander";
4
+ import { secretInput } from "../../runs/run-context.js";
4
5
  import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
5
6
  import { apiFlagOf, apiOptionKey, readApiBody } from "./api-input.js";
6
7
  export const generatedApiCommand = (api) => {
@@ -20,7 +21,8 @@ export const generatedApiCommand = (api) => {
20
21
  command.addOption(parameter.required && parameter.in !== "body" ? option.makeOptionMandatory() : option);
21
22
  }
22
23
  if (operation.request) {
23
- command.option("--body <json>", "the request body as JSON; - reads it from stdin");
24
+ // A field the schema marks secret may ride in an inline body, and the flags never see it.
25
+ command.addOption(secretInput(new Option("--body <json>", "the request body as JSON; - reads it from stdin")));
24
26
  command.option("--body-file <path>", "the request body from a JSON file; - is stdin");
25
27
  }
26
28
  annotate(command, {
@@ -6,6 +6,8 @@ import type { RequestEvent } from "../runs/events.js";
6
6
  export interface SavedRunSettings {
7
7
  record?: boolean | undefined;
8
8
  keepRunsForDays?: number | undefined;
9
+ /** `record_input`: left out, the typed words are kept, secrets hidden. */
10
+ recordInput?: boolean | undefined;
9
11
  }
10
12
  export interface RunSettings {
11
13
  record: boolean;
@@ -59,6 +61,4 @@ export declare const providerRequest: (operation: string, sink?: (event: Request
59
61
  * on. Call it after the program's other `parseAsync` wrappers, so its own is the outermost and sees how the parse
60
62
  * really ended; a refusal in an earlier `preAction` hook is then kept under the words read from the line.
61
63
  */
62
- export declare const recordRuns: (program: Command, options: CommandRunOptions & {
63
- content: string;
64
- }) => void;
64
+ export declare const recordRuns: (program: Command, options: CommandRunOptions) => void;
@@ -2,7 +2,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { isCliError } from "@wirecat/cli-base";
3
3
  import { startRecording, wasSettled } from "../runs/recording.js";
4
4
  import { KEEP_RUNS_FOR_DAYS } from "../runs/run.js";
5
+ import { runContextOf } from "../runs/run-context.js";
5
6
  import { environmentOf } from "./context.js";
7
+ import { rootOf } from "./profile.js";
6
8
  /** `--record`/`--no-record` first, then the config; a failure is kept unless either says no (WC-LIFE-15). */
7
9
  export const runSettings = (flag, saved = {}) => {
8
10
  const record = flag ?? saved.record ?? false;
@@ -41,9 +43,10 @@ const tracker = (options) => {
41
43
  const agent = options.agent ?? environment.agent !== undefined;
42
44
  const streams = options.streams ?? environment.streams;
43
45
  const env = options.env ?? environment.env;
46
+ const command = `${agent ? "mcp " : ""}${words.join(" ") || options.app.command}`;
44
47
  recording = startRecording({
45
48
  app: options.app,
46
- command: `${agent ? "mcp " : ""}${words.join(" ") || options.app.command}`,
49
+ command,
47
50
  ...(profile === undefined ? {} : { profile }),
48
51
  record: settings.record,
49
52
  keepFailed: settings.keepFailedRuns,
@@ -52,6 +55,14 @@ const tracker = (options) => {
52
55
  format,
53
56
  ...(streams ? { streams } : {}),
54
57
  ...(env ? { env } : {}),
58
+ context: runContextOf({
59
+ bin: options.app.command,
60
+ command: words.length > 0 ? command : "",
61
+ program: rootOf(program),
62
+ ...(environment.argv ? { argv: environment.argv } : {}),
63
+ env: env ?? process.env,
64
+ recordInput: saved.recordInput !== false,
65
+ }),
55
66
  });
56
67
  };
57
68
  const finish = () => {
@@ -131,7 +142,7 @@ const isHelpExit = (error) => {
131
142
  */
132
143
  export const recordRuns = (program, options) => {
133
144
  program
134
- .option("--record", `keep this run — ids and timings, never ${options.content}`)
145
+ .option("--record", "keep this run, with the command as typed and secrets hidden; record_input false leaves the typed words out")
135
146
  .option("--no-record", "do not keep this run, even if it fails");
136
147
  program.hook("preAction", (_root, action) => {
137
148
  const parse = parses.getStore();
@@ -62,7 +62,7 @@ export interface BaseEnvironment {
62
62
  handled?: (event: MessageEvent) => void;
63
63
  /** Set by an MCP server on a command it runs for an agent (`runCommandForAgent`). */
64
64
  agent?: AgentRun;
65
- /** Set by `run()`: the words it was given, which a run keeps masked (`maskArgv`). */
65
+ /** Set by `run()`: the words it was given, which a run keeps as `record_input` says (`maskArgv`). */
66
66
  argv?: readonly string[];
67
67
  /**
68
68
  * Set by `run()`: the rest of the command has no default deadline, and Ctrl-C, SIGTERM, a cancel or an explicit
@@ -86,6 +86,7 @@ export const baseContext = (command, resolveSettings) => {
86
86
  program: rootOf(command),
87
87
  ...(environment.argv ? { argv: environment.argv } : {}),
88
88
  env,
89
+ recordInput: settings.recordInput !== false,
89
90
  }),
90
91
  ...(ranFor === undefined ? {} : { profile: ranFor }),
91
92
  record: settings.record,
@@ -24,7 +24,8 @@ export { repliesStatePathFor } from "../replies/state.js";
24
24
  export { type CacheEvent, type DiagnosticEvent, type EventSink, providerErrorKey, type RequestEvent, renderEvent, type WarningEvent, } from "../runs/events.js";
25
25
  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
- export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
27
+ export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
+ export { runContextOf, secretInput } from "../runs/run-context.js";
28
29
  export { currentRun, inRun, type RunScope, useAsProcessRun } from "../runs/scope.js";
29
30
  export { LANGUAGES } from "../search/languages.js";
30
31
  export { guardedApp, guardedPress, guardedStart, type Pressed } from "../sends/buttons.js";
package/dist/cli/index.js CHANGED
@@ -21,6 +21,7 @@ export { providerErrorKey, renderEvent, } from "../runs/events.js";
21
21
  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
+ export { runContextOf, secretInput } from "../runs/run-context.js";
24
25
  export { currentRun, inRun, useAsProcessRun } from "../runs/scope.js";
25
26
  export { LANGUAGES } from "../search/languages.js";
26
27
  export { guardedApp, guardedPress, guardedStart } from "../sends/buttons.js";
@@ -3,6 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { defineQuery } from "@wirecat/cli-base/query";
4
4
  import { Command } from "commander";
5
5
  import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
6
+ import { secretInput } from "../../runs/run-context.js";
6
7
  import { levelFor } from "../../sends/permissions.js";
7
8
  import { phoneOf } from "../../services/index.js";
8
9
  import { dateWindow } from "../../services/moment.js";
@@ -276,7 +277,7 @@ export const contactsCommand = (messenger) => {
276
277
  * ⚠ **The number is asked for or piped, never an argument**: argv is read by `ps` and kept by shell
277
278
  * history, and a phone number is personal data.
278
279
  */
279
- annotate(contacts.command("lookup"), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
280
+ annotate(secretInput(contacts.command("lookup")), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
280
281
  .summary("find an account by a phone number")
281
282
  .description("who has this phone number — asks for it, or reads it from stdin; never an argument")
282
283
  // Commander's own refusal would repeat the number on stderr.
@@ -14,7 +14,7 @@ import { destroysNothing, LOCAL_WRITE, NO_EFFECTS, REMOTE_READ } from "../effect
14
14
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
15
15
  import { unknownPermissionIssues } from "../permission-keys.js";
16
16
  import { NONE_NAMED } from "../settings.js";
17
- import { lineOf, nextStep } from "../steps.js";
17
+ import { lineOf, nextStep, rootOf } from "../steps.js";
18
18
  import { recalledAccount } from "./accounts.js";
19
19
  import { messengerContext } from "./context.js";
20
20
  import { privateFiles, withSqliteSidecars } from "./private-files.js";
@@ -100,6 +100,7 @@ const reportCommand = (messenger) => {
100
100
  sends: profile ? new SendJournal(sendsPathFor(app, profile, env)).entries() : [],
101
101
  now,
102
102
  home: env.HOME ?? env.USERPROFILE,
103
+ program: rootOf(this),
103
104
  ...(options.run === undefined ? {} : { runId: options.run }),
104
105
  });
105
106
  const path = resolve(options.output ?? reportFileName(app.command, now));
@@ -1,3 +1,4 @@
1
+ import { Command } from "commander";
1
2
  import { type RunMetadata } from "../../runs/run.js";
2
3
  import type { SendEntry } from "../../sends/journal.js";
3
4
  export interface Report<Doctor extends object = Record<string, unknown>> {
@@ -29,7 +30,7 @@ export interface ReportContext {
29
30
  * journal are built that way. Two things are added: every id becomes a label, and the home
30
31
  * directory is hidden, since paths name the person's account on this machine.
31
32
  */
32
- export declare const buildReport: <Doctor extends object, Context extends ReportContext = Record<never, never>>({ doctor, runsDir, runId, command, sends, now, home, context, eventFields, }: {
33
+ export declare const buildReport: <Doctor extends object, Context extends ReportContext = Record<never, never>>({ doctor, runsDir, runId, command, sends, now, home, context, eventFields, program, }: {
33
34
  doctor: Doctor;
34
35
  runsDir: string;
35
36
  runId?: string;
@@ -41,6 +42,8 @@ export declare const buildReport: <Doctor extends object, Context extends Report
41
42
  context?: Context;
42
43
  /** Retain allowlisted recorded fields, including time and severity, for an existing consumer contract. */
43
44
  eventFields?: "report" | "recorded";
45
+ /** The CLI's program, which tells a flag's value from a command word when the run's words are masked. */
46
+ program?: Command;
44
47
  }) => Report<Doctor> & Omit<Context, keyof Report>;
45
48
  /**
46
49
  * Every field of a journal line that names a chat, a message or a write. A send's `operationId` is
@@ -1,8 +1,10 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { CliError } from "@wirecat/cli-base";
4
+ import { Command } from "commander";
4
5
  import { LOG_ATTRIBUTES } from "../../runs/log-attributes.js";
5
6
  import { findRun, listRuns, readEvents } from "../../runs/run.js";
7
+ import { maskArgv } from "../../runs/run-context.js";
6
8
  const RECENT_SENDS = 20;
7
9
  /** Only these fields of a run event travel; a field added later is left out until it is named here. */
8
10
  const EVENT_FIELDS = [
@@ -21,7 +23,7 @@ const EVENT_FIELDS = [
21
23
  * journal are built that way. Two things are added: every id becomes a label, and the home
22
24
  * directory is hidden, since paths name the person's account on this machine.
23
25
  */
24
- export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new Date(), home, context, eventFields = "report", }) => {
26
+ export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new Date(), home, context, eventFields = "report", program, }) => {
25
27
  const chosen = runId === undefined ? newestFailed(runsDir) : findRun(runsDir, runId);
26
28
  if (runId !== undefined && !chosen)
27
29
  throw new CliError("not_found", `no run ${runId} — \`${command} runs list\` names them`);
@@ -34,11 +36,18 @@ export const buildReport = ({ doctor, runsDir, runId, command, sends, now = new
34
36
  ...doctor,
35
37
  ...(account?.remembered ? { account: { ...account, remembered: label(account.remembered) } } : {}),
36
38
  },
37
- run: chosen ? { metadata: chosen.metadata, events: readEvents(chosen.dir).map(eventOf(label, eventFields)) } : null,
39
+ run: chosen
40
+ ? {
41
+ metadata: maskedRun(chosen.metadata, program),
42
+ events: readEvents(chosen.dir).map(eventOf(label, eventFields)),
43
+ }
44
+ : null,
38
45
  sends: sends.slice(-RECENT_SENDS).map((entry) => labelled(entry, label)),
39
46
  };
40
47
  return JSON.parse(hideHome(JSON.stringify(report), home));
41
48
  };
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;
42
51
  /**
43
52
  * Every field of a journal line that names a chat, a message or a write. A send's `operationId` is
44
53
  * its `sendId`, so one left raw would undo the other's label.
@@ -10,7 +10,7 @@ export interface ProgramDefinition {
10
10
  /** The CLI's own settings, for keeping a failure that happened before its command could; plain ones without. */
11
11
  configuration?: {
12
12
  permissionKey?: (path: readonly string[]) => string | null | undefined;
13
- resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
13
+ resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "recordInput" | "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
14
14
  };
15
15
  configure?: (program: Command) => void;
16
16
  /** The tool's pitch and "what you can do" lines on `--help` and a bare run. */
@@ -69,7 +69,7 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
69
69
  .option("--full", `${found} with their whole text and every provider field`)
70
70
  .option("--dry-run", "preview parsed arguments and permissions before running the action")
71
71
  .option("--yes", "go ahead without the question an ask level puts before a write")
72
- .option("--record", `keep this run — ids and timings, never ${content}`)
72
+ .option("--record", "keep this run, with the command as typed and secrets hidden; record_input false leaves the typed words out")
73
73
  .option("--no-record", "do not keep it, whatever the configuration says")
74
74
  .showHelpAfterError();
75
75
  for (const command of mounted)
@@ -574,7 +574,8 @@ const settleFailure = async (failure, state) => {
574
574
  /**
575
575
  * **Every failure is kept as a run** — a usage error, a configuration that will not load, a command
576
576
  * that never opens a run — unless recording was turned off. One its own run already kept is skipped.
577
- * Its arguments are kept masked (`maskArgv`): a value that can be a message never is.
577
+ * Its words are kept as `record_input` says (`maskArgv`); a configuration that did not load masks them, since it may
578
+ * be the one that turned recording input off.
578
579
  */
579
580
  const keepFailure = async (failure, { definition, program, rest, argv, profile, options }) => {
580
581
  if (wasSettled(failure))
@@ -599,6 +600,7 @@ const keepFailure = async (failure, { definition, program, rest, argv, profile,
599
600
  program,
600
601
  argv,
601
602
  env,
603
+ recordInput: settings ? settings.recordInput !== false : false,
602
604
  }),
603
605
  // Settings that did not load never accepted the word, which may be a refused name or somebody's words.
604
606
  profile: settings ? namedProfile(settings) : undefined,
@@ -97,6 +97,11 @@ export interface Settings extends AISettings {
97
97
  */
98
98
  commandTimeoutMs: number | undefined;
99
99
  record: boolean;
100
+ /**
101
+ * A run record keeps the words typed, secrets hidden; off masks every value that may be content. Optional so a CLI
102
+ * with settings of its own need not return it: left out, the words are kept.
103
+ */
104
+ recordInput?: boolean;
100
105
  /** A failed run is kept even unrecorded, unless recording was turned off by name. */
101
106
  keepFailedRuns: boolean;
102
107
  keepRunsForDays: number;
@@ -265,6 +270,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
265
270
  color: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
266
271
  sender_colors: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
267
272
  record: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
273
+ record_input: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
268
274
  keep_runs_for_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
269
275
  permissions: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, (issue: v.RegexIssue<string>) => string>]>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
270
276
  sends_per_hour: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
@@ -338,6 +344,7 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
338
344
  color: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
339
345
  sender_colors: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
340
346
  record: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
347
+ record_input: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
341
348
  keep_runs_for_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
342
349
  permissions: v.OptionalSchema<v.RecordSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, (issue: v.RegexIssue<string>) => string>]>, v.PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: v.PicklistIssue) => string>, (issue: v.BaseIssue<unknown>) => string>, undefined>;
343
350
  sends_per_hour: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
@@ -56,6 +56,7 @@ const SHARED_PROFILE_ENTRIES = {
56
56
  color: v.optional(flag),
57
57
  sender_colors: v.optional(flag),
58
58
  record: v.optional(flag),
59
+ record_input: v.optional(flag),
59
60
  keep_runs_for_days: v.optional(count),
60
61
  permissions: v.optional(permissionLevels),
61
62
  sends_per_hour: v.optional(count),
@@ -231,6 +232,7 @@ export const settingsFor = (app, extension = {}) => {
231
232
  const searchCatchUp = fromLayers("search_catch_up", false);
232
233
  const pictureSearch = fromLayers("picture_search", false);
233
234
  const searchReranker = fromLayers("search_reranker", DEFAULT_RERANK_MODEL);
235
+ const recordInput = fromLayers("record_input", true);
234
236
  const keepRunsForDays = fromLayers("keep_runs_for_days", DEFAULT_KEEP_RUNS_FOR_DAYS);
235
237
  // Set where an agent runs unattended; the config file and `--permission` cannot undo it.
236
238
  const readOnlyLock = readOnlyLockOf(app, env);
@@ -287,6 +289,7 @@ export const settingsFor = (app, extension = {}) => {
287
289
  ? undefined
288
290
  : (extension.parseDuration ?? parseDuration)(timeout.value, timeout.from === TIMEOUT ? TIMEOUT : "--timeout"),
289
291
  record: record.value,
292
+ recordInput: recordInput.value,
290
293
  keepFailedRuns: record.value || record.from === "default",
291
294
  keepRunsForDays: keepRunsForDays.value,
292
295
  ...(readOnlyLock ? { readOnlyLock } : {}),
@@ -314,6 +317,7 @@ export const settingsFor = (app, extension = {}) => {
314
317
  pictureSearch: pictureSearch.from,
315
318
  searchReranker: searchReranker.from,
316
319
  record: record.from,
320
+ recordInput: recordInput.from,
317
321
  keepRunsForDays: keepRunsForDays.from,
318
322
  sendsPerHour: sendsPerHour.from,
319
323
  requestsPerMinute: requestsPerMinute.from,
@@ -79,6 +79,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
79
79
  color: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
80
80
  sender_colors: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
81
81
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
82
+ record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
82
83
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
83
84
  permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
84
85
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
@@ -152,6 +153,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
152
153
  color: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
153
154
  sender_colors: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
154
155
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
156
+ record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
155
157
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
156
158
  permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
157
159
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
@@ -280,6 +282,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
280
282
  color: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
281
283
  sender_colors: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
282
284
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
285
+ record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
283
286
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
284
287
  permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
285
288
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
@@ -353,6 +356,7 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
353
356
  color: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
354
357
  sender_colors: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
355
358
  record: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
359
+ record_input: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
356
360
  keep_runs_for_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
357
361
  permissions: import("valibot").OptionalSchema<import("valibot").RecordSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").StringSchema<undefined>, import("valibot").RegexAction<string, (issue: import("valibot").RegexIssue<string>) => string>]>, import("valibot").PicklistSchema<readonly ["deny", "readonly", "ask", "allow"], (issue: import("valibot").PicklistIssue) => string>, (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
358
362
  sends_per_hour: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
@@ -34,6 +34,9 @@ export declare const framesOf: (error: unknown, limit?: number) => string[];
34
34
  * or a token in the path as `:id` / `:token`. The host stays — which service refused is the point.
35
35
  */
36
36
  export declare const endpointTemplate: (raw: string) => string;
37
+ /** A phone number as people type it with its country code, and one written as digits only without the "+". */
38
+ export declare const INTERNATIONAL_PHONE: string;
39
+ export declare const BARE_PHONE: string;
37
40
  /**
38
41
  * An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
39
42
  * phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
@@ -109,6 +109,9 @@ export const endpointTemplate = (raw) => {
109
109
  });
110
110
  return `${scheme}${segments.join("/")}`.slice(0, 300);
111
111
  };
112
+ /** A phone number as people type it with its country code, and one written as digits only without the "+". */
113
+ export const INTERNATIONAL_PHONE = /\+\d[\d\s()-]{6,}\d/.source;
114
+ export const BARE_PHONE = /(^|[^\w-])\d{11,15}\b/.source;
112
115
  /**
113
116
  * An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
114
117
  * phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
@@ -119,10 +122,10 @@ export const redactText = (text) => {
119
122
  let out = home && home !== "/" ? text.split(home).join("~") : text;
120
123
  out = out
121
124
  .replace(/"[^"\n]*"|“[^”\n]*”|«[^»\n]*»/g, '"[redacted]"')
122
- .replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]?\s+)\S+/gi, "$1$2[redacted]")
125
+ .replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]\s*|\s+)[^\s'"]+/gi, "$1$2[redacted]")
123
126
  .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]")
127
+ .replace(new RegExp(INTERNATIONAL_PHONE, "g"), "[number]")
128
+ .replace(new RegExp(BARE_PHONE, "g"), "$1[number]")
126
129
  .replace(/\bbot\d+:[\w-]+/g, "bot[token]")
127
130
  .replace(/\b\d{6,}:[\w-]{20,}\b/g, "[token]")
128
131
  .replace(/(\?|&)[^\s#]*/g, "$1[redacted]")
@@ -1,4 +1,4 @@
1
- import type { Command } from "commander";
1
+ import type { Argument, Command, Option } from "commander";
2
2
  import type { RunContext } from "./run.js";
3
3
  export declare const REDACTED = "[redacted]";
4
4
  /** Which coding agent started the process, and its session when the agent names one. Keys as written in run.json. */
@@ -17,24 +17,39 @@ export interface RunAgent {
17
17
  */
18
18
  export declare const agentOf: (env: NodeJS.ProcessEnv) => RunAgent | undefined;
19
19
  export interface MaskedArgv {
20
- /** The words after the binary, every value that may be content or a secret replaced by `[redacted]`. */
20
+ /** The words after the binary; a secret is `[redacted]`, and with `keep` off so is every value that may be content. */
21
21
  argv: string[];
22
22
  /** The first word that names no command where one was expected. */
23
23
  unknownCommand?: string;
24
24
  }
25
25
  /**
26
- * The command line as typed, safe to keep. Command words, flag names and the profile stay; a value stays only when
27
- * its shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
26
+ * Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`; on a command,
27
+ * every word typed after it that is not an option.
28
+ */
29
+ export declare const secretInput: <T extends Option | Argument | Command>(input: T) => T;
30
+ /**
31
+ * The command line for a run record. With `keep` (the owner's `record_input`, on unless turned off) every word stays
32
+ * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), a word shaped
33
+ * like a credential, and a phone number inside any word. With `keep` off, command words, flag names and the profile stay; a value stays only when its
34
+ * shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
28
35
  * option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
29
36
  * else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
30
37
  */
31
- export declare const maskArgv: (program: Command, words: readonly string[]) => MaskedArgv;
32
- /** A run's context from the parsed program: `max messages list` and its masked words, or the word that named nothing. */
33
- export declare const runContextOf: ({ bin, command, program, argv, env, }: {
38
+ export declare const maskArgv: (program: Command, words: readonly string[], { keep }?: {
39
+ keep?: boolean | undefined;
40
+ }) => MaskedArgv;
41
+ /**
42
+ * A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. A CLI
43
+ * that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
44
+ * keeps no command line and no agent.
45
+ */
46
+ export declare const runContextOf: ({ bin, command, program, argv, env, recordInput, }: {
34
47
  bin: string;
35
48
  /** The run's own name, `messages list` or `mcp messages list`; empty when no command was recognised. */
36
49
  command: string;
37
50
  program?: Command;
38
51
  argv?: readonly string[];
39
52
  env: NodeJS.ProcessEnv;
53
+ /** The owner's `record_input`; left out, every value that may be content is masked. */
54
+ recordInput?: boolean;
40
55
  }) => RunContext;
@@ -1,4 +1,5 @@
1
1
  import { isContentKey } from "@wirecat/cli-base/standards";
2
+ import { BARE_PHONE, INTERNATIONAL_PHONE } from "./error-detail.js";
2
3
  export const REDACTED = "[redacted]";
3
4
  /**
4
5
  * The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
@@ -39,19 +40,60 @@ const sessionOf = (kind, env) => {
39
40
  return (variable === undefined ? undefined : env[variable]?.trim()) || undefined;
40
41
  };
41
42
  const plainWord = (word) => /^[A-Za-z][A-Za-z0-9.-]{0,39}$/.test(word);
43
+ const secretInputs = new WeakSet();
42
44
  /**
43
- * The command line as typed, safe to keep. Command words, flag names and the profile stay; a value stays only when
44
- * its shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
45
+ * Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`; on a command,
46
+ * every word typed after it that is not an option.
47
+ */
48
+ export const secretInput = (input) => {
49
+ secretInputs.add(input);
50
+ return input;
51
+ };
52
+ const SECRET_WORDS = new Set([
53
+ "authorization",
54
+ "cookie",
55
+ "credential",
56
+ "credentials",
57
+ "passphrase",
58
+ "password",
59
+ "secret",
60
+ "token",
61
+ "apikey",
62
+ ]);
63
+ // Undeclared flags carry no mark, so the name is the only signal; CLIs take secrets at a prompt (WC-SECR-5) and this
64
+ // catches the one typed anyway.
65
+ const secretNamed = (name) => {
66
+ const words = name
67
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
68
+ .split(/[\s_.-]+/)
69
+ .map((word) => word.toLowerCase());
70
+ return words.some((word, at) => SECRET_WORDS.has(word) || (word === "api" && words[at + 1] === "key"));
71
+ };
72
+ // A password inside a URL, or one unbroken run of 40 token characters, is a credential wherever it was typed.
73
+ const secretShaped = (word) => /:\/\/[^/@\s:]+:[^/@\s]+@/.test(word) || /(^|[^A-Za-z0-9_-])[A-Za-z0-9_-]{40,}($|[^A-Za-z0-9_-])/.test(word);
74
+ // Messaging CLIs promise a phone number reaches no log, so it goes even from text the record otherwise keeps.
75
+ const withoutPhones = (word) => word.replace(new RegExp(INTERNATIONAL_PHONE, "g"), REDACTED).replace(new RegExp(BARE_PHONE, "g"), `$1${REDACTED}`);
76
+ /**
77
+ * The command line for a run record. With `keep` (the owner's `record_input`, on unless turned off) every word stays
78
+ * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), a word shaped
79
+ * like a credential, and a phone number inside any word. With `keep` off, command words, flag names and the profile stay; a value stays only when its
80
+ * shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
45
81
  * option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
46
82
  * else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
47
83
  */
48
- export const maskArgv = (program, words) => {
84
+ export const maskArgv = (program, words, { keep = false } = {}) => {
49
85
  const argv = [];
50
86
  let unknownCommand;
51
87
  let current = program;
52
88
  let positional = 0;
53
89
  let profileSeen = false;
54
90
  let lost = false;
91
+ const raw = (word) => (keep && !secretShaped(word) ? withoutPhones(word) : REDACTED);
92
+ const keptValue = (name, value, input) => keep
93
+ ? (input && secretInputs.has(input)) || secretNamed(name)
94
+ ? REDACTED
95
+ : raw(value)
96
+ : maskedValue(name, value, input?.argChoices);
55
97
  const optionFor = (flag) => {
56
98
  for (let at = current; at; at = at.parent) {
57
99
  const found = at.options.find((option) => option.long === flag || option.short === flag);
@@ -65,32 +107,37 @@ export const maskArgv = (program, words) => {
65
107
  if (word === "--") {
66
108
  argv.push("--");
67
109
  for (let after = at + 1; after < words.length; after++)
68
- argv.push(REDACTED);
110
+ argv.push(raw(words[after]));
69
111
  break;
70
112
  }
71
113
  if (word.startsWith("-") && word.length > 1 && !NUMBER.test(word)) {
72
114
  const equals = word.indexOf("=");
73
115
  const flag = equals > 0 ? word.slice(0, equals) : word;
74
116
  if (!/^--?[A-Za-z0-9][A-Za-z0-9-]*$/.test(flag)) {
75
- argv.push(REDACTED);
117
+ argv.push(raw(word));
76
118
  continue;
77
119
  }
78
120
  const option = optionFor(flag);
79
121
  const name = option?.attributeName() ?? flag.replace(/^-+/, "");
80
122
  if (equals > 0) {
81
- argv.push(`${flag}=${maskedValue(name, word.slice(equals + 1), option?.argChoices)}`);
123
+ argv.push(`${flag}=${keptValue(name, word.slice(equals + 1), option)}`);
82
124
  continue;
83
125
  }
84
126
  argv.push(flag);
127
+ if (!option && keep && secretNamed(name) && words[at + 1] !== undefined && !words[at + 1]?.startsWith("-")) {
128
+ at += 1;
129
+ argv.push(REDACTED);
130
+ continue;
131
+ }
85
132
  if (!option || !(option.required || option.optional))
86
133
  continue;
87
134
  const takes = (next) => next !== undefined && (option.required || !next.startsWith("-") || NUMBER.test(next));
88
135
  if (takes(words[at + 1])) {
89
136
  at += 1;
90
- argv.push(maskedValue(name, words[at], option.argChoices));
137
+ argv.push(keptValue(name, words[at], option));
91
138
  while (option.variadic && words[at + 1] !== undefined && !words[at + 1].startsWith("-")) {
92
139
  at += 1;
93
- argv.push(maskedValue(name, words[at], option.argChoices));
140
+ argv.push(keptValue(name, words[at], option));
94
141
  }
95
142
  }
96
143
  continue;
@@ -104,20 +151,20 @@ export const maskArgv = (program, words) => {
104
151
  }
105
152
  if (current === program && !profileSeen) {
106
153
  profileSeen = true;
107
- argv.push(PROFILE.test(word) ? word : REDACTED);
154
+ argv.push(keep ? raw(word) : PROFILE.test(word) && withoutPhones(word) === word ? word : REDACTED);
108
155
  continue;
109
156
  }
110
157
  if (current.commands.length > 0 && current.registeredArguments.length === 0) {
111
158
  lost = true;
112
159
  unknownCommand = commandLike(word);
113
- argv.push(unknownCommand);
160
+ argv.push(keep ? raw(word) : unknownCommand);
114
161
  continue;
115
162
  }
116
163
  }
117
164
  const declared = lost ? undefined : current.registeredArguments;
118
165
  const argument = declared?.[Math.min(positional, declared.length - 1)];
119
166
  positional += 1;
120
- argv.push(argument ? maskedValue(argument.name(), word, argument.argChoices) : REDACTED);
167
+ argv.push(secretInputs.has(current) ? REDACTED : argument ? keptValue(argument.name(), word, argument) : raw(word));
121
168
  }
122
169
  return { argv, ...(unknownCommand === undefined ? {} : { unknownCommand }) };
123
170
  };
@@ -143,9 +190,13 @@ const safeShape = (value) => (NUMBER.test(value) && !/^\d{11,}$/.test(value)) ||
143
190
  /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(value) ||
144
191
  value === "true" ||
145
192
  value === "false";
146
- /** A run's context from the parsed program: `max messages list` and its masked words, or the word that named nothing. */
147
- export const runContextOf = ({ bin, command, program, argv, env, }) => {
148
- const masked = program && argv ? maskArgv(program, argv) : undefined;
193
+ /**
194
+ * A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. A CLI
195
+ * that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
196
+ * keeps no command line and no agent.
197
+ */
198
+ export const runContextOf = ({ bin, command, program, argv, env, recordInput = false, }) => {
199
+ const masked = program && argv ? maskArgv(program, argv, { keep: recordInput }) : undefined;
149
200
  const agent = agentOf(env);
150
201
  return {
151
202
  cliCommand: command ? `${bin} ${command}` : bin,
@@ -49,7 +49,7 @@ export interface RunMetadata {
49
49
  traceId?: string;
50
50
  /** The whole command with the program's name, `max messages list`; `command` stays the run's short name. */
51
51
  cliCommand?: string;
52
- /** The words after the program's name, every value that may be content or a secret `[redacted]` (`maskArgv`). */
52
+ /** The words after the program's name, a secret `[redacted]`; with `record_input` off, any value that may be content too (`maskArgv`). */
53
53
  argv?: string[];
54
54
  /** The word that named no command, when that is why the run failed. */
55
55
  unknownCommand?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.438.0",
3
+ "version": "0.440.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
package/parity.json CHANGED
@@ -822,7 +822,7 @@
822
822
  "meaning": "why a task was closed, kept with it — no-reply-needed, for example"
823
823
  },
824
824
  "--record": {
825
- "meaning": "keep this run under `runs` — ids and timings, never message content",
825
+ "meaning": "keep this run under `runs`, with the command as typed and secrets hidden; record_input false leaves the typed words out",
826
826
  "note": "max's own copy is worded differently until T6 moves the command onto the shared one (e13)"
827
827
  },
828
828
  "--remind-time": {