@bli-cockpit/cli 0.2.180 → 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/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-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 +3 -1
- package/dist/commands/local-args.js +6 -1
- 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 +5 -1
- package/dist/commands/local.js +10 -7
- 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/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/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
|
+
}
|
|
@@ -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) {
|
|
@@ -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,8 +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";
|
|
42
43
|
export { parseBrainArgs } from "./local-args-tower-brain.js";
|
|
43
44
|
export { parseCareersArgs } from './local-args-tower-careers.js';
|
|
44
|
-
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, parseBrainArgs, 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,6 +134,8 @@ 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":
|
|
@@ -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.",
|
|
@@ -66,6 +66,7 @@ export const rootCommandNames = new Set([
|
|
|
66
66
|
"memory",
|
|
67
67
|
"clean",
|
|
68
68
|
"sources",
|
|
69
|
+
"glossary",
|
|
69
70
|
"docs",
|
|
70
71
|
"msg",
|
|
71
72
|
"issue",
|
|
@@ -77,6 +78,7 @@ export const rootCommandNames = new Set([
|
|
|
77
78
|
"release",
|
|
78
79
|
"careers",
|
|
79
80
|
"rename",
|
|
81
|
+
"setup",
|
|
80
82
|
"usage",
|
|
81
83
|
"logs",
|
|
82
84
|
"brain",
|
|
@@ -113,7 +115,7 @@ export function localCommandHelp(command) {
|
|
|
113
115
|
" cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
|
|
114
116
|
" cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
|
|
115
117
|
" cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>|place <id>] [--folder <path>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--apply] [--yes] [--dashboard-url <url>] [--json]",
|
|
116
|
-
" cockpit notes [accounts [--show-webhook <account-id>]|connect --provider fellow --workspace <sub> --key-stdin [--name <n>]|webhook <account-id> --secret-stdin|detach <account-id>|sync <account-id>] [--json] (the key and the signing secret are read from stdin, never a flag)",
|
|
118
|
+
" cockpit notes [accounts [--show-webhook <account-id>]|connect --provider fellow --workspace <sub> --key-stdin [--name <n>]|connect --provider wisprflow|recordings <note-id>|recording <id>|webhook <account-id> --secret-stdin|detach <account-id>|sync <account-id>] [--json] (the key and the signing secret are read from stdin, never a flag)",
|
|
117
119
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--lock-wait <seconds>] [--yes] [--workspace <path>] [--json]",
|
|
118
120
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
119
121
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -133,9 +135,11 @@ export function localCommandHelp(command) {
|
|
|
133
135
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
134
136
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
135
137
|
" cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|interview <id> <--at <iso>|--clear>|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
|
|
138
|
+
" cockpit glossary list|add <wrong> --to <name>|remove <wrong> [--dashboard-url <url>] [--json]",
|
|
136
139
|
" cockpit rename [list|resolve <id> --scope <page|everywhere|undo>] [--dashboard-url <url>] [--json]",
|
|
137
140
|
" cockpit logs <collector|dashboard|railway|railway:<service>|-> [--since 15m|1h|2d|<iso>] [--raw <template-id> [--limit <n>]] [--all-levels] [--table <file>] [--json]",
|
|
138
141
|
" cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
142
|
+
" cockpit setup [--person <email>] [--compare <email,email>] [--json]",
|
|
139
143
|
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
140
144
|
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
141
145
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
package/dist/commands/local.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runGlossary } from "./glossary.js";
|
|
1
2
|
import { defaultIo, errorMessage, writeLine } from "./cli-io.js";
|
|
2
3
|
import { isLocalHelpRequest, localCommandHelp, rootCommandNames } from "./local-help.js";
|
|
3
4
|
import { beginStage, endStage } from "../crash-guard.js";
|
|
@@ -11,6 +12,7 @@ import { runLogin } from "./login.js";
|
|
|
11
12
|
import { runLogout } from "./logout.js";
|
|
12
13
|
import { runStart } from "./start.js";
|
|
13
14
|
import { runSync } from "./sync.js";
|
|
15
|
+
import { runDoctorAfterSync } from "./sync-doctor.js";
|
|
14
16
|
import { runAnalyze } from "./analyze.js";
|
|
15
17
|
import { runBackfillCommand } from "./backfill.js";
|
|
16
18
|
import { runDoctor } from "./doctor.js";
|
|
@@ -48,6 +50,7 @@ import { runGithub } from "./github.js";
|
|
|
48
50
|
import { runSearch } from "./search.js";
|
|
49
51
|
import { runCareers } from "./careers.js";
|
|
50
52
|
import { runRename } from "./rename-scope.js";
|
|
53
|
+
import { runSetup } from "./setup.js";
|
|
51
54
|
import { runUsage } from "./usage.js";
|
|
52
55
|
import { runLogs } from "./logs.js";
|
|
53
56
|
import { runBrain } from "./brain.js";
|
|
@@ -61,7 +64,7 @@ export { classifySyncFailureRecords, classifySyncHealthError, redactedSyncErrorD
|
|
|
61
64
|
export { assertCollectionRootPersisted } from "./collection-roots.js";
|
|
62
65
|
export { runSelfUpdate, SelfUpdateError, } from "./install-update.js";
|
|
63
66
|
import { runRoots } from "./roots.js";
|
|
64
|
-
export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
67
|
+
export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorAfterSync }) {
|
|
65
68
|
// BLI-10450: `--quiet` is a global toggle, stripped here so no per-command
|
|
66
69
|
// parser's `allowedFlags` needs to learn it, and mirrored onto
|
|
67
70
|
// `COCKPIT_QUIET` so `tower-client.ts` can react without every call site
|
|
@@ -220,6 +223,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
220
223
|
return await runClean(command, io);
|
|
221
224
|
case "sources":
|
|
222
225
|
return await runSourcesCheck(command, io);
|
|
226
|
+
case "glossary":
|
|
227
|
+
return await runGlossary(command, io);
|
|
223
228
|
case "docs":
|
|
224
229
|
return await runDocs(command, io);
|
|
225
230
|
case "msg":
|
|
@@ -242,12 +247,10 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
242
247
|
return await runCareers(command, io);
|
|
243
248
|
case "rename":
|
|
244
249
|
return await runRename(command, io);
|
|
245
|
-
case "
|
|
246
|
-
|
|
247
|
-
case "logs":
|
|
248
|
-
|
|
249
|
-
case "brain":
|
|
250
|
-
return await runBrain(command, io);
|
|
250
|
+
case "setup": return await runSetup(command, io);
|
|
251
|
+
case "usage": return await runUsage(command, io);
|
|
252
|
+
case "logs": return await runLogs(command, io);
|
|
253
|
+
case "brain": return await runBrain(command, io);
|
|
251
254
|
}
|
|
252
255
|
}
|
|
253
256
|
catch (error) {
|