@bli-cockpit/cli 0.2.237 → 0.2.238
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/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +36 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +6 -2
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
package/dist/commands/local.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { runOpsCommand } from "./local-run-ops.js";
|
|
2
2
|
import { runGlossary } from "./glossary.js";
|
|
3
3
|
import { runSpend } from "./spend.js";
|
|
4
|
-
import { defaultIo,
|
|
5
|
-
import { isLocalHelpRequest, localCommandHelp
|
|
4
|
+
import { defaultIo, writeLine } from "./cli-io.js";
|
|
5
|
+
import { isLocalHelpRequest, localCommandHelp } from "./local-help.js";
|
|
6
6
|
import { beginStage, endStage } from "../crash-guard.js";
|
|
7
|
-
import {
|
|
7
|
+
import { reportCommandFailure, refuseArguments, stripGlobalToggles, watchWrites } from "./local-run-guards.js";
|
|
8
8
|
import { reportInstallEventsBestEffort } from "./install-receipts.js";
|
|
9
9
|
import { persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
|
|
10
10
|
import { rememberDiscoveryLimits } from "./local-discovery.js";
|
|
@@ -70,29 +70,36 @@ export { classifySyncFailureRecords, classifySyncHealthError, redactedSyncErrorD
|
|
|
70
70
|
export { assertCollectionRootPersisted } from "./collection-roots.js";
|
|
71
71
|
export { runSelfUpdate, SelfUpdateError, } from "./install-update.js";
|
|
72
72
|
import { runRoots } from "./roots.js";
|
|
73
|
+
/** Commands that act on THIS machine; the rest talk to Tower. */
|
|
74
|
+
const MACHINE_COMMAND_KINDS = [
|
|
75
|
+
"roots",
|
|
76
|
+
"install",
|
|
77
|
+
"onboard",
|
|
78
|
+
"update",
|
|
79
|
+
"doctor",
|
|
80
|
+
"login",
|
|
81
|
+
"logout",
|
|
82
|
+
"start",
|
|
83
|
+
"sync",
|
|
84
|
+
"analyze",
|
|
85
|
+
"backfill",
|
|
86
|
+
"status",
|
|
87
|
+
"sessions",
|
|
88
|
+
"serve",
|
|
89
|
+
"autostart",
|
|
90
|
+
"agent-rules",
|
|
91
|
+
"memory-log",
|
|
92
|
+
"memory",
|
|
93
|
+
"memory-store",
|
|
94
|
+
"clean",
|
|
95
|
+
"sources",
|
|
96
|
+
"release",
|
|
97
|
+
];
|
|
98
|
+
function isMachineCommand(command) {
|
|
99
|
+
return MACHINE_COMMAND_KINDS.includes(command.kind);
|
|
100
|
+
}
|
|
73
101
|
export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorAfterSync }) {
|
|
74
|
-
|
|
75
|
-
// parser's `allowedFlags` needs to learn it, and mirrored onto
|
|
76
|
-
// `COCKPIT_QUIET` so `tower-client.ts` can react without every call site
|
|
77
|
-
// threading a flag through. `--json` already implies it (tower-client.ts).
|
|
78
|
-
if (argv.includes("--quiet")) {
|
|
79
|
-
argv = argv.filter((token) => token !== "--quiet");
|
|
80
|
-
process.env.COCKPIT_QUIET = "1";
|
|
81
|
-
}
|
|
82
|
-
// BLI-10746: `--fresh`/`--allow-stale` are global toggles for the
|
|
83
|
-
// stale-while-revalidate read cache, same reason and same shape as
|
|
84
|
-
// `--quiet` above — stripped here so none of the seven cached read
|
|
85
|
-
// commands' own `allowedFlags` needs to learn them, and mirrored onto
|
|
86
|
-
// env vars `read-cache.ts` reads. One `cockpit` process makes at most one
|
|
87
|
-
// cacheable request, so a process-wide toggle is exactly a per-request one.
|
|
88
|
-
if (argv.includes("--fresh")) {
|
|
89
|
-
argv = argv.filter((token) => token !== "--fresh");
|
|
90
|
-
process.env.COCKPIT_READ_FRESH = "1";
|
|
91
|
-
}
|
|
92
|
-
if (argv.includes("--allow-stale")) {
|
|
93
|
-
argv = argv.filter((token) => token !== "--allow-stale");
|
|
94
|
-
process.env.COCKPIT_READ_ALLOW_STALE = "1";
|
|
95
|
-
}
|
|
102
|
+
argv = stripGlobalToggles(argv);
|
|
96
103
|
if (isLocalHelpRequest(argv)) {
|
|
97
104
|
writeLine(io.stdout, localCommandHelp(argv[0]));
|
|
98
105
|
return 0;
|
|
@@ -102,29 +109,7 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
102
109
|
command = parseLocalArgs(argv);
|
|
103
110
|
}
|
|
104
111
|
catch (error) {
|
|
105
|
-
|
|
106
|
-
// It used to write nothing there, so `cockpit usage sessions --json`
|
|
107
|
-
// without a filter read to an agent as an empty answer, not a refusal.
|
|
108
|
-
if (argv.includes("--json")) {
|
|
109
|
-
writeLine(io.stdout, JSON.stringify({
|
|
110
|
-
ok: false,
|
|
111
|
-
error: "invalid_arguments",
|
|
112
|
-
command: argv[0] ?? "",
|
|
113
|
-
detail: errorMessage(error),
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
console.error("[cockpit-cli] arguments refused", JSON.stringify({ reason: "invalid_arguments", command: argv[0] ?? "", json: argv.includes("--json") }));
|
|
117
|
-
writeLine(io.stderr, errorMessage(error));
|
|
118
|
-
writeLine(io.stderr, "");
|
|
119
|
-
// A refused `cockpit cal create --meet` used to print its one correct
|
|
120
|
-
// reason line and then the WHOLE collector wall — onboard, update, forty
|
|
121
|
-
// more — so the reason drowned and Edward read it as "it just shows usage"
|
|
122
|
-
// (2026-09-06). A parse error on a KNOWN command gets that command's own
|
|
123
|
-
// manual, which names the flags the reason line just asked for; only an
|
|
124
|
-
// unknown command earns the wall.
|
|
125
|
-
const command = argv[0];
|
|
126
|
-
writeLine(io.stderr, command && rootCommandNames.has(command) ? localCommandHelp(command) : localCommandHelp());
|
|
127
|
-
return 1;
|
|
112
|
+
return refuseArguments(argv, error, io);
|
|
128
113
|
}
|
|
129
114
|
// A limit typed on the command line is remembered for every later run,
|
|
130
115
|
// including the scheduled one nobody types into (BLI-2362). Done here so it
|
|
@@ -141,150 +126,117 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
141
126
|
if (stdoutWatch)
|
|
142
127
|
io = { ...io, stdout: stdoutWatch.stream };
|
|
143
128
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case "update":
|
|
151
|
-
return await runUpdate(command, io);
|
|
152
|
-
case "doctor":
|
|
153
|
-
return await runDoctor(command, io, {
|
|
154
|
-
reportInstallEvents: reportInstallEventsBestEffort,
|
|
155
|
-
selfUpdate: runSelfUpdate,
|
|
156
|
-
runLogin: runDoctorLogin,
|
|
157
|
-
resolveAndSaveRoots: resolveAndSaveDoctorRoots,
|
|
158
|
-
});
|
|
159
|
-
case "login":
|
|
160
|
-
return await runLogin(command, io);
|
|
161
|
-
case "logout":
|
|
162
|
-
return await runLogout(command, io);
|
|
163
|
-
case "start":
|
|
164
|
-
return await runStart(command, io);
|
|
165
|
-
case "sync":
|
|
166
|
-
return await runSync(command, io);
|
|
167
|
-
case "analyze":
|
|
168
|
-
return await runAnalyze(command, io);
|
|
169
|
-
case "jarvis":
|
|
170
|
-
return await runJarvis(command, io);
|
|
171
|
-
case "model":
|
|
172
|
-
return await runModel(command, io);
|
|
173
|
-
case "models":
|
|
174
|
-
return await runModels(command, io);
|
|
175
|
-
case "scout":
|
|
176
|
-
case "ci":
|
|
177
|
-
case "ops":
|
|
178
|
-
return await runOpsCommand(command, io);
|
|
179
|
-
case "slack":
|
|
180
|
-
return await runSlack(command, io);
|
|
181
|
-
case "settings":
|
|
182
|
-
return await runSettings(command, io);
|
|
183
|
-
case "team":
|
|
184
|
-
return await runTeam(command, io);
|
|
185
|
-
case "workbook":
|
|
186
|
-
return await runWorkbook(command, io);
|
|
187
|
-
case "brief":
|
|
188
|
-
// BLI-3458 S2.7/S2.8: `edit` and `rewrite` are the two PANEL actions
|
|
189
|
-
// reaching the terminal. Dispatched here rather than inside `brief.ts`
|
|
190
|
-
// so each one stays its own module — `brief.ts` owns reading and
|
|
191
|
-
// nothing else.
|
|
192
|
-
if (command.action === "edit")
|
|
193
|
-
return await runBriefEdit(command, io);
|
|
194
|
-
if (command.action === "rewrite")
|
|
195
|
-
return await runBriefRewrite(command, io);
|
|
196
|
-
// BLI-3605: the board's done checkbox, at a terminal.
|
|
197
|
-
if (command.action === "done" || command.action === "undone") {
|
|
198
|
-
return await runBriefDone(command, io);
|
|
199
|
-
}
|
|
200
|
-
return await runBrief(command, io);
|
|
201
|
-
case "correct":
|
|
202
|
-
return await runCorrect(command, io);
|
|
203
|
-
case "notes":
|
|
204
|
-
return await runNotes(command, io);
|
|
205
|
-
case "backfill":
|
|
206
|
-
return await runBackfillCommand(command, io);
|
|
207
|
-
case "status":
|
|
208
|
-
return await runStatus(command, io);
|
|
209
|
-
case "sessions":
|
|
210
|
-
return await runSessions(command, io);
|
|
211
|
-
case "serve":
|
|
212
|
-
return await runServe(command, io);
|
|
213
|
-
case "autostart":
|
|
214
|
-
return await runAutostart(command, io);
|
|
215
|
-
case "agent-rules":
|
|
216
|
-
return await runAgentRules(command, io);
|
|
217
|
-
case "memory-log":
|
|
218
|
-
return await runMemoryLog(command, io);
|
|
219
|
-
case "memory":
|
|
220
|
-
// BLI-4047: a bare `cockpit memory` says what the noun does. It used
|
|
221
|
-
// to install; `install` is still one word away and every unattended
|
|
222
|
-
// caller asks for it by name in code.
|
|
223
|
-
if (command.action === "help")
|
|
224
|
-
return runMemoryVerbList(io, command.json);
|
|
225
|
-
return await runMemoryInstall(command, io);
|
|
226
|
-
case "memory-store":
|
|
227
|
-
return await runMemoryStore(command, io);
|
|
228
|
-
case "clean":
|
|
229
|
-
return await runClean(command, io);
|
|
230
|
-
case "sources":
|
|
231
|
-
return await runSourcesCheck(command, io);
|
|
232
|
-
case "glossary":
|
|
233
|
-
return await runGlossary(command, io);
|
|
234
|
-
case "spend": return await runSpend(command, io);
|
|
235
|
-
case "docs": return await runDocs(command, io);
|
|
236
|
-
case "msg": return await runMsg(command, io);
|
|
237
|
-
case "issue": return await runIssue(command, io);
|
|
238
|
-
case "cal": return runCal(command, io);
|
|
239
|
-
case "mail": return await runMail(command, io);
|
|
240
|
-
case "project": return await runProject(command, io);
|
|
241
|
-
case "search": return await runSearch(command, io);
|
|
242
|
-
case "github": return await runGithub(command, io);
|
|
243
|
-
case "release": return await runRelease(command, io);
|
|
244
|
-
case "careers": return await runCareers(command, io);
|
|
245
|
-
case "rename": return await runRename(command, io);
|
|
246
|
-
case "setup": return await runSetup(command, io);
|
|
247
|
-
case "usage": return await runUsage(command, io);
|
|
248
|
-
case "speed": return await runSpeed(command, io);
|
|
249
|
-
case "logs": return await runLogs(command, io);
|
|
250
|
-
case "brain": return await runBrain(command, io);
|
|
251
|
-
case "hits": return await runHits(command, io);
|
|
252
|
-
case "intentions": return await runIntentions(command, io);
|
|
253
|
-
case "who": return await runWho(command, io);
|
|
254
|
-
case "people": return await runPeople(command, io);
|
|
255
|
-
case "approvals": return await runApprovals(command, io);
|
|
256
|
-
case "drills": return await runDrills(command, io);
|
|
257
|
-
case "manager": return await runManager(command, io);
|
|
258
|
-
case "image": return await runImage(command, io);
|
|
259
|
-
}
|
|
129
|
+
// Not awaited, as it never was: a `cal` rejection bypasses the net below.
|
|
130
|
+
if (command.kind === "cal")
|
|
131
|
+
return runCal(command, io);
|
|
132
|
+
if (isMachineCommand(command))
|
|
133
|
+
return await runMachineCommand(command, io);
|
|
134
|
+
return await runTowerCommand(command, io);
|
|
260
135
|
}
|
|
261
136
|
catch (error) {
|
|
262
|
-
|
|
263
|
-
// dropped the name, the code, the syscall and the cause — and never said
|
|
264
|
-
// WHICH subcommand died, so a scheduled `cockpit sync` and a hand-typed
|
|
265
|
-
// `cockpit doctor` failing the same way were indistinguishable in
|
|
266
|
-
// sync.err.log (BLI-3483). The human line is unchanged; the structured one
|
|
267
|
-
// beside it carries names and codes only, scrubbed by `describeError`.
|
|
268
|
-
console.error("[cockpit-cli] command failed", JSON.stringify({
|
|
269
|
-
reason: "command_failed",
|
|
270
|
-
command: command.kind,
|
|
271
|
-
...describeError(error),
|
|
272
|
-
}));
|
|
273
|
-
if (stdoutWatch && !stdoutWatch.written()) {
|
|
274
|
-
writeLine(io.stdout, JSON.stringify({
|
|
275
|
-
ok: false,
|
|
276
|
-
error: "command_failed",
|
|
277
|
-
command: command.kind,
|
|
278
|
-
detail: errorMessage(error),
|
|
279
|
-
}));
|
|
280
|
-
}
|
|
281
|
-
writeLine(io.stderr, errorMessage(error));
|
|
282
|
-
return 1;
|
|
137
|
+
return reportCommandFailure(command, error, io, stdoutWatch);
|
|
283
138
|
}
|
|
284
139
|
finally {
|
|
285
140
|
endStage(`command:${command.kind}`);
|
|
286
141
|
}
|
|
287
142
|
}
|
|
143
|
+
async function runMachineCommand(command, io) {
|
|
144
|
+
switch (command.kind) {
|
|
145
|
+
case "roots": return await runRoots(command, io);
|
|
146
|
+
case "install": return await runInstall(command, io);
|
|
147
|
+
case "onboard": return await runOnboard(command, io);
|
|
148
|
+
case "update": return await runUpdate(command, io);
|
|
149
|
+
case "doctor":
|
|
150
|
+
return await runDoctor(command, io, {
|
|
151
|
+
reportInstallEvents: reportInstallEventsBestEffort,
|
|
152
|
+
selfUpdate: runSelfUpdate,
|
|
153
|
+
runLogin: runDoctorLogin,
|
|
154
|
+
resolveAndSaveRoots: resolveAndSaveDoctorRoots,
|
|
155
|
+
});
|
|
156
|
+
case "login": return await runLogin(command, io);
|
|
157
|
+
case "logout": return await runLogout(command, io);
|
|
158
|
+
case "start": return await runStart(command, io);
|
|
159
|
+
case "sync": return await runSync(command, io);
|
|
160
|
+
case "analyze": return await runAnalyze(command, io);
|
|
161
|
+
case "backfill": return await runBackfillCommand(command, io);
|
|
162
|
+
case "status": return await runStatus(command, io);
|
|
163
|
+
case "sessions": return await runSessions(command, io);
|
|
164
|
+
case "serve": return await runServe(command, io);
|
|
165
|
+
case "autostart": return await runAutostart(command, io);
|
|
166
|
+
case "agent-rules": return await runAgentRules(command, io);
|
|
167
|
+
case "memory-log": return await runMemoryLog(command, io);
|
|
168
|
+
case "memory":
|
|
169
|
+
// BLI-4047: a bare `cockpit memory` says what the noun does. It used
|
|
170
|
+
// to install; `install` is still one word away and every unattended
|
|
171
|
+
// caller asks for it by name in code.
|
|
172
|
+
if (command.action === "help")
|
|
173
|
+
return runMemoryVerbList(io, command.json);
|
|
174
|
+
return await runMemoryInstall(command, io);
|
|
175
|
+
case "memory-store": return await runMemoryStore(command, io);
|
|
176
|
+
case "clean": return await runClean(command, io);
|
|
177
|
+
case "sources": return await runSourcesCheck(command, io);
|
|
178
|
+
case "release": return await runRelease(command, io);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function runTowerCommand(command, io) {
|
|
182
|
+
switch (command.kind) {
|
|
183
|
+
case "jarvis": return await runJarvis(command, io);
|
|
184
|
+
case "model": return await runModel(command, io);
|
|
185
|
+
case "models": return await runModels(command, io);
|
|
186
|
+
case "scout":
|
|
187
|
+
case "ci":
|
|
188
|
+
case "ops":
|
|
189
|
+
return await runOpsCommand(command, io);
|
|
190
|
+
case "slack": return await runSlack(command, io);
|
|
191
|
+
case "settings": return await runSettings(command, io);
|
|
192
|
+
case "team": return await runTeam(command, io);
|
|
193
|
+
case "workbook": return await runWorkbook(command, io);
|
|
194
|
+
case "brief": return await runBriefVerb(command, io);
|
|
195
|
+
case "correct": return await runCorrect(command, io);
|
|
196
|
+
case "notes": return await runNotes(command, io);
|
|
197
|
+
case "glossary": return await runGlossary(command, io);
|
|
198
|
+
case "spend": return await runSpend(command, io);
|
|
199
|
+
case "docs": return await runDocs(command, io);
|
|
200
|
+
case "msg": return await runMsg(command, io);
|
|
201
|
+
case "issue": return await runIssue(command, io);
|
|
202
|
+
case "mail": return await runMail(command, io);
|
|
203
|
+
case "project": return await runProject(command, io);
|
|
204
|
+
case "search": return await runSearch(command, io);
|
|
205
|
+
case "github": return await runGithub(command, io);
|
|
206
|
+
case "careers": return await runCareers(command, io);
|
|
207
|
+
case "rename": return await runRename(command, io);
|
|
208
|
+
case "setup": return await runSetup(command, io);
|
|
209
|
+
case "usage": return await runUsage(command, io);
|
|
210
|
+
case "speed": return await runSpeed(command, io);
|
|
211
|
+
case "logs": return await runLogs(command, io);
|
|
212
|
+
case "brain": return await runBrain(command, io);
|
|
213
|
+
case "hits": return await runHits(command, io);
|
|
214
|
+
case "intentions": return await runIntentions(command, io);
|
|
215
|
+
case "who": return await runWho(command, io);
|
|
216
|
+
case "people": return await runPeople(command, io);
|
|
217
|
+
case "approvals": return await runApprovals(command, io);
|
|
218
|
+
case "drills": return await runDrills(command, io);
|
|
219
|
+
case "manager": return await runManager(command, io);
|
|
220
|
+
case "image": return await runImage(command, io);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* BLI-3458 S2.7/S2.8: `edit` and `rewrite` are the two PANEL actions
|
|
225
|
+
* reaching the terminal. Dispatched here rather than inside `brief.ts`
|
|
226
|
+
* so each one stays its own module — `brief.ts` owns reading and
|
|
227
|
+
* nothing else.
|
|
228
|
+
*/
|
|
229
|
+
async function runBriefVerb(command, io) {
|
|
230
|
+
if (command.action === "edit")
|
|
231
|
+
return await runBriefEdit(command, io);
|
|
232
|
+
if (command.action === "rewrite")
|
|
233
|
+
return await runBriefRewrite(command, io);
|
|
234
|
+
// BLI-3605: the board's done checkbox, at a terminal.
|
|
235
|
+
if (command.action === "done" || command.action === "undone") {
|
|
236
|
+
return await runBriefDone(command, io);
|
|
237
|
+
}
|
|
238
|
+
return await runBrief(command, io);
|
|
239
|
+
}
|
|
288
240
|
async function runDoctorLogin(command, io) {
|
|
289
241
|
return runLogin({
|
|
290
242
|
kind: "login",
|
|
@@ -311,25 +263,4 @@ async function resolveAndSaveDoctorRoots(command, io) {
|
|
|
311
263
|
if (!command.json) {
|
|
312
264
|
writeLine(io.stdout, `Saved collection roots: ${resolution.collectionRoots.join(", ")}`);
|
|
313
265
|
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* The same stream, with a flag that flips on the first write. A Proxy rather
|
|
317
|
-
* than a copied object so `isTTY`, `columns` and every listener method still
|
|
318
|
-
* reach the real stream.
|
|
319
|
-
*/
|
|
320
|
-
function watchWrites(stream) {
|
|
321
|
-
let wrote = false;
|
|
322
|
-
const watched = new Proxy(stream, {
|
|
323
|
-
get(target, property) {
|
|
324
|
-
if (property === "write") {
|
|
325
|
-
return (...args) => {
|
|
326
|
-
wrote = true;
|
|
327
|
-
return target.write.apply(target, args);
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
const value = Reflect.get(target, property, target);
|
|
331
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
return { stream: watched, written: () => wrote };
|
|
335
266
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -33,6 +33,18 @@ export async function runLogin(command, io) {
|
|
|
33
33
|
if (command.pairCode)
|
|
34
34
|
return 1;
|
|
35
35
|
}
|
|
36
|
+
const result = await pairByDashboardApproval(command, dashboardUrl, io);
|
|
37
|
+
reportPaired(command, io, {
|
|
38
|
+
email: result.session.email ?? result.session.auth_subject_id,
|
|
39
|
+
deviceName: result.session.device_name ?? result.session.device_id ?? "unknown",
|
|
40
|
+
sessionFile: result.session_file,
|
|
41
|
+
json: result,
|
|
42
|
+
});
|
|
43
|
+
writeNextStep(command, io, config.default_repo_paths.length > 0);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
/** The legacy arm: sign in by email, then wait for the person to approve the device in the dashboard. */
|
|
47
|
+
async function pairByDashboardApproval(command, dashboardUrl, io) {
|
|
36
48
|
const claimedOwnerEmail = await resolveInteractiveLoginEmail(command, io);
|
|
37
49
|
const pairingAccessToken = await requestPairingAccessToken({
|
|
38
50
|
dashboardUrl,
|
|
@@ -40,7 +52,7 @@ export async function runLogin(command, io) {
|
|
|
40
52
|
noAuth: command.noAuth,
|
|
41
53
|
json: command.json,
|
|
42
54
|
}, io);
|
|
43
|
-
|
|
55
|
+
return await pairLocalCollectorWithAuthFallback({
|
|
44
56
|
homeDir: command.homeDir,
|
|
45
57
|
dashboardUrl,
|
|
46
58
|
claimedOwnerEmail,
|
|
@@ -58,14 +70,6 @@ export async function runLogin(command, io) {
|
|
|
58
70
|
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
59
71
|
},
|
|
60
72
|
}, io);
|
|
61
|
-
reportPaired(command, io, {
|
|
62
|
-
email: result.session.email ?? result.session.auth_subject_id,
|
|
63
|
-
deviceName: result.session.device_name ?? result.session.device_id ?? "unknown",
|
|
64
|
-
sessionFile: result.session_file,
|
|
65
|
-
json: result,
|
|
66
|
-
});
|
|
67
|
-
writeNextStep(command, io, config.default_repo_paths.length > 0);
|
|
68
|
-
return 0;
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
75
|
* The one-link ceremony. Returns null when it could not finish, having already
|
package/dist/commands/logs.js
CHANGED
|
@@ -30,15 +30,8 @@ function refuse(io, json, reason, detail) {
|
|
|
30
30
|
return 1;
|
|
31
31
|
}
|
|
32
32
|
export async function runLogs(command, io, deps = {}) {
|
|
33
|
-
if (command.keyAction)
|
|
34
|
-
|
|
35
|
-
const result = await callTower(tower, { path: "/api/settings/log-ingest-keys", method: "POST", body: { source: command.source, action: command.keyAction }, label: "logs keys" });
|
|
36
|
-
if (!result.ok)
|
|
37
|
-
return writeCommandFailure(io, command.json, result);
|
|
38
|
-
// Deliberate one-time credential output; never stderr, cache or log.
|
|
39
|
-
writeLine(io.stdout, command.json ? JSON.stringify(result.body) : String(asRecord(result.body).key));
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
33
|
+
if (command.keyAction)
|
|
34
|
+
return mintLogKey(command, io, command.keyAction);
|
|
42
35
|
if (command.search)
|
|
43
36
|
return runLogsSearch(command, io);
|
|
44
37
|
// BLI-12855: a project name (or --stored, or --list) reads the digests Tower's cron stored, not a local log.
|
|
@@ -46,52 +39,76 @@ export async function runLogs(command, io, deps = {}) {
|
|
|
46
39
|
if (command.list || stored)
|
|
47
40
|
return runStoredLogs(command, io, { project: stored ?? "tower" });
|
|
48
41
|
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
table = ClassTable.fromJson(await readFile(command.tableFile, "utf8"));
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
return refuse(io, command.json, "table_unreadable", `--table ${command.tableFile} is not a readable class table: ${error instanceof Error ? error.message.slice(0, 160) : "unknown error"}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
let stdinText;
|
|
42
|
+
const tableRead = await readClassTable(command, io);
|
|
43
|
+
if ("exitCode" in tableRead)
|
|
44
|
+
return tableRead.exitCode;
|
|
59
45
|
const stdinIsTerminal = command.source === "-" && isInteractiveStdin(io);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
return refuse(io, command.json, "stdin_too_large", error instanceof Error ? error.message : "stdin could not be read");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
46
|
+
const stdinRead = await readPipedLogs(command, io, stdinIsTerminal);
|
|
47
|
+
if ("exitCode" in stdinRead)
|
|
48
|
+
return stdinRead.exitCode;
|
|
68
49
|
const outcome = await digestSource({
|
|
69
50
|
source: command.source,
|
|
70
51
|
since: command.since,
|
|
71
52
|
raw: command.raw,
|
|
72
53
|
rawLimit: command.rawLimit,
|
|
73
|
-
table,
|
|
54
|
+
table: tableRead.value,
|
|
74
55
|
allLevels: command.allLevels,
|
|
75
56
|
}, {
|
|
76
57
|
stateDir: paths.state_dir,
|
|
77
|
-
stdinText,
|
|
58
|
+
stdinText: stdinRead.value,
|
|
78
59
|
stdinIsTerminal,
|
|
79
60
|
run: deps.run,
|
|
80
61
|
nowMs: deps.nowMs,
|
|
81
62
|
log: (message, fields) => writeLine(io.stderr, `${message} ${JSON.stringify(fields)}`),
|
|
82
63
|
});
|
|
83
|
-
if (!outcome.ok &&
|
|
84
|
-
|
|
85
|
-
writeLine(io.stderr, `${TAG} local source unreadable, reading the stored digest ${JSON.stringify({ reason: outcome.reason })}`);
|
|
86
|
-
return runStoredLogs(command, io, {
|
|
87
|
-
project: "tower",
|
|
88
|
-
fallbackReason: outcome.reason,
|
|
89
|
-
// The person asked for a local read: if the stored digest is out of reach too, the local refusal is the answer.
|
|
90
|
-
onNotPaired: () => refuse(io, command.json, outcome.reason, `${outcome.detail} This machine is not paired with Tower (cockpit login), so the stored digest could not be read either.`),
|
|
91
|
-
});
|
|
64
|
+
if (!outcome.ok && canFallBackToStoredDigest(command, outcome)) {
|
|
65
|
+
return answerFromStoredDigest(command, io, outcome);
|
|
92
66
|
}
|
|
93
67
|
if (!outcome.ok)
|
|
94
68
|
return refuse(io, command.json, outcome.reason, outcome.detail);
|
|
95
69
|
writeLine(io.stdout, command.json ? JSON.stringify(outcome) : outcome.text);
|
|
96
70
|
return 0;
|
|
71
|
+
}
|
|
72
|
+
async function mintLogKey(command, io, keyAction) {
|
|
73
|
+
const tower = await openTower("logs keys", command, io);
|
|
74
|
+
const result = await callTower(tower, { path: "/api/settings/log-ingest-keys", method: "POST", body: { source: command.source, action: keyAction }, label: "logs keys" });
|
|
75
|
+
if (!result.ok)
|
|
76
|
+
return writeCommandFailure(io, command.json, result);
|
|
77
|
+
// Deliberate one-time credential output; never stderr, cache or log.
|
|
78
|
+
writeLine(io.stdout, command.json ? JSON.stringify(result.body) : String(asRecord(result.body).key));
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
async function readClassTable(command, io) {
|
|
82
|
+
if (!command.tableFile)
|
|
83
|
+
return { value: undefined };
|
|
84
|
+
try {
|
|
85
|
+
return { value: ClassTable.fromJson(await readFile(command.tableFile, "utf8")) };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return { exitCode: refuse(io, command.json, "table_unreadable", `--table ${command.tableFile} is not a readable class table: ${error instanceof Error ? error.message.slice(0, 160) : "unknown error"}`) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The piped lines for `logs -`; nothing to read when the source is not stdin or stdin is a terminal. */
|
|
92
|
+
async function readPipedLogs(command, io, stdinIsTerminal) {
|
|
93
|
+
if (command.source !== "-" || stdinIsTerminal)
|
|
94
|
+
return { value: undefined };
|
|
95
|
+
try {
|
|
96
|
+
return { value: await readPipedText(io.stdin, { maxChars: MAX_STDIN_CHARS, overflowMessage: "logs - reads at most 64 MB of piped lines." }) };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { exitCode: refuse(io, command.json, "stdin_too_large", error instanceof Error ? error.message : "stdin could not be read") };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function canFallBackToStoredDigest(command, refusal) {
|
|
103
|
+
return LOCAL_ACCESS_REFUSALS.has(refusal.reason) && (command.source === "dashboard" || command.source === "railway") && !command.raw;
|
|
104
|
+
}
|
|
105
|
+
function answerFromStoredDigest(command, io, refusal) {
|
|
106
|
+
// No local access to the platform: say so, then answer from what the cron stored (a refusal of its own if this machine is not paired).
|
|
107
|
+
writeLine(io.stderr, `${TAG} local source unreadable, reading the stored digest ${JSON.stringify({ reason: refusal.reason })}`);
|
|
108
|
+
return runStoredLogs(command, io, {
|
|
109
|
+
project: "tower",
|
|
110
|
+
fallbackReason: refusal.reason,
|
|
111
|
+
// The person asked for a local read: if the stored digest is out of reach too, the local refusal is the answer.
|
|
112
|
+
onNotPaired: () => refuse(io, command.json, refusal.reason, `${refusal.detail} This machine is not paired with Tower (cockpit login), so the stored digest could not be read either.`),
|
|
113
|
+
});
|
|
97
114
|
}
|
|
@@ -116,14 +116,48 @@ function totalTooLarge(total) {
|
|
|
116
116
|
export async function readMailAttachments(paths) {
|
|
117
117
|
if (paths.length === 0)
|
|
118
118
|
return { ok: true, attachments: [], summary: { count: 0, bytes: 0, sizes: [], content_types: [] } };
|
|
119
|
+
const refusedByName = refuseByCountOrName(paths);
|
|
120
|
+
if (refusedByName)
|
|
121
|
+
return refusedByName;
|
|
122
|
+
const statted = await statEveryAttachment(paths);
|
|
123
|
+
if (!statted.ok)
|
|
124
|
+
return statted;
|
|
125
|
+
const tooLarge = totalTooLarge(statted.sizes.reduce((sum, size) => sum + size, 0));
|
|
126
|
+
if (tooLarge)
|
|
127
|
+
return tooLarge;
|
|
128
|
+
const read = await readEveryAttachment(paths);
|
|
129
|
+
if (!read.ok)
|
|
130
|
+
return read;
|
|
131
|
+
// A file that grew between the stat and the read is the file as it is now.
|
|
132
|
+
const total = read.sizes.reduce((sum, size) => sum + size, 0);
|
|
133
|
+
const grew = totalTooLarge(total);
|
|
134
|
+
if (grew)
|
|
135
|
+
return grew;
|
|
136
|
+
if (read.sizes.includes(0)) {
|
|
137
|
+
return refuse("attachment_unreadable", `Attachment ${read.sizes.indexOf(0) + 1} is empty, and an empty file is not worth sending. ${NOTHING_SENT}`);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
attachments: read.attachments,
|
|
142
|
+
summary: {
|
|
143
|
+
count: read.attachments.length,
|
|
144
|
+
bytes: total,
|
|
145
|
+
sizes: read.sizes,
|
|
146
|
+
content_types: read.attachments.map((attachment) => attachment.content_type),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function refuseByCountOrName(paths) {
|
|
119
151
|
if (paths.length > MAIL_ATTACHMENT_MAX_COUNT) {
|
|
120
152
|
return refuse("too_many_attachments", `${paths.length} files were named with --attach and one message carries at most ${MAIL_ATTACHMENT_MAX_COUNT}. ${NOTHING_SENT}`);
|
|
121
153
|
}
|
|
122
154
|
// Names first, before a single stat or read: a secret-looking file is never opened.
|
|
123
155
|
const secret = paths.findIndex((filePath) => attachmentLooksSecret(path.basename(filePath)));
|
|
124
|
-
if (secret
|
|
125
|
-
return
|
|
126
|
-
}
|
|
156
|
+
if (secret === -1)
|
|
157
|
+
return null;
|
|
158
|
+
return refuse("attachment_secret_like", `Attachment ${secret + 1} has a name that looks like a key or credential file, so it was not opened. ${NOTHING_SENT}`);
|
|
159
|
+
}
|
|
160
|
+
async function statEveryAttachment(paths) {
|
|
127
161
|
const sizes = [];
|
|
128
162
|
for (const [index, filePath] of paths.entries()) {
|
|
129
163
|
const label = `Attachment ${index + 1}`;
|
|
@@ -144,11 +178,12 @@ export async function readMailAttachments(paths) {
|
|
|
144
178
|
return refuse("attachment_unreadable", `${label} is empty, and an empty file is not worth sending. ${NOTHING_SENT}`);
|
|
145
179
|
sizes.push(size);
|
|
146
180
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
return { ok: true, sizes };
|
|
182
|
+
}
|
|
183
|
+
/** The files as they are now: their wire form, and the byte count each one really had. */
|
|
184
|
+
async function readEveryAttachment(paths) {
|
|
150
185
|
const attachments = [];
|
|
151
|
-
const
|
|
186
|
+
const sizes = [];
|
|
152
187
|
for (const [index, filePath] of paths.entries()) {
|
|
153
188
|
let bytes;
|
|
154
189
|
try {
|
|
@@ -158,7 +193,7 @@ export async function readMailAttachments(paths) {
|
|
|
158
193
|
const code = error.code;
|
|
159
194
|
return refuse("attachment_unreadable", `Attachment ${index + 1} could not be read (${code ?? "unknown error"}). ${NOTHING_SENT}`);
|
|
160
195
|
}
|
|
161
|
-
|
|
196
|
+
sizes.push(bytes.length);
|
|
162
197
|
const filename = path.basename(filePath);
|
|
163
198
|
attachments.push({
|
|
164
199
|
filename,
|
|
@@ -166,24 +201,7 @@ export async function readMailAttachments(paths) {
|
|
|
166
201
|
content_base64: bytes.toString("base64"),
|
|
167
202
|
});
|
|
168
203
|
}
|
|
169
|
-
|
|
170
|
-
const total = actual.reduce((sum, size) => sum + size, 0);
|
|
171
|
-
const grew = totalTooLarge(total);
|
|
172
|
-
if (grew)
|
|
173
|
-
return grew;
|
|
174
|
-
if (actual.includes(0)) {
|
|
175
|
-
return refuse("attachment_unreadable", `Attachment ${actual.indexOf(0) + 1} is empty, and an empty file is not worth sending. ${NOTHING_SENT}`);
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
ok: true,
|
|
179
|
-
attachments,
|
|
180
|
-
summary: {
|
|
181
|
-
count: attachments.length,
|
|
182
|
-
bytes: total,
|
|
183
|
-
sizes: actual,
|
|
184
|
-
content_types: attachments.map((attachment) => attachment.content_type),
|
|
185
|
-
},
|
|
186
|
-
};
|
|
204
|
+
return { ok: true, attachments, sizes };
|
|
187
205
|
}
|
|
188
206
|
/**
|
|
189
207
|
* The second check: the whole request, not just the files. Called with the JSON
|