peon-mem 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -20,8 +20,9 @@ prompt. It runs as a daemon on your machine, and nothing leaves it.
20
20
  tools. Each project gets an isolated child brain. Every injection carries both.
21
21
  - **Two memory layers, honestly measured.** Consolidated *beliefs* (decisions, preferences,
22
22
  facts, artifacts) give you the gist. An *episodic* verbatim layer recovers the exact details
23
- that lossy summaries drop. On LongMemEval, raw-episodic recall scored 61% where belief-only
24
- scored 17%.
23
+ that lossy summaries drop. In my own LongMemEval run, raw-episodic recall scored 61% where
24
+ belief-only scored 17% — self-measured, not an independent benchmark; reproduce it with
25
+ `npm run eval`.
25
26
  - **Automatic capture and injection.** Claude Code hooks record messages and events, then
26
27
  inject a query-ranked memory block (with an `⚠ MOST RELEVANT` headline) into every prompt.
27
28
  You never have to remember to save anything.
@@ -73,7 +74,7 @@ upstairs (the global brain). Quiet clerk. Perfect memory. The office runs on him
73
74
  | Hierarchy | **global parent brain → per-project child brains, inherited on every prompt** | user/agent/session scopes | per-agent | per-user | per-project file |
74
75
  | Capture | **automatic via hooks** (zero effort) | SDK calls you write | agent-managed | SDK calls | agent must remember to write |
75
76
  | Conflict handling | supersede/merge, **recoverable — never hard-deletes** | LLM may DELETE | self-edit | invalidation | overwrite |
76
- | Exact recall | episodic layer regression-tested (61% vs 17% belief-only, LongMemEval) | gist only | gist only | graph facts | whatever was written |
77
+ | Exact recall | episodic layer regression-tested (61% vs 17% belief-only on LongMemEval, self-measured) | gist only | gist only | graph facts | whatever was written |
77
78
  | Observability | **live Neural Universe monitor + daily self-audit (STL) + serve telemetry** | dashboard | — | — | — |
78
79
  | Verification | **committed eval ledger; negative results kept** | vendor benchmarks | — | vendor benchmarks | — |
79
80
 
@@ -122,7 +123,7 @@ The guided setup asks four things:
122
123
  3. Installs the **daemon** as an auto-start service (launchd on macOS, systemd user unit on Linux)
123
124
  4. **Detects your AI apps** and wires the MCP server (plus hooks for Claude Code) into the
124
125
  ones you pick. Auto-configured: Claude Code, Claude Desktop, Codex, Gemini CLI, Cursor,
125
- Windsurf, VS Code (Copilot MCP), Zed, LM Studio. Detected with in-app instructions: ChatGPT
126
+ Windsurf, Cline, VS Code (Copilot MCP), Zed, LM Studio. Detected with in-app instructions: ChatGPT
126
127
  Desktop, Perplexity Desktop. Every touched config gets a `.peon-backup`.
127
128
 
128
129
  That builds the package, starts the daemon as a service, wires your Claude Code hooks + MCP
