@bli-cockpit/cli 0.2.32 → 0.2.34

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.
@@ -138,13 +138,13 @@ async function inspectAgentRulesForHost(host, options = {}) {
138
138
  export function cockpitAgentRulesBlock(options = {}) {
139
139
  const scopePaths = normalizeScopePaths(options);
140
140
  const scopeLine = scopePaths.length === 1
141
- ? `- Only applies when the current working directory is inside the Cockpit-onboarded workspace/repo: \`${scopePaths[0]}\`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
141
+ ? `- Only applies when the current working directory is inside the Tower-onboarded workspace/repo: \`${scopePaths[0]}\`. Outside that folder, do not run Tower ticket binding or sync commands for private chats or unrelated repos.`
142
142
  : scopePaths.length > 1
143
- ? `- Only applies when the current working directory is inside one of these Cockpit-onboarded workspace roots: ${scopePaths.map((scopePath) => `\`${scopePath}\``).join(", ")}. Outside those folders, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
144
- : "- Only applies when the current working directory is inside the workspace/repo that ran `cockpit onboard` or `cockpit agent-rules install`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.";
143
+ ? `- Only applies when the current working directory is inside one of these Tower-onboarded workspace roots: ${scopePaths.map((scopePath) => `\`${scopePath}\``).join(", ")}. Outside those folders, do not run Tower ticket binding or sync commands for private chats or unrelated repos.`
144
+ : "- Only applies when the current working directory is inside the workspace/repo that ran `cockpit onboard` or `cockpit agent-rules install`. Outside that folder, do not run Tower ticket binding or sync commands for private chats or unrelated repos.";
145
145
  return [
146
146
  MANAGED_BLOCK_START,
147
- "## Cockpit Ticket Binding",
147
+ "## Tower Ticket Binding",
148
148
  "",
149
149
  scopeLine,
150
150
  "- Ticketed work (implement / debug / review / PR / ship): run `cockpit start --ticket <ticket-id> --workspace \"$PWD\"` before the first code edit (the flag is `--ticket`). No ticket ID visible → search Linear first; none exists and the work is ticket-worthy → create a narrow Linear ticket, then bind.",
package/dist/autostart.js CHANGED
@@ -380,7 +380,7 @@ async function windowsTaskStatus(options) {
380
380
  })}`;
381
381
  const currentScript = await readFile(scriptPath, "utf8").catch(() => null);
382
382
  if (currentScript !== expectedScript) {
383
- registrationProblems.push("sync script does not match the current roots or Cockpit runtime");
383
+ registrationProblems.push("sync script does not match the current roots or Tower runtime");
384
384
  }
385
385
  }
386
386
  // Same shape as the sync-script checks: existence always, content only when
@@ -399,7 +399,7 @@ async function windowsTaskStatus(options) {
399
399
  });
400
400
  const currentLauncher = await readFile(launcherPath, "utf8").catch(() => null);
401
401
  if (currentLauncher !== expectedLauncher) {
402
- registrationProblems.push("sync launcher does not match the current Cockpit runtime");
402
+ registrationProblems.push("sync launcher does not match the current Tower runtime");
403
403
  }
404
404
  }
405
405
  // Both branches log. A line that only fires on failure cannot answer "did
@@ -721,7 +721,7 @@ function windowsActionArgumentProblem(actionArguments, expectedFlags, expectedLa
721
721
  return "task action names no sync launcher to run";
722
722
  }
723
723
  if (!sameWindowsPath(launcherArgument, expectedLauncherPath)) {
724
- return "task action runs a script other than the Cockpit sync launcher";
724
+ return "task action runs a script other than the Tower sync launcher";
725
725
  }
726
726
  return null;
727
727
  }
@@ -883,7 +883,7 @@ function darwinAgentRegistrationProblems(plist, expected) {
883
883
  " </array>",
884
884
  ].join("\n");
885
885
  if (!plist.includes(expectedProgramArguments)) {
886
- problems.push("command does not match the current roots, dashboard URL, or Cockpit runtime");
886
+ problems.push("command does not match the current roots, dashboard URL, or Tower runtime");
887
887
  }
888
888
  return problems;
889
889
  }
@@ -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) {
@@ -60,7 +62,12 @@ async function sendOneTurn(context, prompt, io) {
60
62
  authorization: `Bearer ${context.deviceToken}`,
61
63
  "content-type": "application/json",
62
64
  },
63
- body: JSON.stringify({ question: prompt, thread: context.command.thread }),
65
+ body: JSON.stringify({
66
+ question: prompt,
67
+ thread: context.command.thread,
68
+ subject: context.command.subject,
69
+ model: context.command.model,
70
+ }),
64
71
  });
65
72
  }
66
73
  catch (error) {
@@ -79,19 +86,58 @@ async function sendOneTurn(context, prompt, io) {
79
86
  reply: body.reply,
80
87
  thread: body.thread ?? context.command.thread,
81
88
  model: body.model ?? null,
89
+ trace: body.trace ?? [],
90
+ subject: body.subject ?? null,
82
91
  }));
83
92
  }
84
93
  else {
85
- writeLine(io.stdout, `jarvis> ${body.reply}`);
94
+ const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
95
+ writeLine(io.stdout, `jarvis${subject}> ${body.reply}`);
96
+ writeTraceBlock(io, body.trace);
97
+ writeModelReceipt(io, body.model);
86
98
  }
87
99
  writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
88
100
  prompt_length: prompt.length,
89
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,
90
105
  elapsed_ms: Date.now() - startedAt,
91
106
  thread: context.command.thread === "main" ? "default" : "named",
107
+ subject: context.command.subject ? "selected" : "caller",
92
108
  })}`);
