peon-mem 1.0.0

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +301 -0
  3. package/bin/peon-mem.mjs +273 -0
  4. package/dist/brain.d.ts +72 -0
  5. package/dist/brain.js +224 -0
  6. package/dist/compression.d.ts +9 -0
  7. package/dist/compression.js +37 -0
  8. package/dist/config.d.ts +22 -0
  9. package/dist/config.js +99 -0
  10. package/dist/daemon-cli.d.ts +2 -0
  11. package/dist/daemon-cli.js +54 -0
  12. package/dist/daemon.d.ts +23 -0
  13. package/dist/daemon.js +1078 -0
  14. package/dist/embedding-store.d.ts +43 -0
  15. package/dist/embedding-store.js +169 -0
  16. package/dist/embeddings.d.ts +93 -0
  17. package/dist/embeddings.js +345 -0
  18. package/dist/entities.d.ts +61 -0
  19. package/dist/entities.js +191 -0
  20. package/dist/entity-extraction.d.ts +33 -0
  21. package/dist/entity-extraction.js +75 -0
  22. package/dist/eval-metrics.d.ts +27 -0
  23. package/dist/eval-metrics.js +50 -0
  24. package/dist/evaluation.d.ts +58 -0
  25. package/dist/evaluation.js +244 -0
  26. package/dist/global-extraction.d.ts +15 -0
  27. package/dist/global-extraction.js +61 -0
  28. package/dist/global-memory.d.ts +43 -0
  29. package/dist/global-memory.js +306 -0
  30. package/dist/global-promotion.d.ts +25 -0
  31. package/dist/global-promotion.js +29 -0
  32. package/dist/hyde.d.ts +31 -0
  33. package/dist/hyde.js +46 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +246 -0
  36. package/dist/injection.d.ts +38 -0
  37. package/dist/injection.js +133 -0
  38. package/dist/logger.d.ts +17 -0
  39. package/dist/logger.js +63 -0
  40. package/dist/memory-mutations.d.ts +24 -0
  41. package/dist/memory-mutations.js +57 -0
  42. package/dist/memory-store.d.ts +194 -0
  43. package/dist/memory-store.js +1205 -0
  44. package/dist/monitor.d.ts +13 -0
  45. package/dist/monitor.js +977 -0
  46. package/dist/overview.d.ts +73 -0
  47. package/dist/overview.js +104 -0
  48. package/dist/processor.d.ts +90 -0
  49. package/dist/processor.js +450 -0
  50. package/dist/quality.d.ts +86 -0
  51. package/dist/quality.js +338 -0
  52. package/dist/recuration.d.ts +13 -0
  53. package/dist/recuration.js +65 -0
  54. package/dist/reranker.d.ts +34 -0
  55. package/dist/reranker.js +89 -0
  56. package/dist/retrieval.d.ts +106 -0
  57. package/dist/retrieval.js +392 -0
  58. package/dist/session-index.d.ts +34 -0
  59. package/dist/session-index.js +87 -0
  60. package/dist/temporal.d.ts +20 -0
  61. package/dist/temporal.js +62 -0
  62. package/dist/token-ab-monitor.d.ts +1 -0
  63. package/dist/token-ab-monitor.js +7 -0
  64. package/dist/tools.d.ts +232 -0
  65. package/dist/tools.js +546 -0
  66. package/dist/types.d.ts +169 -0
  67. package/dist/types.js +1 -0
  68. package/docs/assets/neural-universe.png +0 -0
  69. package/package.json +57 -0
  70. package/scripts/claude-peon-hook.mjs +522 -0
  71. package/scripts/codex-peon-hook.mjs +4 -0
  72. package/scripts/eval-retrieval-labeled.mjs +135 -0
  73. package/scripts/eval-retrieval.mjs +96 -0
  74. package/scripts/evaluate-peon.mjs +47 -0
  75. package/scripts/install-peon-stl.mjs +82 -0
  76. package/scripts/install-peon.mjs +318 -0
  77. package/scripts/lib/eval-ledger.mjs +104 -0
  78. package/scripts/lib/stl-classify.mjs +44 -0
  79. package/scripts/longmemeval-eval.mjs +144 -0
  80. package/scripts/peon-report.mjs +155 -0
  81. package/scripts/peon-stl.mjs +506 -0
  82. package/scripts/token-ab-monitor.html +235 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vineet Vora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # ๐Ÿง  Peon โ€” a memory brain for your AI coding agents
