@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.
@@ -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
- "Cockpit is set up to collect from a folder that is not on this machine:",
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 Cockpit at where your projects actually live now",
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 Cockpit can see:",
147
+ "Not sure where your projects are? This lists the folders Tower can see:",
148
148
  " cockpit status",
149
149
  ].join("\n");
150
150
  }
@@ -490,7 +490,7 @@ function writeDoctorOutput(command, io, rows) {
490
490
  }, null, 2));
491
491
  return;
492
492
  }
493
- writeLine(io.stdout, command.dryRun ? "Cockpit doctor dry-run" : "Cockpit doctor");
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; Cockpit CLI was not refreshed.";
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; Cockpit CLI was not refreshed.");
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, "Cockpit local collector installed.");
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; Cockpit CLI was not refreshed.");
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, "Cockpit CLI updated. Rechecking onboarding...");
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 Cockpit CLI from npm (${tag})...`);
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 Cockpit public package release...");
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);
@@ -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 Cockpit session. Run \`cockpit login\`, then try again (${errorMessage(error)}).`);
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 Cockpit email for a 6- to 10-digit code.",
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
- " Cockpit expects digits only, length 6 to 10.",
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 Cockpit pairing? [Y/n] `)).trim().toLowerCase();
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 Cockpit,",
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
- `Cockpit could not finish scanning for repos, so it stopped instead of collecting a partial picture (${result.incomplete_reasons.join(", ")}).`,
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 Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
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 Cockpit it saves a retry and tries again later.",
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 Cockpit to analyse it.",
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 Cockpit web chat and the BLI Slack DM.",
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
- "--json writes one machine-readable response to stdout; operational metadata stays on stderr.",
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 Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
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 Cockpit ticket binding inside that onboarded folder.",
275
+ "so Codex and Claude only run Tower ticket binding inside that onboarded folder.",
274
276
  "Action defaults to `install`.",
275
277
  ],
276
278
  ],