93
109
  return 0;
94
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
+ }
95
141
  async function readReply(response) {
96
142
  const text = await response.text();
97
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", "--thread", "--json"],
615
- valueFlags: ["--home", "--dashboard-url", "--prompt", "--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(" "));
@@ -628,7 +628,11 @@ function parseJarvisArgs(args) {
628
628
  homeDir: optionalNonEmpty(values.flags.get("--home")),
629
629
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
630
630
  prompt: flaggedPrompt ?? positionalPrompt,
631
+ subject: optionalNonEmpty(values.flags.get("--as")),
631
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")),
632
636
  json: values.booleans.has("--json"),
633
637
  };
634
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>] [--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,12 +190,15 @@ function localSubcommandHelp(command) {
190
190
  [
191
191
  "jarvis",
192
192
  [
193
- "Usage: cockpit jarvis [question] [--prompt <question>] [--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
+ "--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.",
196
198
  "Run with no question for an interactive terminal conversation.",
197
199
  "Agents can pass --prompt, positional text, or pipe one question on stdin.",
198
- "--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.",
199
202
  "The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
200
203
  "Run `cockpit login` first if this machine is not paired.",
201
204
  ],
@@ -266,10 +269,10 @@ function localSubcommandHelp(command) {
266
269
  [
267
270
  "Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
268
271
  "",
269
- "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
272
+ "Installs a managed Tower Ticket Binding block into ~/.codex/AGENTS.md",
270
273
  "and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
271
274
  "The block is scoped to --workspace, or the current directory when omitted,",
272
- "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.",
273
276
  "Action defaults to `install`.",
274
277
  ],
275
278
  ],
@@ -185,7 +185,7 @@ async function refreshOnboardAutostart(command, roots, io) {
185
185
  return result;
186
186
  }
187
187
  writeLine(io.stdout, result.loaded
188
- ? "Background autostart refreshed; Cockpit syncs at login and every 15 min."
188
+ ? "Background autostart refreshed; Tower syncs at login and every 15 min."
189
189
  : "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
190
190
  writeLine(io.stdout, autostartLocationLine(result));
191
191
  return result;
@@ -205,7 +205,7 @@ async function refreshOnboardAgentRules(command, roots, io) {
205
205
  }
206
206
  function onboardAgentRulesInstallLine(result) {
207
207
  if (result.targets.some((target) => target.stale_block_replaced)) {
208
- return "updated; replaced stale Cockpit ticket-binding guidance.";
208
+ return "updated; replaced stale Tower ticket-binding guidance.";
209
209
  }
210
210
  switch (result.status) {
211
211
  case "created":
@@ -311,7 +311,7 @@ function backgroundSyncLine(result) {
311
311
  return result.status;
312
312
  }
313
313
  function writeOnboardBanner(command, io) {
314
- writeLine(io.stdout, "Setting up Cockpit");
314
+ writeLine(io.stdout, "Setting up Tower");
315
315
  writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
316
316
  writeLine(io.stdout, `Ticket: ${displayTicketId(command.activeTicketId)}`);
317
317
  }
@@ -530,7 +530,7 @@ async function runOnboard(command, io) {
530
530
  }, null, 2));
531
531
  }
532
532
  if (onboardOk && !command.json) {
533
- writeLine(io.stdout, "PASS: Cockpit is set up and collecting.");
533
+ writeLine(io.stdout, "PASS: Tower is set up and collecting.");
534
534
  writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
535
535
  pair,
536
536
  status,
@@ -661,7 +661,7 @@ async function runOnboard(command, io) {
661
661
  writeOnboardAutostartBlocker(io, autostart);
662
662
  return finish(1);
663
663
  }
664
- writeLine(io.stdout, "PASS: Cockpit is set up and collecting.");
664
+ writeLine(io.stdout, "PASS: Tower is set up and collecting.");
665
665
  writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
666
666
  pair,
667
667
  status,
@@ -736,7 +736,7 @@ async function runMultiRepoOnboard(command, io, worktrees) {
736
736
  writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
737
737
  }
738
738
  else {
739
- writeLine(io.stderr, `BLOCKED: Cockpit collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
739
+ writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
740
740
  }
741
741
  writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
742
742
  }
@@ -773,7 +773,7 @@ async function runLogin(command, io) {
773
773
  onPairStarted: command.json
774
774
  ? undefined
775
775
  : (request) => {
776
- writeLine(io.stdout, "Cockpit device pairing started.");
776
+ writeLine(io.stdout, "Tower device pairing started.");
777
777
  writeLine(io.stdout, `Open: ${request.approve_url}`);
778
778
  writeLine(io.stdout, `Code: ${request.user_code}`);
779
779
  writeLine(io.stdout, "Waiting for dashboard approval...");
@@ -783,13 +783,13 @@ async function runLogin(command, io) {
783
783
  writeLine(io.stdout, JSON.stringify(result, null, 2));
784
784
  return 0;
785
785
  }
786
- writeLine(io.stdout, "Cockpit collector paired.");
786
+ writeLine(io.stdout, "Tower collector paired.");
787
787
  writeLine(io.stdout, `Session: ${result.session_file}`);
788
788
  writeLine(io.stdout, `User: ${result.session.email ?? result.session.auth_subject_id}`);
789
789
  writeLine(io.stdout, `Device: ${result.session.device_name ?? result.session.device_id ?? "unknown"}`);
790
790
  writeLine(io.stdout, config.default_repo_paths.length > 0
791
791
  ? "Next: run `cockpit start` inside the repo."
792
- : "Next: run `cockpit onboard` from your repo root so Cockpit knows which repos to collect.");
792
+ : "Next: run `cockpit onboard` from your repo root so Tower knows which repos to collect.");
793
793
  return 0;
794
794
  }
795
795
  function writePairingInstructions(io, request) {
@@ -862,8 +862,8 @@ async function runLogout(command, io) {
862
862
  return 0;
863
863
  }
864
864
  writeLine(io.stdout, result.removed
865
- ? "Cockpit collector session removed."
866
- : "No Cockpit collector session found.");
865
+ ? "Tower collector session removed."
866
+ : "No Tower collector session found.");
867
867
  return 0;
868
868
  }
869
869
  async function runStart(command, io) {
@@ -888,7 +888,7 @@ async function runStart(command, io) {
888
888
  writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", contexts }, null, 2));
889
889
  return 0;
890
890
  }
891
- writeLine(io.stdout, `Cockpit parent work context active for ${contexts.length} worktree(s).`);
891
+ writeLine(io.stdout, `Tower parent work context active for ${contexts.length} worktree(s).`);
892
892
  for (const context of contexts) {
893
893
  writeLine(io.stdout, `- ${context.repo_label ?? context.repo}/${context.worktree_label ?? "worktree"} · ${context.branch} · ${context.work_context_id}`);
894
894
  }
@@ -899,7 +899,7 @@ async function runStart(command, io) {
899
899
  writeLine(io.stdout, JSON.stringify(context, null, 2));
900
900
  return 0;
901
901
  }
902
- writeLine(io.stdout, "Cockpit work context active.");
902
+ writeLine(io.stdout, "Tower work context active.");
903
903
  writeLine(io.stdout, `Repo: ${context.repo}`);
904
904
  writeLine(io.stdout, `Branch: ${context.branch}`);
905
905
  writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
@@ -1156,7 +1156,7 @@ async function runSyncWithHealthReceipt(command, io) {
1156
1156
  }, null, 2));
1157
1157
  }
1158
1158
  else {
1159
- writeLine(io.stdout, "Cockpit sync already running; skipping this run.");
1159
+ writeLine(io.stdout, "Tower sync already running; skipping this run.");
1160
1160
  }
1161
1161
  return {
1162
1162
  exitCode: 0,
@@ -1249,7 +1249,7 @@ async function runSyncLocked(command, io) {
1249
1249
  }, null, 2));
1250
1250
  return syncResult(run);
1251
1251
  }
1252
- writeLine(run.ok ? io.stdout : io.stderr, `Cockpit parent sync ${collectionRunStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
1252
+ writeLine(run.ok ? io.stdout : io.stderr, `Tower parent sync ${collectionRunStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
1253
1253
  for (const outcome of run.outcomes) {
1254
1254
  const { worktree, sync } = outcome;
1255
1255
  const uploaded = sync.status === "uploaded";
@@ -1281,7 +1281,7 @@ async function runSyncLocked(command, io) {
1281
1281
  }, null, 2));
1282
1282
  return syncResult(run);
1283
1283
  }
1284
- writeLine(run.ok ? io.stdout : io.stderr, `Cockpit sync ${collectionRunStatus}: no git worktrees under this root; session scan ran.`);
1284
+ writeLine(run.ok ? io.stdout : io.stderr, `Tower sync ${collectionRunStatus}: no git worktrees under this root; session scan ran.`);
1285
1285
  writeAgentSessionSummary(io, run.summary);
1286
1286
  if (gc && !gc.skipped)
1287
1287
  writeLine(io.stdout, rawEvidenceGcSummary(gc));
@@ -1305,7 +1305,7 @@ async function runSyncLocked(command, io) {
1305
1305
  return syncResult(run);
1306
1306
  }
1307
1307
  if (run.ok) {
1308
- writeLine(io.stdout, "Cockpit uploaded this session.");
1308
+ writeLine(io.stdout, "Tower uploaded this session.");
1309
1309
  writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
1310
1310
  writeLine(io.stdout, `Context: ${result.work_context_id}`);
1311
1311
  writeLine(io.stdout, `Head: ${shortSha(result.head_sha)}`);
@@ -1320,11 +1320,11 @@ async function runSyncLocked(command, io) {
1320
1320
  return syncResult(run);
1321
1321
  }
1322
1322
  if (result.status === "uploaded") {
1323
- writeLine(io.stderr, "Cockpit uploaded, but some sessions did not make it. Run `cockpit sync` again.");
1323
+ writeLine(io.stderr, "Tower uploaded, but some sessions did not make it. Run `cockpit sync` again.");
1324
1324
  writeAgentSessionSummary(io, run.summary);
1325
1325
  return syncResult(run);
1326
1326
  }
1327
- writeLine(io.stderr, "Cockpit could not upload. It saved a note to retry and will try again on the next sync.");
1327
+ writeLine(io.stderr, "Tower could not upload. It saved a note to retry and will try again on the next sync.");
1328
1328
  writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
1329
1329
  writeLine(io.stderr, `Failure: ${result.failure_reason}`);
1330
1330
  writeLine(io.stderr, `Retry: ${result.retry_command}`);
@@ -1364,8 +1364,8 @@ async function runAnalyze(command, io) {
1364
1364
  sync: syncOutput,
1365
1365
  error: syncStderr.join("").trim()
1366
1366
  || (syncExitCode === 0
1367
- ? "Cockpit sync did not upload fresh evidence."
1368
- : "Cockpit sync failed."),
1367
+ ? "Tower sync did not upload fresh evidence."
1368
+ : "Tower sync failed."),
1369
1369
  }, null, 2));
1370
1370
  }
1371
1371
  else {
@@ -1387,7 +1387,7 @@ async function runAnalyze(command, io) {
1387
1387
  ...describeError(error),
1388
1388
  }));
1389
1389
  }
1390
- throw new Error("Cockpit is not signed in. Run `cockpit onboard` or `cockpit login` first.");
1390
+ throw new Error("Tower is not signed in. Run `cockpit onboard` or `cockpit login` first.");
1391
1391
  });
1392
1392
  const dashboardUrl = normalizeUrl(command.dashboardUrl ?? session.dashboard_url ?? DEFAULT_DASHBOARD_URL);
1393
1393
  const response = await io.fetch(`${dashboardUrl}/api/ambient/analyze`, {
@@ -1433,8 +1433,8 @@ async function runAnalyze(command, io) {
1433
1433
  return 0;
1434
1434
  }
1435
1435
  replayCaptured(io.stderr, syncStderr);
1436
- writeLine(io.stdout, "Cockpit uploaded your latest work.");
1437
- writeLine(io.stdout, "Cockpit analysis queued.");
1436
+ writeLine(io.stdout, "Tower uploaded your latest work.");
1437
+ writeLine(io.stdout, "Tower analysis queued.");
1438
1438
  if (jobId)
1439
1439
  writeLine(io.stdout, `Job: ${jobId}`);
1440
1440
  writeLine(io.stdout, `Status: ${jobStatus === "pending" ? "queued" : jobStatus}`);
@@ -1476,7 +1476,7 @@ async function runServe(command, io) {
1476
1476
  await new Promise((resolve) => {
1477
1477
  server.listen(command.port, "127.0.0.1", resolve);
1478
1478
  });
1479
- writeLine(io.stdout, `Cockpit collector serving on http://127.0.0.1:${command.port}`);
1479
+ writeLine(io.stdout, `Tower collector serving on http://127.0.0.1:${command.port}`);
1480
1480
  await new Promise((resolve) => {
1481
1481
  server.on("close", resolve);
1482
1482
  });
@@ -1544,8 +1544,8 @@ async function runAgentRules(command, io) {
1544
1544
  }
1545
1545
  if ("installed" in result) {
1546
1546
  writeLine(io.stdout, result.installed
1547
- ? "Cockpit agent rules are installed."
1548
- : "Cockpit agent rules are not installed.");
1547
+ ? "Tower agent rules are installed."
1548
+ : "Tower agent rules are not installed.");
1549
1549
  }
1550
1550
  else {
1551
1551
  writeLine(io.stdout, agentRulesStatusLine(result));
@@ -1561,13 +1561,13 @@ function agentRuleHosts(host) {
1561
1561
  function agentRulesStatusLine(result) {
1562
1562
  switch (result.status) {
1563
1563
  case "created":
1564
- return "Cockpit agent rules installed.";
1564
+ return "Tower agent rules installed.";
1565
1565
  case "updated":
1566
- return "Cockpit agent rules updated.";
1566
+ return "Tower agent rules updated.";
1567
1567
  case "unchanged":
1568
- return "Cockpit agent rules already current.";
1568
+ return "Tower agent rules already current.";
1569
1569
  case "missing":
1570
- return "Cockpit agent rules were not installed.";
1570
+ return "Tower agent rules were not installed.";
1571
1571
  }
1572
1572
  }
1573
1573
  function agentRuleHostLabel(host) {
@@ -1577,8 +1577,8 @@ function writeAutostartResult(io, result) {
1577
1577
  switch (result.status) {
1578
1578
  case "installed":
1579
1579
  writeLine(io.stdout, result.loaded
1580
- ? "Cockpit autostart installed and loaded."
1581
- : "Cockpit autostart installed (the operating-system scheduler reported a problem).");
1580
+ ? "Tower autostart installed and loaded."
1581
+ : "Tower autostart installed (the operating-system scheduler reported a problem).");
1582
1582
  writeLine(io.stdout, `Label: ${result.label}`);
1583
1583
  writeLine(io.stdout, autostartLocationLine(result));
1584
1584
  writeLine(io.stdout, `Interval: every ${result.interval_seconds}s`);
@@ -1588,19 +1588,19 @@ function writeAutostartResult(io, result) {
1588
1588
  writeLine(io.stderr, result.message);
1589
1589
  return;
1590
1590
  case "uninstalled":
1591
- writeLine(io.stdout, "Cockpit autostart removed.");
1591
+ writeLine(io.stdout, "Tower autostart removed.");
1592
1592
  writeLine(io.stdout, autostartLocationLine(result));
1593
1593
  return;
1594
1594
  case "absent":
1595
- writeLine(io.stdout, "Cockpit autostart is not installed.");
1595
+ writeLine(io.stdout, "Tower autostart is not installed.");
1596
1596
  writeLine(io.stdout, autostartLocationLine(result));
1597
1597
  return;
1598
1598
  case "loaded":
1599
- writeLine(io.stdout, "Cockpit autostart is installed and loaded.");
1599
+ writeLine(io.stdout, "Tower autostart is installed and loaded.");
1600
1600
  writeLine(io.stdout, autostartLocationLine(result));
1601
1601
  return;
1602
1602
  case "not_loaded":
1603
- writeLine(io.stdout, "Cockpit autostart exists but is disabled or not loaded; run `cockpit autostart install` to repair it.");
1603
+ writeLine(io.stdout, "Tower autostart exists but is disabled or not loaded; run `cockpit autostart install` to repair it.");
1604
1604
  writeLine(io.stdout, autostartLocationLine(result));
1605
1605
  if (result.message)
1606
1606
  writeLine(io.stderr, result.message);
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.32");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.34");
19
19
  return 0;
20
20
  }
21
21
 
@@ -89,7 +89,7 @@ export async function runSessions(command, io) {
89
89
  }, null, 2));
90
90
  return 0;
91
91
  }
92
- writeLine(io.stdout, "Cockpit sessions (read-only attribution)");
92
+ writeLine(io.stdout, "Tower sessions (read-only attribution)");
93
93
  writeLine(io.stdout, `window: ${sessionsWindowLine(window)}`);
94
94
  for (const row of codexRows) {
95
95
  writeLine(io.stdout, sessionRowLine(row));
@@ -35,7 +35,7 @@ export async function runStatus(command, io) {
35
35
  writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", statuses, backfill_cursor: backfillCursor }, null, 2));
36
36
  return 0;
37
37
  }
38
- writeLine(io.stdout, "Cockpit status — every repo below this folder");
38
+ writeLine(io.stdout, "Tower status — every repo below this folder");
39
39
  writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
40
40
  for (const status of statuses) {
41
41
  writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
@@ -47,7 +47,7 @@ export async function runStatus(command, io) {
47
47
  writeLine(io.stdout, JSON.stringify({ ...status, backfill_cursor: backfillCursor }, null, 2));
48
48
  return 0;
49
49
  }
50
- writeLine(io.stdout, "Cockpit status");
50
+ writeLine(io.stdout, "Tower status");
51
51
  writeLine(io.stdout, `Installed: ${status.installed ? "yes" : "no"}`);
52
52
  writeLine(io.stdout, `Signed in: ${signedInLine(status.session_state)}`);
53
53
  writeLine(io.stdout, `Repo: ${status.repo}`);
@@ -332,7 +332,7 @@ export function missingCollectionRootMessage(options = {}) {
332
332
  const homeDir = path.resolve(options.homeDir ?? os.homedir());
333
333
  return [
334
334
  "No collection root was confirmed:",
335
- " Cockpit only scans folders you name or confirm.",
335
+ " Tower only scans folders you name or confirm.",
336
336
  "What you can do:",
337
337
  ` 1) Paste one work parent: ${path.join(homeDir, "BLI")}`,
338
338
  ` 2) Paste specific folders: ${path.join(homeDir, "repo-a")},${path.join(homeDir, "repo-b")}`,
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "cockpit": "dist/cli.js"
7
+ "cockpit": "dist/cli.js",
8
+ "tower": "dist/cli.js"
8
9
  },
9
10
  "files": [
10
11
  "dist/",