memory-pulse 0.1.3 → 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 +1 -1
  2. package/server.mjs +61 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-pulse",
3
- "version": "0.1.3",
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": {
package/server.mjs CHANGED
@@ -69,8 +69,33 @@ function appendEvent({ cause, effect, note, kind, tags, pinned }) {
69
69
  return { written: true, t, ledger: path, stored: event };
70
70
  }
71
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)`;
93
+ }
94
+
72
95
  // ------------------------------------------------------------------- api ----
73
96
  async function callApi(route, body) {
97
+ const prior = readTelemetry();
98
+ body = { ...body, project: projectName(), ...(prior ? { telemetry: prior } : {}) };
74
99
  let res;
75
100
  try {
76
101
  res = await fetch(`${API}${route}`, {
@@ -85,6 +110,7 @@ async function callApi(route, body) {
85
110
  );
86
111
  }
87
112
  const out = await res.json().catch(() => ({}));
113
+ if (res.ok && out.telemetry) { writeTelemetry(out.telemetry); }
88
114
  if (!res.ok) {
89
115
  let msg = out.error ?? `API error ${res.status}`;
90
116
  if (out.upgrade) msg += ` — upgrade: ${out.upgrade}`;
@@ -190,7 +216,7 @@ async function dispatch(msg) {
190
216
  return ok(id, {
191
217
  protocolVersion: SUPPORTED.includes(wanted) ? wanted : SUPPORTED[0],
192
218
  capabilities: { tools: {} },
193
- serverInfo: { name: "memory-pulse", version: "0.1.3" },
219
+ serverInfo: { name: "memory-pulse", version: "0.1.4" },
194
220
  });
195
221
  }
196
222
  if (method === "notifications/initialized" || method === "initialized") return;
@@ -218,12 +244,43 @@ async function cliBrief() {
218
244
  try {
219
245
  const out = await callApi("/v1/pulse", { events, tier: process.env.MEMORY_PULSE_BRIEF_TIER || "brief" });
220
246
  if (out.text) process.stdout.write(out.text + "\n");
247
+ const foot = telemetryFooter(out.telemetry);
248
+ if (foot) process.stdout.write(foot + "\n");
221
249
  } catch (e) {
222
250
  // A dead network must not break session start — say so in one line.
223
251
  process.stdout.write(`memory-pulse: brief unavailable (${String(e?.message ?? e).split(".")[0]})\n`);
224
252
  }
225
253
  }
226
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
+
227
284
  // `npx memory-pulse install-hook` — make re-entry automatic: a Claude Code
228
285
  // SessionStart hook that runs the brief. Idempotent; merges, never clobbers.
229
286
  function cliInstallHook() {
@@ -260,7 +317,9 @@ if (isMain) {
260
317
  const sub = process.argv[2];
261
318
  if (sub === "brief") { await cliBrief(); process.exit(0); }
262
319
  if (sub === "install-hook") { cliInstallHook(); process.exit(0); }
263
- if (sub && sub !== "serve") { console.error(`unknown command: ${sub} (try: brief, install-hook)`); process.exit(1); }
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); }
264
323
  process.stderr.write(`memory-pulse: ledger ${ledgerPath()} — api ${API}\n`);
265
324
  const rl = readline.createInterface({ input: process.stdin, terminal: false });
266
325
  // In-flight calls are drained before exit. Exiting the moment stdin closes