@bli-cockpit/cli 0.2.33 → 0.2.35
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/agent-rules.js +4 -4
- package/dist/autostart.js +4 -4
- package/dist/commands/backfill.js +493 -408
- package/dist/commands/collection-roots.js +3 -3
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/install-update.js +6 -6
- package/dist/commands/jarvis.js +40 -1
- package/dist/commands/local-args.js +5 -2
- package/dist/commands/local-auth.js +3 -3
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help.js +11 -9
- package/dist/commands/local.js +411 -337
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync.js +323 -210
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/onboarding-roots.js +1 -1
- package/package.json +4 -3
|
@@ -131,20 +131,20 @@ function collectionRootNotPersistedMessage(homeDir) {
|
|
|
131
131
|
function collectionRootMissingOnDiskMessage(saved, homeDir) {
|
|
132
132
|
const home = path.resolve(homeDir ?? os.homedir());
|
|
133
133
|
return [
|
|
134
|
-
"
|
|
134
|
+
"Tower is set up to collect from a folder that is not on this machine:",
|
|
135
135
|
...saved.map((root) => ` ${root}`),
|
|
136
136
|
"",
|
|
137
137
|
"That usually means the folder was renamed, moved, or deleted since setup.",
|
|
138
138
|
"",
|
|
139
139
|
"Fix it by running ONE of these:",
|
|
140
140
|
"",
|
|
141
|
-
" # Point
|
|
141
|
+
" # Point Tower at where your projects actually live now",
|
|
142
142
|
` cockpit do-everything --workspace ${path.join(home, "BLI")}`,
|
|
143
143
|
"",
|
|
144
144
|
" # Or sync everything on this machine and stop worrying about the path",
|
|
145
145
|
" cockpit do-everything --allow-home-root",
|
|
146
146
|
"",
|
|
147
|
-
"Not sure where your projects are? This lists the folders
|
|
147
|
+
"Not sure where your projects are? This lists the folders Tower can see:",
|
|
148
148
|
" cockpit status",
|
|
149
149
|
].join("\n");
|
|
150
150
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -490,7 +490,7 @@ function writeDoctorOutput(command, io, rows) {
|
|
|
490
490
|
}, null, 2));
|
|
491
491
|
return;
|
|
492
492
|
}
|
|
493
|
-
writeLine(io.stdout, command.dryRun ? "
|
|
493
|
+
writeLine(io.stdout, command.dryRun ? "Tower doctor dry-run" : "Tower doctor");
|
|
494
494
|
// The machine `code` stays in `--json`; a human reading the table wants the
|
|
495
495
|
// sentence, not the label (BLI-3194).
|
|
496
496
|
writeLine(io.stdout, "state step result");
|
|
@@ -664,7 +664,7 @@ function selfUpdateFailureMessage(error) {
|
|
|
664
664
|
" 3) Do not use `sudo npm i -g`; it makes the ownership problem come back.",
|
|
665
665
|
].join("\n");
|
|
666
666
|
}
|
|
667
|
-
return "npm install failed;
|
|
667
|
+
return "npm install failed; Tower CLI was not refreshed.";
|
|
668
668
|
}
|
|
669
669
|
function npmPrefixFromError(stderr) {
|
|
670
670
|
if (stderr.includes("/usr/local/"))
|
|
@@ -19,7 +19,7 @@ export class SelfUpdateError extends Error {
|
|
|
19
19
|
result;
|
|
20
20
|
eacces;
|
|
21
21
|
constructor(result) {
|
|
22
|
-
super("npm install failed;
|
|
22
|
+
super("npm install failed; Tower CLI was not refreshed.");
|
|
23
23
|
this.name = "SelfUpdateError";
|
|
24
24
|
this.result = result;
|
|
25
25
|
this.eacces = isNpmEaccesFailure(result.stderr);
|
|
@@ -51,7 +51,7 @@ export async function runInstall(command, io) {
|
|
|
51
51
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
52
52
|
return finish(0);
|
|
53
53
|
}
|
|
54
|
-
writeLine(io.stdout, "
|
|
54
|
+
writeLine(io.stdout, "Tower local collector installed.");
|
|
55
55
|
writeLine(io.stdout, `Config: ${result.paths.config_file}`);
|
|
56
56
|
writeLine(io.stdout, `Session: ${result.paths.session_file}`);
|
|
57
57
|
writeLine(io.stdout, "Auth: missing; upload stays local-only until pairing/login.");
|
|
@@ -111,7 +111,7 @@ export async function runUpdate(command, io) {
|
|
|
111
111
|
}, null, 2));
|
|
112
112
|
}
|
|
113
113
|
else {
|
|
114
|
-
writeLine(io.stderr, "BLOCKED: npm install failed;
|
|
114
|
+
writeLine(io.stderr, "BLOCKED: npm install failed; Tower CLI was not refreshed.");
|
|
115
115
|
if (error.eacces) {
|
|
116
116
|
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
117
117
|
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
@@ -121,7 +121,7 @@ export async function runUpdate(command, io) {
|
|
|
121
121
|
}
|
|
122
122
|
addInstallEvent(installEvents, "npm_install", "ok");
|
|
123
123
|
if (!command.json) {
|
|
124
|
-
writeLine(io.stdout, "
|
|
124
|
+
writeLine(io.stdout, "Tower CLI updated. Rechecking onboarding...");
|
|
125
125
|
}
|
|
126
126
|
const onboard = await exec("cockpit", [
|
|
127
127
|
"onboard",
|
|
@@ -135,7 +135,7 @@ export async function runSelfUpdate(io, options = {}) {
|
|
|
135
135
|
const exec = io.exec ?? defaultExec();
|
|
136
136
|
const tag = options.tag ?? "latest";
|
|
137
137
|
if (!options.json) {
|
|
138
|
-
writeLine(io.stdout, `Updating
|
|
138
|
+
writeLine(io.stdout, `Updating Tower CLI from npm (${tag})...`);
|
|
139
139
|
}
|
|
140
140
|
const install = await exec("npm", selfUpdateInstallArgs(tag));
|
|
141
141
|
writeExecOutput(io, install, { stdout: !options.json, stderr: true });
|
|
@@ -229,7 +229,7 @@ export async function runRelease(command, io) {
|
|
|
229
229
|
const gitReady = await prepareReleaseMainBranch(releaseRoot, exec, io);
|
|
230
230
|
if (!gitReady)
|
|
231
231
|
return 1;
|
|
232
|
-
writeLine(io.stdout, "Running
|
|
232
|
+
writeLine(io.stdout, "Running Tower public package release...");
|
|
233
233
|
const npmArgs = ["--prefix", releaseRoot, "run", "publish:public"];
|
|
234
234
|
if (command.args.length > 0)
|
|
235
235
|
npmArgs.push("--", ...command.args);
|
package/dist/commands/jarvis.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { errorMessage, isInteractiveStdin, readLine, writeLine } from "./cli-io.js";
|
|
9
9
|
import { getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "../local-state.js";
|
|
10
|
+
const TRACE_DIM = "\x1b[2m";
|
|
11
|
+
const TRACE_RESET = "\x1b[0m";
|
|
10
12
|
export async function runJarvis(command, io) {
|
|
11
13
|
const session = await loadPairedSession(command.homeDir);
|
|
12
14
|
const dashboardUrl = command.dashboardUrl ?? session.dashboard_url;
|
|
@@ -39,7 +41,7 @@ async function loadPairedSession(homeDir) {
|
|
|
39
41
|
return session;
|
|
40
42
|
}
|
|
41
43
|
catch (error) {
|
|
42
|
-
throw new Error(`JARVIS needs a valid paired
|
|
44
|
+
throw new Error(`JARVIS needs a valid paired Tower session. Run \`cockpit login\`, then try again (${errorMessage(error)}).`);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
async function resolveOneShotPrompt(command, io) {
|
|
@@ -64,6 +66,7 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
64
66
|
question: prompt,
|
|
65
67
|
thread: context.command.thread,
|
|
66
68
|
subject: context.command.subject,
|
|
69
|
+
model: context.command.model,
|
|
67
70
|
}),
|
|
68
71
|
});
|
|
69
72
|
}
|
|
@@ -83,22 +86,58 @@ async function sendOneTurn(context, prompt, io) {
|
|
|
83
86
|
reply: body.reply,
|
|
84
87
|
thread: body.thread ?? context.command.thread,
|
|
85
88
|
model: body.model ?? null,
|
|
89
|
+
trace: body.trace ?? [],
|
|
86
90
|
subject: body.subject ?? null,
|
|
87
91
|
}));
|
|
88
92
|
}
|
|
89
93
|
else {
|
|
90
94
|
const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
|
|
91
95
|
writeLine(io.stdout, `jarvis${subject}> ${body.reply}`);
|
|
96
|
+
writeTraceBlock(io, body.trace);
|
|
97
|
+
writeModelReceipt(io, body.model);
|
|
92
98
|
}
|
|
93
99
|
writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
|
|
94
100
|
prompt_length: prompt.length,
|
|
95
101
|
reply_length: body.reply.length,
|
|
102
|
+
trace_steps: body.trace?.length ?? 0,
|
|
103
|
+
trace_failed: body.trace?.filter((step) => step.status === "failed").length ?? 0,
|
|
104
|
+
model_requested: context.command.model ?? null,
|
|
96
105
|
elapsed_ms: Date.now() - startedAt,
|
|
97
106
|
thread: context.command.thread === "main" ? "default" : "named",
|
|
98
107
|
subject: context.command.subject ? "selected" : "caller",
|
|
99
108
|
})}`);
|
|
100
109
|
return 0;
|
|
101
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* One dim line per tool the turn called (BLI-3381): what it was, how long it
|
|
113
|
+
* took, and — for a failure — the reason, matching what the web thinking
|
|
114
|
+
* trace shows (`describeToolTraceEvent` on the server side built these
|
|
115
|
+
* labels; this only renders them). Silent when no tool ran.
|
|
116
|
+
*/
|
|
117
|
+
function writeTraceBlock(io, trace) {
|
|
118
|
+
if (!trace || trace.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
for (const step of trace) {
|
|
121
|
+
const elapsed = typeof step.elapsedMs === "number" ? ` (${step.elapsedMs}ms)` : "";
|
|
122
|
+
const failure = step.status === "failed" ? ` — failed: ${step.detail ?? "no reason given"}` : "";
|
|
123
|
+
writeLine(io.stdout, `${TRACE_DIM} ⏺ ${step.label}${elapsed}${failure}${TRACE_RESET}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* One line, only when the answer did not come from what was requested — a
|
|
128
|
+
* fallback, or an explicit `--model` that landed on a different model
|
|
129
|
+
* (BLI-3381). Silent otherwise, matching the web panel's own fallback notice.
|
|
130
|
+
*/
|
|
131
|
+
function writeModelReceipt(io, model) {
|
|
132
|
+
if (!model)
|
|
133
|
+
return;
|
|
134
|
+
const requested = typeof model.requestedModel === "string" ? model.requestedModel : null;
|
|
135
|
+
const answered = typeof model.model === "string" ? model.model : null;
|
|
136
|
+
const mismatched = requested !== null && answered !== null && requested !== answered;
|
|
137
|
+
if (!model.fallback && !mismatched)
|
|
138
|
+
return;
|
|
139
|
+
writeLine(io.stdout, `Model: ${answered ?? "an unknown model"} answered (${requested ?? "the requested model"} unavailable)`);
|
|
140
|
+
}
|
|
102
141
|
async function readReply(response) {
|
|
103
142
|
const text = await response.text();
|
|
104
143
|
if (!text)
|
|
@@ -611,8 +611,8 @@ function parseAgentRulesHost(value) {
|
|
|
611
611
|
}
|
|
612
612
|
function parseJarvisArgs(args) {
|
|
613
613
|
const values = parseNamedArgs(args, {
|
|
614
|
-
allowedFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread", "--json"],
|
|
615
|
-
valueFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread"],
|
|
614
|
+
allowedFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread", "--model", "--json"],
|
|
615
|
+
valueFlags: ["--home", "--dashboard-url", "--prompt", "--as", "--thread", "--model"],
|
|
616
616
|
});
|
|
617
617
|
const flaggedPrompt = optionalNonEmpty(values.flags.get("--prompt"));
|
|
618
618
|
const positionalPrompt = optionalNonEmpty(values.positionals.join(" "));
|
|
@@ -630,6 +630,9 @@ function parseJarvisArgs(args) {
|
|
|
630
630
|
prompt: flaggedPrompt ?? positionalPrompt,
|
|
631
631
|
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
632
632
|
thread,
|
|
633
|
+
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
634
|
+
// to the inference server's own allowlist and relays its refusal.
|
|
635
|
+
model: optionalNonEmpty(values.flags.get("--model")),
|
|
633
636
|
json: values.booleans.has("--json"),
|
|
634
637
|
};
|
|
635
638
|
}
|
|
@@ -13,7 +13,7 @@ import { normalizeUrl } from "./local-args.js";
|
|
|
13
13
|
import { getCollectorRuntimePaths, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, } from "../local-state.js";
|
|
14
14
|
const OTP_CODE_PROMPT = [
|
|
15
15
|
"Email code needed:",
|
|
16
|
-
" Check your latest
|
|
16
|
+
" Check your latest Tower email for a 6- to 10-digit code.",
|
|
17
17
|
"What you can do:",
|
|
18
18
|
" 1) Paste the code here.",
|
|
19
19
|
" 2) No code yet: wait for the resend window, then rerun this command.",
|
|
@@ -22,7 +22,7 @@ const OTP_CODE_PROMPT = [
|
|
|
22
22
|
].join("\n");
|
|
23
23
|
const OTP_INVALID_MESSAGE = [
|
|
24
24
|
"Email code was not accepted:",
|
|
25
|
-
"
|
|
25
|
+
" Tower expects digits only, length 6 to 10.",
|
|
26
26
|
"What you can do:",
|
|
27
27
|
" 1) Rerun and paste the latest email code.",
|
|
28
28
|
" 2) Use manual approval: rerun this command with --no-auth --email <you@buildlaunchiterate.ca>",
|
|
@@ -59,7 +59,7 @@ export async function resolveOnboardEmail(command, roots, config, io) {
|
|
|
59
59
|
}
|
|
60
60
|
if (!interactive)
|
|
61
61
|
return inferred.email;
|
|
62
|
-
const answer = (await readLine(io, `Use ${inferred.email} for
|
|
62
|
+
const answer = (await readLine(io, `Use ${inferred.email} for Tower pairing? [Y/n] `)).trim().toLowerCase();
|
|
63
63
|
const typedEmail = answer.split(/\s+/u).find((part) => part.includes("@"));
|
|
64
64
|
if (typedEmail)
|
|
65
65
|
return typedEmail;
|
|
@@ -63,7 +63,7 @@ function unreadableDirectoriesMessage(unreadable) {
|
|
|
63
63
|
return [
|
|
64
64
|
`WARNING: ${unreadable.length} folder(s) could not be opened, so anything inside them was not scanned:`,
|
|
65
65
|
...unreadable.map((dir) => ` ${dir.path} (${dir.code})`),
|
|
66
|
-
"Collection continued for everything else. If a repo is missing from
|
|
66
|
+
"Collection continued for everything else. If a repo is missing from Tower,",
|
|
67
67
|
"check the permissions on the folders above.",
|
|
68
68
|
].join("\n");
|
|
69
69
|
}
|
|
@@ -84,7 +84,7 @@ function incompleteDiscoveryMessage(input) {
|
|
|
84
84
|
`--max-repos ${nextRepos}`,
|
|
85
85
|
].join(" ");
|
|
86
86
|
return [
|
|
87
|
-
`
|
|
87
|
+
`Tower could not finish scanning for repos, so it stopped instead of collecting a partial picture (${result.incomplete_reasons.join(", ")}).`,
|
|
88
88
|
"It stops rather than continuing because a partial scan would mark these repos as already checked and skip them from now on.",
|
|
89
89
|
"",
|
|
90
90
|
"Could not fully scan:",
|
|
@@ -46,7 +46,7 @@ export function localCommandHelp(command) {
|
|
|
46
46
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
47
47
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
48
48
|
" cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
49
|
-
" cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--dashboard-url <url>] [--json]",
|
|
49
|
+
" cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--model <key>] [--dashboard-url <url>] [--json]",
|
|
50
50
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
51
51
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
52
52
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -72,7 +72,7 @@ function localSubcommandHelp(command) {
|
|
|
72
72
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
73
73
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
74
74
|
"Run with no flags in a terminal and it prompts for the dashboard email and OTP code; pass --email to skip the email prompt. Use --no-auth to force the manual approval fallback.",
|
|
75
|
-
"Interactive runs also offer to add
|
|
75
|
+
"Interactive runs also offer to add Tower ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
|
|
76
76
|
],
|
|
77
77
|
],
|
|
78
78
|
[
|
|
@@ -160,7 +160,7 @@ function localSubcommandHelp(command) {
|
|
|
160
160
|
[
|
|
161
161
|
"Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
|
|
162
162
|
"",
|
|
163
|
-
"Uploads your latest collected work. If it cannot reach
|
|
163
|
+
"Uploads your latest collected work. If it cannot reach Tower it saves a retry and tries again later.",
|
|
164
164
|
"Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
|
|
165
165
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
166
166
|
"Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
|
|
@@ -181,7 +181,7 @@ function localSubcommandHelp(command) {
|
|
|
181
181
|
[
|
|
182
182
|
"Usage: cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--json]",
|
|
183
183
|
"",
|
|
184
|
-
"Uploads your latest collected work, then asks
|
|
184
|
+
"Uploads your latest collected work, then asks Tower to analyse it.",
|
|
185
185
|
"The command returns after the batch is queued; view status and results in My Work.",
|
|
186
186
|
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
187
187
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
@@ -190,13 +190,15 @@ function localSubcommandHelp(command) {
|
|
|
190
190
|
[
|
|
191
191
|
"jarvis",
|
|
192
192
|
[
|
|
193
|
-
"Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--dashboard-url <url>] [--json]",
|
|
193
|
+
"Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--thread <name>] [--model <key>] [--dashboard-url <url>] [--json]",
|
|
194
194
|
"",
|
|
195
|
-
"Chats with the same JARVIS used by
|
|
195
|
+
"Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
|
|
196
196
|
"--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
|
|
197
|
+
"--model <key> requests one provider:model pair (e.g. openai:gpt-5.6-terra); an unrecognised key is refused by the dashboard, not this command.",
|
|
197
198
|
"Run with no question for an interactive terminal conversation.",
|
|
198
199
|
"Agents can pass --prompt, positional text, or pipe one question on stdin.",
|
|
199
|
-
"
|
|
200
|
+
"Each answer prints a per-tool trace line and, only on a fallback or model mismatch, one model receipt line.",
|
|
201
|
+
"--json writes one machine-readable response (including trace and model) to stdout; operational metadata stays on stderr.",
|
|
200
202
|
"The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
|
|
201
203
|
"Run `cockpit login` first if this machine is not paired.",
|
|
202
204
|
],
|
|
@@ -267,10 +269,10 @@ function localSubcommandHelp(command) {
|
|
|
267
269
|
[
|
|
268
270
|
"Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
269
271
|
"",
|
|
270
|
-
"Installs a managed
|
|
272
|
+
"Installs a managed Tower Ticket Binding block into ~/.codex/AGENTS.md",
|
|
271
273
|
"and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
|
|
272
274
|
"The block is scoped to --workspace, or the current directory when omitted,",
|
|
273
|
-
"so Codex and Claude only run
|
|
275
|
+
"so Codex and Claude only run Tower ticket binding inside that onboarded folder.",
|
|
274
276
|
"Action defaults to `install`.",
|
|
275
277
|
],
|
|
276
278
|
],
|