@bli-cockpit/cli 0.2.291 → 0.2.293
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/dist/adapters/claude-attribution-discovery.js +22 -0
- package/dist/adapters/claude-attribution-memoized-reads.js +3 -3
- package/dist/adapters/claude-attribution.js +5 -2
- package/dist/adapters/codex-attribution-memoized-reads.js +3 -3
- package/dist/adapters/codex-attribution.js +30 -12
- package/dist/adapters/codex-segment-discovery.js +66 -0
- package/dist/adapters/codex-session-revision.js +139 -0
- package/dist/adapters/codex-session-signals.js +2 -2
- package/dist/adapters/pi-session-facts.js +28 -0
- package/dist/adapters/pi-session-reader.js +132 -0
- package/dist/adapters/raw-evidence-codex-reader.js +17 -7
- package/dist/adapters/raw-evidence-transcript-body.js +3 -0
- package/dist/adapters/raw-evidence-transcript-file.js +27 -4
- package/dist/adapters/raw-evidence-transcript-stream.js +33 -1
- package/dist/agent-plugins-converge.js +24 -5
- package/dist/collection-scope.js +2 -0
- package/dist/commands/backfill-checkpoint.js +3 -1
- package/dist/commands/backfill-history-scan.js +38 -0
- package/dist/commands/backfill-issues.js +8 -20
- package/dist/commands/backfill-scan.js +18 -40
- package/dist/commands/backfill-upload.js +1 -0
- package/dist/commands/books-usage.js +2 -1
- package/dist/commands/books.js +12 -4
- package/dist/commands/codex-hooks-trust-choice.js +51 -0
- package/dist/commands/codex-hooks-trust-prompt.js +172 -0
- package/dist/commands/codex-managed-hooks.js +226 -0
- package/dist/commands/codex-revision-facts.js +64 -0
- package/dist/commands/doctor-report.js +2 -2
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-args-collector-status.js +5 -3
- package/dist/commands/local-args-notes-targets.js +35 -0
- package/dist/commands/local-args-tower-notes.js +7 -18
- package/dist/commands/local-help-commands-tower.js +3 -2
- package/dist/commands/local-help-commands.js +3 -2
- package/dist/commands/local-help-notes.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/memory-install-codex-hooks.js +95 -2
- package/dist/commands/memory-install-receipt.js +17 -1
- package/dist/commands/memory-install-report.js +5 -4
- package/dist/commands/memory-install.js +15 -6
- package/dist/commands/memory-log.js +13 -2
- package/dist/commands/memory-map.js +6 -2
- package/dist/commands/memory-verb-list.js +1 -1
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +3 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-record.js +3 -1
- package/dist/commands/session-sync-scan.js +14 -6
- package/dist/commands/session-sync-shelf.js +2 -0
- package/dist/commands/session-sync-upload.js +1 -0
- package/dist/commands/setup-receipt-lines.js +2 -8
- package/dist/commands/sources-discovery.js +63 -14
- package/dist/commands/sources-reconciliation.js +7 -5
- package/dist/commands/sources-verdict.js +3 -0
- package/dist/cursors/raw-evidence-cursor.js +1 -0
- package/dist/image-description-session.js +4 -2
- package/dist/native-session-roots.js +31 -0
- package/dist/t3-native-homes.js +64 -0
- package/package.json +4 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Auto-trust the BLI Memory hooks in Codex? [Y/n]" (BLI-24349).
|
|
3
|
+
*
|
|
4
|
+
* Codex skips a hook from a person's own `~/.codex/hooks.json` until they
|
|
5
|
+
* trust it in `/hooks`, and asks again whenever the hook changes. On
|
|
6
|
+
* 2026-11-04 three of eight reporting machines were stuck there. This is the
|
|
7
|
+
* one question that ends it: a yes puts the four BLI Memory hooks in Codex's
|
|
8
|
+
* system folder, where Codex runs them as managed hooks with no trust step
|
|
9
|
+
* (`codex-managed-hooks.ts` has the proof), and takes the user-scope copy out
|
|
10
|
+
* so nothing runs twice.
|
|
11
|
+
*
|
|
12
|
+
* Rules (Edward, 2026-11-04):
|
|
13
|
+
* - Default yes: Enter or y trusts.
|
|
14
|
+
* - Asked only when a person is plainly there (same test as the BLI Memory
|
|
15
|
+
* question: stdin and stdout are terminals, no `--json`, not the scheduled
|
|
16
|
+
* tick, not `--check`/`--dry-run`, not CI). Every other run never prompts
|
|
17
|
+
* and never changes trust; the receipt keeps saying `needs_trust`.
|
|
18
|
+
* - A no is remembered and not asked again. `cockpit doctor
|
|
19
|
+
* --trust-codex-hooks` is the yes without the question, for a person who
|
|
20
|
+
* changes their mind.
|
|
21
|
+
* - The elevation (one sudo on macOS, one UAC prompt on Windows) happens only
|
|
22
|
+
* after a yes.
|
|
23
|
+
* - Every branch logs one reason label: `trusted_by_prompt`, `declined`,
|
|
24
|
+
* `non_interactive_skip`, `elevation_refused`, `write_failed:<reason>`,
|
|
25
|
+
* plus the no-question ones (`already_managed`, `already_trusted`,
|
|
26
|
+
* `declined_before`, `not_needed:<why>`, `unclear_answer`).
|
|
27
|
+
*/
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import { readLine, writeLine } from "./cli-io.js";
|
|
30
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
31
|
+
import { isMemoryCaptureOff } from "../memory-capture-choice.js";
|
|
32
|
+
import { parseYesNo } from "../yes-no.js";
|
|
33
|
+
import { memoryChoicePromptBlocker } from "./memory-capture-prompt.js";
|
|
34
|
+
import { codexSystemHooksFile, defaultCodexElevationRunner, readCodexManagedHooks, writeCodexManagedHooks, } from "./codex-managed-hooks.js";
|
|
35
|
+
import { expectedCodexTrustRows, installCodexMemoryHooks, readCodexHookTrustFromDisk, } from "./memory-install-codex-hooks.js";
|
|
36
|
+
import { readCodexHooksTrustChoice, writeCodexHooksTrustChoice } from "./codex-hooks-trust-choice.js";
|
|
37
|
+
import { defaultMemoryFileIo } from "./memory-install-files.js";
|
|
38
|
+
import { resolveMemoryConfig } from "./memory-install-config.js";
|
|
39
|
+
export const CODEX_HOOKS_TRUST_QUESTION = "Auto-trust the BLI Memory hooks in Codex? [Y/n] ";
|
|
40
|
+
export const CODEX_HOOKS_TRUST_FLAG = "--trust-codex-hooks";
|
|
41
|
+
/**
|
|
42
|
+
* The whole question, start to finish. Never throws: a doctor run is never
|
|
43
|
+
* lost to it. Returns the reason label it logged.
|
|
44
|
+
*/
|
|
45
|
+
export async function settleCodexHooksTrust(io, options, deps = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const outcome = await decide(io, options, deps);
|
|
48
|
+
console.error("[codex-hooks-trust] settled", JSON.stringify({ reason: outcome }));
|
|
49
|
+
return outcome;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const outcome = "write_failed:step_threw";
|
|
53
|
+
console.error("[codex-hooks-trust] settled", JSON.stringify({ reason: outcome, error_name: error instanceof Error ? error.name : typeof error }));
|
|
54
|
+
return outcome;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function decide(io, options, deps) {
|
|
58
|
+
// First, and before anything is read: a run with nobody at the keyboard
|
|
59
|
+
// never prompts and never changes trust. The receipt carries the state.
|
|
60
|
+
const blocker = memoryChoicePromptBlocker(io, options);
|
|
61
|
+
if (blocker) {
|
|
62
|
+
console.error("[codex-hooks-trust] not asked; nobody at a terminal", JSON.stringify({ reason: "non_interactive_skip", blocker }));
|
|
63
|
+
return "non_interactive_skip";
|
|
64
|
+
}
|
|
65
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
66
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
67
|
+
const platform = deps.platform ?? process.platform;
|
|
68
|
+
const fileIo = deps.io ?? defaultMemoryFileIo();
|
|
69
|
+
const off = await (deps.memoryOff ?? (() => isMemoryCaptureOff(paths, { fetch: io.fetch })))();
|
|
70
|
+
if (off)
|
|
71
|
+
return "not_needed:memory_off";
|
|
72
|
+
const config = await (deps.resolveConfig ?? (() => defaultResolveConfig(io, options, platform)))();
|
|
73
|
+
if (!config)
|
|
74
|
+
return "not_needed:bin_missing";
|
|
75
|
+
const trust = await readCodexHookTrustFromDisk({ homeDir, io: fileIo });
|
|
76
|
+
if (trust.feature === "disabled")
|
|
77
|
+
return "not_needed:hooks_disabled_in_config";
|
|
78
|
+
const file = deps.systemHooksFile ?? codexSystemHooksFile(platform, io.env ?? process.env);
|
|
79
|
+
const managed = await readCodexManagedHooks({ config, io: fileIo, file });
|
|
80
|
+
if (managed.state === "current")
|
|
81
|
+
return "already_managed";
|
|
82
|
+
// A machine somebody already trusted in /hooks runs the hooks today. The
|
|
83
|
+
// question is for untrusted machines (and a stale managed copy).
|
|
84
|
+
if (managed.state !== "stale" && trust.trustedRows >= expectedCodexTrustRows(config))
|
|
85
|
+
return "already_trusted";
|
|
86
|
+
if (!options.trustCodexHooks) {
|
|
87
|
+
const choice = await readCodexHooksTrustChoice(paths);
|
|
88
|
+
if (choice.choice === "declined") {
|
|
89
|
+
writeLine(io.stdout, `Codex hooks: not auto-trusted (you said no earlier). \`cockpit doctor ${CODEX_HOOKS_TRUST_FLAG}\` trusts them.`);
|
|
90
|
+
return "declined_before";
|
|
91
|
+
}
|
|
92
|
+
const answer = await ask(io, managed.state === "stale");
|
|
93
|
+
if (answer === "unclear") {
|
|
94
|
+
writeLine(io.stdout, "No answer recorded; the next `cockpit doctor` asks again.");
|
|
95
|
+
return "unclear_answer";
|
|
96
|
+
}
|
|
97
|
+
if (answer === "no") {
|
|
98
|
+
await recordChoice(paths, "declined", "prompt");
|
|
99
|
+
writeLine(io.stdout, `OK, not trusted. Codex skips the BLI Memory hooks until they are trusted; this is not asked again. ` +
|
|
100
|
+
`Change your mind: cockpit doctor ${CODEX_HOOKS_TRUST_FLAG} (or trust them in Codex's /hooks).`);
|
|
101
|
+
return "declined";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
writeLine(io.stdout, platform === "win32"
|
|
105
|
+
? `Windows may ask once for administrator permission to write ${file}.`
|
|
106
|
+
: `This writes ${file}; your computer asks for your password once (sudo).`);
|
|
107
|
+
const written = await writeCodexManagedHooks({
|
|
108
|
+
config,
|
|
109
|
+
io: fileIo,
|
|
110
|
+
file,
|
|
111
|
+
platform,
|
|
112
|
+
elevate: deps.elevate ?? defaultCodexElevationRunner(),
|
|
113
|
+
});
|
|
114
|
+
if (written.outcome === "elevation_refused") {
|
|
115
|
+
writeLine(io.stdout, "Not trusted: the administrator step was cancelled. The next `cockpit doctor` asks again.");
|
|
116
|
+
return "elevation_refused";
|
|
117
|
+
}
|
|
118
|
+
if (written.outcome === "write_failed") {
|
|
119
|
+
writeLine(io.stdout, `Not trusted: the system hooks file could not be written (${written.reason}). Fallback: trust them in Codex's /hooks.`);
|
|
120
|
+
return written.reason;
|
|
121
|
+
}
|
|
122
|
+
// The system copy carries the hooks now; take the user copy out so nothing runs twice.
|
|
123
|
+
const userCopy = await installCodexMemoryHooks({ homeDir, config, dryRun: false, io: fileIo, systemHooksFile: file });
|
|
124
|
+
if (userCopy.status === "failed") {
|
|
125
|
+
console.error("[codex-hooks-trust] system copy written; user copy not removed", JSON.stringify({ reason: userCopy.reason }));
|
|
126
|
+
}
|
|
127
|
+
await recordChoice(paths, "trusted", options.trustCodexHooks ? "flag" : "prompt");
|
|
128
|
+
writeLine(io.stdout, "Codex hooks: trusted. Codex now runs the BLI Memory hooks as managed hooks, with no /hooks review.");
|
|
129
|
+
return options.trustCodexHooks ? "trusted_by_flag" : "trusted_by_prompt";
|
|
130
|
+
}
|
|
131
|
+
async function ask(io, stale) {
|
|
132
|
+
writeLine(io.stdout, stale
|
|
133
|
+
? "BLI Memory's Codex hooks changed since they were trusted on this machine. Yes updates the trusted copy."
|
|
134
|
+
: "Codex will not run BLI Memory's hooks (recall and save) until they are trusted. Yes trusts them for this machine, once.");
|
|
135
|
+
let verdict = parseYesNo(await readLine(io, CODEX_HOOKS_TRUST_QUESTION), { emptyIsYes: true });
|
|
136
|
+
if (verdict === "unclear") {
|
|
137
|
+
verdict = parseYesNo(await readLine(io, `Please answer y or n. ${CODEX_HOOKS_TRUST_QUESTION}`), { emptyIsYes: true });
|
|
138
|
+
}
|
|
139
|
+
return verdict;
|
|
140
|
+
}
|
|
141
|
+
async function recordChoice(paths, choice, via) {
|
|
142
|
+
try {
|
|
143
|
+
await writeCodexHooksTrustChoice(paths, choice, via);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// The trust itself (or the "no") already happened; only the memory of it is lost.
|
|
147
|
+
console.error("[codex-hooks-trust] answer not saved", JSON.stringify({ reason: "choice_write_failed", choice, error_name: error instanceof Error ? error.name : typeof error }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function defaultResolveConfig(io, options, platform) {
|
|
151
|
+
const resolved = await resolveMemoryConfig({
|
|
152
|
+
kind: "memory",
|
|
153
|
+
action: "status",
|
|
154
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
155
|
+
...(options.dashboardUrl ? { dashboardUrl: options.dashboardUrl } : {}),
|
|
156
|
+
dryRun: false,
|
|
157
|
+
json: false,
|
|
158
|
+
}, io, platform, {});
|
|
159
|
+
return resolved.config;
|
|
160
|
+
}
|
|
161
|
+
/** What `cockpit doctor` calls: its own flags mapped onto the question's options. */
|
|
162
|
+
export function settleDoctorCodexHooksTrust(command, io) {
|
|
163
|
+
return settleCodexHooksTrust(io, {
|
|
164
|
+
json: command.json,
|
|
165
|
+
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
166
|
+
...(command.automaticTick ? { automaticTick: true } : {}),
|
|
167
|
+
dryRun: command.dryRun,
|
|
168
|
+
...(command.checkOnly ? { checkOnly: true } : {}),
|
|
169
|
+
dashboardUrl: command.dashboardUrl,
|
|
170
|
+
...(command.trustCodexHooks ? { trustCodexHooks: true } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLI Memory's Codex hooks as MANAGED hooks (BLI-24349).
|
|
3
|
+
*
|
|
4
|
+
* Codex will not run a hook from a person's own `~/.codex/hooks.json` until
|
|
5
|
+
* they trust it in `/hooks`, and asks again whenever the hook text changes.
|
|
6
|
+
* Hooks in Codex's SYSTEM config folder are different: Codex marks them
|
|
7
|
+
* "managed, trusted by policy" and runs them with no review at all.
|
|
8
|
+
*
|
|
9
|
+
* macOS (and any Unix) /etc/codex/hooks.json
|
|
10
|
+
* Windows %ProgramData%\OpenAI\Codex\hooks.json
|
|
11
|
+
*
|
|
12
|
+
* ## Proven, not assumed (2026-11-04, Codex CLI 0.166.0)
|
|
13
|
+
*
|
|
14
|
+
* On a Linux box signed in with a personal ChatGPT account (plan `pro`, no
|
|
15
|
+
* Business workspace), with no `[hooks.state]` row anywhere, Codex's own
|
|
16
|
+
* `hooks/list` reported hooks in `/etc/codex/hooks.json` as `source: system,
|
|
17
|
+
* isManaged: true, trustStatus: managed`, and a real turn fired session-start,
|
|
18
|
+
* prompt and stop. The source agrees: `hook_metadata_for_config_layer_source`
|
|
19
|
+
* in openai/codex `codex-rs/hooks/src/engine/discovery.rs` maps the system
|
|
20
|
+
* config layer to `(HookSource::System, true)`, and nothing on that path reads
|
|
21
|
+
* the account. Windows was NOT run on a real machine (the test VM was out of
|
|
22
|
+
* reach); its folder comes from `windows_codex_system_dir` in
|
|
23
|
+
* `codex-rs/config/src/loader/mod.rs` (known folder ProgramData). Receipts:
|
|
24
|
+
* the BLI-24349 ticket comments and `docs/runbooks/codex-hooks-trust.md`.
|
|
25
|
+
*
|
|
26
|
+
* ## Why `hooks.json`, not `requirements.toml`
|
|
27
|
+
*
|
|
28
|
+
* Hooks in `requirements.toml` are managed too, but a broken write to that file
|
|
29
|
+
* stops Codex from starting at all ("failed to parse requirements layer",
|
|
30
|
+
* exit 1). A broken system `hooks.json` costs one warning line and the turn
|
|
31
|
+
* still runs. `requirements.toml` is also the admin policy file an
|
|
32
|
+
* organisation's device management owns; this module never touches it, and
|
|
33
|
+
* never sets `allow_managed_hooks_only` (that would switch off a person's own
|
|
34
|
+
* hooks).
|
|
35
|
+
*
|
|
36
|
+
* ## The write
|
|
37
|
+
*
|
|
38
|
+
* The system folder belongs to the administrator, so the write is tried as
|
|
39
|
+
* the person first (works on Windows when ProgramData lets users create
|
|
40
|
+
* folders, which is its default) and elevated only when that is refused:
|
|
41
|
+
* one `sudo` on macOS, one UAC prompt on Windows. Only our hook groups are
|
|
42
|
+
* replaced; anything else in the file survives, exactly like the user-scope
|
|
43
|
+
* writer (`applyHookGroups`). The result is read back off disk before it
|
|
44
|
+
* counts (BLI-2541).
|
|
45
|
+
*/
|
|
46
|
+
import { spawnSync } from "node:child_process";
|
|
47
|
+
import { randomBytes } from "node:crypto";
|
|
48
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
49
|
+
import os from "node:os";
|
|
50
|
+
import path from "node:path";
|
|
51
|
+
import { applyHookGroups, hookGroupsMatch, ourHookCount, parseJsonRecord, serializeJson, } from "./memory-install-claude.js";
|
|
52
|
+
/**
|
|
53
|
+
* Moves the system folder somewhere else. The test suite sets it
|
|
54
|
+
* (`vitest.setup.ts`) so no test ever reads or elevates into the real
|
|
55
|
+
* `/etc/codex` or ProgramData, the same guard it keeps on HOME. Read from the
|
|
56
|
+
* process too, because callers inject a trimmed `env` for other reasons.
|
|
57
|
+
*/
|
|
58
|
+
export const CODEX_SYSTEM_DIR_ENV = "COCKPIT_CODEX_SYSTEM_DIR";
|
|
59
|
+
/** Codex's system config folder on this platform. */
|
|
60
|
+
export function codexSystemDir(platform, env = process.env) {
|
|
61
|
+
const override = (env[CODEX_SYSTEM_DIR_ENV] ?? process.env[CODEX_SYSTEM_DIR_ENV] ?? "").trim();
|
|
62
|
+
if (override)
|
|
63
|
+
return override;
|
|
64
|
+
if (platform === "win32") {
|
|
65
|
+
const programData = (env["ProgramData"] ?? env["PROGRAMDATA"] ?? "").trim() || "C:\\ProgramData";
|
|
66
|
+
return path.win32.join(programData, "OpenAI", "Codex");
|
|
67
|
+
}
|
|
68
|
+
return "/etc/codex";
|
|
69
|
+
}
|
|
70
|
+
/** `/etc/codex/hooks.json`, or `%ProgramData%\OpenAI\Codex\hooks.json` on Windows. */
|
|
71
|
+
export function codexSystemHooksFile(platform, env = process.env) {
|
|
72
|
+
const dir = codexSystemDir(platform, env);
|
|
73
|
+
if ((env[CODEX_SYSTEM_DIR_ENV] ?? process.env[CODEX_SYSTEM_DIR_ENV] ?? "").trim())
|
|
74
|
+
return path.join(dir, "hooks.json");
|
|
75
|
+
return platform === "win32" ? path.win32.join(dir, "hooks.json") : path.posix.join(dir, "hooks.json");
|
|
76
|
+
}
|
|
77
|
+
/** An injected override (null = do not look) or this platform's system hooks file. */
|
|
78
|
+
export function codexSystemHooksFileFor(override, platform, env) {
|
|
79
|
+
return override !== undefined ? override : codexSystemHooksFile(platform, env ?? process.env);
|
|
80
|
+
}
|
|
81
|
+
export async function readCodexManagedHooks(options) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = await options.io.readText(options.file);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { state: "unreadable", file: options.file };
|
|
88
|
+
}
|
|
89
|
+
if (raw === null || !raw.trim())
|
|
90
|
+
return { state: "absent", file: options.file };
|
|
91
|
+
const root = parseJsonRecord(raw);
|
|
92
|
+
if (!root)
|
|
93
|
+
return { state: "unreadable", file: options.file };
|
|
94
|
+
if (hookGroupsMatch(root, options.config.hooks))
|
|
95
|
+
return { state: "current", file: options.file };
|
|
96
|
+
return { state: ourHookCount(root) > 0 ? "stale" : "absent", file: options.file };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Puts our four hooks into the system file. Never throws.
|
|
100
|
+
*
|
|
101
|
+
* Order: read what is there (refuse to replace a file that will not parse,
|
|
102
|
+
* same rule as every other config we touch), merge our groups in, try the
|
|
103
|
+
* write as the person, elevate only on a permission refusal, read back.
|
|
104
|
+
*/
|
|
105
|
+
export async function writeCodexManagedHooks(options) {
|
|
106
|
+
const { file } = options;
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = await options.io.readText(file);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// Reading a world-readable file failed for a reason other than "absent".
|
|
113
|
+
return { outcome: "write_failed", reason: `write_failed:${errorCode(error, "read_failed")}`, file };
|
|
114
|
+
}
|
|
115
|
+
const existing = raw === null || !raw.trim() ? {} : parseJsonRecord(raw);
|
|
116
|
+
if (!existing) {
|
|
117
|
+
// Somebody else's system hooks file that does not parse: not ours to replace.
|
|
118
|
+
return { outcome: "write_failed", reason: "write_failed:system_file_unparseable", file };
|
|
119
|
+
}
|
|
120
|
+
if (hookGroupsMatch(existing, options.config.hooks)) {
|
|
121
|
+
return { outcome: "written", reason: "already_current", file };
|
|
122
|
+
}
|
|
123
|
+
const contents = serializeJson(applyHookGroups(structuredClone(existing), options.config.hooks), raw ?? "");
|
|
124
|
+
let reason = "wrote_direct";
|
|
125
|
+
try {
|
|
126
|
+
await options.io.writeText(file, contents);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const code = errorCode(error, "write_error");
|
|
130
|
+
if (code !== "EACCES" && code !== "EPERM") {
|
|
131
|
+
return { outcome: "write_failed", reason: `write_failed:${code}`, file };
|
|
132
|
+
}
|
|
133
|
+
console.error("[codex-hooks-trust] the system hooks folder needs an administrator; asking the OS once", JSON.stringify({ reason: "elevation_needed", platform: options.platform, error_code: code }));
|
|
134
|
+
const elevated = await elevatedCopy({ ...options, contents });
|
|
135
|
+
if (elevated.status === "refused")
|
|
136
|
+
return { outcome: "elevation_refused", reason: "elevation_refused", file };
|
|
137
|
+
if (elevated.status === "failed")
|
|
138
|
+
return { outcome: "write_failed", reason: `write_failed:${elevated.reason}`, file };
|
|
139
|
+
reason = "wrote_elevated";
|
|
140
|
+
}
|
|
141
|
+
// BLI-2541: what the disk holds, not what we sent.
|
|
142
|
+
const stored = await readCodexManagedHooks({ config: options.config, io: options.io, file });
|
|
143
|
+
if (stored.state !== "current") {
|
|
144
|
+
return { outcome: "write_failed", reason: "write_failed:read_back_mismatch", file };
|
|
145
|
+
}
|
|
146
|
+
return { outcome: "written", reason, file };
|
|
147
|
+
}
|
|
148
|
+
async function elevatedCopy(options) {
|
|
149
|
+
let stagingDir = null;
|
|
150
|
+
try {
|
|
151
|
+
stagingDir = await mkdtemp(path.join(options.tempDir ?? os.tmpdir(), "cockpit-codex-hooks-"));
|
|
152
|
+
const stagedFile = path.join(stagingDir, `hooks.${randomBytes(4).toString("hex")}.json`);
|
|
153
|
+
await writeFile(stagedFile, options.contents, "utf8");
|
|
154
|
+
const pathApi = options.platform === "win32" ? path.win32 : path.posix;
|
|
155
|
+
return await options.elevate({
|
|
156
|
+
platform: options.platform,
|
|
157
|
+
stagedFile,
|
|
158
|
+
targetDir: pathApi.dirname(options.file),
|
|
159
|
+
targetFile: options.file,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return { status: "failed", reason: errorCode(error, "staging_failed") };
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (stagingDir)
|
|
167
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The real elevation: `sudo` on macOS, UAC on Windows. Both talk to the person
|
|
172
|
+
* directly (sudo reads the password from the terminal, UAC is its own secure
|
|
173
|
+
* desktop), so nothing secret ever passes through this process.
|
|
174
|
+
*/
|
|
175
|
+
export function defaultCodexElevationRunner(spawn = spawnSync) {
|
|
176
|
+
return async (request) => (request.platform === "win32" ? elevateWindows(request, spawn) : elevateUnix(request, spawn));
|
|
177
|
+
}
|
|
178
|
+
function elevateUnix(request, spawn) {
|
|
179
|
+
// `sudo -v` is the one password prompt; a wrong password three times, Ctrl-C
|
|
180
|
+
// or a machine where this person is not an administrator all end here.
|
|
181
|
+
const validate = spawn("sudo", ["-v"], { stdio: ["inherit", "inherit", "inherit"], windowsHide: true });
|
|
182
|
+
if (validate.error)
|
|
183
|
+
return { status: "failed", reason: errorCode(validate.error, "sudo_unavailable") };
|
|
184
|
+
if (validate.status !== 0)
|
|
185
|
+
return { status: "refused" };
|
|
186
|
+
// `-n`: the credential is cached now, so this never prompts a second time.
|
|
187
|
+
// Paths travel as arguments, never spliced into the script.
|
|
188
|
+
const copy = spawn("sudo", ["-n", "/bin/sh", "-c", 'mkdir -p "$1" && cp "$2" "$3" && chmod 644 "$3"', "sh", request.targetDir, request.stagedFile, request.targetFile], { stdio: ["ignore", "inherit", "inherit"], windowsHide: true });
|
|
189
|
+
if (copy.error)
|
|
190
|
+
return { status: "failed", reason: errorCode(copy.error, "sudo_copy_failed") };
|
|
191
|
+
return copy.status === 0 ? { status: "ok" } : { status: "failed", reason: `elevated_exit_${copy.status ?? "signal"}` };
|
|
192
|
+
}
|
|
193
|
+
/** Windows exit code for "the operation was cancelled by the user" (UAC "No"). */
|
|
194
|
+
const ERROR_CANCELLED = 1223;
|
|
195
|
+
function elevateWindows(request, spawn) {
|
|
196
|
+
const inner = codexWindowsElevatedScript(request);
|
|
197
|
+
const encoded = Buffer.from(inner, "utf16le").toString("base64");
|
|
198
|
+
const outer = "try { $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden " +
|
|
199
|
+
`-ArgumentList @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-EncodedCommand','${encoded}'); exit $p.ExitCode } ` +
|
|
200
|
+
`catch { exit ${ERROR_CANCELLED} }`;
|
|
201
|
+
const result = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", outer], {
|
|
202
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
203
|
+
windowsHide: true,
|
|
204
|
+
});
|
|
205
|
+
if (result.error)
|
|
206
|
+
return { status: "failed", reason: errorCode(result.error, "powershell_unavailable") };
|
|
207
|
+
if (result.status === ERROR_CANCELLED)
|
|
208
|
+
return { status: "refused" };
|
|
209
|
+
return result.status === 0 ? { status: "ok" } : { status: "failed", reason: `elevated_exit_${result.status ?? "signal"}` };
|
|
210
|
+
}
|
|
211
|
+
/** The script the elevated PowerShell runs. Exported for its test; paths are single-quoted literals. */
|
|
212
|
+
export function codexWindowsElevatedScript(request) {
|
|
213
|
+
const quote = (value) => `'${value.replace(/'/gu, "''")}'`;
|
|
214
|
+
return [
|
|
215
|
+
"$ErrorActionPreference = 'Stop'",
|
|
216
|
+
`New-Item -ItemType Directory -Force -Path ${quote(request.targetDir)} | Out-Null`,
|
|
217
|
+
`Copy-Item -LiteralPath ${quote(request.stagedFile)} -Destination ${quote(request.targetFile)} -Force`,
|
|
218
|
+
"exit 0",
|
|
219
|
+
].join("; ");
|
|
220
|
+
}
|
|
221
|
+
/** A short, charset-safe code for a failure: `EACCES`, `ENOENT`, or the fallback. */
|
|
222
|
+
function errorCode(error, fallback) {
|
|
223
|
+
const code = error?.code;
|
|
224
|
+
const raw = typeof code === "string" && code.trim() ? code : fallback;
|
|
225
|
+
return raw.replace(/[^A-Za-z0-9_.-]/gu, "_").slice(0, 40);
|
|
226
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Compare a composite revision with the SAME line/slim/redaction rules as upload. No raw bundle or network. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { findEmbeddedImages, modeDropsImages, replaceImagesWithMarkers } from "@bli-cockpit/telemetry-core";
|
|
4
|
+
import { imageStateKey } from "../image-description-state.js";
|
|
5
|
+
import { openCodexSessionRevision } from "../adapters/codex-session-revision.js";
|
|
6
|
+
import { createSlimmer } from "../slim/slim-transcript.js";
|
|
7
|
+
import { scanLines, decideLinePlan, writeLinePlan } from "../adapters/raw-evidence-sanitize-lines.js";
|
|
8
|
+
export function codexRevisionFacts(paths, mode, describedKeys, slimAware) {
|
|
9
|
+
const form = async (drop) => {
|
|
10
|
+
const source = await openCodexSessionRevision(paths);
|
|
11
|
+
try {
|
|
12
|
+
const lines = { async *lines() {
|
|
13
|
+
const slimmer = createSlimmer("codex");
|
|
14
|
+
for await (const line of source.lines()) {
|
|
15
|
+
if (line.start === source.length)
|
|
16
|
+
continue;
|
|
17
|
+
let text = new TextDecoder("utf-8", { fatal: true }).decode(line.bytes);
|
|
18
|
+
if (drop) {
|
|
19
|
+
const images = findEmbeddedImages(text);
|
|
20
|
+
text = replaceImagesWithMarkers(text, images, image => describedKeys.has(imageStateKey(image.sha256))).text;
|
|
21
|
+
text = slimmer.slimLine(text);
|
|
22
|
+
}
|
|
23
|
+
yield text + "\n";
|
|
24
|
+
}
|
|
25
|
+
} };
|
|
26
|
+
const options = { redactedFields: ["codex_jsonl.body"], secretLikeFileName: false };
|
|
27
|
+
const plan = decideLinePlan(await scanLines(lines, options), options);
|
|
28
|
+
if (plan.kind === "whole_mask" || plan.kind === "guard_line")
|
|
29
|
+
return null;
|
|
30
|
+
const hash = createHash("sha256");
|
|
31
|
+
let byte_size = 0;
|
|
32
|
+
const write = async (text) => { hash.update(text); byte_size += Buffer.byteLength(text); };
|
|
33
|
+
if (plan.kind === "clean") {
|
|
34
|
+
for await (const line of lines.lines())
|
|
35
|
+
await write(line);
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
await writeLinePlan(lines, plan, options, write);
|
|
39
|
+
await source.verifyUnchanged();
|
|
40
|
+
return { sha256: hash.digest("hex"), byte_size };
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await source.close();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
let full, slim;
|
|
47
|
+
return {
|
|
48
|
+
slimAware,
|
|
49
|
+
async rawSha256() { const source = await openCodexSessionRevision(paths); try {
|
|
50
|
+
return source.sha256;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await source.close();
|
|
54
|
+
} },
|
|
55
|
+
wholeForm: () => full ??= form(false),
|
|
56
|
+
sanitized: () => modeDropsImages(mode) ? slim ??= form(true) : full ??= form(false),
|
|
57
|
+
async slimSplit() {
|
|
58
|
+
if (!slimAware)
|
|
59
|
+
return null;
|
|
60
|
+
const [a, b] = await Promise.all([full ??= form(false), slim ??= form(true)]);
|
|
61
|
+
return a && b && a.sha256 !== b.sha256 ? { full: a, slim: b } : null;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { codexHooksTrustAction, setupReceiptPieces } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import { redactedHealthDetail } from "../health-detail.js";
|
|
3
3
|
import { setupReceiptBlock } from "./setup-receipt-lines.js";
|
|
4
4
|
export function ok(id, code, message) {
|
|
@@ -194,7 +194,7 @@ export function doctorNeedsPerson(rows, receipt) {
|
|
|
194
194
|
const step = key === "device" || key === "browser" ? "authed" : key === "collector.autostart" ? "autostart-alive" : "memory-registered";
|
|
195
195
|
if (piece.status !== "needs_trust" && items.some((item) => item.step === step))
|
|
196
196
|
continue;
|
|
197
|
-
items.push({ step: key, reason: piece.reason ?? piece.status, action: piece.status === "needs_trust" ?
|
|
197
|
+
items.push({ step: key, reason: piece.reason ?? piece.status, action: piece.status === "needs_trust" ? codexHooksTrustAction(piece.reason) : defaults[step] });
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
return items;
|
|
@@ -80,6 +80,8 @@ export function reexecDoctor(command, io) {
|
|
|
80
80
|
args.push("--no-usage-telemetry");
|
|
81
81
|
if (command.noMemory)
|
|
82
82
|
args.push("--no-memory");
|
|
83
|
+
if (command.trustCodexHooks)
|
|
84
|
+
args.push("--trust-codex-hooks");
|
|
83
85
|
if (command.homeDir)
|
|
84
86
|
args.push("--home", command.homeDir);
|
|
85
87
|
if (command.backfillBudgetSeconds)
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkSlackConnected } from "./doctor-slack.js";
|
|
2
2
|
import { checkUsageTelemetry, fixUsageTelemetry } from "./doctor-usage-telemetry.js";
|
|
3
3
|
import { settleDoctorMemoryChoice } from "./memory-capture-prompt.js";
|
|
4
|
+
import { settleDoctorCodexHooksTrust } from "./codex-hooks-trust-prompt.js";
|
|
4
5
|
import { reportIgnoredOptOuts } from "../capture-opt-outs.js";
|
|
5
6
|
import { checkShelfRouting, fixShelfRouting } from "./doctor-shelf.js";
|
|
6
7
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
@@ -105,6 +106,7 @@ export async function runDoctorWithDeps(command, io, deps) {
|
|
|
105
106
|
console.error("[doctor] row done", JSON.stringify({ row: invariant.id, elapsed_ms: rowCheckMs + fixMs, check_ms: rowCheckMs, fix_ms: fixMs, verdict: row.status, reason: row.code }));
|
|
106
107
|
rows.push(row);
|
|
107
108
|
}
|
|
109
|
+
await (deps.settleCodexHooksTrust ?? ((c) => settleDoctorCodexHooksTrust(c.command, c.io)))(context); // BLI-24349: after memory-registered, before the re-read
|
|
108
110
|
// Re-read host state after repairs. Sync is proven by its upload receipts;
|
|
109
111
|
// re-running its diagnostic would always request a new upload by design.
|
|
110
112
|
if (!diagnoseOnly) {
|
|
@@ -96,6 +96,7 @@ const DOCTOR_ALLOWED_FLAGS = [
|
|
|
96
96
|
"--no-repair",
|
|
97
97
|
"--no-usage-telemetry",
|
|
98
98
|
"--no-memory",
|
|
99
|
+
"--trust-codex-hooks",
|
|
99
100
|
"--lock-wait",
|
|
100
101
|
"--backfill-budget",
|
|
101
102
|
"--json",
|
|
@@ -143,6 +144,7 @@ export function parseDoctorArgs(alias, args) {
|
|
|
143
144
|
kind: "doctor",
|
|
144
145
|
noUsageTelemetry: values.booleans.has("--no-usage-telemetry"),
|
|
145
146
|
noMemory: values.booleans.has("--no-memory"),
|
|
147
|
+
trustCodexHooks: values.booleans.has("--trust-codex-hooks"),
|
|
146
148
|
checkOnly: values.booleans.has("--check") || values.booleans.has("--no-repair"),
|
|
147
149
|
// Absent stays absent (BLI-11101): `doctor-budget.ts` owns the defaults,
|
|
148
150
|
// and `--json` without either flag runs under one bounded budget.
|
|
@@ -191,11 +191,13 @@ export function parseMemoryArgs(args) {
|
|
|
191
191
|
throw new Error("memory log takes one reason or --reason-stdin, never --dry-run");
|
|
192
192
|
if (verdict !== "win" && verdict !== "loss" && verdict !== "noise")
|
|
193
193
|
throw new Error("memory log needs win|loss|noise");
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// BLI-24380: BLI Memory is the only store, so --store is optional (bli). The retired
|
|
195
|
+
// `supermemory` and `both` still parse; runMemoryLog logs them as retired, never an error.
|
|
196
|
+
if (store !== undefined && store !== "bli" && store !== "supermemory" && store !== "both")
|
|
197
|
+
throw new Error("memory log --store takes bli (the only store)");
|
|
196
198
|
if (!reasonStdin && !reason.trim())
|
|
197
199
|
throw new Error("memory log needs a reason");
|
|
198
|
-
return { kind: "memory-log", verdict, store, reason, reasonStdin, homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
200
|
+
return { kind: "memory-log", verdict, store: store ?? "bli", reason, reasonStdin, homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
199
201
|
}
|
|
200
202
|
if (values.flags.has("--store") || values.booleans.has("--reason-stdin"))
|
|
201
203
|
throw new Error("--store and --reason-stdin require memory log");
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--to` readers of `cockpit notes`: `move --to "<shelf>"` and, since BLI-24437,
|
|
3
|
+
* `correct <id> --from "<misheard>" --to "<right>" [--transcript]`. Sibling of
|
|
4
|
+
* `local-args-tower-notes.ts`, which calls both; split out so that file stays a table of contents.
|
|
5
|
+
*/
|
|
6
|
+
import { optionalNonEmpty } from "./local-arg-values.js";
|
|
7
|
+
export function readNotesShelfTarget(action, values) {
|
|
8
|
+
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
9
|
+
const clearShelf = values.booleans.has("--clear-shelf");
|
|
10
|
+
if (action !== "move")
|
|
11
|
+
return { to, clearShelf };
|
|
12
|
+
if (to && clearShelf) {
|
|
13
|
+
throw new Error("notes move accepts either --to or --clear-shelf, not both.");
|
|
14
|
+
}
|
|
15
|
+
if (!to && !clearShelf && !values.flags.get("--folder")) {
|
|
16
|
+
throw new Error('notes move needs --to "<shelf>", --clear-shelf, or --folder "<path>".');
|
|
17
|
+
}
|
|
18
|
+
return { to, clearShelf };
|
|
19
|
+
}
|
|
20
|
+
/** BLI-24437: `correct` needs both terms; `--from` and `--transcript` mean nothing on any other verb. */
|
|
21
|
+
export function readNotesCorrection(action, values) {
|
|
22
|
+
const from = optionalNonEmpty(values.flags.get("--from"));
|
|
23
|
+
const transcript = values.booleans.has("--transcript");
|
|
24
|
+
if (action !== "correct") {
|
|
25
|
+
if (from !== undefined)
|
|
26
|
+
throw new Error("--from belongs to `cockpit notes correct <note-id> --from <term> --to <term>`.");
|
|
27
|
+
if (transcript)
|
|
28
|
+
throw new Error("--transcript belongs to `cockpit notes correct <note-id> --from <term> --to <term> --transcript`.");
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
if (!from || !optionalNonEmpty(values.flags.get("--to"))) {
|
|
32
|
+
throw new Error('notes correct needs --from "<misheard term>" and --to "<right term>".');
|
|
33
|
+
}
|
|
34
|
+
return { from, ...(transcript ? { transcript: true } : {}) };
|
|
35
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* address they have always had.
|
|
8
8
|
*/
|
|
9
9
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
10
|
+
import { readNotesCorrection, readNotesShelfTarget } from "./local-args-notes-targets.js";
|
|
10
11
|
/**
|
|
11
12
|
* The notetakers `cockpit notes connect` will actually seal a key for
|
|
12
13
|
* (BLI-4391). It mirrors `CONNECTABLE_NOTES_PROVIDERS` in the dashboard, and
|
|
@@ -34,7 +35,7 @@ const NOTES_ACTIONS = new Set([
|
|
|
34
35
|
"upload",
|
|
35
36
|
"paste",
|
|
36
37
|
"share",
|
|
37
|
-
"unshare",
|
|
38
|
+
"unshare", "correct",
|
|
38
39
|
"summarize",
|
|
39
40
|
"move",
|
|
40
41
|
"place",
|
|
@@ -61,7 +62,7 @@ const NOTES_ACTIONS = new Set([
|
|
|
61
62
|
const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
62
63
|
"show",
|
|
63
64
|
"share",
|
|
64
|
-
"unshare",
|
|
65
|
+
"unshare", "correct",
|
|
65
66
|
"summarize",
|
|
66
67
|
"move",
|
|
67
68
|
"place",
|
|
@@ -82,7 +83,7 @@ const NOTES_ALLOWED_FLAGS = [
|
|
|
82
83
|
"--file",
|
|
83
84
|
"--name",
|
|
84
85
|
"--exclude",
|
|
85
|
-
"--to",
|
|
86
|
+
"--to", "--from", "--transcript", // BLI-24437: `correct <id> --from <term> --to <term> [--transcript]`
|
|
86
87
|
"--folder",
|
|
87
88
|
"--clear-shelf",
|
|
88
89
|
"--apply",
|
|
@@ -122,7 +123,7 @@ const NOTES_VALUE_FLAGS = [
|
|
|
122
123
|
"--file",
|
|
123
124
|
"--name",
|
|
124
125
|
"--exclude",
|
|
125
|
-
"--to",
|
|
126
|
+
"--to", "--from",
|
|
126
127
|
"--folder",
|
|
127
128
|
"--series",
|
|
128
129
|
"--kind",
|
|
@@ -180,6 +181,7 @@ export function parseNotesArgs(args) {
|
|
|
180
181
|
exclude: optionalNonEmpty(values.flags.get("--exclude")),
|
|
181
182
|
...(to ? { to } : {}),
|
|
182
183
|
...(clearShelf ? { clearShelf } : {}),
|
|
184
|
+
...readNotesCorrection(action, values),
|
|
183
185
|
// BLI-4058. `place` says where a note belongs; only `--apply` moves it.
|
|
184
186
|
...(values.booleans.has("--apply") ? { apply: true } : {}),
|
|
185
187
|
series: optionalNonEmpty(values.flags.get("--series")),
|
|
@@ -201,7 +203,7 @@ function readNotesAction(first) {
|
|
|
201
203
|
// almost always wants — same reasoning as bare `cockpit` running convergence.
|
|
202
204
|
const action = (first === undefined ? "list" : first);
|
|
203
205
|
if (!NOTES_ACTIONS.has(action)) {
|
|
204
|
-
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status, transcription-stats, or review (and record start|stop|status|cancel, app install|uninstall|status).`);
|
|
206
|
+
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, correct, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status, transcription-stats, or review (and record start|stop|status|cancel, app install|uninstall|status).`);
|
|
205
207
|
}
|
|
206
208
|
return action;
|
|
207
209
|
}
|
|
@@ -368,19 +370,6 @@ function readNotesPaths(action, rest) {
|
|
|
368
370
|
}
|
|
369
371
|
return undefined;
|
|
370
372
|
}
|
|
371
|
-
function readNotesShelfTarget(action, values) {
|
|
372
|
-
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
373
|
-
const clearShelf = values.booleans.has("--clear-shelf");
|
|
374
|
-
if (action !== "move")
|
|
375
|
-
return { to, clearShelf };
|
|
376
|
-
if (to && clearShelf) {
|
|
377
|
-
throw new Error("notes move accepts either --to or --clear-shelf, not both.");
|
|
378
|
-
}
|
|
379
|
-
if (!to && !clearShelf && !values.flags.get("--folder")) {
|
|
380
|
-
throw new Error('notes move needs --to "<shelf>", --clear-shelf, or --folder "<path>".');
|
|
381
|
-
}
|
|
382
|
-
return { to, clearShelf };
|
|
383
|
-
}
|
|
384
373
|
function readNotesMonth(action, values) {
|
|
385
374
|
const month = optionalNonEmpty(values.flags.get("--month"));
|
|
386
375
|
if (month === undefined)
|