@bli-cockpit/cli 0.2.49 → 0.2.51
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/adapters/raw-evidence-claude-reader.js +108 -0
- package/dist/adapters/raw-evidence-codex-reader.js +147 -0
- package/dist/adapters/raw-evidence-collection-state.js +199 -0
- package/dist/adapters/raw-evidence-facts.js +338 -0
- package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
- package/dist/adapters/raw-evidence-image-reader.js +107 -0
- package/dist/adapters/raw-evidence-sanitize.js +56 -0
- package/dist/adapters/raw-evidence-transcript-file.js +182 -0
- package/dist/adapters/raw-evidence.js +63 -1183
- package/dist/commands/backfill-batches.js +34 -0
- package/dist/commands/backfill-candidates.js +54 -0
- package/dist/commands/backfill-checkpoint.js +101 -0
- package/dist/commands/backfill-command-line.js +70 -0
- package/dist/commands/backfill-evidence-outcomes.js +104 -0
- package/dist/commands/backfill-issues.js +265 -0
- package/dist/commands/backfill-output.js +75 -0
- package/dist/commands/backfill-plan.js +71 -0
- package/dist/commands/backfill-reasons.js +107 -0
- package/dist/commands/backfill-report.js +298 -0
- package/dist/commands/backfill-result.js +150 -0
- package/dist/commands/backfill-scan.js +274 -0
- package/dist/commands/backfill-scope.js +114 -0
- package/dist/commands/backfill-session-report.js +145 -0
- package/dist/commands/backfill-types.js +1 -0
- package/dist/commands/backfill-upload.js +212 -0
- package/dist/commands/backfill.js +41 -1961
- package/dist/commands/doctor.js +57 -0
- package/dist/commands/jarvis-trace.js +184 -0
- package/dist/commands/jarvis.js +144 -4
- package/dist/commands/local-args-collector.js +26 -0
- package/dist/commands/local-args-tower.js +21 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help.js +19 -2
- package/dist/commands/local.js +3 -0
- package/dist/commands/memory-install-claude.js +294 -0
- package/dist/commands/memory-install-codex.js +205 -0
- package/dist/commands/memory-install-contract.js +286 -0
- package/dist/commands/memory-install-files.js +63 -0
- package/dist/commands/memory-install-skills.js +121 -0
- package/dist/commands/memory-install-toml.js +265 -0
- package/dist/commands/memory-install.js +465 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sync-followups.js +105 -0
- package/dist/commands/sync.js +7 -1
- package/dist/local-state-attributed-target.js +75 -0
- package/dist/local-state-config.js +147 -0
- package/dist/local-state-files.js +59 -0
- package/dist/local-state-identity.js +73 -0
- package/dist/local-state-pairing.js +263 -0
- package/dist/local-state-paths.js +61 -0
- package/dist/local-state-session.js +68 -0
- package/dist/local-state-status.js +163 -0
- package/dist/local-state-work-context.js +190 -0
- package/dist/local-state.js +34 -848
- package/dist/tower-client.js +3 -2
- package/dist/tower-stream.js +57 -3
- package/package.json +2 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interface this installer consumes from `@bli-cockpit/memory-mcp`
|
|
3
|
+
* (BLI-3580, core slice on branch `edward/bli-3580-agent-memory-core`).
|
|
4
|
+
*
|
|
5
|
+
* That package does not exist on `main` yet, and the two pull requests must be
|
|
6
|
+
* able to land in either order — so the contract lives HERE, in one small
|
|
7
|
+
* module, and is stated twice on purpose:
|
|
8
|
+
*
|
|
9
|
+
* 1. `bli-memory-mcp --print-config --claude` prints the JSON below, and
|
|
10
|
+
* `--print-config --codex` prints the TOML table below. When the bin
|
|
11
|
+
* resolves, what it prints WINS: the server knows its own shape.
|
|
12
|
+
* 2. `builtinMemoryInstallConfig()` is the same shape, built locally, for a
|
|
13
|
+
* server that resolved but does not answer `--print-config` yet.
|
|
14
|
+
*
|
|
15
|
+
* Both require a RESOLVED bin. A machine that does not have the server gets
|
|
16
|
+
* nothing written at all and a `skipped bin_missing` receipt — the template is
|
|
17
|
+
* a fallback for an older server, never a substitute for an absent one
|
|
18
|
+
* (`no_bin_no_write`, memory-install.ts).
|
|
19
|
+
*
|
|
20
|
+
* The printed JSON contract (`--print-config --claude`):
|
|
21
|
+
*
|
|
22
|
+
* ```json
|
|
23
|
+
* {
|
|
24
|
+
* "schema": "bli-memory-install-config.v1",
|
|
25
|
+
* "server_id": "bli-memory",
|
|
26
|
+
* "mcp_server": { "command": "bli-memory-mcp", "args": [], "env": {} },
|
|
27
|
+
* "hooks": [
|
|
28
|
+
* { "event": "SessionStart", "command": "bli-memory-mcp hook session-start", "timeout_seconds": 30 },
|
|
29
|
+
* { "event": "UserPromptSubmit", "command": "bli-memory-mcp hook prompt", "timeout_seconds": 5 },
|
|
30
|
+
* { "event": "Stop", "command": "bli-memory-mcp hook stop", "timeout_seconds": 30 }
|
|
31
|
+
* ],
|
|
32
|
+
* "permissions_allow": ["mcp__bli-memory__search_memory"]
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The printed TOML contract (`--print-config --codex`) is exactly one table:
|
|
37
|
+
*
|
|
38
|
+
* ```toml
|
|
39
|
+
* [mcp_servers.bli-memory]
|
|
40
|
+
* command = "bli-memory-mcp"
|
|
41
|
+
* args = []
|
|
42
|
+
* env = { COCKPIT_DASHBOARD_URL = "https://…" }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* **The printed `command` is a bare name and is never written as-is.** The bin
|
|
46
|
+
* cannot know where it was installed, and it lives nested inside
|
|
47
|
+
* `@bli-cockpit/cli`'s `node_modules` rather than on PATH — so
|
|
48
|
+
* `withResolvedBinPath` re-points every command at the absolute path this
|
|
49
|
+
* machine resolved before anything is written. Skipping that step registered
|
|
50
|
+
* three hooks that answered `command not found` on every turn.
|
|
51
|
+
*
|
|
52
|
+
* Nothing here touches the filesystem. The halves that do are
|
|
53
|
+
* `memory-install-claude.ts` and `memory-install-codex.ts`.
|
|
54
|
+
*/
|
|
55
|
+
/** The published bin name. Both hosts spawn this. */
|
|
56
|
+
export const MEMORY_MCP_BIN = "bli-memory-mcp";
|
|
57
|
+
/** The MCP server id, as it appears in `mcp__<server>__<tool>`. */
|
|
58
|
+
export const MEMORY_MCP_SERVER_ID = "bli-memory";
|
|
59
|
+
/** The one env var the server is handed. A URL, never a token. */
|
|
60
|
+
export const MEMORY_DASHBOARD_URL_ENV = "COCKPIT_DASHBOARD_URL";
|
|
61
|
+
/** Claude Code hook events this installer registers, in the order it writes them. */
|
|
62
|
+
export const MEMORY_HOOK_EVENTS = [
|
|
63
|
+
"SessionStart",
|
|
64
|
+
"UserPromptSubmit",
|
|
65
|
+
"Stop",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* The bin's own hook subcommands. `session-start` recalls at session open,
|
|
69
|
+
* `prompt` recalls per turn, `stop` saves the turn — the same three jobs the
|
|
70
|
+
* Supermemory plugin's `session-start.js` / `recall-directive.js` / `capture.js`
|
|
71
|
+
* did, collapsed onto one bin.
|
|
72
|
+
*/
|
|
73
|
+
export const MEMORY_HOOK_SUBCOMMAND = {
|
|
74
|
+
SessionStart: "hook session-start",
|
|
75
|
+
UserPromptSubmit: "hook prompt",
|
|
76
|
+
Stop: "hook stop",
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Injection hooks must stay fast — Claude Code discards a hook that outlives
|
|
80
|
+
* its timeout, and a recall that arrives late is a recall that did not happen.
|
|
81
|
+
* These mirror the vendor plugin's own budgets (30 s / 5 s / 30 s).
|
|
82
|
+
*/
|
|
83
|
+
export const MEMORY_HOOK_TIMEOUT_SECONDS = {
|
|
84
|
+
SessionStart: 30,
|
|
85
|
+
UserPromptSubmit: 5,
|
|
86
|
+
Stop: 30,
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Auto-approved tools. READ ONLY, deliberately: the vendor's
|
|
90
|
+
* `recall-approve.js` allowed its read set and left every write prompting, and
|
|
91
|
+
* a memory the agent can silently overwrite is worse than one it has to ask
|
|
92
|
+
* about. `save_memory` / `update_memory` / `forget_memory` are absent on purpose.
|
|
93
|
+
*/
|
|
94
|
+
export const MEMORY_AUTO_APPROVE_TOOLS = [
|
|
95
|
+
`mcp__${MEMORY_MCP_SERVER_ID}__search_memory`,
|
|
96
|
+
];
|
|
97
|
+
/** Characters that would let a resolved path change the meaning of a hook command string. */
|
|
98
|
+
const UNSAFE_PATH_CHARACTERS = /["'`$;&|<>\r\n]/u;
|
|
99
|
+
export function isUnsafeBinPath(binPath) {
|
|
100
|
+
return UNSAFE_PATH_CHARACTERS.test(binPath);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A Claude Code hook `command` is a shell string by the platform's design, so
|
|
104
|
+
* the only defence is quoting — and a path that could not be quoted safely is
|
|
105
|
+
* refused upstream (see `resolveMemoryMcpBin`) rather than escaped cleverly.
|
|
106
|
+
*/
|
|
107
|
+
export function shellQuoteBinPath(binPath) {
|
|
108
|
+
return /\s/u.test(binPath) ? `"${binPath}"` : binPath;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Windows cannot spawn an npm `.cmd` shim directly — Node refuses it without a
|
|
112
|
+
* shell, which is why `commands/editor.ts` REFUSES a `.cmd` editor rather than
|
|
113
|
+
* running it through cmd.exe with a person's path interpolated in. Here the
|
|
114
|
+
* path is ours, not a person's, and both hosts spawn the MCP server themselves
|
|
115
|
+
* with an argument array, so `cmd /c <shim>` is the correct and safe shape: the
|
|
116
|
+
* arguments never become one string we built.
|
|
117
|
+
*/
|
|
118
|
+
export function memoryMcpServerEntry(options) {
|
|
119
|
+
const env = options.dashboardUrl
|
|
120
|
+
? { [MEMORY_DASHBOARD_URL_ENV]: options.dashboardUrl }
|
|
121
|
+
: {};
|
|
122
|
+
if (options.platform === "win32") {
|
|
123
|
+
return { command: "cmd", args: ["/c", options.binPath], env };
|
|
124
|
+
}
|
|
125
|
+
return { command: options.binPath, args: [], env };
|
|
126
|
+
}
|
|
127
|
+
export function builtinMemoryInstallConfig(options) {
|
|
128
|
+
const quoted = shellQuoteBinPath(options.binPath);
|
|
129
|
+
return {
|
|
130
|
+
server_id: MEMORY_MCP_SERVER_ID,
|
|
131
|
+
mcp_server: memoryMcpServerEntry(options),
|
|
132
|
+
hooks: MEMORY_HOOK_EVENTS.map((event) => ({
|
|
133
|
+
event,
|
|
134
|
+
command: `${quoted} ${MEMORY_HOOK_SUBCOMMAND[event]}`,
|
|
135
|
+
timeout_seconds: MEMORY_HOOK_TIMEOUT_SECONDS[event],
|
|
136
|
+
})),
|
|
137
|
+
permissions_allow: [...MEMORY_AUTO_APPROVE_TOOLS],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Recognises a hook entry as OURS, wherever the bin ended up and however it was
|
|
142
|
+
* quoted. Identity is the bin name followed by `hook`, so an absolute path, a
|
|
143
|
+
* `.cmd` shim and a bare name all match — and somebody else's hook never does.
|
|
144
|
+
* This is what makes a re-run replace our three entries instead of appending a
|
|
145
|
+
* fourth copy.
|
|
146
|
+
*/
|
|
147
|
+
export function isMemoryHookCommand(command) {
|
|
148
|
+
return new RegExp(`${escapeRegExp(MEMORY_MCP_BIN)}(?:\\.cmd|\\.exe|\\.bat)?["']?\\s+hook\\b`, "u").test(command);
|
|
149
|
+
}
|
|
150
|
+
function escapeRegExp(value) {
|
|
151
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parses what `bli-memory-mcp --print-config --claude` printed. Anything that
|
|
155
|
+
* is not the documented shape returns null and the caller falls back to the
|
|
156
|
+
* built-in template with a named reason — a half-understood config is worse
|
|
157
|
+
* than the one we can write ourselves.
|
|
158
|
+
*/
|
|
159
|
+
export function parsePrintedMemoryInstallConfig(stdout) {
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(stdout.trim());
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (!parsed || typeof parsed !== "object")
|
|
168
|
+
return null;
|
|
169
|
+
const record = parsed;
|
|
170
|
+
const server = record["mcp_server"];
|
|
171
|
+
if (!server || typeof server !== "object")
|
|
172
|
+
return null;
|
|
173
|
+
const serverRecord = server;
|
|
174
|
+
const command = serverRecord["command"];
|
|
175
|
+
if (typeof command !== "string" || !command.trim())
|
|
176
|
+
return null;
|
|
177
|
+
const args = normalizeStringArray(serverRecord["args"]);
|
|
178
|
+
const env = normalizeStringRecord(serverRecord["env"]);
|
|
179
|
+
if (args === null || env === null)
|
|
180
|
+
return null;
|
|
181
|
+
const rawHooks = record["hooks"];
|
|
182
|
+
if (!Array.isArray(rawHooks))
|
|
183
|
+
return null;
|
|
184
|
+
const hooks = [];
|
|
185
|
+
for (const rawHook of rawHooks) {
|
|
186
|
+
if (!rawHook || typeof rawHook !== "object")
|
|
187
|
+
return null;
|
|
188
|
+
const hookRecord = rawHook;
|
|
189
|
+
const event = hookRecord["event"];
|
|
190
|
+
const hookCommand = hookRecord["command"];
|
|
191
|
+
if (typeof event !== "string" || typeof hookCommand !== "string")
|
|
192
|
+
return null;
|
|
193
|
+
if (!MEMORY_HOOK_EVENTS.includes(event))
|
|
194
|
+
return null;
|
|
195
|
+
const timeout = hookRecord["timeout_seconds"];
|
|
196
|
+
hooks.push({
|
|
197
|
+
event: event,
|
|
198
|
+
command: hookCommand,
|
|
199
|
+
timeout_seconds: typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
200
|
+
? timeout
|
|
201
|
+
: MEMORY_HOOK_TIMEOUT_SECONDS[event],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (hooks.length === 0)
|
|
205
|
+
return null;
|
|
206
|
+
const allow = normalizeStringArray(record["permissions_allow"]);
|
|
207
|
+
const serverId = record["server_id"];
|
|
208
|
+
return {
|
|
209
|
+
server_id: typeof serverId === "string" && serverId.trim()
|
|
210
|
+
? serverId.trim()
|
|
211
|
+
: MEMORY_MCP_SERVER_ID,
|
|
212
|
+
mcp_server: { command, args, env },
|
|
213
|
+
hooks,
|
|
214
|
+
permissions_allow: allow ?? [...MEMORY_AUTO_APPROVE_TOOLS],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Put THIS machine's resolved bin path into a config the bin printed.
|
|
219
|
+
*
|
|
220
|
+
* The printed contract says `command: "bli-memory-mcp"` — a bare name, because
|
|
221
|
+
* the bin has no idea where it was installed. `bli-memory-mcp` is a DEPENDENCY
|
|
222
|
+
* of `@bli-cockpit/cli`, nested inside its `node_modules`, and therefore **not
|
|
223
|
+
* on anybody's PATH**: a host handed the bare name answers `command not found`
|
|
224
|
+
* on every SessionStart, every prompt and every Stop. The installer is the only
|
|
225
|
+
* thing that knows the absolute path, so path qualification happens here and
|
|
226
|
+
* the printed strings are never written verbatim (BLI-3580; found by running
|
|
227
|
+
* the registered hook command in a simulated global install, not by reading
|
|
228
|
+
* the code).
|
|
229
|
+
*
|
|
230
|
+
* What the printed config still owns: which hook EVENTS exist, their
|
|
231
|
+
* subcommands, their timeouts, the server id and the allow-list. What this
|
|
232
|
+
* function owns: where the program is, and the per-platform launch shape.
|
|
233
|
+
*
|
|
234
|
+
* Returns null when a printed hook command cannot be understood well enough to
|
|
235
|
+
* re-point — the caller then falls back to the built-in template, which is
|
|
236
|
+
* always path-qualified. Guessing at a command we cannot parse would write a
|
|
237
|
+
* hook that runs something else.
|
|
238
|
+
*/
|
|
239
|
+
export function withResolvedBinPath(config, options) {
|
|
240
|
+
const quoted = shellQuoteBinPath(options.binPath);
|
|
241
|
+
const hooks = [];
|
|
242
|
+
for (const hook of config.hooks) {
|
|
243
|
+
const tail = hookSubcommandTail(hook.command);
|
|
244
|
+
if (!tail)
|
|
245
|
+
return null;
|
|
246
|
+
hooks.push({ ...hook, command: `${quoted} ${tail}` });
|
|
247
|
+
}
|
|
248
|
+
const entry = memoryMcpServerEntry(options);
|
|
249
|
+
return {
|
|
250
|
+
...config,
|
|
251
|
+
// The bin's own env wins nothing and loses nothing: it prints `{}`, and the
|
|
252
|
+
// dashboard URL is a machine fact the installer holds.
|
|
253
|
+
mcp_server: { ...entry, env: { ...config.mcp_server.env, ...entry.env } },
|
|
254
|
+
hooks,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `…/bli-memory-mcp hook prompt` → `hook prompt`. The subcommand is the part we
|
|
259
|
+
* keep; everything before it is a path that may be wrong for this machine.
|
|
260
|
+
*/
|
|
261
|
+
function hookSubcommandTail(command) {
|
|
262
|
+
const match = /(^|\s)(hook\s+\S+.*)$/u.exec(command.trim());
|
|
263
|
+
return match?.[2]?.trim() ?? null;
|
|
264
|
+
}
|
|
265
|
+
function normalizeStringArray(value) {
|
|
266
|
+
if (value === undefined)
|
|
267
|
+
return [];
|
|
268
|
+
if (!Array.isArray(value))
|
|
269
|
+
return null;
|
|
270
|
+
if (!value.every((entry) => typeof entry === "string"))
|
|
271
|
+
return null;
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
function normalizeStringRecord(value) {
|
|
275
|
+
if (value === undefined)
|
|
276
|
+
return {};
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
278
|
+
return null;
|
|
279
|
+
const out = {};
|
|
280
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
281
|
+
if (typeof entry !== "string")
|
|
282
|
+
return null;
|
|
283
|
+
out[key] = entry;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one filesystem seam `cockpit memory install` writes through.
|
|
3
|
+
*
|
|
4
|
+
* It exists for two reasons. The first is production: every file this installer
|
|
5
|
+
* touches belongs to a host that may be RUNNING while we write — Claude Code
|
|
6
|
+
* rewrites `~/.claude.json` on its own schedule — so the write is staged beside
|
|
7
|
+
* the target and renamed over it, which is the closest thing to atomic either
|
|
8
|
+
* host family offers. A reader never sees a half-written config.
|
|
9
|
+
*
|
|
10
|
+
* The second is proof. The read-back check (BLI-2541) is only worth something if
|
|
11
|
+
* a test can make the stored bytes differ from the written ones; injecting this
|
|
12
|
+
* interface is how `memory-install.test.ts` proves `read_back_mismatch` fires
|
|
13
|
+
* instead of asserting that it would.
|
|
14
|
+
*/
|
|
15
|
+
import { chmod, mkdir, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { describeError, isMissingFileFailure } from "../health-detail.js";
|
|
19
|
+
export function defaultMemoryFileIo() {
|
|
20
|
+
return {
|
|
21
|
+
async readText(file) {
|
|
22
|
+
try {
|
|
23
|
+
return await readFile(file, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (isMissingFileFailure(error))
|
|
27
|
+
return null;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
async writeText(file, contents) {
|
|
32
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
33
|
+
const staged = `${file}.${randomBytes(6).toString("hex")}.tmp`;
|
|
34
|
+
// A staged file is created with the process umask and the rename carries
|
|
35
|
+
// that mode onto the target — so a `~/.claude.json` or `~/.codex/config.toml`
|
|
36
|
+
// the person keeps at 0600 would come out 0644, quietly widened by an
|
|
37
|
+
// installer that was only meant to add one key. Carry the existing mode
|
|
38
|
+
// across; a file that does not exist yet keeps the umask default.
|
|
39
|
+
const existingMode = await stat(file)
|
|
40
|
+
.then((info) => info.mode & 0o777)
|
|
41
|
+
.catch(() => null);
|
|
42
|
+
try {
|
|
43
|
+
await writeFile(staged, contents, "utf8");
|
|
44
|
+
if (existingMode !== null)
|
|
45
|
+
await chmod(staged, existingMode);
|
|
46
|
+
await rename(staged, file);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// A rename that will not land (a locked destination on Windows, a
|
|
50
|
+
// cross-device staging directory) must not lose the write — and it must
|
|
51
|
+
// say which of the two happened, because "install failed" on a config
|
|
52
|
+
// file is exactly the silent failure the fleet contract forbids.
|
|
53
|
+
console.error("[memory-install] staged write could not be renamed; writing in place", JSON.stringify({
|
|
54
|
+
reason: "staged_rename_failed",
|
|
55
|
+
byte_size: contents.length,
|
|
56
|
+
...describeError(error),
|
|
57
|
+
}));
|
|
58
|
+
await unlink(staged).catch(() => undefined);
|
|
59
|
+
await writeFile(file, contents, "utf8");
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Codex skill `cockpit memory install` writes to
|
|
3
|
+
* `~/.codex/skills/bli-memory/` (BLI-3580).
|
|
4
|
+
*
|
|
5
|
+
* It replaces the role of the four vendor skills a Codex machine carries today
|
|
6
|
+
* (`supermemory-search`, `-save`, `-forget`, `-login`): one discoverable skill
|
|
7
|
+
* directory with `SKILL.md` as the index and one reference per operation —
|
|
8
|
+
* search, save, update, forget. There is no login reference because there is no
|
|
9
|
+
* fourth credential: the MCP server authenticates with the collector's own
|
|
10
|
+
* device token, the one `cockpit login` already put on the machine.
|
|
11
|
+
*
|
|
12
|
+
* Content is static prose. It is written whole and compared whole, so an
|
|
13
|
+
* install is idempotent to the byte and a drifted copy is replaced rather than
|
|
14
|
+
* merged — the same deal `cockpit agent-rules` offers for its managed block.
|
|
15
|
+
*/
|
|
16
|
+
/** Relative path → exact file contents. The map IS the install. */
|
|
17
|
+
export function memoryCodexSkillFiles() {
|
|
18
|
+
return {
|
|
19
|
+
"SKILL.md": SKILL_MD,
|
|
20
|
+
"references/search.md": SEARCH_MD,
|
|
21
|
+
"references/save.md": SAVE_MD,
|
|
22
|
+
"references/update.md": UPDATE_MD,
|
|
23
|
+
"references/forget.md": FORGET_MD,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const SKILL_MD = `---
|
|
27
|
+
name: bli-memory
|
|
28
|
+
description: BLI Memory — durable memory for this machine. Use when you need to recall what was decided before, or when a session produced a durable decision, preference or correction worth keeping.
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# BLI Memory
|
|
32
|
+
|
|
33
|
+
Durable memory lives in BLI's own store, reached through the \`bli-memory\` MCP
|
|
34
|
+
server. Four tools, and nothing else:
|
|
35
|
+
|
|
36
|
+
| Tool | When |
|
|
37
|
+
|---|---|
|
|
38
|
+
| \`search_memory\` | before answering anything that depends on earlier work |
|
|
39
|
+
| \`save_memory\` | a durable decision, preference, correction or constraint appeared |
|
|
40
|
+
| \`update_memory\` | a saved memory is now wrong and has a successor |
|
|
41
|
+
| \`forget_memory\` | a saved memory should never surface again |
|
|
42
|
+
|
|
43
|
+
Rules that do not bend:
|
|
44
|
+
|
|
45
|
+
- **Recall before you assert.** An empty result means the record is silent, not
|
|
46
|
+
that nothing happened. Say which one you are looking at.
|
|
47
|
+
- **Save the decision, not the transcript.** The nightly rollups already
|
|
48
|
+
distil sessions; this store is for the durable line a person would repeat.
|
|
49
|
+
- **Never fuzzy-match when forgetting.** Exact id, or exact text inside one
|
|
50
|
+
container. Deleting the wrong memory is worse than keeping a stale one.
|
|
51
|
+
- **Say what you did.** A save that produced an id says so; a search that
|
|
52
|
+
answered from memory credits it.
|
|
53
|
+
|
|
54
|
+
References: \`references/search.md\`, \`references/save.md\`,
|
|
55
|
+
\`references/update.md\`, \`references/forget.md\`.
|
|
56
|
+
|
|
57
|
+
Installed and kept current by \`cockpit memory install\`. Do not edit by hand —
|
|
58
|
+
the next sync tick replaces it.
|
|
59
|
+
`;
|
|
60
|
+
const SEARCH_MD = `# search_memory
|
|
61
|
+
|
|
62
|
+
\`\`\`
|
|
63
|
+
in : { query: string (1-1000, required),
|
|
64
|
+
containerTag?: string, // omitted -> this machine's default container
|
|
65
|
+
limit?: 1-20 = 5,
|
|
66
|
+
includeProfile?: boolean = true }
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
Returns relevance-ranked memories plus, when \`includeProfile\`, the container's
|
|
70
|
+
most recent context. Rows that were forgotten or superseded are unreachable by
|
|
71
|
+
construction and never come back.
|
|
72
|
+
|
|
73
|
+
An empty answer is a finding: report "the record is silent on this", never
|
|
74
|
+
"there is nothing".
|
|
75
|
+
`;
|
|
76
|
+
const SAVE_MD = `# save_memory
|
|
77
|
+
|
|
78
|
+
\`\`\`
|
|
79
|
+
in : { content: string (1-200000, required),
|
|
80
|
+
containerTag?: string,
|
|
81
|
+
customId?: string, // e.g. the session id — the upsert key
|
|
82
|
+
metadata?: object }
|
|
83
|
+
out: { id, containerTag, created|updated, chars }
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
Stores what it is given; it does not summarise. Pass \`customId\` when a session
|
|
87
|
+
may save more than once, so the second save updates the first row instead of
|
|
88
|
+
adding a duplicate.
|
|
89
|
+
|
|
90
|
+
Save the durable thing: a decision and its reason, a preference, a correction, a
|
|
91
|
+
constraint discovered the hard way. Do not save the whole transcript.
|
|
92
|
+
`;
|
|
93
|
+
const UPDATE_MD = `# update_memory
|
|
94
|
+
|
|
95
|
+
\`\`\`
|
|
96
|
+
in : { id: uuid (required), content: string (required), metadata?: object }
|
|
97
|
+
out: { newId, supersededId, containerTag, reEmbedded: true }
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
An update is an insert plus a link: the new text becomes a new row, the old one
|
|
101
|
+
is marked superseded and stops being reachable from recall while staying
|
|
102
|
+
readable for audit.
|
|
103
|
+
|
|
104
|
+
It refuses — loudly, never silently — when the id is unknown, already forgotten,
|
|
105
|
+
or already superseded. The last case names the successor so you can retry
|
|
106
|
+
against it.
|
|
107
|
+
`;
|
|
108
|
+
const FORGET_MD = `# forget_memory
|
|
109
|
+
|
|
110
|
+
\`\`\`
|
|
111
|
+
in : { id?: uuid, content?: string, containerTag?: string, reason?: string }
|
|
112
|
+
exactly one of id | content; containerTag is REQUIRED with content
|
|
113
|
+
out: { forgotten: [{ id, containerTag, textPreview, created_at }], count }
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
Soft delete: the row is marked forgotten and never surfaces through recall
|
|
117
|
+
again. Matching by content is exact after whitespace normalisation — never
|
|
118
|
+
fuzzy, and never widened to every container.
|
|
119
|
+
|
|
120
|
+
It returns what it removed. A bare "done" is not an answer.
|
|
121
|
+
`;
|