@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.
- package/dist/agent-rules.js +4 -4
- package/dist/autostart.js +4 -4
- 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 +49 -3
- package/dist/commands/local-args.js +6 -2
- package/dist/commands/local-auth.js +3 -3
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help.js +12 -9
- package/dist/commands/local.js +37 -37
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/onboarding-roots.js +1 -1
- package/package.json +3 -2
package/dist/agent-rules.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
"##
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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) {
|
|
@@ -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({
|
|
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
|
-
|
|
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
|
|
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>] [--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,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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
275
|
+
"so Codex and Claude only run Tower ticket binding inside that onboarded folder.",
|
|
273
276
|
"Action defaults to `install`.",
|
|
274
277
|
],
|
|
275
278
|
],
|
package/dist/commands/local.js
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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, "
|
|
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, "
|
|
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
|
|
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
|
-
? "
|
|
866
|
-
: "No
|
|
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, `
|
|
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, "
|
|
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, "
|
|
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, `
|
|
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, `
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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
|
-
? "
|
|
1368
|
-
: "
|
|
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("
|
|
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, "
|
|
1437
|
-
writeLine(io.stdout, "
|
|
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, `
|
|
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
|
-
? "
|
|
1548
|
-
: "
|
|
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 "
|
|
1564
|
+
return "Tower agent rules installed.";
|
|
1565
1565
|
case "updated":
|
|
1566
|
-
return "
|
|
1566
|
+
return "Tower agent rules updated.";
|
|
1567
1567
|
case "unchanged":
|
|
1568
|
-
return "
|
|
1568
|
+
return "Tower agent rules already current.";
|
|
1569
1569
|
case "missing":
|
|
1570
|
-
return "
|
|
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
|
-
? "
|
|
1581
|
-
: "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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.
|
|
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, "
|
|
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));
|
package/dist/commands/status.js
CHANGED
|
@@ -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, "
|
|
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, "
|
|
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}`);
|
package/dist/onboarding-roots.js
CHANGED
|
@@ -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
|
-
"
|
|
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