@bli-cockpit/cli 0.2.286 → 0.2.288
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/capture-opt-outs.js +129 -0
- package/dist/collection-scope.js +201 -0
- package/dist/commands/agent-session-report.js +3 -0
- package/dist/commands/agreements-render.js +61 -0
- package/dist/commands/agreements.js +196 -0
- package/dist/commands/analyze.js +2 -0
- package/dist/commands/backfill-scan.js +16 -11
- package/dist/commands/backfill-scope.js +16 -4
- package/dist/commands/backfill-session-report.js +2 -0
- package/dist/commands/books-files.js +159 -0
- package/dist/commands/books-usage.js +14 -0
- package/dist/commands/books.js +214 -0
- package/dist/commands/cli-io.js +1 -3
- package/dist/commands/docs-share.js +21 -11
- package/dist/commands/doctor-access.js +7 -0
- package/dist/commands/doctor-registration.js +39 -0
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor-shelf.js +62 -0
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor-usage-telemetry.js +18 -2
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/heartbeat-post.js +19 -0
- package/dist/commands/heartbeat.js +13 -2
- package/dist/commands/install-receipts.js +4 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-args-collector-status.js +7 -21
- package/dist/commands/local-args-tower-agreements.js +80 -0
- package/dist/commands/local-args-tower-docs-msg.js +17 -5
- package/dist/commands/local-args-tower.js +2 -0
- package/dist/commands/local-args.js +6 -1
- package/dist/commands/local-command-shapes-agreements.js +5 -0
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help-agreements.js +24 -0
- package/dist/commands/local-help-commands-tower-ops.js +3 -3
- package/dist/commands/local-help-commands-tower.js +13 -6
- package/dist/commands/local-help-commands.js +20 -5
- package/dist/commands/local-help.js +8 -2
- package/dist/commands/local.js +9 -0
- package/dist/commands/memory-capture-prompt.js +130 -0
- package/dist/commands/memory-install-claude.js +51 -4
- package/dist/commands/memory-install-report.js +19 -3
- package/dist/commands/memory-install-toml.js +22 -0
- package/dist/commands/memory-install.js +110 -11
- package/dist/commands/memory-store.js +5 -0
- package/dist/commands/memory-switch.js +165 -0
- package/dist/commands/memory-uninstall.js +187 -0
- package/dist/commands/memory-verb-list.js +6 -2
- package/dist/commands/onboard.js +5 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/roots-label.js +85 -0
- package/dist/commands/roots.js +28 -10
- package/dist/commands/session-sync-plan.js +8 -4
- package/dist/commands/session-sync-scan.js +25 -3
- package/dist/commands/session-sync-shelf.js +143 -0
- package/dist/commands/session-sync-upload.js +6 -2
- package/dist/commands/session-sync.js +68 -8
- package/dist/commands/settings.js +1 -1
- package/dist/commands/shelf.js +165 -0
- package/dist/commands/sources-check.js +2 -2
- package/dist/commands/sources-reconciliation.js +2 -1
- package/dist/commands/speed-seen.js +2 -2
- package/dist/commands/sync-doctor.js +2 -1
- package/dist/commands/sync-receipt.js +1 -0
- package/dist/commands/sync-report.js +1 -0
- package/dist/commands/sync-stub-heal.js +2 -2
- package/dist/commands/sync.js +12 -0
- package/dist/error-message.js +11 -0
- package/dist/evidence-reconcile-client.js +2 -0
- package/dist/evidence-redelivery-plan.js +6 -0
- package/dist/evidence-redelivery.js +6 -1
- package/dist/evidence-stubs-client.js +2 -0
- package/dist/evidence-transcripts-client.js +2 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/dist/founder-standing-file.js +52 -0
- package/dist/founder-standing.js +117 -0
- package/dist/image-describe-client.js +2 -1
- package/dist/live-founder-memo.js +57 -0
- package/dist/local-state-config.js +13 -7
- package/dist/memory-capture-choice.js +73 -0
- package/dist/root-work.js +55 -7
- package/dist/shelf-decision.js +52 -0
- package/dist/shelf-headers.js +15 -0
- package/dist/shelf-reports.js +167 -0
- package/dist/shelf-state.js +112 -0
- package/dist/source-opt-out.js +83 -6
- package/dist/state-dir-audit.js +2 -0
- package/dist/tower-client.js +4 -1
- package/dist/upload-agent-artifacts.js +2 -0
- package/dist/upload-envelope-build.js +2 -2
- package/dist/upload-ingest-receipt.js +2 -0
- package/dist/upload-session-reports-wire.js +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `cockpit agreements` help (docs/architecture/agreements.md), kept beside the parser in local-args-tower-agreements.ts. */
|
|
2
|
+
export const AGREEMENTS_HELP = [
|
|
3
|
+
"agreements",
|
|
4
|
+
[
|
|
5
|
+
"Usage: cockpit agreements [list] [--dashboard-url <url>] [--json]",
|
|
6
|
+
"",
|
|
7
|
+
" cockpit agreements show <request-id> the exact text, its boxes (required or optional), the status, and the record once signed",
|
|
8
|
+
" cockpit agreements copy <request-id> [--out <file>] your signed copy as plain text: the text, then the record. Signed agreements only",
|
|
9
|
+
" cockpit agreements documents founders: every document, its versions, and who was asked to sign each",
|
|
10
|
+
" cockpit agreements draft founders: save a draft; the POST /api/agreements/documents body as JSON on stdin",
|
|
11
|
+
" cockpit agreements request --version <id> --person <user-id> [--due <date>] founders: ask a person to sign a published version",
|
|
12
|
+
" cockpit agreements cancel <request-id> founders: withdraw a request that is not signed",
|
|
13
|
+
"A founder publishes a document as a fixed version and asks named people to sign it. Your own requests are yours to read; only the founders read anyone else's.",
|
|
14
|
+
"Signing is done in Tower in your browser, never here: show prints the link. Asking for a signing code, signing and publishing a version have no CLI verb or MCP tool,",
|
|
15
|
+
"because an agent holding your token can often read your inbox and could fetch the code and sign, and a published version freezes text people will sign.",
|
|
16
|
+
"draft JSON: {slug, title, label, language?, body, consent_boxes: [{key, wording, required}], needs_countersign?, required_for_access?, needs_handwritten_confirmation?}",
|
|
17
|
+
"creates a document; {document_id, label, ...} adds a draft version to it; {version_id, label, ...} edits a draft that is not published.",
|
|
18
|
+
"--home <dir> and --dashboard-url <url> are for tests and staging.",
|
|
19
|
+
"--json writes one object to stdout (the door's body) and nothing else; a refusal is { ok: false, error, detail } and exits 1",
|
|
20
|
+
"(founders_only, not_found, not_open, not_published, already_asked, slug_taken, label_taken, already_published, invalid_input, not_paired, ...).",
|
|
21
|
+
"Teaching: docs/architecture/agreements.md. The bli-tower MCP server has the twins: agreements_list, agreements_show, agreements_copy, agreements_documents,",
|
|
22
|
+
"agreements_draft, agreements_request, agreements_cancel.",
|
|
23
|
+
],
|
|
24
|
+
];
|
|
@@ -41,7 +41,7 @@ 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
|
-
"cockpit logs keys mint|rotate <source> [--json]:
|
|
44
|
+
"cockpit logs keys mint|rotate <source> [--json]: admin or developer only; prints the source ingest key once. Rotation invalidates the old key.",
|
|
45
45
|
"cockpit logs search <project> [--since 30m|6h (default 1h) | --from <iso> [--to <iso>]] [--source gcp|supabase|vercel|railway|deploys|db-host|<label>] [--stream api|postgres|auth]",
|
|
46
46
|
" [--status 500|5xx|500-503] [--method GET] [--route <text>] [--severity error] [--q <text>] [--limit <n>] [--context] [--json]: owner or admin only.",
|
|
47
47
|
" Reads the project's RAW logs live (window at most 24h), scrubs every line, caps the answer (default 100, at most 200) and says \"capped at N of M\".",
|
|
@@ -75,7 +75,7 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
75
75
|
"Retry: cockpit brain retry <id...> [--all-needs-human] [--project <p>] --reason <text> [--json]",
|
|
76
76
|
"Ids are incident UUIDs or BLI issue identifiers. Retry reopens needs_human or not_actionable rows, resets attempts and claims,",
|
|
77
77
|
"and records who, why and when. --all-needs-human selects only needs_human; --project narrows every selection. Other states are skipped.",
|
|
78
|
-
"Only members and
|
|
78
|
+
"Only team members (members, developers and owners) can retry. One comment is posted per incident issue. MCP twin: brain_retry.",
|
|
79
79
|
"",
|
|
80
80
|
"Ladder (BLI-19250, the Tower owner only): cockpit brain ladder show|promote <class> [--anyway] [--note <text>]|demote <class>|predict \"<question>\"|answer \"<reply>\" [--json]",
|
|
81
81
|
"Before the brain or an agent asks you for a call it writes down what it predicts you will answer; your answer scores it. `show` lists",
|
|
@@ -403,7 +403,7 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
403
403
|
"is `scout_board` on the bli-tower server, over the same GET /api/cockpit/scout door.",
|
|
404
404
|
"--days <n> widens or narrows the window (1 to 90; the board defaults to 14).",
|
|
405
405
|
"Reading follows the Scout page audience setting, so whoever can open /scout can run this.",
|
|
406
|
-
"start, dismiss, and undo move one card and are
|
|
406
|
+
"start, dismiss, and undo move one card and are admin or developer actions on every surface;",
|
|
407
407
|
"a refusal is printed as Tower worded it and the board stays readable underneath.",
|
|
408
408
|
"The id can be the first 6 or more characters printed beside each card; an ambiguous",
|
|
409
409
|
"prefix is refused with the candidates rather than acting on the first match.",
|
|
@@ -20,6 +20,7 @@ import { DOCS_INBOX_HELP } from "./local-help-docs-inbox.js";
|
|
|
20
20
|
import { DOCS_RECOVERY_HELP } from "./local-help-docs-recovery.js";
|
|
21
21
|
import { PROJECT_HELP } from "./local-help-project.js";
|
|
22
22
|
import { MILESTONE_HELP } from "./local-help-milestone.js";
|
|
23
|
+
import { AGREEMENTS_HELP } from "./local-help-agreements.js";
|
|
23
24
|
import { INBOX_HELP_LINES } from "./local-help-inbox.js";
|
|
24
25
|
import { WORK_NOTIFY_HELP_LINES } from "./local-help-work-notify.js";
|
|
25
26
|
import { VIEWS_HELP_LINES } from "./local-help-views.js";
|
|
@@ -76,14 +77,14 @@ export const TOWER_COMMAND_HELP = [
|
|
|
76
77
|
" --offset <n> --limit <n> — a character range of the body, instead of the whole thing.",
|
|
77
78
|
" --json shape: one ref answers { document }, unchanged from before BLI-10450. Several refs answer { documents: [{ ref, ok, document } | { ref, ok: false, error, detail }] }. The meta line (and --json document.revision) shows the page revision. Agent writes are never queued offline: a write that did not reach Tower fails with its reason, and `docs mutation-status <request-id>` tells whether it landed. Pass the revision you read as expected_revision.",
|
|
78
79
|
"read --parent <id>|root --bodies [--limit <n>] — every document filed directly under that parent, bodies included, in ONE call (GET /api/docs/documents?parent_id=...&bodies=true). --limit here caps the DOCUMENT COUNT (1-50), not characters. --json shape: { documents: [{ id, ..., body_markdown }] }.",
|
|
79
|
-
"create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default. --json shape: { document }.",
|
|
80
|
+
"create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default. A new page is visible to everyone (org) unless you pass --visibility private; a page made with --parent starts with its parent's visibility and a copy of the parent's named people, and the parent's owner joins it as an editor (share it with `cockpit docs share`). --json shape: { document }.",
|
|
80
81
|
"update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] [--expected-revision <n>] [--request-id <uuid>] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document, mutation }.",
|
|
81
82
|
" --expected-revision <n> is the page revision you read (`docs read --json` shows document.revision). The save is one atomic step: if someone saved since, nothing is written and you get revision_conflict with the current page (--json: mutation.current). Omit it for an unconditional save. Every save advances the revision.",
|
|
82
83
|
" --request-id <uuid> makes a retry apply once (minted and printed when absent). Reusing it with different input is request_id_reused. If the response is lost, run `cockpit docs mutation-status <uuid>`, then retry the SAME input with the SAME id.",
|
|
83
84
|
"mutation-status <request-uuid> — your own save receipt: committed (with the stored revision), not_committed (with the reason) or unknown_check_again (no receipt yet; it never proves nothing changed). --json shape: { mutation }.",
|
|
84
85
|
" --allow-empty clears the page on purpose. A body that would empty a document that holds text is refused (refused_empty_body) unless you say so, because far more often it is a surface that lost the content than a person who meant it.",
|
|
85
|
-
"access <id|slug> — who may do what on a page and why, in plain sentences (BLI-21313): your role (author, admin, granted_editor, reader), can_read/can_edit/can_manage, the named editors you may see (the owner and administrators
|
|
86
|
-
"share <id|slug> --add <email> | --remove <email> [--reason \"<note>\"] — owner
|
|
86
|
+
"access <id|slug> — who may do what on a page and why, in plain sentences (BLI-21313, BLI-23558): your role (author, admin, granted_editor, granted_viewer, reader), can_read/can_edit/can_manage, the named editors and viewers you may see (on a private page everyone who can open it sees everyone named; on an Everyone page the owner and administrators do, and a named editor sees only their own entry), and the grant history for the owner. An administrator has no special access to a private page: they read it only if the owner named them. The same explanation the page, its versions and its export carry. --json shape: { ok, document_id, permission: { role, can_read, can_edit, can_manage, editors, viewers, editors_complete, reasons }, events }.",
|
|
87
|
+
"share <id|slug> --add <email> [--role viewer|editor] | --remove <email> [--reason \"<note>\"] — the page owner only (an administrator too, on an Everyone page). Names a person on a page, private or Everyone, by exact email, never fuzzy. --role viewer lets them read it; --role editor (the default) also lets them change its title and text, but not visibility, move, delete or who has access. Adding someone already named with a different role changes their role; --remove takes them off whatever role they hold. Refusals: not_grant_manager, person_not_found, grantee_is_author, invalid_role, document_not_found_or_unreadable. Adding someone already named in that role, or removing someone not named, answers outcome unchanged. --json shape: { ok, outcome, reason, role, document_id, grantee_id, request_id, permission }.",
|
|
87
88
|
"comments <id|slug> [--status open|resolved|all] [--cursor <c>] — the page's discussion (BLI-21701): threads newest activity first, 50 per page, each with its comments (author, time, edited), a \"A comment was deleted by <who> on <when>.\" line for a deleted one, and who resolved a resolved thread. Anyone who can read the page can read it. Ends with the open/resolved counts, a `More: --cursor` line, and a coverage note when some comments could not be read. --json shape: { ok, status, can_comment, counts: { open, resolved }, threads: [{ id, resolved, resolved_at, resolved_by_name, comments: [{ id, author_name, body, created_at, edited_at, deleted, marker, can_edit, can_delete }] }], next_cursor, coverage: exhausted|capped|failed_partial, coverage_note, note }.",
|
|
88
89
|
"comment <id|slug> \"<text>\" | - [--request-id <uuid>] — starts a thread (the text is the second argument, or `-` to read stdin; 1-10,000 characters). Add --reply <thread id> to answer a thread, --edit <comment id> to replace your own comment's text (earlier text is not kept), --delete <comment id> (its author, the page owner or an administrator; a soft delete), --resolve <thread id> or --reopen <thread id>. Exactly one action. Only the page owner, administrators and named editors of an organization page can write; everyone else who can read the page is refused with not_commenter and nothing is written. Other labels, verbatim: not_comment_author, comment_not_found, thread_not_found, invalid_body, document_not_found_or_unreadable. The same --request-id twice gives one comment (outcome unchanged); resolving a resolved thread answers unchanged. --json shape: { ok, outcome, reason, document_id, thread_id, comment_id, request_id }.",
|
|
89
90
|
"delete <id|slug> [<id|slug>...] — soft-deletes one or more documents (BLI-10555): the browser's own delete, sets deleted_at, never a real SQL DELETE. Recoverable: `cockpit docs trash` lists what you may restore and `cockpit docs restore <id>` brings it back (BLI-21413). Deleting a parent does NOT delete its children; they keep their own parent_id and stay exactly where they were. Each ref is one exact lookup, then one DELETE each — one bad ref never loses the rest. --json shape: one ref answers { ok, document_id }; several answer { documents: [{ ref, ok, document_id } | { ref, ok: false, error, detail }] }.",
|
|
@@ -220,6 +221,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
220
221
|
],
|
|
221
222
|
PROJECT_HELP,
|
|
222
223
|
MILESTONE_HELP,
|
|
224
|
+
AGREEMENTS_HELP,
|
|
223
225
|
[
|
|
224
226
|
"speed",
|
|
225
227
|
[
|
|
@@ -241,7 +243,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
241
243
|
"--seen reads what REAL visits saw instead: per page the visit count, p50 and p95 first-useful milliseconds (the moment",
|
|
242
244
|
"real content showed, from the last-known screen or the fresh one), the share painted from the snapshot, and median HTML",
|
|
243
245
|
"and script transfer. The browser sends one metadata-only beacon per page view; nothing is measured from this machine.",
|
|
244
|
-
"--since <n>m|h|d is the window (default 24h, at most 30d). --everyone reads
|
|
246
|
+
"--since <n>m|h|d is the window (default 24h, at most 30d). --everyone reads other people's visits (only Vee, Jeetesh and Ed; a founder's visits stay with that founder; anyone else is refused everyone_needs_super_admin and still gets their own).",
|
|
245
247
|
"A member sees their own visits. --page limits the table to those pages. The same read is the speed_seen MCP tool.",
|
|
246
248
|
],
|
|
247
249
|
],
|
|
@@ -256,7 +258,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
256
258
|
"--by-client (ticket 19258) shows which app each person worked in (Claude Code CLI, desktop or IDE; Codex CLI, desktop, IDE or exec): the share of sessions and of session time, and the median hours from session start to the pull request opened and merged and the active minutes per merged PR, each with its N, compared only within one person and only with 5 or more sessions on each side. --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.",
|
|
257
259
|
"Claude Code and Codex usage per person: sessions observed and extracted, tokens (total, output,",
|
|
258
260
|
"and the input / cache split when the row carries it), and an API list-price equivalent that is",
|
|
259
|
-
"labelled as such and is never actual spend.
|
|
261
|
+
"labelled as such and is never actual spend. You see your own numbers; Vee, Jeetesh and Ed also see everyone else's, except a founder's, which only that founder sees (BLI-23585, BLI-23624). When rows were left out the answer says how many people are shown and how many were withheld. `usage sessions` follows the same rule.",
|
|
260
262
|
"--since takes <n>d (1 to 365), <n>h (1 to 8760), or an ISO timestamp; --until an ISO timestamp (default now).",
|
|
261
263
|
"--detail adds Output, Input, Cache read and Cache creation. Counts use K/M/B; dollars are rounded.",
|
|
262
264
|
"PRs opened, PRs merged and Commits come from the pull requests Tower mirrors from GitHub, matched to people by GitHub login. n/a means Tower cannot match that person to a login: unknown, not zero. The footer says how many repositories the mirror holds, the range of its pull requests and what matched nobody; --json lists the repositories.",
|
|
@@ -412,7 +414,7 @@ export const TOWER_COMMAND_HELP = [
|
|
|
412
414
|
[
|
|
413
415
|
"memory",
|
|
414
416
|
[
|
|
415
|
-
"Usage: cockpit memory [map|knowledge|rules|search|save|update|forget|private|public|profile|consolidation|chain|source|exam|requeue|install|status|log] [flags]",
|
|
417
|
+
"Usage: cockpit memory [map|knowledge|rules|search|save|update|forget|private|public|profile|consolidation|chain|source|exam|requeue|on|off|install|uninstall|status|log] [flags]",
|
|
416
418
|
"",
|
|
417
419
|
" map [--json] — projects, topics, memory counts and importance from the shared map.",
|
|
418
420
|
" knowledge [--person <name>] [--project <name>] [--by-project] [--limit <n>] [--json] — ranked project ideas.",
|
|
@@ -469,6 +471,11 @@ export const TOWER_COMMAND_HELP = [
|
|
|
469
471
|
"Idempotent: it merges with what is already there, never duplicates its own",
|
|
470
472
|
"entries, and reads the stored config back before reporting success.",
|
|
471
473
|
"`cockpit doctor` runs it, and the sync tick re-runs it at most once a day.",
|
|
474
|
+
"off (alias uninstall) turns BLI Memory off on THIS machine: it records the choice, removes",
|
|
475
|
+
"our hooks and MCP entry from both hosts (your other hooks stay), and doctor, onboard,",
|
|
476
|
+
"update and the sync tick never add them back; the Stop and PreCompact hooks send nothing.",
|
|
477
|
+
"on (alias install) records the opposite choice and registers them again. Each prints",
|
|
478
|
+
"one line saying what changed and how to undo it. See docs/architecture/agent-memory.md.",
|
|
472
479
|
"See docs/runbooks/bli-memory-install.md.",
|
|
473
480
|
"log appends an opinion to ~/.codex/AGENT-EXPERIENCE.md and posts it to Tower.",
|
|
474
481
|
"Use --reason-stdin instead of a quoted reason; --json reports shipped status.",
|
|
@@ -17,16 +17,29 @@
|
|
|
17
17
|
* something breaks, so it is a user-visible contract like any other.
|
|
18
18
|
*/
|
|
19
19
|
import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
20
|
+
import { BOOKS_USAGE } from "./books-usage.js";
|
|
20
21
|
import { localCommandHelp, SURFACE_ALIASES } from "./local-help.js";
|
|
21
22
|
import { TOWER_COMMAND_HELP } from "./local-help-commands-tower.js";
|
|
22
23
|
import { TOWER_OPS_COMMAND_HELP } from "./local-help-commands-tower-ops.js";
|
|
23
24
|
/** One manual per collector command; the Tower nouns are spread in from their own file. */
|
|
24
25
|
const COMMAND_HELP = new Map([
|
|
26
|
+
["books", [BOOKS_USAGE, "Client Books: each engagement's record (timeline, actions, documents, meetings, ...). A member of a book reads it; a member with edit access, or a founder, writes it; only founders create books and manage members. put without --merge replaces the record's data; --merge changes only the fields given. --version <n> makes a write conditional: refused as version_conflict when someone changed the record since you read version n (0 = must not exist yet). delete is soft. export writes <dir>/<collection>/<doc_id>.json; import reads that layout and writes 50 records per batch. MCP twins: books_list, books_create, books_members, books_read, books_write, books_batch. Docs: docs/architecture/client-books.md."]],
|
|
25
27
|
["glossary", ["Usage: cockpit glossary list|add <wrong> --to <name>|remove <wrong> [--json]", "Quote phrases. Removing a form prevents future corrections of it."]],
|
|
26
|
-
["skills", ["Usage: cockpit skills library [--weeks <1-52>] [--json] | cockpit skills refresh [--weeks <1-104>] [--json]", "The Skill Library: every agent skill and command our GitHub repos define (name, description, repo, path, versions) with weekly use by person, from the sessions Tower already collects. Uses that match no scanned skill are listed as unmatched, never dropped. The first line is the coverage: repos scanned, skills, invocations matched of total.
|
|
28
|
+
["skills", ["Usage: cockpit skills library [--weeks <1-52>] [--json] | cockpit skills refresh [--weeks <1-104>] [--json]", "The Skill Library: every agent skill and command our GitHub repos define (name, description, repo, path, versions) with weekly use by person, from the sessions Tower already collects. Uses that match no scanned skill are listed as unmatched, never dropped. The first line is the coverage: repos scanned, skills, invocations matched of total. You see your own usage; Vee, Jeetesh and Ed see everyone's except a founder's. refresh scans the repos and recomputes the weekly totals now (super_admin only; a stale library also refreshes itself when read). MCP twins: skills_library, skills_refresh. Web: TEAM > Skills."]],
|
|
27
29
|
["push", ["Usage: cockpit push status | test [--dashboard-url <url>] [--json]", "Phone notifications. status lists your saved devices and whether the server has its push keys; test sends a test push to every saved device. Turn a phone on from Settings on the phone."]],
|
|
28
30
|
["spend", ["Usage: cockpit spend memory [--days 1|7|30] [--dashboard-url <url>] [--json]", "Spend by memory operation (write, recall, dream, update_forget, backfill, label, exam, predictor, people, who, intentions, salience, chat, other), ranked top first with list and real dollars, share, trend against the previous window, and how much of the real OpenAI bill the ledger explains. What it does not explain is shown as untagged. A super_admin reads it."]],
|
|
29
|
-
["roots", [
|
|
31
|
+
["roots", [
|
|
32
|
+
"Usage: cockpit roots add <dir> [--allow-home-root] [--json] | label <dir> team|drawer [--json] | list [--json]",
|
|
33
|
+
"add approves a work directory, saves an approval receipt, and adds it to collection. Run cockpit doctor afterward.",
|
|
34
|
+
"label (founders' laptops): team sends that folder's sessions and memories to Tower as today; drawer keeps them on this laptop and sends only bare numbers. safe is refused until the safe is live.",
|
|
35
|
+
"list shows each approved or labelled folder and the shelf it resolves to.",
|
|
36
|
+
]],
|
|
37
|
+
["shelf", [
|
|
38
|
+
"Usage: cockpit shelf status | default team|drawer | metrics --allow <day,tool,model,spawn|none> | drawer purge --older-than <days> [--json]",
|
|
39
|
+
"A founder's shelves on THIS laptop. status: Tower's founder lock, the default, and what is in the drawer here (not backed up).",
|
|
40
|
+
"default: where work outside every labelled folder goes. metrics: the opt-in categories a drawer session's bare record may carry (all off by default).",
|
|
41
|
+
"drawer purge: the only way drawer items are removed.",
|
|
42
|
+
]],
|
|
30
43
|
// BLI-3709: the Tower nouns live next door, split along the same seam the
|
|
31
44
|
// argument parsers already use — see `local-help-commands-tower.ts`.
|
|
32
45
|
...TOWER_COMMAND_HELP,
|
|
@@ -92,12 +105,14 @@ const COMMAND_HELP = new Map([
|
|
|
92
105
|
[
|
|
93
106
|
"doctor",
|
|
94
107
|
[
|
|
95
|
-
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
108
|
+
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--no-usage-telemetry] [--no-memory] [--lock-wait <seconds>] [--backfill-budget <seconds>] [--json]",
|
|
96
109
|
"",
|
|
97
110
|
"Diagnose, repair, and verify this machine.",
|
|
98
111
|
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
99
112
|
"roots-cover-work is informational: it names how many sessions are inside vs. outside saved roots (all already uploaded, outside ones labelled) and gives cockpit roots add commands for attributing a folder's sessions to a repo (outside-root transcripts upload either way). It never needs a fix.",
|
|
100
113
|
"--no-usage-telemetry switches off Claude Code and Codex usage export, including later doctor runs.",
|
|
114
|
+
"--no-memory turns BLI Memory off on this machine (same as `cockpit memory off`), including later runs and the background sync.",
|
|
115
|
+
"In a terminal, the first doctor or onboard asks once whether BLI Memory may save your sessions; `cockpit memory on|off` changes the answer.",
|
|
101
116
|
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
102
117
|
"--backfill-budget defaults to 900 seconds; catch-up repeats chunks until complete, with progress every 30 seconds.",
|
|
103
118
|
"Doctor never waits for the background sync: a running sync is reported as running (with how long ago it started) and doctor finishes; a sync that stopped responding or made no progress for 15 minutes is ended and doctor carries on. --lock-wait is accepted and changes nothing. Progress prints at most every 30 seconds and says what changed.",
|
|
@@ -199,7 +214,7 @@ const COMMAND_HELP = new Map([
|
|
|
199
214
|
" cockpit team invite <email> --role <role> [--team-id <uuid>]",
|
|
200
215
|
" Admin only. Defaults to your own team; Tower emails them a sign-in link.",
|
|
201
216
|
" cockpit team role <userId> --role <role> [--yes]",
|
|
202
|
-
" Admin only. Asks first unless --yes. You cannot change your own role.",
|
|
217
|
+
" Admin only. Asks first unless --yes. You cannot change your own role. Making someone an admin or developer works only from a signed-in browser.",
|
|
203
218
|
" cockpit team grant <person-uuid> careers [see|act] [--json]",
|
|
204
219
|
" cockpit team revoke <person-uuid> careers [see|act] [--json]",
|
|
205
220
|
" Super admins manage separate See and Act grants. Default level: see.",
|
|
@@ -210,7 +225,7 @@ const COMMAND_HELP = new Map([
|
|
|
210
225
|
" (revoked reason, or when it was approved, or that it's still waiting).",
|
|
211
226
|
" Admin only.",
|
|
212
227
|
" cockpit team device scope <id|name> notetaker_bot|none [--json]",
|
|
213
|
-
"
|
|
228
|
+
" Admin or developer only. Gives the meeting bot's device the scope Tower's claim door requires,",
|
|
214
229
|
" or takes it away. Without it the bot is refused every meeting (bot_scope_required).",
|
|
215
230
|
" cockpit team device mark-service <id|name> --reason <label> [--json]",
|
|
216
231
|
" cockpit team device unmark-service <id|name> [--json]",
|
|
@@ -36,6 +36,7 @@ export const SURFACE_ALIASES = new Map([
|
|
|
36
36
|
]);
|
|
37
37
|
export const rootCommandNames = new Set([
|
|
38
38
|
"roots",
|
|
39
|
+
"shelf",
|
|
39
40
|
"onboard",
|
|
40
41
|
"update",
|
|
41
42
|
"upgrade",
|
|
@@ -75,6 +76,7 @@ export const rootCommandNames = new Set([
|
|
|
75
76
|
"clean",
|
|
76
77
|
"sources",
|
|
77
78
|
"glossary",
|
|
79
|
+
"books",
|
|
78
80
|
"skills",
|
|
79
81
|
"spend",
|
|
80
82
|
"push",
|
|
@@ -102,6 +104,7 @@ export const rootCommandNames = new Set([
|
|
|
102
104
|
"people",
|
|
103
105
|
"approvals",
|
|
104
106
|
"drills",
|
|
107
|
+
"agreements",
|
|
105
108
|
"manager",
|
|
106
109
|
"image",
|
|
107
110
|
...SURFACE_ALIASES.keys(),
|
|
@@ -110,7 +113,8 @@ export const rootCommandNames = new Set([
|
|
|
110
113
|
export const publicCommandNames = new Set([...rootCommandNames].filter((name) => name !== "do-everything" && name !== "fix"));
|
|
111
114
|
/** The one-screen summary `cockpit --help` prints: one line per command, then the shared footnotes. */
|
|
112
115
|
const COMMAND_SUMMARY_LINES = [
|
|
113
|
-
" cockpit roots add <dir> [--allow-home-root] [--json]",
|
|
116
|
+
" cockpit roots add <dir> [--allow-home-root] [--json] | label <dir> team|drawer | list",
|
|
117
|
+
" cockpit shelf status | default team|drawer | metrics --allow <categories|none> | drawer purge --older-than <days> [--json]",
|
|
114
118
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
115
119
|
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
116
120
|
" cockpit upgrade [same flags as update]",
|
|
@@ -148,7 +152,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
148
152
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
149
153
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
150
154
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
151
|
-
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
155
|
+
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|profile|on|off|install|uninstall|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--include-originals|--no-originals] [--include-source|--no-source] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
152
156
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
153
157
|
" cockpit sources check [--reconcile] [--older-than <days>] [--dashboard-url <url>] [--json]",
|
|
154
158
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>|delete <id|slug>|trash|versions <id|slug>|restore <id>] [--parent <id>|root|--clear-parent] [--query <text> [--word]] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
|
@@ -164,6 +168,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
164
168
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
165
169
|
" cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|interview <id> <--at <iso>|--clear>|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
|
|
166
170
|
" cockpit glossary list|add <wrong> --to <name>|remove <wrong> [--dashboard-url <url>] [--json]",
|
|
171
|
+
" cockpit books list|create|members|ls|get|put|delete|batch|export|import ... [--dashboard-url <url>] [--json]",
|
|
167
172
|
" cockpit skills library [--weeks <1-52>] | refresh [--weeks <1-104>] [--dashboard-url <url>] [--json]",
|
|
168
173
|
" cockpit spend memory [--days 1|7|30] [--dashboard-url <url>] [--json]",
|
|
169
174
|
" cockpit push status|test [--dashboard-url <url>] [--json]",
|
|
@@ -181,6 +186,7 @@ const COMMAND_SUMMARY_LINES = [
|
|
|
181
186
|
" cockpit ai-skills [--who <me|team|email|user-id>] [--weeks <n>] [--week <YYYY-MM-DD>] [--dashboard-url <url>] [--json]",
|
|
182
187
|
" cockpit approvals [list|show <id>|create|approve <id> [--edit]|edit <id> [--approve]|reject <id>|rules [--active true|false]|rules create <card-id> [--any-audience] [--label <text>]|rules revoke <rule-id> [--reason <text>]] [--status <s>] [--limit <n>] [--before <cursor>] [--dashboard-url <url>] [--json]",
|
|
183
188
|
" cockpit drills [list|start|answer --confidence <1-5> [--text <words>] [--attempt <id>]|theme|theme set <theme> [--play <t>] [--watch <t>] [--train <t>]|review] [--dashboard-url <url>] [--json]",
|
|
189
|
+
" cockpit agreements [list|show <request-id>|copy <request-id> [--out <file>]|documents|draft|request --version <id> --person <user-id> [--due <date>]|cancel <request-id>] [--dashboard-url <url>] [--json]",
|
|
184
190
|
" cockpit manager <clients|client <client>|run <run-id>|list <client>|queue <client>|show <id>|add <client> --title <t>|edit <id> --version <v>|done|reopen|link|approve|dismiss|proposal|archive|unarchive> [--timeline] [--dashboard-url <url>] [--json]",
|
|
185
191
|
" cockpit image <describe <file> [--session <id>]|show <sha256-or-prefix>> [--dashboard-url <url>] [--json]",
|
|
186
192
|
" cockpit setup [--since <duration|date>] [--until <date>] [--machines] [--details] [--person <name|email>] [--compare <name,name>] [--json]",
|
package/dist/commands/local.js
CHANGED
|
@@ -9,6 +9,8 @@ export { rootCommandNames, localCommandHelp, SURFACE_ALIASES, unknownCommandJson
|
|
|
9
9
|
/** Commands that act on THIS machine; the rest talk to Tower. */
|
|
10
10
|
const MACHINE_COMMAND_KINDS = [
|
|
11
11
|
"roots",
|
|
12
|
+
// BLI-23478 (three shelves): acts on this laptop only.
|
|
13
|
+
"shelf",
|
|
12
14
|
"install",
|
|
13
15
|
"onboard",
|
|
14
16
|
"update",
|
|
@@ -81,6 +83,7 @@ export async function runLocalCockpitCli(argv, io = { ...defaultIo(), runDoctorA
|
|
|
81
83
|
async function runMachineCommand(command, io) {
|
|
82
84
|
switch (command.kind) {
|
|
83
85
|
case "roots": return await (await import("./roots.js")).runRoots(command, io);
|
|
86
|
+
case "shelf": return await (await import("./shelf.js")).runShelf(command, io);
|
|
84
87
|
case "install": return await (await import("./install-update.js")).runInstall(command, io);
|
|
85
88
|
case "onboard": return await (await import("./onboard.js")).runOnboard(command, io);
|
|
86
89
|
case "update": return await (await import("./install-update.js")).runUpdate(command, io);
|
|
@@ -109,6 +112,10 @@ async function runMachineCommand(command, io) {
|
|
|
109
112
|
// caller asks for it by name in code.
|
|
110
113
|
if (command.action === "help")
|
|
111
114
|
return (await import("./memory-store.js")).runMemoryVerbList(io, command.json);
|
|
115
|
+
// BLI-22640: the per-machine switch. `install`/`status` keep their page.
|
|
116
|
+
if (command.action === "on" || command.action === "off" || command.action === "uninstall") {
|
|
117
|
+
return await (await import("./memory-switch.js")).runMemorySwitch({ ...command, action: command.action }, io);
|
|
118
|
+
}
|
|
112
119
|
return await (await import("./memory-install.js")).runMemoryInstall(command, io);
|
|
113
120
|
case "memory-store": return await (await import("./memory-store.js")).runMemoryStore(command, io);
|
|
114
121
|
case "clean": return await (await import("./clean.js")).runClean(command, io);
|
|
@@ -136,6 +143,7 @@ async function runTowerCommand(command, io) {
|
|
|
136
143
|
case "correct": return await (await import("./correct.js")).runCorrect(command, io);
|
|
137
144
|
case "notes": return await (await import("./notes.js")).runNotes(command, io);
|
|
138
145
|
case "glossary": return await (await import("./glossary.js")).runGlossary(command, io);
|
|
146
|
+
case "books": return await (await import("./books.js")).runBooks(command, io);
|
|
139
147
|
case "skills": return await (await import("./skills.js")).runSkills(command, io);
|
|
140
148
|
case "sessions-forget": return await (await import("./sessions-forget.js")).runSessionsForget(command, io);
|
|
141
149
|
case "spend": return await (await import("./spend.js")).runSpend(command, io);
|
|
@@ -162,6 +170,7 @@ async function runTowerCommand(command, io) {
|
|
|
162
170
|
case "people": return await (await import("./people.js")).runPeople(command, io);
|
|
163
171
|
case "approvals": return await (await import("./approvals.js")).runApprovals(command, io);
|
|
164
172
|
case "drills": return await (await import("./approvals.js")).runDrills(command, io);
|
|
173
|
+
case "agreements": return await (await import("./agreements.js")).runAgreements(command, io);
|
|
165
174
|
case "manager": return await (await import("./manager.js")).runManager(command, io);
|
|
166
175
|
case "image": return await (await import("./image.js")).runImage(command, io);
|
|
167
176
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-time BLI Memory question (BLI-22640).
|
|
3
|
+
*
|
|
4
|
+
* An interactive `cockpit doctor` or `cockpit onboard` on a machine that has
|
|
5
|
+
* never chosen asks once, and the answer becomes this machine's choice
|
|
6
|
+
* (`memory-capture-choice.ts`). Once answered, never asked again: `cockpit
|
|
7
|
+
* memory on|off` is how a person changes their mind.
|
|
8
|
+
*
|
|
9
|
+
* Asked only when a person is plainly there: stdin AND stdout are terminals,
|
|
10
|
+
* `--json` is off, and this is not the scheduler's tick, a dry run or a
|
|
11
|
+
* `--check`. launchd, Task Scheduler, CI and every piped run keep today's
|
|
12
|
+
* default (on) without a question, and say why in the log.
|
|
13
|
+
*
|
|
14
|
+
* Founders only (Jeetesh's review of 2026-10-25): the question is shown only
|
|
15
|
+
* when Tower answers, live, that this machine's person is a founder. Anybody
|
|
16
|
+
* else never sees it and keeps BLI Memory on; the log names why
|
|
17
|
+
* (`not_founder`, or `founder_check_unavailable` when Tower could not be
|
|
18
|
+
* asked). A cached "not a founder" skips the question without asking Tower.
|
|
19
|
+
*
|
|
20
|
+
* An answer that is neither yes nor no is asked once more; a second unclear
|
|
21
|
+
* answer records NOTHING, so the default holds and the next interactive run
|
|
22
|
+
* asks again. A choice this important is never inferred from a typo.
|
|
23
|
+
*/
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import { readLine, writeLine } from "./cli-io.js";
|
|
26
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
27
|
+
import { readMemoryCaptureChoice, writeMemoryCaptureChoice } from "../memory-capture-choice.js";
|
|
28
|
+
import { mayRecordOff } from "../founder-standing.js";
|
|
29
|
+
import { parseYesNo } from "../yes-no.js";
|
|
30
|
+
export const MEMORY_CHOICE_QUESTION = "BLI Memory: save what you work on so agents remember it across sessions? [Y/n] ";
|
|
31
|
+
export const MEMORY_CHOICE_EXPLAINER = "Answering n keeps BLI Memory's hooks off on this machine, so nothing from your sessions is sent to memory; `cockpit memory on` or `cockpit memory off` changes it later.";
|
|
32
|
+
/** Why this run may not ask, or null when it may. */
|
|
33
|
+
export function memoryChoicePromptBlocker(io, options) {
|
|
34
|
+
if (options.json)
|
|
35
|
+
return "json";
|
|
36
|
+
if (options.automaticTick)
|
|
37
|
+
return "scheduled_tick";
|
|
38
|
+
if (options.dryRun || options.checkOnly)
|
|
39
|
+
return "diagnose_only";
|
|
40
|
+
if (((io.env ?? process.env)["CI"] ?? "") !== "")
|
|
41
|
+
return "ci";
|
|
42
|
+
const stdinTty = Boolean(io.stdin.isTTY);
|
|
43
|
+
const stdoutTty = Boolean(io.stdout.isTTY);
|
|
44
|
+
return stdinTty && stdoutTty ? null : "not_a_tty";
|
|
45
|
+
}
|
|
46
|
+
export async function askMemoryChoiceOnce(io, options) {
|
|
47
|
+
const blocker = memoryChoicePromptBlocker(io, options);
|
|
48
|
+
if (blocker) {
|
|
49
|
+
console.error("[memory-capture] question not asked; the default stands", JSON.stringify({ reason: blocker }));
|
|
50
|
+
return "not_interactive";
|
|
51
|
+
}
|
|
52
|
+
const paths = getCollectorRuntimePaths(options.homeDir ?? os.homedir());
|
|
53
|
+
const current = await readMemoryCaptureChoice(paths);
|
|
54
|
+
if (current.state !== "unchosen") {
|
|
55
|
+
console.error("[memory-capture] question not asked; already answered", JSON.stringify({ reason: "already_chosen", state: current.state }));
|
|
56
|
+
return "already_chosen";
|
|
57
|
+
}
|
|
58
|
+
const founder = await founderMayBeAsked(io, options);
|
|
59
|
+
if (founder !== null)
|
|
60
|
+
return founder;
|
|
61
|
+
writeLine(io.stdout, MEMORY_CHOICE_EXPLAINER);
|
|
62
|
+
let verdict = parseYesNo(await readLine(io, MEMORY_CHOICE_QUESTION), { emptyIsYes: true });
|
|
63
|
+
if (verdict === "unclear") {
|
|
64
|
+
verdict = parseYesNo(await readLine(io, `Please answer y or n. ${MEMORY_CHOICE_QUESTION}`), { emptyIsYes: true });
|
|
65
|
+
}
|
|
66
|
+
if (verdict === "unclear") {
|
|
67
|
+
console.error("[memory-capture] answer unclear twice; nothing recorded", JSON.stringify({ reason: "unclear_answer" }));
|
|
68
|
+
writeLine(io.stdout, "No answer recorded; BLI Memory stays on for now, and the next `cockpit doctor` asks again.");
|
|
69
|
+
return "unclear_answer";
|
|
70
|
+
}
|
|
71
|
+
const on = verdict === "yes";
|
|
72
|
+
try {
|
|
73
|
+
await writeMemoryCaptureChoice(paths, on, "prompt");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error("[memory-capture] answer could not be saved", JSON.stringify({ reason: "choice_write_failed", error_name: error instanceof Error ? error.name : typeof error }));
|
|
77
|
+
writeLine(io.stdout, "Your answer could not be saved; run `cockpit memory " + (on ? "on" : "off") + "` to record it.");
|
|
78
|
+
return "choice_write_failed";
|
|
79
|
+
}
|
|
80
|
+
writeLine(io.stdout, on
|
|
81
|
+
? "BLI Memory is on for this machine. Undo: cockpit memory off"
|
|
82
|
+
: "BLI Memory stays off on this machine; its hooks are removed and not added back. Undo: cockpit memory on");
|
|
83
|
+
return on ? "chose_on" : "chose_off";
|
|
84
|
+
}
|
|
85
|
+
/** Null when the question may be shown; otherwise why not. Logs both branches. */
|
|
86
|
+
async function founderMayBeAsked(io, options) {
|
|
87
|
+
// Only Tower's live "founder" is enough to ask, because the answer can
|
|
88
|
+
// record an off; the saved copy decides nothing (BLI-22640).
|
|
89
|
+
const permission = await mayRecordOff({
|
|
90
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
91
|
+
...(options.dashboardUrl ? { dashboardUrl: options.dashboardUrl } : {}),
|
|
92
|
+
fetch: io.fetch,
|
|
93
|
+
door: "prompt",
|
|
94
|
+
});
|
|
95
|
+
if (permission.allowed)
|
|
96
|
+
return null;
|
|
97
|
+
const reason = permission.reason === "memory_off_founders_only" ? "not_founder" : "founder_check_unavailable";
|
|
98
|
+
console.error("[memory-capture] question not asked; founders only", JSON.stringify({ reason, source: "live", detail: permission.detail }));
|
|
99
|
+
return reason;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What `cockpit doctor` does with the choice before its first row: honour
|
|
103
|
+
* `--no-memory`, otherwise ask once when a person is there. Never throws; a
|
|
104
|
+
* doctor run is never lost to this question.
|
|
105
|
+
*/
|
|
106
|
+
export async function settleDoctorMemoryChoice(command, io) {
|
|
107
|
+
try {
|
|
108
|
+
if (command.noMemory && !command.dryRun && !command.checkOnly) {
|
|
109
|
+
// Founders only, on Tower's live answer; anybody else is told why and doctor carries on.
|
|
110
|
+
const permission = await mayRecordOff({
|
|
111
|
+
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
112
|
+
...(command.dashboardUrl ? { dashboardUrl: command.dashboardUrl } : {}),
|
|
113
|
+
fetch: io.fetch,
|
|
114
|
+
door: "doctor_no_memory",
|
|
115
|
+
});
|
|
116
|
+
if (!permission.allowed) {
|
|
117
|
+
// stdout belongs to `--json`; the refusal is on stderr there, by name.
|
|
118
|
+
writeLine(command.json ? io.stderr : io.stdout, `--no-memory: ${permission.line}`);
|
|
119
|
+
return "flag_refused";
|
|
120
|
+
}
|
|
121
|
+
await writeMemoryCaptureChoice(getCollectorRuntimePaths(command.homeDir ?? os.homedir()), false, "doctor_flag");
|
|
122
|
+
return "flag_off";
|
|
123
|
+
}
|
|
124
|
+
return await askMemoryChoiceOnce(io, command);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error("[memory-capture] choice step failed; doctor carries on with what is recorded", JSON.stringify({ reason: "choice_step_threw", error_name: error instanceof Error ? error.name : typeof error }));
|
|
128
|
+
return "choice_write_failed";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -200,8 +200,10 @@ export function applyHookGroups(root, hookSpecs) {
|
|
|
200
200
|
? hooks[spec.event].slice()
|
|
201
201
|
: [];
|
|
202
202
|
// Somebody else's Stop hook survives untouched; only OUR entries — matched
|
|
203
|
-
// by bin name plus `hook`, wherever the bin ended up — are replaced.
|
|
204
|
-
|
|
203
|
+
// by bin name plus `hook`, wherever the bin ended up — are replaced. A group
|
|
204
|
+
// a person shares with ours keeps their commands (BLI-22640, the Codex
|
|
205
|
+
// review point): only our command comes out of it.
|
|
206
|
+
const survivors = stripOurHooks(groups).groups;
|
|
205
207
|
survivors.push({
|
|
206
208
|
hooks: [
|
|
207
209
|
{
|
|
@@ -268,9 +270,54 @@ export function ourHookCount(root) {
|
|
|
268
270
|
}
|
|
269
271
|
return count;
|
|
270
272
|
}
|
|
271
|
-
|
|
273
|
+
/** Exported for `memory-uninstall.ts` (BLI-22640): the off-switch removes exactly what the installer would replace. */
|
|
274
|
+
export function groupIsOurs(group) {
|
|
272
275
|
return hookCommands(group).some(isMemoryHookCommand);
|
|
273
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* BLI-22640 (the Codex review point on PR 2615). Takes OUR commands out of
|
|
279
|
+
* each hook group and keeps everything else: a group a person shares with
|
|
280
|
+
* ours keeps their own commands (and its matcher and any other key), and only
|
|
281
|
+
* a group left with no hooks at all is dropped. A group that holds none of
|
|
282
|
+
* ours comes back as the same object, byte for byte once serialised.
|
|
283
|
+
*
|
|
284
|
+
* `removed` counts our commands taken out. Used by the installer (before it
|
|
285
|
+
* appends our fresh group) and by `cockpit memory off`.
|
|
286
|
+
*/
|
|
287
|
+
export function stripOurHooks(groups) {
|
|
288
|
+
const kept = [];
|
|
289
|
+
let removed = 0;
|
|
290
|
+
for (const group of groups) {
|
|
291
|
+
const record = asRecord(group);
|
|
292
|
+
const entries = record && Array.isArray(record["hooks"]) ? record["hooks"] : null;
|
|
293
|
+
if (!record || !entries) {
|
|
294
|
+
kept.push(group);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const others = entries.filter((entry) => {
|
|
298
|
+
const command = asRecord(entry)?.["command"];
|
|
299
|
+
return !(typeof command === "string" && isMemoryHookCommand(command));
|
|
300
|
+
});
|
|
301
|
+
removed += entries.length - others.length;
|
|
302
|
+
if (others.length === entries.length)
|
|
303
|
+
kept.push(group);
|
|
304
|
+
else if (others.length > 0)
|
|
305
|
+
kept.push({ ...record, hooks: others });
|
|
306
|
+
}
|
|
307
|
+
return { groups: kept, removed };
|
|
308
|
+
}
|
|
309
|
+
/** How many of OUR hook commands a settings or hooks.json root holds, across every event and group. */
|
|
310
|
+
export function ourHookCommandCount(root) {
|
|
311
|
+
const hooks = asRecord(root["hooks"]);
|
|
312
|
+
if (!hooks)
|
|
313
|
+
return 0;
|
|
314
|
+
let count = 0;
|
|
315
|
+
for (const groups of Object.values(hooks)) {
|
|
316
|
+
if (Array.isArray(groups))
|
|
317
|
+
count += stripOurHooks(groups).removed;
|
|
318
|
+
}
|
|
319
|
+
return count;
|
|
320
|
+
}
|
|
274
321
|
function hookCommands(group) {
|
|
275
322
|
const record = asRecord(group);
|
|
276
323
|
if (!record || !Array.isArray(record["hooks"]))
|
|
@@ -310,7 +357,7 @@ function sameStringArray(value, expected) {
|
|
|
310
357
|
* itself, so this is cosmetic — but a diff that reindents a person's whole
|
|
311
358
|
* configuration reads like damage even when it is not.
|
|
312
359
|
*/
|
|
313
|
-
function serializeJson(root, previous) {
|
|
360
|
+
export function serializeJson(root, previous) {
|
|
314
361
|
const indent = detectIndent(previous);
|
|
315
362
|
return `${JSON.stringify(root, null, indent)}\n`;
|
|
316
363
|
}
|
|
@@ -75,12 +75,17 @@ export function logMemoryOutcome(outcome, platform) {
|
|
|
75
75
|
memory_on: memoryInstallGaps(outcome.receipt).length === 0,
|
|
76
76
|
memory_gaps: memoryInstallGaps(outcome.receipt),
|
|
77
77
|
codex_hooks: outcome.receipt.codex.hooks,
|
|
78
|
+
...(outcome.opted_out ? { opted_out: true, removed_count: outcome.removed_count ?? 0 } : {}),
|
|
78
79
|
};
|
|
79
80
|
// stderr on both branches: launchd captures it to sync.err.log, and stdout is
|
|
80
81
|
// reserved for `--json`.
|
|
81
|
-
console.error(outcome.
|
|
82
|
-
?
|
|
83
|
-
|
|
82
|
+
console.error(outcome.opted_out
|
|
83
|
+
? outcome.status === "failed"
|
|
84
|
+
? "[memory-install] BLI Memory is off on this machine and an entry could not be removed"
|
|
85
|
+
: "[memory-install] BLI Memory is off on this machine; nothing of it was registered"
|
|
86
|
+
: outcome.status === "failed"
|
|
87
|
+
? "[memory-install] BLI Memory is not fully registered on this machine"
|
|
88
|
+
: "[memory-install] BLI Memory registration converged", JSON.stringify(fields));
|
|
84
89
|
}
|
|
85
90
|
export function memoryOutcomeLines(outcome) {
|
|
86
91
|
const headline = memoryOutcomeHeadline(outcome);
|
|
@@ -89,6 +94,11 @@ export function memoryOutcomeLines(outcome) {
|
|
|
89
94
|
// daemon", because nothing looked.
|
|
90
95
|
if (outcome.daemon)
|
|
91
96
|
lines.push(` ${memoryDaemonLine(outcome.daemon)}`);
|
|
97
|
+
// BLI-22640: a non-founder's "off" is ignored, and said so where they read.
|
|
98
|
+
if (outcome.opt_out_ignored) {
|
|
99
|
+
lines.push(" This machine's BLI Memory \"off\" is ignored: only Tower's founders can turn it off, " +
|
|
100
|
+
"because work on BLI machines goes to Tower (opt_out_ignored_not_founder).");
|
|
101
|
+
}
|
|
92
102
|
// The trust step is the one thing a person has to do by hand, so it is said
|
|
93
103
|
// here in words rather than left as a status word they have to look up.
|
|
94
104
|
if (outcome.receipt.codex.hooks === "needs_trust") {
|
|
@@ -108,6 +118,12 @@ export function memoryOutcomeLines(outcome) {
|
|
|
108
118
|
return lines;
|
|
109
119
|
}
|
|
110
120
|
function memoryOutcomeHeadline(outcome) {
|
|
121
|
+
// BLI-22640: the person's choice is the headline, whatever the files say.
|
|
122
|
+
if (outcome.opted_out) {
|
|
123
|
+
return outcome.action === "status"
|
|
124
|
+
? "BLI Memory is off on this machine (your choice). `cockpit memory on` turns it back on."
|
|
125
|
+
: "BLI Memory is off on this machine (your choice); none of it was registered. `cockpit memory on` turns it back on.";
|
|
126
|
+
}
|
|
111
127
|
if (outcome.status === "installed")
|
|
112
128
|
return "BLI Memory registered on this machine.";
|
|
113
129
|
if (outcome.status === "already")
|
|
@@ -148,6 +148,28 @@ export function removeTomlTable(text, tablePath) {
|
|
|
148
148
|
const lines = text.split("\n");
|
|
149
149
|
return [...lines.slice(0, span.start_line), ...lines.slice(span.end_line)].join("\n");
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* BLI-22640. Removes our table AND every sub-table under it (`[a.b.env]`,
|
|
153
|
+
* `[a.b.tools.x]`), each with the lines it owns; every other table survives
|
|
154
|
+
* byte for byte. `cockpit memory off` uses it, so no half of our entry is left
|
|
155
|
+
* for a host to start a server from.
|
|
156
|
+
*/
|
|
157
|
+
export function removeTomlTableTree(text, tablePath) {
|
|
158
|
+
const lines = text.split("\n");
|
|
159
|
+
const kept = [];
|
|
160
|
+
let dropping = false;
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const header = parseTableHeader(line);
|
|
163
|
+
if (header || isArrayTableHeader(line)) {
|
|
164
|
+
dropping = header !== null &&
|
|
165
|
+
header.length >= tablePath.length &&
|
|
166
|
+
samePath(header.slice(0, tablePath.length), tablePath);
|
|
167
|
+
}
|
|
168
|
+
if (!dropping)
|
|
169
|
+
kept.push(line);
|
|
170
|
+
}
|
|
171
|
+
return kept.join("\n");
|
|
172
|
+
}
|
|
151
173
|
export function renderTomlTable(tablePath, entries) {
|
|
152
174
|
const header = `[${tablePath.map(renderKey).join(".")}]`;
|
|
153
175
|
const body = entries.map(([key, value]) => `${renderKey(key)} = ${renderValue(value)}`);
|