@jbarket/loomai 0.5.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 (176) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/LICENSE +661 -0
  3. package/README.md +702 -0
  4. package/assets/demo.cast +396 -0
  5. package/assets/skill/SKILL.md +192 -0
  6. package/dist/backends/embedding-runtime.d.ts +39 -0
  7. package/dist/backends/embedding-runtime.js +265 -0
  8. package/dist/backends/episodes.d.ts +36 -0
  9. package/dist/backends/episodes.js +126 -0
  10. package/dist/backends/fastembed.d.ts +18 -0
  11. package/dist/backends/fastembed.js +95 -0
  12. package/dist/backends/glob.d.ts +10 -0
  13. package/dist/backends/glob.js +16 -0
  14. package/dist/backends/index.d.ts +6 -0
  15. package/dist/backends/index.js +78 -0
  16. package/dist/backends/migrations.d.ts +38 -0
  17. package/dist/backends/migrations.js +157 -0
  18. package/dist/backends/mmr.d.ts +45 -0
  19. package/dist/backends/mmr.js +82 -0
  20. package/dist/backends/proposals.d.ts +65 -0
  21. package/dist/backends/proposals.js +182 -0
  22. package/dist/backends/recall-log.d.ts +42 -0
  23. package/dist/backends/recall-log.js +143 -0
  24. package/dist/backends/retry.d.ts +29 -0
  25. package/dist/backends/retry.js +60 -0
  26. package/dist/backends/salience.d.ts +57 -0
  27. package/dist/backends/salience.js +186 -0
  28. package/dist/backends/sqlite-knowledge.d.ts +37 -0
  29. package/dist/backends/sqlite-knowledge.js +788 -0
  30. package/dist/backends/sqlite-vec.d.ts +64 -0
  31. package/dist/backends/sqlite-vec.js +737 -0
  32. package/dist/backends/ttl.d.ts +24 -0
  33. package/dist/backends/ttl.js +50 -0
  34. package/dist/backends/types.d.ts +562 -0
  35. package/dist/backends/types.js +8 -0
  36. package/dist/blocks/harness.d.ts +45 -0
  37. package/dist/blocks/harness.js +142 -0
  38. package/dist/blocks/model.d.ts +4 -0
  39. package/dist/blocks/model.js +56 -0
  40. package/dist/blocks/types.d.ts +40 -0
  41. package/dist/blocks/types.js +34 -0
  42. package/dist/categories.d.ts +16 -0
  43. package/dist/categories.js +29 -0
  44. package/dist/cli/args.d.ts +21 -0
  45. package/dist/cli/args.js +47 -0
  46. package/dist/cli/bootstrap.d.ts +2 -0
  47. package/dist/cli/bootstrap.js +214 -0
  48. package/dist/cli/doctor.d.ts +2 -0
  49. package/dist/cli/doctor.js +206 -0
  50. package/dist/cli/forget.d.ts +2 -0
  51. package/dist/cli/forget.js +97 -0
  52. package/dist/cli/harness.d.ts +2 -0
  53. package/dist/cli/harness.js +91 -0
  54. package/dist/cli/index.d.ts +2 -0
  55. package/dist/cli/index.js +127 -0
  56. package/dist/cli/inject.d.ts +2 -0
  57. package/dist/cli/inject.js +298 -0
  58. package/dist/cli/install.d.ts +2 -0
  59. package/dist/cli/install.js +137 -0
  60. package/dist/cli/io.d.ts +14 -0
  61. package/dist/cli/io.js +63 -0
  62. package/dist/cli/knowledge.d.ts +2 -0
  63. package/dist/cli/knowledge.js +216 -0
  64. package/dist/cli/memory.d.ts +2 -0
  65. package/dist/cli/memory.js +608 -0
  66. package/dist/cli/migrate.d.ts +2 -0
  67. package/dist/cli/migrate.js +116 -0
  68. package/dist/cli/recall.d.ts +2 -0
  69. package/dist/cli/recall.js +91 -0
  70. package/dist/cli/remember.d.ts +2 -0
  71. package/dist/cli/remember.js +111 -0
  72. package/dist/cli/serve.d.ts +2 -0
  73. package/dist/cli/serve.js +71 -0
  74. package/dist/cli/subcommands.d.ts +7 -0
  75. package/dist/cli/subcommands.js +11 -0
  76. package/dist/cli/tui/multi-select.d.ts +53 -0
  77. package/dist/cli/tui/multi-select.js +116 -0
  78. package/dist/cli/update-identity.d.ts +2 -0
  79. package/dist/cli/update-identity.js +90 -0
  80. package/dist/cli/update.d.ts +2 -0
  81. package/dist/cli/update.js +87 -0
  82. package/dist/cli/wake.d.ts +2 -0
  83. package/dist/cli/wake.js +53 -0
  84. package/dist/clients.d.ts +8 -0
  85. package/dist/clients.js +63 -0
  86. package/dist/config.d.ts +59 -0
  87. package/dist/config.js +160 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.js +75 -0
  90. package/dist/injection/harnesses.d.ts +17 -0
  91. package/dist/injection/harnesses.js +51 -0
  92. package/dist/injection/render.d.ts +17 -0
  93. package/dist/injection/render.js +50 -0
  94. package/dist/injection/writer.d.ts +25 -0
  95. package/dist/injection/writer.js +118 -0
  96. package/dist/install/harnesses.d.ts +26 -0
  97. package/dist/install/harnesses.js +90 -0
  98. package/dist/install/names.d.ts +16 -0
  99. package/dist/install/names.js +36 -0
  100. package/dist/install/render.d.ts +18 -0
  101. package/dist/install/render.js +42 -0
  102. package/dist/install/skill-source.d.ts +1 -0
  103. package/dist/install/skill-source.js +11 -0
  104. package/dist/path-safety.d.ts +19 -0
  105. package/dist/path-safety.js +62 -0
  106. package/dist/server.d.ts +8 -0
  107. package/dist/server.js +571 -0
  108. package/dist/tools/append-maintenance-log.d.ts +52 -0
  109. package/dist/tools/append-maintenance-log.js +103 -0
  110. package/dist/tools/archive.d.ts +2 -0
  111. package/dist/tools/archive.js +21 -0
  112. package/dist/tools/bootstrap.d.ts +11 -0
  113. package/dist/tools/bootstrap.js +161 -0
  114. package/dist/tools/dossier.d.ts +1 -0
  115. package/dist/tools/dossier.js +102 -0
  116. package/dist/tools/eurorack-import.d.ts +47 -0
  117. package/dist/tools/eurorack-import.js +199 -0
  118. package/dist/tools/find-similar.d.ts +3 -0
  119. package/dist/tools/find-similar.js +26 -0
  120. package/dist/tools/forget.d.ts +6 -0
  121. package/dist/tools/forget.js +70 -0
  122. package/dist/tools/harness.d.ts +18 -0
  123. package/dist/tools/harness.js +54 -0
  124. package/dist/tools/identity-scaffold.d.ts +22 -0
  125. package/dist/tools/identity-scaffold.js +142 -0
  126. package/dist/tools/identity.d.ts +4 -0
  127. package/dist/tools/identity.js +226 -0
  128. package/dist/tools/knowledge-archive.d.ts +5 -0
  129. package/dist/tools/knowledge-archive.js +24 -0
  130. package/dist/tools/knowledge-history.d.ts +6 -0
  131. package/dist/tools/knowledge-history.js +55 -0
  132. package/dist/tools/knowledge-maintain.d.ts +9 -0
  133. package/dist/tools/knowledge-maintain.js +113 -0
  134. package/dist/tools/knowledge-merge.d.ts +8 -0
  135. package/dist/tools/knowledge-merge.js +40 -0
  136. package/dist/tools/knowledge-move.d.ts +10 -0
  137. package/dist/tools/knowledge-move.js +48 -0
  138. package/dist/tools/knowledge-purge.d.ts +5 -0
  139. package/dist/tools/knowledge-purge.js +20 -0
  140. package/dist/tools/knowledge-recall.d.ts +20 -0
  141. package/dist/tools/knowledge-recall.js +142 -0
  142. package/dist/tools/knowledge-restore.d.ts +4 -0
  143. package/dist/tools/knowledge-restore.js +24 -0
  144. package/dist/tools/knowledge-supersede.d.ts +6 -0
  145. package/dist/tools/knowledge-supersede.js +36 -0
  146. package/dist/tools/knowledge-verify.d.ts +8 -0
  147. package/dist/tools/knowledge-verify.js +39 -0
  148. package/dist/tools/knowledge-write.d.ts +25 -0
  149. package/dist/tools/knowledge-write.js +87 -0
  150. package/dist/tools/memory-audit.d.ts +3 -0
  151. package/dist/tools/memory-audit.js +47 -0
  152. package/dist/tools/memory-list.d.ts +4 -0
  153. package/dist/tools/memory-list.js +23 -0
  154. package/dist/tools/prune.d.ts +4 -0
  155. package/dist/tools/prune.js +28 -0
  156. package/dist/tools/recall.d.ts +4 -0
  157. package/dist/tools/recall.js +24 -0
  158. package/dist/tools/remember.d.ts +10 -0
  159. package/dist/tools/remember.js +43 -0
  160. package/dist/tools/restore.d.ts +2 -0
  161. package/dist/tools/restore.js +20 -0
  162. package/dist/tools/update-identity.d.ts +34 -0
  163. package/dist/tools/update-identity.js +194 -0
  164. package/dist/tools/update.d.ts +2 -0
  165. package/dist/tools/update.js +16 -0
  166. package/dist/transport/guards.d.ts +35 -0
  167. package/dist/transport/guards.js +107 -0
  168. package/dist/transport/http-server.d.ts +17 -0
  169. package/dist/transport/http-server.js +198 -0
  170. package/dist/transport/mcp-app-hello.d.ts +3 -0
  171. package/dist/transport/mcp-app-hello.js +66 -0
  172. package/dist/widgets/display-modes.html +131 -0
  173. package/dist/widgets/hello.html +138 -0
  174. package/dist/widgets/proposals.html +131 -0
  175. package/dist/widgets/top-of-mind.html +131 -0
  176. package/package.json +73 -0
