@bli-cockpit/cli 0.2.222 → 0.2.236

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.
Files changed (75) hide show
  1. package/dist/agent-plugins-converge.js +182 -0
  2. package/dist/agent-plugins.js +116 -0
  3. package/dist/autostart-darwin-plist.js +2 -2
  4. package/dist/autostart-windows-scripts.js +2 -2
  5. package/dist/commands/agent-door.js +35 -0
  6. package/dist/commands/approvals.js +3 -1
  7. package/dist/commands/brain.js +23 -0
  8. package/dist/commands/ci.js +23 -0
  9. package/dist/commands/docs-export.js +62 -0
  10. package/dist/commands/docs.js +3 -0
  11. package/dist/commands/doctor-access.js +2 -2
  12. package/dist/commands/doctor-agent-plugins.js +82 -0
  13. package/dist/commands/doctor-pipeline.js +2 -2
  14. package/dist/commands/doctor-report.js +1 -0
  15. package/dist/commands/doctor-update.js +2 -2
  16. package/dist/commands/doctor.js +6 -0
  17. package/dist/commands/drills-render.js +63 -0
  18. package/dist/commands/drills.js +152 -0
  19. package/dist/commands/hits.js +83 -0
  20. package/dist/commands/intentions.js +49 -0
  21. package/dist/commands/local-args-docs-export.js +23 -0
  22. package/dist/commands/local-args-tower-admin.js +8 -0
  23. package/dist/commands/local-args-tower-brain-ladder.js +90 -0
  24. package/dist/commands/local-args-tower-brain.js +11 -1
  25. package/dist/commands/local-args-tower-docs-msg.js +9 -3
  26. package/dist/commands/local-args-tower-drills.js +87 -0
  27. package/dist/commands/local-args-tower-hits.js +93 -0
  28. package/dist/commands/local-args-tower-intentions.js +70 -0
  29. package/dist/commands/local-args-tower-logs-search.js +51 -0
  30. package/dist/commands/local-args-tower-logs.js +3 -0
  31. package/dist/commands/local-args-tower-memory.js +67 -1
  32. package/dist/commands/local-args-tower-pages.js +3 -1
  33. package/dist/commands/local-args-tower-people.js +25 -0
  34. package/dist/commands/local-args-tower-usage.js +4 -4
  35. package/dist/commands/local-args-tower-who.js +36 -0
  36. package/dist/commands/local-args-tower.js +8 -1
  37. package/dist/commands/local-args.js +15 -1
  38. package/dist/commands/local-auth.js +5 -2
  39. package/dist/commands/local-command-shapes-auth.js +1 -0
  40. package/dist/commands/local-command-shapes-drills.js +7 -0
  41. package/dist/commands/local-command-shapes-hits.js +5 -0
  42. package/dist/commands/local-help-commands-tower-ops.js +123 -1
  43. package/dist/commands/local-help-commands-tower.js +19 -7
  44. package/dist/commands/local-help-commands.js +3 -0
  45. package/dist/commands/local-help.js +24 -4
  46. package/dist/commands/local-run-ops.js +11 -0
  47. package/dist/commands/local.js +16 -8
  48. package/dist/commands/logs-search.js +45 -0
  49. package/dist/commands/logs.js +3 -0
  50. package/dist/commands/memory-exam.js +47 -0
  51. package/dist/commands/memory-repeats.js +44 -0
  52. package/dist/commands/memory-search-print.js +95 -0
  53. package/dist/commands/memory-source.js +39 -0
  54. package/dist/commands/memory-store.js +18 -30
  55. package/dist/commands/memory-verb-list.js +7 -2
  56. package/dist/commands/notes-reads.js +20 -0
  57. package/dist/commands/notes-writes.js +24 -0
  58. package/dist/commands/notes.js +4 -2
  59. package/dist/commands/people.js +29 -0
  60. package/dist/commands/public-root.js +1 -1
  61. package/dist/commands/spend.js +79 -0
  62. package/dist/commands/sync-heal-agent-plugins.js +46 -0
  63. package/dist/commands/sync-heal.js +6 -0
  64. package/dist/commands/team.js +35 -0
  65. package/dist/commands/usage.js +9 -0
  66. package/dist/commands/who.js +36 -0
  67. package/dist/evidence-upload-direct.js +44 -0
  68. package/dist/evidence-upload-object.js +18 -4
  69. package/dist/evidence-upload-plan.js +1 -0
  70. package/dist/evidence-upload-transport.js +2 -0
  71. package/dist/local-state-config.js +21 -1
  72. package/dist/local-state.js +1 -1
  73. package/dist/process-runner.js +7 -3
  74. package/dist/state-dir-audit.js +1 -0
  75. package/package.json +4 -4
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `cockpit drills` argument parsing (BLI-19259).
3
+ *
4
+ * cockpit drills [list] your open drill, queue, bosses, streak, history
5
+ * cockpit drills start open the next drill (answer first: the cause is not shown)
6
+ * cockpit drills answer --confidence <1-5> [--text <words>] [--attempt <id>] (the answer on stdin when --text is absent)
7
+ * cockpit drills theme your theme and the choices
8
+ * cockpit drills theme set <theme> [--play <t>] [--watch <t>] [--train <t>]
9
+ * cockpit drills review your own week: hits, wins, highs, lows, plateaus, what to practise
10
+ *
11
+ * Every verb is one `/api/drills` door on this machine's device token, the same doors the web page, the MCP twins and JARVIS use.
12
+ */
13
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
14
+ const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15
+ const ONE_VERB = "drills takes one verb: list, start, answer, theme, theme set or review. Example: `cockpit drills` or `cockpit drills start`.";
16
+ const FLAGS_BY_ACTION = {
17
+ list: [],
18
+ start: [],
19
+ answer: ["--confidence", "--text", "--attempt"],
20
+ theme: [],
21
+ "theme-set": ["--play", "--watch", "--train"],
22
+ review: [],
23
+ };
24
+ const OWN_FLAGS = ["--confidence", "--text", "--attempt", "--play", "--watch", "--train"];
25
+ const COMMON_FLAGS = ["--home", "--dashboard-url", "--json"];
26
+ export function parseDrillsArgs(args) {
27
+ const values = parseNamedArgs(args, {
28
+ allowedFlags: [...OWN_FLAGS, ...COMMON_FLAGS],
29
+ valueFlags: [...OWN_FLAGS, "--home", "--dashboard-url"],
30
+ });
31
+ const first = values.positionals[0];
32
+ let action;
33
+ let consumed = 1;
34
+ if (first === undefined) {
35
+ action = "list";
36
+ consumed = 0;
37
+ }
38
+ else if (first === "theme" && values.positionals[1] === "set") {
39
+ action = "theme-set";
40
+ consumed = 2;
41
+ }
42
+ else if (first === "list" || first === "start" || first === "answer" || first === "theme" || first === "review") {
43
+ action = first;
44
+ }
45
+ else
46
+ throw new Error(ONE_VERB);
47
+ const given = [...values.flags.keys()].filter((flag) => OWN_FLAGS.includes(flag));
48
+ const owned = FLAGS_BY_ACTION[action];
49
+ const stray = given.find((flag) => !owned.includes(flag));
50
+ if (stray)
51
+ throw new Error(`${stray} does not belong to \`cockpit drills ${action === "theme-set" ? "theme set" : action}\`.`);
52
+ const rest = values.positionals.slice(consumed);
53
+ let theme;
54
+ if (action === "theme-set") {
55
+ theme = rest.join(" ").trim();
56
+ if (!theme)
57
+ throw new Error("`cockpit drills theme set` takes a theme: video-game, boxing, muay-thai, crime-drama, anime, sports, or your own words.");
58
+ }
59
+ else if (rest.length > 0)
60
+ throw new Error(`\`cockpit drills ${action}\` takes no positional arguments.`);
61
+ let confidence;
62
+ const rawConfidence = values.flags.get("--confidence");
63
+ if (action === "answer") {
64
+ if (rawConfidence === undefined)
65
+ throw new Error("`cockpit drills answer` needs --confidence 1 to 5: how sure you are, from a guess to certain.");
66
+ confidence = Number(rawConfidence);
67
+ if (!Number.isInteger(confidence) || confidence < 1 || confidence > 5)
68
+ throw new Error("--confidence wants a whole number from 1 (a guess) to 5 (certain).");
69
+ }
70
+ const attempt = optionalNonEmpty(values.flags.get("--attempt"));
71
+ if (attempt && !ID.test(attempt))
72
+ throw new Error("--attempt looks like 8f14e45f-ceea-467a-9575-1d3a4e7b2c10 (`cockpit drills` shows the open one).");
73
+ return {
74
+ kind: "drills",
75
+ action,
76
+ ...(attempt ? { attemptId: attempt } : {}),
77
+ ...(confidence !== undefined ? { confidence } : {}),
78
+ ...(optionalNonEmpty(values.flags.get("--text")) ? { text: optionalNonEmpty(values.flags.get("--text")) } : {}),
79
+ ...(theme ? { theme } : {}),
80
+ ...(optionalNonEmpty(values.flags.get("--play")) ? { play: optionalNonEmpty(values.flags.get("--play")) } : {}),
81
+ ...(optionalNonEmpty(values.flags.get("--watch")) ? { watch: optionalNonEmpty(values.flags.get("--watch")) } : {}),
82
+ ...(optionalNonEmpty(values.flags.get("--train")) ? { train: optionalNonEmpty(values.flags.get("--train")) } : {}),
83
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
84
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
85
+ json: values.booleans.has("--json"),
86
+ };
87
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `cockpit hits` argument parsing (BLI-19257).
3
+ *
4
+ * cockpit hits [--mine | --person <email|github-login|name> | --agent | --project <id>] [--days <n>] [--patterns] [--learning] [--top <n>] [--json]
5
+ * cockpit hits coach [--json]
6
+ * cockpit hits timeline [--weeks <n>] [--json]
7
+ * cockpit hits approve --scope <team|agents|project:<id>> [--version <n>] [--json]
8
+ *
9
+ * The hit ledger over `GET /api/hits`, `GET /api/hits/coach` and `POST /api/hits/patterns/approve` with this machine's
10
+ * device token. Who may see what is the database's rule and the door's refusal, not this parser's.
11
+ */
12
+ import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
13
+ const PROJECT_ID = /^[a-z0-9][a-z0-9-]{0,40}$/;
14
+ const SCOPE = /^(?:team|agents|project:[a-z0-9][a-z0-9-]{0,40})$/;
15
+ const READ_ONLY_FLAGS = ["--mine", "--person", "--agent", "--project", "--days", "--top", "--patterns", "--learning"];
16
+ export function parseHitsArgs(args) {
17
+ const values = parseNamedArgs(args, {
18
+ allowedFlags: [...READ_ONLY_FLAGS, "--scope", "--version", "--weeks", "--home", "--dashboard-url", "--json"],
19
+ valueFlags: ["--person", "--project", "--days", "--top", "--scope", "--version", "--weeks", "--home", "--dashboard-url"],
20
+ });
21
+ const verb = values.positionals[0];
22
+ if (values.positionals.length > 1 || (verb !== undefined && verb !== "read" && verb !== "coach" && verb !== "approve" && verb !== "timeline")) {
23
+ throw new Error("hits takes no verb (read the ledger), `timeline`, `coach` or `approve`. Example: `cockpit hits --mine` or `cockpit hits timeline`.");
24
+ }
25
+ const action = verb === "coach" || verb === "approve" || verb === "timeline" ? verb : "read";
26
+ const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
27
+ const common = {
28
+ kind: "hits",
29
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
30
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
31
+ json: values.booleans.has("--json"),
32
+ };
33
+ if (action !== "read") {
34
+ const stray = READ_ONLY_FLAGS.find(given);
35
+ if (stray)
36
+ throw new Error(`${stray} belongs to \`cockpit hits\` (reading the ledger), not to hits ${action}.`);
37
+ }
38
+ if (given("--weeks") && action !== "timeline")
39
+ throw new Error("--weeks belongs to hits timeline.");
40
+ if (action === "timeline") {
41
+ if (given("--scope") || given("--version"))
42
+ throw new Error("--scope and --version belong to hits approve.");
43
+ const weeks = optionalPositiveInteger(values.flags.get("--weeks"), "--weeks");
44
+ if (weeks !== undefined && weeks > 26)
45
+ throw new Error("--weeks must be 26 or fewer.");
46
+ return { ...common, action, ...(weeks !== undefined ? { weeks } : {}) };
47
+ }
48
+ if (action === "coach") {
49
+ if (given("--scope") || given("--version"))
50
+ throw new Error("--scope and --version belong to hits approve.");
51
+ return { ...common, action };
52
+ }
53
+ if (action === "approve") {
54
+ const scope = optionalNonEmpty(values.flags.get("--scope"));
55
+ if (!scope || !SCOPE.test(scope))
56
+ throw new Error("hits approve wants --scope team, agents or project:<id> (for example project:pearl-portal).");
57
+ return { ...common, action, scope, ...versionOf(values.flags.get("--version")) };
58
+ }
59
+ if (given("--scope") || given("--version"))
60
+ throw new Error("--scope and --version belong to hits approve.");
61
+ const whoFlags = ["--mine", "--person", "--agent", "--project"].filter(given);
62
+ if (whoFlags.length > 1)
63
+ throw new Error(`Pick one of --mine, --person, --agent or --project (got ${whoFlags.join(" and ")}).`);
64
+ const person = optionalNonEmpty(values.flags.get("--person"))?.trim();
65
+ const project = optionalNonEmpty(values.flags.get("--project"));
66
+ if (project && !PROJECT_ID.test(project))
67
+ throw new Error("--project wants a registry project id like pearl-portal.");
68
+ const days = optionalPositiveInteger(values.flags.get("--days"), "--days");
69
+ if (days !== undefined && days > 365)
70
+ throw new Error("--days must be 365 or fewer.");
71
+ const top = optionalPositiveInteger(values.flags.get("--top"), "--top");
72
+ if (top !== undefined && top > 50)
73
+ throw new Error("--top must be 50 or fewer.");
74
+ const learning = values.booleans.has("--learning");
75
+ const who = values.booleans.has("--mine") ? "mine" : person ? "person" : values.booleans.has("--agent") ? "agents" : project ? "project" : undefined;
76
+ if (learning && who !== "mine" && who !== "person")
77
+ throw new Error("--learning is read for one person: use --mine or --person <name>.");
78
+ return {
79
+ ...common,
80
+ action,
81
+ ...(who ? { who } : {}),
82
+ ...(person ? { person } : {}),
83
+ ...(project ? { project } : {}),
84
+ ...(days !== undefined ? { days } : {}),
85
+ ...(top !== undefined ? { top } : {}),
86
+ ...(values.booleans.has("--patterns") ? { patterns: true } : {}),
87
+ ...(learning ? { learning: true } : {}),
88
+ };
89
+ }
90
+ function versionOf(raw) {
91
+ const version = optionalPositiveInteger(raw, "--version");
92
+ return version === undefined ? {} : { version };
93
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `cockpit intentions` argument parsing (BLI-19248).
3
+ *
4
+ * cockpit intentions [list] [--status open|overdue|done|dropped|all] [--project <name>] [--limit <n>] [--json]
5
+ * cockpit intentions show <id> [--json]
6
+ * cockpit intentions done <id> --evidence "<what settled it>" [--by person|agent] [--json]
7
+ * cockpit intentions drop <id> --evidence "<why>" [--by person|agent] [--json]
8
+ * cockpit intentions reopen <id> [--json]
9
+ *
10
+ * Prospective memory: what a person or an agent said they would do later, tied to a trigger. Reads and writes
11
+ * `/api/intentions` with this machine's device token.
12
+ */
13
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
14
+ export const INTENTION_LIST_STATUSES = ["open", "overdue", "done", "dropped", "all"];
15
+ const ACTIONS = ["list", "show", "done", "drop", "reopen"];
16
+ export function parseIntentionsArgs(args) {
17
+ const values = parseNamedArgs(args, {
18
+ allowedFlags: ["--status", "--project", "--limit", "--evidence", "--by", "--home", "--dashboard-url", "--json"],
19
+ valueFlags: ["--status", "--project", "--limit", "--evidence", "--by", "--home", "--dashboard-url"],
20
+ });
21
+ const first = values.positionals[0];
22
+ const action = (first ?? "list");
23
+ if (!ACTIONS.includes(action)) {
24
+ throw new Error("intentions takes one verb: list, show, done, drop or reopen. Example: `cockpit intentions list --status overdue`.");
25
+ }
26
+ const rest = values.positionals.slice(first ? 1 : 0);
27
+ const status = optionalNonEmpty(values.flags.get("--status"));
28
+ const project = optionalNonEmpty(values.flags.get("--project"));
29
+ const evidence = optionalNonEmpty(values.flags.get("--evidence"))?.trim();
30
+ const by = optionalNonEmpty(values.flags.get("--by"));
31
+ const rawLimit = optionalNonEmpty(values.flags.get("--limit"));
32
+ if (status && !INTENTION_LIST_STATUSES.includes(status))
33
+ throw new Error(`--status wants one of ${INTENTION_LIST_STATUSES.join(", ")}.`);
34
+ if (by && by !== "person" && by !== "agent")
35
+ throw new Error("--by wants person or agent.");
36
+ const limit = rawLimit ? Number(rawLimit) : undefined;
37
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200))
38
+ throw new Error("--limit wants a whole number from 1 to 200.");
39
+ if (action === "list") {
40
+ if (rest.length)
41
+ throw new Error("intentions list takes no id; use `cockpit intentions show <id>`.");
42
+ if (evidence || by)
43
+ throw new Error("--evidence and --by belong to intentions done and drop.");
44
+ }
45
+ else {
46
+ if (rest.length !== 1)
47
+ throw new Error(`intentions ${action} takes exactly one id (the uuid or the 8-character prefix list prints).`);
48
+ if (status || project || limit !== undefined)
49
+ throw new Error("--status, --project and --limit belong to intentions list.");
50
+ if (action === "done" || action === "drop") {
51
+ if (!evidence || evidence.length < 3 || evidence.length > 300)
52
+ throw new Error(`intentions ${action} needs --evidence "<what settled it>" of 3 to 300 characters.`);
53
+ }
54
+ else if (evidence || by)
55
+ throw new Error("--evidence and --by belong to intentions done and drop.");
56
+ }
57
+ return {
58
+ kind: "intentions",
59
+ action,
60
+ ...(rest[0] ? { id: rest[0] } : {}),
61
+ ...(status ? { status } : {}),
62
+ ...(project ? { project } : {}),
63
+ ...(limit !== undefined ? { limit } : {}),
64
+ ...(evidence ? { evidence } : {}),
65
+ ...(by ? { by: by } : {}),
66
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
67
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
68
+ json: values.booleans.has("--json"),
69
+ };
70
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `cockpit logs search` argument parsing (BLI-20654).
3
+ *
4
+ * cockpit logs search <project> [--source gcp|supabase|vercel|railway|deploys|db-host|<label>] [--stream api|postgres|auth]
5
+ * [--since 30m|6h (default 1h) | --from <iso> [--to <iso>]] [--status 500|5xx|500-503] [--method GET] [--route <text>]
6
+ * [--severity error] [--q <text>] [--limit <n>] [--context] [--json]
7
+ *
8
+ * Plain words: it asks Tower to read the project's raw logs NOW, from the platform, and print the matching lines, scrubbed.
9
+ * Owner or admin only (the server decides). The values are validated by the server; this parser only checks the shape.
10
+ */
11
+ import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
12
+ export function parseLogsSearchArgs(args) {
13
+ const values = parseNamedArgs(args, {
14
+ allowedFlags: ["--source", "--stream", "--since", "--from", "--to", "--status", "--method", "--route", "--severity", "--q", "--limit", "--context", "--home", "--dashboard-url", "--json"],
15
+ valueFlags: ["--source", "--stream", "--since", "--from", "--to", "--status", "--method", "--route", "--severity", "--q", "--limit", "--home", "--dashboard-url"],
16
+ });
17
+ const project = values.positionals[0];
18
+ if (!project || values.positionals.length > 1)
19
+ throw new Error("Use cockpit logs search <project> --since 30m [--source gcp] [--status 500] [--method GET] [--route <text>] [--q <text>] [--context] [--json].");
20
+ const from = optionalNonEmpty(values.flags.get("--from"));
21
+ // No window named means the last hour: a search is for something that just happened, and a default keeps the bare verb runnable.
22
+ const since = optionalNonEmpty(values.flags.get("--since")) ?? (from ? undefined : "1h");
23
+ if (since && from)
24
+ throw new Error("logs search takes --since or --from, not both.");
25
+ const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
26
+ const pick = (name) => optionalNonEmpty(values.flags.get(name));
27
+ return {
28
+ kind: "logs",
29
+ source: project,
30
+ stored: false,
31
+ list: false,
32
+ allLevels: false,
33
+ search: {
34
+ ...(pick("--source") ? { logSource: pick("--source") } : {}),
35
+ ...(pick("--stream") ? { stream: pick("--stream") } : {}),
36
+ ...(since ? { since } : {}),
37
+ ...(from ? { from } : {}),
38
+ ...(pick("--to") ? { to: pick("--to") } : {}),
39
+ ...(pick("--status") ? { status: pick("--status") } : {}),
40
+ ...(pick("--method") ? { method: pick("--method") } : {}),
41
+ ...(pick("--route") ? { route: pick("--route") } : {}),
42
+ ...(pick("--severity") ? { severity: pick("--severity") } : {}),
43
+ ...(pick("--q") ? { q: pick("--q") } : {}),
44
+ ...(limit === undefined ? {} : { limit }),
45
+ context: values.booleans.has("--context"),
46
+ },
47
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
48
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
49
+ json: values.booleans.has("--json"),
50
+ };
51
+ }
@@ -16,12 +16,15 @@
16
16
  */
17
17
  import { LOG_PROJECT_PATTERN } from "@bli-cockpit/telemetry-core/log-digest";
18
18
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
19
+ import { parseLogsSearchArgs } from "./local-args-tower-logs-search.js";
19
20
  /** The source spellings, for the usage line and the refusal. `railway:<service>` takes any service name. */
20
21
  export const LOGS_SOURCES = ["collector", "dashboard", "railway", "railway:<service>", "-", "<project>"];
21
22
  /** The spellings that read a local log; every other name is a registry project (stored digests). */
22
23
  const LOCAL_SOURCE_PATTERN = /^(collector|dashboard|railway|railway:[A-Za-z0-9][A-Za-z0-9._-]{0,63}|-)$/;
23
24
  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
25
  export function parseLogsArgs(args) {
26
+ if (args[0] === "search")
27
+ return parseLogsSearchArgs(args.slice(1));
25
28
  if (args[0] === "keys") {
26
29
  const action = args[1];
27
30
  const source = args[2];
@@ -32,10 +32,12 @@
32
32
  * them; `parseMemoryArgs` in `local-args-collector-status.ts` routes to here.
33
33
  */
34
34
  import { parseMemoryMapArgs } from "./memory-map.js";
35
+ import { parseMemoryRepeatsArgs } from "./memory-repeats.js";
35
36
  import { parseMemoryRulesArgs } from "./memory-rules.js";
36
37
  import { optionalNonEmpty, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
37
38
  export const MEMORY_STORE_ACTIONS = new Set([
38
39
  "map",
40
+ "repeats",
39
41
  "rules",
40
42
  "knowledge",
41
43
  "search",
@@ -54,6 +56,10 @@ export const MEMORY_STORE_ACTIONS = new Set([
54
56
  "consolidation",
55
57
  // BLI-13376: the originals behind a consolidated memory, one id.
56
58
  "chain",
59
+ // BLI-19255: the raw session or meeting behind a memory, as a bounded verbatim excerpt.
60
+ "source",
61
+ // BLI-19249: a READ of the nightly memory exam score. The exam is a Railway job; owner-only.
62
+ "exam",
57
63
  "requeue",
58
64
  ]);
59
65
  const ALLOWED_FLAGS = [
@@ -64,6 +70,12 @@ const ALLOWED_FLAGS = [
64
70
  "--container",
65
71
  "--limit",
66
72
  "--no-recent",
73
+ "--include-originals",
74
+ "--no-originals",
75
+ "--include-source",
76
+ "--no-source",
77
+ "--query",
78
+ "--chars",
67
79
  "--custom-id",
68
80
  "--alias",
69
81
  "--extract",
@@ -75,6 +87,8 @@ const ALLOWED_FLAGS = [
75
87
  "--source",
76
88
  "--dry-run",
77
89
  "--apply",
90
+ "--misses",
91
+ "--night",
78
92
  "--json",
79
93
  ];
80
94
  const VALUE_FLAGS = [
@@ -84,6 +98,8 @@ const VALUE_FLAGS = [
84
98
  "--repo",
85
99
  "--container",
86
100
  "--limit",
101
+ "--query",
102
+ "--chars",
87
103
  "--custom-id",
88
104
  "--alias",
89
105
  "--file",
@@ -91,10 +107,13 @@ const VALUE_FLAGS = [
91
107
  "--since",
92
108
  "--until",
93
109
  "--source",
110
+ "--night",
94
111
  ];
95
112
  export function parseMemoryStoreArgs(action, args) {
96
113
  if (action === "map")
97
114
  return parseMemoryMapArgs(args);
115
+ if (action === "repeats")
116
+ return parseMemoryRepeatsArgs(args);
98
117
  if (action === "rules" || action === "knowledge")
99
118
  return parseMemoryRulesArgs(args, action);
100
119
  const values = parseNamedArgs(args, { allowedFlags: ALLOWED_FLAGS, valueFlags: VALUE_FLAGS });
@@ -106,6 +125,21 @@ export function parseMemoryStoreArgs(action, args) {
106
125
  if (action !== "search" && (values.flags.has("--limit") || values.booleans.has("--no-recent"))) {
107
126
  throw new Error("--limit and --no-recent only apply to memory search.");
108
127
  }
128
+ if (action !== "search" && (values.booleans.has("--include-originals") || values.booleans.has("--no-originals"))) {
129
+ throw new Error("--include-originals and --no-originals only apply to memory search.");
130
+ }
131
+ if (values.booleans.has("--include-originals") && values.booleans.has("--no-originals")) {
132
+ throw new Error("--include-originals and --no-originals are opposites; name one.");
133
+ }
134
+ if (action !== "search" && (values.booleans.has("--include-source") || values.booleans.has("--no-source"))) {
135
+ throw new Error("--include-source and --no-source only apply to memory search.");
136
+ }
137
+ if (values.booleans.has("--include-source") && values.booleans.has("--no-source")) {
138
+ throw new Error("--include-source and --no-source are opposites; name one.");
139
+ }
140
+ if (action !== "source" && (values.flags.has("--query") || values.flags.has("--chars"))) {
141
+ throw new Error("--query and --chars only apply to memory source.");
142
+ }
109
143
  if (action === "consolidation" && rest.length > 0) {
110
144
  throw new Error("memory consolidation takes no positionals: `cockpit memory consolidation [--container <tag>] [--json]`.");
111
145
  }
@@ -126,6 +160,9 @@ export function parseMemoryStoreArgs(action, args) {
126
160
  || values.booleans.has("--dry-run") || values.booleans.has("--apply"))) {
127
161
  throw new Error("--since, --until, --source, --dry-run and --apply only apply to memory requeue.");
128
162
  }
163
+ if (action !== "exam" && (values.flags.has("--night") || values.booleans.has("--misses"))) {
164
+ throw new Error("--night and --misses only apply to memory exam.");
165
+ }
129
166
  if (values.flags.has("--file") && action !== "save" && action !== "update") {
130
167
  throw new Error("--file only applies to memory save and memory update.");
131
168
  }
@@ -187,11 +224,40 @@ export function parseMemoryStoreArgs(action, args) {
187
224
  const query = rest.join(" ").trim();
188
225
  if (!query)
189
226
  throw new Error("memory search needs a question, e.g. `cockpit memory search \"pagination rule\"`.");
190
- return { ...command, action, query, limit: readLimit(values.flags.get("--limit")), includeRecent: !values.booleans.has("--no-recent") };
227
+ // BLI-19246: both flags absent leaves the choice to the door (exact-detail questions reach the originals).
228
+ const includeOriginals = values.booleans.has("--include-originals") ? true : values.booleans.has("--no-originals") ? false : undefined;
229
+ // BLI-19255: the raw source behind the top hits; absent lets the door decide by the question.
230
+ const includeSource = values.booleans.has("--include-source") ? true : values.booleans.has("--no-source") ? false : undefined;
231
+ return {
232
+ ...command, action, query, limit: readLimit(values.flags.get("--limit")), includeRecent: !values.booleans.has("--no-recent"),
233
+ ...(includeOriginals === undefined ? {} : { includeOriginals }),
234
+ ...(includeSource === undefined ? {} : { includeSource }),
235
+ };
191
236
  }
192
237
  if (action === "consolidation") {
193
238
  return { ...command, action };
194
239
  }
240
+ if (action === "exam") {
241
+ if (rest.length > 0)
242
+ throw new Error("memory exam takes no words: `cockpit memory exam [--night <YYYY-MM-DD>] [--misses] [--json]`.");
243
+ const night = optionalNonEmpty(values.flags.get("--night"));
244
+ if (night && !/^\d{4}-\d{2}-\d{2}$/.test(night))
245
+ throw new Error("--night must be a date like 2026-10-14.");
246
+ return { ...command, action, ...(night ? { night } : {}), misses: values.booleans.has("--misses") };
247
+ }
248
+ if (action === "source") {
249
+ // BLI-19255: one memory, named by the id `cockpit memory search` printed; `--query` steers which part of the source.
250
+ const memoryId = optionalNonEmpty(rest[0]);
251
+ if (!memoryId)
252
+ throw new Error("memory source needs the memory id `cockpit memory search` printed.");
253
+ if (rest.length > 1)
254
+ throw new Error("memory source takes one id.");
255
+ const rawChars = values.flags.get("--chars");
256
+ const chars = rawChars === undefined ? undefined : Number(rawChars);
257
+ if (chars !== undefined && (!Number.isInteger(chars) || chars < 200 || chars > 6000))
258
+ throw new Error("--chars is a whole number from 200 to 6000.");
259
+ return { ...command, action, memoryId, ...(values.flags.get("--query") ? { sourceQuery: values.flags.get("--query") } : {}), ...(chars === undefined ? {} : { sourceChars: chars }) };
260
+ }
195
261
  if (action === "private" || action === "public" || action === "chain") {
196
262
  // BLI-12927: one memory, named by the id `cockpit memory search` printed.
197
263
  const memoryId = optionalNonEmpty(rest[0]);
@@ -200,6 +200,7 @@ const NOTES_ACTIONS = new Set([
200
200
  "paste",
201
201
  "share",
202
202
  "unshare",
203
+ "summarize",
203
204
  "move",
204
205
  "place",
205
206
  // BLI-4383: the notetaker a person connects, and the meetings it records.
@@ -223,6 +224,7 @@ const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
223
224
  "show",
224
225
  "share",
225
226
  "unshare",
227
+ "summarize",
226
228
  "move",
227
229
  "place",
228
230
  // BLI-12926: `recordings` names the note, `recording` names the recording.
@@ -299,7 +301,7 @@ export function parseNotesArgs(args) {
299
301
  const first = values.positionals[0];
300
302
  const action = (first === undefined ? "list" : first);
301
303
  if (!NOTES_ACTIONS.has(action)) {
302
- 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, granola-status, or transcription-stats.`);
304
+ throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, summarize, move, place, accounts, connect, detach, sync, sync-status, recordings, recording, webhook, granola-status, or transcription-stats.`);
303
305
  }
304
306
  const rest = values.positionals.slice(first === undefined ? 0 : 1);
305
307
  const json = values.booleans.has("--json");
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `cockpit people` argument parsing (ticket 19252).
3
+ *
4
+ * cockpit people show [<person>] [--json]
5
+ *
6
+ * The owner-only people models: one profile per principal and teammate. Reads `GET /api/people` with this machine's
7
+ * device token; only the company owner (and the owner's own owner-scoped brain token) is answered. A name of several
8
+ * words needs no quotes. No person lists everyone with a profile.
9
+ */
10
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
11
+ export function parsePeopleArgs(args) {
12
+ const values = parseNamedArgs(args, { allowedFlags: ["--home", "--dashboard-url", "--json"], valueFlags: ["--home", "--dashboard-url"] });
13
+ const [action, ...rest] = values.positionals;
14
+ if (action !== "show")
15
+ throw new Error("people takes one verb: `show`. Example: `cockpit people show` or `cockpit people show Ada Lovelace`.");
16
+ const person = optionalNonEmpty(rest.join(" ").trim());
17
+ return {
18
+ kind: "people",
19
+ action: "show",
20
+ ...(person ? { person } : {}),
21
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
22
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
23
+ json: values.booleans.has("--json"),
24
+ };
25
+ }
@@ -1,8 +1,8 @@
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", "--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
- if (["--by-task", "--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
5
- throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject or --by-task.");
3
+ const values = parseNamedArgs(args, { allowedFlags: ["--by-client", "--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
+ if (["--by-task", "--by-repo", "--by-topic", "--by-subject", "--by-client"].filter(flag => values.booleans.has(flag)).length > 1)
5
+ throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject, --by-task or --by-client.");
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.");
@@ -11,5 +11,5 @@ export function parseUsageArgs(args) {
11
11
  throw new Error("usage takes people, sessions or session <id>.");
12
12
  if (action === "session" && !values.positionals[1])
13
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") };
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"), byClient: values.booleans.has("--by-client"), 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
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `cockpit who` argument parsing (ticket 19251).
3
+ *
4
+ * cockpit who [<topic>] [--layer frontend|backend|data|infrastructure|docs|tooling] [--limit <1-10>] [--json]
5
+ *
6
+ * Who knows what: the ranked owners of a project, a layer or a category, with the evidence counts behind each.
7
+ * Reads `GET /api/who` with this machine's device token. A topic of several words needs no quotes.
8
+ */
9
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
10
+ /** The layers the directory ranks; `general` is a label for "none of these" and owns nothing. */
11
+ export const WHO_LAYERS = ["frontend", "backend", "data", "infrastructure", "docs", "tooling"];
12
+ export function parseWhoArgs(args) {
13
+ const values = parseNamedArgs(args, {
14
+ allowedFlags: ["--layer", "--limit", "--home", "--dashboard-url", "--json"],
15
+ valueFlags: ["--layer", "--limit", "--home", "--dashboard-url"],
16
+ });
17
+ const topic = optionalNonEmpty(values.positionals.join(" ").trim());
18
+ const layer = optionalNonEmpty(values.flags.get("--layer"))?.toLowerCase();
19
+ if (layer && !WHO_LAYERS.includes(layer))
20
+ throw new Error(`--layer wants one of ${WHO_LAYERS.join(", ")}.`);
21
+ const rawLimit = optionalNonEmpty(values.flags.get("--limit"));
22
+ const limit = rawLimit === undefined ? undefined : Number(rawLimit);
23
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 10))
24
+ throw new Error("--limit wants a whole number from 1 to 10.");
25
+ if (layer && !topic)
26
+ throw new Error("--layer needs a topic: `cockpit who Tower --layer backend`.");
27
+ return {
28
+ kind: "who",
29
+ ...(topic ? { topic } : {}),
30
+ ...(layer ? { layer } : {}),
31
+ ...(limit ? { limit } : {}),
32
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
33
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
34
+ json: values.booleans.has("--json"),
35
+ };
36
+ }
@@ -42,9 +42,16 @@ export { parseSpeedArgs } from "./local-args-tower-speed.js";
42
42
  export { parseUsageArgs } from "./local-args-tower-usage.js";
43
43
  export { parseLogsArgs } from "./local-args-tower-logs.js";
44
44
  export { parseBrainArgs } from "./local-args-tower-brain.js";
45
+ export { parseHitsArgs } from "./local-args-tower-hits.js";
46
+ export { parseIntentionsArgs } from "./local-args-tower-intentions.js";
47
+ export { parseWhoArgs } from "./local-args-tower-who.js";
48
+ export { parsePeopleArgs } from "./local-args-tower-people.js";
45
49
  export { parseApprovalsArgs } from "./local-args-tower-approvals.js";
50
+ export { parseDrillsArgs } from "./local-args-tower-drills.js";
46
51
  export { parseManagerArgs } from "./local-args-tower-manager.js";
47
52
  export { parseImageArgs } from "./local-args-tower-image.js";
48
53
  export { parseCareersArgs } from './local-args-tower-careers.js';
49
54
  export { parseRenameArgs } from './local-args-tower-rename.js';
50
- export { parseGlossaryArgs } from "./glossary.js";
55
+ export { parseGlossaryArgs } from "./glossary.js";
56
+ export { parseSpendArgs } from "./spend.js";
57
+ export { parseCiArgs } from "./ci.js";