@bli-cockpit/cli 0.2.179 → 0.2.183
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/commands/agent-setup.js +152 -0
- package/dist/commands/brain.js +35 -0
- package/dist/commands/doctor-update.js +4 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/glossary.js +17 -0
- package/dist/commands/heartbeat.js +5 -1
- package/dist/commands/local-args-tower-admin.js +6 -1
- package/dist/commands/local-args-tower-brain.js +38 -0
- package/dist/commands/local-args-tower-memory.js +15 -0
- package/dist/commands/local-args-tower-pages.js +16 -3
- package/dist/commands/local-args-tower-setup.js +10 -0
- package/dist/commands/local-args-tower.js +4 -1
- package/dist/commands/local-args.js +8 -1
- package/dist/commands/local-command-shapes-brain.js +5 -0
- package/dist/commands/local-help-commands-tower-ops.js +17 -0
- package/dist/commands/local-help-commands-tower.js +12 -4
- package/dist/commands/local-help-commands.js +3 -0
- package/dist/commands/local-help.js +7 -1
- package/dist/commands/local.js +11 -5
- package/dist/commands/memory-store.js +23 -3
- package/dist/commands/memory-verb-list.js +4 -1
- package/dist/commands/memory-visibility.js +48 -0
- package/dist/commands/notes-accounts.js +102 -20
- package/dist/commands/notes-reads.js +1 -1
- package/dist/commands/notes-recordings.js +113 -0
- package/dist/commands/notes-wisprflow-login.js +219 -0
- package/dist/commands/notes.js +6 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/setup.js +39 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync-doctor-request.js +13 -0
- package/dist/commands/sync-doctor.js +145 -0
- package/dist/commands/sync.js +4 -3
- package/dist/commands/team.js +19 -0
- package/dist/local-state-status.js +5 -1
- package/dist/raw-evidence-stuck-summary.js +21 -1
- package/package.json +4 -4
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { AgentSetupSchema, SetupNameSchema } from "@bli-cockpit/telemetry-core";
|
|
7
|
+
import { LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const files = {
|
|
10
|
+
read: file => fs.readFile(file, "utf8"), list: directory => fs.readdir(directory),
|
|
11
|
+
version: async (tool, platform) => {
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await exec(platform === "win32" ? `${tool}.exe` : tool, ["--version"], { timeout: 750, maxBuffer: 4096, windowsHide: true });
|
|
14
|
+
return /(?:^|\s)(\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?)(?:\s|$)/i.exec(stdout)?.[1] ?? null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const safeName = (value) => SetupNameSchema.safeParse(value).success ? value : null;
|
|
22
|
+
const names = (values) => [...new Set(values.flatMap(value => safeName(value) ? [value] : []))].sort().slice(0, 500);
|
|
23
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
24
|
+
/** Bounded, explicit locations only. Never recurse through a home or read env files. */
|
|
25
|
+
export async function collectAgentSetup(home, platform = process.platform, io = files, now = new Date()) {
|
|
26
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
27
|
+
const join = (...parts) => paths.join(home, ...parts);
|
|
28
|
+
const read = async (file) => {
|
|
29
|
+
try {
|
|
30
|
+
const text = await io.read(file);
|
|
31
|
+
return text.length <= 1_000_000 ? text : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const list = async (...parts) => {
|
|
38
|
+
try {
|
|
39
|
+
return names(await io.list(join(...parts)));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return error.code === "ENOENT" ? [] : null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const json = async (...parts) => {
|
|
46
|
+
const text = await read(join(...parts));
|
|
47
|
+
if (text === null)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
return object(JSON.parse(text));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const installedVersion = async (tool) => {
|
|
57
|
+
const version = await io.version?.(tool, platform).catch(() => null);
|
|
58
|
+
if (safeName(version))
|
|
59
|
+
return version;
|
|
60
|
+
// npm shims on native Windows cannot be execFile'd without a shell. Read
|
|
61
|
+
// their installed package metadata instead, never the update-available cache.
|
|
62
|
+
const packageName = tool === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
|
|
63
|
+
const packageFile = platform === "win32" ? join("AppData", "Roaming", "npm", "node_modules", ...packageName.split("/"), "package.json") :
|
|
64
|
+
paths.join(paths.dirname(process.execPath), "..", "lib", "node_modules", ...packageName.split("/"), "package.json");
|
|
65
|
+
const text = await read(packageFile);
|
|
66
|
+
try {
|
|
67
|
+
return safeName(object(JSON.parse(text ?? "null")).version);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const claudeVersion = await installedVersion("claude");
|
|
74
|
+
const codexVersion = await installedVersion("codex");
|
|
75
|
+
const codexHooks = await json(".codex", "hooks.json");
|
|
76
|
+
const claudePlugins = await json(".claude", "plugins", "installed_plugins.json");
|
|
77
|
+
const pluginSkills = [], pluginAgents = [];
|
|
78
|
+
let pluginInventoryUnknown = false;
|
|
79
|
+
for (const [pluginName, installations] of Object.entries(object(claudePlugins?.plugins))) {
|
|
80
|
+
if (!safeName(pluginName) || !Array.isArray(installations))
|
|
81
|
+
continue;
|
|
82
|
+
for (const installation of installations.slice(0, 20)) {
|
|
83
|
+
const directory = object(installation).installPath;
|
|
84
|
+
if (typeof directory !== "string" || !paths.isAbsolute(directory)) {
|
|
85
|
+
pluginInventoryUnknown = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const category of ["skills", "agents"]) {
|
|
89
|
+
try {
|
|
90
|
+
const entries = names(await io.list(paths.join(directory, category)));
|
|
91
|
+
(category === "skills" ? pluginSkills : pluginAgents).push(...entries.map(name => `${pluginName}.${name}`));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== "ENOENT")
|
|
95
|
+
pluginInventoryUnknown = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const claudeSkills = await list(".claude", "skills");
|
|
101
|
+
const claudeAgents = await list(".claude", "agents");
|
|
102
|
+
const sharedSkills = await list(".agents", "skills");
|
|
103
|
+
const codexSkills = await list(".codex", "skills");
|
|
104
|
+
const systemSkills = await list(".codex", "skills", ".system");
|
|
105
|
+
const claudeSettings = await json(".claude", "settings.json");
|
|
106
|
+
const claudeConfig = await json(".claude.json");
|
|
107
|
+
const toml = await read(join(".codex", "config.toml"));
|
|
108
|
+
// Read only simple quoted scalar values. Complex TOML is unknown, not guessed.
|
|
109
|
+
const rootToml = toml?.split(/^\s*\[/m)[0] ?? "";
|
|
110
|
+
const scalar = (key) => safeName(new RegExp(`^${key}\\s*=\\s*"([^"\\n]+)"\\s*(?:#.*)?$`, "m").exec(rootToml)?.[1]);
|
|
111
|
+
const codexMcp = toml === null ? null : [...toml.matchAll(/^\s*\[mcp_servers\.(?:"([a-z0-9_.@+-]+)"|([a-z0-9_-]+))\]\s*$/gim)].flatMap(match => {
|
|
112
|
+
const name = safeName(match[1] ?? match[2]);
|
|
113
|
+
if (!name)
|
|
114
|
+
return [];
|
|
115
|
+
const block = toml.slice(match.index + match[0].length).split(/^\s*\[/m)[0];
|
|
116
|
+
return [{ name, transport: /^\s*command\s*=/m.test(block) ? "stdio" : /^\s*url\s*=/m.test(block) ? "http" : "unknown" }];
|
|
117
|
+
}).slice(0, 500);
|
|
118
|
+
const claudeServers = [object(claudeConfig?.mcpServers), ...Object.values(object(claudeConfig?.projects)).map(project => object(object(project).mcpServers))];
|
|
119
|
+
const claudeMcp = claudeConfig === null ? null : claudeServers.flatMap(servers => Object.entries(servers)).flatMap(([key, value]) => {
|
|
120
|
+
const name = safeName(key);
|
|
121
|
+
if (!name)
|
|
122
|
+
return [];
|
|
123
|
+
const config = object(value);
|
|
124
|
+
const transport = config.type === "sse" ? "sse" : config.type === "http" || config.url ? "http" : config.command ? "stdio" : "unknown";
|
|
125
|
+
return [{ name, transport }];
|
|
126
|
+
}).slice(0, 500);
|
|
127
|
+
const instructions = await Promise.all(["CLAUDE.md", "AGENTS.md"].map(async (name) => {
|
|
128
|
+
const file = join(name === "CLAUDE.md" ? ".claude" : ".codex", name);
|
|
129
|
+
try {
|
|
130
|
+
const content = await io.read(file);
|
|
131
|
+
return { name, exists: true, sha256: crypto.createHash("sha256").update(content).digest("hex"), bytes: Buffer.byteLength(content) };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return { name, exists: error.code === "ENOENT" ? false : null, sha256: null, bytes: null };
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
const result = AgentSetupSchema.parse({
|
|
138
|
+
checked_at: now.toISOString(), os: platform === "darwin" || platform === "win32" ? platform : "other", cli_version: LOCAL_COLLECTOR_VERSION,
|
|
139
|
+
claude: { version: claudeVersion, model: safeName(claudeSettings?.model), effort: safeName(claudeSettings?.effortLevel),
|
|
140
|
+
skills: claudeSkills === null || pluginInventoryUnknown ? null : names([...claudeSkills, ...pluginSkills]), agents: claudeAgents === null || pluginInventoryUnknown ? null : names([...claudeAgents, ...pluginAgents]),
|
|
141
|
+
plugins: claudePlugins === null ? null : names(Object.keys(object(claudePlugins.plugins))),
|
|
142
|
+
hooks: claudeSettings === null ? null : names(Object.keys(object(claudeSettings.hooks))), mcp: claudeMcp },
|
|
143
|
+
codex: { version: codexVersion, model: scalar("model"), effort: scalar("model_reasoning_effort"),
|
|
144
|
+
skills: codexSkills === null || sharedSkills === null || systemSkills === null ? null : names([...(codexSkills ?? []), ...(sharedSkills ?? []), ...(systemSkills ?? [])]),
|
|
145
|
+
agents: toml === null ? null : names([...toml.matchAll(/^\s*\[agents\.([a-z0-9_-]+)\]\s*$/gim)].map(match => match[1])),
|
|
146
|
+
plugins: toml === null ? null : names([...toml.matchAll(/^\s*\[plugins\."([a-z0-9_@.+-]+)"\]\s*$/gim)].map(match => match[1])),
|
|
147
|
+
hooks: codexHooks === null && !/^notify\s*=/m.test(rootToml) ? null : names([...(codexHooks === null ? [] : Object.keys(object(codexHooks.hooks))), ...(/^notify\s*=/m.test(rootToml) ? ["notify"] : [])]), mcp: codexMcp }, instructions,
|
|
148
|
+
});
|
|
149
|
+
const unknownFields = [result.claude, result.codex].reduce((count, tool) => count + Object.values(tool).filter(value => value === null).length, 0);
|
|
150
|
+
console.error("[setup] inventory read", JSON.stringify({ reason: unknownFields ? "inventory_partial" : "inventory_observed", unknown_fields: unknownFields, os: result.os, mcp_count: (result.claude.mcp?.length ?? 0) + (result.codex.mcp?.length ?? 0) }));
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[brain cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 55_000;
|
|
5
|
+
export async function runBrain(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("brain", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const query = new URLSearchParams();
|
|
20
|
+
if (command.project)
|
|
21
|
+
query.set("project", command.project);
|
|
22
|
+
if (command.show)
|
|
23
|
+
query.set("status", command.show);
|
|
24
|
+
const asked = query.toString();
|
|
25
|
+
const path = asked ? `/api/brain/incidents?${asked}` : "/api/brain/incidents";
|
|
26
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "brain status", timeoutMs: READ_DEADLINE_MS });
|
|
27
|
+
if (!answer.ok)
|
|
28
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
29
|
+
const body = answer.body;
|
|
30
|
+
writeLine(io.stderr, `${TAG} status read ${JSON.stringify({ projects: body.projects.length, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
31
|
+
if (command.json)
|
|
32
|
+
return emitAgentDoor(door, body);
|
|
33
|
+
writeLine(io.stdout, body.text);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
@@ -27,6 +27,10 @@ export async function fixCliLatest(context, state) {
|
|
|
27
27
|
catch (error) {
|
|
28
28
|
return fail("cli-latest", selfUpdateFailureCode(error), selfUpdateFailureMessage(error));
|
|
29
29
|
}
|
|
30
|
+
if (context.command.automaticTick) {
|
|
31
|
+
console.error("[doctor] update installed for the next tick", JSON.stringify({ reason: "restart_next_tick" }));
|
|
32
|
+
return { id: "cli-latest", status: "skipped", code: "restart_next_tick", message: "CLI updated; the next tick uses the new version." };
|
|
33
|
+
}
|
|
30
34
|
if (context.io.env["COCKPIT_DOCTOR_REEXEC"] === "1") {
|
|
31
35
|
if (state.code === "stale_cli") {
|
|
32
36
|
return fail("cli-latest", "stale_after_self_update", "self-update ran but this process still reports the old CLI version; rerun `cockpit doctor`.");
|
package/dist/commands/doctor.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function runDoctorWithDeps(command, io, deps) {
|
|
|
47
47
|
try {
|
|
48
48
|
row = diagnosed.get(invariant.id);
|
|
49
49
|
const needsCollection = ["backfill-complete", "sync-fresh"].includes(invariant.id);
|
|
50
|
-
const prerequisitesReady = !needsCollection || rows.filter((prior) => prior.id === "authed" || prior.id === "roots-ok").every((prior) => prior.status === "ok");
|
|
50
|
+
const prerequisitesReady = !needsCollection || rows.filter((prior) => prior.id === "authed" || (!command.automaticTick && prior.id === "roots-ok")).every((prior) => prior.status === "ok");
|
|
51
51
|
const canFix = !diagnoseOnly && prerequisitesReady && invariant.fix &&
|
|
52
52
|
(!invariant.requiresInteractiveFix || isInteractiveDoctorFix(context) || Boolean(command.collectionRoots?.length));
|
|
53
53
|
let spent = row.status !== "ok" && row.status !== "skipped" && canFix ? budgetSpentRow(context, row) : null;
|
|
@@ -117,7 +117,6 @@ export async function runDoctorWithDeps(command, io, deps) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
// BLI-12828: every repair's receipt says what it found and what it left.
|
|
120
|
-
finalizeDoctorRepairs(repairs, diagnosed, rows, attempted);
|
|
121
120
|
const floor = await maybeReportDoctorEvents(context, rows);
|
|
122
121
|
if (floor && isSemverBelow(LOCAL_COLLECTOR_VERSION, floor)) {
|
|
123
122
|
const index = rows.findIndex((row) => row.id === "cli-latest");
|
|
@@ -134,8 +133,10 @@ export async function runDoctorWithDeps(command, io, deps) {
|
|
|
134
133
|
rows.push({ id: "memory-registered", status: "needs_fix", code: "receipt_read_threw", message: "Run `cockpit doctor` to read the final setup receipt." });
|
|
135
134
|
}
|
|
136
135
|
}
|
|
136
|
+
finalizeDoctorRepairs(repairs, diagnosed, rows, attempted);
|
|
137
137
|
const needsPerson = doctorNeedsPerson(rows, receipt).length;
|
|
138
138
|
console.error("[doctor] run done", JSON.stringify({ elapsed_ms: Date.now() - runStarted, rows: rows.length, needs_person: needsPerson, check_only: Boolean(diagnoseOnly) }));
|
|
139
|
+
deps.onReceipt?.(rows, repairs, receipt);
|
|
139
140
|
writeDoctorOutput(command, io, rows, receipt, repairs);
|
|
140
141
|
return needsPerson === 0 ? 0 : 1;
|
|
141
142
|
}
|
|
@@ -232,6 +233,7 @@ function doctorInvariants() {
|
|
|
232
233
|
}
|
|
233
234
|
function defaultDoctorDeps(hooks) {
|
|
234
235
|
return {
|
|
236
|
+
onReceipt: hooks.onReceipt,
|
|
235
237
|
checkAgentRules: checkDoctorAgentRules,
|
|
236
238
|
fixAgentRules: fixDoctorAgentRules,
|
|
237
239
|
latestCliVersion: latestCliVersionFromNpm,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { parseNamedArgs, optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
3
|
+
export function parseGlossaryArgs(args) {
|
|
4
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--to", "--json", "--home", "--dashboard-url"], valueFlags: ["--to", "--home", "--dashboard-url"] });
|
|
5
|
+
const [action = "list", wrong, ...rest] = values.positionals;
|
|
6
|
+
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
7
|
+
if (!["list", "add", "remove"].includes(action) || rest.length || (action === "list" && (wrong || to)) || (action !== "list" && !wrong) || (action === "add" && !to) || (action === "remove" && to))
|
|
8
|
+
throw new Error("Usage: cockpit glossary list | add <wrong> --to <name> | remove <wrong>");
|
|
9
|
+
return { kind: "glossary", action: action, wrong, to, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
10
|
+
}
|
|
11
|
+
export async function runGlossary(command, io) {
|
|
12
|
+
const door = await openAgentDoor("glossary", command, io);
|
|
13
|
+
const answer = await askAgentDoor(door, { path: "/api/glossary", method: command.action === "list" ? "GET" : "POST", label: "glossary", timeoutMs: 30_000, ...(command.action === "list" ? {} : { body: { action: command.action, wrong: command.wrong, to: command.to } }) });
|
|
14
|
+
if (!answer.ok)
|
|
15
|
+
return failAgentDoor(door, "[glossary cli]", answer.reason, answer.detail);
|
|
16
|
+
return emitAgentDoor(door, answer.body);
|
|
17
|
+
}
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* The path itself never leaves the machine, exactly as with `cwd_hash` on the
|
|
20
20
|
* session rows.
|
|
21
21
|
*/
|
|
22
|
+
import { noteDoctorRequest } from "./sync-doctor-request.js";
|
|
22
23
|
import crypto from "node:crypto";
|
|
24
|
+
import { collectAgentSetup } from "./agent-setup.js";
|
|
23
25
|
import fs from "node:fs";
|
|
24
26
|
import os from "node:os";
|
|
25
27
|
import path from "node:path";
|
|
@@ -268,7 +270,7 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
268
270
|
const heartbeat = buildCollectorHeartbeat({
|
|
269
271
|
roots: options.roots,
|
|
270
272
|
facts: options.facts,
|
|
271
|
-
setupReceipt,
|
|
273
|
+
setupReceipt: { ...(setupReceipt ?? { schema_version: "setup-receipt.v1", checked_at: new Date().toISOString(), browser: { status: "unknown" }, device: { status: "unknown" }, collector: { autostart: { status: "unknown" } } }), agent_setup: await collectAgentSetup(options.homeDir ?? os.homedir()) },
|
|
272
274
|
...(options.now ? { now: options.now } : {}),
|
|
273
275
|
memoryInstall: memory.receipt,
|
|
274
276
|
});
|
|
@@ -307,6 +309,8 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
307
309
|
// BLI-4019. The server slides `token_expires_at` out on every call it
|
|
308
310
|
// authenticates and answers with the result; the sibling records it, so
|
|
309
311
|
// this machine's own copy follows the server rather than drifting.
|
|
312
|
+
const reply = await response.clone().json().catch(() => null);
|
|
313
|
+
noteDoctorRequest(reply?.device_action);
|
|
310
314
|
await recordHeartbeatTokenExpiry(options.homeDir, response);
|
|
311
315
|
console.error("[heartbeat] checked in", JSON.stringify({
|
|
312
316
|
reason: "heartbeat_recorded",
|
|
@@ -332,6 +332,11 @@ function parseTeamDeviceArgs(rest, flags, common) {
|
|
|
332
332
|
throw new Error("team device list does not accept positional arguments.");
|
|
333
333
|
return { kind: "team", action: "device-list", ...common };
|
|
334
334
|
}
|
|
335
|
+
if (deviceVerb === "doctor") {
|
|
336
|
+
if (!deviceRef)
|
|
337
|
+
throw new Error("team device doctor needs a device id or name.");
|
|
338
|
+
return { kind: "team", action: "device-doctor", deviceRef, ...common };
|
|
339
|
+
}
|
|
335
340
|
if (deviceVerb === "revoke") {
|
|
336
341
|
if (!deviceRef)
|
|
337
342
|
throw new Error("team device revoke needs a device id or name.");
|
|
@@ -351,7 +356,7 @@ function parseTeamDeviceArgs(rest, flags, common) {
|
|
|
351
356
|
...common,
|
|
352
357
|
};
|
|
353
358
|
}
|
|
354
|
-
throw new Error("team device takes one verb: list or revoke <id|name>.");
|
|
359
|
+
throw new Error("team device takes one verb: list, doctor <id|name>, or revoke <id|name>.");
|
|
355
360
|
}
|
|
356
361
|
/** `cockpit model [show|set <key>]` — the alias surface for the personal chat model. */
|
|
357
362
|
export function parseModelArgs(args) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit brain` argument parsing (BLI-12859).
|
|
3
|
+
*
|
|
4
|
+
* cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--json]
|
|
5
|
+
*
|
|
6
|
+
* Tower's incident queue: per project, how many incidents are open, being worked, waiting on a
|
|
7
|
+
* person, fixed, not actionable or cleared, the newest ones with their Tower issue, and the last
|
|
8
|
+
* 7 days of verdicts. It reads `GET /api/brain/incidents` with this machine's device token.
|
|
9
|
+
*/
|
|
10
|
+
import { LOG_PROJECT_PATTERN } from "@bli-cockpit/telemetry-core/log-digest";
|
|
11
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
12
|
+
/** The statuses the queue's door accepts for `--show`; the migration's CHECK constraint is the other copy. */
|
|
13
|
+
export const BRAIN_SHOW_STATUSES = ["open", "running", "needs_human", "fixed", "not_actionable", "cleared"];
|
|
14
|
+
export function parseBrainArgs(args) {
|
|
15
|
+
const values = parseNamedArgs(args, {
|
|
16
|
+
allowedFlags: ["--project", "--show", "--home", "--dashboard-url", "--json"],
|
|
17
|
+
valueFlags: ["--project", "--show", "--home", "--dashboard-url"],
|
|
18
|
+
});
|
|
19
|
+
const action = values.positionals[0];
|
|
20
|
+
if (action !== "status" || values.positionals.length > 1) {
|
|
21
|
+
throw new Error("brain takes one verb: `status`. Example: `cockpit brain status` or `cockpit brain status --project pearl-portal`.");
|
|
22
|
+
}
|
|
23
|
+
const project = optionalNonEmpty(values.flags.get("--project"));
|
|
24
|
+
if (project && !LOG_PROJECT_PATTERN.test(project))
|
|
25
|
+
throw new Error("--project wants a registry project id like pearl-portal (`cockpit logs --list` names them).");
|
|
26
|
+
const show = optionalNonEmpty(values.flags.get("--show"));
|
|
27
|
+
if (show && !BRAIN_SHOW_STATUSES.includes(show))
|
|
28
|
+
throw new Error(`--show wants one of ${BRAIN_SHOW_STATUSES.join(", ")}.`);
|
|
29
|
+
return {
|
|
30
|
+
kind: "brain",
|
|
31
|
+
action: "status",
|
|
32
|
+
...(project ? { project } : {}),
|
|
33
|
+
...(show ? { show } : {}),
|
|
34
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
35
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
36
|
+
json: values.booleans.has("--json"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -37,6 +37,11 @@ export const MEMORY_STORE_ACTIONS = new Set([
|
|
|
37
37
|
"save",
|
|
38
38
|
"update",
|
|
39
39
|
"forget",
|
|
40
|
+
// BLI-12927: BLI Memory is public to the team until its owner narrows it.
|
|
41
|
+
// `private <id>` / `public <id>` flip one memory; same door as the
|
|
42
|
+
// `set_memory_visibility` tool.
|
|
43
|
+
"private",
|
|
44
|
+
"public",
|
|
40
45
|
"profile",
|
|
41
46
|
// BLI-4534: a READ of the nightly consolidation receipts, not a fifth way to
|
|
42
47
|
// write a memory. It runs no pass and changes nothing — the pass is a
|
|
@@ -176,6 +181,16 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
176
181
|
if (action === "consolidation") {
|
|
177
182
|
return { ...command, action };
|
|
178
183
|
}
|
|
184
|
+
if (action === "private" || action === "public") {
|
|
185
|
+
// BLI-12927: one memory, named by the id `cockpit memory search` printed.
|
|
186
|
+
const memoryId = optionalNonEmpty(rest[0]);
|
|
187
|
+
if (!memoryId) {
|
|
188
|
+
throw new Error(`memory ${action} needs the memory id \`cockpit memory search\` printed.`);
|
|
189
|
+
}
|
|
190
|
+
if (rest.length > 1)
|
|
191
|
+
throw new Error(`memory ${action} takes one id.`);
|
|
192
|
+
return { ...command, action, memoryId };
|
|
193
|
+
}
|
|
179
194
|
if (action === "save") {
|
|
180
195
|
if (rest.length > 0) {
|
|
181
196
|
throw new Error("memory save reads the memory from stdin or --file, never from the command line: `echo \"...\" | cockpit memory save`.");
|
|
@@ -181,7 +181,13 @@ export function parseBriefArgs(args) {
|
|
|
181
181
|
* `otter` and `granola` are deliberately absent: Otter's public API is
|
|
182
182
|
* Enterprise-only and Granola still runs on its own server-side cron.
|
|
183
183
|
*/
|
|
184
|
-
const CONNECTABLE_PROVIDERS = ["fellow", "circleback"];
|
|
184
|
+
const CONNECTABLE_PROVIDERS = ["fellow", "circleback", "wisprflow"];
|
|
185
|
+
/**
|
|
186
|
+
* BLI-12926. Providers with no API key at all: the person signs in through
|
|
187
|
+
* their browser and `connect` runs that sign-in itself. `--key-stdin` is
|
|
188
|
+
* optional for these, and carries a pre-built sign-in bundle when given.
|
|
189
|
+
*/
|
|
190
|
+
const PROVIDERS_SIGNED_IN_BY_BROWSER = ["wisprflow"];
|
|
185
191
|
/** The provider whose API is per workspace, so its account needs a subdomain. */
|
|
186
192
|
const PROVIDERS_NEEDING_A_WORKSPACE = ["fellow"];
|
|
187
193
|
const NOTES_ACTIONS = new Set([
|
|
@@ -202,6 +208,9 @@ const NOTES_ACTIONS = new Set([
|
|
|
202
208
|
"detach",
|
|
203
209
|
"sync",
|
|
204
210
|
"sync-status",
|
|
211
|
+
// BLI-12926: the alternates of a meeting several notetakers recorded.
|
|
212
|
+
"recordings",
|
|
213
|
+
"recording",
|
|
205
214
|
// BLI-4394: arm that notetaker's webhook with the vendor's signing secret.
|
|
206
215
|
"webhook",
|
|
207
216
|
// Ticket 10718: the Granola cron's own last-several-runs health.
|
|
@@ -214,6 +223,9 @@ const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
|
214
223
|
"unshare",
|
|
215
224
|
"move",
|
|
216
225
|
"place",
|
|
226
|
+
// BLI-12926: `recordings` names the note, `recording` names the recording.
|
|
227
|
+
"recordings",
|
|
228
|
+
"recording",
|
|
217
229
|
]);
|
|
218
230
|
/**
|
|
219
231
|
* BLI-4383. Actions whose first positional is a NOTETAKER ACCOUNT id, not a
|
|
@@ -277,7 +289,7 @@ export function parseNotesArgs(args) {
|
|
|
277
289
|
const first = values.positionals[0];
|
|
278
290
|
const action = (first === undefined ? "list" : first);
|
|
279
291
|
if (!NOTES_ACTIONS.has(action)) {
|
|
280
|
-
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, move, place, accounts, connect, detach, sync, sync-status, webhook, or granola-status.`);
|
|
292
|
+
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, or granola-status.`);
|
|
281
293
|
}
|
|
282
294
|
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
283
295
|
const json = values.booleans.has("--json");
|
|
@@ -340,7 +352,7 @@ export function parseNotesArgs(args) {
|
|
|
340
352
|
if (workspace !== undefined && !/^[a-z0-9-]{1,63}$/.test(workspace)) {
|
|
341
353
|
throw new Error("--workspace is a workspace subdomain: lowercase letters, digits and hyphens, 1 to 63 characters.");
|
|
342
354
|
}
|
|
343
|
-
if (!values.booleans.has("--key-stdin")) {
|
|
355
|
+
if (!values.booleans.has("--key-stdin") && !PROVIDERS_SIGNED_IN_BY_BROWSER.includes(provider)) {
|
|
344
356
|
// Naming the flag is the point: it says out loud that the key is not an
|
|
345
357
|
// argument, so nobody goes looking for a `--key` that will never exist.
|
|
346
358
|
throw new Error('notes connect reads the key from stdin. Say so: printf "%s" "<key>" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin');
|
|
@@ -397,6 +409,7 @@ export function parseNotesArgs(args) {
|
|
|
397
409
|
...(provider ? { provider } : {}),
|
|
398
410
|
...(workspace ? { workspace } : {}),
|
|
399
411
|
...(showWebhook ? { showWebhook: true } : {}),
|
|
412
|
+
...(action === "connect" && values.booleans.has("--key-stdin") ? { keyStdin: true } : {}),
|
|
400
413
|
...(paths ? { paths } : {}),
|
|
401
414
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
402
415
|
name: optionalNonEmpty(values.flags.get("--name")),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
|
+
export function parseSetupArgs(args) {
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--person", "--compare", "--json", "--home", "--dashboard-url"], valueFlags: ["--person", "--compare", "--home", "--dashboard-url"] });
|
|
4
|
+
if (values.positionals.length)
|
|
5
|
+
throw new Error("setup takes --person or --compare, not a subcommand.");
|
|
6
|
+
const compare = optionalNonEmpty(values.flags.get("--compare"));
|
|
7
|
+
if (compare && (compare.split(",").length !== 2 || compare.split(",").some(value => !value.trim())))
|
|
8
|
+
throw new Error("--compare takes two emails separated by a comma.");
|
|
9
|
+
return { kind: "setup", person: optionalNonEmpty(values.flags.get("--person")), compare, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
10
|
+
}
|
|
@@ -37,7 +37,10 @@ export { SEARCH_KINDS, parseSearchArgs } from "./local-args-tower-search.js";
|
|
|
37
37
|
export { parseModelsArgs } from "./local-args-tower-models.js";
|
|
38
38
|
export { parseMailArgs } from "./local-args-tower-mail.js";
|
|
39
39
|
export { parseCalArgs } from "./local-args-tower-cal.js";
|
|
40
|
+
export { parseSetupArgs } from "./local-args-tower-setup.js";
|
|
40
41
|
export { parseUsageArgs } from "./local-args-tower-usage.js";
|
|
41
42
|
export { parseLogsArgs } from "./local-args-tower-logs.js";
|
|
43
|
+
export { parseBrainArgs } from "./local-args-tower-brain.js";
|
|
42
44
|
export { parseCareersArgs } from './local-args-tower-careers.js';
|
|
43
|
-
export { parseRenameArgs } from './local-args-tower-rename.js';
|
|
45
|
+
export { parseRenameArgs } from './local-args-tower-rename.js';
|
|
46
|
+
export { parseGlossaryArgs } from "./glossary.js";
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* its own rate. Behavior-preserving extraction throughout: functions moved
|
|
16
16
|
* verbatim, no logic change.
|
|
17
17
|
*/
|
|
18
|
+
import { parseGlossaryArgs } from "./glossary.js";
|
|
18
19
|
import { parseRootsArgs } from "./roots.js";
|
|
19
20
|
import { SURFACE_ALIASES } from "./local-help.js";
|
|
20
21
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
21
|
-
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseLogsArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
22
|
+
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseSetupArgs, parseLogsArgs, parseBrainArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
22
23
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
23
24
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
24
25
|
// even though it now lives next door. The same goes for the four names the
|
|
@@ -109,6 +110,8 @@ export function parseLocalArgs(argv) {
|
|
|
109
110
|
return parseCleanArgs(argv.slice(1));
|
|
110
111
|
case "sources":
|
|
111
112
|
return parseSourcesArgs(argv.slice(1));
|
|
113
|
+
case "glossary":
|
|
114
|
+
return parseGlossaryArgs(argv.slice(1));
|
|
112
115
|
case "docs":
|
|
113
116
|
return parseDocsArgs(argv.slice(1));
|
|
114
117
|
case "msg":
|
|
@@ -131,10 +134,14 @@ export function parseLocalArgs(argv) {
|
|
|
131
134
|
return parseCareersArgs(argv.slice(1));
|
|
132
135
|
case "rename":
|
|
133
136
|
return parseRenameArgs(argv.slice(1));
|
|
137
|
+
case "setup":
|
|
138
|
+
return parseSetupArgs(argv.slice(1));
|
|
134
139
|
case "usage":
|
|
135
140
|
return parseUsageArgs(argv.slice(1));
|
|
136
141
|
case "logs":
|
|
137
142
|
return parseLogsArgs(argv.slice(1));
|
|
143
|
+
case "brain":
|
|
144
|
+
return parseBrainArgs(argv.slice(1));
|
|
138
145
|
default:
|
|
139
146
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
140
147
|
}
|
|
@@ -44,6 +44,23 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
44
44
|
"The bli-tower MCP server has the twin: logs_digest.",
|
|
45
45
|
],
|
|
46
46
|
],
|
|
47
|
+
[
|
|
48
|
+
"brain",
|
|
49
|
+
[
|
|
50
|
+
"Usage: cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
51
|
+
"",
|
|
52
|
+
"Tower's incident queue. Every 15 minutes the log-digest cron turns each recurring problem in a project's logs (classes",
|
|
53
|
+
"our_bug, config_drift, capacity, unknown) into ONE incident with ONE Tower issue; a repeat adds to its count, never a twin.",
|
|
54
|
+
"A template quiet for 24 hours is marked cleared. This prints, per project, how many incidents are open, being worked,",
|
|
55
|
+
"waiting on a person, fixed, not actionable or cleared, the newest ones in the --show status (default open) with their Tower",
|
|
56
|
+
"issue, how often each was seen and what it said, and the last 7 days of verdicts. Needs only this machine's Tower login.",
|
|
57
|
+
"--project <id> one project (`cockpit logs --list` names them).",
|
|
58
|
+
"--show <status> list the newest incidents in that status instead of open.",
|
|
59
|
+
"--json writes one object to stdout: { ok, generated_at, projects[{ project, counts, incidents[], verdicts_7d }], totals, coverage, text };",
|
|
60
|
+
"a refusal is { ok: false, error, detail } and exits 1 (unknown_project, invalid_status, stored_read_failed, not_paired, ...).",
|
|
61
|
+
"Teaching: docs/architecture/tower-brain.md. The bli-tower MCP server has the twin: brain_status.",
|
|
62
|
+
],
|
|
63
|
+
],
|
|
47
64
|
[
|
|
48
65
|
"github",
|
|
49
66
|
[
|
|
@@ -341,7 +341,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
341
341
|
[
|
|
342
342
|
"notes",
|
|
343
343
|
[
|
|
344
|
-
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]] [flags]",
|
|
344
|
+
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>|accounts|connect|detach <id>|sync <id>|sync-status [<id>]|recordings <note-id>|recording <id>] [flags]",
|
|
345
345
|
"Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 4 MB. --wait polls transcription for at most 330 seconds.",
|
|
346
346
|
"",
|
|
347
347
|
"folders [--tree] [--json]: list folders and visible note counts.",
|
|
@@ -362,10 +362,13 @@ export const TOWER_COMMAND_HELP = [
|
|
|
362
362
|
"move <id> (--to \"<shelf>\"|--clear-shelf) — put it on a different shelf, or take the shelf off.",
|
|
363
363
|
"place <id> [--apply] — say which shelf this note belongs on and why, from the meeting it is part of, who was in the room and its own name. Moves nothing without --apply, and never overrules a shelf somebody typed.",
|
|
364
364
|
"",
|
|
365
|
-
"Your own notetaker (BLI-4383). Fellow and
|
|
366
|
-
"accounts [--json] — the notetakers you have connected, with health and when each was last read.",
|
|
365
|
+
"Your own notetaker (BLI-4383). Fellow, Circleback and Wispr Flow today; Otter needs an Enterprise plan and Granola still runs on its own cron.",
|
|
366
|
+
"accounts [--json] — the notetakers you have connected, with health and when each was last read, and under it who on the team feeds Tower with which notetaker (a member with none is named: that is the gap).",
|
|
367
|
+
"recordings <note-id> [--json] — every recording of the meeting this note is. When several notetakers or people recorded one meeting, the library shows ONE note, the recording with the longest transcript; the others are alternates kept by id (BLI-12926).",
|
|
368
|
+
"recording <recording-id> [--json] — one recording, text and all, shown or alternate.",
|
|
367
369
|
"accounts --show-webhook <account-id> — that account's webhook URL, where to paste it at the vendor, and whether a signing secret is stored (BLI-4394). Until one is, every request to that URL is refused, which is the right way to be off.",
|
|
368
370
|
"webhook <account-id> --secret-stdin — store the VENDOR's webhook signing secret. THE SECRET IS READ FROM STDIN, never a flag: printf \"%s\" \"<signing secret>\" | cockpit notes webhook <account-id> --secret-stdin",
|
|
371
|
+
"connect --provider wisprflow [--name <n>] — Wispr Flow has no API key: this prints an address and a code, you approve in your browser (Google, Apple, Microsoft or company single sign-on), and Tower reads your Notetaker meetings from then on. Needs Notetaker and MCP access on your Wispr account (BLI-12926).",
|
|
369
372
|
"connect --provider fellow|circleback [--workspace <subdomain>] --key-stdin [--name <n>] — connect one. Fellow needs --workspace, its API is per workspace. THE API KEY IS READ FROM STDIN, never a flag: printf \"%s\" \"<key>\" | cockpit notes connect --provider fellow --workspace <subdomain> --key-stdin",
|
|
370
373
|
"detach <account-id> — take it off. The stored key goes with it; the meetings it already filed stay.",
|
|
371
374
|
"sync <account-id> — read that notetaker now. Prints the run's own counts and reason label, and exits 1 when the pass failed.",
|
|
@@ -380,7 +383,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
380
383
|
[
|
|
381
384
|
"memory",
|
|
382
385
|
[
|
|
383
|
-
"Usage: cockpit memory [search|save|update|forget|profile|consolidation|requeue|install|status|log] [flags]",
|
|
386
|
+
"Usage: cockpit memory [search|save|update|forget|private|public|profile|consolidation|requeue|install|status|log] [flags]",
|
|
384
387
|
"",
|
|
385
388
|
"The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
|
|
386
389
|
" search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
|
|
@@ -395,6 +398,11 @@ export const TOWER_COMMAND_HELP = [
|
|
|
395
398
|
" update <id> [--file <path>] replacement text on stdin; the old row is kept as history.",
|
|
396
399
|
" forget <id> [--reason <label>]",
|
|
397
400
|
" forget --content-stdin --container <tag> [--reason <label>] forget by exact text.",
|
|
401
|
+
" Memory is public to every member and super admin, but only a memory's owner",
|
|
402
|
+
" (or a super admin) can update or forget it.",
|
|
403
|
+
" private <id> only you and super admins can read this memory.",
|
|
404
|
+
" public <id> every member can read it again; this is how every memory starts.",
|
|
405
|
+
" A teammate's memory in a search says who filed it and on what day.",
|
|
398
406
|
" profile [--container <tag>] the compiled one-page briefing; a read, never a compile.",
|
|
399
407
|
" consolidation [--container <tag>] what the nightly tidy-up pass did.",
|
|
400
408
|
" Clusters looked at, edits by verb, refusals by reason, model and cost.",
|
|
@@ -22,10 +22,12 @@ import { TOWER_COMMAND_HELP } from "./local-help-commands-tower.js";
|
|
|
22
22
|
import { TOWER_OPS_COMMAND_HELP } from "./local-help-commands-tower-ops.js";
|
|
23
23
|
export function localSubcommandHelp(command) {
|
|
24
24
|
const helpByCommand = new Map([
|
|
25
|
+
["glossary", ["Usage: cockpit glossary list|add <wrong> --to <name>|remove <wrong> [--json]", "Quote phrases. Removing a form prevents future corrections of it."]],
|
|
25
26
|
["roots", ["Usage: cockpit roots add <dir> [--allow-home-root] [--json]", "Approves a work directory, saves an approval receipt, and adds it to collection. Run cockpit doctor afterward."]],
|
|
26
27
|
// BLI-3709: the Tower nouns live next door, split along the same seam the
|
|
27
28
|
// argument parsers already use — see `local-help-commands-tower.ts`.
|
|
28
29
|
...TOWER_COMMAND_HELP,
|
|
30
|
+
["setup", ["Usage: cockpit setup [--person <email>] [--compare <email,email>] [--json]", "Read agent setups beside 30-day outcomes, or compare two exact emails."]],
|
|
29
31
|
[
|
|
30
32
|
"onboard",
|
|
31
33
|
[
|
|
@@ -191,6 +193,7 @@ export function localSubcommandHelp(command) {
|
|
|
191
193
|
" cockpit team role <userId> --role <role> [--yes]",
|
|
192
194
|
" Admin only. Asks first unless --yes. You cannot change your own role.",
|
|
193
195
|
" cockpit team device [list] [--json]",
|
|
196
|
+
" cockpit team device doctor <id|name> [--json]",
|
|
194
197
|
" Every paired collector device: name, id, trust state, owner, and why",
|
|
195
198
|
" (revoked reason, or when it was approved, or that it's still waiting).",
|
|
196
199
|
" Admin only.",
|