@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 +54 -0
- package/README.md +2 -2
- package/dist/cli/bot/api.js +3 -1
- package/dist/cli/command-runs.d.ts +3 -3
- package/dist/cli/command-runs.js +13 -2
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +1 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/contacts-command.js +2 -1
- package/dist/cli/messenger/doctor-command.js +2 -1
- package/dist/cli/messenger/report.d.ts +4 -1
- package/dist/cli/messenger/report.js +11 -2
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +4 -2
- package/dist/cli/settings.d.ts +7 -0
- package/dist/cli/settings.js +4 -0
- package/dist/cli/shared-command-exports.d.ts +4 -0
- package/dist/runs/error-detail.d.ts +3 -0
- package/dist/runs/error-detail.js +6 -3
- package/dist/runs/run-context.d.ts +22 -7
- package/dist/runs/run-context.js +65 -14
- package/dist/runs/run.d.ts +1 -1
- package/package.json +1 -1
- package/parity.json +1 -1
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
|
|
221
|
-
|
|
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,
|
package/dist/cli/bot/api.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/cli/command-runs.js
CHANGED
|
@@ -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
|
|
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",
|
|
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();
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cli/context.js
CHANGED
|
@@ -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,
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -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. */
|
package/dist/cli/program.js
CHANGED
|
@@ -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",
|
|
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
|
|
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,
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/settings.js
CHANGED
|
@@ -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*[:=]
|
|
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(
|
|
125
|
-
.replace(
|
|
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
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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[]
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
package/dist/runs/run-context.js
CHANGED
|
@@ -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
|
-
*
|
|
44
|
-
*
|
|
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(
|
|
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(
|
|
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}=${
|
|
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(
|
|
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(
|
|
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 ?
|
|
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
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
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,
|
package/dist/runs/run.d.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
|
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": {
|