2
+
3
+ **Local-first, hierarchical, self-improving memory for Claude Code, Codex, and any MCP client.**
4
+
5
+ Your AI forgets everything between sessions. Peon doesn't. It records your sessions, consolidates
6
+ them into typed *beliefs* with an LLM, and injects the relevant ones back into every prompt โ€”
7
+ automatically, from a daemon that never leaves your machine.
8
+
9
+ ```
10
+ PEON GLOBAL BRAIN โ† user-level facts & preferences, inherited everywhere
11
+ / | \
12
+ project A project B project C โ† rooted child brains (.peon/ in each project)
13
+ ```
14
+
15
+ ## Why Peon
16
+
17
+ - **Hierarchical brains** โ€” one global parent brain (who you are, your rules, your tools) plus an
18
+ isolated child brain per project. Every injection = project memory + inherited global memory.
19
+ - **Two memory layers, honestly measured** โ€” consolidated *beliefs* (decisions, preferences, facts,
20
+ artifactsโ€ฆ) for gist, plus an *episodic* verbatim layer that recovers exact details lossy
21
+ summaries drop (measured on LongMemEval: raw-episodic recall 61% vs belief-only 17%).
22
+ - **Auto capture + auto injection** โ€” Claude Code hooks record messages/events and inject a
23
+ query-ranked memory block (with an `โš  MOST RELEVANT` headline) into every prompt. Zero effort.
24
+ - **Cost-gated consolidation** โ€” an LLM distills sessions into beliefs only when enough new
25
+ memory accumulates. Supersede / merge / conflict-detect; nothing is destructively deleted.
26
+ - **Hybrid retrieval** โ€” lexical + semantic RRF fusion, MMR diversity, reinforcement, recency;
27
+ query-embedding cache (persisted) so repeat prompts cost nothing.
28
+ - **The Neural Universe** โ€” a live monitor at `localhost:3737/monitor` that renders every belief
29
+ as a star: projects are galaxies, search makes matches flare, autonomous curation pulses.
30
+ - **A daily self-improvement loop (STL)** โ€” Peon audits itself every day: what it recorded,
31
+ injected, what failed, what consolidation did โ€” and files a report with a verdict.
32
+ - **Eval-gated development** โ€” a committed results ledger (git SHA + qrels + brain fingerprint per
33
+ row) so retrieval changes are *proven*, not asserted. Negative results stay documented.
34
+ - **Local-first & locked down** โ€” plain JSONL you can read, loopback-only daemon with
35
+ DNS-rebinding protection, secret redaction at the injection boundary, path-traversal guards.
36
+
37
+
38
+
39
+
40
+ ![The Neural Universe โ€” every belief is a star, projects are galaxies](docs/assets/neural-universe.png)
41
+ *The live monitor: 18k real beliefs rendered as stars. Type to make matching beliefs flare; click one to inspect it.*
42
+
43
+ ## Why "Peon"?
44
+
45
+ The name comes from Indian offices. Every office had a **peon** โ€” the person who walked desk to
46
+ desk all day: collect a file from this table, note who needs what, carry it to the next table,
47
+ remember where everything is. Not the boss, not the star โ€” but the one person the whole office
48
+ quietly ran on. Nothing moved without him, and he never forgot where anything was.
49
+
50
+ That's exactly this framework, with AI. Peon walks between your sessions and your projects โ€”
51
+ collects what happened at one desk (a session), files it in the right cabinet (a project brain),
52
+ carries the relevant papers to the next desk before you ask (injection), and keeps the master
53
+ ledger upstairs (the global brain). Quiet clerk. Perfect memory. The office runs on him.
54
+
55
+ ## How Peon differs from existing memory tools
56
+
57
+ | | **Peon** | mem0 | Letta/MemGPT | Zep/Graphiti | flat memory files (MEMORY.md) |
58
+ |---|---|---|---|---|---|
59
+ | Runs | **100% local daemon** | cloud or self-host | server | cloud/server | local |
60
+ | Storage | human-readable JSONL you can `cat` | vector DB | DB | graph DB | markdown |
61
+ | Memory model | **beliefs + verbatim episodic layer** | extracted facts | self-edited blocks | temporal knowledge graph | prose |
62
+ | Hierarchy | **global parent brain โ†’ per-project child brains, inherited on every prompt** | user/agent/session scopes | per-agent | per-user | per-project file |
63
+ | Capture | **automatic via hooks** (zero effort) | SDK calls you write | agent-managed | SDK calls | agent must remember to write |
64
+ | Conflict handling | supersede/merge, **recoverable โ€” never hard-deletes** | LLM may DELETE | self-edit | invalidation | overwrite |
65
+ | Exact recall | episodic layer regression-tested (61% vs 17% belief-only, LongMemEval) | gist only | gist only | graph facts | whatever was written |
66
+ | Observability | **live Neural Universe monitor + daily self-audit (STL) + serve telemetry** | dashboard | โ€” | โ€” | โ€” |
67
+ | Verification | **committed eval ledger; negative results kept** | vendor benchmarks | โ€” | vendor benchmarks | โ€” |
68
+
69
+ Positioning in one line: mem0/Zep are memory **platforms for products you build**; Peon is a
70
+ memory **brain for the coding agents you already use** โ€” plug into Claude Code/Codex in five
71
+ minutes, watch it think, audit every number.
72
+
73
+ ## Quickstart
74
+
75
+ Requirements: Node 20+, macOS or Linux. An [OpenRouter](https://openrouter.ai) API key is
76
+ recommended (consolidation + semantic embeddings); without one Peon still works lexical-only.
77
+
78
+ One line:
79
+
80
+ ```bash
81
+ curl -fsSL https://raw.githubusercontent.com/VineetV2/peon-mem/main/install.sh | bash
82
+ ```
83
+
84
+ (or, once you have the CLI: `npm install -g peon-mem && peon-mem install`)
85
+
86
+ The guided setup asks four things:
87
+
88
+ 1. **Where your global brain lives** (default: `~/Library/Application Support/Peon`)
89
+ 2. **Which LLM** โ€” OpenRouter (one key, any model) ยท OpenAI ยท Anthropic ยท **Ollama (100% local & free)** ยท or skip
90
+ 3. Installs the **daemon** as an auto-start service
91
+ 4. **Detects your AI apps** and wires the MCP server (+ hooks for Claude Code) into the ones you
92
+ pick โ€” auto-configured: Claude Code, Claude Desktop, Codex, Gemini CLI, Cursor, Windsurf,
93
+ VS Code (Copilot MCP), Zed, LM Studio; detected with in-app instructions: ChatGPT Desktop,
94
+ Perplexity Desktop. Every touched config gets a `.peon-backup`.
95
+
96
+ That builds the package, starts the daemon as a service, wires your Claude Code hooks + MCP
97
+ server (with a backup of your settings), and writes a config template. Then add your key to
98
+ `~/Library/Application Support/Peon/.env` and open the monitor. `peon-mem install --dry-run`
99
+ shows every action first; `peon-mem uninstall` reverses it (memory data is never touched).
100
+
101
+ Or manually:
102
+
103
+ ```bash
104
+ git clone https://github.com/VineetV2/peon-mem && cd peon-mem
105
+ npm install && npm run build
106
+ node bin/peon-mem.mjs install --dry-run # inspect, then run without --dry-run
107
+ ```
108
+
109
+ Create `.env` in the repo root:
110
+
111
+ ```bash
112
+ OPENROUTER_API_KEY=sk-or-...
113
+ PEON_PROCESSING_MODEL=google/gemini-2.5-flash-lite # cheap + good enough (measured)
114
+ PEON_EMBEDDING_MODEL=openai/text-embedding-3-small
115
+ ```
116
+
117
+ Start the daemon (the installer prints a launchd/systemd recipe, or just):
118
+
119
+ ```bash
120
+ node dist/daemon-cli.js # serves 127.0.0.1:3737
121
+ ```
122
+
123
+ Then wire your agent (the installer prints these filled in for your paths):
124
+
125
+ - **Claude Code** โ€” add the hook to `~/.claude/settings.json` (SessionStart / UserPromptSubmit /
126
+ SessionEnd โ†’ `scripts/claude-peon-hook.mjs`) and the MCP server (`dist/index.js`).
127
+ - **Codex / any MCP client** โ€” register `dist/index.js` as a stdio MCP server; 16 tools
128
+ (`start_session`, `get_context`, `search_memory`, `record_message`, `process_memory`, โ€ฆ).
129
+
130
+ Open `http://127.0.0.1:3737/monitor` and watch your brain grow.
131
+
132
+ ## How it works
133
+
134
+ 1. **Record** โ€” hooks stream messages/events/tool-calls into `<project>/.peon/raw/` (append-only).
135
+ 2. **Consolidate** โ€” past a size gate, an LLM turns the session delta into typed belief records in
136
+ `.peon/brain/memories.jsonl` (importance/confidence scores, entities, provenance pointers),
137
+ reconciling against existing beliefs: supersede, merge, conflict-flag. Recoverable, never deleted.
138
+ 3. **Retrieve + inject** โ€” on every prompt, beliefs are ranked (RRF lexical+semantic, MMR,
139
+ reinforcement) and injected alongside episodic verbatim matches and inherited global beliefs.
140
+ 4. **Self-curate** โ€” a background brain pass reinforces recalled beliefs, compresses stale
141
+ clusters, resolves duplicates โ€” every action logged and undoable.
142
+ 5. **Self-audit (STL)** โ€” a daily job reports: recorded / injected / went-wrong / consolidation
143
+ correctness, with serve-latency telemetry and a health verdict.
144
+
145
+
146
+ ## Full install (copy-paste)
147
+
148
+ ### 1. Daemon (always-on, macOS launchd)
149
+
150
+ ```bash
151
+ node scripts/install-peon.mjs # prints everything below filled in for YOUR paths
152
+ ```
153
+
154
+ Or manually โ€” `~/Library/LaunchAgents/com.peon.daemon.plist`:
155
+
156
+ ```xml
157
+ <?xml version="1.0" encoding="UTF-8"?>
158
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
159
+ <plist version="1.0"><dict>
160
+ <key>Label</key><string>com.peon.daemon</string>
161
+ <key>ProgramArguments</key><array>
162
+ <string>/opt/homebrew/bin/node</string>
163
+ <string>/ABSOLUTE/PATH/TO/peon/dist/daemon-cli.js</string>
164
+ </array>
165
+ <key>RunAtLoad</key><true/>
166
+ <key>KeepAlive</key><true/>
167
+ </dict></plist>
168
+ ```
169
+
170
+ ```bash
171
+ launchctl load ~/Library/LaunchAgents/com.peon.daemon.plist
172
+ curl http://127.0.0.1:3737/health # โ†’ {"ok":true}
173
+ ```
174
+
175
+ Linux: run `node dist/daemon-cli.js` under systemd (`Restart=always`).
176
+
177
+ ### 2. Claude Code โ€” hooks (auto capture + injection)
178
+
179
+ Merge into `~/.claude/settings.json` (replace the path):
180
+
181
+ ```json
182
+ {
183
+ "hooks": {
184
+ "SessionStart": [{ "hooks": [{ "type": "command",
185
+ "command": "node /ABSOLUTE/PATH/TO/peon/scripts/claude-peon-hook.mjs" }] }],
186
+ "UserPromptSubmit": [{ "hooks": [{ "type": "command",
187
+ "command": "node /ABSOLUTE/PATH/TO/peon/scripts/claude-peon-hook.mjs" }] }],
188
+ "SessionEnd": [{ "hooks": [{ "type": "command",
189
+ "command": "node /ABSOLUTE/PATH/TO/peon/scripts/claude-peon-hook.mjs" }] }]
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### 3. Claude Code โ€” MCP server (search/inspect tools)
195
+
196
+ ```bash
197
+ claude mcp add peon -- node /ABSOLUTE/PATH/TO/peon/dist/index.js
198
+ ```
199
+
200
+ ### 4. Codex / any MCP client
201
+
202
+ `~/.codex/config.toml`:
203
+
204
+ ```toml
205
+ [mcp_servers.peon]
206
+ command = "node"
207
+ args = ["/ABSOLUTE/PATH/TO/peon/dist/index.js"]
208
+ [mcp_servers.peon.env]
209
+ PEON_DAEMON_URL = "http://127.0.0.1:3737"
210
+ ```
211
+
212
+ Codex has no hooks โ€” add usage rules to `~/.codex/AGENTS.md` telling it to call
213
+ `start_session` + `get_context` at session start and `record_message` for durable facts
214
+ (example block in [docs/](docs/)).
215
+
216
+ ### 5. Verify
217
+
218
+ ```bash
219
+ curl "http://127.0.0.1:3737/context?projectPath=$PWD&query=test" # JSON context
220
+ open http://127.0.0.1:3737/monitor # the Neural Universe
221
+ ```
222
+
223
+ Start a Claude Code session in any project, say something decision-shaped, end the session โ€”
224
+ within a minute the monitor shows the belief. Next session injects it.
225
+
226
+ ### MCP tools exposed
227
+
228
+ `start_session` ยท `record_message` ยท `record_event` ยท `end_session` ยท `get_context` ยท
229
+ `search_memory` ยท `inspect_brain` ยท `build_injection` ยท `query_projects` (cross-project search) ยท
230
+ `quality_report` ยท `remember_global` ยท `search_global_memory` ยท `import_global_memory` ยท
231
+ `evaluate_project` ยท `process_memory` ยท `maybe_process_memory`
232
+
233
+ ### Uninstall
234
+
235
+ ```bash
236
+ launchctl unload ~/Library/LaunchAgents/com.peon.daemon.plist
237
+ # remove the hook entries + MCP server from your agent config
238
+ # your memory stays in <project>/.peon/ and ~/Library/Application\ Support/Peon/ โ€” plain files, delete when ready
239
+ ```
240
+
241
+ ## Troubleshooting / FAQ
242
+
243
+ - **No injection appearing?** `curl http://127.0.0.1:3737/health`; check hook is registered
244
+ (`claude` โ†’ run any prompt โ†’ monitor Systems page shows the request).
245
+ - **431 errors on huge prompts?** Handled โ€” the hook caps the retrieval query at 2k chars.
246
+ - **No OpenRouter key?** Everything still runs; retrieval is lexical + episodic only
247
+ (semantic ranking and consolidation need a model). `PEON_EMBEDDING_MODE=ollama` works too.
248
+ - **Cost?** Consolidation is gated (default: fires per ~6k new chars, ~cents/day with
249
+ flash-lite). Query embeddings are cached to disk โ€” repeats are free.
250
+ - **Multiple machines?** Brains are plain files in your repos โ€” commit `.peon/` if you want
251
+ memory to travel (redact first: raw layer contains session text).
252
+ - **Is my data sent anywhere?** Only consolidation/embedding calls to your configured model
253
+ provider. No telemetry, no cloud store. Daemon rejects non-loopback callers.
254
+
255
+ ## Configuration (env)
256
+
257
+ | Var | Default | Purpose |
258
+ |---|---|---|
259
+ | `OPENROUTER_API_KEY` | โ€” | consolidation + embeddings |
260
+ | `PEON_PROCESSING_MODEL` | `google/gemini-2.5-flash-lite` | consolidation model |
261
+ | `PEON_EMBEDDING_MODE` | auto | `api` / `ollama` / `local` / `off` |
262
+ | `PEON_EMBEDDING_MODEL` | โ€” | e.g. `openai/text-embedding-3-small` or an Ollama model |
263
+ | `PEON_OLLAMA_URL` | `http://127.0.0.1:11434` | local embedding server |
264
+ | `PEON_AI_MODE` | `gated` | `off` disables all LLM calls |
265
+ | `PEON_FLUSH_MIN_CHARS` | `6000` | consolidation cost gate |
266
+ | `PEON_MEMORY_DIR` | `.peon` | per-project brain dir name |
267
+ | `PEON_DAEMON_URL` | `http://127.0.0.1:3737` | daemon address |
268
+ | `PEON_CONSOLIDATION_MAX_DELTA_CHARS` | `60000` | anti truncation-stall chunking |
269
+ | `PEON_DISABLED` | โ€” | hard off-switch for A/B testing |
270
+
271
+ ## Project brains
272
+
273
+ - A brain lives in `<project>/.peon/` โ€” human-readable JSONL + markdown. Commit it or ignore it;
274
+ your choice (`.gitignore` ships ignoring it).
275
+ - `.peon/root` marks a brain boundary. New brains are born rooted; a parent directory can never
276
+ swallow a project's memory.
277
+ - The global brain lives in `~/Library/Application Support/Peon/global/` (macOS).
278
+
279
+ ## Honesty section
280
+
281
+ Peon's development is eval-gated and keeps its negative results: an associative entity graph was
282
+ built, measured (โˆ’2.9% Recall@10), and turned OFF by default. Consolidation is lossy by design โ€”
283
+ that's why the episodic layer exists and is regression-tested. The eval harness + committed
284
+ ledger (`npm run eval`) let you verify retrieval changes on your own brain.
285
+
286
+ ## Security
287
+
288
+ - Daemon binds `127.0.0.1` only and rejects non-loopback `Host`/`Origin` (DNS-rebinding defense).
289
+ - Secrets (API keys, tokens, JWTs) are redacted at the injection boundary.
290
+ - Path-traversal guarded; per-project write locks; atomic tmp+rename writes; automatic backups
291
+ before destructive-adjacent operations. Nothing is hard-deleted.
292
+
293
+ ## License
294
+
295
+ MIT ยฉ Vineet Vora
296
+
297
+ ## Contributing
298
+
299
+ PRs welcome. Rules of the house: every retrieval/quality change ships with a test and an
300
+ eval-ledger run (`npm run eval`); negative results get documented, not deleted; nothing may
301
+ hard-delete user memory. `npm test` must stay green (255 tests).
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * peon-mem โ€” guided installer/manager for the Peon memory brain.
4
+ *
5
+ * peon-mem install [--yes] [--dry-run] guided setup: memory home โ†’ LLM โ†’ daemon โ†’ MCP apps
6
+ * peon-mem uninstall remove service + hooks (memory data is never touched)
7
+ * peon-mem daemon run the daemon in the foreground
8
+ * peon-mem doctor health + config check
9
+ */
10
+ import { execFileSync, spawnSync } from "node:child_process";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
12
+ import { homedir, platform } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { createInterface } from "node:readline/promises";
16
+
17
+ const PKG = dirname(dirname(fileURLToPath(import.meta.url)));
18
+ const HOME = homedir();
19
+ const MAC = platform() === "darwin";
20
+ const DEFAULT_HOME = MAC ? join(HOME, "Library", "Application Support", "Peon") : join(HOME, ".local", "share", "peon");
21
+ const PLIST = join(HOME, "Library", "LaunchAgents", "com.peon.daemon.plist");
22
+ const HOOK = join(PKG, "scripts", "claude-peon-hook.mjs");
23
+ const DAEMON = join(PKG, "dist", "daemon-cli.js");
24
+ const MCP = join(PKG, "dist", "index.js");
25
+ const NODE = process.execPath;
26
+
27
+ const cmd = process.argv[2] || "help";
28
+ const DRY = process.argv.includes("--dry-run");
29
+ const YES = process.argv.includes("--yes") || !process.stdout.isTTY;
30
+ const log = (s) => console.log(s);
31
+ const act = (desc, fn) => { log((DRY ? " [dry-run] " : " โœ” ") + desc); if (!DRY) fn(); };
32
+ const rl = YES ? null : createInterface({ input: process.stdin, output: process.stdout });
33
+ async function ask(q, def) {
34
+ if (!rl) return def;
35
+ const a = (await rl.question(`${q}${def ? ` [${def}]` : ""}: `)).trim();
36
+ return a || def;
37
+ }
38
+
39
+ function which(bin) { return spawnSync("which", [bin], { stdio: "pipe" }).status === 0; }
40
+ function backupWrite(file, content) {
41
+ if (existsSync(file)) copyFileSync(file, file + ".peon-backup");
42
+ mkdirSync(dirname(file), { recursive: true });
43
+ writeFileSync(file, content);
44
+ }
45
+
46
+ // ---------- app detection ----------
47
+ // wire kinds: hooks+CLI (claude code) ยท toml (codex) ยท mcpServers JSON (most apps) ยท
48
+ // vscode "servers" JSON ยท zed context_servers ยท manual (UI-configured apps get instructions)
49
+ const APP_SUPPORT = MAC ? join(HOME, "Library", "Application Support") : join(HOME, ".config");
50
+ function detectApps() {
51
+ return [
52
+ { id: "claude", name: "Claude Code", kind: "claude-code",
53
+ found: which("claude") || existsSync(join(HOME, ".claude")) },
54
+ { id: "claude-desktop", name: "Claude Desktop", kind: "json",
55
+ file: join(APP_SUPPORT, "Claude", "claude_desktop_config.json"),
56
+ found: existsSync(join(APP_SUPPORT, "Claude")) || existsSync("/Applications/Claude.app") },
57
+ { id: "codex", name: "Codex", kind: "toml",
58
+ found: existsSync(join(HOME, ".codex")) },
59
+ { id: "gemini", name: "Gemini CLI", kind: "json",
60
+ file: join(HOME, ".gemini", "settings.json"),
61
+ found: which("gemini") || existsSync(join(HOME, ".gemini")) },
62
+ { id: "cursor", name: "Cursor", kind: "json",
63
+ file: join(HOME, ".cursor", "mcp.json"),
64
+ found: existsSync(join(HOME, ".cursor")) || existsSync("/Applications/Cursor.app") },
65
+ { id: "windsurf", name: "Windsurf", kind: "json",
66
+ file: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
67
+ found: existsSync(join(HOME, ".codeium", "windsurf")) || existsSync("/Applications/Windsurf.app") },
68
+ { id: "vscode", name: "VS Code (Copilot MCP)", kind: "vscode",
69
+ file: MAC ? join(APP_SUPPORT, "Code", "User", "mcp.json") : join(HOME, ".config", "Code", "User", "mcp.json"),
70
+ found: which("code") || existsSync(MAC ? join(APP_SUPPORT, "Code") : join(HOME, ".config", "Code")) },
71
+ { id: "zed", name: "Zed", kind: "zed",
72
+ file: join(HOME, ".config", "zed", "settings.json"),
73
+ found: existsSync(join(HOME, ".config", "zed")) || existsSync("/Applications/Zed.app") },
74
+ { id: "lmstudio", name: "LM Studio", kind: "json",
75
+ file: join(HOME, ".lmstudio", "mcp.json"),
76
+ found: existsSync(join(HOME, ".lmstudio")) || existsSync("/Applications/LM Studio.app") },
77
+ { id: "chatgpt", name: "ChatGPT Desktop", kind: "manual",
78
+ found: existsSync("/Applications/ChatGPT.app"),
79
+ how: "ChatGPT โ†’ Settings โ†’ Connectors โ†’ Advanced โ†’ enable Developer Mode โ†’ add MCP server: command=" },
80
+ { id: "perplexity", name: "Perplexity Desktop", kind: "manual",
81
+ found: existsSync("/Applications/Perplexity.app") || existsSync("/Applications/Perplexity- Ask Anything.app"),
82
+ how: "Perplexity โ†’ Settings โ†’ Connectors โ†’ Add Connector โ†’ Advanced: command=" }
83
+ ];
84
+ }
85
+
86
+ // ---------- per-app wiring ----------
87
+ function wireClaude() {
88
+ const settings = join(HOME, ".claude", "settings.json");
89
+ let s = {}; try { s = JSON.parse(readFileSync(settings, "utf8")); } catch {}
90
+ s.hooks = s.hooks || {};
91
+ let changed = false;
92
+ for (const ev of ["SessionStart", "UserPromptSubmit", "SessionEnd"]) {
93
+ const arr = (s.hooks[ev] = s.hooks[ev] || []);
94
+ if (!JSON.stringify(arr).includes("claude-peon-hook.mjs")) {
95
+ arr.push({ hooks: [{ type: "command", command: `"${NODE}" "${HOOK}"` }] }); changed = true;
96
+ }
97
+ }
98
+ if (changed) act("Claude Code: hooks โ†’ " + settings + " (backup kept)", () => backupWrite(settings, JSON.stringify(s, null, 2) + "\n"));
99
+ else log(" โœ” Claude Code: hooks already present");
100
+ if (!DRY) {
101
+ const r = spawnSync("claude", ["mcp", "add", "peon", "--", NODE, MCP], { stdio: "ignore" });
102
+ log(r.status === 0 ? " โœ” Claude Code: MCP server registered" : ` โ†’ run manually: claude mcp add peon -- "${NODE}" "${MCP}"`);
103
+ } else log(" [dry-run] Claude Code: claude mcp add peon");
104
+ }
105
+ function wireCodex() {
106
+ const f = join(HOME, ".codex", "config.toml");
107
+ let s = ""; try { s = readFileSync(f, "utf8"); } catch {}
108
+ if (s.includes("[mcp_servers.peon]")) return log(" โœ” Codex: MCP already configured");
109
+ const block = `\n[mcp_servers.peon]\ncommand = "${NODE}"\nargs = ["${MCP}"]\n\n[mcp_servers.peon.env]\nPEON_DAEMON_URL = "http://127.0.0.1:3737"\n`;
110
+ act("Codex: [mcp_servers.peon] โ†’ " + f + " (backup kept)", () => backupWrite(f, s + block));
111
+ }
112
+ function wireJsonMcp(name, file) {
113
+ let s = {}; try { s = JSON.parse(readFileSync(file, "utf8")); } catch {}
114
+ s.mcpServers = s.mcpServers || {};
115
+ if (s.mcpServers.peon) return log(` โœ” ${name}: MCP already configured`);
116
+ s.mcpServers.peon = { command: NODE, args: [MCP], env: { PEON_DAEMON_URL: "http://127.0.0.1:3737" } };
117
+ act(`${name}: mcpServers.peon โ†’ ${file} (backup kept)`, () => backupWrite(file, JSON.stringify(s, null, 2) + "\n"));
118
+ }
119
+
120
+ function wireVsCode(file) {
121
+ let s = {}; try { s = JSON.parse(readFileSync(file, "utf8")); } catch {}
122
+ s.servers = s.servers || {};
123
+ if (s.servers.peon) return log(" โœ” VS Code: MCP already configured");
124
+ s.servers.peon = { type: "stdio", command: NODE, args: [MCP], env: { PEON_DAEMON_URL: "http://127.0.0.1:3737" } };
125
+ act("VS Code: servers.peon โ†’ " + file + " (backup kept)", () => backupWrite(file, JSON.stringify(s, null, 2) + "\n"));
126
+ }
127
+ function wireZed(file) {
128
+ let s = {}; try { s = JSON.parse(readFileSync(file, "utf8")); } catch {}
129
+ s.context_servers = s.context_servers || {};
130
+ if (s.context_servers.peon) return log(" โœ” Zed: MCP already configured");
131
+ s.context_servers.peon = { command: { path: NODE, args: [MCP] }, settings: {} };
132
+ act("Zed: context_servers.peon โ†’ " + file + " (backup kept)", () => backupWrite(file, JSON.stringify(s, null, 2) + "\n"));
133
+ }
134
+
135
+ // ---------- service ----------
136
+ function installService(memoryHome) {
137
+ if (MAC) {
138
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
139
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
140
+ <plist version="1.0"><dict>
141
+ <key>Label</key><string>com.peon.daemon</string>
142
+ <key>ProgramArguments</key><array>
143
+ <string>${NODE}</string>
144
+ <string>${DAEMON}</string>
145
+ </array>
146
+ <key>WorkingDirectory</key><string>${memoryHome}</string>
147
+ <key>RunAtLoad</key><true/>
148
+ <key>KeepAlive</key><true/>
149
+ <key>StandardOutPath</key><string>${memoryHome}/daemon.out.log</string>
150
+ <key>StandardErrorPath</key><string>${memoryHome}/daemon.err.log</string>
151
+ </dict></plist>\n`;
152
+ act("daemon service (launchd, auto-start) โ†’ " + PLIST, () => {
153
+ mkdirSync(dirname(PLIST), { recursive: true });
154
+ writeFileSync(PLIST, plist);
155
+ spawnSync("launchctl", ["unload", PLIST], { stdio: "ignore" });
156
+ execFileSync("launchctl", ["load", PLIST]);
157
+ });
158
+ } else {
159
+ log(" โ†’ Linux: systemd user unit:");
160
+ log(` ExecStart=${NODE} ${DAEMON}\n WorkingDirectory=${memoryHome}\n Restart=always`);
161
+ }
162
+ }
163
+
164
+ async function health() {
165
+ try { const r = await fetch("http://127.0.0.1:3737/health"); return (await r.json()).ok === true; } catch { return false; }
166
+ }
167
+
168
+ // ================= commands =================
169
+ if (cmd === "install") {
170
+ log("\n๐Ÿง  Peon setup โ€” a memory brain for your AI agents\n");
171
+
172
+ // Step 1 โ€” memory home
173
+ log("Step 1/4 ยท Where should Peon's GLOBAL brain live?");
174
+ log(" (press Enter to accept the default)");
175
+ const memoryHome = await ask(" memory home", DEFAULT_HOME);
176
+ act("create " + memoryHome, () => mkdirSync(memoryHome, { recursive: true }));
177
+
178
+ // Step 2 โ€” LLM provider
179
+ log("\nStep 2/4 ยท LLM for consolidation + semantic retrieval");
180
+ log(" 1) OpenRouter โ€” one key, any model (default: gemini-2.5-flash-lite, ~cents/day)");
181
+ log(" 2) OpenAI โ€” gpt-4o-mini + text-embedding-3-small");
182
+ log(" 3) Anthropic โ€” Claude Haiku (embeddings fall back local โ€” pair with Ollama for semantic)");
183
+ log(" 4) Ollama โ€” 100% local + free (llama3.2 + nomic-embed-text on your machine)");
184
+ log(" 5) skip โ€” lexical-only memory, add a provider later");
185
+ const choice = await ask(" choose 1-5", "1");
186
+ const envFile = join(memoryHome, ".env");
187
+ let envLines = [`# Peon config โ€” generated by peon-mem install`];
188
+ if (choice === "1") {
189
+ const key = await ask(" OpenRouter API key (sk-or-โ€ฆ)", "");
190
+ envLines.push("PEON_PROVIDER=openrouter", `OPENROUTER_API_KEY=${key}`,
191
+ "PEON_PROCESSING_MODEL=google/gemini-2.5-flash-lite", "PEON_EMBEDDING_MODEL=openai/text-embedding-3-small");
192
+ } else if (choice === "2") {
193
+ const key = await ask(" OpenAI API key (sk-โ€ฆ)", "");
194
+ envLines.push("PEON_PROVIDER=openai", `OPENAI_API_KEY=${key}`,
195
+ "PEON_PROCESSING_MODEL=gpt-4o-mini", "PEON_EMBEDDING_MODEL=text-embedding-3-small");
196
+ } else if (choice === "3") {
197
+ const key = await ask(" Anthropic API key (sk-ant-โ€ฆ)", "");
198
+ envLines.push("PEON_PROVIDER=anthropic", `ANTHROPIC_API_KEY=${key}`,
199
+ "PEON_PROCESSING_MODEL=claude-haiku-4-5-20251001",
200
+ "# Anthropic has no embeddings API โ€” install Ollama + set PEON_EMBEDDING_MODE=ollama for semantic retrieval");
201
+ } else if (choice === "4") {
202
+ envLines.push("PEON_PROVIDER=ollama", "PEON_EMBEDDING_MODE=ollama",
203
+ "PEON_PROCESSING_MODEL=llama3.2", "PEON_EMBEDDING_MODEL=nomic-embed-text");
204
+ const up = await fetch("http://127.0.0.1:11434/api/tags").then((r) => r.ok).catch(() => false);
205
+ if (!up) log(" โ†’ Ollama not running. Install: https://ollama.com then: ollama pull llama3.2 && ollama pull nomic-embed-text");
206
+ else log(" โœ” Ollama detected on :11434 โ€” pull models if missing: ollama pull llama3.2 && ollama pull nomic-embed-text");
207
+ } else {
208
+ envLines.push("# no provider configured โ€” Peon runs lexical-only. Re-run `peon-mem install` anytime.");
209
+ }
210
+ if (existsSync(envFile)) log(" โœ” keeping existing " + envFile);
211
+ else act("write " + envFile, () => writeFileSync(envFile, envLines.join("\n") + "\n"));
212
+
213
+ // Step 3 โ€” daemon
214
+ log("\nStep 3/4 ยท Daemon (always-on, 127.0.0.1:3737)");
215
+ installService(memoryHome);
216
+
217
+ // Step 4 โ€” apps
218
+ log("\nStep 4/4 ยท Wire your AI apps (detected on this machine)");
219
+ const apps = detectApps();
220
+ apps.forEach((a, i) => log(` ${i + 1}) ${a.found ? "๐ŸŸข" : "โšช"} ${a.name}${a.found ? "" : " (not detected)"}`));
221
+ const detected = apps.filter((a) => a.found).map((a) => a.id);
222
+ const pick = await ask(` install MCP into (comma ids or 'all') โ€” detected: ${detected.join(",") || "none"}`, detected.join(",") || "none");
223
+ const chosen = pick === "all" ? apps.map((a) => a.id) : pick.split(",").map((x) => x.trim()).filter(Boolean);
224
+ for (const id of chosen) {
225
+ const app = apps.find((a) => a.id === id);
226
+ if (!app) { log(" โš  unknown app id: " + id); continue; }
227
+ if (app.kind === "claude-code") wireClaude();
228
+ else if (app.kind === "toml") wireCodex();
229
+ else if (app.kind === "json") wireJsonMcp(app.name, app.file);
230
+ else if (app.kind === "vscode") wireVsCode(app.file);
231
+ else if (app.kind === "zed") wireZed(app.file);
232
+ else if (app.kind === "manual") {
233
+ log(` โ†’ ${app.name} is configured in-app (no config file). In the app:`);
234
+ log(` ${app.how}"${NODE}" args=["${MCP}"]`);
235
+ }
236
+ }
237
+
238
+ if (!DRY) {
239
+ await new Promise((r) => setTimeout(r, 1500));
240
+ log("\n" + ((await health()) ? "โœ” daemon healthy โ€” http://127.0.0.1:3737" : "โš  daemon not answering โ€” check " + memoryHome + "/daemon.err.log"));
241
+ }
242
+ log("๐ŸŒŒ Monitor (the Neural Universe): http://127.0.0.1:3737/monitor");
243
+ log("Memory lives in <project>/.peon/ (child brains) + " + memoryHome + " (global brain)\n");
244
+ rl?.close();
245
+ } else if (cmd === "uninstall") {
246
+ if (MAC && existsSync(PLIST)) act("stop + remove daemon service", () => spawnSync("launchctl", ["unload", PLIST], { stdio: "ignore" }));
247
+ const settings = join(HOME, ".claude", "settings.json");
248
+ try {
249
+ const s = JSON.parse(readFileSync(settings, "utf8"));
250
+ for (const ev of Object.keys(s.hooks || {}))
251
+ s.hooks[ev] = s.hooks[ev].filter((h) => !JSON.stringify(h).includes("claude-peon-hook.mjs"));
252
+ act("remove Peon hooks from " + settings, () => backupWrite(settings, JSON.stringify(s, null, 2) + "\n"));
253
+ } catch {}
254
+ log("Remove [mcp_servers.peon] / mcpServers.peon from Codex/Gemini/Cursor configs if you added them.");
255
+ log("Memory data untouched: <project>/.peon/ and " + DEFAULT_HOME);
256
+ rl?.close();
257
+ } else if (cmd === "daemon") {
258
+ await import(DAEMON);
259
+ } else if (cmd === "doctor") {
260
+ log("package : " + PKG);
261
+ log("daemon : " + ((await health()) ? "healthy (127.0.0.1:3737)" : "DOWN"));
262
+ const settings = join(HOME, ".claude", "settings.json");
263
+ log("hooks : " + (existsSync(settings) && readFileSync(settings, "utf8").includes("claude-peon-hook.mjs") ? "installed" : "not installed"));
264
+ log("config : " + join(DEFAULT_HOME, ".env") + (existsSync(join(DEFAULT_HOME, ".env")) ? "" : " (missing โ€” run peon-mem install)"));
265
+ rl?.close();
266
+ } else {
267
+ log("peon-mem โ€” memory brain for AI coding agents");
268
+ log(" peon-mem install [--yes] [--dry-run] guided setup (memory home โ†’ LLM โ†’ daemon โ†’ apps)");
269
+ log(" peon-mem uninstall remove service + hooks (data stays)");
270
+ log(" peon-mem daemon run daemon in foreground");
271
+ log(" peon-mem doctor health check");
272
+ rl?.close();
273
+ }