@bli-cockpit/cli 0.2.199 → 0.2.201
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/doctor-report.js +1 -0
- package/dist/commands/doctor-slack.js +25 -0
- package/dist/commands/doctor.js +3 -0
- package/dist/commands/local-args-tower-admin.js +11 -3
- package/dist/commands/local-args-tower-memory.js +3 -2
- package/dist/commands/local-help-commands-tower.js +1 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/memory-rules.js +23 -12
- package/dist/commands/memory-store.js +1 -0
- package/dist/commands/memory-verb-list.js +3 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/slack.js +24 -0
- package/package.json +2 -2
|
@@ -164,6 +164,7 @@ export function repairReceiptLines(repairs) {
|
|
|
164
164
|
}
|
|
165
165
|
export function doctorNeedsPerson(rows, receipt) {
|
|
166
166
|
const defaults = {
|
|
167
|
+
"slack-connected": "cockpit slack connect",
|
|
167
168
|
"cli-latest": "npm i -g @bli-cockpit/cli && cockpit doctor",
|
|
168
169
|
"authed": "cockpit doctor",
|
|
169
170
|
"roots-ok": "cockpit doctor",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
2
|
+
import { asRecord, callTower, openTower } from "./tower-command.js";
|
|
3
|
+
export async function checkSlackConnected(context) {
|
|
4
|
+
const message = "Slack connected for private-channel coverage";
|
|
5
|
+
try {
|
|
6
|
+
const tower = await openTower("doctor", context.command, context.io);
|
|
7
|
+
const response = await callTower(tower, { path: "/api/slack/connections", label: "doctor-slack", timeoutMs: 5_000 });
|
|
8
|
+
if (!response.ok)
|
|
9
|
+
return { id: "slack-connected", status: "needs_fix", code: response.reason, message: `${message}: could not read connection state.`, nextAction: `${tower.dashboardUrl}/settings/personal` };
|
|
10
|
+
const connections = asRecord(response.body)["connections"];
|
|
11
|
+
if (!connections || connections.length !== 2)
|
|
12
|
+
throw new Error("connection_state_incomplete");
|
|
13
|
+
const missing = connections.filter(row => !row["connected"]);
|
|
14
|
+
const links = missing.map(row => new URL(String(row["connectPath"]), tower.dashboardUrl).toString());
|
|
15
|
+
console.error("[doctor slack] checked", JSON.stringify({ missing: missing.length, reason: missing.length ? "slack_connect_required" : "ok" }));
|
|
16
|
+
return { id: "slack-connected", status: missing.length ? "needs_fix" : "ok", code: missing.length ? "slack_connect_required" : "slack_connected",
|
|
17
|
+
message: missing.length ? `${message}: connect ${missing.map(row => row["workspace"]).join(", ")} at ${links.join(" or ")}` : message,
|
|
18
|
+
...(missing.length ? { nextAction: links.join(" ") } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error("[doctor slack] refused", JSON.stringify({ reason: "connection_read_failed" }));
|
|
23
|
+
return { id: "slack-connected", status: "skipped", code: "connection_read_failed", message: `${message}: unavailable until this machine is signed in.`, nextAction: `${context.command.dashboardUrl ?? DEFAULT_DASHBOARD_URL}/settings/personal` };
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkSlackConnected } from "./doctor-slack.js";
|
|
1
2
|
import { checkUsageTelemetry, fixUsageTelemetry } from "./doctor-usage-telemetry.js";
|
|
2
3
|
import { LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
3
4
|
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
@@ -153,6 +154,7 @@ export function doctorInvariantTable() {
|
|
|
153
154
|
function doctorInvariants() {
|
|
154
155
|
return [
|
|
155
156
|
{ id: "cli-latest", check: checkCliLatest, fix: fixCliLatest },
|
|
157
|
+
{ id: "slack-connected", check: context => context.deps.checkSlackConnected ? context.deps.checkSlackConnected(context) : Promise.resolve({ id: "slack-connected", status: "skipped", code: "not_probed", message: "Slack connection check not provided." }) },
|
|
156
158
|
{
|
|
157
159
|
id: "authed",
|
|
158
160
|
check: (context) => context.deps.readAuth(context),
|
|
@@ -248,6 +250,7 @@ function defaultDoctorDeps(hooks) {
|
|
|
248
250
|
readRootsCoverWork,
|
|
249
251
|
resolveAndSaveRoots: (context) => hooks.resolveAndSaveRoots(context.command, context.io),
|
|
250
252
|
checkSingleInstall: checkSingleInstallState,
|
|
253
|
+
checkSlackConnected,
|
|
251
254
|
checkAutostart: checkAutostartState,
|
|
252
255
|
fixAutostart: fixAutostartState,
|
|
253
256
|
checkUsageTelemetry,
|
|
@@ -193,11 +193,11 @@ export function parseSlackArgs(args) {
|
|
|
193
193
|
],
|
|
194
194
|
});
|
|
195
195
|
if (values.positionals.length > 1) {
|
|
196
|
-
throw new Error("slack accepts one action: coverage or
|
|
196
|
+
throw new Error("slack accepts one action: coverage, read, connect or disconnect.");
|
|
197
197
|
}
|
|
198
198
|
const rawAction = values.positionals[0] ?? "coverage";
|
|
199
|
-
if (
|
|
200
|
-
throw new Error("slack action must be coverage or
|
|
199
|
+
if (!["coverage", "read", "connect", "disconnect"].includes(rawAction)) {
|
|
200
|
+
throw new Error("slack action must be coverage, read, connect or disconnect.");
|
|
201
201
|
}
|
|
202
202
|
const base = {
|
|
203
203
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
@@ -210,6 +210,14 @@ export function parseSlackArgs(args) {
|
|
|
210
210
|
if (values.booleans.has("--list") && rawAction !== "coverage") {
|
|
211
211
|
throw new Error("--list belongs to `cockpit slack coverage`.");
|
|
212
212
|
}
|
|
213
|
+
if (rawAction === "connect" || rawAction === "disconnect") {
|
|
214
|
+
const workspace = optionalNonEmpty(values.flags.get("--workspace"));
|
|
215
|
+
if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace))
|
|
216
|
+
throw new Error("Unknown Slack workspace.");
|
|
217
|
+
if (rawAction === "disconnect" && !workspace)
|
|
218
|
+
throw new Error("slack disconnect needs --workspace bli|blue_pearl.");
|
|
219
|
+
return { kind: "slack", action: rawAction, workspace, ...base };
|
|
220
|
+
}
|
|
213
221
|
if (rawAction === "coverage") {
|
|
214
222
|
// `--workspace` here is a Slack workspace key, NOT the collection-root
|
|
215
223
|
// `--workspace <path>` every setup command takes. Named at the point of use
|
|
@@ -35,6 +35,7 @@ import { parseMemoryRulesArgs } from "./memory-rules.js";
|
|
|
35
35
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
|
|
36
36
|
export const MEMORY_STORE_ACTIONS = new Set([
|
|
37
37
|
"rules",
|
|
38
|
+
"knowledge",
|
|
38
39
|
"search",
|
|
39
40
|
"save",
|
|
40
41
|
"update",
|
|
@@ -88,8 +89,8 @@ const VALUE_FLAGS = [
|
|
|
88
89
|
"--source",
|
|
89
90
|
];
|
|
90
91
|
export function parseMemoryStoreArgs(action, args) {
|
|
91
|
-
if (action === "rules")
|
|
92
|
-
return parseMemoryRulesArgs(args);
|
|
92
|
+
if (action === "rules" || action === "knowledge")
|
|
93
|
+
return parseMemoryRulesArgs(args, action);
|
|
93
94
|
const values = parseNamedArgs(args, { allowedFlags: ALLOWED_FLAGS, valueFlags: VALUE_FLAGS });
|
|
94
95
|
const rest = values.positionals.slice(1);
|
|
95
96
|
const containers = values.flagValues.get("--container") ?? [];
|
|
@@ -262,7 +262,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
262
262
|
[
|
|
263
263
|
"slack",
|
|
264
264
|
[
|
|
265
|
-
"Usage: cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [filters] [--format compact|full]] [--json]",
|
|
265
|
+
"Usage: cockpit slack [connect [--workspace bli|blue_pearl] | disconnect --workspace bli|blue_pearl | coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [filters] [--format compact|full]] [--json]",
|
|
266
266
|
"",
|
|
267
267
|
" cockpit slack coverage",
|
|
268
268
|
" What Cockpit's Slack bot can see: how many channels it is in, what each",
|
|
@@ -111,7 +111,7 @@ export function localCommandHelp(command) {
|
|
|
111
111
|
" cockpit models [list|show <provider:model>|compare <provider:model> <provider:model> [...]] [--highlight] [--dashboard-url <url>] [--json]",
|
|
112
112
|
" cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
|
|
113
113
|
" cockpit ops [status [--job <id>] [--skips] [--memory] [--models] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
|
|
114
|
-
" cockpit slack [coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [--person <p>] [--channel <c>] [--query <text> [--word]] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--format compact|full]] [--json]",
|
|
114
|
+
" cockpit slack [connect [--workspace bli|blue_pearl] | disconnect --workspace bli|blue_pearl | coverage [--workspace bli|blue_pearl] [--stale-only] [--list] | read [--person <p>] [--channel <c>] [--query <text> [--word]] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--format compact|full]] [--json]",
|
|
115
115
|
" cockpit settings [personal [--chat-model <key>] [--brief-model <key>] | switches [set <key> <value>] | models [set --chat <key>] [--memory <id>] | cli-floor [<version>] | env list|set --project <p> --file <f> --content-stdin|delete --id <uuid> [--yes]] [--json]",
|
|
116
116
|
" cockpit team [members | invite <email> --role <role> [--team-id <uuid>] | role <userId> --role <role> [--yes] | device [list] | device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]] [--json]",
|
|
117
117
|
" cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
2
|
import { writeLine } from "./cli-io.js";
|
|
3
3
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
4
|
-
export function parseMemoryRulesArgs(args) {
|
|
5
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--person", "--project", "--limit", "--json", "--home", "--dashboard-url"],
|
|
4
|
+
export function parseMemoryRulesArgs(args, action = "rules") {
|
|
5
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--person", "--project", "--limit", "--json", "--by-project", "--home", "--dashboard-url"],
|
|
6
6
|
valueFlags: ["--person", "--project", "--limit", "--home", "--dashboard-url"] });
|
|
7
7
|
if (values.positionals.slice(1).length)
|
|
8
|
-
throw new Error(
|
|
8
|
+
throw new Error(`memory ${action} takes no positionals; use --person and --project.`);
|
|
9
9
|
const raw = values.flags.get("--limit");
|
|
10
10
|
if (raw !== undefined && (!/^\d+$/.test(raw) || Number(raw) < 1 || Number(raw) > 1000))
|
|
11
11
|
throw new Error("--limit must be between 1 and 1000.");
|
|
12
|
-
return { kind: "memory-store", action: "
|
|
12
|
+
return { kind: "memory-store", action, byProject: values.booleans.has("--by-project"), containerTags: [],
|
|
13
13
|
person: optionalNonEmpty(values.flags.get("--person")), project: optionalNonEmpty(values.flags.get("--project")),
|
|
14
14
|
limit: raw === undefined ? undefined : Number(raw), json: values.booleans.has("--json"),
|
|
15
15
|
homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
@@ -22,21 +22,32 @@ export async function runMemoryRules(command, door) {
|
|
|
22
22
|
params.set("project", command.project);
|
|
23
23
|
if (command.limit !== undefined)
|
|
24
24
|
params.set("limit", String(command.limit));
|
|
25
|
-
const answer = await askAgentDoor(door, { path: `/api/memory
|
|
26
|
-
method: "GET", label:
|
|
25
|
+
const answer = await askAgentDoor(door, { path: `/api/memory/${command.action}${params.size ? `?${params}` : ""}`,
|
|
26
|
+
method: "GET", label: `memory ${command.action}`, timeoutMs: 60_000 });
|
|
27
27
|
if (!answer.ok)
|
|
28
28
|
return failAgentDoor(door, "[memory cli]", answer.reason, answer.detail);
|
|
29
29
|
const body = answer.body;
|
|
30
|
-
writeLine(door.io.stderr, `[memory cli]
|
|
30
|
+
writeLine(door.io.stderr, `[memory cli] ${command.action} ${JSON.stringify({ reason: body.capped ? "capped" : "exhausted", returned: body.returned, groups: body.projectGroups?.length ?? body.groups?.length ?? 0 })}`);
|
|
31
31
|
if (door.json)
|
|
32
32
|
return emitAgentDoor(door, answer.body);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
if (command.byProject || command.action === "knowledge") {
|
|
34
|
+
for (const group of body.projectGroups ?? []) {
|
|
35
|
+
writeLine(door.io.stdout, `${group.project} (${group.count}) : ${group.headline.split("\n")[0]}`);
|
|
36
|
+
for (const topic of group.topics) {
|
|
37
|
+
writeLine(door.io.stdout, ` ${topic.title}`);
|
|
38
|
+
for (const memory of topic.memories)
|
|
39
|
+
writeLine(door.io.stdout, ` ${memory.text}${memory.repetition > 1 ? ` (said ${memory.repetition} times)` : ""}${memory.importance !== null ? ` [importance ${memory.importance}/100]` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
37
42
|
}
|
|
43
|
+
else
|
|
44
|
+
for (const group of body.groups) {
|
|
45
|
+
writeLine(door.io.stdout, group.owner);
|
|
46
|
+
for (const rule of group.rules)
|
|
47
|
+
writeLine(door.io.stdout, ` [${rule.created_at.slice(0, 10)}] ${rule.project ? `[${rule.project}] ` : ""}${rule.text}${group.owner === "unattributed" ? ` (space: ${rule.container})` : ""}`);
|
|
48
|
+
}
|
|
38
49
|
if (!body.returned)
|
|
39
|
-
writeLine(door.io.stdout,
|
|
50
|
+
writeLine(door.io.stdout, `No ${command.action === "knowledge" ? "project knowledge" : "ground rules"} matched.`);
|
|
40
51
|
if (body.capped)
|
|
41
52
|
writeLine(door.io.stdout, "Result capped. Narrow by person or project, or increase --limit (max 1000).");
|
|
42
53
|
return 0;
|
|
@@ -65,6 +65,7 @@ const FORGET_MAX_CHARS = 10_000;
|
|
|
65
65
|
export async function runMemoryStore(command, io) {
|
|
66
66
|
const door = await openAgentDoor("memory", command, io);
|
|
67
67
|
switch (command.action) {
|
|
68
|
+
case "knowledge":
|
|
68
69
|
case "rules":
|
|
69
70
|
return runMemoryRules(command, door);
|
|
70
71
|
case "search":
|
|
@@ -26,7 +26,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
26
26
|
ok: true,
|
|
27
27
|
noun: "memory",
|
|
28
28
|
machine_verbs: ["install", "status"],
|
|
29
|
-
store_verbs: ["search", "save", "update", "forget", "private", "public", "profile", "consolidation", "requeue"],
|
|
29
|
+
store_verbs: ["rules", "knowledge", "search", "save", "update", "forget", "private", "public", "profile", "consolidation", "requeue"],
|
|
30
30
|
log_verbs: ["log"],
|
|
31
31
|
}));
|
|
32
32
|
return 0;
|
|
@@ -44,7 +44,8 @@ export function runMemoryVerbList(io, json) {
|
|
|
44
44
|
" you can forget or update only your own memories (a super admin, any)",
|
|
45
45
|
" cockpit memory private <id> [--json] only you and super admins can read this memory",
|
|
46
46
|
" cockpit memory public <id> [--json] every member can read it again (how every memory starts)",
|
|
47
|
-
" cockpit memory rules [--person <name>] [--project <name>] [--limit <n>] [--json]",
|
|
47
|
+
" cockpit memory rules [--person <name>] [--project <name-or-slug>] [--by-project] [--limit <n>] [--json]",
|
|
48
|
+
" cockpit memory knowledge [--person <name>] [--project <name-or-slug>] [--by-project] [--limit <n>] [--json]",
|
|
48
49
|
" cockpit memory profile [--container <tag>] [--json]",
|
|
49
50
|
" the compiled one-page briefing every session opens with; a read, never a compile",
|
|
50
51
|
" cockpit memory consolidation [--container <tag>] [--json]",
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.201");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/commands/slack.js
CHANGED
|
@@ -24,6 +24,24 @@ import { colorEnabled, dim, writeLine } from "./cli-io.js";
|
|
|
24
24
|
import { asRecord, callTower, openTower, writeCommandFailure } from "./tower-command.js";
|
|
25
25
|
export async function runSlack(command, io) {
|
|
26
26
|
const tower = await openTower("slack", command, io);
|
|
27
|
+
if (command.action === "connect" || command.action === "disconnect") {
|
|
28
|
+
const result = await callTower(tower, { path: `/api/slack/connections${command.action === "disconnect" ? `?workspace=${command.workspace}` : ""}`,
|
|
29
|
+
method: command.action === "disconnect" ? "DELETE" : "GET", label: "slack-connection" });
|
|
30
|
+
if (!result.ok)
|
|
31
|
+
return writeCommandFailure(io, command.json, result);
|
|
32
|
+
writeLine(io.stderr, `[slack cli] connection answered ${JSON.stringify({ action: command.action, workspace: command.workspace ?? "all", reason: "ok" })}`);
|
|
33
|
+
const body = asRecord(result.body);
|
|
34
|
+
if (command.json)
|
|
35
|
+
writeLine(io.stdout, JSON.stringify(body));
|
|
36
|
+
else if (command.action === "disconnect")
|
|
37
|
+
writeLine(io.stdout, `Disconnected Slack for ${command.workspace}.`);
|
|
38
|
+
else
|
|
39
|
+
for (const connection of (body["connections"] ?? [])) {
|
|
40
|
+
if (!command.workspace || command.workspace === connection["workspace"])
|
|
41
|
+
writeLine(io.stdout, `${connection["workspace"]}: ${connection["connected"] ? "connected" : "Connect Slack"} ${new URL(String(connection["connectPath"]), tower.dashboardUrl).toString()}`);
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
27
45
|
return command.action === "coverage"
|
|
28
46
|
? runCoverageAction(command, io, tower)
|
|
29
47
|
: runReadAction(command, io, tower);
|
|
@@ -86,6 +104,12 @@ function coverageLines(coverage, workspaces, staleOnly, list, paint) {
|
|
|
86
104
|
for (const workspace of workspaces) {
|
|
87
105
|
writeWorkspaceCoverageLines(lines, workspace, staleAfter, staleOnly, list, paint);
|
|
88
106
|
}
|
|
107
|
+
for (const entry of (coverage["privateCoverage"] ?? [])) {
|
|
108
|
+
lines.push(`${entry["workspace"]}: ${entry["botMemberCount"]}/${entry["privateChannelsKnown"]} known private channels have the bot; ${entry["peopleConnected"]} people connected; as of ${entry["asOf"] ?? "no scan yet"}`);
|
|
109
|
+
lines.push(` Not connected: ${(entry["peopleNotConnected"] ?? []).join(", ") || "none"}`);
|
|
110
|
+
}
|
|
111
|
+
if (coverage["privateCoverageError"])
|
|
112
|
+
lines.push(`Private coverage unavailable: ${coverage["privateCoverageError"]}`);
|
|
89
113
|
writeCoverageFooter(lines, coverage, paint);
|
|
90
114
|
return lines;
|
|
91
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.201",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.60",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.113",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.69"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|