@bli-cockpit/cli 0.2.49 → 0.2.51
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/raw-evidence-claude-reader.js +108 -0
- package/dist/adapters/raw-evidence-codex-reader.js +147 -0
- package/dist/adapters/raw-evidence-collection-state.js +199 -0
- package/dist/adapters/raw-evidence-facts.js +338 -0
- package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
- package/dist/adapters/raw-evidence-image-reader.js +107 -0
- package/dist/adapters/raw-evidence-sanitize.js +56 -0
- package/dist/adapters/raw-evidence-transcript-file.js +182 -0
- package/dist/adapters/raw-evidence.js +63 -1183
- package/dist/commands/backfill-batches.js +34 -0
- package/dist/commands/backfill-candidates.js +54 -0
- package/dist/commands/backfill-checkpoint.js +101 -0
- package/dist/commands/backfill-command-line.js +70 -0
- package/dist/commands/backfill-evidence-outcomes.js +104 -0
- package/dist/commands/backfill-issues.js +265 -0
- package/dist/commands/backfill-output.js +75 -0
- package/dist/commands/backfill-plan.js +71 -0
- package/dist/commands/backfill-reasons.js +107 -0
- package/dist/commands/backfill-report.js +298 -0
- package/dist/commands/backfill-result.js +150 -0
- package/dist/commands/backfill-scan.js +274 -0
- package/dist/commands/backfill-scope.js +114 -0
- package/dist/commands/backfill-session-report.js +145 -0
- package/dist/commands/backfill-types.js +1 -0
- package/dist/commands/backfill-upload.js +212 -0
- package/dist/commands/backfill.js +41 -1961
- package/dist/commands/doctor.js +57 -0
- package/dist/commands/jarvis-trace.js +184 -0
- package/dist/commands/jarvis.js +144 -4
- package/dist/commands/local-args-collector.js +26 -0
- package/dist/commands/local-args-tower.js +21 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help.js +19 -2
- package/dist/commands/local.js +3 -0
- package/dist/commands/memory-install-claude.js +294 -0
- package/dist/commands/memory-install-codex.js +205 -0
- package/dist/commands/memory-install-contract.js +286 -0
- package/dist/commands/memory-install-files.js +63 -0
- package/dist/commands/memory-install-skills.js +121 -0
- package/dist/commands/memory-install-toml.js +265 -0
- package/dist/commands/memory-install.js +465 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sync-followups.js +105 -0
- package/dist/commands/sync.js +7 -1
- package/dist/local-state-attributed-target.js +75 -0
- package/dist/local-state-config.js +147 -0
- package/dist/local-state-files.js +59 -0
- package/dist/local-state-identity.js +73 -0
- package/dist/local-state-pairing.js +263 -0
- package/dist/local-state-paths.js +61 -0
- package/dist/local-state-session.js +68 -0
- package/dist/local-state-status.js +163 -0
- package/dist/local-state-work-context.js +190 -0
- package/dist/local-state.js +34 -848
- package/dist/tower-client.js +3 -2
- package/dist/tower-stream.js +57 -3
- package/package.json +2 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
|
10
10
|
import { detectSecondCockpitInstall } from "../second-install.js";
|
|
11
11
|
import { runRawEvidenceLocalGc, rawEvidenceGcSummary } from "../raw-evidence-gc.js";
|
|
12
12
|
import { runBackfillCommand } from "./backfill.js";
|
|
13
|
+
import { installMemoryIntegration, inspectMemoryIntegration, } from "./memory-install.js";
|
|
13
14
|
import { createInteractiveExecRunner } from "../process-runner.js";
|
|
14
15
|
const GC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15
16
|
export async function runDoctor(command, io, hooks, overrides = {}) {
|
|
@@ -81,6 +82,14 @@ function doctorInvariants() {
|
|
|
81
82
|
check: (context) => context.deps.checkAutostart(context),
|
|
82
83
|
fix: (context, _state) => context.deps.fixAutostart(context),
|
|
83
84
|
},
|
|
85
|
+
// BLI-3580. Runs right after autostart because it is the same kind of
|
|
86
|
+
// thing: a registration on this machine that nobody should have to be
|
|
87
|
+
// asked about. Its fix writes host configs, never installs software.
|
|
88
|
+
{
|
|
89
|
+
id: "memory-registered",
|
|
90
|
+
check: (context) => context.deps.checkMemory(context),
|
|
91
|
+
fix: (context, _state) => context.deps.fixMemory(context),
|
|
92
|
+
},
|
|
84
93
|
{
|
|
85
94
|
id: "backfill-complete",
|
|
86
95
|
check: (context) => context.deps.checkBackfill(context),
|
|
@@ -111,6 +120,8 @@ function defaultDoctorDeps(hooks) {
|
|
|
111
120
|
checkSingleInstall: checkSingleInstallState,
|
|
112
121
|
checkAutostart: checkAutostartState,
|
|
113
122
|
fixAutostart: fixAutostartState,
|
|
123
|
+
checkMemory: checkMemoryState,
|
|
124
|
+
fixMemory: fixMemoryState,
|
|
114
125
|
checkBackfill: checkBackfillState,
|
|
115
126
|
fixBackfill: fixBackfillState,
|
|
116
127
|
checkGc: checkGcState,
|
|
@@ -307,6 +318,52 @@ async function fixAutostartState(context) {
|
|
|
307
318
|
}
|
|
308
319
|
return ok("autostart-alive", "installed", "background sync installed and running");
|
|
309
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* BLI Memory's registration on this machine (BLI-3580).
|
|
323
|
+
*
|
|
324
|
+
* The step owns the CONFIG, never the software: it writes the MCP entry, the
|
|
325
|
+
* hooks and the Codex table only when `bli-memory-mcp` actually resolves. With
|
|
326
|
+
* no server it writes NOTHING and reads `skipped bin_missing` — registering
|
|
327
|
+
* hooks that point at an absent binary would make every Claude Code turn print
|
|
328
|
+
* a hook failure, which is worse than waiting a day.
|
|
329
|
+
*/
|
|
330
|
+
async function checkMemoryState(context) {
|
|
331
|
+
const outcome = await inspectMemoryIntegration(memoryCommandFor(context), context.io);
|
|
332
|
+
return memoryStepState(outcome, "check");
|
|
333
|
+
}
|
|
334
|
+
async function fixMemoryState(context) {
|
|
335
|
+
const outcome = await installMemoryIntegration(memoryCommandFor(context), context.io);
|
|
336
|
+
return memoryStepState(outcome, "fix");
|
|
337
|
+
}
|
|
338
|
+
function memoryCommandFor(context) {
|
|
339
|
+
return {
|
|
340
|
+
kind: "memory",
|
|
341
|
+
action: "install",
|
|
342
|
+
homeDir: context.command.homeDir,
|
|
343
|
+
dashboardUrl: context.command.dashboardUrl,
|
|
344
|
+
dryRun: false,
|
|
345
|
+
json: context.command.json,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function memoryStepState(outcome, phase) {
|
|
349
|
+
const broken = outcome.targets.filter((target) => target.target !== "bin" && target.status === "failed");
|
|
350
|
+
if (broken.length > 0) {
|
|
351
|
+
const first = broken[0];
|
|
352
|
+
const message = `${first?.target ?? "memory"}: ${first?.reason ?? "failed"}`;
|
|
353
|
+
return phase === "fix"
|
|
354
|
+
? fail("memory-registered", first?.reason ?? "memory_install_failed", message)
|
|
355
|
+
: needsFix("memory-registered", first?.reason ?? "memory_install_failed", message);
|
|
356
|
+
}
|
|
357
|
+
const pending = outcome.targets.filter((target) => target.target !== "bin" &&
|
|
358
|
+
(target.status === "missing" || target.status === "mismatch"));
|
|
359
|
+
if (pending.length > 0) {
|
|
360
|
+
return needsFix("memory-registered", "registration_incomplete", `BLI Memory is not registered with ${pending.map((target) => target.target).join(", ")}`);
|
|
361
|
+
}
|
|
362
|
+
if (!outcome.bin_found) {
|
|
363
|
+
return skipped("memory-registered", "bin_missing", "bli-memory-mcp is not on this machine yet; nothing was written and the next run will try again");
|
|
364
|
+
}
|
|
365
|
+
return ok("memory-registered", phase === "fix" ? "installed" : "already_installed", "BLI Memory is registered with both agent hosts");
|
|
366
|
+
}
|
|
310
367
|
/**
|
|
311
368
|
* Pure so it can be unit-tested without touching the real machine's home
|
|
312
369
|
* directory (`getCollectorRuntimePaths()` defaults to `os.homedir()` and
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit jarvis --trace <id|last>` — the step tree of one turn (BLI-3560).
|
|
3
|
+
*
|
|
4
|
+
* The one thing anybody here ever opened Langfuse for, in the terminal where
|
|
5
|
+
* the turn was asked. Every model step, every tool call, every server-side
|
|
6
|
+
* memory step (BLI-3416's doubled answer was one of those and no other surface
|
|
7
|
+
* showed it), with how long each took, which model ran it, what it spent and
|
|
8
|
+
* whether it failed.
|
|
9
|
+
*
|
|
10
|
+
* ## `last` and the remembered receipt
|
|
11
|
+
*
|
|
12
|
+
* A turn now hands back its own trace id, and this module writes that id — with
|
|
13
|
+
* the conversation it belonged to — to one small file beside the collector's
|
|
14
|
+
* other state. `--trace last` reads it, so the loop is: ask a question, watch
|
|
15
|
+
* it come back slow, type `cockpit jarvis --trace last`. If the file is not
|
|
16
|
+
* there (a fresh shell, a machine that has not asked anything since the
|
|
17
|
+
* release), the file's remembered THREAD is used to ask the server for that
|
|
18
|
+
* conversation's newest turn instead — and when there is neither, the refusal
|
|
19
|
+
* says so in words rather than printing nothing.
|
|
20
|
+
*
|
|
21
|
+
* The file holds two opaque ids and a timestamp. No question, no answer, no
|
|
22
|
+
* name; it is a bookmark, not a transcript.
|
|
23
|
+
*
|
|
24
|
+
* ## The lines
|
|
25
|
+
*
|
|
26
|
+
* Rendered SERVER-SIDE and printed verbatim. That is deliberate: the ops door,
|
|
27
|
+
* this terminal and JARVIS's own `readTurnTrace` tool all read the same
|
|
28
|
+
* sentences from `lib/ops/turn-trace-lines.ts`, so an older CLI against a newer
|
|
29
|
+
* dashboard still prints the current tree rather than its own stale idea of one.
|
|
30
|
+
*/
|
|
31
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
32
|
+
import os from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import { colorEnabled, dim, writeLine } from "./cli-io.js";
|
|
35
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
36
|
+
import { towerFailureDetail, towerJsonRequest } from "../tower-client.js";
|
|
37
|
+
/** The bookmark file's name, inside the collector's own state directory. */
|
|
38
|
+
const LAST_TURN_FILE = "last-turn-trace.json";
|
|
39
|
+
function bookmarkPath(homeDir) {
|
|
40
|
+
const paths = getCollectorRuntimePaths(homeDir ?? os.homedir());
|
|
41
|
+
return path.join(paths.state_dir, LAST_TURN_FILE);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Remember the turn that just happened. Never throws: a bookmark that could not
|
|
45
|
+
* be written must not fail the answer a person already received, so the failure
|
|
46
|
+
* is named on stderr and the turn stands.
|
|
47
|
+
*/
|
|
48
|
+
export async function rememberTurnTrace(turn, io, homeDir) {
|
|
49
|
+
if (!turn.traceId && !turn.threadId)
|
|
50
|
+
return;
|
|
51
|
+
const file = bookmarkPath(homeDir);
|
|
52
|
+
const payload = {
|
|
53
|
+
traceId: turn.traceId ?? null,
|
|
54
|
+
threadId: turn.threadId ?? null,
|
|
55
|
+
at: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
59
|
+
await writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
writeLine(io.stderr, `[jarvis cli] could not remember this turn's trace ${JSON.stringify({
|
|
63
|
+
reason: "bookmark_write_failed",
|
|
64
|
+
error: error instanceof Error ? error.name : "unknown",
|
|
65
|
+
})}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The remembered turn, or null when there is not one. Never throws. */
|
|
69
|
+
export async function readRememberedTurn(homeDir) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readFile(bookmarkPath(homeDir), "utf8");
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
const traceId = typeof parsed.traceId === "string" ? parsed.traceId : null;
|
|
74
|
+
const threadId = typeof parsed.threadId === "string" ? parsed.threadId : null;
|
|
75
|
+
if (!traceId && !threadId)
|
|
76
|
+
return null;
|
|
77
|
+
return { traceId, threadId, at: typeof parsed.at === "string" ? parsed.at : "" };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// A missing or unreadable bookmark is "nothing is remembered", which is the
|
|
81
|
+
// exact answer asked for. The caller says so out loud in a sentence.
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function runJarvisTrace(context, io) {
|
|
86
|
+
const log = (line) => writeLine(io.stderr, line);
|
|
87
|
+
let traceId = context.requested;
|
|
88
|
+
let resolvedFrom = "argument";
|
|
89
|
+
if (context.requested === "last") {
|
|
90
|
+
const remembered = await readRememberedTurn(context.homeDir);
|
|
91
|
+
if (!remembered) {
|
|
92
|
+
return refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt.");
|
|
93
|
+
}
|
|
94
|
+
if (remembered.traceId) {
|
|
95
|
+
traceId = remembered.traceId;
|
|
96
|
+
resolvedFrom = "bookmark";
|
|
97
|
+
}
|
|
98
|
+
else if (remembered.threadId) {
|
|
99
|
+
// The bookmark knew the conversation but not the turn (an older receipt).
|
|
100
|
+
// Ask the server for that conversation's newest turn rather than nothing.
|
|
101
|
+
const newest = await newestTraceForThread(context, remembered.threadId, io);
|
|
102
|
+
if (!newest) {
|
|
103
|
+
return refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new.");
|
|
104
|
+
}
|
|
105
|
+
traceId = newest;
|
|
106
|
+
resolvedFrom = "thread";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const result = await towerJsonRequest({
|
|
110
|
+
dashboardUrl: context.dashboardUrl,
|
|
111
|
+
path: `/api/ops/trace/${encodeURIComponent(traceId)}`,
|
|
112
|
+
deviceToken: context.deviceToken,
|
|
113
|
+
fetch: io.fetch,
|
|
114
|
+
method: "GET",
|
|
115
|
+
label: "jarvis:trace",
|
|
116
|
+
timeoutMs: 30_000,
|
|
117
|
+
log,
|
|
118
|
+
});
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
return refuse(context, io, result.reason, towerFailureDetail(result.reason, result.detail));
|
|
121
|
+
}
|
|
122
|
+
const body = result.body;
|
|
123
|
+
if (body.ok === false) {
|
|
124
|
+
return refuse(context, io, body.error ?? "trace_unavailable", body.message ?? "Tower had nothing to show for that turn.");
|
|
125
|
+
}
|
|
126
|
+
if (context.json) {
|
|
127
|
+
writeLine(io.stdout, JSON.stringify(body));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
writeTraceTree(io, traceId, body);
|
|
131
|
+
}
|
|
132
|
+
writeLine(io.stderr, `[jarvis cli] trace read ${JSON.stringify({
|
|
133
|
+
resolved_from: resolvedFrom,
|
|
134
|
+
spans: body.trace?.spanCount ?? null,
|
|
135
|
+
errors: body.trace?.errorCount ?? null,
|
|
136
|
+
running: body.trace?.runningCount ?? null,
|
|
137
|
+
duration_ms: body.trace?.durationMs ?? null,
|
|
138
|
+
truncated: body.truncated ?? null,
|
|
139
|
+
})}`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
async function newestTraceForThread(context, threadId, io) {
|
|
143
|
+
const result = await towerJsonRequest({
|
|
144
|
+
dashboardUrl: context.dashboardUrl,
|
|
145
|
+
path: `/api/ops/trace?thread=${encodeURIComponent(threadId)}&limit=1`,
|
|
146
|
+
deviceToken: context.deviceToken,
|
|
147
|
+
fetch: io.fetch,
|
|
148
|
+
method: "GET",
|
|
149
|
+
label: "jarvis:trace-thread",
|
|
150
|
+
timeoutMs: 30_000,
|
|
151
|
+
log: (line) => writeLine(io.stderr, line),
|
|
152
|
+
});
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
return null;
|
|
155
|
+
const body = result.body;
|
|
156
|
+
return body.traces?.[0]?.traceId ?? null;
|
|
157
|
+
}
|
|
158
|
+
function writeTraceTree(io, traceId, body) {
|
|
159
|
+
const styled = colorEnabled(io);
|
|
160
|
+
writeLine(io.stdout, `Turn ${traceId}`);
|
|
161
|
+
if (body.headline)
|
|
162
|
+
writeLine(io.stdout, dim(body.headline, styled));
|
|
163
|
+
writeLine(io.stdout, "");
|
|
164
|
+
for (const line of body.lines ?? [])
|
|
165
|
+
writeLine(io.stdout, line);
|
|
166
|
+
if ((body.lines ?? []).length === 0) {
|
|
167
|
+
// Never a blank screen: an empty tree is a fact and it gets a sentence.
|
|
168
|
+
writeLine(io.stdout, "That turn recorded no steps.");
|
|
169
|
+
}
|
|
170
|
+
if (body.truncated) {
|
|
171
|
+
writeLine(io.stdout, "");
|
|
172
|
+
writeLine(io.stdout, dim("Only the first 1000 spans of this turn are shown.", styled));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function refuse(context, io, reason, detail) {
|
|
176
|
+
if (context.json) {
|
|
177
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
writeLine(io.stderr, detail);
|
|
181
|
+
}
|
|
182
|
+
writeLine(io.stderr, `[jarvis cli] trace unavailable ${JSON.stringify({ reason })}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
package/dist/commands/jarvis.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { colorEnabled, dim, isInteractiveStdin, readLine, readPipedText, writeFragment, writeLine, } from "./cli-io.js";
|
|
9
9
|
import { attachedFileRefusalSentence, readAttachedImage, } from "./jarvis-attachment.js";
|
|
10
|
+
import { rememberTurnTrace, runJarvisTrace } from "./jarvis-trace.js";
|
|
10
11
|
import { loadPairedSession, towerFailureDetail, towerJsonRequest, towerRequest, } from "../tower-client.js";
|
|
11
|
-
import { readTowerTurn, streamFailureDetail, } from "../tower-stream.js";
|
|
12
|
+
import { readTowerTurn, streamFailureDetail, turnTimingFields, } from "../tower-stream.js";
|
|
12
13
|
/**
|
|
13
14
|
* A citation line's link, as the dashboard mints it (BLI-3570):
|
|
14
15
|
* `Source: BLI-1234 — Judge outage <https://linear.app/…>`. The words are the
|
|
@@ -74,7 +75,12 @@ function writeReplyContinuation(io, tail) {
|
|
|
74
75
|
*/
|
|
75
76
|
const TURN_DEADLINE_MS = 125_000;
|
|
76
77
|
export async function runJarvis(command, io) {
|
|
78
|
+
// Milliseconds since this process started (`performance.now()` counts from
|
|
79
|
+
// `timeOrigin`), captured before this command does anything of its own.
|
|
80
|
+
const bootMs = Math.round(performance.now());
|
|
81
|
+
const sessionStartedAt = Date.now();
|
|
77
82
|
const session = await loadPairedSession("jarvis", command.homeDir);
|
|
83
|
+
const sessionMs = Date.now() - sessionStartedAt;
|
|
78
84
|
const dashboardUrl = command.dashboardUrl ?? session.dashboard_url;
|
|
79
85
|
// BLI-3458: reading back what was already said. No turn, no model, no
|
|
80
86
|
// conversation-ledger write — and, on the server, only ever THIS device
|
|
@@ -82,9 +88,32 @@ export async function runJarvis(command, io) {
|
|
|
82
88
|
if (command.threads || command.history) {
|
|
83
89
|
return readHistory({ command, dashboardUrl, deviceToken: session.device_token }, io);
|
|
84
90
|
}
|
|
91
|
+
// BLI-3560: reading back what a turn DID. Same class of act, same absence of
|
|
92
|
+
// a model call — this one asks the ops trace door rather than the ledger.
|
|
93
|
+
if (command.trace) {
|
|
94
|
+
return runJarvisTrace({
|
|
95
|
+
dashboardUrl,
|
|
96
|
+
deviceToken: session.device_token,
|
|
97
|
+
requested: command.trace,
|
|
98
|
+
json: command.json,
|
|
99
|
+
homeDir: command.homeDir,
|
|
100
|
+
}, io);
|
|
101
|
+
}
|
|
102
|
+
const promptStartedAt = Date.now();
|
|
85
103
|
const oneShotPrompt = await resolveOneShotPrompt(command, io);
|
|
86
104
|
if (oneShotPrompt !== null) {
|
|
87
|
-
return sendOneTurn({
|
|
105
|
+
return sendOneTurn({
|
|
106
|
+
command,
|
|
107
|
+
dashboardUrl,
|
|
108
|
+
deviceToken: session.device_token,
|
|
109
|
+
boot: {
|
|
110
|
+
bootMs,
|
|
111
|
+
sessionMs,
|
|
112
|
+
// Only meaningful when the question was piped; an argument costs
|
|
113
|
+
// nothing to read and reporting a stdin wait for it would be a lie.
|
|
114
|
+
promptMs: command.prompt ? null : Date.now() - promptStartedAt,
|
|
115
|
+
},
|
|
116
|
+
}, oneShotPrompt, io);
|
|
88
117
|
}
|
|
89
118
|
if (command.json) {
|
|
90
119
|
throw new Error("cockpit jarvis --json needs --prompt, positional text, or piped stdin.");
|
|
@@ -96,7 +125,14 @@ export async function runJarvis(command, io) {
|
|
|
96
125
|
continue;
|
|
97
126
|
if (prompt === "/exit" || prompt === "/quit")
|
|
98
127
|
return 0;
|
|
99
|
-
const exitCode = await sendOneTurn({
|
|
128
|
+
const exitCode = await sendOneTurn({
|
|
129
|
+
command,
|
|
130
|
+
dashboardUrl,
|
|
131
|
+
deviceToken: session.device_token,
|
|
132
|
+
// The typing wait belongs to the person, not to the turn, so no prompt
|
|
133
|
+
// span is reported for an interactive turn.
|
|
134
|
+
boot: { bootMs, sessionMs, promptMs: null },
|
|
135
|
+
}, prompt, io);
|
|
100
136
|
if (exitCode !== 0)
|
|
101
137
|
return exitCode;
|
|
102
138
|
}
|
|
@@ -216,6 +252,13 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
216
252
|
// works against both server versions with no flag.
|
|
217
253
|
const wantsStream = context.command.stream !== false;
|
|
218
254
|
const log = (line) => writeLine(io.stderr, line);
|
|
255
|
+
// BLI-3591: everything this turn did before the question left the machine.
|
|
256
|
+
// There is no self-update probe, no floor check and no settings fetch on
|
|
257
|
+
// this path — the ONLY awaits between the command starting and the POST are
|
|
258
|
+
// the paired-session read, the piped-prompt read and an attached image, and
|
|
259
|
+
// all three are named. If this number is ever large, the step that made it
|
|
260
|
+
// large is on the same line.
|
|
261
|
+
const preRequestMs = Date.now() - startedAt;
|
|
219
262
|
const requested = await towerRequest({
|
|
220
263
|
dashboardUrl: context.dashboardUrl,
|
|
221
264
|
path: "/api/jarvis/cli",
|
|
@@ -283,6 +326,11 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
283
326
|
// because it already sent every step live; the activity we collected is that
|
|
284
327
|
// same trace, so `--json` still gets one.
|
|
285
328
|
const trace = body.trace ?? activityToTrace(turn.activity);
|
|
329
|
+
// BLI-3560: bookmark this turn so `cockpit jarvis --trace last` can open its
|
|
330
|
+
// step tree. Written before the reply is printed for no reason other than
|
|
331
|
+
// keeping the failure — which is only ever a stderr line — above the answer
|
|
332
|
+
// rather than after it.
|
|
333
|
+
await rememberTurnTrace({ traceId: body.traceId ?? null, threadId: body.traceThread ?? null }, io, context.command.homeDir);
|
|
286
334
|
if (context.command.json) {
|
|
287
335
|
writeLine(io.stdout, JSON.stringify({
|
|
288
336
|
ok: true,
|
|
@@ -291,6 +339,23 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
291
339
|
model: body.model ?? null,
|
|
292
340
|
trace,
|
|
293
341
|
subject: body.subject ?? null,
|
|
342
|
+
// BLI-3582: the server's own split of the wait. `null` from a
|
|
343
|
+
// dashboard that does not measure it yet.
|
|
344
|
+
latency: latencyFields(body.latency),
|
|
345
|
+
// BLI-3591: this side's own half of the same wait, so a consumer can
|
|
346
|
+
// put the whole journey together without a stopwatch of its own.
|
|
347
|
+
clientLatency: {
|
|
348
|
+
bootMs: context.boot.bootMs,
|
|
349
|
+
sessionMs: context.boot.sessionMs,
|
|
350
|
+
promptMs: context.boot.promptMs,
|
|
351
|
+
preRequestMs,
|
|
352
|
+
respondedMs: requested.respondedMs,
|
|
353
|
+
firstByteMs: turn.timing.firstByteMs,
|
|
354
|
+
firstFrameMs: turn.timing.firstFrameMs,
|
|
355
|
+
firstTokenMs: turn.timing.firstTokenMs,
|
|
356
|
+
elapsedMs: Date.now() - startedAt,
|
|
357
|
+
},
|
|
358
|
+
...(body.traceId ? { traceId: body.traceId } : {}),
|
|
294
359
|
}));
|
|
295
360
|
}
|
|
296
361
|
else {
|
|
@@ -324,9 +389,43 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
324
389
|
// dashboard means the answer landed all at once.
|
|
325
390
|
streamed_chars: turn.draft.length,
|
|
326
391
|
revised: body.revised === true,
|
|
392
|
+
// BLI-3582: `elapsed_ms` above is the whole wait as this side felt it;
|
|
393
|
+
// these three say which part of it was the dashboard's prep, which was
|
|
394
|
+
// the model's first token, and how long the socket stayed silent before
|
|
395
|
+
// the first byte. All null against a dashboard that does not send them.
|
|
396
|
+
...latencyLogFields(body.latency),
|
|
397
|
+
// BLI-3591: the terminal's own half, on the same line, so the gap
|
|
398
|
+
// between the dashboard's first token and the first character a person
|
|
399
|
+
// sees stops being a mystery with nobody's name on it. `boot_ms` and
|
|
400
|
+
// `session_ms` are process facts (identical on every turn of an
|
|
401
|
+
// interactive session); the rest are this turn's, measured from the
|
|
402
|
+
// moment the turn started.
|
|
403
|
+
...clientTimingFields(context.boot, {
|
|
404
|
+
preRequestMs,
|
|
405
|
+
respondedMs: requested.respondedMs,
|
|
406
|
+
timing: turn.timing,
|
|
407
|
+
}),
|
|
327
408
|
})}`);
|
|
328
409
|
return 0;
|
|
329
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* The terminal's own spans as log fields (BLI-3591).
|
|
413
|
+
*
|
|
414
|
+
* Deliberately snake_case beside the dashboard's three, and deliberately
|
|
415
|
+
* distinct names: `model_ttft_ms` is the dashboard's clock, `first_token_ms`
|
|
416
|
+
* is this one, and confusing them is how a latency line stops being
|
|
417
|
+
* answerable.
|
|
418
|
+
*/
|
|
419
|
+
function clientTimingFields(boot, turn) {
|
|
420
|
+
return {
|
|
421
|
+
boot_ms: boot.bootMs,
|
|
422
|
+
session_ms: boot.sessionMs,
|
|
423
|
+
prompt_ms: boot.promptMs,
|
|
424
|
+
pre_request_ms: turn.preRequestMs,
|
|
425
|
+
responded_ms: turn.respondedMs,
|
|
426
|
+
...turnTimingFields(turn.timing),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
330
429
|
/**
|
|
331
430
|
* The answer as it is written, in a terminal (BLI-3517).
|
|
332
431
|
*
|
|
@@ -524,7 +623,7 @@ function writeModelReceipt(io, model) {
|
|
|
524
623
|
if (!model)
|
|
525
624
|
return;
|
|
526
625
|
const requested = typeof model.requestedModel === "string" ? model.requestedModel : null;
|
|
527
|
-
const answered =
|
|
626
|
+
const answered = namedModel(model.model);
|
|
528
627
|
const mismatched = requested !== null && answered !== null && requested !== answered;
|
|
529
628
|
if (!model.fallback && !mismatched)
|
|
530
629
|
return;
|
|
@@ -533,6 +632,47 @@ function writeModelReceipt(io, model) {
|
|
|
533
632
|
// schema. Say what the receipt actually reports.
|
|
534
633
|
writeLine(io.stdout, `Model: ${answered ?? "an unknown model"} answered instead of ${requested ?? "the requested model"}`);
|
|
535
634
|
}
|
|
635
|
+
/** One millisecond span off the wire, or null for anything that is not a number. */
|
|
636
|
+
function latencyMs(value) {
|
|
637
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
638
|
+
}
|
|
639
|
+
/** The latency split for `--json`, always the same three keys so a consumer can rely on them. */
|
|
640
|
+
function latencyFields(latency) {
|
|
641
|
+
return {
|
|
642
|
+
wireMs: latencyMs(latency?.wireMs),
|
|
643
|
+
prepMs: latencyMs(latency?.prepMs),
|
|
644
|
+
modelTtftMs: latencyMs(latency?.modelTtftMs),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/** The same three spans as log fields, snake_case like everything else on that line. */
|
|
648
|
+
function latencyLogFields(latency) {
|
|
649
|
+
const split = latencyFields(latency);
|
|
650
|
+
return {
|
|
651
|
+
wire_ms: split.wireMs,
|
|
652
|
+
prep_ms: split.prepMs,
|
|
653
|
+
model_ttft_ms: split.modelTtftMs,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* The receipt's `model` field, unless it is the server's own "I could not tell"
|
|
658
|
+
* sentinel (BLI-3582).
|
|
659
|
+
*
|
|
660
|
+
* `modelReceipt` on the dashboard writes the literal string `"unknown"` when
|
|
661
|
+
* the run reported no model, and this side used to read that as a model NAME —
|
|
662
|
+
* so a turn whose model was simply unnamed printed
|
|
663
|
+
* `Model: unknown answered instead of gpt-5.6-terra`, a fallback notice for a
|
|
664
|
+
* fallback that never happened. Every streamed turn hit it. The dashboard fix
|
|
665
|
+
* makes the streamed path name the model again; this one makes the sentinel
|
|
666
|
+
* unable to invent a fallback on any dashboard version, old or new.
|
|
667
|
+
*/
|
|
668
|
+
function namedModel(value) {
|
|
669
|
+
if (typeof value !== "string")
|
|
670
|
+
return null;
|
|
671
|
+
const trimmed = value.trim();
|
|
672
|
+
if (!trimmed || trimmed.toLowerCase() === "unknown")
|
|
673
|
+
return null;
|
|
674
|
+
return trimmed;
|
|
675
|
+
}
|
|
536
676
|
function writeFailure(command, io, reason, detail) {
|
|
537
677
|
if (command.json) {
|
|
538
678
|
writeLine(io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
|
|
@@ -570,6 +570,32 @@ export function parseAgentRulesArgs(args) {
|
|
|
570
570
|
json: values.booleans.has("--json"),
|
|
571
571
|
};
|
|
572
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* BLI-3580. `install` is the default action on purpose: this command exists to
|
|
575
|
+
* be run unasked (by `do-everything` and by the sync tick), and a bare
|
|
576
|
+
* `cockpit memory` should do the thing rather than print usage.
|
|
577
|
+
*/
|
|
578
|
+
export function parseMemoryArgs(args) {
|
|
579
|
+
const values = parseNamedArgs(args, {
|
|
580
|
+
allowedFlags: ["--home", "--dashboard-url", "--dry-run", "--json"],
|
|
581
|
+
valueFlags: ["--home", "--dashboard-url"],
|
|
582
|
+
});
|
|
583
|
+
if (values.positionals.length > 1) {
|
|
584
|
+
throw new Error("memory accepts at most one action (install|status).");
|
|
585
|
+
}
|
|
586
|
+
const action = values.positionals[0] ?? "install";
|
|
587
|
+
if (action !== "install" && action !== "status") {
|
|
588
|
+
throw new Error("memory action must be install or status.");
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
kind: "memory",
|
|
592
|
+
action,
|
|
593
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
594
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
595
|
+
dryRun: values.booleans.has("--dry-run"),
|
|
596
|
+
json: values.booleans.has("--json"),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
573
599
|
function parseAgentRulesHost(value) {
|
|
574
600
|
const host = value?.trim().toLowerCase() || "all";
|
|
575
601
|
if (host === "codex" || host === "claude" || host === "all")
|
|
@@ -32,6 +32,8 @@ export function parseJarvisArgs(args) {
|
|
|
32
32
|
// something new. Neither takes a turn or reaches the model.
|
|
33
33
|
"--threads",
|
|
34
34
|
"--history",
|
|
35
|
+
// BLI-3560: reading back what a turn DID — the step tree of one turn.
|
|
36
|
+
"--trace",
|
|
35
37
|
"--limit",
|
|
36
38
|
"--json",
|
|
37
39
|
],
|
|
@@ -42,6 +44,7 @@ export function parseJarvisArgs(args) {
|
|
|
42
44
|
"--as",
|
|
43
45
|
"--thread",
|
|
44
46
|
"--model",
|
|
47
|
+
"--trace",
|
|
45
48
|
"--image",
|
|
46
49
|
"--file",
|
|
47
50
|
"--date",
|
|
@@ -87,6 +90,23 @@ export function parseJarvisArgs(args) {
|
|
|
87
90
|
if ((threads || history) && date) {
|
|
88
91
|
throw new Error("jarvis --threads and --history replay what was said; they bind no page.");
|
|
89
92
|
}
|
|
93
|
+
// BLI-3560. `--trace` reads back what a turn DID, the same class of act as
|
|
94
|
+
// `--threads` and `--history`, and refused alongside them for the same
|
|
95
|
+
// reason: a command that quietly asked a question while you asked to see one
|
|
96
|
+
// is worse than a refusal.
|
|
97
|
+
const trace = optionalNonEmpty(values.flags.get("--trace"));
|
|
98
|
+
if (trace && (threads || history)) {
|
|
99
|
+
throw new Error("jarvis --trace shows one turn's steps; --threads and --history replay what was said. Pass one.");
|
|
100
|
+
}
|
|
101
|
+
if (trace && (flaggedPrompt || positionalPrompt)) {
|
|
102
|
+
throw new Error("jarvis --trace shows a turn that already ran; it does not take a question.");
|
|
103
|
+
}
|
|
104
|
+
if (trace && image) {
|
|
105
|
+
throw new Error("jarvis --trace does not take an attachment.");
|
|
106
|
+
}
|
|
107
|
+
if (trace !== undefined && !/^(last|[A-Za-z0-9_-]{1,64})$/.test(trace)) {
|
|
108
|
+
throw new Error("jarvis --trace takes `last` or a trace id (up to 64 letters, numbers, underscores or hyphens).");
|
|
109
|
+
}
|
|
90
110
|
return {
|
|
91
111
|
kind: "jarvis",
|
|
92
112
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
@@ -96,6 +116,7 @@ export function parseJarvisArgs(args) {
|
|
|
96
116
|
thread,
|
|
97
117
|
threads,
|
|
98
118
|
history,
|
|
119
|
+
...(trace ? { trace } : {}),
|
|
99
120
|
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
100
121
|
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
101
122
|
// to the inference server's own allowlist and relays its refusal.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
1
|
+
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
2
2
|
import { parseBriefArgs, parseCorrectArgs, parseJarvisArgs, parseModelArgs, parseNotesArgs, parseOpsArgs, parseScoutArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, } from "./local-args-tower.js";
|
|
3
3
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
4
4
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
@@ -80,6 +80,8 @@ export function parseLocalArgs(argv) {
|
|
|
80
80
|
return parseAutostartArgs(argv.slice(1));
|
|
81
81
|
case "agent-rules":
|
|
82
82
|
return parseAgentRulesArgs(argv.slice(1));
|
|
83
|
+
case "memory":
|
|
84
|
+
return parseMemoryArgs(argv.slice(1));
|
|
83
85
|
case "release":
|
|
84
86
|
return parseReleaseArgs(argv.slice(1));
|
|
85
87
|
default:
|
|
@@ -38,6 +38,7 @@ export const rootCommandNames = new Set([
|
|
|
38
38
|
"serve",
|
|
39
39
|
"autostart",
|
|
40
40
|
"agent-rules",
|
|
41
|
+
"memory",
|
|
41
42
|
"release",
|
|
42
43
|
]);
|
|
43
44
|
export function localCommandHelp(command) {
|
|
@@ -56,7 +57,7 @@ export function localCommandHelp(command) {
|
|
|
56
57
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
57
58
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
58
59
|
" cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
59
|
-
" cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
|
|
60
|
+
" cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--trace <id|last>] [--dashboard-url <url>] [--json]",
|
|
60
61
|
" cockpit model [show|set <provider:model>] [--json]",
|
|
61
62
|
" cockpit scout [start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
|
|
62
63
|
" cockpit ops [status [--job <id>] [--skips] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
|
|
@@ -74,6 +75,7 @@ export function localCommandHelp(command) {
|
|
|
74
75
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
75
76
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
76
77
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
78
|
+
" cockpit memory [install|status] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
77
79
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
78
80
|
"",
|
|
79
81
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
@@ -279,7 +281,7 @@ function localSubcommandHelp(command) {
|
|
|
279
281
|
[
|
|
280
282
|
"jarvis",
|
|
281
283
|
[
|
|
282
|
-
"Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--dashboard-url <url>] [--json]",
|
|
284
|
+
"Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--trace <id|last>] [--dashboard-url <url>] [--json]",
|
|
283
285
|
"",
|
|
284
286
|
"Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
|
|
285
287
|
"--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
|
|
@@ -294,6 +296,7 @@ function localSubcommandHelp(command) {
|
|
|
294
296
|
"A turn that outlives the dashboard's own ceiling is reported as turn_timed_out — an incomplete answer is never printed as a finished one.",
|
|
295
297
|
"The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
|
|
296
298
|
"--threads lists the terminal conversations on this account; --thread <name> --history replays one (the newest --limit <n> messages). Neither takes a question, and neither can reach anybody else's conversations.",
|
|
299
|
+
"--trace <id|last> prints the step tree of one turn — every model step, tool call and server-side memory step, with how long each took, which model ran it, the tokens it spent and what failed. `last` opens the turn this machine asked most recently. Like --threads it takes no question and reaches no model.",
|
|
297
300
|
"Run `cockpit login` first if this machine is not paired.",
|
|
298
301
|
],
|
|
299
302
|
],
|
|
@@ -529,6 +532,20 @@ function localSubcommandHelp(command) {
|
|
|
529
532
|
"Action defaults to `install`.",
|
|
530
533
|
],
|
|
531
534
|
],
|
|
535
|
+
[
|
|
536
|
+
"memory",
|
|
537
|
+
[
|
|
538
|
+
"Usage: cockpit memory [install|status] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
539
|
+
"",
|
|
540
|
+
"Registers BLI Memory on this machine: the `bli-memory` MCP server plus the",
|
|
541
|
+
"recall/save hooks, for Claude Code (~/.claude.json, ~/.claude/settings.json)",
|
|
542
|
+
"and for Codex (~/.codex/config.toml, ~/.codex/skills/bli-memory/).",
|
|
543
|
+
"Idempotent: it merges with what is already there, never duplicates its own",
|
|
544
|
+
"entries, and reads the stored config back before reporting success.",
|
|
545
|
+
"`do-everything` runs it, and the sync tick re-runs it at most once a day.",
|
|
546
|
+
"Action defaults to `install`. See docs/runbooks/bli-memory-install.md.",
|
|
547
|
+
],
|
|
548
|
+
],
|
|
532
549
|
[
|
|
533
550
|
"release",
|
|
534
551
|
[
|
package/dist/commands/local.js
CHANGED
|
@@ -30,6 +30,7 @@ import { runNotes } from "./notes.js";
|
|
|
30
30
|
import { runServe } from "./serve.js";
|
|
31
31
|
import { runAutostart } from "./autostart-command.js";
|
|
32
32
|
import { runAgentRules } from "./agent-rules-command.js";
|
|
33
|
+
import { runMemoryInstall } from "./memory-install.js";
|
|
33
34
|
import { parseLocalArgs } from "./local-args.js";
|
|
34
35
|
// `./local.js` is the published entry point for this command surface: the
|
|
35
36
|
// public CLI's generated root, commands/root.ts, doctor.ts and the test suite
|
|
@@ -125,6 +126,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
125
126
|
return await runAutostart(command, io);
|
|
126
127
|
case "agent-rules":
|
|
127
128
|
return await runAgentRules(command, io);
|
|
129
|
+
case "memory":
|
|
130
|
+
return await runMemoryInstall(command, io);
|
|
128
131
|
case "release":
|
|
129
132
|
return await runRelease(command, io);
|
|
130
133
|
}
|