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 +15 -5
- package/bin/peon-mem.mjs +19 -4
- package/dist/monitor.js +173 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# π§ Peon β a memory brain for your AI coding agents
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/peon-mem) [](LICENSE) [](test/)
|
|
3
|
+
[](https://www.npmjs.com/package/peon-mem) [](https://www.npmjs.com/package/peon-mem) [](LICENSE) [](test/) [](https://registry.modelcontextprotocol.io) [](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.
|
|
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,
|
|
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:
|
|
71
|
-
found: which("code") || existsSync(
|
|
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)
|
|
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 {"&":"&","<":"<",">":">",'"':""","'":"'"}[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
|
-
|
|
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
|
-
|
|
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(""):
|
|
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
|
-
|
|
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
|
-
|
|
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>
|