@bli-cockpit/cli 0.2.200 → 0.2.202
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/issue.js +3 -0
- package/dist/commands/local-args-tower-admin.js +15 -1
- package/dist/commands/local-args-tower-logs.js +11 -0
- package/dist/commands/local-args-tower-memory.js +7 -2
- package/dist/commands/local-args-tower-usage.js +6 -6
- package/dist/commands/local-args-tower-work.js +16 -3
- package/dist/commands/local-help-commands-tower-ops.js +2 -1
- package/dist/commands/local-help-commands-tower.js +8 -5
- package/dist/commands/local-help-commands.js +7 -1
- package/dist/commands/local-help.js +3 -2
- package/dist/commands/logs.js +10 -0
- package/dist/commands/memory-map.js +29 -0
- package/dist/commands/memory-rules.js +23 -12
- package/dist/commands/memory-store.js +4 -0
- package/dist/commands/memory-verb-list.js +4 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/speed-pages.js +6 -9
- package/dist/commands/team.js +41 -0
- package/dist/commands/tickets-board.js +43 -0
- package/dist/commands/usage.js +7 -2
- package/package.json +4 -4
package/dist/commands/issue.js
CHANGED
|
@@ -29,12 +29,15 @@ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./ag
|
|
|
29
29
|
import { writeLine } from "./cli-io.js";
|
|
30
30
|
import { assigneeLabel, beginAssigneeDirectoryFetch, finishAssigneeDirectory, withAssigneeIdentity } from "./issue-directory.js";
|
|
31
31
|
import { cacheAgeLabel, cacheCoverage, readThroughCache } from "./read-cache.js";
|
|
32
|
+
import { readTicketBoard } from "./tickets-board.js";
|
|
32
33
|
import { showIssue } from "./issue-show.js";
|
|
33
34
|
import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
|
|
34
35
|
import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
|
|
35
36
|
export async function runIssue(command, io) {
|
|
36
37
|
const door = await openAgentDoor("issue", command, io);
|
|
37
38
|
switch (command.action) {
|
|
39
|
+
case "board":
|
|
40
|
+
return readTicketBoard(command, door);
|
|
38
41
|
case "list":
|
|
39
42
|
return listIssues(command, door);
|
|
40
43
|
case "show":
|
|
@@ -293,9 +293,23 @@ export function parseTeamArgs(args) {
|
|
|
293
293
|
json: values.booleans.has("--json"),
|
|
294
294
|
};
|
|
295
295
|
const [verb, ...rest] = values.positionals;
|
|
296
|
+
if (verb === "github-login") {
|
|
297
|
+
const [operation, personRef, githubLogin, ...extra] = rest;
|
|
298
|
+
if (!["add", "remove", "list"].includes(operation ?? "") || !personRef || extra.length ||
|
|
299
|
+
(operation === "list" ? githubLogin !== undefined : !githubLogin)) {
|
|
300
|
+
throw new Error("team github-login takes add|remove <person> <login>, or list <person>.");
|
|
301
|
+
}
|
|
302
|
+
return { kind: "team", action: `github-login-${operation}`, personRef, githubLogin, ...common };
|
|
303
|
+
}
|
|
296
304
|
if (verb === "device") {
|
|
297
305
|
return parseTeamDeviceArgs(rest, values.flags, common);
|
|
298
306
|
}
|
|
307
|
+
if (verb === "grant" || verb === "revoke") {
|
|
308
|
+
const [person, section, level = "see", ...extra] = rest;
|
|
309
|
+
if (!person || section !== "careers" || !["see", "act"].includes(level) || extra.length)
|
|
310
|
+
throw new Error("team grant|revoke <person uuid> careers [see|act]");
|
|
311
|
+
return { kind: "team", action: verb, targetUserId: person, section: "careers", level: level, ...common };
|
|
312
|
+
}
|
|
299
313
|
const [subject, ...extra] = rest;
|
|
300
314
|
if (extra.length > 0)
|
|
301
315
|
throw new Error("team does not accept that many arguments.");
|
|
@@ -321,7 +335,7 @@ export function parseTeamArgs(args) {
|
|
|
321
335
|
throw new Error("team role needs --role <role>.");
|
|
322
336
|
return { kind: "team", action: "role", targetUserId: subject, role, ...common };
|
|
323
337
|
}
|
|
324
|
-
throw new Error("team takes one verb: members, invite, role, or device.");
|
|
338
|
+
throw new Error("team takes one verb: members, invite, role, grant, revoke, or device.");
|
|
325
339
|
}
|
|
326
340
|
/**
|
|
327
341
|
* `cockpit team device [list] [--json]` or
|
|
@@ -22,6 +22,17 @@ export const LOGS_SOURCES = ["collector", "dashboard", "railway", "railway:<serv
|
|
|
22
22
|
const LOCAL_SOURCE_PATTERN = /^(collector|dashboard|railway|railway:[A-Za-z0-9][A-Za-z0-9._-]{0,63}|-)$/;
|
|
23
23
|
const SINCE_PATTERN = /^(\d{1,4}[smhd]|\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})?)?)$/;
|
|
24
24
|
export function parseLogsArgs(args) {
|
|
25
|
+
if (args[0] === "keys") {
|
|
26
|
+
const action = args[1];
|
|
27
|
+
const source = args[2];
|
|
28
|
+
if ((action !== "mint" && action !== "rotate") || !source)
|
|
29
|
+
throw new Error("Use cockpit logs keys mint|rotate <source>.");
|
|
30
|
+
const values = parseNamedArgs(args.slice(3), { allowedFlags: ["--json", "--home", "--dashboard-url"], valueFlags: ["--home", "--dashboard-url"] });
|
|
31
|
+
if (values.positionals.length)
|
|
32
|
+
throw new Error("Unexpected logs keys argument.");
|
|
33
|
+
return { kind: "logs", source, keyAction: action, stored: false, list: false, allLevels: false,
|
|
34
|
+
json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
35
|
+
}
|
|
25
36
|
const values = parseNamedArgs(args, {
|
|
26
37
|
allowedFlags: ["--home", "--since", "--raw", "--limit", "--table", "--all-levels", "--stored", "--list", "--dashboard-url", "--json"],
|
|
27
38
|
valueFlags: ["--home", "--since", "--raw", "--limit", "--table", "--dashboard-url"],
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
* MCP server and three hooks) and these act on Tower. `memory-store.ts` runs
|
|
32
32
|
* them; `parseMemoryArgs` in `local-args-collector-status.ts` routes to here.
|
|
33
33
|
*/
|
|
34
|
+
import { parseMemoryMapArgs } from "./memory-map.js";
|
|
34
35
|
import { parseMemoryRulesArgs } from "./memory-rules.js";
|
|
35
36
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
|
|
36
37
|
export const MEMORY_STORE_ACTIONS = new Set([
|
|
38
|
+
"map",
|
|
37
39
|
"rules",
|
|
40
|
+
"knowledge",
|
|
38
41
|
"search",
|
|
39
42
|
"save",
|
|
40
43
|
"update",
|
|
@@ -88,8 +91,10 @@ const VALUE_FLAGS = [
|
|
|
88
91
|
"--source",
|
|
89
92
|
];
|
|
90
93
|
export function parseMemoryStoreArgs(action, args) {
|
|
91
|
-
if (action === "
|
|
92
|
-
return
|
|
94
|
+
if (action === "map")
|
|
95
|
+
return parseMemoryMapArgs(args);
|
|
96
|
+
if (action === "rules" || action === "knowledge")
|
|
97
|
+
return parseMemoryRulesArgs(args, action);
|
|
93
98
|
const values = parseNamedArgs(args, { allowedFlags: ALLOWED_FLAGS, valueFlags: VALUE_FLAGS });
|
|
94
99
|
const rest = values.positionals.slice(1);
|
|
95
100
|
const containers = values.flagValues.get("--container") ?? [];
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseUsageArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--by-task", "--task", "--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--task", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--by-task", "--task", "--by-subject", "--subject", "--tool", "--cursor", "--limit", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--task", "--subject", "--tool", "--cursor", "--limit", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
4
|
if (["--by-task", "--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
|
|
5
5
|
throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject or --by-task.");
|
|
6
6
|
const minConfidence = values.flags.get("--min-confidence");
|
|
7
7
|
if (minConfidence !== undefined && !["high", "medium", "low"].includes(minConfidence))
|
|
8
8
|
throw new Error("--min-confidence accepts high, medium or low.");
|
|
9
9
|
const action = values.positionals[0] ?? "people";
|
|
10
|
-
if (!["people", "sessions"].includes(action) || values.positionals.length > 1)
|
|
11
|
-
throw new Error("usage takes people or
|
|
12
|
-
if (action === "
|
|
13
|
-
throw new Error("usage
|
|
14
|
-
return { byTask: values.booleans.has("--by-task"), task: optionalNonEmpty(values.flags.get("--task")), bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
10
|
+
if (!["people", "sessions", "session"].includes(action) || values.positionals.length > (action === "session" ? 2 : 1))
|
|
11
|
+
throw new Error("usage takes people, sessions or session <id>.");
|
|
12
|
+
if (action === "session" && !values.positionals[1])
|
|
13
|
+
throw new Error("usage session requires an id.");
|
|
14
|
+
return { byTask: values.booleans.has("--by-task"), task: optionalNonEmpty(values.flags.get("--task")), bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), tool: optionalNonEmpty(values.flags.get("--tool")), cursor: optionalNonEmpty(values.flags.get("--cursor")), limit: optionalNonEmpty(values.flags.get("--limit")), session: action === "session" ? values.positionals[1] : optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: (action === "session" ? "sessions" : action), since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
15
15
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
19
19
|
import { estimateForSize } from "./issue-contracts.js";
|
|
20
20
|
const ISSUE_ACTIONS = new Set([
|
|
21
|
+
"board",
|
|
21
22
|
"list",
|
|
22
23
|
"show",
|
|
23
24
|
"create",
|
|
@@ -52,6 +53,8 @@ export function parseIssueArgs(args) {
|
|
|
52
53
|
"--state",
|
|
53
54
|
"--assignee",
|
|
54
55
|
"--project",
|
|
56
|
+
"--label",
|
|
57
|
+
"--rows",
|
|
55
58
|
"--limit",
|
|
56
59
|
"--title",
|
|
57
60
|
"--priority",
|
|
@@ -67,6 +70,8 @@ export function parseIssueArgs(args) {
|
|
|
67
70
|
"--state",
|
|
68
71
|
"--assignee",
|
|
69
72
|
"--project",
|
|
73
|
+
"--label",
|
|
74
|
+
"--rows",
|
|
70
75
|
"--limit",
|
|
71
76
|
"--title",
|
|
72
77
|
"--priority",
|
|
@@ -80,6 +85,12 @@ export function parseIssueArgs(args) {
|
|
|
80
85
|
validateIssueState(moveState, "issue move state must be one of: ");
|
|
81
86
|
const stateFilter = readIssueStateFilter(action, values);
|
|
82
87
|
const title = readIssueTitle(action, values);
|
|
88
|
+
if (action !== "board" && (values.flags.has("--label") || values.flags.has("--rows"))) {
|
|
89
|
+
throw new Error("--label and --rows filter cockpit tickets board only.");
|
|
90
|
+
}
|
|
91
|
+
const boardProject = optionalNonEmpty(values.flags.get("--project"));
|
|
92
|
+
if (action === "board" && boardProject && !UUID_PATTERN.test(boardProject))
|
|
93
|
+
throw new Error("board --project requires a uuid.");
|
|
83
94
|
const priority = readIssuePriority(values);
|
|
84
95
|
const estimate = readIssueSize(values);
|
|
85
96
|
const assignee = readIssueAssignee(action, values);
|
|
@@ -90,7 +101,7 @@ function readIssueAction(values) {
|
|
|
90
101
|
const first = values.positionals[0];
|
|
91
102
|
const action = (first === undefined ? "list" : first);
|
|
92
103
|
if (!ISSUE_ACTIONS.has(action)) {
|
|
93
|
-
throw new Error(`Unknown issue command: ${first}. Try list, show, create, update, move, comment, or history.`);
|
|
104
|
+
throw new Error(`Unknown issue command: ${first}. Try board, list, show, create, update, move, comment, or history.`);
|
|
94
105
|
}
|
|
95
106
|
return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
|
|
96
107
|
}
|
|
@@ -137,7 +148,7 @@ function validateIssueState(state, errorPrefix) {
|
|
|
137
148
|
function readIssueStateFilter(action, values) {
|
|
138
149
|
const stateFilter = optionalNonEmpty(values.flags.get("--state"));
|
|
139
150
|
validateIssueState(stateFilter, "--state must be one of: ");
|
|
140
|
-
if (stateFilter !== undefined && action !== "list") {
|
|
151
|
+
if (stateFilter !== undefined && action !== "list" && action !== "board") {
|
|
141
152
|
throw new Error("--state filters `cockpit issue list`; to move an issue use `cockpit issue move <id> <state>`.");
|
|
142
153
|
}
|
|
143
154
|
return stateFilter;
|
|
@@ -173,7 +184,7 @@ function readIssueAssignee(action, values) {
|
|
|
173
184
|
if (assignee !== undefined && assignee !== "me" && assignee !== "unassigned" && !UUID_PATTERN.test(assignee)) {
|
|
174
185
|
throw new Error('--assignee must be "me", "unassigned", or a person\'s uuid (see `cockpit team`).');
|
|
175
186
|
}
|
|
176
|
-
if (assignee === "unassigned" && action !== "list") {
|
|
187
|
+
if (assignee === "unassigned" && action !== "list" && action !== "board") {
|
|
177
188
|
throw new Error('--assignee unassigned filters `cockpit issue list`; to clear an assignee pass --assignee "" is not supported yet.');
|
|
178
189
|
}
|
|
179
190
|
return assignee;
|
|
@@ -196,6 +207,8 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
|
|
|
196
207
|
...(moveState ? { moveState } : {}),
|
|
197
208
|
...(stateFilter ? { stateFilter } : {}),
|
|
198
209
|
...(assignee ? { assignee } : {}),
|
|
210
|
+
label: optionalNonEmpty(values.flags.get("--label")),
|
|
211
|
+
rows: optionalNonEmpty(values.flags.get("--rows")),
|
|
199
212
|
project: optionalNonEmpty(values.flags.get("--project")),
|
|
200
213
|
...(limit === undefined ? {} : { limit }),
|
|
201
214
|
title,
|
|
@@ -41,7 +41,8 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
41
41
|
"project unknown_project, since_exceeds_retention, stored_read_failed, not_paired, ...",
|
|
42
42
|
"--json writes one object to stdout: { ok, kind, source, window, lines, templates[], routine, notes, text, measure };",
|
|
43
43
|
"a refusal is { ok: false, error, detail } and exits 1. Run list and teaching: docs/runbooks/tower-logs.md.",
|
|
44
|
-
"
|
|
44
|
+
"cockpit logs keys mint|rotate <source> [--json]: super admin only; prints the source ingest key once. Rotation invalidates the old key.",
|
|
45
|
+
"The bli-tower MCP server has the twins: logs_digest and logs_key_mint.",
|
|
45
46
|
],
|
|
46
47
|
],
|
|
47
48
|
[
|
|
@@ -93,10 +93,11 @@ export const TOWER_COMMAND_HELP = [
|
|
|
93
93
|
[
|
|
94
94
|
"issue",
|
|
95
95
|
[
|
|
96
|
-
"Usage: cockpit issue [list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>] [flags]",
|
|
96
|
+
"Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>] [flags]",
|
|
97
97
|
"",
|
|
98
98
|
"Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
|
|
99
99
|
"list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] — the issues you may see, most recently updated first. Each row carries assignee_name/assignee_email alongside the bare assignee_id (BLI-10450, one extra `cockpit team members` call for the whole page, never per row), and a `size` label (S/M/L) alongside the raw `estimate` when it matches Tower's own scale (BLI-10575). --json shape: { issues: [{ id, identifier, title, state, priority, estimate, size, assignee_id, assignee_name, assignee_email, project_id, parent_id, updated_at }] }.",
|
|
100
|
+
"board [--json] [--state <s>] [--assignee me|unassigned|<uuid>] [--project <uuid>] [--label <name>] [--rows <state:n,...>] — the Tickets board, counts, coverage and top five cards per column, through GET /api/work/board. Also cockpit tickets board.",
|
|
100
101
|
"show <id> [<id>...] — one or more issues: title, state, priority, size, assignee (name and email, not just its id), description, and every comment on each. Several ids read in one call — one bad id never loses the rest.",
|
|
101
102
|
" --json shape: one id answers { issue, comments }, unchanged from before BLI-10450 (plus `estimate`/`size`, BLI-10575). Several ids answer { issues: [{ ref, ok, issue, comments } | { ref, ok: false, error, detail }] }.",
|
|
102
103
|
"create --title \"<title>\" [--project <name|id>] [--priority 0-4] [--size S|M|L] [--assignee me|<uuid>] [--parent <id>] [--file <path>] — the DESCRIPTION comes from --file or stdin (`cat plan.md | cockpit issue create --title \"...\"`); neither means no description. --size maps onto the existing `estimate` column, Tower's own scale: S=1, M=3, L=5 (BLI-10575) — an estimate outside that scale (an imported Linear point value, say) just has no size label on a read. --json shape: { issue }.",
|
|
@@ -189,7 +190,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
189
190
|
"(nearest rank). Also the HTTP status, the response size and the door's own Server-Timing total.",
|
|
190
191
|
"Slowest first. A page your account may not open (Careers for a member) reads \"not permitted\", not as a failure.",
|
|
191
192
|
"--page <name> limits the run to one page; repeat it for several. Names: today, meeting-notes, careers, scout,",
|
|
192
|
-
"messages, mail, docs, memory, tickets, setup, usage, calendar, workbook, settings.",
|
|
193
|
+
"messages, mail, docs, memory-knowledge, memory-rules, tickets, tickets-list, setup, usage, calendar, workbook, settings.",
|
|
193
194
|
"--runs <n> sets the requests per page (default 5, at most 50).",
|
|
194
195
|
"--json writes one object to stdout ({ ok, runs, pages: [...] }); every log line stays on stderr.",
|
|
195
196
|
"It times the DATA DOOR the page opens on, not the page's HTML or paint, and it never uses the local read cache.",
|
|
@@ -207,8 +208,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
207
208
|
"usage",
|
|
208
209
|
[
|
|
209
210
|
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
210
|
-
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
211
|
-
" cockpit usage
|
|
211
|
+
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
212
|
+
" cockpit usage session <id> [--from-window <n>] [--json]",
|
|
212
213
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
213
214
|
"",
|
|
214
215
|
"--by-topic adds topic rows, including (unlabelled). Choose only one grouping. --by-repo adds project rows under each person (top 10; --all shows every repo). --person me uses your signed-in email.",
|
|
@@ -406,8 +407,10 @@ export const TOWER_COMMAND_HELP = [
|
|
|
406
407
|
[
|
|
407
408
|
"memory",
|
|
408
409
|
[
|
|
409
|
-
"Usage: cockpit memory [rules|search|save|update|forget|private|public|profile|consolidation|requeue|install|status|log] [flags]",
|
|
410
|
+
"Usage: cockpit memory [map|knowledge|rules|search|save|update|forget|private|public|profile|consolidation|requeue|install|status|log] [flags]",
|
|
410
411
|
"",
|
|
412
|
+
" map [--json] — projects, topics, memory counts and importance from the shared map.",
|
|
413
|
+
" knowledge [--person <name>] [--project <name>] [--by-project] [--limit <n>] [--json] — ranked project ideas.",
|
|
411
414
|
" rules [--person <name>] [--project <name>] [--limit <n>] [--json] — standing rules grouped by person.",
|
|
412
415
|
"The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
|
|
413
416
|
" search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
|
|
@@ -184,15 +184,21 @@ export function localSubcommandHelp(command) {
|
|
|
184
184
|
[
|
|
185
185
|
"team",
|
|
186
186
|
[
|
|
187
|
-
"Usage: cockpit team [members | invite <email> --role <role> | role <userId> --role <role> | device [list] | device revoke <id|name> --reason <label>] [--json]",
|
|
187
|
+
"Usage: cockpit team [members | invite <email> --role <role> | role <userId> --role <role> | grant|revoke <person> careers [see|act] | device [list] | device revoke <id|name> --reason <label>] [--json]",
|
|
188
188
|
"",
|
|
189
189
|
" cockpit team [members]",
|
|
190
190
|
" Everyone on your team — roster and accounts together, with each person's id.",
|
|
191
191
|
" Open to every role; it says so when the view is read-only for you.",
|
|
192
|
+
" cockpit team github-login add|remove <person> <login> [--json] — exact roster UUID, name or email; super admin or a member managing their own extra logins. History stays.",
|
|
193
|
+
" cockpit team github-login list <person> [--json] — primary and extra GitHub logins.",
|
|
192
194
|
" cockpit team invite <email> --role <role> [--team-id <uuid>]",
|
|
193
195
|
" Admin only. Defaults to your own team; Tower emails them a sign-in link.",
|
|
194
196
|
" cockpit team role <userId> --role <role> [--yes]",
|
|
195
197
|
" Admin only. Asks first unless --yes. You cannot change your own role.",
|
|
198
|
+
" cockpit team grant <person-uuid> careers [see|act] [--json]",
|
|
199
|
+
" cockpit team revoke <person-uuid> careers [see|act] [--json]",
|
|
200
|
+
" Super admins manage separate See and Act grants. Default level: see.",
|
|
201
|
+
" Find the account uuid with cockpit team members --json. Revocation retains history.",
|
|
196
202
|
" cockpit team device [list] [--json]",
|
|
197
203
|
" cockpit team device doctor <id|name> [--json]",
|
|
198
204
|
" Every paired collector device: name, id, trust state, owner, and why",
|
|
@@ -113,6 +113,7 @@ export function localCommandHelp(command) {
|
|
|
113
113
|
" cockpit ops [status [--job <id>] [--skips] [--memory] [--models] | recompile --person <email|name|id> [--dry-run]] [--dashboard-url <url>] [--json]",
|
|
114
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
|
+
" cockpit team github-login add|remove <person> <login> | list <person> [--json]",
|
|
116
117
|
" 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
118
|
" cockpit workbook [<project> [<doc>]] [--section <id>] [--markdown] [--width <n>] [--dashboard-url <url>] [--json]",
|
|
118
119
|
" cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
|
|
@@ -145,8 +146,8 @@ export function localCommandHelp(command) {
|
|
|
145
146
|
" cockpit logs <collector|dashboard|railway|railway:<service>|-> [--since 15m|1h|2d|<iso>] [--raw <template-id> [--limit <n>]] [--all-levels] [--table <file>] [--json]",
|
|
146
147
|
" cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
147
148
|
" cockpit setup [--since <7d|30d|date>] [--details] [--person <name|email>] [--compare <name,name>] [--json]",
|
|
148
|
-
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
149
|
-
" cockpit usage
|
|
149
|
+
" cockpit usage sessions [--person <exact-name>] [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--tool claude|codex] [--since <window>] [--until <iso>] [--cursor <cursor>] [--limit <n>] [--json]",
|
|
150
|
+
" cockpit usage session <id> [--from-window <n>] [--json]",
|
|
150
151
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
151
152
|
" cockpit ticket [same verbs and flags as cockpit issue] (the Tickets page; projects: `cockpit project list`)",
|
|
152
153
|
" cockpit tickets ... same as cockpit ticket",
|
package/dist/commands/logs.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readFile } from "node:fs/promises";
|
|
16
16
|
import { ClassTable, LOCAL_ACCESS_REFUSALS, digestSource } from "@bli-cockpit/telemetry-core/log-digest";
|
|
17
|
+
import { asRecord, callTower, openTower, writeCommandFailure } from "./tower-command.js";
|
|
17
18
|
import { getCollectorRuntimePaths } from "../local-state-paths.js";
|
|
18
19
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
19
20
|
import { runStoredLogs, storedProject } from "./logs-stored.js";
|
|
@@ -28,6 +29,15 @@ function refuse(io, json, reason, detail) {
|
|
|
28
29
|
return 1;
|
|
29
30
|
}
|
|
30
31
|
export async function runLogs(command, io, deps = {}) {
|
|
32
|
+
if (command.keyAction) {
|
|
33
|
+
const tower = await openTower("logs keys", command, io);
|
|
34
|
+
const result = await callTower(tower, { path: "/api/settings/log-ingest-keys", method: "POST", body: { source: command.source, action: command.keyAction }, label: "logs keys" });
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
return writeCommandFailure(io, command.json, result);
|
|
37
|
+
// Deliberate one-time credential output; never stderr, cache or log.
|
|
38
|
+
writeLine(io.stdout, command.json ? JSON.stringify(result.body) : String(asRecord(result.body).key));
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
31
41
|
// BLI-12855: a project name (or --stored, or --list) reads the digests Tower's cron stored, not a local log.
|
|
32
42
|
const stored = storedProject(command);
|
|
33
43
|
if (command.list || stored)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
4
|
+
/** The map takes no filters: the backend answers with the caller's visible projects and topics. */
|
|
5
|
+
export function parseMemoryMapArgs(args) {
|
|
6
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--json", "--home", "--dashboard-url"], valueFlags: ["--home", "--dashboard-url"] });
|
|
7
|
+
if (values.positionals.slice(1).length)
|
|
8
|
+
throw new Error("memory map takes no positionals.");
|
|
9
|
+
return { kind: "memory-store", action: "map", containerTags: [], json: values.booleans.has("--json"),
|
|
10
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
11
|
+
}
|
|
12
|
+
export async function runMemoryMap(door) {
|
|
13
|
+
const answer = await askAgentDoor(door, { method: "GET", path: "/api/memory/map", label: "memory map", timeoutMs: 60_000 });
|
|
14
|
+
if (!answer.ok)
|
|
15
|
+
return failAgentDoor(door, "[memory map cli]", answer.reason, answer.detail);
|
|
16
|
+
const body = answer.body;
|
|
17
|
+
writeLine(door.io.stderr, `[memory map cli] answered ${JSON.stringify({ reason: body.capped ? "capped" : "exhausted", examined: body.examined, total: body.total })}`);
|
|
18
|
+
if (door.json)
|
|
19
|
+
return emitAgentDoor(door, body);
|
|
20
|
+
for (const project of body.projects) {
|
|
21
|
+
writeLine(door.io.stdout, `${project.project} (${project.count} memories)`);
|
|
22
|
+
for (const topic of project.topics)
|
|
23
|
+
writeLine(door.io.stdout, ` ${topic.label}: ${topic.count}${topic.averageImportance === null ? " (unscored)" : ` (importance ${Math.round(topic.averageImportance)}/100)`}`);
|
|
24
|
+
}
|
|
25
|
+
writeLine(door.io.stdout, `${body.examined} of ${body.total} visible memories counted; as of ${body.computedAt}.${body.capped ? " Scan capped at 10,000." : ""}`);
|
|
26
|
+
if (!body.projects.length)
|
|
27
|
+
writeLine(door.io.stdout, "No project knowledge yet.");
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
@@ -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;
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* `ps` shows an argument list, shell history keeps it, and a memory with a
|
|
43
43
|
* newline in it cannot be quoted correctly anyway.
|
|
44
44
|
*/
|
|
45
|
+
import { runMemoryMap } from "./memory-map.js";
|
|
45
46
|
import { runMemoryRules } from "./memory-rules.js";
|
|
46
47
|
import { memoryProjectPrefix } from "./memory-project-label.js";
|
|
47
48
|
import { memorySaveOutsideRoots } from "@bli-cockpit/memory-mcp/dist/recorded-outside-roots.js";
|
|
@@ -65,6 +66,9 @@ const FORGET_MAX_CHARS = 10_000;
|
|
|
65
66
|
export async function runMemoryStore(command, io) {
|
|
66
67
|
const door = await openAgentDoor("memory", command, io);
|
|
67
68
|
switch (command.action) {
|
|
69
|
+
case "map":
|
|
70
|
+
return runMemoryMap(door);
|
|
71
|
+
case "knowledge":
|
|
68
72
|
case "rules":
|
|
69
73
|
return runMemoryRules(command, door);
|
|
70
74
|
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,9 @@ 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
|
|
47
|
+
" cockpit memory map [--json]",
|
|
48
|
+
" cockpit memory rules [--person <name>] [--project <name-or-slug>] [--by-project] [--limit <n>] [--json]",
|
|
49
|
+
" cockpit memory knowledge [--person <name>] [--project <name-or-slug>] [--by-project] [--limit <n>] [--json]",
|
|
48
50
|
" cockpit memory profile [--container <tag>] [--json]",
|
|
49
51
|
" the compiled one-page briefing every session opens with; a read, never a compile",
|
|
50
52
|
" 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.202");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* (`packages/bli-cockpit-mcp/src/speed-tools.ts`) carries its own copy, and a
|
|
12
12
|
* test in that package fails when the two lists drift.
|
|
13
13
|
*/
|
|
14
|
-
/** The pages in rail order
|
|
15
|
-
export function speedPages(
|
|
14
|
+
/** The pages in rail order; Memory has one data door per tab. */
|
|
15
|
+
export function speedPages(_memoryContainer) {
|
|
16
16
|
return [
|
|
17
17
|
{ name: "today", label: "Today (JARVIS home)", path: "/api/jarvis/brief" },
|
|
18
18
|
{ name: "meeting-notes", label: "Meeting notes", path: "/api/notes/library" },
|
|
@@ -21,13 +21,10 @@ export function speedPages(memoryContainer) {
|
|
|
21
21
|
{ name: "messages", label: "Messages", path: "/api/msg/channels" },
|
|
22
22
|
{ name: "mail", label: "Mail", path: "/api/mail/inbox" },
|
|
23
23
|
{ name: "docs", label: "Docs", path: "/api/docs/tree" },
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
note: "the page reads agent_memories on the server; the profile door is the nearest read",
|
|
29
|
-
},
|
|
30
|
-
{ name: "tickets", label: "Tickets", path: "/api/work/issues" },
|
|
24
|
+
{ name: "memory-knowledge", label: "Memory knowledge", path: "/api/memory/knowledge" },
|
|
25
|
+
{ name: "memory-rules", label: "Memory rules", path: "/api/memory/rules" },
|
|
26
|
+
{ name: "tickets", label: "Tickets", path: "/api/work/board" },
|
|
27
|
+
{ name: "tickets-list", label: "Tickets list", path: "/api/work/issues" },
|
|
31
28
|
{ name: "setup", label: "Setup", path: "/api/setup" },
|
|
32
29
|
{ name: "usage", label: "Usage", path: "/api/usage/people?since=30d" },
|
|
33
30
|
{ name: "calendar", label: "Calendar", path: "/api/cal/week" },
|
package/dist/commands/team.js
CHANGED
|
@@ -23,10 +23,14 @@ import { confirmDestructive } from "./settings.js";
|
|
|
23
23
|
import { asRecord, callTower, openTower, writeCommandFailure, } from "./tower-command.js";
|
|
24
24
|
export async function runTeam(command, io) {
|
|
25
25
|
const tower = await openTower("team", command, io);
|
|
26
|
+
if (command.action.startsWith("github-login-"))
|
|
27
|
+
return githubLogin(command, tower, io);
|
|
26
28
|
if (command.action === "members")
|
|
27
29
|
return showMembers(command, tower, io);
|
|
28
30
|
if (command.action === "invite")
|
|
29
31
|
return invite(command, tower, io);
|
|
32
|
+
if (command.action === "grant" || command.action === "revoke")
|
|
33
|
+
return changeGrant(command, tower, io);
|
|
30
34
|
if (command.action === "role")
|
|
31
35
|
return changeRole(command, tower, io);
|
|
32
36
|
if (command.action === "device-list")
|
|
@@ -207,4 +211,41 @@ async function deviceDoctor(command, tower, io) {
|
|
|
207
211
|
return writeCommandFailure(io, command.json, result);
|
|
208
212
|
writeLine(io.stdout, command.json ? JSON.stringify({ ok: true, ...asRecord(result.body) }) : `Doctor requested for ${device.device_name ?? device.device_id}. Waiting for its next check-in.`);
|
|
209
213
|
return 0;
|
|
214
|
+
}
|
|
215
|
+
/** Exact directory labels locate a roster id; attribution remains the server resolver's job. */
|
|
216
|
+
async function githubLogin(command, tower, io) {
|
|
217
|
+
let personId = command.personRef ?? "";
|
|
218
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(personId)) {
|
|
219
|
+
const directory = await callTower(tower, { path: "/api/team/members", label: "team github-login lookup" });
|
|
220
|
+
if (!directory.ok)
|
|
221
|
+
return writeCommandFailure(io, command.json, directory);
|
|
222
|
+
const body = asRecord(directory.body);
|
|
223
|
+
if (body.rosterError)
|
|
224
|
+
return writeCommandFailure(io, command.json, { reason: "roster_read_failed", detail: "The roster could not be read. Use a person UUID or retry." });
|
|
225
|
+
const needle = personId.toLowerCase();
|
|
226
|
+
const matches = (Array.isArray(body.people) ? body.people : []).map(asRecord).filter(person => [person.rosterId, person.name, person.email].some(label => typeof label === "string" && label.toLowerCase() === needle));
|
|
227
|
+
const ids = [...new Set(matches.map(person => person.rosterId).filter((id) => typeof id === "string"))];
|
|
228
|
+
if (ids.length !== 1)
|
|
229
|
+
return writeCommandFailure(io, command.json, { reason: ids.length ? "ambiguous_person" : "person_not_found", detail: "Use an exact roster UUID, name or email from cockpit team members." });
|
|
230
|
+
personId = ids[0];
|
|
231
|
+
}
|
|
232
|
+
const method = command.action === "github-login-list" ? "GET" : command.action === "github-login-add" ? "POST" : "DELETE";
|
|
233
|
+
const result = await callTower(tower, { path: `/api/team/members/${encodeURIComponent(personId)}/github-logins`, method,
|
|
234
|
+
label: "team github-login", ...(method === "GET" ? {} : { body: { login: command.githubLogin } }) });
|
|
235
|
+
writeLine(io.stderr, `[team cli] github login ${JSON.stringify({ person_id: personId, method, reason: result.ok ? "answered" : result.reason })}`);
|
|
236
|
+
if (!result.ok)
|
|
237
|
+
return writeCommandFailure(io, command.json, result);
|
|
238
|
+
const body = asRecord(result.body);
|
|
239
|
+
writeLine(io.stdout, command.json ? JSON.stringify(body) :
|
|
240
|
+
`GitHub logins: ${[body.primary_login, ...(Array.isArray(body.extra_logins) ? body.extra_logins : [])].filter(Boolean).join(", ") || "none"}. History is preserved.`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
async function changeGrant(command, tower, io) {
|
|
244
|
+
const result = await callTower(tower, { path: "/api/team/grants", method: "POST", label: "team section grant",
|
|
245
|
+
body: { person_id: command.targetUserId, section: command.section, level: command.level ?? "see", action: command.action } });
|
|
246
|
+
writeLine(io.stderr, `[team cli] section grant ${JSON.stringify({ action: command.action, level: command.level ?? "see", reason: result.ok ? "ok" : result.reason })}`);
|
|
247
|
+
if (!result.ok)
|
|
248
|
+
return writeCommandFailure(io, command.json, result);
|
|
249
|
+
writeLine(io.stdout, command.json ? JSON.stringify({ ok: true, ...asRecord(result.body) }) : `Careers ${command.level ?? "see"} ${command.action === "grant" ? "granted to" : "revoked for"} ${command.targetUserId}.`);
|
|
250
|
+
return 0;
|
|
210
251
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
import { ISSUE_STATES } from "./local-args-tower-work.js";
|
|
4
|
+
import { READ_DEADLINE_MS } from "./issue-contracts.js";
|
|
5
|
+
export function ticketBoardLines(board, command) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
for (const state of ISSUE_STATES) {
|
|
8
|
+
if (command.stateFilter && command.stateFilter !== state)
|
|
9
|
+
continue;
|
|
10
|
+
const cards = board.cards.filter(card => card.state === state).sort((a, b) => (a.priority || 5) - (b.priority || 5) || b.updatedAt.localeCompare(a.updatedAt));
|
|
11
|
+
const total = board.countsByState?.[state];
|
|
12
|
+
const countLine = command.label ? `${cards.length} loaded` :
|
|
13
|
+
total == null ? `${cards.length} loaded · total unavailable` : `${cards.length} loaded of ${total}`;
|
|
14
|
+
const failure = board.failedStates.includes(state) ? " · read failed" : "";
|
|
15
|
+
const preview = cards.length > 5 ? " · top 5 below" : "";
|
|
16
|
+
lines.push(`${state.toUpperCase().replaceAll("_", " ")}: ${countLine}${failure}${preview}`);
|
|
17
|
+
lines.push(...cards.slice(0, 5).map(card => ` ${card.identifier} ${card.title}`));
|
|
18
|
+
}
|
|
19
|
+
if (board.countsCoverage)
|
|
20
|
+
lines.push(`Counts: ${board.countsCoverage}`);
|
|
21
|
+
if (board.countsUnavailableReason)
|
|
22
|
+
lines.push(`Counts unavailable: ${board.countsUnavailableReason}`);
|
|
23
|
+
if (board.gaps.length)
|
|
24
|
+
lines.push(`Unavailable: ${board.gaps.join(", ")}`);
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
export async function readTicketBoard(command, door) {
|
|
28
|
+
const query = new URLSearchParams();
|
|
29
|
+
for (const [key, value] of Object.entries({ state: command.stateFilter, assignee: command.assignee,
|
|
30
|
+
project: command.project, label: command.label, rows: command.rows }))
|
|
31
|
+
if (value)
|
|
32
|
+
query.set(key, value);
|
|
33
|
+
const suffix = query.toString();
|
|
34
|
+
const answer = await askAgentDoor(door, { path: `/api/work/board${suffix ? `?${suffix}` : ""}`, method: "GET", label: "tickets board", timeoutMs: READ_DEADLINE_MS });
|
|
35
|
+
if (!answer.ok)
|
|
36
|
+
return failAgentDoor(door, "[tickets board]", answer.reason, answer.detail);
|
|
37
|
+
writeLine(door.io.stderr, `[tickets board] read ${JSON.stringify({ reason: "board_read", cards: answer.body.cards.length })}`);
|
|
38
|
+
if (door.json)
|
|
39
|
+
return emitAgentDoor(door, answer.body);
|
|
40
|
+
for (const line of ticketBoardLines(answer.body, command))
|
|
41
|
+
writeLine(door.io.stdout, line);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function runUsage(command, io) {
|
|
|
20
20
|
if (command.includeAutomated)
|
|
21
21
|
query.set("include_automated", "1");
|
|
22
22
|
if (command.action === "sessions") {
|
|
23
|
-
for (const [key, value] of [["task", command.task], ["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
23
|
+
for (const [key, value] of [["person", command.person], ["tool", command.tool], ["cursor", command.cursor], ["limit", command.limit], ["task", command.task], ["subject", command.subject], ["repo", command.repo], ["session", command.session], ["fromWindow", command.fromWindow]])
|
|
24
24
|
if (value)
|
|
25
25
|
query.set(key, value);
|
|
26
26
|
const response = await askAgentDoor(door, { path: `/api/usage/sessions?${query}`, method: "GET", label: "usage sessions", timeoutMs: 30_000 });
|
|
@@ -30,8 +30,13 @@ export async function runUsage(command, io) {
|
|
|
30
30
|
return emitAgentDoor(door, response.body);
|
|
31
31
|
const sessions = response.body.sessions ?? [];
|
|
32
32
|
for (const session of sessions)
|
|
33
|
-
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.task_type} ${session.title}`);
|
|
33
|
+
writeLine(io.stdout, `${session.session_id} ${formatCount(session.tokens)} ${session.start_time ?? "start unknown"} ${session.tool ?? session.task_type} ${session.model ?? "model unknown"} ${session.title ?? "no title yet"}`);
|
|
34
34
|
writeLine(io.stdout, `${sessions.length} sessions`);
|
|
35
|
+
const page = response.body;
|
|
36
|
+
for (const line of page.read_coverage ?? [])
|
|
37
|
+
writeLine(io.stdout, line);
|
|
38
|
+
if (page.next_cursor)
|
|
39
|
+
writeLine(io.stdout, `Next page: ${page.window ? `--since ${page.window.since} --until ${page.window.until} ` : ""}--cursor ${page.next_cursor}`);
|
|
35
40
|
return 0;
|
|
36
41
|
}
|
|
37
42
|
const answer = await askAgentDoor(door, { path: `/api/usage/people?${query}`, method: "GET", label: "usage people", timeoutMs: 30_000 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.202",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.61",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.114",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.70"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|