@agfpd/iapeer-memory 0.2.0 → 0.2.1
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/package.json +2 -2
- package/src/binary.ts +17 -10
- package/src/cli.ts +26 -8
- package/src/commands/hook.ts +7 -8
- package/src/commands/init.ts +32 -29
- package/src/commands/install-binary.ts +3 -2
- package/src/commands/memoryd.ts +3 -2
- package/src/commands/status.ts +15 -8
- package/src/commands/uninstall.ts +19 -26
- package/src/commands/update.ts +9 -8
- package/src/commands/verify.ts +15 -8
- package/src/egress.ts +181 -0
- package/src/fleet.ts +33 -35
- package/src/provision.ts +3 -2
- package/src/roles.ts +2 -1
- package/src/signing.ts +19 -23
- package/src/slot.ts +30 -30
- package/src/surfaces/claude.ts +9 -6
- package/src/surfaces/codex.ts +3 -2
- package/src/sync-versions.ts +3 -2
- package/src/templates/index.ts +2 -1
- package/src/watcher.ts +72 -68
package/src/surfaces/claude.ts
CHANGED
|
@@ -24,9 +24,11 @@
|
|
|
24
24
|
* (embedded bodies, bytes-compare; the directory prefix is OUR
|
|
25
25
|
* namespace — unprovision removes every match).
|
|
26
26
|
*
|
|
27
|
-
* Pickup semantics (documented, boris design input):
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Pickup semantics (documented, boris design input): hooks and MCP land on
|
|
28
|
+
* the peer's NEXT session start — a live session does not re-read them (same
|
|
29
|
+
* semantics the plugin form had). Skills are picked up HOT by live sessions
|
|
30
|
+
* (live observation, боевой E2E 11.06) — «next restart» is the conservative
|
|
31
|
+
* guarantee for all three, exact for hooks/MCP.
|
|
30
32
|
*
|
|
31
33
|
* Idempotent by construction: same version re-run → `already` on every
|
|
32
34
|
* surface; drift (a mangled/deleted entry) → re-written. The remove path is
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
import fs from "node:fs";
|
|
39
41
|
import path from "node:path";
|
|
40
42
|
import { SKILL_BODIES, SKILL_DIR_PREFIX, SKILL_NAMES } from "../templates/skills.js";
|
|
43
|
+
import { guardedWriteFileSync, guardedUnlinkSync, guardedRmSync } from "@agfpd/iapeer-memory-core";
|
|
41
44
|
|
|
42
45
|
export const MCP_SERVER_KEY = "iapeer-memory";
|
|
43
46
|
/**
|
|
@@ -87,7 +90,7 @@ function shimContent(verb: "post-write" | "session-start"): string {
|
|
|
87
90
|
function writeFileAtomic(filePath: string, content: string, mode?: number): void {
|
|
88
91
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
89
92
|
const tmp = `${filePath}.tmp`;
|
|
90
|
-
|
|
93
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
91
94
|
if (mode !== undefined) fs.chmodSync(tmp, mode);
|
|
92
95
|
fs.renameSync(tmp, filePath);
|
|
93
96
|
}
|
|
@@ -352,7 +355,7 @@ export function removeClaudeMcp(opts: { cwd: string }): SurfaceOutcome {
|
|
|
352
355
|
if (serversEmpty && onlyServersKey) {
|
|
353
356
|
// semantically empty file — with our key gone it says nothing; a file we
|
|
354
357
|
// most likely created. Removing it leaves the cwd exactly as found.
|
|
355
|
-
|
|
358
|
+
guardedUnlinkSync(mcpPath);
|
|
356
359
|
return { surface: "mcp", action: "removed", path: mcpPath, detail: "file removed (empty after our key)" };
|
|
357
360
|
}
|
|
358
361
|
writeFileAtomic(mcpPath, `${JSON.stringify(current, null, 2)}\n`);
|
|
@@ -399,7 +402,7 @@ export function removeClaudeSkills(opts: { cwd: string }): SurfaceOutcome {
|
|
|
399
402
|
return { surface: "skills", action: "absent", path: skillsDir };
|
|
400
403
|
}
|
|
401
404
|
for (const e of ours) {
|
|
402
|
-
|
|
405
|
+
guardedRmSync(path.join(skillsDir, e), { recursive: true, force: true });
|
|
403
406
|
}
|
|
404
407
|
// sweep empty containers we may have created (skills/ then .claude/)
|
|
405
408
|
for (const dir of [skillsDir, path.dirname(skillsDir)]) {
|
package/src/surfaces/codex.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import fs from "node:fs";
|
|
37
37
|
import path from "node:path";
|
|
38
38
|
import type { SurfaceOutcome } from "./claude.js";
|
|
39
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
|
|
39
40
|
|
|
40
41
|
export const CODEX_MCP_SECTION = "mcp_servers.iapeer-memory";
|
|
41
42
|
const SECTION_HEADER_RE = /^\s*\[/;
|
|
@@ -61,7 +62,7 @@ export function expectedCodexBlock(port: number): string {
|
|
|
61
62
|
function writeFileAtomic(filePath: string, content: string): void {
|
|
62
63
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
64
|
const tmp = `${filePath}.tmp`;
|
|
64
|
-
|
|
65
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
65
66
|
fs.renameSync(tmp, filePath);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -115,7 +116,7 @@ export function removeCodexMcp(opts: { cwd: string }): SurfaceOutcome {
|
|
|
115
116
|
const foreign = withoutOurSections(text.split("\n"));
|
|
116
117
|
if (foreign.every((l) => l.trim() === "")) {
|
|
117
118
|
// nothing but our block lived here — leave the cwd exactly as found
|
|
118
|
-
|
|
119
|
+
guardedUnlinkSync(configPath);
|
|
119
120
|
return { surface: "mcp", action: "removed", path: configPath, detail: "file removed (empty after our block)" };
|
|
120
121
|
}
|
|
121
122
|
writeFileAtomic(configPath, `${foreign.join("\n")}\n`);
|
package/src/sync-versions.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
16
17
|
|
|
17
18
|
export type SyncOutcome = { file: string; action: "updated" | "identical" | "missing" };
|
|
18
19
|
|
|
@@ -46,7 +47,7 @@ export function syncVersions(opts: {
|
|
|
46
47
|
}
|
|
47
48
|
manifest.version = opts.version;
|
|
48
49
|
// 2-space indent + trailing newline — the repo's manifest style.
|
|
49
|
-
|
|
50
|
+
guardedWriteFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
50
51
|
outcomes.push({ file: rel, action: "updated" });
|
|
51
52
|
}
|
|
52
53
|
return outcomes;
|
|
@@ -81,7 +82,7 @@ export function syncCoreDependencyPin(opts: {
|
|
|
81
82
|
}
|
|
82
83
|
deps["@agfpd/iapeer-memory-core"] = opts.version;
|
|
83
84
|
manifest.dependencies = deps;
|
|
84
|
-
|
|
85
|
+
guardedWriteFileSync(
|
|
85
86
|
opts.packageManifestPath,
|
|
86
87
|
JSON.stringify(manifest, null, 2) + "\n",
|
|
87
88
|
"utf-8",
|
package/src/templates/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import type { LocaleId } from "@agfpd/iapeer-memory-core";
|
|
17
17
|
import { GUIDE_EN } from "./guide-en.js";
|
|
18
18
|
import { GUIDE_RU } from "./guide-ru.js";
|
|
19
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
19
20
|
import {
|
|
20
21
|
SCRIBER_DOCTRINE_EN,
|
|
21
22
|
DREAMWEAVER_DOCTRINE_EN,
|
|
@@ -152,7 +153,7 @@ export function materialiseTemplates(opts: {
|
|
|
152
153
|
}
|
|
153
154
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
154
155
|
const tmp = `${file}.tmp`;
|
|
155
|
-
|
|
156
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
156
157
|
fs.renameSync(tmp, file);
|
|
157
158
|
written.push(file);
|
|
158
159
|
}
|
package/src/watcher.ts
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
|
|
36
36
|
import fs from "node:fs";
|
|
37
37
|
import path from "node:path";
|
|
38
|
+
import { IAPEER_BIN, type Egress } from "./egress.js";
|
|
39
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
38
40
|
|
|
39
41
|
export const WATCHER_TRIGGER_ID = "iapeer-memory-memoryd";
|
|
40
42
|
/** Fail-open sweep (инверсия, ADR-015): check-gated hourly timer → index. */
|
|
@@ -69,7 +71,7 @@ function writeExecutable(filePath: string, content: string): "written" | "identi
|
|
|
69
71
|
}
|
|
70
72
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
73
|
const tmp = `${filePath}.tmp`;
|
|
72
|
-
|
|
74
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
73
75
|
fs.chmodSync(tmp, 0o755);
|
|
74
76
|
fs.renameSync(tmp, filePath);
|
|
75
77
|
return "written";
|
|
@@ -152,7 +154,7 @@ export function patchWakePolicyEphemeral(
|
|
|
152
154
|
if (profile.wake_policy === "ephemeral") return "identical";
|
|
153
155
|
profile.wake_policy = "ephemeral";
|
|
154
156
|
const tmp = `${profilePath}.tmp`;
|
|
155
|
-
|
|
157
|
+
guardedWriteFileSync(tmp, `${JSON.stringify(profile, null, 2)}\n`, "utf-8");
|
|
156
158
|
fs.renameSync(tmp, profilePath);
|
|
157
159
|
return "written";
|
|
158
160
|
}
|
|
@@ -225,45 +227,35 @@ export function fromIdentity(personality: string, runtime = DEFAULT_FROM_RUNTIME
|
|
|
225
227
|
return `${runtime}-${personality}`;
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
function iapSend(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
detail: "iap send suppressed (test sandbox)",
|
|
250
|
-
};
|
|
230
|
+
function iapSend(
|
|
231
|
+
egress: Egress,
|
|
232
|
+
opts: {
|
|
233
|
+
message: string;
|
|
234
|
+
fromIdentity: string;
|
|
235
|
+
/** Notifier peer to address: `watcher` (events) or `timer` (cron/sweep). */
|
|
236
|
+
to?: "watcher" | "timer";
|
|
237
|
+
iapeerBin?: string;
|
|
238
|
+
},
|
|
239
|
+
): IapSendResult {
|
|
240
|
+
// The hard fuse of incident 10.06 (prod Index: crashlooping temp-path
|
|
241
|
+
// triggers; /tmp tmux sockets are host-global, no sandbox env contains a
|
|
242
|
+
// real send) lives in the EGRESS CONSTRUCTOR now (deny-by-default §4):
|
|
243
|
+
// under a test sandbox this handle refuses the spawn before it happens.
|
|
244
|
+
const bin = opts.iapeerBin ?? IAPEER_BIN;
|
|
245
|
+
const proc = egress.spawnSync(
|
|
246
|
+
[bin, "send", opts.to ?? "watcher", "--from", opts.fromIdentity, "--message", opts.message],
|
|
247
|
+
{ explicitBin: opts.iapeerBin !== undefined },
|
|
248
|
+
);
|
|
249
|
+
if (proc.refused) {
|
|
250
|
+
return { ok: false, suppressed: true, detail: "iap send suppressed (test sandbox)" };
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
proc = Bun.spawnSync(
|
|
256
|
-
[bin, "send", opts.to ?? "watcher", "--from", opts.fromIdentity, "--message", opts.message],
|
|
257
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
258
|
-
);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
return { ok: false, detail: `${bin} unavailable: ${String(err)}` };
|
|
252
|
+
if (proc.spawnError) {
|
|
253
|
+
return { ok: false, detail: `${bin} unavailable: ${proc.spawnError}` };
|
|
261
254
|
}
|
|
262
255
|
if (proc.exitCode !== 0) {
|
|
263
256
|
return {
|
|
264
257
|
ok: false,
|
|
265
|
-
detail:
|
|
266
|
-
(proc.stderr?.toString().trim() || "") || `iapeer send exited ${proc.exitCode}`,
|
|
258
|
+
detail: proc.stderr.trim() || `iapeer send exited ${proc.exitCode}`,
|
|
267
259
|
};
|
|
268
260
|
}
|
|
269
261
|
// NB: a 0 exit code means DELIVERED, not "registration valid" — the
|
|
@@ -272,18 +264,21 @@ function iapSend(opts: {
|
|
|
272
264
|
return { ok: true, detail: "delivered (confirm via the registrant's profile)" };
|
|
273
265
|
}
|
|
274
266
|
|
|
275
|
-
export function registerWatcher(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
267
|
+
export function registerWatcher(
|
|
268
|
+
egress: Egress,
|
|
269
|
+
opts: {
|
|
270
|
+
launcherPath: string;
|
|
271
|
+
/** Registrant PERSONALITY (owner of the durable trigger + default target). */
|
|
272
|
+
registrant?: string;
|
|
273
|
+
/** Runtime prefix of the registrant's identity (claude for role peers). */
|
|
274
|
+
runtime?: string;
|
|
275
|
+
target?: string;
|
|
276
|
+
id?: string;
|
|
277
|
+
iapeerBin?: string;
|
|
278
|
+
},
|
|
279
|
+
): IapSendResult {
|
|
285
280
|
const registrant = opts.registrant ?? DEFAULT_REGISTRANT;
|
|
286
|
-
return iapSend({
|
|
281
|
+
return iapSend(egress, {
|
|
287
282
|
fromIdentity: fromIdentity(registrant, opts.runtime),
|
|
288
283
|
iapeerBin: opts.iapeerBin,
|
|
289
284
|
message: registrationMessage({
|
|
@@ -296,13 +291,16 @@ export function registerWatcher(opts: {
|
|
|
296
291
|
});
|
|
297
292
|
}
|
|
298
293
|
|
|
299
|
-
export function unregisterWatcher(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
294
|
+
export function unregisterWatcher(
|
|
295
|
+
egress: Egress,
|
|
296
|
+
opts: {
|
|
297
|
+
registrant?: string;
|
|
298
|
+
runtime?: string;
|
|
299
|
+
id?: string;
|
|
300
|
+
iapeerBin?: string;
|
|
301
|
+
},
|
|
302
|
+
): IapSendResult {
|
|
303
|
+
return iapSend(egress, {
|
|
306
304
|
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
307
305
|
iapeerBin: opts.iapeerBin,
|
|
308
306
|
message: JSON.stringify({ cmd: "unregister", id: opts.id ?? WATCHER_TRIGGER_ID }),
|
|
@@ -310,13 +308,16 @@ export function unregisterWatcher(opts: {
|
|
|
310
308
|
}
|
|
311
309
|
|
|
312
310
|
/** Register a timer trigger (sweep/dream) — body built by *TimerMessage(). */
|
|
313
|
-
export function registerTimer(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
311
|
+
export function registerTimer(
|
|
312
|
+
egress: Egress,
|
|
313
|
+
opts: {
|
|
314
|
+
message: string;
|
|
315
|
+
registrant?: string;
|
|
316
|
+
runtime?: string;
|
|
317
|
+
iapeerBin?: string;
|
|
318
|
+
},
|
|
319
|
+
): IapSendResult {
|
|
320
|
+
return iapSend(egress, {
|
|
320
321
|
to: "timer",
|
|
321
322
|
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
322
323
|
iapeerBin: opts.iapeerBin,
|
|
@@ -324,13 +325,16 @@ export function registerTimer(opts: {
|
|
|
324
325
|
});
|
|
325
326
|
}
|
|
326
327
|
|
|
327
|
-
export function unregisterTimer(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
328
|
+
export function unregisterTimer(
|
|
329
|
+
egress: Egress,
|
|
330
|
+
opts: {
|
|
331
|
+
id: string;
|
|
332
|
+
registrant?: string;
|
|
333
|
+
runtime?: string;
|
|
334
|
+
iapeerBin?: string;
|
|
335
|
+
},
|
|
336
|
+
): IapSendResult {
|
|
337
|
+
return iapSend(egress, {
|
|
334
338
|
to: "timer",
|
|
335
339
|
fromIdentity: fromIdentity(opts.registrant ?? DEFAULT_REGISTRANT, opts.runtime),
|
|
336
340
|
iapeerBin: opts.iapeerBin,
|