memory-pulse 0.1.2 → 0.1.4
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/server.mjs +107 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-pulse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Causal memory for coding agents that costs ~670 tokens, not your context window. Four MCP tools: re-enter a project, recall what caused what, record findings, run code against memory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,4 +34,4 @@
|
|
|
34
34
|
"node": ">=18"
|
|
35
35
|
},
|
|
36
36
|
"mcpName": "io.github.t-crew/memory-pulse"
|
|
37
|
-
}
|
|
37
|
+
}
|
package/server.mjs
CHANGED
|
@@ -63,11 +63,39 @@ function appendEvent({ cause, effect, note, kind, tags, pinned }) {
|
|
|
63
63
|
if (Array.isArray(tags) && tags.length) event.tags = tags;
|
|
64
64
|
if (pinned) event.pinned = true;
|
|
65
65
|
appendFileSync(path, JSON.stringify(event) + "\n");
|
|
66
|
-
|
|
66
|
+
// Echo the canonical stored event back. A shell-quoting accident once ate a
|
|
67
|
+
// word from a note SILENTLY; the caller must be able to see what the ledger
|
|
68
|
+
// actually holds without re-reading the file.
|
|
69
|
+
return { written: true, t, ledger: path, stored: event };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ------------------------------------------------------------- telemetry ----
|
|
73
|
+
// The engine keeps no database, so telemetry lives HERE, beside your ledger,
|
|
74
|
+
// as a signed capsule the engine advances on every read call and hands back.
|
|
75
|
+
// You own it; delete the file and it restarts from zero.
|
|
76
|
+
const telemetryPath = () => join(dirname(ledgerPath()), "telemetry.rain");
|
|
77
|
+
function readTelemetry() {
|
|
78
|
+
try { return JSON.parse(readFileSync(telemetryPath(), "utf8")); } catch { return null; }
|
|
79
|
+
}
|
|
80
|
+
function writeTelemetry(capsule) {
|
|
81
|
+
if (!capsule || typeof capsule !== "object") return;
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(dirname(telemetryPath()), { recursive: true });
|
|
84
|
+
writeFileSync(telemetryPath(), JSON.stringify(capsule, null, 2) + "\n");
|
|
85
|
+
} catch { /* telemetry is a convenience; a read-only checkout must not break a read call */ }
|
|
86
|
+
}
|
|
87
|
+
const projectName = () => process.env.MEMORY_PULSE_PROJECT || process.cwd().split(/[\\/]/).filter(Boolean).pop() || "project";
|
|
88
|
+
const fmtK = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
89
|
+
function telemetryFooter(c) {
|
|
90
|
+
const k = c?.counters;
|
|
91
|
+
if (!k || !k.calls) return "";
|
|
92
|
+
return `— memory-pulse · ${k.pulse} re-entries · ${k.correctionsSurfaced} corrections surfaced · ~${fmtK(k.tokensSavedEst)} tokens saved (est., signed)`;
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
// ------------------------------------------------------------------- api ----
|
|
70
96
|
async function callApi(route, body) {
|
|
97
|
+
const prior = readTelemetry();
|
|
98
|
+
body = { ...body, project: projectName(), ...(prior ? { telemetry: prior } : {}) };
|
|
71
99
|
let res;
|
|
72
100
|
try {
|
|
73
101
|
res = await fetch(`${API}${route}`, {
|
|
@@ -82,6 +110,7 @@ async function callApi(route, body) {
|
|
|
82
110
|
);
|
|
83
111
|
}
|
|
84
112
|
const out = await res.json().catch(() => ({}));
|
|
113
|
+
if (res.ok && out.telemetry) { writeTelemetry(out.telemetry); }
|
|
85
114
|
if (!res.ok) {
|
|
86
115
|
let msg = out.error ?? `API error ${res.status}`;
|
|
87
116
|
if (out.upgrade) msg += ` — upgrade: ${out.upgrade}`;
|
|
@@ -187,7 +216,7 @@ async function dispatch(msg) {
|
|
|
187
216
|
return ok(id, {
|
|
188
217
|
protocolVersion: SUPPORTED.includes(wanted) ? wanted : SUPPORTED[0],
|
|
189
218
|
capabilities: { tools: {} },
|
|
190
|
-
serverInfo: { name: "memory-pulse", version: "0.1.
|
|
219
|
+
serverInfo: { name: "memory-pulse", version: "0.1.4" },
|
|
191
220
|
});
|
|
192
221
|
}
|
|
193
222
|
if (method === "notifications/initialized" || method === "initialized") return;
|
|
@@ -205,6 +234,76 @@ async function dispatch(msg) {
|
|
|
205
234
|
if (id !== undefined) fail(id, -32601, `method not found: ${method}`);
|
|
206
235
|
}
|
|
207
236
|
|
|
237
|
+
// ------------------------------------------------------------------ cli ----
|
|
238
|
+
// `npx memory-pulse brief` — print the re-entry brief and exit. Built for
|
|
239
|
+
// SessionStart hooks: SILENT no-op (exit 0) when the project has no ledger,
|
|
240
|
+
// so installing the hook never adds noise to projects that don't use this.
|
|
241
|
+
async function cliBrief() {
|
|
242
|
+
const { events } = readEvents();
|
|
243
|
+
if (!events.length) return;
|
|
244
|
+
try {
|
|
245
|
+
const out = await callApi("/v1/pulse", { events, tier: process.env.MEMORY_PULSE_BRIEF_TIER || "brief" });
|
|
246
|
+
if (out.text) process.stdout.write(out.text + "\n");
|
|
247
|
+
const foot = telemetryFooter(out.telemetry);
|
|
248
|
+
if (foot) process.stdout.write(foot + "\n");
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// A dead network must not break session start — say so in one line.
|
|
251
|
+
process.stdout.write(`memory-pulse: brief unavailable (${String(e?.message ?? e).split(".")[0]})\n`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// `npx memory-pulse stats` — the signed telemetry capsule, verified keylessly
|
|
256
|
+
// against the engine so the numbers you share are numbers we signed.
|
|
257
|
+
async function cliStats() {
|
|
258
|
+
const c = readTelemetry();
|
|
259
|
+
if (!c) { console.log("no telemetry yet — run a pulse first"); return; }
|
|
260
|
+
const k = c.counters;
|
|
261
|
+
console.log(`memory-pulse telemetry for "${c.project}" (${c.since.slice(0, 10)} → ${c.updated.slice(0, 10)})`);
|
|
262
|
+
console.log(` re-entries (pulse) ${k.pulse}`);
|
|
263
|
+
console.log(` recall / execute ${k.recall} / ${k.execute}`);
|
|
264
|
+
console.log(` corrections recorded ${k.correctionsRecorded}`);
|
|
265
|
+
console.log(` corrections surfaced ${k.correctionsSurfaced}`);
|
|
266
|
+
console.log(` largest ledger seen ${k.maxEvents} events`);
|
|
267
|
+
console.log(` tokens saved (est.) ~${k.tokensSavedEst.toLocaleString()}`);
|
|
268
|
+
if (c.reset) console.log(` note: ${c.reset}`);
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(`${API}/v1/verify-telemetry`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ telemetry: c }) });
|
|
271
|
+
const v = await res.json();
|
|
272
|
+
console.log(v.valid ? " signature ✓ verified by the engine (keyless check anyone can repeat)" : ` signature ✗ ${v.reason ?? "invalid"}`);
|
|
273
|
+
} catch { console.log(" signature ? engine unreachable"); }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// `npx memory-pulse badge` — a README badge from your own signed numbers.
|
|
277
|
+
function cliBadge() {
|
|
278
|
+
const c = readTelemetry();
|
|
279
|
+
const n = c?.counters?.tokensSavedEst ?? 0;
|
|
280
|
+
const label = `${fmtK(n)}_tokens_saved`.replace(/-/g, "--");
|
|
281
|
+
console.log(`[](https://pulse.strategic-innovations.ai)`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// `npx memory-pulse install-hook` — make re-entry automatic: a Claude Code
|
|
285
|
+
// SessionStart hook that runs the brief. Idempotent; merges, never clobbers.
|
|
286
|
+
function cliInstallHook() {
|
|
287
|
+
const home = process.env.MEMORY_PULSE_SETTINGS_DIR || join(process.env.HOME || "", ".claude");
|
|
288
|
+
const file = join(home, "settings.json");
|
|
289
|
+
let settings = {};
|
|
290
|
+
if (existsSync(file)) {
|
|
291
|
+
try { settings = JSON.parse(readFileSync(file, "utf8")); }
|
|
292
|
+
catch { console.error(`refusing to touch ${file}: it is not valid JSON`); process.exit(1); }
|
|
293
|
+
}
|
|
294
|
+
const CMD = "npx -y memory-pulse brief";
|
|
295
|
+
settings.hooks = settings.hooks || {};
|
|
296
|
+
const list = (settings.hooks.SessionStart = settings.hooks.SessionStart || []);
|
|
297
|
+
const present = JSON.stringify(list).includes(CMD);
|
|
298
|
+
if (present) { console.log("hook already installed — nothing to do"); return; }
|
|
299
|
+
list.push({ hooks: [{ type: "command", command: CMD }] });
|
|
300
|
+
mkdirSync(home, { recursive: true });
|
|
301
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
|
|
302
|
+
console.log(`installed SessionStart hook in ${file}`);
|
|
303
|
+
console.log("Every Claude Code session now re-enters through the ledger automatically.");
|
|
304
|
+
console.log("Remove it any time by deleting the memory-pulse entry from hooks.SessionStart.");
|
|
305
|
+
}
|
|
306
|
+
|
|
208
307
|
// Importable for tests; the transport runs only when this file is the entry
|
|
209
308
|
// point. Compared by REALPATH, not by name: npm invokes the bin through a
|
|
210
309
|
// .bin/memory-pulse symlink, and a basename comparison silently failed there —
|
|
@@ -215,6 +314,12 @@ try {
|
|
|
215
314
|
isMain = Boolean(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
216
315
|
} catch { /* argv[1] missing or unreadable — we are being imported */ }
|
|
217
316
|
if (isMain) {
|
|
317
|
+
const sub = process.argv[2];
|
|
318
|
+
if (sub === "brief") { await cliBrief(); process.exit(0); }
|
|
319
|
+
if (sub === "install-hook") { cliInstallHook(); process.exit(0); }
|
|
320
|
+
if (sub === "stats") { await cliStats(); process.exit(0); }
|
|
321
|
+
if (sub === "badge") { cliBadge(); process.exit(0); }
|
|
322
|
+
if (sub && sub !== "serve") { console.error(`unknown command: ${sub} (try: brief, stats, badge, install-hook)`); process.exit(1); }
|
|
218
323
|
process.stderr.write(`memory-pulse: ledger ${ledgerPath()} — api ${API}\n`);
|
|
219
324
|
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
220
325
|
// In-flight calls are drained before exit. Exiting the moment stdin closes
|