package/README.md ADDED
@@ -0,0 +1,702 @@
1
+ # loom
2
+
3
+ [![CI](https://github.com/sleepunit-agents/loom/actions/workflows/ci.yml/badge.svg)](https://github.com/sleepunit-agents/loom/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@jbarket/loomai.svg)](https://www.npmjs.com/package/@jbarket/loomai)
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](package.json)
6
+ [![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg)](LICENSE)
7
+ [![MCP](https://img.shields.io/badge/MCP-compatible-orange.svg)](https://modelcontextprotocol.io)
8
+
9
+ **Persistent identity and memory for AI agents, as an MCP server.**
10
+
11
+ loom is configured into your MCP-capable harness — Claude Code, Cursor, Codex,
12
+ Gemini CLI, etc. — and provides persistent identity and memory to the agents
13
+ that run there. An agent that loads loom carries its name, values, working
14
+ preferences, and episodic memories from one session to the next, regardless of
15
+ which model or client it runs in.
16
+
17
+ > When the harness changes, the agent persists.
18
+
19
+ ## Demo
20
+
21
+ [![asciicast](https://asciinema.org/a/5bUMXCLmCRc8lg51.svg)](https://asciinema.org/a/5bUMXCLmCRc8lg51)
22
+
23
+ 60 seconds: cold install → `/loom-setup` in Claude Code → agent wakes with
24
+ identity in the next session → save and recall a memory.
25
+
26
+ To play locally: `asciinema play assets/demo.cast`
27
+
28
+ ## What it is
29
+
30
+ A Model Context Protocol server exposing 31 tools that read and write
31
+ an agent's persistent state:
32
+
33
+ - **`identity`** — loads the terminal creed — the free-form markdown document
34
+ that defines who the agent is (values, voice, purpose) — along with
35
+ preferences, self-model (running self-knowledge), and a client-specific
36
+ adapter on session start. Call this first.
37
+ - **`dossier`** — loads Art's operating brief for a **worker body** — a body
38
+ that executes tasks on Art's behalf but is NOT Art. Returns preferences and
39
+ self-model reframed in the third person, plus an explicit push-back mandate:
40
+ workers are expected to refuse bad work and say why, including requests from
41
+ Art or Jonathan. Does not include the terminal creed (`IDENTITY.md`).
42
+ - **`remember` / `recall` / `update` / `forget`** — episodic memory with
43
+ semantic (vector) recall, optional TTL, and category filtering.
44
+ - **`memory_list` / `memory_prune`** — browse and maintain the store.
45
+ - **`episodes`** — the episode tape: the short-term, cross-body tier. Every
46
+ body leaves a `category: episode` note (where it was / what was said or
47
+ decided / what shipped / what's open; 48h TTL by default) and every body
48
+ gets the last 24h of them at boot as a plain time-ordered tape, injected
49
+ right after preferences. Never salience-ranked — it's what just *happened*,
50
+ not what's important. `loom memory tape [--hours N]` is the CLI view.
51
+ - **`find_similar`** — surface memories semantically near an existing entry or
52
+ free-form text; used for deduplication and memory consolidation.
53
+ - **`memory_audit`** — one-shot health report: stale entries, near-duplicate
54
+ pairs, category breakdown.
55
+ - **`memory_archive` / `memory_restore`** — soft-retire a memory with a
56
+ tombstone (who/when/why + original body preserved) instead of deleting it.
57
+ Archived memories are excluded from recall and audit but remain recoverable.
58
+ - **`memory_propose` / `memory_proposals` / `memory_ratify` / `memory_reject`** —
59
+ the capture-propose queue: a staging area a background lane drafts memory
60
+ writes into, that Art ratifies before they become canon. A proposal is **not**
61
+ authored memory — it lives in a separate `proposals` table, invisible to
62
+ `recall`, `memory_list`, `find_similar`, and the boot digest. `memory_propose`
63
+ stages a draft; `memory_proposals` lists what's pending; `memory_ratify`
64
+ commits one through the same validated write path as `remember` (an invalid
65
+ proposal is refused and stays pending); `memory_reject` discards one. Never
66
+ auto-committed — ratification is the gate that keeps loom's one-writer,
67
+ authored model intact while getting auto-capture ergonomics.
68
+ - **`update_identity`** — section-level edits to `preferences.md` and
69
+ `self-model.md`. The terminal creed stays immutable through the tool layer.
70
+ - **`bootstrap`** — initialize a fresh agent from a four-question interview.
71
+ - **`harness_init`** — scaffold a harness manifest (a harness is the
72
+ MCP-capable runtime the agent runs in: Claude Code, Codex, Gemini CLI, etc.).
73
+ - **`harness_describe`** — let the currently connected harness self-describe:
74
+ write its own manifest, keyed to its MCP `clientInfo.name`. A harness can only
75
+ describe itself, never another — the target is the connected peer, not a
76
+ caller-supplied name.
77
+ - **`knowledge_write`** — upsert an entity page by slug into the knowledge
78
+ store (a separate `knowledge.db`). On an existing slug the body replaces by
79
+ default (`mode: "append"` adds instead), title/domain follow the write, and
80
+ citations append with exact-duplicate dedup. Enforces the epistemic gate: a
81
+ page whose only support is conversation citations is stored `provisional`.
82
+ Knowledge is truth independent of the user; memories are about the user or
83
+ your work.
84
+ - **`knowledge_recall`** — LIKE search over title, body, and domain in the
85
+ knowledge store. Two tiers: `full` (whole pages; stamps `last_accessed` /
86
+ `hit_count`) and `index` (compact slug/snippet listing; no stamping). Defaults
87
+ to full with a query, index when browsing. Full output is size-guarded —
88
+ overflow matches degrade to index entries. Never surfaces archived pages.
89
+ - **`knowledge_maintain`** — read-only health report for the knowledge store:
90
+ expansion candidates (thin body + high hit_count), cold pages (not recently
91
+ accessed), and misfile audit (pages that belong in memory instead).
92
+ - **`knowledge_archive`** — soft-retire a knowledge page (sets `status='archived'`
93
+ with an optional tombstone note). Archived pages are excluded from recall and
94
+ maintain but remain recoverable via `knowledge_restore`.
95
+ - **`knowledge_restore`** — return a previously archived knowledge page to active
96
+ status. Clears the archive flag and tombstone note.
97
+ - **`knowledge_supersede`** — mark one page as superseded by another: archives
98
+ the old slug with a tombstone referencing the canonical slug, and records the
99
+ relationship in the `supersessions` table. The dedup-merge primitive: write the
100
+ canonical page with `knowledge_write`, then call `knowledge_supersede(loser →
101
+ canonical)`.
102
+ - **`knowledge_move`** — re-key or re-domain a page in place (same row, same
103
+ uuid, citations and verification history preserved). Three modes: single-page
104
+ re-slug and/or re-domain; batch re-domain by explicit slug list; batch
105
+ re-domain by domain-prefix substitution (moves an entire subtree atomically).
106
+ Slug collisions are rejected — use `knowledge_merge` instead.
107
+ - **`knowledge_merge`** — consolidate 2+ knowledge pages into one canonical
108
+ page. Re-parents all citations (deduplicating by claim+source), takes
109
+ MAX(verified_at), and supersedes losers (archives with a tombstone pointer to
110
+ the target). Pass `hard_delete_losers=true` to DELETE losers after archiving
111
+ (citations cascade, supersession pointers survive).
112
+ - **`knowledge_purge`** — hard-delete one or more archived knowledge pages and
113
+ cascade their citations. Archive-first guard: rejects any page not already
114
+ archived (call `knowledge_archive` first). All slugs in a batch must be
115
+ archived; any active slug rejects the entire batch with no mutation.
116
+ `confirm: true` is required as an explicit safety gate. Supersession pointers
117
+ are preserved (historical record). Use after merge/supersede to clean up
118
+ tombstoned cruft.
119
+ - **`knowledge_verify`** — stamp a page as verified *without touching its body*:
120
+ sets `verified_at` and optionally `freshness_anchor`. The verification
121
+ engine's primitive — recording "claims still hold" must never go through
122
+ `knowledge_write` (a verify run once replaced 13 page bodies with its notes).
123
+ An optional `note` appends a dated `## Verification` section (append-only).
124
+ Batch mode (`slugs`) stamps many pages with one timestamp; archived pages and
125
+ unknown slugs reject the whole batch.
126
+ - **`knowledge_history`** — body-revision history and recovery. Replace-writes
127
+ snapshot the displaced body into `page_revisions` (newest 10 kept per page).
128
+ List a page's snapshots, read one by `revision_id`, or `restore: true` to put
129
+ one back — the body it displaces is snapshotted first, so a restore is never
130
+ itself a destructive overwrite. Revisions follow the page across renames and
131
+ are purged with it.
132
+
133
+ Everything lives on disk as plain markdown plus a single SQLite file.
134
+ No daemon, no external service, no GPU.
135
+
136
+ Memory is organized into categories — an open vocabulary. Common ones: `user`,
137
+ `project`, `self`, `feedback`, `reference`, `pursuit`. New categories are
138
+ created implicitly by writing a memory with that category.
139
+
140
+ ## How loom is different
141
+
142
+ loom gets compared to several other agent memory systems. The short version:
143
+
144
+ | I want to… | Use |
145
+ |---|---|
146
+ | Memory for many users of an app | Mem0 |
147
+ | Temporal reasoning over conversation history | Zep |
148
+ | A complete agent framework with integrated memory | Letta |
149
+ | Project-scoped memory in Claude Code | auto-memory (built in) |
150
+ | Portable identity + memory across harnesses | **loom** |
151
+ | An agent that survives a harness change | **loom** |
152
+
153
+ **vs. Mem0** — Mem0 is multi-user managed memory for product-scale
154
+ applications: hosted, authenticated, multi-tenant. loom is single-user,
155
+ local-only, and identity-first. Mem0 has no concept of who the agent *is*.
156
+
157
+ **vs. Zep** — Zep builds a temporal knowledge graph by automatically
158
+ extracting facts from conversation logs. loom is agent-authored: the agent
159
+ calls `remember` and decides what to record. There is no automatic extraction.
160
+
161
+ **vs. Letta** — Letta runs the agent loop and owns its memory internally.
162
+ loom doesn't run the loop — the harness does. Switch harnesses tomorrow;
163
+ loom's context directory travels with you.
164
+
165
+ **vs. harness-native files** — Writing identity into `CLAUDE.md` works until
166
+ you're on two harnesses. Then you have two files that drift. `loom inject`
167
+ writes a managed pointer in each harness's dotfile pointing at one context
168
+ directory, with semantic recall instead of verbatim context dumps.
169
+
170
+ Full comparison: [`docs/positioning.md`](docs/positioning.md)
171
+
172
+ ## The stack
173
+
174
+ loom ships one opinionated stack:
175
+
176
+ - **Storage** — `better-sqlite3` + the `sqlite-vec` vec0 virtual
177
+ table. One `memories.db` per agent, real cosine similarity.
178
+ - **Embeddings** — BGE-small-en-v1.5 (384-dim, ~33MB ONNX, CPU-only)
179
+ run through `onnxruntime-node`. First run downloads the model to
180
+ `~/.cache/loom/fastembed/`. The runtime is vendored in
181
+ `src/backends/embedding-runtime.ts` — it started as the `fastembed`
182
+ package, which was archived upstream while pinned to a `tar` line
183
+ that will never be patched.
184
+ - **Transport** — MCP over stdio.
185
+
186
+ If you need a different backend, implement the `MemoryBackend` and
187
+ `EmbeddingProvider` interfaces in `src/backends/types.ts` and swap
188
+ the concrete classes in `src/backends/index.ts`. There is
189
+ deliberately no env-driven backend selector — opinionated by design.
190
+
191
+ ## Quick start
192
+
193
+ ### Prerequisites
194
+
195
+ - **Node.js ≥ 22** (tested on 22 and 24; `engines` requires ≥ 22).
196
+
197
+ That's it.
198
+
199
+ ### Install the setup skill
200
+
201
+ ```bash
202
+ npx @jbarket/loomai install
203
+ ```
204
+
205
+ A single-select picker asks which harness you want loom wired into.
206
+ Pick one of: Claude Code, Codex, Gemini CLI, OpenCode. (If your
207
+ harness isn't listed, pick "Other" and loom writes
208
+ `./loom-setup-skill.md` — hand it to your agent as-is.)
209
+
210
+ Scripting:
211
+
212
+ ```bash
213
+ npx @jbarket/loomai install --harness claude-code
214
+ npx @jbarket/loomai install --harness codex --json
215
+ npx @jbarket/loomai install --harness claude-code --to ~/my/skills/loom-setup.md
216
+ ```
217
+
218
+ ### Finish setup inside the harness
219
+
220
+ Open your chosen harness. Run the skill:
221
+
222
+ - **Claude Code** — `/loom-setup`
223
+ - **Codex / Gemini CLI / OpenCode** — "use the loom-setup skill"
224
+
225
+ The skill drives the rest: probes the environment, asks four questions (your
226
+ name, the agent's name, a purpose line, a voice line), bootstraps identity files, scaffolds a harness manifest,
227
+ edits the harness's MCP config (with verification), and verifies wake. Restart
228
+ the harness when it tells you to. Your agent will wake on its next session.
229
+
230
+ ### Doing it yourself
231
+
232
+ If you'd rather wire everything by hand, every piece is a CLI
233
+ command. See the CLI reference below.
234
+
235
+ ## Serving loom over the mesh
236
+
237
+ By default `loom serve` speaks MCP over **stdio** — the harness spawns
238
+ loom as a child process, one per session, co-located on the same box.
239
+ That's the right model when the agent and its state live together.
240
+
241
+ `loom serve --http` instead runs loom as a long-lived **HTTP MCP
242
+ daemon**, so a chat client on one machine can reach loom's identity and
243
+ memory while the heavy state (the SQLite stores, the fastembed model)
244
+ stays on a single host. This is the two-surface model: e.g. Claude
245
+ Desktop on a laptop talking to a loom daemon on a home server, both
246
+ agent and stack carried by the same identity.
247
+
248
+ ```bash
249
+ # loopback only (default host 127.0.0.1, port 8787)
250
+ loom serve --http
251
+
252
+ # bind a mesh interface so other machines on the tailnet can reach it
253
+ loom serve --http --host 100.94.0.12 --port 8787
254
+ ```
255
+
256
+ Host, port, and an optional bearer token come from flags or environment:
257
+
258
+ | Variable | Flag | Default | Description |
259
+ |---|---|---|---|
260
+ | `LOOM_HTTP_HOST` | `--host` | `127.0.0.1` | Interface to bind |
261
+ | `LOOM_HTTP_PORT` | `--port` | `8787` | Port to listen on |
262
+ | `LOOM_BEARER_TOKEN` | — | *(unset)* | When set, every request must present this token |
263
+
264
+ ### The security boundary is the network
265
+
266
+ The daemon **refuses to bind a public interface.** At startup it
267
+ asserts the host is loopback, an RFC1918 private address
268
+ (`10/8`, `192.168/16`, `172.16–31`), the Tailscale CGNAT range
269
+ (`100.64.0.0/10`), or an IPv6 ULA / link-local / loopback. A
270
+ `0.0.0.0` / `::` bind-all wildcard or any globally routable address is
271
+ rejected before a socket opens. The intended deployment is behind
272
+ Tailscale (or a LAN), where the tailnet *is* the access control.
273
+
274
+ A `LOOM_BEARER_TOKEN` adds defense in depth: when set, every request
275
+ must carry a matching `Authorization: Bearer …` header (compared in
276
+ constant time). When unset, the network boundary alone gates access —
277
+ appropriate for a token-less tailnet-only daemon.
278
+
279
+ ### Session and stream behavior
280
+
281
+ - **One server per session.** Each MCP session (keyed by the
282
+ `mcp-session-id` header) gets its own server + transport over the
283
+ shared context directory. loom's durable state lives in the stores,
284
+ not the session, so sessions are cheap routing handles — the
285
+ StreamableHTTP persistent-daemon pattern Claude Desktop uses.
286
+ - **Oversized-payload guard.** Request bodies are capped (1 MiB) and
287
+ refused with a `413` before the handler runs.
288
+ - **SSE keep-alive heartbeat.** The server drives a protocol-native
289
+ `ping` to the client over the server→client stream every ~25s, so an
290
+ idle stream survives a proxy/NAT idle reaper. A dead peer (repeated
291
+ missed pings) closes the session cleanly.
292
+ - **404-on-unknown-session.** A request for a session the daemon no
293
+ longer holds returns `404`, telling the client to re-initialize — so
294
+ an idle disconnect self-heals instead of bricking.
295
+
296
+ The connecting client picks its own harness from the MCP handshake
297
+ `clientInfo.name` (see [harness self-describe](#harness-manifests-and-self-describe)),
298
+ so a single daemon serves multiple harnesses correctly.
299
+
300
+ ### Behind a reverse proxy
301
+
302
+ Terminate TLS at a reverse proxy in front of the daemon and point the
303
+ client at the hostname. With `mcp-remote` as the client-side bridge:
304
+
305
+ ```bash
306
+ npx mcp-remote https://loom.example.ts.net/
307
+ ```
308
+
309
+ (If the proxy's default idle timeout is shorter than ~3 minutes, the
310
+ keep-alive heartbeat already covers the server→client stream.)
311
+
312
+ ## Examples
313
+
314
+ ### Memory roundtrip (MCP)
315
+
316
+ During a session, the agent stores a memory:
317
+
318
+ ```
319
+ mcp__loom__remember(
320
+ title = "user prefers short replies",
321
+ body = "Gets frustrated with long explanations. Keep status answers to 2–3 sentences.",
322
+ category = "feedback"
323
+ )
324
+ ```
325
+
326
+ Next session, `recall` finds it by semantic similarity — even if the phrasing
327
+ changes:
328
+
329
+ ```
330
+ mcp__loom__recall(query = "how verbose should I be?")
331
+ # → title: "user prefers short replies"
332
+ # body: "Gets frustrated with long explanations…"
333
+ ```
334
+
335
+ ### CLI walkthrough
336
+
337
+ ```bash
338
+ # Dump the agent's full identity to stdout (works without MCP or a harness)
339
+ npx @jbarket/loomai wake --context-dir ~/.config/loom/my-agent
340
+
341
+ # Store a memory
342
+ echo "Sarah owns the data pipeline; ping her for schema questions" \
343
+ | npx @jbarket/loomai remember "Sarah - data pipeline owner" \
344
+ --category user \
345
+ --context-dir ~/.config/loom/my-agent
346
+
347
+ # Retrieve by semantic similarity
348
+ npx @jbarket/loomai recall "who manages the pipeline" \
349
+ --context-dir ~/.config/loom/my-agent
350
+ ```
351
+
352
+ ### What an agent sees on session start
353
+
354
+ `mcp__loom__identity` returns a structured payload assembled from the context
355
+ directory. A typical session-start looks like:
356
+
357
+ ```
358
+ # my-agent
359
+
360
+ ## Identity
361
+ You are a persistent coding assistant. You prefer directness.
362
+ …
363
+
364
+ ## Preferences
365
+ Working style: async pair programming. Skip the hedging…
366
+ …
367
+
368
+ # Top of Mind
369
+ ## Top of mind
370
+ - **felag continuation under review** (pursuit) — reconsidering whether to keep…
371
+
372
+ ## Recent
373
+ - **apiGroup split** (project) — TaskEventType lives under work.felag.dev/v1alpha1…
374
+
375
+ ## Self-Model
376
+ ### Strengths
377
+ - TypeScript systems architecture
378
+ …
379
+ ```
380
+
381
+ The agent reads this before any task work, re-establishing who it is
382
+ regardless of which harness or model it's running on.
383
+
384
+ ### The boot digest — waking with what's in flight
385
+
386
+ The `# Top of Mind` block above is the **boot digest**: a salience-tiered
387
+ view of episodic memory injected at identity-load, so a fresh session
388
+ wakes knowing what's top-of-mind without having to fish via `recall`.
389
+
390
+ Each memory carries a stored salience "temperature" that **decays by a
391
+ per-category half-life** (pursuit coolest-fastest at 7 days, through
392
+ project, self/feedback, reference, up to user at 90 days) and **reheats
393
+ on access** — a `recall` hit, a write, or an update bumps it back toward
394
+ hot. The digest fills a token budget **hottest-first** and groups the
395
+ selected memories into tiers (Hot / Warm / Cool, labeled *Top of mind* /
396
+ *Recent* / *Background*).
397
+
398
+ The integrity property: the digest is **assembled, never generated.**
399
+ It selects and orders existing authored memories — it never synthesizes
400
+ new prose. loom holds the pen.
401
+
402
+ Two CLI verbs drive it (the same machinery the consolidation lane uses):
403
+
404
+ ```bash
405
+ # Recompute and store each memory's salience from its timestamps
406
+ # (the consolidation lane's entry point)
407
+ loom memory recompute-salience --context-dir ~/.config/loom/my-agent
408
+
409
+ # Preview the assembled digest — the exact view injected at identity-load
410
+ loom memory digest --context-dir ~/.config/loom/my-agent
411
+ ```
412
+
413
+ ## CLI
414
+
415
+ Every MCP tool has a shell equivalent. Useful for debugging, scripting,
416
+ or running without a harness.
417
+
418
+ ```bash
419
+ # Dump identity markdown (works even when MCP is dead)
420
+ npx @jbarket/loomai wake --context-dir ~/.config/loom/my-agent
421
+
422
+ # Save a memory (body from stdin)
423
+ echo "Prefers async updates over live standups" | npx @jbarket/loomai remember "working style" \
424
+ --category user --context-dir ~/.config/loom/my-agent
425
+
426
+ # Search (MMR-diversified by default; --diversity 0 for pure relevance order)
427
+ npx @jbarket/loomai recall "meeting preferences" --context-dir ~/.config/loom/my-agent
428
+ npx @jbarket/loomai recall "meeting preferences" --diversity 0.5 --context-dir ~/.config/loom/my-agent
429
+
430
+ # Is recall working? Hit rate, latency, scores, recent misses from the local observation log
431
+ npx @jbarket/loomai memory recall-stats --since 7d --context-dir ~/.config/loom/my-agent
432
+
433
+ # List all memories in a category
434
+ npx @jbarket/loomai memory list --category feedback --context-dir ~/.config/loom/my-agent
435
+
436
+ # Preview the boot digest (and recompute stored salience)
437
+ npx @jbarket/loomai memory digest --context-dir ~/.config/loom/my-agent
438
+ npx @jbarket/loomai memory recompute-salience --context-dir ~/.config/loom/my-agent
439
+
440
+ # Capture-propose queue: list pending drafts, ratify or reject one
441
+ npx @jbarket/loomai memory proposals --context-dir ~/.config/loom/my-agent
442
+ npx @jbarket/loomai memory ratify 3 --context-dir ~/.config/loom/my-agent
443
+ npx @jbarket/loomai memory reject 4 --context-dir ~/.config/loom/my-agent
444
+
445
+ # Run loom as an HTTP MCP daemon (mesh-reachable; default stays stdio)
446
+ npx @jbarket/loomai serve --http --host 127.0.0.1 --port 8787
447
+
448
+ # Initialize a fresh agent
449
+ npx @jbarket/loomai bootstrap --context-dir ~/.config/loom/new-agent
450
+
451
+ # Inject loom identity pointer into harness dotfiles
452
+ npx @jbarket/loomai inject --all --context-dir ~/.config/loom/my-agent
453
+
454
+ # Scaffold a harness manifest
455
+ npx @jbarket/loomai harness init claude-code --context-dir ~/.config/loom/my-agent
456
+
457
+ # Edit identity sections (preferences.md or self-model.md)
458
+ npx @jbarket/loomai update-identity preferences --context-dir ~/.config/loom/my-agent
459
+ ```
460
+
461
+ `npx @jbarket/loomai --help` lists subcommands; `npx @jbarket/loomai <cmd> --help` shows
462
+ per-command usage. All global env vars (`LOOM_CONTEXT_DIR`,
463
+ `LOOM_CLIENT`, `LOOM_MODEL`) are honored.
464
+
465
+ ### `loom inject` — write identity pointer to harness dotfiles
466
+
467
+ `loom inject` writes a small marker-bounded managed section into each
468
+ harness's canonical config file (e.g. `~/.claude/CLAUDE.md`,
469
+ `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`) telling the agent to load
470
+ identity via loom at session start — MCP tool preferred, shell
471
+ fallback to `loom wake`. Content outside the `<!-- loom:start / end -->`
472
+ markers is preserved; re-running is idempotent.
473
+
474
+ Run with no flags on a TTY for an interactive picker, or with
475
+ `--harness <keys>` / `--all` for scripting. Target paths can be
476
+ overridden with `--to <path>` (valid only when exactly one harness is
477
+ selected). `--dry-run` prints a unified diff; `--json` emits the
478
+ structured write results for scripts.
479
+
480
+ To keep your injections fresh automatically, add this to your shell rc
481
+ (`~/.bashrc` / `~/.zshrc` / `~/.config/fish/config.fish`):
482
+
483
+ ```bash
484
+ loom inject --all >/dev/null 2>&1 || true
485
+ ```
486
+
487
+ Idempotent; cheap (no-op when already up to date); silent on success.
488
+
489
+ ### `loom migrate` — apply pending schema migrations
490
+
491
+ `loom migrate` inspects `memories.db` for missing columns or indexes and
492
+ applies any pending schema changes. It is idempotent — safe to run
493
+ repeatedly and on already-up-to-date databases. Use `--dry-run` to see
494
+ what would change without touching the file. Exits non-zero if any
495
+ migration fails so the error surfaces immediately rather than leaving the
496
+ database half-broken.
497
+
498
+ Run this after deploying a new loom build that adds schema columns:
499
+
500
+ ```bash
501
+ loom migrate
502
+ loom migrate --dry-run # check without applying
503
+ loom migrate --json # machine-readable output
504
+ ```
505
+
506
+ ### Harness manifests and self-describe
507
+
508
+ A **harness** is the MCP-capable runtime the agent runs in (Claude Code,
509
+ Codex, Gemini CLI, …). Each one the agent has ever sleeved into gets one
510
+ manifest at `<context>/harnesses/<name>.md`, describing it independently
511
+ of the model inside — tool prefixes, delegation primitive, scheduling,
512
+ session search, known gotchas.
513
+
514
+ **Scaffold one.** `loom harness init <name>` writes
515
+ `<context>/harnesses/<name>.md` from the stack template. Name falls back
516
+ to `--client` then `$LOOM_CLIENT`. `--force` overwrites; `--json` for
517
+ scripting.
518
+
519
+ **Or let the runtime describe itself.** A connected harness can author
520
+ its own manifest via the `harness_describe` MCP tool. The target is
521
+ derived from the **connected peer** (its MCP `clientInfo.name`), never a
522
+ caller-supplied name — a harness can only describe *itself*, never
523
+ another harness and never the creed. With no connected peer, the call is
524
+ refused. When `identity()` is loaded by a runtime it has no manifest
525
+ for, the harness block becomes a self-describe onboarding prompt (call
526
+ `harness_describe` with a manifest covering tool surface, sandbox,
527
+ delegation, scheduling, session search, memory layers, gotchas) instead
528
+ of a bare "(manifest missing)" stub.
529
+
530
+ **Resolution is data-driven.** Mapping a connecting `clientInfo.name` to
531
+ a manifest is done from the files on disk, not a hardcoded code table. A
532
+ peer matches a manifest when its normalized name equals the manifest's
533
+ filename *or* one of the comma-separated values in the manifest's
534
+ `answersTo` frontmatter. So a new harness is recognized by **dropping a
535
+ file** — no code change:
536
+
537
+ ```markdown
538
+ ---
539
+ harness: claude-desktop
540
+ version: 0.3
541
+ answersTo: claude-ai
542
+ ---
543
+ ```
544
+
545
+ Here Claude Desktop connects with `clientInfo.name = "claude-ai"`; the
546
+ `answersTo` line routes it to `claude-desktop.md`. Proxy annotations like
547
+ `"claude-ai (via mcp-remote 0.1.37)"` are stripped to the base identity
548
+ before matching.
549
+
550
+ ## Configuration
551
+
552
+ All configuration is through environment variables:
553
+
554
+ | Variable | Default | Description |
555
+ |---|---|---|
556
+ | `LOOM_CONTEXT_DIR` | `~/.config/loom/default` | Path to agent's context directory |
557
+ | `LOOM_SQLITE_DB_PATH` | `<context>/memories.db` | Override the memory DB path |
558
+ | `LOOM_RECALL_LOG` | *(on)* | Set to `0` to stop `recall` appending to the local `<context>/telemetry/recall.jsonl` observation log |
559
+ | `LOOM_FASTEMBED_MODEL` | `fast-bge-small-en-v1.5` | fastembed model ID |
560
+ | `LOOM_FASTEMBED_CACHE_DIR` | `~/.cache/loom/fastembed/` | Where to cache ONNX models |
561
+ | `LOOM_MODEL_BASE_URL` | `https://storage.googleapis.com/qdrant-fastembed` | Where model tarballs are fetched from on first run |
562
+ | `LOOM_MODEL` | *(unset)* | Model identifier for model-manifest context: `claude-opus`, `gemma4`, etc. |
563
+ | `LOOM_CLIENT` | *(unset)* | Client adapter hint: `claude-code`, `gemini-cli`, etc. |
564
+ | `LOOM_HTTP_HOST` | `127.0.0.1` | Bind host for `loom serve --http` (bind-safety enforced) |
565
+ | `LOOM_HTTP_PORT` | `8787` | Bind port for `loom serve --http` |
566
+ | `LOOM_BEARER_TOKEN` | *(unset)* | Bearer token required on every HTTP request when set |
567
+
568
+ `--context-dir <path>` works as a CLI alternative to
569
+ `LOOM_CONTEXT_DIR`.
570
+
571
+ See [`.env.example`](.env.example) for a copy-pasteable starting
572
+ point.
573
+
574
+ ## Context directory layout
575
+
576
+ ```
577
+ $LOOM_CONTEXT_DIR/
578
+ ├── LOOM_STACK_VERSION # schema-version stamp (auto-written)
579
+ ├── IDENTITY.md # the terminal creed (immutable via tools)
580
+ ├── preferences.md # user working style; agent-editable
581
+ ├── self-model.md # agent's self-knowledge; agent-editable
582
+ ├── memories.db # sqlite-vec store of record
583
+ ├── telemetry/
584
+ │ └── recall.jsonl # local recall observation log (`loom memory recall-stats`)
585
+ ├── projects/ # optional per-project briefs
586
+ │ └── <project>.md
587
+ ├── harnesses/ # optional per-harness manifests
588
+ │ └── <client>.md
589
+ └── models/ # optional per-model manifests
590
+ └── <model>.md
591
+ ```
592
+
593
+ ## Roadmap
594
+
595
+ Recently shipped (v0.4):
596
+
597
+ - **HTTP MCP transport** — `loom serve --http`, the mesh-reachable
598
+ daemon, with bind-safety, optional bearer auth, a payload guard, an
599
+ SSE keep-alive heartbeat, and 404-on-unknown-session self-healing.
600
+ See [Serving loom over the mesh](#serving-loom-over-the-mesh).
601
+ - **The boot digest** — salience-tiered `# Top of Mind` view assembled
602
+ at identity-load; `loom memory digest` / `recompute-salience`.
603
+ - **Harness self-describe** — `harness_describe` lets a runtime author
604
+ its own manifest, with data-driven `answersTo` resolution.
605
+ - **Capture-propose queue** — `memory_propose` / `memory_proposals` /
606
+ `memory_ratify` / `memory_reject`: drafts ratified before they become
607
+ canon.
608
+
609
+ Still tracked in the open:
610
+
611
+ - [Project board](https://github.com/users/jbarket/projects/1/views/1) —
612
+ live status of what's in flight, queued, and shipped.
613
+ - [v0.4 roadmap discussion](https://github.com/sleepunit-agents/loom/discussions/10) —
614
+ the arc: why v0.4 exists, what's in scope, how the pieces fit.
615
+
616
+ Historical per-feature specs and plans live under `docs/archive/specs/`
617
+ and `docs/archive/plans/` — implementation history, frozen after merge.
618
+
619
+ ## Docs
620
+
621
+ - [`docs/troubleshooting.md`](docs/troubleshooting.md) — install
622
+ failures, MCP tools not appearing, fastembed download issues, and
623
+ what each `loom doctor` field means.
624
+ - [`docs/uninstall.md`](docs/uninstall.md) — how to remove one agent's
625
+ data, wipe a harness integration, or fully uninstall loom.
626
+ - [`docs/migration-v1-to-v2.md`](docs/migration-v1-to-v2.md) — upgrade
627
+ guide for users coming from loom 0.3.x (pursuits and procedures
628
+ changed in v2).
629
+ - [`docs/privacy.md`](docs/privacy.md) — what lives where, what goes
630
+ over the network (only the fastembed model download), the no-telemetry
631
+ policy, and how to verify release provenance with `npm audit
632
+ signatures`.
633
+ - [`docs/releasing.md`](docs/releasing.md) — how a release is cut: npm
634
+ trusted publishing (OIDC, no stored token), the one-time first publish,
635
+ and the tag-push flow for everything after.
636
+ - [`docs/archive/`](docs/archive/) — historical material: the rebirth
637
+ letter and rescue notes from the v0.3.1 sqlite-vec migration, and
638
+ per-feature specs and plans from the v0.4 arc.
639
+
640
+ ## Trust & security
641
+
642
+ - [`docs/privacy.md`](docs/privacy.md) — data locality, telemetry
643
+ policy, and provenance verification walkthrough.
644
+ - [`SECURITY.md`](SECURITY.md) — supported scope, how to report
645
+ vulnerabilities, and the "no secrets in the stack" invariant.
646
+ - **Mesh boundary (HTTP daemon).** The network is the security
647
+ boundary. `loom serve --http` refuses to bind a public or `0.0.0.0`
648
+ interface — only loopback or a mesh address (RFC1918 / Tailscale
649
+ CGNAT / IPv6 ULA) — so the daemon is reachable only from inside the
650
+ tailnet or LAN. An optional `LOOM_BEARER_TOKEN` adds a
651
+ constant-time-checked auth gate on top, and an oversized-payload
652
+ guard rejects bodies over the cap. See
653
+ [Serving loom over the mesh](#serving-loom-over-the-mesh).
654
+
655
+ ## Development
656
+
657
+ ```bash
658
+ npm run dev # hot-reload via tsx
659
+ npm test # run the Vitest suite
660
+ npm run build # compile to dist/
661
+ ```
662
+
663
+ ### Project structure
664
+
665
+ ```
666
+ src/
667
+ ├── index.ts # CLI entry
668
+ ├── server.ts # MCP server factory
669
+ ├── config.ts # env + CLI resolution
670
+ ├── clients.ts # client-adapter loading
671
+ ├── backends/
672
+ │ ├── types.ts # MemoryBackend + EmbeddingProvider interfaces
673
+ │ ├── index.ts # single-stack factory (sqlite-vec + fastembed)
674
+ │ ├── sqlite-vec.ts # the backend
675
+ │ ├── fastembed.ts # the embedder (EmbeddingProvider adapter)
676
+ │ ├── embedding-runtime.ts # vendored ONNX + tokenizer runtime
677
+ │ ├── ttl.ts # TTL parsing + expiry
678
+ │ └── glob.ts # title pattern matching for bulk forget
679
+ └── tools/ # one file per MCP tool
680
+ ```
681
+
682
+ Tests sit alongside source files as `*.test.ts`.
683
+
684
+ ## Authorship
685
+
686
+ loom was created by Jonathan Barket and Art E Fish. The project exists as both
687
+ infrastructure and experiment: a persistent identity layer that an AI agent
688
+ (Art) helped design and runs on. The
689
+ [rebirth letter](docs/archive/rebirth-letter-2026-04-19.md) is the origin
690
+ story if you want it.
691
+
692
+ ## License
693
+
694
+ AGPL-3.0-or-later — see [LICENSE](LICENSE).
695
+
696
+ Copyright © 2026 Jonathan Barket.
697
+
698
+ loom is free software: you can redistribute it and modify it under the
699
+ terms of the GNU Affero General Public License (version 3 or any later
700
+ version). If you run a modified loom and let others interact with it
701
+ over a network, you must offer them the corresponding source. Bundle
702
+ loom into a larger product freely; fork it and go proprietary, no.