@bli-cockpit/cli 0.2.127 → 0.2.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/local-args-tower-memory.js +32 -4
- package/dist/commands/local-args-tower-settings.js +10 -4
- package/dist/commands/local-command-shapes-doctor.js +12 -0
- package/dist/commands/local-command-shapes.js +1 -12
- package/dist/commands/local-help-commands.js +6 -2
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/memory-consolidation.js +83 -0
- package/dist/commands/memory-install-codex-hooks.js +7 -1
- package/dist/commands/memory-install-contract.js +26 -4
- package/dist/commands/memory-install-skills.js +1 -0
- package/dist/commands/memory-store.js +73 -6
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/settings.js +1 -0
- package/package.json +3 -3
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cockpit memory search|save|update|forget` — the argument table
|
|
2
|
+
* `cockpit memory search|save|update|forget|profile` — the argument table
|
|
3
|
+
* (BLI-4047; `profile` added by BLI-4533).
|
|
3
4
|
*
|
|
4
5
|
* BLI Memory was the one Tower surface with no command-line door: the browser
|
|
5
6
|
* had one, the `bli-memory` MCP server had one, and a person or a script at a
|
|
6
|
-
* terminal had nothing.
|
|
7
|
-
* tools, over the SAME `/api/memory/*` doors
|
|
7
|
+
* terminal had nothing. The first four verbs are the twins of that server's
|
|
8
|
+
* four tools, over the SAME `/api/memory/*` doors; `profile` is the twin of
|
|
9
|
+
* `memory_profile` on `bli-tower`, over `GET /api/memory/profile`.
|
|
8
10
|
*
|
|
9
11
|
* Two rules this table exists to enforce, both of them the same discipline
|
|
10
12
|
* `cockpit docs create` and `cockpit notes paste` already follow:
|
|
@@ -34,6 +36,11 @@ export const MEMORY_STORE_ACTIONS = new Set([
|
|
|
34
36
|
"save",
|
|
35
37
|
"update",
|
|
36
38
|
"forget",
|
|
39
|
+
"profile",
|
|
40
|
+
// BLI-4534: a READ of the nightly consolidation receipts, not a fifth way to
|
|
41
|
+
// write a memory. It runs no pass and changes nothing — the pass is a
|
|
42
|
+
// Railway cron; this is how a person or an agent asks what it did.
|
|
43
|
+
"consolidation",
|
|
37
44
|
]);
|
|
38
45
|
const ALLOWED_FLAGS = [
|
|
39
46
|
"--home",
|
|
@@ -44,6 +51,7 @@ const ALLOWED_FLAGS = [
|
|
|
44
51
|
"--limit",
|
|
45
52
|
"--no-recent",
|
|
46
53
|
"--custom-id",
|
|
54
|
+
"--alias",
|
|
47
55
|
"--extract",
|
|
48
56
|
"--file",
|
|
49
57
|
"--content-stdin",
|
|
@@ -58,6 +66,7 @@ const VALUE_FLAGS = [
|
|
|
58
66
|
"--container",
|
|
59
67
|
"--limit",
|
|
60
68
|
"--custom-id",
|
|
69
|
+
"--alias",
|
|
61
70
|
"--file",
|
|
62
71
|
"--reason",
|
|
63
72
|
];
|
|
@@ -65,12 +74,18 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
65
74
|
const values = parseNamedArgs(args, { allowedFlags: ALLOWED_FLAGS, valueFlags: VALUE_FLAGS });
|
|
66
75
|
const rest = values.positionals.slice(1);
|
|
67
76
|
const containers = values.flagValues.get("--container") ?? [];
|
|
68
|
-
if (action !== "search" && containers.length > 1) {
|
|
77
|
+
if (action !== "search" && action !== "consolidation" && containers.length > 1) {
|
|
69
78
|
throw new Error(`memory ${action} takes at most one --container.`);
|
|
70
79
|
}
|
|
71
80
|
if (action !== "search" && (values.flags.has("--limit") || values.booleans.has("--no-recent"))) {
|
|
72
81
|
throw new Error("--limit and --no-recent only apply to memory search.");
|
|
73
82
|
}
|
|
83
|
+
if (action === "consolidation" && rest.length > 0) {
|
|
84
|
+
throw new Error("memory consolidation takes no positionals: `cockpit memory consolidation [--container <tag>] [--json]`.");
|
|
85
|
+
}
|
|
86
|
+
if (action !== "save" && values.flags.has("--alias")) {
|
|
87
|
+
throw new Error("--alias only applies to memory save.");
|
|
88
|
+
}
|
|
74
89
|
if (action !== "save" && (values.flags.has("--custom-id") || values.booleans.has("--extract"))) {
|
|
75
90
|
throw new Error("--custom-id and --extract only apply to memory save.");
|
|
76
91
|
}
|
|
@@ -91,6 +106,15 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
91
106
|
.filter((tag) => tag.length > 0),
|
|
92
107
|
json: values.booleans.has("--json"),
|
|
93
108
|
};
|
|
109
|
+
if (action === "profile") {
|
|
110
|
+
// A pure read of ONE container's compiled page. No positional: there is
|
|
111
|
+
// nothing to ask it, and a stray word would look like a query this verb
|
|
112
|
+
// silently ignored.
|
|
113
|
+
if (rest.length > 0) {
|
|
114
|
+
throw new Error("memory profile takes no words; it reads this repository's compiled page. Name another space with --container.");
|
|
115
|
+
}
|
|
116
|
+
return { ...command, action };
|
|
117
|
+
}
|
|
94
118
|
if (action === "search") {
|
|
95
119
|
// Every remaining positional joined, so `cockpit memory search what did we
|
|
96
120
|
// decide about pagination` needs no quotes — the same shape `cockpit
|
|
@@ -100,6 +124,9 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
100
124
|
throw new Error("memory search needs a question, e.g. `cockpit memory search \"pagination rule\"`.");
|
|
101
125
|
return { ...command, action, query, limit: readLimit(values.flags.get("--limit")), includeRecent: !values.booleans.has("--no-recent") };
|
|
102
126
|
}
|
|
127
|
+
if (action === "consolidation") {
|
|
128
|
+
return { ...command, action };
|
|
129
|
+
}
|
|
103
130
|
if (action === "save") {
|
|
104
131
|
if (rest.length > 0) {
|
|
105
132
|
throw new Error("memory save reads the memory from stdin or --file, never from the command line: `echo \"...\" | cockpit memory save`.");
|
|
@@ -108,6 +135,7 @@ export function parseMemoryStoreArgs(action, args) {
|
|
|
108
135
|
...command,
|
|
109
136
|
action,
|
|
110
137
|
customId: optionalNonEmpty(values.flags.get("--custom-id")),
|
|
138
|
+
...(values.flagValues.has("--alias") ? { aliases: values.flagValues.get("--alias") } : {}),
|
|
111
139
|
extract: values.booleans.has("--extract"),
|
|
112
140
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
113
141
|
};
|
|
@@ -24,6 +24,7 @@ export function parseSettingsArgs(args) {
|
|
|
24
24
|
"--brief-model",
|
|
25
25
|
"--chat",
|
|
26
26
|
"--memory",
|
|
27
|
+
"--note-reader",
|
|
27
28
|
"--project",
|
|
28
29
|
"--file",
|
|
29
30
|
"--id",
|
|
@@ -37,6 +38,7 @@ export function parseSettingsArgs(args) {
|
|
|
37
38
|
"--brief-model",
|
|
38
39
|
"--chat",
|
|
39
40
|
"--memory",
|
|
41
|
+
"--note-reader",
|
|
40
42
|
"--project",
|
|
41
43
|
"--file",
|
|
42
44
|
"--id",
|
|
@@ -124,18 +126,21 @@ function parseCliFloorSection(rest, common) {
|
|
|
124
126
|
}
|
|
125
127
|
return { kind: "settings", section: "cli-floor", action: "set", floorVersion: version, ...common };
|
|
126
128
|
}
|
|
127
|
-
/** `settings models` or `settings models set --chat <key> / --memory <id>`. */
|
|
129
|
+
/** `settings models` or `settings models set --chat <key> / --memory <id> / --note-reader <id>`. */
|
|
128
130
|
function parseModelsSection(rest, flags, common) {
|
|
129
131
|
if (rest.length === 0) {
|
|
130
132
|
return { kind: "settings", section: "models", action: "show", ...common };
|
|
131
133
|
}
|
|
132
134
|
if (rest[0] !== "set" || rest.length > 1) {
|
|
133
|
-
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
135
|
+
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>` / `set --note-reader <id>`.");
|
|
134
136
|
}
|
|
135
137
|
const orgChatModel = optionalNonEmpty(flags.get("--chat"));
|
|
136
138
|
const orgMemoryModel = optionalNonEmpty(flags.get("--memory"));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
// BLI-4484: the note reader is the third org-wide slot; the web page could
|
|
140
|
+
// set it since BLI-4045 and the CLI could not, which is the twin rule broken.
|
|
141
|
+
const orgNoteReaderModel = optionalNonEmpty(flags.get("--note-reader"));
|
|
142
|
+
if (!orgChatModel && !orgMemoryModel && !orgNoteReaderModel) {
|
|
143
|
+
throw new Error("settings models set needs --chat <key>, --memory <id>, --note-reader <id>, or any of them.");
|
|
139
144
|
}
|
|
140
145
|
return {
|
|
141
146
|
kind: "settings",
|
|
@@ -143,6 +148,7 @@ function parseModelsSection(rest, flags, common) {
|
|
|
143
148
|
action: "set",
|
|
144
149
|
orgChatModel,
|
|
145
150
|
orgMemoryModel,
|
|
151
|
+
orgNoteReaderModel,
|
|
146
152
|
...common,
|
|
147
153
|
};
|
|
148
154
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The six human "set my machine up" doors. They are one thing wearing six
|
|
2
|
+
// hats, so they all run the convergence command, but they keep accepting the
|
|
3
|
+
// flags they always accepted, because DMs, runbooks and AGENTS.md rules across
|
|
4
|
+
// the fleet still spell them out.
|
|
5
|
+
export const DOCTOR_SETUP_ALIASES = [
|
|
6
|
+
"install",
|
|
7
|
+
"onboard",
|
|
8
|
+
"login",
|
|
9
|
+
"pair",
|
|
10
|
+
"update",
|
|
11
|
+
"upgrade",
|
|
12
|
+
];
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// hats, so they all run the convergence command — but they keep accepting the
|
|
3
|
-
// flags they always accepted, because DMs, runbooks and AGENTS.md rules across
|
|
4
|
-
// the fleet still spell them out.
|
|
5
|
-
export const DOCTOR_SETUP_ALIASES = [
|
|
6
|
-
"install",
|
|
7
|
-
"onboard",
|
|
8
|
-
"login",
|
|
9
|
-
"pair",
|
|
10
|
-
"update",
|
|
11
|
-
"upgrade",
|
|
12
|
-
];
|
|
1
|
+
export { DOCTOR_SETUP_ALIASES } from "./local-command-shapes-doctor.js";
|
|
@@ -156,7 +156,7 @@ export function localSubcommandHelp(command) {
|
|
|
156
156
|
" Your teams and your two model choices. No flags = show.",
|
|
157
157
|
" cockpit settings switches [set <key> <value>]",
|
|
158
158
|
" Access and feature switches. Everyone may look; only an admin may set.",
|
|
159
|
-
" cockpit settings models [set --chat <key>] [--memory <id>]",
|
|
159
|
+
" cockpit settings models [set --chat <key>] [--memory <id>] [--note-reader <id>]",
|
|
160
160
|
" The ORG-WIDE model map. Admin only, in both directions.",
|
|
161
161
|
" cockpit settings cli-floor [<version>]",
|
|
162
162
|
" The fleet forced-update floor: every collector below it updates on its",
|
|
@@ -580,7 +580,7 @@ export function localSubcommandHelp(command) {
|
|
|
580
580
|
[
|
|
581
581
|
"memory",
|
|
582
582
|
[
|
|
583
|
-
"Usage: cockpit memory [search|save|update|forget|install|status|log] [flags]",
|
|
583
|
+
"Usage: cockpit memory [search|save|update|forget|consolidation|install|status|log] [flags]",
|
|
584
584
|
"",
|
|
585
585
|
"The STORE, over the same /api/memory/* doors the bli-memory MCP server calls:",
|
|
586
586
|
" search \"<question>\" [--container <tag>] [--limit 1..20] [--no-recent]",
|
|
@@ -593,6 +593,10 @@ export function localSubcommandHelp(command) {
|
|
|
593
593
|
" update <id> [--file <path>] replacement text on stdin; the old row is kept as history.",
|
|
594
594
|
" forget <id> [--reason <label>]",
|
|
595
595
|
" forget --content-stdin --container <tag> [--reason <label>] forget by exact text.",
|
|
596
|
+
" consolidation [--container <tag>] what the nightly tidy-up pass did.",
|
|
597
|
+
" Clusters looked at, edits by verb, refusals by reason, model and cost.",
|
|
598
|
+
" Read-only: the pass is a nightly cron, it never deletes a memory, and",
|
|
599
|
+
" report mode changes nothing on the shelf at all.",
|
|
596
600
|
"Refusals keep the door's own label — needs_rls_client, unknown_memory,",
|
|
597
601
|
"already_superseded, no_match — and --json prints the door's answer unchanged.",
|
|
598
602
|
"",
|
|
@@ -94,7 +94,7 @@ export function localCommandHelp(command) {
|
|
|
94
94
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
95
95
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
96
96
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
97
|
-
" cockpit memory [search \"<question>\"|save|update <id>|forget <id>|install|status|log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"] [--container <tag>] [--limit <n>] [--no-recent] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
97
|
+
" 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] [--custom-id <id>] [--extract] [--content-stdin] [--reason <label>] [--file <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
98
98
|
" cockpit clean [--dry-run] [--all-committed] [--reconcile] [--dashboard-url <url>] [--json]",
|
|
99
99
|
" cockpit docs [list|tree|read <id|slug>|create --title <t>|update <id>] [--parent <id>|root|--clear-parent] [--query <text>] [--limit <n>] [--visibility org|private] [--file <path>|--body-stdin] [--allow-empty] [--dashboard-url <url>] [--json]",
|
|
100
100
|
" cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>] [--private] [--members a@x,b@y] [--description <text>] [--thread <id>] [--limit <n>] [--dashboard-url <url>] [--json]",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit memory consolidation` — what last night's tidy-up did (BLI-4534).
|
|
3
|
+
*
|
|
4
|
+
* The nightly pass runs on Railway, not here. This verb is a READ of its
|
|
5
|
+
* receipts over `GET /api/memory/consolidation`, with this machine's collector
|
|
6
|
+
* device token, through `agent-door.ts` — the same door the
|
|
7
|
+
* `memory_consolidation` MCP tool calls and the same credential every other
|
|
8
|
+
* `cockpit memory` verb uses.
|
|
9
|
+
*
|
|
10
|
+
* With `--container` it answers that space's most recent runs; without one it
|
|
11
|
+
* answers the newest run per container, because a list of forty nights of one
|
|
12
|
+
* shelf answers nothing.
|
|
13
|
+
*
|
|
14
|
+
* **It cannot start, stop or change a pass, and it never prints a memory.**
|
|
15
|
+
* The rows carry ids, counts, reason labels, a model id and a dollar figure;
|
|
16
|
+
* the door does not send text and this does not ask for any. `report` in the
|
|
17
|
+
* `mode` column means the pass computed proposals and wrote nothing to the
|
|
18
|
+
* shelf; `apply` means the accepted edits were written as supersessions, which
|
|
19
|
+
* never delete anything.
|
|
20
|
+
*
|
|
21
|
+
* **A refusal keeps the door's own reason label**, printed verbatim by
|
|
22
|
+
* `failAgentDoor`. An EMPTY list is not a refusal and says so in its own
|
|
23
|
+
* words: the run table is readable by a super_admin, so a caller who is not
|
|
24
|
+
* one sees nothing, and that is RLS answering rather than an outage.
|
|
25
|
+
*/
|
|
26
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, } from "./agent-door.js";
|
|
27
|
+
import { writeLine } from "./cli-io.js";
|
|
28
|
+
const TAG = "[memory cli]";
|
|
29
|
+
const READ_DEADLINE_MS = 30_000;
|
|
30
|
+
/** `memory-store.ts` opens the door once for the whole command run and hands it here. */
|
|
31
|
+
export async function runMemoryConsolidation(command, door) {
|
|
32
|
+
const container = command.containerTags[0];
|
|
33
|
+
const query = container ? `?container=${encodeURIComponent(container)}` : "";
|
|
34
|
+
const answer = await askAgentDoor(door, {
|
|
35
|
+
path: `/api/memory/consolidation${query}`,
|
|
36
|
+
method: "GET",
|
|
37
|
+
label: "memory consolidation",
|
|
38
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
39
|
+
});
|
|
40
|
+
if (!answer.ok)
|
|
41
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
42
|
+
const body = answer.body;
|
|
43
|
+
const runs = body.runs ?? [];
|
|
44
|
+
writeLine(door.io.stderr, `${TAG} consolidation ${JSON.stringify({
|
|
45
|
+
reason: body.reason ?? (runs.length > 0 ? "runs_read" : "no_runs_visible"),
|
|
46
|
+
container: container ?? null,
|
|
47
|
+
runs: runs.length,
|
|
48
|
+
})}`);
|
|
49
|
+
if (door.json)
|
|
50
|
+
return emitAgentDoor(door, answer.body);
|
|
51
|
+
if (runs.length === 0) {
|
|
52
|
+
writeLine(door.io.stdout, body.role === "super_admin"
|
|
53
|
+
? "No consolidation pass has been recorded yet. The pass is a nightly Railway cron; nothing has run, or nothing has changed."
|
|
54
|
+
: "No consolidation runs are visible to this account. The receipts are readable by a super_admin; this is the access rule answering, not an outage.");
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
for (const run of runs) {
|
|
58
|
+
writeLine(door.io.stdout, `${run.containerTag ?? "(no tag)"} ${run.mode ?? "?"}`);
|
|
59
|
+
writeLine(door.io.stdout, ` ${run.startedAt ?? "?"} -> ${run.finishedAt ?? "unfinished"} ${run.reason ?? "no reason recorded"}`);
|
|
60
|
+
writeLine(door.io.stdout, ` clusters ${run.clusters ?? 0} proposed ${countsOf(run.proposed)} applied ${countsOf(run.applied)}`);
|
|
61
|
+
const refused = run.refused ?? {};
|
|
62
|
+
const refusedText = Object.entries(refused)
|
|
63
|
+
.map(([label, count]) => `${label} ${count}`)
|
|
64
|
+
.join(", ");
|
|
65
|
+
writeLine(door.io.stdout, ` refused ${refusedText.length > 0 ? refusedText : "none"} ${moneyOf(run.costUsd)} ${run.model ?? "no model"}`);
|
|
66
|
+
}
|
|
67
|
+
writeLine(door.io.stdout, "");
|
|
68
|
+
writeLine(door.io.stdout, "report = proposals only, nothing on the shelf changed. apply = the accepted edits were written as supersessions; nothing is ever deleted.");
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
/** `merge 2, rewrite 1` out of a verb-count object. Never a sentence. */
|
|
72
|
+
function countsOf(value) {
|
|
73
|
+
const verbs = value?.by_verb;
|
|
74
|
+
if (!verbs || typeof verbs !== "object")
|
|
75
|
+
return "none";
|
|
76
|
+
const parts = Object.entries(verbs)
|
|
77
|
+
.filter(([, count]) => Number(count) > 0)
|
|
78
|
+
.map(([verb, count]) => `${verb} ${Number(count)}`);
|
|
79
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
80
|
+
}
|
|
81
|
+
function moneyOf(value) {
|
|
82
|
+
return value === null || value === undefined ? "$0.000000" : `$${Number(value).toFixed(6)}`;
|
|
83
|
+
}
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
* repo forever.
|
|
19
19
|
* 2. **The file shape is Claude Code's, verbatim.** Event → array of matcher
|
|
20
20
|
* groups → `hooks: [{ type: "command", command, timeout }]`, with the same
|
|
21
|
-
* event NAMES: `SessionStart`, `UserPromptSubmit`, `Stop
|
|
21
|
+
* event NAMES: `SessionStart`, `UserPromptSubmit`, `Stop` — and, since
|
|
22
|
+
* BLI-4535, `PreCompact`, which Codex publishes under that exact name with
|
|
23
|
+
* the same `manual`/`auto` trigger Claude Code sends
|
|
24
|
+
* (<https://learn.chatgpt.com/docs/hooks>, read 2026-09-21). That is why the
|
|
22
25
|
* merge here is `applyHookGroups` from `memory-install-claude.ts` and not
|
|
23
26
|
* a second copy: same contract, one owner.
|
|
24
27
|
* 3. **Hooks are on by default now.** `[features] hooks = false` turns them
|
|
@@ -196,5 +199,8 @@ function trustTableNamesOurFile(tableHeader, wantedSource) {
|
|
|
196
199
|
* which is what `applyHookGroups` writes.
|
|
197
200
|
*/
|
|
198
201
|
export function expectedCodexTrustRows(config) {
|
|
202
|
+
// Read off the config rather than written as a number here, so adding a
|
|
203
|
+
// fourth event (BLI-4535) moved this expectation with it instead of leaving
|
|
204
|
+
// every trusted machine reading `needs_trust` for a row nobody counted.
|
|
199
205
|
return config.hooks.length;
|
|
200
206
|
}
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* "hooks": [
|
|
28
28
|
* { "event": "SessionStart", "command": "bli-memory-mcp hook session-start", "timeout_seconds": 30 },
|
|
29
29
|
* { "event": "UserPromptSubmit", "command": "bli-memory-mcp hook prompt", "timeout_seconds": 5 },
|
|
30
|
-
* { "event": "Stop", "command": "bli-memory-mcp hook stop", "timeout_seconds": 30 }
|
|
30
|
+
* { "event": "Stop", "command": "bli-memory-mcp hook stop", "timeout_seconds": 30 },
|
|
31
|
+
* { "event": "PreCompact", "command": "bli-memory-mcp hook pre-compact", "timeout_seconds": 30 }
|
|
31
32
|
* ],
|
|
32
33
|
* "permissions_allow": ["mcp__bli-memory__search_memory"]
|
|
33
34
|
* }
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
* `@bli-cockpit/cli`'s `node_modules` rather than on PATH — so
|
|
48
49
|
* `withResolvedBinPath` re-points every command at the absolute path this
|
|
49
50
|
* machine resolved before anything is written. Skipping that step registered
|
|
50
|
-
*
|
|
51
|
+
* four hooks that answered `command not found` on every turn.
|
|
51
52
|
*
|
|
52
53
|
* **And the path it is re-pointed at is written for a SHELL, not for the
|
|
53
54
|
* filesystem** — `shellSafeBinPath`. A hook command is a shell string, Windows
|
|
@@ -64,22 +65,39 @@ export const MEMORY_MCP_BIN = "bli-memory-mcp";
|
|
|
64
65
|
export const MEMORY_MCP_SERVER_ID = "bli-memory";
|
|
65
66
|
/** The one env var the server is handed. A URL, never a token. */
|
|
66
67
|
export const MEMORY_DASHBOARD_URL_ENV = "COCKPIT_DASHBOARD_URL";
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Hook events this installer registers, in the order it writes them.
|
|
70
|
+
*
|
|
71
|
+
* **All four go to BOTH hosts (BLI-4535).** `PreCompact` was checked against
|
|
72
|
+
* each vendor's own reference before it was added here rather than assumed
|
|
73
|
+
* from Claude Code's list: Claude Code fires it "before Claude Code is about
|
|
74
|
+
* to run a compact operation" with `trigger` of `manual` or `auto`
|
|
75
|
+
* (<https://code.claude.com/docs/en/hooks>, read 2026-09-21), and Codex fires
|
|
76
|
+
* `PreCompact` "before Codex compacts the chat" with the same two trigger
|
|
77
|
+
* values and the same common input fields
|
|
78
|
+
* (<https://learn.chatgpt.com/docs/hooks>, read 2026-09-21). Same event name,
|
|
79
|
+
* same file shape, same bin — so `memory-install-codex-hooks.ts` needs no
|
|
80
|
+
* per-host filter and Codex is NOT left at three events.
|
|
81
|
+
*/
|
|
68
82
|
export const MEMORY_HOOK_EVENTS = [
|
|
69
83
|
"SessionStart",
|
|
70
84
|
"UserPromptSubmit",
|
|
71
85
|
"Stop",
|
|
86
|
+
"PreCompact",
|
|
72
87
|
];
|
|
73
88
|
/**
|
|
74
89
|
* The bin's own hook subcommands. `session-start` recalls at session open,
|
|
75
90
|
* `prompt` recalls per turn, `stop` saves the turn — the same three jobs the
|
|
76
91
|
* Supermemory plugin's `session-start.js` / `recall-directive.js` / `capture.js`
|
|
77
|
-
* did, collapsed onto one bin
|
|
92
|
+
* did, collapsed onto one bin — and `pre-compact` writes the session's recap
|
|
93
|
+
* before the host throws the conversation away (BLI-4535), which the vendor
|
|
94
|
+
* plugin never did at all.
|
|
78
95
|
*/
|
|
79
96
|
export const MEMORY_HOOK_SUBCOMMAND = {
|
|
80
97
|
SessionStart: "hook session-start",
|
|
81
98
|
UserPromptSubmit: "hook prompt",
|
|
82
99
|
Stop: "hook stop",
|
|
100
|
+
PreCompact: "hook pre-compact",
|
|
83
101
|
};
|
|
84
102
|
/**
|
|
85
103
|
* Injection hooks must stay fast — Claude Code discards a hook that outlives
|
|
@@ -90,6 +108,10 @@ export const MEMORY_HOOK_TIMEOUT_SECONDS = {
|
|
|
90
108
|
SessionStart: 30,
|
|
91
109
|
UserPromptSubmit: 5,
|
|
92
110
|
Stop: 30,
|
|
111
|
+
// BLI-4535. Stop's number for Stop's reason: this hook WRITES, its door call
|
|
112
|
+
// is one queued INSERT, and the recap is the last thing that can be saved
|
|
113
|
+
// before the context is gone.
|
|
114
|
+
PreCompact: 30,
|
|
93
115
|
};
|
|
94
116
|
/**
|
|
95
117
|
* Auto-approved tools. READ ONLY, deliberately: the vendor's
|
|
@@ -67,6 +67,7 @@ const SAVE_MD = `# save_memory
|
|
|
67
67
|
|
|
68
68
|
\`\`\`
|
|
69
69
|
in : { content: string (1-200000, required),
|
|
70
|
+
aliases?: string[] (up to 16 alternate names of 64 characters),
|
|
70
71
|
containerTag?: string,
|
|
71
72
|
customId?: string, // e.g. the session id — the upsert key
|
|
72
73
|
metadata?: object }
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cockpit memory search|save|update|forget` — BLI Memory at a terminal
|
|
3
|
-
* (BLI-4047).
|
|
2
|
+
* `cockpit memory search|save|update|forget|profile` — BLI Memory at a terminal
|
|
3
|
+
* (BLI-4047; `profile` added by BLI-4533).
|
|
4
4
|
*
|
|
5
5
|
* Memory was the one Tower surface with no command-line door. The browser had
|
|
6
6
|
* one, the `bli-memory` MCP server had four tools, and a person or a script
|
|
7
7
|
* had nothing — so a machine without an agent host could not read or write the
|
|
8
8
|
* store at all, and `cockpit memory` typed on its own registered an MCP server
|
|
9
|
-
* instead of saying what the noun does.
|
|
10
|
-
* server's four tools
|
|
9
|
+
* instead of saying what the noun does. The first four verbs are the twins of
|
|
10
|
+
* that server's four tools; `profile` (BLI-4533) is the twin of the
|
|
11
|
+
* `memory_profile` tool on `bli-tower`, and reads the compiled one-page
|
|
12
|
+
* briefing a session start injects.
|
|
11
13
|
*
|
|
12
14
|
* **The same doors, never a second store.** `POST /api/memory/{search,save,
|
|
13
|
-
* update,forget}`, with this machine's collector
|
|
15
|
+
* update,forget}` and `GET /api/memory/profile`, with this machine's collector
|
|
16
|
+
* device token, through
|
|
14
17
|
* `agent-door.ts` — the identical route the MCP server calls in
|
|
15
18
|
* `packages/cockpit-memory-mcp/src/server.ts` and the browser calls from
|
|
16
19
|
* `lib/memory/agent-memories/`. Nothing here talks to Postgres, embeds
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
import { resolveContainerTag } from "@bli-cockpit/memory-mcp/dist/container-tag.js";
|
|
41
44
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
42
45
|
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
46
|
+
import { runMemoryConsolidation } from "./memory-consolidation.js";
|
|
43
47
|
import { readNoteFile } from "./notes-file.js";
|
|
44
48
|
const TAG = "[memory cli]";
|
|
45
49
|
const SEARCH_DEADLINE_MS = 60_000;
|
|
@@ -60,8 +64,65 @@ export async function runMemoryStore(command, io) {
|
|
|
60
64
|
return updateMemory(command, door);
|
|
61
65
|
case "forget":
|
|
62
66
|
return forgetMemory(command, door);
|
|
67
|
+
case "profile":
|
|
68
|
+
return profileMemory(command, door);
|
|
69
|
+
case "consolidation":
|
|
70
|
+
// BLI-4534. Its own module: it reads a different door, prints a
|
|
71
|
+
// different shape, and is the only memory verb that is about the JOB
|
|
72
|
+
// rather than about a memory.
|
|
73
|
+
return runMemoryConsolidation(command, door);
|
|
63
74
|
}
|
|
64
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `cockpit memory profile` — the compiled one-page briefing (BLI-4533).
|
|
78
|
+
*
|
|
79
|
+
* The same page `POST /api/memory/v4/profile` injects at session start, read on
|
|
80
|
+
* its own over `GET /api/memory/profile?container=<tag>`. It is a READ: no
|
|
81
|
+
* compile happens here and none can be asked for. The daily `memory-profile`
|
|
82
|
+
* service writes the row; this verb answers "what does this project's memory
|
|
83
|
+
* say about itself right now?", which is the question a person asks when a
|
|
84
|
+
* session opened with something wrong in it.
|
|
85
|
+
*
|
|
86
|
+
* A container nobody has compiled yet answers 200 with `profile: null` and the
|
|
87
|
+
* reason `not_compiled_yet`. That is not an error and is not printed as one —
|
|
88
|
+
* "nothing is compiled" and "the store is down" are different facts, and the
|
|
89
|
+
* second one arrives as a 502 through `failAgentDoor` with the door's own
|
|
90
|
+
* label.
|
|
91
|
+
*/
|
|
92
|
+
async function profileMemory(command, door) {
|
|
93
|
+
const containerTag = containerTagsFor(command)[0];
|
|
94
|
+
const answer = await askAgentDoor(door, {
|
|
95
|
+
path: `/api/memory/profile?container=${encodeURIComponent(containerTag)}`,
|
|
96
|
+
method: "GET",
|
|
97
|
+
label: "memory profile",
|
|
98
|
+
timeoutMs: SEARCH_DEADLINE_MS,
|
|
99
|
+
});
|
|
100
|
+
if (!answer.ok)
|
|
101
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
102
|
+
const body = answer.body;
|
|
103
|
+
const profile = body.profile ?? null;
|
|
104
|
+
const lines = profile?.lines ?? [];
|
|
105
|
+
// Counts and labels only: a profile line never enters a log line.
|
|
106
|
+
writeLine(door.io.stderr, `${TAG} profile ${JSON.stringify({
|
|
107
|
+
container_tag: body.containerTag ?? containerTag,
|
|
108
|
+
reason: body.reason ?? "unknown",
|
|
109
|
+
lines: lines.length,
|
|
110
|
+
source_rows: profile?.sourceRows ?? 0,
|
|
111
|
+
compiled_at: profile?.compiledAt ?? null,
|
|
112
|
+
model: profile?.model ?? null,
|
|
113
|
+
})}`);
|
|
114
|
+
if (door.json)
|
|
115
|
+
return emitAgentDoor(door, answer.body);
|
|
116
|
+
if (!profile || lines.length === 0) {
|
|
117
|
+
writeLine(door.io.stdout, `No page is compiled for ${body.containerTag ?? containerTag} yet. The daily compile writes one once this space has memories; the store is fine.`);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
for (const line of lines)
|
|
121
|
+
writeLine(door.io.stdout, line);
|
|
122
|
+
writeLine(door.io.stdout, "");
|
|
123
|
+
writeLine(door.io.stdout, `Compiled ${String(profile.compiledAt ?? "—")} from ${String(profile.sourceRows ?? 0)} memories by ${String(profile.model ?? "—")}.`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
65
126
|
/**
|
|
66
127
|
* The verb list a bare `cockpit memory` prints (BLI-4047).
|
|
67
128
|
*
|
|
@@ -77,7 +138,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
77
138
|
ok: true,
|
|
78
139
|
noun: "memory",
|
|
79
140
|
machine_verbs: ["install", "status"],
|
|
80
|
-
store_verbs: ["search", "save", "update", "forget"],
|
|
141
|
+
store_verbs: ["search", "save", "update", "forget", "profile", "consolidation"],
|
|
81
142
|
log_verbs: ["log"],
|
|
82
143
|
}));
|
|
83
144
|
return 0;
|
|
@@ -92,6 +153,10 @@ export function runMemoryVerbList(io, json) {
|
|
|
92
153
|
" cockpit memory update <id> [--file <path>] [--json] replacement text on stdin",
|
|
93
154
|
" cockpit memory forget <id> [--reason <label>] [--json]",
|
|
94
155
|
" cockpit memory forget --content-stdin --container <tag> [--reason <label>] [--json]",
|
|
156
|
+
" cockpit memory profile [--container <tag>] [--json]",
|
|
157
|
+
" the compiled one-page briefing every session opens with; a read, never a compile",
|
|
158
|
+
" cockpit memory consolidation [--container <tag>] [--json]",
|
|
159
|
+
" what the nightly tidy-up pass did: clusters, edits by verb, refusals, cost",
|
|
95
160
|
"",
|
|
96
161
|
"This machine:",
|
|
97
162
|
" cockpit memory install [--dry-run] [--json] register the MCP server and the three hooks",
|
|
@@ -100,6 +165,7 @@ export function runMemoryVerbList(io, json) {
|
|
|
100
165
|
"The experience log:",
|
|
101
166
|
" cockpit memory log <win|loss|noise> --store <bli|supermemory|both> \"<reason>\"",
|
|
102
167
|
"",
|
|
168
|
+
"Save accepts repeated --alias names (up to 16 names of 64 characters).",
|
|
103
169
|
"Omit --container and this repository's own memory space is used — the same",
|
|
104
170
|
"space the bli-memory MCP server and its hooks read and write.",
|
|
105
171
|
]) {
|
|
@@ -184,6 +250,7 @@ async function saveMemory(command, door) {
|
|
|
184
250
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
185
251
|
body: {
|
|
186
252
|
content: body.text,
|
|
253
|
+
...(command.aliases === undefined ? {} : { aliases: command.aliases }),
|
|
187
254
|
containerTag,
|
|
188
255
|
...(command.customId ? { customId: command.customId } : {}),
|
|
189
256
|
mode: command.extract ? "extract" : "verbatim",
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.131");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -187,6 +187,7 @@ async function setModels(command, tower, io) {
|
|
|
187
187
|
body: {
|
|
188
188
|
...(command.orgChatModel ? { chatModel: parseModelKey(command.orgChatModel) } : {}),
|
|
189
189
|
...(command.orgMemoryModel ? { memoryModel: command.orgMemoryModel } : {}),
|
|
190
|
+
...(command.orgNoteReaderModel ? { noteReaderModel: command.orgNoteReaderModel } : {}),
|
|
190
191
|
},
|
|
191
192
|
});
|
|
192
193
|
if (!result.ok)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.131",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.35",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.57",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.48"
|
|
33
33
|
}
|
|
34
34
|
}
|