package/bin/peon-mem.mjs CHANGED
@@ -48,6 +48,11 @@ function backupWrite(file, content) {
48
48
  // wire kinds: hooks+CLI (claude code) · toml (codex) · mcpServers JSON (most apps) ·
49
49
  // vscode "servers" JSON · zed context_servers · manual (UI-configured apps get instructions)
50
50
  const APP_SUPPORT = MAC ? join(HOME, "Library", "Application Support") : join(HOME, ".config");
51
+ const CODE_USER = join(APP_SUPPORT, "Code", "User");
52
+ const CLINE_EXTENSION_HOME = join(CODE_USER, "globalStorage", "saoudrizwan.claude-dev");
53
+ const CLINE_EXTENSION_FILE = join(CLINE_EXTENSION_HOME, "settings", "cline_mcp_settings.json");
54
+ const CLINE_CLI_HOME = join(HOME, ".cline");
55
+ const CLINE_CLI_FILE = join(CLINE_CLI_HOME, "data", "settings", "cline_mcp_settings.json");
51
56
  function detectApps() {
52
57
  return [
53
58
  { id: "claude", name: "Claude Code", kind: "claude-code",
@@ -66,9 +71,12 @@ function detectApps() {
66
71
  { id: "windsurf", name: "Windsurf", kind: "json",
67
72
  file: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
68
73
  found: existsSync(join(HOME, ".codeium", "windsurf")) || existsSync("/Applications/Windsurf.app") },
74
+ { id: "cline", name: "Cline", kind: "json",
75
+ file: existsSync(CLINE_EXTENSION_HOME) ? CLINE_EXTENSION_FILE : CLINE_CLI_FILE,
76
+ found: existsSync(CLINE_EXTENSION_HOME) || which("cline") || existsSync(CLINE_CLI_HOME) },
69
77
  { id: "vscode", name: "VS Code (Copilot MCP)", kind: "vscode",
70
- file: MAC ? join(APP_SUPPORT, "Code", "User", "mcp.json") : join(HOME, ".config", "Code", "User", "mcp.json"),
71
- found: which("code") || existsSync(MAC ? join(APP_SUPPORT, "Code") : join(HOME, ".config", "Code")) },
78
+ file: join(CODE_USER, "mcp.json"),
79
+ found: which("code") || existsSync(join(APP_SUPPORT, "Code")) },
72
80
  { id: "zed", name: "Zed", kind: "zed",
73
81
  file: join(HOME, ".config", "zed", "settings.json"),
74
82
  found: existsSync(join(HOME, ".config", "zed")) || existsSync("/Applications/Zed.app") },
@@ -272,7 +280,14 @@ if (cmd === "install") {
272
280
  log("\n" + ((await health()) ? "✔ daemon healthy — http://127.0.0.1:3737" : "⚠ daemon not answering — check " + memoryHome + "/daemon.err.log"));
273
281
  }
274
282
  log("🌌 Monitor (the Neural Universe): http://127.0.0.1:3737/monitor");
275
- log("Memory lives in <project>/.peon/ (child brains) + " + memoryHome + " (global brain)\n");
283
+ log("Memory lives in <project>/.peon/ (child brains) + " + memoryHome + " (global brain)");
284
+ // People who just installed are the ones most able to say whether this works. Ask once, here —
285
+ // never nag from the daemon or the hooks, which run constantly and would become noise.
286
+ log("");
287
+ log(" Peon is built by one person. Two things that genuinely help:");
288
+ log(" ★ star it, so others find it https://github.com/VineetV2/peon-mem");
289
+ log(" ✎ tell me what broke or felt confusing — that is a bug report");
290
+ log(" https://github.com/VineetV2/peon-mem/issues\n");
276
291
  rl?.close();
277
292
  } else if (cmd === "uninstall") {
278
293
  if (MAC && existsSync(PLIST)) act("stop + remove daemon service", () => spawnSync("launchctl", ["unload", PLIST], { stdio: "ignore" }));
@@ -288,7 +303,7 @@ if (cmd === "install") {
288
303
  s.hooks[ev] = s.hooks[ev].filter((h) => !JSON.stringify(h).includes("claude-peon-hook.mjs"));
289
304
  act("remove Peon hooks from " + settings, () => backupWrite(settings, JSON.stringify(s, null, 2) + "\n"));
290
305
  } catch {}
291
- log("Remove [mcp_servers.peon] / mcpServers.peon from Codex/Gemini/Cursor configs if you added them.");
306
+ log("Remove [mcp_servers.peon] / mcpServers.peon from Codex/Gemini/Cursor/Cline configs if you added them.");
292
307
  log("Memory data untouched: <project>/.peon/ and " + DEFAULT_HOME);
293
308
  rl?.close();
294
309
  } else if (cmd === "daemon") {
@@ -40,4 +40,4 @@ export declare class EmbeddingStore {
40
40
  /** Serialize a vector as base64 of its float32 bytes — ~4x smaller + faster to parse than JSON float64. */
41
41
  export declare function encodeVector(vector: EmbeddingVector): string;
42
42
  /** Decode a base64 float32 vector back to number[]; null on malformed/misaligned input. */
43
- export declare function decodeVector(b64: string): number[] | null;
43
+ export declare function decodeVector(b64: string): Float32Array | null;
@@ -140,7 +140,10 @@ export function decodeVector(b64) {
140
140
  const buf = Buffer.from(b64, "base64");
141
141
  if (buf.byteLength === 0 || buf.byteLength % 4 !== 0)
142
142
  return null;
143
- return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
143
+ // Return the Float32Array itself rather than Array.from(...): a number[] stores every
144
+ // dimension as a double, doubling memory and copying 28k vectors on every cold load.
145
+ // slice() so the vector owns its bytes instead of pinning Node's shared Buffer pool.
146
+ return new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
144
147
  }
145
148
  catch {
146
149
  return null;
@@ -15,7 +15,7 @@ import type { PeonConfig } from "./config.js";
15
15
  *
16
16
  * - "off" mode: no embeddings; retrieval stays purely lexical.
17
17
  */
18
- export type EmbeddingVector = number[];
18
+ export type EmbeddingVector = number[] | Float32Array;
19
19
  export declare const LOCAL_EMBEDDING_DIM = 256;
20
20
  export declare const LOCAL_EMBEDDING_MODEL = "peon-local-trigram-v1";
21
21
  export interface EmbeddingClient {
@@ -28,7 +28,10 @@ export function l2normalize(vector) {
28
28
  norm = Math.sqrt(norm);
29
29
  if (norm === 0)
30
30
  return vector.slice();
31
- return vector.map((value) => value / norm);
31
+ const out = new Float32Array(vector.length);
32
+ for (let i = 0; i < vector.length; i += 1)
33
+ out[i] = vector[i] / norm;
34
+ return out;
32
35
  }
33
36
  /**
34
37
  * Deterministic local embedding: hashed character trigrams folded into a fixed
@@ -96,7 +99,8 @@ function b64decode(b64) {
96
99
  const buf = Buffer.from(b64, "base64");
97
100
  if (buf.byteLength === 0 || buf.byteLength % 4 !== 0)
98
101
  return null;
99
- return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
102
+ // slice() to own the bytes: a view onto buf.buffer would pin Node's shared Buffer pool.
103
+ return new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
100
104
  }
101
105
  catch {
102
106
  return null;
@@ -1,5 +1,5 @@
1
1
  import { type PeonConfig } from "./config.js";
2
- import { type EmbeddingClient } from "./embeddings.js";
2
+ import { type EmbeddingClient, type EmbeddingVector } from "./embeddings.js";
3
3
  import type { MemoryQualityReport } from "./quality.js";
4
4
  import { type MemoryPatch } from "./memory-mutations.js";
5
5
  import { type BrainAction, type Summarizer } from "./brain.js";
@@ -134,7 +134,7 @@ export declare class PeonMemoryStore {
134
134
  */
135
135
  rankRecordsReadonly(query: string | undefined, options?: {
136
136
  limit?: number;
137
- queryVector?: number[];
137
+ queryVector?: EmbeddingVector;
138
138
  }): Promise<RankedMemoryRecord[]>;
139
139
  private buildSemanticInput;
140
140
  writeQualityReport(report: MemoryQualityReport): Promise<void>;
package/dist/monitor.js CHANGED
@@ -27,6 +27,10 @@ const CLIENT_SCRIPT = String.raw `
27
27
  var ui = { project:"", filter:"all", search:null, focus:null, editing:null };
28
28
  var latest = null, overview = null, network = null, dash = null, busy = false;
29
29
  var ovLoadedAt = 0, netLoadedAt = 0, dashLoadedAt = 0, HEAVY_TTL = 12000;
30
+ // Load state for the heavy endpoints. Without this, a page renders its EMPTY state while data is
31
+ // still in flight — /network takes seconds on a large brain, so "No projects yet" was shown as
32
+ // fact for the whole wait, and stayed forever if the request failed (the catch was silent).
33
+ var loadState = { net:"idle", ov:"idle", dash:"idle" }; // idle | loading | ok | error
30
34
  var seenActivity = {}, activityFirstLoad = true, recentActivity = []; // for live popups
31
35
  function freshNow(){ try{ return Date.now(); }catch(e){ return 0; } }
32
36
  var KIND_ICON = { resolve_conflict:"⚖", merge_duplicate:"⛓", compress_cluster:"🗜", reinforce:"✦" };
@@ -111,7 +115,15 @@ const CLIENT_SCRIPT = String.raw `
111
115
  async function loadDashboard(force){
112
116
  if(!force && (freshNow()-dashLoadedAt)<HEAVY_TTL) return;
113
117
  dashLoadedAt=freshNow();
114
- try{ dash=await fetch("/global/dashboard",{cache:"no-store"}).then(function(r){return r.json();}); renderBrainHome(); }catch(e){}
118
+ loadState.dash="loading";
119
+ try{
120
+ dash=await fetch("/global/dashboard",{cache:"no-store"}).then(function(r){
121
+ if(!r.ok) throw new Error("HTTP "+r.status);
122
+ return r.json();
123
+ });
124
+ loadState.dash="ok";
125
+ }catch(e){ loadState.dash="error"; dashLoadedAt=0; } // failed load retries on next visit
126
+ renderBrainHome();
115
127
  }
116
128
  // ===== NEURAL UNIVERSE — every real belief is a star; projects are galaxies =====
117
129
  var UNI = { nodes:[], clusters:[], grid:{}, cell:46, cam:{x:0,y:0,z:1}, tx:{x:0,y:0,z:1},
@@ -503,7 +515,13 @@ const CLIENT_SCRIPT = String.raw `
503
515
  '<div class="pcname">'+esc(p.projectName)+dis+'</div>'+
504
516
  '<div class="pcstats"><span class="pcg">'+p.active+'</span> active beliefs'+(p.pinned?' · '+p.pinned+' pinned':'')+'</div>'+
505
517
  '<div class="pcgo">open insights →</div></button>';
506
- }).join(""):'<div class="empty">No projects yet</div>';
518
+ }).join(""):(
519
+ // Never claim "no projects" while we are still asking, or when the ask failed — on a large
520
+ // brain /network takes seconds, and a silent failure used to look identical to an empty box.
521
+ loadState.net==="loading" ? '<div class="empty">Reading project brains…</div>' :
522
+ loadState.net==="error" ? '<div class="empty">Could not reach the daemon. <button class="btn sm" id="proj-retry">Retry</button></div>' :
523
+ '<div class="empty">No projects yet</div>');
524
+ var pr=EL("proj-retry"); if(pr) pr.addEventListener("click",function(){ loadNetwork(true); });
507
525
  Array.prototype.forEach.call(document.querySelectorAll(".pcard"),function(b){ b.addEventListener("click",function(){ ui.project=b.getAttribute("data-p"); ui.search=null; ui.focus=null; overview=null; syncSwitcher(); location.hash="#/overview"; }); });
508
526
  }
509
527
 
@@ -512,7 +530,15 @@ const CLIENT_SCRIPT = String.raw `
512
530
  if(!ui.project) return;
513
531
  if(!force && (freshNow()-ovLoadedAt)<HEAVY_TTL) return; // throttle the heavy endpoint
514
532
  ovLoadedAt=freshNow();
515
- try{ overview=await fetch("/overview?projectPath="+encodeURIComponent(ui.project),{cache:"no-store"}).then(function(r){return r.json();}); renderOverviewPage(); }catch(e){}
533
+ loadState.ov="loading";
534
+ try{
535
+ overview=await fetch("/overview?projectPath="+encodeURIComponent(ui.project),{cache:"no-store"}).then(function(r){
536
+ if(!r.ok) throw new Error("HTTP "+r.status);
537
+ return r.json();
538
+ });
539
+ loadState.ov="ok";
540
+ }catch(e){ loadState.ov="error"; ovLoadedAt=0; } // failed load retries on next visit
541
+ renderOverviewPage();
516
542
  }
517
543
  function renderOverviewPage(){
518
544
  var sel=selected();
@@ -634,7 +660,18 @@ const CLIENT_SCRIPT = String.raw `
634
660
  async function loadNetwork(force){
635
661
  if(!force && (freshNow()-netLoadedAt)<HEAVY_TTL) return; // throttle the heavy endpoint
636
662
  netLoadedAt=freshNow();
637
- try{ network=await fetch("/network",{cache:"no-store"}).then(function(r){return r.json();}); renderProjectsGrid(); }catch(e){}
663
+ loadState.net="loading"; renderProjectsGrid();
664
+ try{
665
+ network=await fetch("/network",{cache:"no-store"}).then(function(r){
666
+ if(!r.ok) throw new Error("HTTP "+r.status);
667
+ return r.json();
668
+ });
669
+ loadState.net="ok";
670
+ }catch(e){
671
+ loadState.net="error";
672
+ netLoadedAt=0; // a failed load must not be throttled — let the next visit retry
673
+ }
674
+ renderProjectsGrid();
638
675
  }
639
676
 
640
677
  // ===== OPS (tokens + activity) =====
@@ -746,6 +783,10 @@ const DOCUMENT = String.raw `<!doctype html>
746
783
  box-shadow:0 0 18px -4px rgba(89,227,255,.7), inset 0 0 12px rgba(89,227,255,.12); text-shadow:0 0 10px rgba(89,227,255,.8);}
747
784
  .switcher{background:rgba(5,18,32,.9); border:1px solid var(--line2); color:var(--cyan-ink); clip-path:var(--cham);
748
785
  padding:7px 12px; max-width:230px; font-size:12px; font-family:var(--mono);}
786
+ /* Quiet star link — same weight as the status text, never competes with the field. */
787
+ .ghlink{flex:none; font-family:var(--mono); font-size:10px; letter-spacing:.12em; text-transform:uppercase;
788
+ color:var(--muted); text-decoration:none; border:1px solid rgba(89,227,255,.25); padding:4px 9px; clip-path:var(--cham);}
789
+ .ghlink:hover{color:var(--cyan); border-color:var(--cyan); box-shadow:0 0 16px -6px rgba(89,227,255,.8);}
749
790
  .live{display:flex; align-items:center; gap:8px; font-size:10.5px; color:var(--muted); flex:none;
750
791
  font-family:var(--mono); text-transform:uppercase; letter-spacing:.14em;}
751
792
  .dot{width:8px; height:8px; border-radius:50%; background:var(--cyan); box-shadow:0 0 12px var(--cyan); animation:beat 2s infinite;}
@@ -1026,6 +1067,8 @@ const DOCUMENT = String.raw `<!doctype html>
1026
1067
  <a id="nav-ops" href="#/ops">Systems</a>
1027
1068
  </nav>
1028
1069
  <select class="switcher" id="switcher" aria-label="Active project"></select>
1070
+ <a class="ghlink" href="https://github.com/VineetV2/peon-mem" target="_blank" rel="noopener noreferrer"
1071
+ title="Peon is open source — star it so others find it">★ Star</a>
1029
1072
  <div class="live"><span class="dot" id="dot"></span><span id="status">linking…</span></div>
1030
1073
  </header>
1031
1074
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peon-mem",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {