peon-mem 1.0.3 β†’ 1.0.5

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
@@ -1,6 +1,6 @@
1
1
  # 🧠 Peon β€” a memory brain for your AI coding agents
2
2
 
3
- [![npm](https://img.shields.io/npm/v/peon-mem)](https://www.npmjs.com/package/peon-mem) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](test/)
3
+ [![npm](https://img.shields.io/npm/v/peon-mem)](https://www.npmjs.com/package/peon-mem) [![downloads](https://img.shields.io/npm/dm/peon-mem)](https://www.npmjs.com/package/peon-mem) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](test/) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-listed-6b4fbb)](https://registry.modelcontextprotocol.io) [![Star on GitHub](https://img.shields.io/github/stars/VineetV2/peon-mem?style=social)](https://github.com/VineetV2/peon-mem)
4
4
 
5
5
  **Local-first, hierarchical, self-improving memory for Claude Code, Codex, and any MCP client.**
6
6
 
@@ -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
@@ -433,3 +434,12 @@ or send a PR directly.
433
434
  Rules of the house: every retrieval/quality change ships with a test and an eval-ledger run
434
435
  (`npm run eval`); negative results get documented, not deleted; nothing may hard-delete user
435
436
  memory. `npm test` must stay green.
437
+
438
+ ---
439
+
440
+ <div align="center">
441
+
442
+ **If Peon is useful to you, [β˜… star it on GitHub](https://github.com/VineetV2/peon-mem)** β€” it is
443
+ how other people building with coding agents find it.
444
+
445
+ </div>
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") {
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:"✦" };
@@ -58,6 +62,8 @@ const CLIENT_SCRIPT = String.raw `
58
62
 
59
63
  function esc(v){ return String(v==null?"":v).replace(/[&<>"']/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c];}); }
60
64
  function clip(v,n){ var s=String(v==null?"":v).trim(); return s.length>n?s.slice(0,n)+"…":s; }
65
+ // "summary" β†’ "summaries", "fact" β†’ "facts". Belief type names are user-visible in search results.
66
+ function plural(w,n){ if(n===1) return w; return /(s|x|z|ch|sh)$/.test(w)?w+"es":/[^aeiou]y$/.test(w)?w.slice(0,-1)+"ies":w+"s"; }
61
67
  function fmt(n){ return (Number(n)||0).toLocaleString("en-US"); }
62
68
  function tm(iso){ try{ return new Date(iso).toLocaleTimeString([], {hour:"2-digit",minute:"2-digit",second:"2-digit"}); }catch(e){ return ""; } }
63
69
  function name(p){ return String(p||"").split("/").filter(Boolean).pop()||"project"; }
@@ -109,7 +115,15 @@ const CLIENT_SCRIPT = String.raw `
109
115
  async function loadDashboard(force){
110
116
  if(!force && (freshNow()-dashLoadedAt)<HEAVY_TTL) return;
111
117
  dashLoadedAt=freshNow();
112
- 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();
113
127
  }
114
128
  // ===== NEURAL UNIVERSE β€” every real belief is a star; projects are galaxies =====
115
129
  var UNI = { nodes:[], clusters:[], grid:{}, cell:46, cam:{x:0,y:0,z:1}, tx:{x:0,y:0,z:1},
@@ -316,6 +330,96 @@ const CLIENT_SCRIPT = String.raw `
316
330
  UNI.hits=set;
317
331
  if(hitsEl) hitsEl.textContent=m?(m+" MATCH"+(m>1?"ES":"")):"NO MATCH";
318
332
  if(m){ UNI.tx.x=sx/m; UNI.tx.y=sy/m; UNI.tx.z=Math.max(UNI.cam.z, m<20?1.1:0.55); }
333
+ uniResults(q, toks);
334
+ }
335
+
336
+ /* Readable answer for a query: an EXTRACTIVE summary (no model call β€” works with AI off) plus
337
+ the ranked hits. Flaring stars tell you a match exists; this tells you what it says. */
338
+ function uniResults(q, toks){
339
+ var el=EL("uni-results"); if(!el) return;
340
+ if(!q||!UNI.hits||!UNI.hits.size){ el.hidden=true; el.innerHTML=""; return; }
341
+ // Dedupe by record id: the same belief can be drawn as more than one star (a global belief
342
+ // also appears under a project galaxy), and counting/listing it twice would misreport memory.
343
+ var seen={}, hits=[];
344
+ UNI.nodes.forEach(function(n){
345
+ if(!UNI.hits.has(n.id)||seen[n.id]) return;
346
+ seen[n.id]=1; hits.push(n);
347
+ });
348
+ // Rank: query-term density, then belief strength/importance, then recency.
349
+ hits.forEach(function(n){
350
+ var hay=(n.rec.content+" "+(n.rec.entities||[]).join(" ")).toLowerCase(), d=0;
351
+ toks.forEach(function(t){ var i=0; while((i=hay.indexOf(t,i))>=0){ d++; i+=t.length; } });
352
+ var imp=(n.rec.score&&n.rec.score.importance)||0;
353
+ n._r=d*2 + imp*3 + (n.rec.strength||0) + (n.rec.recallCount||0)*0.2;
354
+ // Current truth outranks history: a superseded/archived phrasing must never sit above the
355
+ // active belief that replaced it (same reasoning as stale-shadow demotion in retrieval).
356
+ if(n.rec.status&&n.rec.status!=="active") n._r-=6;
357
+ if(n.rec.pinned) n._r+=3;
358
+ // Prefer beliefs that ANSWER the question over pointers to where an answer lives: a bare
359
+ // file path matching on its filename is not a summary. Prose types lead; paths sink.
360
+ var c=n.rec.content||"";
361
+ var pathish=/^[~/.]|^[A-Za-z]:\\/.test(c.trim()) && c.trim().split(/\s+/).length<=3;
362
+ if(pathish) n._r-=4;
363
+ if(n.rec.type==="decision"||n.rec.type==="fact"||n.rec.type==="preference") n._r+=1.5;
364
+ });
365
+ hits.sort(function(a,b){ return b._r-a._r; });
366
+
367
+ // Facets: what KIND of memory answered, and where it lives.
368
+ var byType={}, byProj={}, ents={}, oldest=null, newest=null;
369
+ hits.forEach(function(n){
370
+ var r=n.rec;
371
+ byType[r.type]=(byType[r.type]||0)+1;
372
+ byProj[n.cl.name]=(byProj[n.cl.name]||0)+1;
373
+ (r.entities||[]).forEach(function(e){ ents[e]=(ents[e]||0)+1; });
374
+ var t=r.updatedAt||r.createdAt;
375
+ if(t){ if(!oldest||t<oldest) oldest=t; if(!newest||t>newest) newest=t; }
376
+ });
377
+ var topEnts=Object.keys(ents).sort(function(a,b){ return ents[b]-ents[a]; }).slice(0,6);
378
+ var typeStr=Object.keys(byType).sort(function(a,b){ return byType[b]-byType[a]; })
379
+ .map(function(t){ return byType[t]+" "+plural(t.replace(/_/g," "),byType[t]); }).join(" Β· ");
380
+ var projStr=Object.keys(byProj).sort(function(a,b){ return byProj[b]-byProj[a]; }).slice(0,3).join(", ");
381
+
382
+ // The summary line: the single strongest belief, verbatim, plus the shape of the rest.
383
+ // Beliefs recorded against a file often read "<absolute path>: <the actual point>" β€” the path
384
+ // is provenance (kept in the hit list), so lead with the point instead of the filename.
385
+ var lead=String(hits[0].rec.content||"").replace(/^\s*[~/][^\s:]{12,}:\s*/,"");
386
+ var summary='<b>'+esc(clip(lead,260))+'</b>';
387
+ if(hits.length>1) summary+='<br><span style="color:var(--muted)">+ '+(hits.length-1)+' more across '+esc(projStr)+
388
+ (newest?', last updated '+esc(ago(newest)):'')+'.</span>';
389
+
390
+ var facets='<span class="ur-facet">'+esc(typeStr)+'</span>'+
391
+ topEnts.map(function(e){ return '<span class="ur-facet">'+esc(e)+'</span>'; }).join("");
392
+
393
+ var SHOW=12;
394
+ var list=hits.slice(0,SHOW).map(function(n,i){
395
+ var r=n.rec, meta=[];
396
+ if(r.status&&r.status!=="active") meta.push(r.status);
397
+ meta.push(n.cl.name);
398
+ if(r.recallCount) meta.push("recalled "+r.recallCount+"Γ—");
399
+ if(r.updatedAt) meta.push(ago(r.updatedAt));
400
+ return '<button class="ur-hit" data-i="'+i+'">'+
401
+ '<div class="h-t" style="color:'+n.c+'">'+esc(r.type)+'</div>'+
402
+ esc(clip(r.content,180))+
403
+ '<div class="h-m">'+esc(meta.join(" Β· "))+'</div></button>';
404
+ }).join("");
405
+
406
+ el.hidden=false;
407
+ el.innerHTML='<button class="ui-x" id="ur-x">βœ•</button>'+
408
+ '<div class="ur-head">'+hits.length+' belief'+(hits.length>1?"s":"")+' answer "'+esc(clip(q,40))+'"</div>'+
409
+ '<div class="ur-sum">'+summary+'</div>'+
410
+ '<div class="ur-facets">'+facets+'</div>'+
411
+ '<div class="ur-list">'+list+'</div>'+
412
+ (hits.length>SHOW?'<div class="ur-more">showing top '+SHOW+' of '+hits.length+'</div>':"");
413
+
414
+ var x=EL("ur-x"); if(x) x.addEventListener("click",function(){ el.hidden=true; });
415
+ Array.prototype.forEach.call(el.querySelectorAll(".ur-hit"),function(b){
416
+ b.addEventListener("click",function(){
417
+ var n=hits[Number(b.getAttribute("data-i"))];
418
+ if(!n) return;
419
+ uniInspect(n); // full detail + provenance
420
+ UNI.tx.x=n.x; UNI.tx.y=n.y; UNI.tx.z=Math.max(1.6,UNI.cam.z); // fly to the star
421
+ });
422
+ });
319
423
  }
320
424
  function uniInspect(n){
321
425
  var el=EL("uni-inspect"); if(!el) return;
@@ -329,7 +433,19 @@ const CLIENT_SCRIPT = String.raw `
329
433
  '<span class="g">IMP <b>'+pct(r.score&&r.score.importance)+'</b><i class="bar"><i style="width:'+pct(r.score&&r.score.importance)+'%"></i></i></span>'+
330
434
  '<span class="g">CONF <b>'+pct(r.score&&r.score.confidence)+'</b><i class="bar"><i class="b2" style="width:'+pct(r.score&&r.score.confidence)+'%"></i></i></span></div>'+
331
435
  ((r.entities&&r.entities.length)?'<div class="ui-ents">'+r.entities.slice(0,8).map(function(e){return '<span class="ent mono">'+esc(e)+'</span>';}).join("")+'</div>':"")+
332
- '<div class="ui-proj mono">'+esc(n.cl.name)+(r.updatedAt?' Β· '+esc(ago(r.updatedAt)):'')+'</div>'+
436
+ // Provenance: where this belief came from and how it has been used. Answers "why does my
437
+ // agent believe this, and is it still current?" without opening the JSONL by hand.
438
+ '<div class="ui-proj mono">'+(function(){
439
+ var p=[];
440
+ if(r.createdAt) p.push("learned "+ago(r.createdAt));
441
+ if(r.updatedAt&&r.updatedAt!==r.createdAt) p.push("updated "+ago(r.updatedAt));
442
+ if(r.recallCount) p.push("recalled "+r.recallCount+"Γ—"+(r.lastRecalledAt?" (last "+ago(r.lastRecalledAt)+")":""));
443
+ if(r.pinned) p.push("pinned");
444
+ if(r.summarizedBy) p.push("folded into a summary");
445
+ if(r.source&&r.source.reason) p.push("via "+clip(r.source.reason,48));
446
+ if(r.provenance&&r.provenance.ref) p.push("source: "+clip(r.provenance.ref,44));
447
+ return esc(n.cl.name)+(p.length?' Β· '+esc(p.join(" Β· ")):'');
448
+ })()+'</div>'+
333
449
  (n.cl.path?'<button class="btn sm" id="ui-open">OPEN IN MEMORY BANKS β†’</button>':"");
334
450
  var x=EL("ui-x"); if(x) x.addEventListener("click",function(){ uniInspect(null); });
335
451
  var op=EL("ui-open"); if(op) op.addEventListener("click",function(){
@@ -399,7 +515,13 @@ const CLIENT_SCRIPT = String.raw `
399
515
  '<div class="pcname">'+esc(p.projectName)+dis+'</div>'+
400
516
  '<div class="pcstats"><span class="pcg">'+p.active+'</span> active beliefs'+(p.pinned?' Β· '+p.pinned+' pinned':'')+'</div>'+
401
517
  '<div class="pcgo">open insights β†’</div></button>';
402
- }).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); });
403
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"; }); });
404
526
  }
405
527
 
@@ -408,7 +530,15 @@ const CLIENT_SCRIPT = String.raw `
408
530
  if(!ui.project) return;
409
531
  if(!force && (freshNow()-ovLoadedAt)<HEAVY_TTL) return; // throttle the heavy endpoint
410
532
  ovLoadedAt=freshNow();
411
- 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();
412
542
  }
413
543
  function renderOverviewPage(){
414
544
  var sel=selected();
@@ -530,7 +660,18 @@ const CLIENT_SCRIPT = String.raw `
530
660
  async function loadNetwork(force){
531
661
  if(!force && (freshNow()-netLoadedAt)<HEAVY_TTL) return; // throttle the heavy endpoint
532
662
  netLoadedAt=freshNow();
533
- 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();
534
675
  }
535
676
 
536
677
  // ===== OPS (tokens + activity) =====
@@ -642,6 +783,10 @@ const DOCUMENT = String.raw `<!doctype html>
642
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);}
643
784
  .switcher{background:rgba(5,18,32,.9); border:1px solid var(--line2); color:var(--cyan-ink); clip-path:var(--cham);
644
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);}
645
790
  .live{display:flex; align-items:center; gap:8px; font-size:10.5px; color:var(--muted); flex:none;
646
791
  font-family:var(--mono); text-transform:uppercase; letter-spacing:.14em;}
647
792
  .dot{width:8px; height:8px; border-radius:50%; background:var(--cyan); box-shadow:0 0 12px var(--cyan); animation:beat 2s infinite;}
@@ -873,6 +1018,26 @@ const DOCUMENT = String.raw `<!doctype html>
873
1018
  .ui-meta{display:flex; gap:14px; margin-bottom:9px;}
874
1019
  .ui-ents{display:flex; flex-wrap:wrap; gap:5px; margin-bottom:10px;}
875
1020
  .ui-proj{color:var(--faint); font-size:10px; margin-bottom:11px;}
1021
+ /* Search RESULTS panel (right side, mirrors .uni-inspect on the left): the readable answer to
1022
+ a query β€” an extractive summary of what memory says, then the ranked hits you can click. */
1023
+ .uni-results{position:absolute; top:60px; right:14px; z-index:8; width:360px; max-height:calc(100% - 130px); overflow:auto;
1024
+ background:linear-gradient(165deg, rgba(8,26,42,.96), rgba(4,14,26,.96)); border:1px solid var(--cyan); clip-path:var(--cham);
1025
+ padding:16px; box-shadow:0 0 34px -8px rgba(89,227,255,.5);}
1026
+ .ur-head{font-family:var(--mono); font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:var(--cyan);
1027
+ margin-bottom:10px; text-shadow:0 0 10px rgba(89,227,255,.6);}
1028
+ .ur-sum{font-size:12px; line-height:1.6; color:var(--ink); border-left:2px solid var(--cyan);
1029
+ padding:2px 0 2px 10px; margin-bottom:12px;}
1030
+ .ur-sum b{color:var(--cyan-ink);}
1031
+ .ur-facets{display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px;}
1032
+ .ur-facet{font-family:var(--mono); font-size:9.5px; letter-spacing:.08em; text-transform:uppercase;
1033
+ border:1px solid rgba(89,227,255,.35); color:var(--muted); padding:2px 6px; clip-path:var(--cham);}
1034
+ .ur-list{display:flex; flex-direction:column; gap:7px;}
1035
+ .ur-hit{text-align:left; width:100%; background:rgba(6,20,34,.7); border:1px solid rgba(89,227,255,.18);
1036
+ padding:8px 10px; clip-path:var(--cham); cursor:pointer; color:var(--ink); font-size:11.5px; line-height:1.5;}
1037
+ .ur-hit:hover{border-color:var(--cyan); background:rgba(10,30,48,.9);}
1038
+ .ur-hit .h-t{font-family:var(--mono); font-size:9px; letter-spacing:.12em; text-transform:uppercase; color:var(--cyan); margin-bottom:3px;}
1039
+ .ur-hit .h-m{font-family:var(--mono); font-size:9px; color:var(--faint); margin-top:4px;}
1040
+ .ur-more{font-family:var(--mono); font-size:9.5px; color:var(--faint); margin-top:9px; text-align:center;}
876
1041
  .uni-ticker{position:absolute; left:0; right:0; bottom:0; z-index:6; padding:8px 16px 10px;
877
1042
  background:linear-gradient(180deg, transparent, rgba(2,8,16,.9) 40%); font-family:var(--mono); font-size:10px; color:var(--muted);}
878
1043
  .utk{padding:2px 0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
@@ -902,6 +1067,8 @@ const DOCUMENT = String.raw `<!doctype html>
902
1067
  <a id="nav-ops" href="#/ops">Systems</a>
903
1068
  </nav>
904
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>
905
1072
  <div class="live"><span class="dot" id="dot"></span><span id="status">linking…</span></div>
906
1073
  </header>
907
1074
 
@@ -918,6 +1085,7 @@ const DOCUMENT = String.raw `<!doctype html>
918
1085
  <div class="uni-legend" id="uni-legend"></div>
919
1086
  <div class="uni-tip mono" id="uni-tip" hidden></div>
920
1087
  <div class="uni-inspect" id="uni-inspect" hidden></div>
1088
+ <div class="uni-results" id="uni-results" hidden></div>
921
1089
  <div class="uni-ticker" id="uni-ticker"></div>
922
1090
  </div>
923
1091
  <div id="bh-body"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peon-mem",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {