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.
Files changed (2) hide show
  1. package/package.json +2 -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.2",
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
- return { written: true, t, ledger: path };
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.2" },
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(`[![memory-pulse](https://img.shields.io/badge/memory--pulse-${label}-6366f1)](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