@clauderecallhq/cli 0.12.5 → 0.61.2

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 (218) hide show
  1. package/LICENSE +37 -17
  2. package/README.md +110 -22
  3. package/dist/cli.js +1641 -353
  4. package/dist/daemon/entrypoint.js +1872 -54
  5. package/dist/mcp-server.js +930 -0
  6. package/dist/share/fonts/Inter-Bold.woff +0 -0
  7. package/dist/share/fonts/Inter-Regular.woff +0 -0
  8. package/dist/share/fonts/JetBrainsMono-Regular.woff +0 -0
  9. package/dist/web/assets/_brand-Bw9uSB4O.js +1 -0
  10. package/dist/web/assets/captureNode-9CVj9vYC.js +2 -0
  11. package/dist/web/assets/card-a-minimal-ujNERzX7.js +1 -0
  12. package/dist/web/assets/card-b-terminal-DpJ_tVpg.js +1 -0
  13. package/dist/web/assets/card-c-gradient-CZXVGuNd.js +1 -0
  14. package/dist/web/assets/card-d-dashboard-CHKD-PnB.js +1 -0
  15. package/dist/web/assets/dist-CWaokT35.js +56 -0
  16. package/dist/web/assets/index-B-HrjaDy.css +1 -0
  17. package/dist/web/assets/index-BZYcD76T.js +633 -0
  18. package/dist/web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  19. package/dist/web/assets/patterns-BPeZb9N0.js +1 -0
  20. package/dist/web/assets/stats-BSwqSiFU.js +1 -0
  21. package/dist/web/assets/thread-D2AXyhOx.js +1 -0
  22. package/dist/web/index.html +8 -2
  23. package/package.json +56 -16
  24. package/scripts/postinstall.mjs +38 -0
  25. package/dist/cli.js.map +0 -1
  26. package/dist/commands/activate.js +0 -69
  27. package/dist/commands/activate.js.map +0 -1
  28. package/dist/commands/audit-secrets.js +0 -103
  29. package/dist/commands/audit-secrets.js.map +0 -1
  30. package/dist/commands/blame.js +0 -35
  31. package/dist/commands/blame.js.map +0 -1
  32. package/dist/commands/config-verification.js +0 -18
  33. package/dist/commands/config-verification.js.map +0 -1
  34. package/dist/commands/context.js +0 -144
  35. package/dist/commands/context.js.map +0 -1
  36. package/dist/commands/correlate.js +0 -70
  37. package/dist/commands/correlate.js.map +0 -1
  38. package/dist/commands/digest.js +0 -78
  39. package/dist/commands/digest.js.map +0 -1
  40. package/dist/commands/health.js +0 -62
  41. package/dist/commands/health.js.map +0 -1
  42. package/dist/commands/index.js +0 -247
  43. package/dist/commands/index.js.map +0 -1
  44. package/dist/commands/install-extension.js +0 -138
  45. package/dist/commands/install-extension.js.map +0 -1
  46. package/dist/commands/license.js +0 -39
  47. package/dist/commands/license.js.map +0 -1
  48. package/dist/commands/list.js +0 -47
  49. package/dist/commands/list.js.map +0 -1
  50. package/dist/commands/mcp.js +0 -29
  51. package/dist/commands/mcp.js.map +0 -1
  52. package/dist/commands/open.js +0 -28
  53. package/dist/commands/open.js.map +0 -1
  54. package/dist/commands/paste.js +0 -154
  55. package/dist/commands/paste.js.map +0 -1
  56. package/dist/commands/projects.js +0 -36
  57. package/dist/commands/projects.js.map +0 -1
  58. package/dist/commands/search.js +0 -67
  59. package/dist/commands/search.js.map +0 -1
  60. package/dist/commands/semantic.js +0 -173
  61. package/dist/commands/semantic.js.map +0 -1
  62. package/dist/commands/show.js +0 -121
  63. package/dist/commands/show.js.map +0 -1
  64. package/dist/commands/start.js +0 -47
  65. package/dist/commands/start.js.map +0 -1
  66. package/dist/commands/stats.js +0 -133
  67. package/dist/commands/stats.js.map +0 -1
  68. package/dist/commands/status.js +0 -45
  69. package/dist/commands/status.js.map +0 -1
  70. package/dist/commands/stop.js +0 -29
  71. package/dist/commands/stop.js.map +0 -1
  72. package/dist/commands/thread.js +0 -396
  73. package/dist/commands/thread.js.map +0 -1
  74. package/dist/context/formatter.js +0 -103
  75. package/dist/context/formatter.js.map +0 -1
  76. package/dist/daemon/auto-tag-config.js +0 -103
  77. package/dist/daemon/auto-tag-config.js.map +0 -1
  78. package/dist/daemon/auto-tag-config.test.js +0 -72
  79. package/dist/daemon/auto-tag-config.test.js.map +0 -1
  80. package/dist/daemon/auto-title-config.js +0 -70
  81. package/dist/daemon/auto-title-config.js.map +0 -1
  82. package/dist/daemon/bulk-title-jobs.js +0 -170
  83. package/dist/daemon/bulk-title-jobs.js.map +0 -1
  84. package/dist/daemon/correlator.js +0 -320
  85. package/dist/daemon/correlator.js.map +0 -1
  86. package/dist/daemon/discover.js +0 -316
  87. package/dist/daemon/discover.js.map +0 -1
  88. package/dist/daemon/editor-detection.js +0 -186
  89. package/dist/daemon/editor-detection.js.map +0 -1
  90. package/dist/daemon/entrypoint.js.map +0 -1
  91. package/dist/daemon/git-correlator.js +0 -256
  92. package/dist/daemon/git-correlator.js.map +0 -1
  93. package/dist/daemon/mcp-installer.js +0 -108
  94. package/dist/daemon/mcp-installer.js.map +0 -1
  95. package/dist/daemon/onboarding-state.js +0 -140
  96. package/dist/daemon/onboarding-state.js.map +0 -1
  97. package/dist/daemon/pidfile.js +0 -57
  98. package/dist/daemon/pidfile.js.map +0 -1
  99. package/dist/daemon/ports.js +0 -48
  100. package/dist/daemon/ports.js.map +0 -1
  101. package/dist/daemon/scanProgressRegistry.js +0 -62
  102. package/dist/daemon/scanProgressRegistry.js.map +0 -1
  103. package/dist/daemon/server.js +0 -2010
  104. package/dist/daemon/server.js.map +0 -1
  105. package/dist/daemon/tag-scanner/anthropic-client.js +0 -40
  106. package/dist/daemon/tag-scanner/anthropic-client.js.map +0 -1
  107. package/dist/daemon/tag-scanner/autopilot.js +0 -131
  108. package/dist/daemon/tag-scanner/autopilot.js.map +0 -1
  109. package/dist/daemon/tag-scanner/claude-cli-driver.js +0 -250
  110. package/dist/daemon/tag-scanner/claude-cli-driver.js.map +0 -1
  111. package/dist/daemon/tag-scanner/orchestrator.js +0 -88
  112. package/dist/daemon/tag-scanner/orchestrator.js.map +0 -1
  113. package/dist/daemon/tag-scanner/prompt.js +0 -46
  114. package/dist/daemon/tag-scanner/prompt.js.map +0 -1
  115. package/dist/daemon/tag-scanner/prompt.test.js +0 -48
  116. package/dist/daemon/tag-scanner/prompt.test.js.map +0 -1
  117. package/dist/daemon/tag-scanner/scan-state.js +0 -49
  118. package/dist/daemon/tag-scanner/scan-state.js.map +0 -1
  119. package/dist/daemon/tag-scanner/session-fetcher.js +0 -82
  120. package/dist/daemon/tag-scanner/session-fetcher.js.map +0 -1
  121. package/dist/daemon/tag-scanner/session-fetcher.test.js +0 -34
  122. package/dist/daemon/tag-scanner/session-fetcher.test.js.map +0 -1
  123. package/dist/daemon/tag-scanner/validator.js +0 -50
  124. package/dist/daemon/tag-scanner/validator.js.map +0 -1
  125. package/dist/daemon/tag-scanner/validator.test.js +0 -41
  126. package/dist/daemon/tag-scanner/validator.test.js.map +0 -1
  127. package/dist/daemon/terminal-registry.js +0 -443
  128. package/dist/daemon/terminal-registry.js.map +0 -1
  129. package/dist/daemon/ui.js +0 -64
  130. package/dist/daemon/ui.js.map +0 -1
  131. package/dist/daemon/watcher.js +0 -256
  132. package/dist/daemon/watcher.js.map +0 -1
  133. package/dist/db/client.js +0 -22
  134. package/dist/db/client.js.map +0 -1
  135. package/dist/db/schema.js +0 -496
  136. package/dist/db/schema.js.map +0 -1
  137. package/dist/license/api-base.js +0 -13
  138. package/dist/license/api-base.js.map +0 -1
  139. package/dist/license/manager.js +0 -43
  140. package/dist/license/manager.js.map +0 -1
  141. package/dist/license/public-key.js +0 -19
  142. package/dist/license/public-key.js.map +0 -1
  143. package/dist/license/storage.js +0 -27
  144. package/dist/license/storage.js.map +0 -1
  145. package/dist/license/verify.js +0 -23
  146. package/dist/license/verify.js.map +0 -1
  147. package/dist/mcp/audit.js +0 -126
  148. package/dist/mcp/audit.js.map +0 -1
  149. package/dist/mcp/prompts.js +0 -180
  150. package/dist/mcp/prompts.js.map +0 -1
  151. package/dist/mcp/server.js +0 -502
  152. package/dist/mcp/server.js.map +0 -1
  153. package/dist/mcp/thread-tools.js +0 -363
  154. package/dist/mcp/thread-tools.js.map +0 -1
  155. package/dist/mcp/write-tools.js +0 -239
  156. package/dist/mcp/write-tools.js.map +0 -1
  157. package/dist/parser/jsonl.js +0 -150
  158. package/dist/parser/jsonl.js.map +0 -1
  159. package/dist/semantic/chunker.js +0 -47
  160. package/dist/semantic/chunker.js.map +0 -1
  161. package/dist/semantic/config.js +0 -74
  162. package/dist/semantic/config.js.map +0 -1
  163. package/dist/semantic/embedder.js +0 -54
  164. package/dist/semantic/embedder.js.map +0 -1
  165. package/dist/semantic/fusion.js +0 -38
  166. package/dist/semantic/fusion.js.map +0 -1
  167. package/dist/semantic/model-download.js +0 -69
  168. package/dist/semantic/model-download.js.map +0 -1
  169. package/dist/semantic/pipeline.js +0 -375
  170. package/dist/semantic/pipeline.js.map +0 -1
  171. package/dist/semantic/query.js +0 -42
  172. package/dist/semantic/query.js.map +0 -1
  173. package/dist/semantic/worker.js +0 -78
  174. package/dist/semantic/worker.js.map +0 -1
  175. package/dist/stats/backfill.js +0 -151
  176. package/dist/stats/backfill.js.map +0 -1
  177. package/dist/stats/health.js +0 -102
  178. package/dist/stats/health.js.map +0 -1
  179. package/dist/stats/query.js +0 -385
  180. package/dist/stats/query.js.map +0 -1
  181. package/dist/utils/aliases.js +0 -107
  182. package/dist/utils/aliases.js.map +0 -1
  183. package/dist/utils/autoCollections.js +0 -635
  184. package/dist/utils/autoCollections.js.map +0 -1
  185. package/dist/utils/autoTitle.js +0 -348
  186. package/dist/utils/autoTitle.js.map +0 -1
  187. package/dist/utils/collections.js +0 -446
  188. package/dist/utils/collections.js.map +0 -1
  189. package/dist/utils/format.js +0 -46
  190. package/dist/utils/format.js.map +0 -1
  191. package/dist/utils/notes.js +0 -270
  192. package/dist/utils/notes.js.map +0 -1
  193. package/dist/utils/paths.js +0 -50
  194. package/dist/utils/paths.js.map +0 -1
  195. package/dist/utils/pricing.js +0 -257
  196. package/dist/utils/pricing.js.map +0 -1
  197. package/dist/utils/secret-scanner.js +0 -166
  198. package/dist/utils/secret-scanner.js.map +0 -1
  199. package/dist/utils/sessionLabel.js +0 -64
  200. package/dist/utils/sessionLabel.js.map +0 -1
  201. package/dist/utils/tags.js +0 -97
  202. package/dist/utils/tags.js.map +0 -1
  203. package/dist/utils/thread-context.js +0 -129
  204. package/dist/utils/thread-context.js.map +0 -1
  205. package/dist/utils/threadFilter.js +0 -18
  206. package/dist/utils/threadFilter.js.map +0 -1
  207. package/dist/utils/threads-titler.js +0 -298
  208. package/dist/utils/threads-titler.js.map +0 -1
  209. package/dist/utils/threads.js +0 -383
  210. package/dist/utils/threads.js.map +0 -1
  211. package/dist/utils/usage.js +0 -76
  212. package/dist/utils/usage.js.map +0 -1
  213. package/dist/verification/compute.js +0 -88
  214. package/dist/verification/compute.js.map +0 -1
  215. package/dist/verification/config.js +0 -34
  216. package/dist/verification/config.js.map +0 -1
  217. package/dist/web/assets/index-CIr6J4Fw.js +0 -1201
  218. package/dist/web/assets/index-Ctc8g9Jw.css +0 -1
@@ -0,0 +1,930 @@
1
+ #!/usr/bin/env node
2
+ /* Claude Recall (proprietary). See LICENSE for terms. */
3
+ var gn=Object.defineProperty;var H=(e,s)=>()=>(e&&(s=e(e=0)),s);var Ie=(e,s)=>{for(var t in s)gn(e,t,{get:s[t],enumerable:!0})};import{homedir as rt}from"node:os";import{join as xe,basename as xo}from"node:path";import{existsSync as _n,mkdirSync as fn,chmodSync as hn,readdirSync as vo,statSync as Do}from"node:fs";function x(){_n(S)||fn(S,{recursive:!0,mode:448}),process.platform!=="win32"&&hn(S,448)}var ko,S,oe,F=H(()=>{"use strict";ko=xe(rt(),".claude","projects"),S=process.env.RECALL_HOME?process.env.RECALL_HOME:xe(rt(),".recall"),oe=xe(S,"db.sqlite")});function at(e){let s=e.prepare("PRAGMA table_info(sessions)").all(),t=new Set(s.map(g=>g.name)),n=[["total_input_tokens","INTEGER"],["total_output_tokens","INTEGER"],["total_cache_create_tokens","INTEGER"],["total_cache_read_tokens","INTEGER"],["primary_model","TEXT"],["auto_title","TEXT"],["auto_title_source","TEXT"],["auto_title_generated_at","INTEGER"],["auto_title_history","TEXT"],["verification_status","TEXT"],["verification_computed_at","INTEGER"],["title_quality","TEXT"],["title_quality_computed_at","INTEGER"]];for(let[g,_]of n)t.has(g)||e.exec(`ALTER TABLE sessions ADD COLUMN ${g} ${_}`);let i=e.prepare("PRAGMA table_info(collection_sessions)").all(),r=new Set(i.map(g=>g.name)),a=[["source","TEXT NOT NULL DEFAULT 'manual'"],["rule_id","TEXT"]];for(let[g,_]of a)r.has(g)||e.exec(`ALTER TABLE collection_sessions ADD COLUMN ${g} ${_}`);let o=e.prepare("PRAGMA table_info(session_notes)").all(),c=new Set(o.map(g=>g.name)),l=[["auto_synopsis","TEXT"],["auto_synopsis_generated_at","INTEGER"],["auto_synopsis_history","TEXT"]];for(let[g,_]of l)c.has(g)||e.exec(`ALTER TABLE session_notes ADD COLUMN ${g} ${_}`);let d=e.prepare("PRAGMA table_info(threads)").all();new Set(d.map(g=>g.name)).has("folder_id")||(e.exec("ALTER TABLE threads ADD COLUMN folder_id TEXT REFERENCES thread_folders(id) ON DELETE SET NULL"),e.exec("CREATE INDEX IF NOT EXISTS idx_threads_folder ON threads(folder_id)"));let u=e.prepare("PRAGMA table_info(thread_folders)").all(),m=new Set(u.map(g=>g.name));m.has("project_scope")||(e.exec("ALTER TABLE thread_folders ADD COLUMN project_scope TEXT"),e.exec("CREATE INDEX IF NOT EXISTS idx_thread_folders_project ON thread_folders(project_scope)")),m.has("sort_order")||e.exec("ALTER TABLE thread_folders ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"),e.exec(`
4
+ CREATE TABLE IF NOT EXISTS message_embeddings (
5
+ message_uuid TEXT PRIMARY KEY REFERENCES messages(uuid) ON DELETE CASCADE,
6
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
7
+ embedding BLOB NOT NULL,
8
+ embedding_model_id TEXT NOT NULL DEFAULT 'bge-base-en-v1.5',
9
+ embedding_dim INTEGER NOT NULL DEFAULT 768,
10
+ text_length INTEGER NOT NULL,
11
+ generated_at TEXT NOT NULL
12
+ );
13
+ CREATE INDEX IF NOT EXISTS idx_message_embeddings_session ON message_embeddings(session_id);
14
+ CREATE INDEX IF NOT EXISTS idx_message_embeddings_generated ON message_embeddings(generated_at DESC);
15
+ `)}var ot,ct=H(()=>{"use strict";ot=`
16
+ PRAGMA journal_mode = WAL;
17
+ PRAGMA synchronous = NORMAL;
18
+ PRAGMA foreign_keys = ON;
19
+
20
+ CREATE TABLE IF NOT EXISTS projects (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ encoded_path TEXT UNIQUE NOT NULL,
23
+ decoded_path TEXT NOT NULL,
24
+ name TEXT NOT NULL
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS sessions (
28
+ id TEXT PRIMARY KEY,
29
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
30
+ file_path TEXT NOT NULL,
31
+ file_mtime REAL NOT NULL,
32
+ started_at TEXT,
33
+ ended_at TEXT,
34
+ message_count INTEGER NOT NULL DEFAULT 0,
35
+ user_message_count INTEGER NOT NULL DEFAULT 0,
36
+ assistant_message_count INTEGER NOT NULL DEFAULT 0,
37
+ first_user_message TEXT,
38
+ cwd TEXT,
39
+ git_branch TEXT,
40
+ version TEXT,
41
+ indexed_at TEXT NOT NULL
42
+ );
43
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
44
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
45
+
46
+ CREATE TABLE IF NOT EXISTS messages (
47
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ uuid TEXT UNIQUE NOT NULL,
49
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
50
+ parent_uuid TEXT,
51
+ type TEXT NOT NULL,
52
+ role TEXT,
53
+ timestamp TEXT,
54
+ is_sidechain INTEGER NOT NULL DEFAULT 0,
55
+ content_text TEXT,
56
+ tool_names TEXT,
57
+ raw_json TEXT NOT NULL
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
60
+
61
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
62
+ content_text,
63
+ tool_names,
64
+ content='messages',
65
+ content_rowid='rowid',
66
+ tokenize = 'porter unicode61'
67
+ );
68
+
69
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
70
+ INSERT INTO messages_fts(rowid, content_text, tool_names)
71
+ VALUES (new.rowid, new.content_text, new.tool_names);
72
+ END;
73
+
74
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
75
+ INSERT INTO messages_fts(messages_fts, rowid, content_text, tool_names)
76
+ VALUES ('delete', old.rowid, old.content_text, old.tool_names);
77
+ END;
78
+
79
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
80
+ INSERT INTO messages_fts(messages_fts, rowid, content_text, tool_names)
81
+ VALUES ('delete', old.rowid, old.content_text, old.tool_names);
82
+ INSERT INTO messages_fts(rowid, content_text, tool_names)
83
+ VALUES (new.rowid, new.content_text, new.tool_names);
84
+ END;
85
+
86
+ -- v0.4.1 \u2014 session aliases. The UUID stays the primary key forever; aliases
87
+ -- are a display layer on top. Every edit archives the prior value into
88
+ -- previous_aliases (JSON array) \u2014 we never destroy history.
89
+ CREATE TABLE IF NOT EXISTS session_aliases (
90
+ session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
91
+ alias TEXT NOT NULL,
92
+ updated_at TEXT NOT NULL,
93
+ previous_aliases TEXT NOT NULL DEFAULT '[]'
94
+ );
95
+
96
+ -- v0.4.2 \u2014 per-session markdown notes.
97
+ -- Every save archives the prior content into previous_versions and also mirrors
98
+ -- the full note out to ~/.recall/notes/<session_id>.md on disk. Nothing is ever
99
+ -- destroyed: an empty string means "cleared" with full history preserved.
100
+ CREATE TABLE IF NOT EXISTS session_notes (
101
+ session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
102
+ content TEXT NOT NULL DEFAULT '',
103
+ updated_at TEXT NOT NULL,
104
+ previous_versions TEXT NOT NULL DEFAULT '[]'
105
+ );
106
+
107
+ -- v0.4.3 \u2014 session tags. Many-to-many. Never destructively delete:
108
+ -- tag_events is an append-only log of every add and remove across all time,
109
+ -- so the full history of what was tagged when and why is always recoverable.
110
+ CREATE TABLE IF NOT EXISTS session_tags (
111
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
112
+ tag TEXT NOT NULL,
113
+ created_at TEXT NOT NULL,
114
+ PRIMARY KEY (session_id, tag)
115
+ );
116
+ CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
117
+
118
+ CREATE TABLE IF NOT EXISTS tag_events (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ session_id TEXT NOT NULL,
121
+ tag TEXT NOT NULL,
122
+ action TEXT NOT NULL, -- 'add' | 'remove'
123
+ at TEXT NOT NULL
124
+ );
125
+ CREATE INDEX IF NOT EXISTS idx_tag_events_session ON tag_events(session_id, at DESC);
126
+ CREATE INDEX IF NOT EXISTS idx_tag_events_tag ON tag_events(tag, at DESC);
127
+
128
+ -- v0.4.5 \u2014 opt-in clipboard archive. This is the ONE table where a user can
129
+ -- truly purge a row on demand (not soft-delete), because it may contain
130
+ -- accidentally-archived secrets. This carve-out is documented as a deliberate
131
+ -- exception to the "never delete" rule specifically for this data class.
132
+ CREATE TABLE IF NOT EXISTS paste_archives (
133
+ id TEXT PRIMARY KEY,
134
+ created_at TEXT NOT NULL,
135
+ content TEXT NOT NULL,
136
+ size_bytes INTEGER NOT NULL,
137
+ source TEXT NOT NULL DEFAULT 'cli', -- 'cli' | 'cli-piped' | 'ui' | \u2026
138
+ label TEXT -- optional user-supplied short description
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_paste_archives_created ON paste_archives(created_at DESC);
141
+
142
+ -- v0.8 \u2014 collections. User-curated hand-picked groupings of sessions that
143
+ -- cut across the coarse-grained cwd-based "projects". A collection can nest
144
+ -- as a tree (parent_id \u2192 null means root). Soft-deletion only: archived_at
145
+ -- hides rows from default views, the row itself stays forever.
146
+ --
147
+ -- Durability: SQLite (this table + collection_events append-only log) plus a
148
+ -- plain-text mirror at ~/.recall/collections.json rewritten on every mutation.
149
+ CREATE TABLE IF NOT EXISTS collections (
150
+ id TEXT PRIMARY KEY,
151
+ name TEXT NOT NULL,
152
+ description TEXT,
153
+ icon TEXT,
154
+ color TEXT,
155
+ parent_id TEXT REFERENCES collections(id) ON DELETE SET NULL,
156
+ sort_key TEXT NOT NULL DEFAULT '',
157
+ created_at TEXT NOT NULL,
158
+ updated_at TEXT NOT NULL,
159
+ archived_at TEXT
160
+ );
161
+ CREATE INDEX IF NOT EXISTS idx_collections_parent ON collections(parent_id);
162
+ CREATE INDEX IF NOT EXISTS idx_collections_archived ON collections(archived_at);
163
+
164
+ CREATE TABLE IF NOT EXISTS collection_sessions (
165
+ collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
166
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
167
+ added_at TEXT NOT NULL,
168
+ note TEXT,
169
+ PRIMARY KEY (collection_id, session_id)
170
+ );
171
+ CREATE INDEX IF NOT EXISTS idx_collection_sessions_session ON collection_sessions(session_id);
172
+
173
+ CREATE TABLE IF NOT EXISTS collection_events (
174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ collection_id TEXT NOT NULL,
176
+ session_id TEXT,
177
+ action TEXT NOT NULL, -- 'create' | 'rename' | 'describe' | 'recolor' | 'add' | 'remove' | 'archive' | 'restore' | 'move' | 'reorder'
178
+ payload TEXT, -- JSON context (old_name, new_parent, etc.)
179
+ at TEXT NOT NULL
180
+ );
181
+ CREATE INDEX IF NOT EXISTS idx_collection_events_at ON collection_events(at DESC);
182
+ CREATE INDEX IF NOT EXISTS idx_collection_events_collection ON collection_events(collection_id, at DESC);
183
+
184
+ -- v0.11 \u2014 semantic search (Tier 0: shells out to the user's local claude CLI to
185
+ -- summarize each session into 3-sentence prose + a keyword set). Both columns
186
+ -- are TEXT and feed sessions_fts so a conceptual query can hit this index in
187
+ -- addition to the per-message FTS5 index. Pipeline is OFF by default; users opt
188
+ -- in via "recall semantic on". Plain-text mirror at ~/.recall/semantic/<id>.json.
189
+ CREATE TABLE IF NOT EXISTS session_semantic (
190
+ session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
191
+ summary TEXT NOT NULL DEFAULT '',
192
+ keywords TEXT NOT NULL DEFAULT '', -- comma-separated
193
+ model TEXT,
194
+ source_message_count INTEGER NOT NULL DEFAULT 0,
195
+ generated_at TEXT NOT NULL
196
+ );
197
+
198
+ CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
199
+ summary,
200
+ keywords,
201
+ content='session_semantic',
202
+ content_rowid='rowid',
203
+ tokenize = 'porter unicode61'
204
+ );
205
+
206
+ CREATE TRIGGER IF NOT EXISTS session_semantic_ai AFTER INSERT ON session_semantic BEGIN
207
+ INSERT INTO sessions_fts(rowid, summary, keywords)
208
+ VALUES (new.rowid, new.summary, new.keywords);
209
+ END;
210
+
211
+ CREATE TRIGGER IF NOT EXISTS session_semantic_ad AFTER DELETE ON session_semantic BEGIN
212
+ INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords)
213
+ VALUES ('delete', old.rowid, old.summary, old.keywords);
214
+ END;
215
+
216
+ CREATE TRIGGER IF NOT EXISTS session_semantic_au AFTER UPDATE ON session_semantic BEGIN
217
+ INSERT INTO sessions_fts(sessions_fts, rowid, summary, keywords)
218
+ VALUES ('delete', old.rowid, old.summary, old.keywords);
219
+ INSERT INTO sessions_fts(rowid, summary, keywords)
220
+ VALUES (new.rowid, new.summary, new.keywords);
221
+ END;
222
+
223
+ -- v0.7 \u2014 vector search tier (Pro-only). Chunks are per-conversational-turn
224
+ -- segments of messages, embedded via local bge-base-en-v1.5 (768d). The vector
225
+ -- data is a derived cache; if lost, it is recomputable from messages.
226
+ -- Tables stay empty on Free tier (no model downloaded, no worker started).
227
+
228
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(
229
+ embedding float[768] distance_metric=cosine
230
+ );
231
+
232
+ CREATE TABLE IF NOT EXISTS chunk_meta (
233
+ rowid INTEGER PRIMARY KEY,
234
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
235
+ message_uuids TEXT NOT NULL DEFAULT '[]',
236
+ text TEXT NOT NULL DEFAULT '',
237
+ embedding_model_id TEXT NOT NULL DEFAULT 'bge-base-en-v1.5',
238
+ embedding_dim INTEGER NOT NULL DEFAULT 768,
239
+ stale INTEGER NOT NULL DEFAULT 0,
240
+ generated_at TEXT NOT NULL DEFAULT ''
241
+ );
242
+ CREATE INDEX IF NOT EXISTS idx_chunk_meta_session ON chunk_meta(session_id);
243
+ CREATE INDEX IF NOT EXISTS idx_chunk_meta_stale ON chunk_meta(stale) WHERE stale = 1;
244
+
245
+ CREATE TABLE IF NOT EXISTS chunk_queue (
246
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
247
+ session_id TEXT NOT NULL,
248
+ message_uuid TEXT,
249
+ action TEXT NOT NULL CHECK (action IN ('embed', 'reembed', 'delete')),
250
+ enqueued_at TEXT NOT NULL DEFAULT (datetime('now'))
251
+ );
252
+ CREATE INDEX IF NOT EXISTS idx_chunk_queue_session ON chunk_queue(session_id);
253
+
254
+ CREATE TRIGGER IF NOT EXISTS messages_vec_ai AFTER INSERT ON messages
255
+ WHEN new.is_sidechain = 0 AND new.content_text IS NOT NULL
256
+ BEGIN
257
+ INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
258
+ VALUES (new.session_id, new.uuid, 'embed', datetime('now'));
259
+ END;
260
+
261
+ CREATE TRIGGER IF NOT EXISTS messages_vec_ad AFTER DELETE ON messages BEGIN
262
+ INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
263
+ VALUES (old.session_id, old.uuid, 'delete', datetime('now'));
264
+ END;
265
+
266
+ CREATE TRIGGER IF NOT EXISTS messages_vec_au AFTER UPDATE OF content_text ON messages
267
+ WHEN new.is_sidechain = 0
268
+ BEGIN
269
+ INSERT INTO chunk_queue(session_id, message_uuid, action, enqueued_at)
270
+ VALUES (new.session_id, new.uuid, 'reembed', datetime('now'));
271
+ END;
272
+
273
+ -- v0.10a \u2014 cost / token analytics. Claude Code already writes per-assistant-
274
+ -- message usage + model into the source JSONLs; we persist the raw counts
275
+ -- here and derive dollar amounts at render time (pricing changes; token
276
+ -- counts don't). message_usage is 1:1 with messages (keyed by message_uuid)
277
+ -- so a reindex rebuilds it cleanly; rollup columns on sessions are a
278
+ -- cache for fast list rendering.
279
+ CREATE TABLE IF NOT EXISTS message_usage (
280
+ message_uuid TEXT PRIMARY KEY REFERENCES messages(uuid) ON DELETE CASCADE,
281
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
282
+ model TEXT,
283
+ input_tokens INTEGER NOT NULL DEFAULT 0,
284
+ output_tokens INTEGER NOT NULL DEFAULT 0,
285
+ cache_create_tokens INTEGER NOT NULL DEFAULT 0,
286
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
287
+ timestamp TEXT
288
+ );
289
+ CREATE INDEX IF NOT EXISTS idx_message_usage_session ON message_usage(session_id);
290
+ CREATE INDEX IF NOT EXISTS idx_message_usage_model ON message_usage(model);
291
+ -- Windowed stats queries (7d / 30d) filter on mu.timestamp >= since.
292
+ -- Without this index every refresh full-scans message_usage.
293
+ CREATE INDEX IF NOT EXISTS idx_message_usage_timestamp
294
+ ON message_usage(timestamp);
295
+ -- Daily-bucket aggregation groups by substr(timestamp, 1, 10) and joins
296
+ -- on session_id. The composite covers the common "by session in window"
297
+ -- access pattern and lets the engine skip the row lookup for many plans.
298
+ CREATE INDEX IF NOT EXISTS idx_message_usage_session_ts
299
+ ON message_usage(session_id, timestamp);
300
+
301
+ -- v0.10b \u2014 git correlation. For any session whose cwd is a git worktree we
302
+ -- run a read-only \`git log\` scoped to that cwd for the [started_at, ended_at]
303
+ -- window and record every resulting commit. Composite PK (session_id,
304
+ -- commit_sha) lets a single commit belong to multiple sessions (rare, but
305
+ -- happens when a long session ends right as another starts). The reverse
306
+ -- index on commit_sha powers \`recall blame <sha>\`.
307
+ --
308
+ -- correlated_at lets the correlator skip sessions it has already processed
309
+ -- recently; cwd_snapshot captures the directory we ran git in so stale rows
310
+ -- can be identified if the user later points the session elsewhere.
311
+ CREATE TABLE IF NOT EXISTS session_commits (
312
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
313
+ commit_sha TEXT NOT NULL,
314
+ committed_at TEXT,
315
+ subject TEXT,
316
+ cwd_snapshot TEXT,
317
+ correlated_at TEXT NOT NULL,
318
+ PRIMARY KEY (session_id, commit_sha)
319
+ );
320
+ CREATE INDEX IF NOT EXISTS idx_session_commits_sha ON session_commits(commit_sha);
321
+ CREATE INDEX IF NOT EXISTS idx_session_commits_session ON session_commits(session_id);
322
+ -- v0.13 \u2014 MCP write audit log. Every write tool invocation appends one row,
323
+ -- regardless of outcome (ok / error / rate_limited). Tags and collections
324
+ -- already have their own append-only event tables (tag_events,
325
+ -- collection_events) \u2014 this table covers note/alias writes and provides a
326
+ -- single chronological feed across all MCP write activity for review.
327
+ CREATE TABLE IF NOT EXISTS mcp_audit_events (
328
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
329
+ tool TEXT NOT NULL,
330
+ args_json TEXT NOT NULL,
331
+ result TEXT NOT NULL, -- 'ok' | 'error' | 'rate_limited'
332
+ error_message TEXT,
333
+ caller TEXT,
334
+ at TEXT NOT NULL
335
+ );
336
+ CREATE INDEX IF NOT EXISTS idx_mcp_audit_events_at ON mcp_audit_events(at DESC);
337
+ CREATE INDEX IF NOT EXISTS idx_mcp_audit_events_tool ON mcp_audit_events(tool, at DESC);
338
+
339
+ -- v0.15 T6 \u2014 auto-collections. Rules match sessions by cwd prefix, project
340
+ -- id, tag, plan-file reference, or git branch prefix, and insert auto
341
+ -- memberships into collection_sessions (tagged with source='auto' + the
342
+ -- originating rule_id). Manual memberships (source='manual') are never
343
+ -- touched by the rule engine \u2014 so a user's hand-curated pick always wins.
344
+ --
345
+ -- Suggestions are the discovery half: the daemon periodically surveys the
346
+ -- corpus, detects clusters that *would* make good auto-collections, and
347
+ -- stashes them here for the user to accept or dismiss. UNIQUE(type,pattern)
348
+ -- means the same cluster won't be re-suggested every scan.
349
+ --
350
+ -- Durability mirror: ~/.recall/auto-rules/{rules.json,suggestions.json}
351
+ -- rewritten on every mutation. Deleting a rule removes ONLY its auto
352
+ -- memberships (matched via rule_id); the target collection stays so the
353
+ -- user can keep whatever they manually added into it.
354
+ CREATE TABLE IF NOT EXISTS auto_collection_rules (
355
+ id TEXT PRIMARY KEY,
356
+ name TEXT NOT NULL,
357
+ type TEXT NOT NULL CHECK (type IN ('cwd-prefix','project-id','tag','plan-file','git-branch-prefix')),
358
+ pattern TEXT NOT NULL,
359
+ collection_id TEXT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
360
+ priority INTEGER NOT NULL DEFAULT 100,
361
+ enabled INTEGER NOT NULL DEFAULT 1,
362
+ created_at TEXT NOT NULL,
363
+ created_by TEXT NOT NULL DEFAULT 'user' -- 'user' | 'suggestion-accepted' | 'seed'
364
+ );
365
+ CREATE INDEX IF NOT EXISTS idx_auto_rules_type_enabled ON auto_collection_rules(type, enabled);
366
+ CREATE INDEX IF NOT EXISTS idx_auto_rules_collection ON auto_collection_rules(collection_id);
367
+
368
+ CREATE TABLE IF NOT EXISTS auto_collection_suggestions (
369
+ id TEXT PRIMARY KEY,
370
+ type TEXT NOT NULL,
371
+ pattern TEXT NOT NULL,
372
+ suggested_name TEXT NOT NULL,
373
+ suggested_parent_collection_id TEXT,
374
+ session_count INTEGER NOT NULL,
375
+ detected_at TEXT NOT NULL,
376
+ dismissed INTEGER NOT NULL DEFAULT 0,
377
+ UNIQUE(type, pattern)
378
+ );
379
+ CREATE INDEX IF NOT EXISTS idx_auto_suggestions_detected ON auto_collection_suggestions(detected_at DESC);
380
+
381
+ -- v0.15 Threads. The headline intent-grouping primitive: a DAG of sessions
382
+ -- connected by shared purpose (one or more origin sessions plus their
383
+ -- children). Additive to Projects and Collections; neither is refactored.
384
+ -- thread_edges is many-to-many so a session can belong to multiple threads
385
+ -- (planning session that seeds two features). parent_session_id is
386
+ -- nullable; when null, the row is an origin. confidence = 1.0 for manual
387
+ -- edges; < 1.0 for auto-detected edges (v0.15b). source tracks provenance
388
+ -- for the audit/undo paths.
389
+ CREATE TABLE IF NOT EXISTS threads (
390
+ id TEXT PRIMARY KEY,
391
+ name TEXT NOT NULL,
392
+ summary TEXT,
393
+ created_at TEXT NOT NULL,
394
+ closed_at TEXT,
395
+ archived INTEGER NOT NULL DEFAULT 0
396
+ );
397
+
398
+ CREATE TABLE IF NOT EXISTS thread_edges (
399
+ thread_id TEXT NOT NULL,
400
+ session_id TEXT NOT NULL,
401
+ parent_session_id TEXT,
402
+ role TEXT NOT NULL CHECK(role IN ('origin','child')),
403
+ confidence REAL NOT NULL DEFAULT 1.0 CHECK(confidence >= 0 AND confidence <= 1),
404
+ source TEXT NOT NULL DEFAULT 'manual',
405
+ added_at TEXT NOT NULL,
406
+ PRIMARY KEY (thread_id, session_id),
407
+ FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
408
+ );
409
+ CREATE INDEX IF NOT EXISTS idx_thread_edges_session ON thread_edges(session_id);
410
+ CREATE INDEX IF NOT EXISTS idx_thread_edges_parent ON thread_edges(parent_session_id);
411
+ CREATE INDEX IF NOT EXISTS idx_thread_edges_thread_role ON thread_edges(thread_id, role);
412
+
413
+ -- v0.6 #4: user-defined SUBFOLDERS that nest inside the auto-derived
414
+ -- per-project folders the Threads sidebar already renders. Critical
415
+ -- design rule: auto-project folders are NOT in this table \u2014 they are
416
+ -- computed at render time from project membership of each thread's
417
+ -- sessions. So even with this table empty, every project folder still
418
+ -- renders and every thread still has a visible home. That's the
419
+ -- safety property that the previous full-replacement attempt lost
420
+ -- (when no user folders existed, every thread fell into a single
421
+ -- collapsed "(unfiled)" bucket and the sidebar looked broken).
422
+ --
423
+ -- project_scope: the project this user folder is nested under. NULL
424
+ -- means top-level (cross-project, e.g. "v0.6 Launch" that holds
425
+ -- threads from multiple repos).
426
+ -- parent_folder_id: another USER folder this one nests inside. NULL
427
+ -- means it's a direct child of project_scope's auto folder, or
428
+ -- top-level if project_scope is also NULL.
429
+ CREATE TABLE IF NOT EXISTS thread_folders (
430
+ id TEXT PRIMARY KEY,
431
+ name TEXT NOT NULL,
432
+ parent_folder_id TEXT REFERENCES thread_folders(id) ON DELETE CASCADE,
433
+ project_scope TEXT,
434
+ created_at TEXT NOT NULL,
435
+ archived INTEGER NOT NULL DEFAULT 0,
436
+ sort_order INTEGER NOT NULL DEFAULT 0
437
+ );
438
+ CREATE INDEX IF NOT EXISTS idx_thread_folders_parent ON thread_folders(parent_folder_id);
439
+ CREATE INDEX IF NOT EXISTS idx_thread_folders_project ON thread_folders(project_scope);
440
+
441
+ -- v0.17 -- recall event log. Every \`recall context\` invocation writes one
442
+ -- row. Powers share-card metadata ("recalled today") and monthly wrap
443
+ -- aggregation (total recalls, most-recalled session, etc.). Append-only;
444
+ -- no deletes. Plain-text mirror at ~/.recall/recall-events.json rewritten
445
+ -- on mutation.
446
+ CREATE TABLE IF NOT EXISTS recall_events (
447
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
448
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
449
+ recalled_at TEXT NOT NULL,
450
+ token_count INTEGER NOT NULL DEFAULT 0,
451
+ mode TEXT NOT NULL DEFAULT 'full',
452
+ caller TEXT NOT NULL DEFAULT 'cli'
453
+ );
454
+ CREATE INDEX IF NOT EXISTS idx_recall_events_session ON recall_events(session_id, recalled_at DESC);
455
+ CREATE INDEX IF NOT EXISTS idx_recall_events_at ON recall_events(recalled_at DESC);
456
+
457
+ -- v0.18 cog-graph Phase C \u2014 multi-edge schema.
458
+ --
459
+ -- Two parallel edge systems live side by side and MUST NOT be merged:
460
+ -- 1. thread_edges (above) \u2014 hierarchical DAG, intent grouping.
461
+ -- 2. session_links (below) \u2014 non-hierarchical: citations, semantic
462
+ -- similarity, skill tracks, bug-pattern membership, manual wiki
463
+ -- links, temporal proximity. Joined at query time; never schema-
464
+ -- level merged.
465
+ --
466
+ -- Every row carries provenance (source + evidence JSON + confidence) so
467
+ -- "why does this edge exist?" always has an answer. The unique constraint
468
+ -- on (source, target, link_type) is the soft idempotency key \u2014 re-running
469
+ -- inference upserts instead of duplicating. Approval gating via
470
+ -- session_link_suggestions (the queue) \u2192 session_links (the live store)
471
+ -- keeps trust intact: nothing auto-promotes without user opt-in.
472
+ --
473
+ -- Mirror layout: ~/.recall/links/<source-session-id>.json (per-source
474
+ -- denormalised; rewritten on mutation), ~/.recall/suggestions/index.json
475
+ -- (single file rewritten on mutation).
476
+ CREATE TABLE IF NOT EXISTS session_links (
477
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
478
+ source_session_id TEXT NOT NULL,
479
+ target_session_id TEXT NOT NULL,
480
+ link_type TEXT NOT NULL CHECK(link_type IN (
481
+ 'citation','similar','skill_track','bug_pattern',
482
+ 'wiki_link','temporal_proximity'
483
+ )),
484
+ confidence REAL NOT NULL CHECK(confidence BETWEEN 0 AND 1),
485
+ source TEXT NOT NULL,
486
+ evidence TEXT NOT NULL,
487
+ approved INTEGER NOT NULL DEFAULT 0,
488
+ created_at TEXT NOT NULL,
489
+ updated_at TEXT NOT NULL,
490
+ UNIQUE(source_session_id, target_session_id, link_type)
491
+ );
492
+ CREATE INDEX IF NOT EXISTS idx_links_source ON session_links(source_session_id);
493
+ CREATE INDEX IF NOT EXISTS idx_links_target ON session_links(target_session_id);
494
+ CREATE INDEX IF NOT EXISTS idx_links_type ON session_links(link_type);
495
+
496
+ -- v0.18 cog-graph Phase C \u2014 per-session structured outputs.
497
+ --
498
+ -- The "what did this session produce" index. Phase D's LLM-augmented
499
+ -- extractor sub-agent populates this row (Phase C ships the empty
500
+ -- table). Stays empty in Phase C. extractor_version lets us re-derive
501
+ -- when the extraction prompt or schema improves without losing the
502
+ -- existing rows. raw_extraction is the full sub-agent output blob so we
503
+ -- can re-derive structured fields without re-running extraction.
504
+ --
505
+ -- Mirror: ~/.recall/output-index/<session-id>.json rewritten on mutation.
506
+ CREATE TABLE IF NOT EXISTS session_output_index (
507
+ session_id TEXT PRIMARY KEY,
508
+ files_written TEXT,
509
+ brands_mentioned TEXT,
510
+ terms_introduced TEXT,
511
+ plan_ids_referenced TEXT,
512
+ bug_signatures TEXT,
513
+ raw_extraction TEXT,
514
+ extracted_at TEXT NOT NULL,
515
+ extractor_version INTEGER NOT NULL DEFAULT 1
516
+ );
517
+
518
+ -- v0.18 cog-graph Phase C \u2014 pending edge suggestions awaiting review.
519
+ --
520
+ -- Auto-inferred edges (from L1/L2/L3/L4 inference jobs) land here first
521
+ -- with status='pending'. User accepts \u2192 status='approved' AND a row gets
522
+ -- created in session_links with approved=1. User rejects \u2192 status=
523
+ -- 'rejected' (tombstone \u2014 prevents re-proposing the same pair from the
524
+ -- same inferer). Phase F builds the queue UI; Phase C ships the empty
525
+ -- table. The unique key includes inferred_by so two layers (e.g. L2
526
+ -- citation match + L3 embedding match) can both contribute evidence
527
+ -- without one stomping the other.
528
+ CREATE TABLE IF NOT EXISTS session_link_suggestions (
529
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
530
+ source_session_id TEXT NOT NULL,
531
+ target_session_id TEXT NOT NULL,
532
+ link_type TEXT NOT NULL,
533
+ confidence REAL NOT NULL,
534
+ evidence TEXT NOT NULL,
535
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected')),
536
+ inferred_by TEXT NOT NULL,
537
+ created_at TEXT NOT NULL,
538
+ decided_at TEXT,
539
+ UNIQUE(source_session_id, target_session_id, link_type, inferred_by)
540
+ );
541
+ CREATE INDEX IF NOT EXISTS idx_suggestions_pending ON session_link_suggestions(status, created_at) WHERE status = 'pending';
542
+ CREATE INDEX IF NOT EXISTS idx_suggestions_source ON session_link_suggestions(source_session_id);
543
+ CREATE INDEX IF NOT EXISTS idx_suggestions_target ON session_link_suggestions(target_session_id);
544
+
545
+ -- v0.18 cog-graph Phase C \u2014 bug-pattern clusters. Empty in Phase C;
546
+ -- populated by Phase H (HDBSCAN over bug-signature embeddings). Tables
547
+ -- ship now so the schema is stable across the milestone. resolved_in_
548
+ -- session_id is set when the user marks "this fix worked" \u2014 surfaced
549
+ -- the next time a session's bug signature falls into the same cluster.
550
+ CREATE TABLE IF NOT EXISTS bug_pattern_clusters (
551
+ id TEXT PRIMARY KEY,
552
+ signature_hash TEXT NOT NULL,
553
+ example_message TEXT NOT NULL,
554
+ occurrence_count INTEGER NOT NULL,
555
+ first_seen_at TEXT NOT NULL,
556
+ last_seen_at TEXT NOT NULL,
557
+ resolved_in_session_id TEXT,
558
+ fix_summary TEXT
559
+ );
560
+ CREATE INDEX IF NOT EXISTS idx_bug_clusters_signature ON bug_pattern_clusters(signature_hash);
561
+ CREATE INDEX IF NOT EXISTS idx_bug_clusters_last_seen ON bug_pattern_clusters(last_seen_at DESC);
562
+
563
+ CREATE TABLE IF NOT EXISTS bug_pattern_members (
564
+ cluster_id TEXT NOT NULL REFERENCES bug_pattern_clusters(id) ON DELETE CASCADE,
565
+ session_id TEXT NOT NULL,
566
+ matched_at TEXT NOT NULL,
567
+ PRIMARY KEY (cluster_id, session_id)
568
+ );
569
+ CREATE INDEX IF NOT EXISTS idx_bug_members_session ON bug_pattern_members(session_id);
570
+
571
+ -- v0.20 / project rollups. The user's mental model is "I have ~18 repos"
572
+ -- but Recall's project model is "every cwd is a project" (~32 entries).
573
+ -- Macro repos are a deterministic, manual grouping layer the user
574
+ -- defines: pick N projects, label them as one logical repo. Display
575
+ -- surfaces collapse member projects into the macro repo when the user
576
+ -- toggles "by macro repo." See docs/internal/project-rollups.md.
577
+ --
578
+ -- We deliberately reject auto-grouping: a wrong auto-rollup is worse
579
+ -- than no rollup. Membership is always explicit, set by the user.
580
+ CREATE TABLE IF NOT EXISTS macro_repos (
581
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
582
+ name TEXT NOT NULL UNIQUE,
583
+ description TEXT,
584
+ created_at TEXT NOT NULL,
585
+ updated_at TEXT NOT NULL
586
+ );
587
+
588
+ CREATE TABLE IF NOT EXISTS macro_repo_members (
589
+ macro_repo_id INTEGER NOT NULL REFERENCES macro_repos(id) ON DELETE CASCADE,
590
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
591
+ added_at TEXT NOT NULL,
592
+ PRIMARY KEY (macro_repo_id, project_id)
593
+ );
594
+ CREATE INDEX IF NOT EXISTS idx_macro_repo_members_project ON macro_repo_members(project_id);
595
+
596
+ -- v0.20 / synthesis result persistence. Every successful Bug Pattern
597
+ -- synthesis run writes its full Markdown output here so the user can
598
+ -- revisit reports later without re-spending tokens. Keyed by
599
+ -- (scope, target_id, mode, created_at) \u2014 multiple runs over time on the
600
+ -- same target are kept as a history (newest-first browse). Token spend
601
+ -- is recorded for audit. NOT subject to the three-layer durability rule
602
+ -- because the source data (bug_pattern_clusters + member sessions) can
603
+ -- always re-derive the synthesis.
604
+ CREATE TABLE IF NOT EXISTS bug_synthesis_results (
605
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
606
+ scope TEXT NOT NULL CHECK(scope IN ('cluster', 'project')),
607
+ target_id TEXT NOT NULL,
608
+ mode TEXT NOT NULL CHECK(mode IN ('synopsis', 'priorities', 'root_cause')),
609
+ model TEXT NOT NULL,
610
+ output_markdown TEXT NOT NULL,
611
+ input_tokens INTEGER NOT NULL DEFAULT 0,
612
+ output_tokens INTEGER NOT NULL DEFAULT 0,
613
+ context_summary TEXT NOT NULL DEFAULT '{}',
614
+ created_at TEXT NOT NULL,
615
+ job_id TEXT
616
+ );
617
+ CREATE INDEX IF NOT EXISTS idx_synth_results_target
618
+ ON bug_synthesis_results(scope, target_id, created_at DESC);
619
+ CREATE INDEX IF NOT EXISTS idx_synth_results_created
620
+ ON bug_synthesis_results(created_at DESC);
621
+ `});import En from"better-sqlite3";import*as lt from"sqlite-vec";function p(){if(O)return O;x(),O=new En(oe),lt.load(O),O.pragma("cache_size = -64000"),O.pragma("mmap_size = 268435456"),O.pragma("temp_store = MEMORY"),O.pragma("busy_timeout = 5000"),O.pragma("journal_size_limit = 67108864"),O.pragma("wal_autocheckpoint = 1000"),O.exec(ot),at(O);try{O.exec("PRAGMA optimize")}catch{}return O}function dt(){if(O){try{O.exec("PRAGMA optimize")}catch{}try{O.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES('merge', 4);")}catch{}try{O.exec("INSERT INTO sessions_fts(sessions_fts, rank) VALUES('merge', 4);")}catch{}try{O.pragma("wal_checkpoint(TRUNCATE)")}catch{}O.close(),O=null}}var O,L=H(()=>{"use strict";F();ct();O=null});import{writeFileSync as wn}from"node:fs";import{join as Ln}from"node:path";function q(e){return e.trim().toLowerCase().replace(/^#+/,"").replace(/[\s/\\]+/g,"-").replace(/[^a-z0-9\-._]/g,"").slice(0,40)}function ge(e,s){let t=q(s);if(!t)throw new Error("tag must contain at least one alphanumeric character");let n=p(),i=new Date().toISOString();return n.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag = ?").get(e,t)?{tag:t,added:!1}:(n.transaction(()=>{n.prepare("INSERT INTO session_tags (session_id, tag, created_at) VALUES (?, ?, ?)").run(e,t,i),n.prepare("INSERT INTO tag_events (session_id, tag, action, at) VALUES (?, ?, 'add', ?)").run(e,t,i)})(),gt(),{tag:t,added:!0})}function pt(e,s){let t=q(s);if(!t)return{tag:"",removed:!1};let n=p(),i=new Date().toISOString();return n.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag = ?").get(e,t)?(n.transaction(()=>{n.prepare("DELETE FROM session_tags WHERE session_id = ? AND tag = ?").run(e,t),n.prepare("INSERT INTO tag_events (session_id, tag, action, at) VALUES (?, ?, 'remove', ?)").run(e,t,i)})(),gt(),{tag:t,removed:!0}):{tag:t,removed:!1}}function _e(e){return p().prepare("SELECT tag FROM session_tags WHERE session_id = ? ORDER BY tag").all(e).map(s=>s.tag)}function mt(){return p().prepare(`SELECT tag, COUNT(*) AS count FROM session_tags
622
+ GROUP BY tag ORDER BY count DESC, tag ASC`).all()}function gt(){try{x();let e=p(),s=e.prepare("SELECT session_id, tag, created_at FROM session_tags ORDER BY session_id, tag").all(),t=e.prepare("SELECT id, session_id, tag, action, at FROM tag_events ORDER BY at ASC, id ASC").all(),n={schema:"claude-recall.tags.v1",backed_up_at:new Date().toISOString(),current:s,events:t};wn(An,JSON.stringify(n,null,2))}catch(e){console.error("[tags] backup failed:",e)}}var An,fe=H(()=>{"use strict";L();F();An=Ln(S,"tags.json")});function On(e,s){let t=e.filter(r=>r.content_text&&r.content_text.trim().length>0);if(t.length<=s)return t;let n=new Set;n.add(0),n.add(t.length-1);let i=(t.length-2)/Math.max(1,s-2);for(let r=1;r<s-1;r++)n.add(Math.floor(r*i));return Array.from(n).sort((r,a)=>r-a).slice(0,s).map(r=>t[r])}function he(e){let s=p(),t={limit:e.limit??500},n=e.sessionIds&&e.sessionIds.length>0,i=n?"1=1":"s.message_count > 2";if(n){let a=e.sessionIds.map((o,c)=>`@sid_${c}`).join(", ");i+=` AND s.id IN (${a})`,e.sessionIds.forEach((o,c)=>{t[`sid_${c}`]=o})}return e.untaggedOnly&&(i+=" AND NOT EXISTS (SELECT 1 FROM session_tags st WHERE st.session_id = s.id)"),e.project&&(i+=" AND p.name = @project",t.project=e.project),e.collectionId&&(i+=" AND s.id IN (SELECT session_id FROM collection_sessions WHERE collection_id = @col)",t.col=e.collectionId),s.prepare(`SELECT s.id, p.name AS project, s.git_branch,
623
+ NULLIF(sa.alias, '') AS alias,
624
+ COALESCE(s.first_user_message, '') AS first_user_message
625
+ FROM sessions s
626
+ JOIN projects p ON p.id = s.project_id
627
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
628
+ WHERE ${i}
629
+ ORDER BY COALESCE(s.started_at, '') DESC
630
+ LIMIT @limit`).all(t).map(a=>{let o=s.prepare(`SELECT role, COALESCE(content_text, '') AS content_text
631
+ FROM messages WHERE session_id = ?
632
+ ORDER BY COALESCE(timestamp, ''), rowid`).all(a.id),l=On(o,5).map(d=>`${d.role}: ${d.content_text.slice(0,400)}`).join(`
633
+ ---
634
+ `);return{id:a.id,project:a.project,git_branch:a.git_branch,alias:a.alias,first_user_message:a.first_user_message,message_sample:l,current_tags:_e(a.id)}})}var Ce=H(()=>{"use strict";L();fe()});import{z as j}from"zod";function ke(e){let s=e.minTags??2,t=e.maxTags??4,n=e.untaggedOnly??!e.sessionId,i=["Auto-tag my Claude Recall sessions using the MCP tools available to you.","","1. Call `list_tags` first to see which tags I already use (prefer those for consistency over inventing new ones).","2. Call `list_sessions_to_tag` with these filters:"],r=[];return e.sessionId?(r.push("limit: 1"),i.push(` ${r.join(", ")}`),i.push(` Then match the session id ${e.sessionId} from the returned list.`)):(n&&r.push("untaggedOnly: true"),e.project&&r.push(`project: "${e.project}"`),e.collectionId&&r.push(`collectionId: "${e.collectionId}"`),r.push(`limit: ${e.limit??100}`),i.push(` ${r.join(", ")}`)),i.push(""),i.push(`3. For each session returned, look at the alias, first user message, git branch, and sampled messages. Pick ${s}-${t} concise, lowercase, hyphen-separated tags describing:`),i.push(" - domain/subsystem (auth, db, frontend, billing, etc.)"),i.push(" - kind of work (bugfix, feature, refactor, research)"),i.push(" - prominent tools or libraries if relevant"),i.push(""),i.push("4. Call `apply_tags` once per session to write the tags."),i.push(""),i.push("Work through EVERY session returned \u2014 do not stop partway. When done, reply with a short summary of how many sessions were tagged. Do not ask clarifying questions; just do the work."),i.join(`
635
+ `)}function Pn(e){let s=e.mode==="detailed";return[`Summarize Claude Recall session ${e.sessionId} using the MCP tools available to you.`,"",`1. Call \`context_for_session\` with id "${e.sessionId}" and mode "condensed" to get the transcript as markdown.`,s?"2. Write a 1-paragraph overview (\u22643 sentences) of what this session was for, then 5-8 bullet points covering:":"2. Write 3-5 bullet points covering:"," - What was accomplished (shipped, decided, learned)"," - What was tried and abandoned"," - Any explicit open questions or follow-ups","","Rules:",'- Be concrete. Name files, functions, and decisions. Avoid vague "discussed X".',"- If nothing was actually shipped or decided, say so plainly.",'- Reply with just the summary \u2014 no preamble, no "Here is the summary:".'].join(`
636
+ `)}function $n(e){return[`Extract every architectural or product decision made in Claude Recall session ${e.sessionId}.`,"",`1. Call \`context_for_session\` with id "${e.sessionId}" and mode "full" (so tool I/O is included \u2014 sometimes the decision lives in a commit message or diff).`,"2. For each distinct decision, emit one block:",""," - **Decision:** one sentence."," - **Why:** the stated rationale (quote if possible).",' - **Alternatives considered:** bullet list, or "none mentioned".',' - **Where it landed:** file path / function name / commit SHA if identifiable, otherwise "TBD".',"","Rules:","- Include only decisions that were actually made, not ideas merely discussed.","- If an alternative was rejected, list it with a one-line reason.","- Group related decisions under a short heading when useful.",'- If the session made zero real decisions, reply exactly with: "No decisions made in this session."',"- No preamble, no closing, just the decision blocks."].join(`
637
+ `)}function Hn(e){let s=e.limit??5;return[`Find ${s} Recall sessions most similar to session ${e.sessionId}.`,"",`1. Call \`get_session\` with id "${e.sessionId}" \u2014 note its alias, first user message, tags, and git branch.`,'2. Derive 2-3 short search queries from that content (topic words, library names, error strings \u2014 NOT generic words like "fix" or "add").',`3. Call \`search\` once per query (limit: ${Math.max(5,s*2)}). Dedupe hits by session_id.`,"4. Also call `list_sessions` with the same tag(s) if the target session has any (pick the most specific tag).","5. Union the results. Exclude the target session itself. Rank by a mix of:"," - Shared tags (strongest signal)"," - Matching search hits across multiple queries"," - Recency as a tiebreaker","",`6. Return the top ${s} as a numbered list. For each:`," - **<short_id> \xB7 <project>** \u2014 <alias or first_user_message truncated>",' - One sentence on WHY it is similar (not a generic "same topic" \u2014 be specific).',"","If fewer than 2 genuinely-similar sessions exist, say so rather than padding with weak matches.","No preamble. Just the ranked list."].join(`
638
+ `)}var Mn,Un,jn,Xn,Bn,Wn,Yn,Gn,ft,Fe=H(()=>{"use strict";Mn={project:j.string().optional().describe("Exact project name match (optional)."),collectionId:j.string().optional().describe("Restrict to sessions in this collection (optional)."),sessionId:j.string().optional().describe("Full session UUID to tag just one session (optional)."),untaggedOnly:j.boolean().optional().describe("Skip sessions that already have any tag (default: true)."),limit:j.number().int().min(1).max(500).optional().describe("Max sessions to process (default: 100)."),minTags:j.number().int().min(1).max(10).optional().describe("Minimum tags per session (default: 2)."),maxTags:j.number().int().min(1).max(10).optional().describe("Maximum tags per session (default: 4).")};Un={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to summarize."),mode:j.enum(["brief","detailed"]).optional().describe("brief = 3-5 bullets; detailed = paragraph + bullets. Default: brief.")};jn={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to extract decisions from.")};Xn={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to find similar sessions to."),limit:j.number().int().min(1).max(20).optional().describe("How many similar sessions to surface (default: 5).")};Bn={name:"auto_tag_sessions",title:"Auto-tag Recall sessions",description:"Have the agent auto-tag Recall sessions using the Recall MCP tools. Scope can be restricted to a project, collection, or single session.",argsSchema:Mn,build:ke,allowedTools:["mcp__recall__list_tags","mcp__recall__list_sessions_to_tag","mcp__recall__apply_tags"]},Wn={name:"summarize_session",title:"Summarize a session",description:"Produce a concise, concrete summary of one session \u2014 what shipped, what was tried, what's still open.",argsSchema:Un,build:Pn,allowedTools:["mcp__recall__get_session","mcp__recall__context_for_session"]},Yn={name:"extract_decisions",title:"Extract architectural decisions",description:"Scan a session and emit one structured block per architectural / product decision: what, why, alternatives, where it landed.",argsSchema:jn,build:$n,allowedTools:["mcp__recall__get_session","mcp__recall__context_for_session"]},Gn={name:"find_similar_sessions",title:"Find similar sessions",description:"Given a session, find other sessions that touched the same topic / library / error \u2014 ranked with reasons.",argsSchema:Xn,build:Hn,allowedTools:["mcp__recall__get_session","mcp__recall__search","mcp__recall__list_sessions","mcp__recall__list_tags"]},ft=[Bn,Wn,Yn,Gn]});function qt(e,s){let t=xi.get(e);if(!(!t||t.size===0))for(let n of t)try{n(s)}catch{}}var xi,Qt=H(()=>{"use strict";xi=new Map});var ts={};Ie(ts,{buildScanPrompt:()=>Zt,isClaudeCliAvailable:()=>Mi,runClaudeCliScan:()=>Xi,spawnClaudePrompt:()=>Hi});import{execFileSync as Ci,execSync as vi,spawn as Di}from"node:child_process";function Fi(){if(le)return le;try{le=vi("which claude",{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{le="claude"}return le}function Mi(){try{return Ci("command",["-v","claude"],{stdio:"ignore"}),!0}catch{return!1}}function Zt(e){return ke({project:e.project,collectionId:e.collectionId,sessionId:e.sessionIds&&e.sessionIds.length===1?e.sessionIds[0]:void 0,untaggedOnly:e.untaggedOnly,limit:e.limit})}function Ui(e,s){let t=s.get(e);return t||e.slice(0,8)}function Pi(e){try{return he(e).map(t=>({id:t.id,label:t.alias&&t.alias.trim().length>0?t.alias:t.first_user_message&&t.first_user_message.trim().length>0?t.first_user_message.slice(0,60):t.id.slice(0,8)}))}catch{return[]}}function ji(e){let{scanId:s,total:t,labelTable:n}=e,i=new Set;return r=>{let a=r.trim();if(!a.startsWith("{"))return;let o;try{o=JSON.parse(a)}catch{return}if(!o||typeof o!="object")return;let c=o;if(!(c.type!=="assistant"||!c.message?.content))for(let l of c.message.content){if(l?.type!=="tool_use"||l.name!=="mcp__recall__apply_tags")continue;let d=l.input,u=typeof d?.sessionId=="string"?d.sessionId:null;!u||i.has(u)||(i.add(u),qt(s,{type:"progress",current:i.size,total:t,sessionId:u,sessionLabel:Ui(u,n)}))}}}function $i(e){let s="";return t=>{s+=t.toString("utf8");let n=s.indexOf(`
639
+ `);for(;n!==-1;){let i=s.slice(0,n);s=s.slice(n+1),i.length>0&&e(i),n=s.indexOf(`
640
+ `)}}}async function Xi(e,s={},t){let n=!!s.scanId,i=n?Pi(e):[],r=new Map(i.map(c=>[c.id,c.label])),a=i.length,o;return n&&s.scanId&&(o=ji({scanId:s.scanId,total:a,labelTable:r})),es({prompt:Zt(e),allowedTools:ki.split(","),opts:s,onProgress:t,onStdoutLine:o,outputFormat:n?"stream-json":"json"})}async function Hi(e,s,t={},n){return es({prompt:e,allowedTools:s,opts:t,onProgress:n,outputFormat:"json"})}function es(e){let{prompt:s,allowedTools:t,opts:n,onProgress:i,onStdoutLine:r,outputFormat:a}=e,o=["-p",s,"--output-format",a,"--allowedTools",t.join(","),"--permission-mode","bypassPermissions"];return a==="stream-json"&&o.push("--verbose"),n.model&&o.push("--model",n.model),new Promise(c=>{let l=Di(Fi(),o,{stdio:["ignore","pipe","pipe"]}),d=[],u=[],m=r?$i(r):void 0;l.stdout.on("data",_=>{d.push(_),m&&m(_)}),l.stderr.on("data",_=>{if(u.push(_),i){let f=_.toString("utf8").trim();f&&i(f)}});let g=setTimeout(()=>{l.kill("SIGKILL")},1800*1e3);l.on("close",_=>{clearTimeout(g),c({success:_===0,stdout:Buffer.concat(d).toString("utf8"),stderr:Buffer.concat(u).toString("utf8"),exitCode:_})}),l.on("error",_=>{clearTimeout(g),c({success:!1,stdout:"",stderr:String(_),exitCode:null})})})}var ki,le,ss=H(()=>{"use strict";Ce();Fe();Qt();ki=["mcp__recall__list_tags","mcp__recall__list_sessions_to_tag","mcp__recall__apply_tags"].join(",")});import Y from"chalk";import{formatDistanceToNowStrict as hl,parseISO as El}from"date-fns";var E,st=H(()=>{"use strict";E={dim:Y.gray,bold:Y.bold,project:Y.cyan,user:Y.blue,assistant:Y.green,tool:Y.magenta,warn:Y.yellow,err:Y.red,ok:Y.green,accent:Y.hex("#f97316")}});var dn={};Ie(dn,{buildHealthReport:()=>ln,buildPipelineDiagnostic:()=>cn,getFreeDiskBytes:()=>go,runDoctor:()=>mo});import{existsSync as en,readFileSync as io,statSync as tn,statfsSync as sn}from"node:fs";import{join as nn}from"node:path";import*as rn from"node:http";function ao(e){let s=[];for(let t of e)if(t.alias){if(oo.test(t.alias)){s.push({session_id:t.session_id,alias:t.alias,violation:"fabricated-origin-label",cwd:t.cwd});continue}if(t.cwd){let n=t.cwd.replace(/\/+$/,"").split("/").pop();n&&t.alias.startsWith(`${n} \xB7 `)&&s.push({session_id:t.session_id,alias:t.alias,violation:"fabricated-cwd-branch",cwd:t.cwd})}}return s}function co(){let e=nn(S,"daemon.port");if(!en(e))return null;try{let s=io(e,"utf8").trim();if(s.startsWith("{")){let n=JSON.parse(s);return typeof n.port=="number"?n.port:null}let t=Number.parseInt(s,10);return Number.isFinite(t)&&t>0&&t<65536?t:null}catch{return null}}function lo(e,s,t=1500){return new Promise(n=>{let i=rn.request({host:"127.0.0.1",port:e,path:s,method:"GET",timeout:t,headers:{host:"127.0.0.1","user-agent":"recall-doctor"}},r=>{let a=[];r.on("data",o=>a.push(Buffer.isBuffer(o)?o:Buffer.from(o))),r.on("end",()=>{if(!r.statusCode||r.statusCode<200||r.statusCode>=300){n(null);return}try{n(JSON.parse(Buffer.concat(a).toString("utf8")))}catch{n(null)}})});i.on("error",()=>n(null)),i.on("timeout",()=>{i.destroy(),n(null)}),i.end()})}function uo(){let e=nn(S,"terminals.json");if(!en(e))return{exists:!1,mtimeMs:null,ageSeconds:null};try{let s=tn(e),t=Math.floor((Date.now()-s.mtimeMs)/1e3);return{exists:!0,mtimeMs:s.mtimeMs,ageSeconds:t}}catch{return{exists:!1,mtimeMs:null,ageSeconds:null}}}function po(){let e=new Date(Date.now()-nt*36e5).toISOString();try{let s=p().prepare(`SELECT
641
+ COUNT(*) AS total,
642
+ SUM(CASE WHEN sa.alias IS NULL OR sa.alias = '' THEN 1 ELSE 0 END) AS without_alias,
643
+ SUM(CASE WHEN (sa.alias IS NULL OR sa.alias = '')
644
+ AND s.auto_title_source = 'heuristic' THEN 1 ELSE 0 END) AS heuristic_only
645
+ FROM sessions s
646
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
647
+ WHERE s.started_at >= ?`).get(e),t=s.total??0,n=s.without_alias??0,i=s.heuristic_only??0,r=t>0?i/t:0;return{total:t,withoutAlias:n,heuristicOnly:i,fractionHeuristic:r}}catch{return{total:0,withoutAlias:0,heuristicOnly:0,fractionHeuristic:0}}}async function cn(){let e=co(),s=uo(),t=po(),n=!1,i=null,r=null,a=null,o=null;if(e){let d=await lo(e,"/api/health");d&&(n=!0,i=typeof d.uptimeSeconds=="number"?d.uptimeSeconds:null,r=typeof d.version=="string"?d.version:null,a=typeof d.pipeline?.silentTerminalRejections=="number"?d.pipeline.silentTerminalRejections:null,o=d.pipeline?.lastTerminalSyncAt??null)}let c=[];if(n||c.push("Daemon not reachable on 127.0.0.1 \u2014 start it with `recall start` before further diagnosis."),a!==null&&a>0&&c.push(`Daemon rejected ${a.toLocaleString()} /api/terminal/* request(s) without a valid X-Recall-Token. The editor extension is outdated relative to this daemon \u2014 tab names are not flowing through. Reinstall: \`code --install-extension extensions/vscode/clauderecall-vscode-*.vsix\`, then reload the extension host (Cmd/Ctrl+Shift+P \u2192 "Developer: Restart Extension Host").`),n&&i!==null&&i>30)if(!o)c.push(`Daemon has been running ${Math.round(i/60)} min but has not yet seen a single successful /api/terminal/sync. Either no editor extension is installed/active, or every attempt is being rejected (see counter above).`);else{let d=Date.now()-Date.parse(o);Number.isFinite(d)&&d>on&&c.push(`Last successful /api/terminal/sync was ${Math.round(d/6e4)} min ago \u2014 extension may have crashed, been disabled, or is failing auth. Run \`recall doctor\` again after restarting the extension host.`)}return!n&&s.exists&&s.ageSeconds!==null&&s.ageSeconds>24*3600&&c.push(`~/.recall/terminals.json is ${Math.round(s.ageSeconds/3600)}h old \u2014 pipeline has not produced fresh data recently. Likely root cause is the same as the rejection counter would show; start the daemon and re-run.`),t.total>=3&&t.fractionHeuristic>=an&&c.push(`${t.heuristicOnly}/${t.total} sessions in the last ${nt}h (${Math.round(t.fractionHeuristic*100)}%) fell back to the heuristic first-message title. A healthy pipeline rate is < 20%. Either the extension is not syncing tab names, or the correlator cannot disambiguate. Reinstall the extension and verify the rejection counter drops to 0.`),{state:n?c.length>0?"degraded":"ok":"down",flags:c,daemon:{running:n,port:e,uptimeSeconds:i,version:r},runtime:{silentTerminalRejections:a,lastTerminalSyncAt:o},terminalsJson:s,recentSessions:t}}function Z(e){return e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(2)} GB`}function Qs(e){try{return tn(e).size}catch{return 0}}function Zs(e){try{return p().prepare(`SELECT COUNT(*) AS n FROM ${e}_data WHERE block = 1`).get().n}catch{return 0}}function ln(){let e=p(),s=e.pragma("page_size",{simple:!0})||4096,t=e.pragma("page_count",{simple:!0})||0,n=e.pragma("freelist_count",{simple:!0})||0,i="ok";try{let f=e.pragma("quick_check").map(h=>h.quick_check);i=f.length===1&&f[0]==="ok"?"ok":f.join("; ")}catch(_){i=`check failed: ${_.message}`}let r=Qs(oe),a=Qs(`${oe}-wal`),o=0,c=0;try{let _=sn(S);o=Number(_.bavail)*Number(_.bsize),c=Number(_.blocks)*Number(_.bsize)}catch{}let l=e.prepare(`SELECT
648
+ (SELECT COUNT(*) FROM projects) AS projects,
649
+ (SELECT COUNT(*) FROM sessions) AS sessions,
650
+ (SELECT COUNT(*) FROM messages) AS messages,
651
+ (SELECT COUNT(*) FROM message_usage) AS message_usage`).get(),d=0;try{d=e.prepare("SELECT COUNT(*) AS n FROM vec_chunks").get().n}catch{}let u=[];o>0&&o<1*1024**3&&u.push(`Disk free is ${Z(o)} \u2014 heavy operations (synthesis, extract-outputs, vector ingest) may fail.`),a>50*1024**2&&u.push(`WAL is ${Z(a)} \u2014 run \`recall optimize\` to truncate it.`),n>t*.2&&t>1e3&&u.push(`${n.toLocaleString()} free pages (${(n/t*100).toFixed(0)}% of file) \u2014 \`recall optimize --vacuum\` will reclaim them.`);let m=Zs("messages_fts"),g=Zs("sessions_fts");return m>16&&u.push(`messages_fts has ${m} segments \u2014 \`recall optimize\` will merge them.`),{db:{sizeBytes:r,walSizeBytes:a,pageCount:t,pageSize:s,freelistCount:n,freelistBytes:n*s,integrity:i},disk:{freeBytes:o,totalBytes:c},fts:{messages:{fragments:m},sessions:{fragments:g}},vectors:{rows:d},rows:{projects:l.projects,sessions:l.sessions,messages:l.messages,messageUsage:l.message_usage},warnings:u}}async function mo(e={}){let s=p().prepare(`SELECT sa.session_id AS session_id, sa.alias AS alias, s.cwd AS cwd
652
+ FROM session_aliases sa
653
+ LEFT JOIN sessions s ON s.id = sa.session_id
654
+ WHERE sa.alias IS NOT NULL AND sa.alias != ''`).all(),t=ao(s),n=ln(),i=await cn();if(e.json){process.stdout.write(JSON.stringify({scanned:s.length,violations:t.length,items:t,health:n,pipeline:i},null,2)),process.stdout.write(`
655
+ `);let o=i.state==="degraded";return t.length===0&&n.db.integrity==="ok"&&!o?0:1}if(console.log(E.dim("\u2014 System health \u2014")),console.log(` Database ${Z(n.db.sizeBytes)} (${n.rows.messages.toLocaleString()} messages across ${n.rows.sessions.toLocaleString()} sessions, ${n.rows.projects.toLocaleString()} projects)`),console.log(` WAL ${Z(n.db.walSizeBytes)} (capped at 64 MB; truncated on clean shutdown)`),console.log(` Free pages ${n.db.freelistCount.toLocaleString()} (${Z(n.db.freelistBytes)} reclaimable via VACUUM)`),console.log(` FTS segments messages=${n.fts.messages.fragments}, sessions=${n.fts.sessions.fragments} (lower is faster \u2014 \`recall optimize\` merges them)`),console.log(` Vector rows ${n.vectors.rows.toLocaleString()}`),n.disk.totalBytes>0){let o=n.disk.freeBytes/n.disk.totalBytes*100;console.log(` Disk free ${Z(n.disk.freeBytes)} of ${Z(n.disk.totalBytes)} (${o.toFixed(1)}%)`)}if(console.log(` Integrity ${n.db.integrity==="ok"?E.ok("ok"):E.err(n.db.integrity)}`),n.warnings.length>0){console.log("");for(let o of n.warnings)console.log(` ${E.warn("!")} ${o}`)}if(console.log(""),console.log(E.dim("\u2014 Pipeline health (tab-name \u2192 session alias) \u2014")),i.daemon.running?console.log(` Daemon ${E.ok("running")} (port ${i.daemon.port}, version ${i.daemon.version??"?"}, up ${i.daemon.uptimeSeconds!==null?Math.round(i.daemon.uptimeSeconds/60)+" min":"?"})`):console.log(` Daemon ${E.warn("not reachable")}`),i.runtime.silentTerminalRejections!==null){let o=i.runtime.silentTerminalRejections;console.log(` Auth rejections ${o===0?E.ok("0"):E.err(o.toLocaleString())} (extension /api/terminal/* requests denied without a valid X-Recall-Token)`)}if(i.runtime.lastTerminalSyncAt!==null){let o=Date.now()-Date.parse(i.runtime.lastTerminalSyncAt),c=Number.isFinite(o)?Math.round(o/6e4):null,l=c!==null&&o>on;console.log(` Last ext sync ${l?E.warn(`${c} min ago`):E.ok(c===0?"just now":`${c} min ago`)} (most recent successful POST /api/terminal/sync)`)}else i.daemon.running&&console.log(` Last ext sync ${E.warn("never")} (no extension has called /api/terminal/sync since the daemon started)`);if(i.terminalsJson.exists&&i.terminalsJson.ageSeconds!==null){let o=Math.round(i.terminalsJson.ageSeconds/3600),c=i.terminalsJson.ageSeconds>24*3600;console.log(` terminals.json ${c?E.warn(`${o}h old`):E.ok(`${o}h old`)} (persisted registry mtime \u2014 fresh means extensions are connecting)`)}let r=i.recentSessions;if(r.total>0){let o=Math.round(r.fractionHeuristic*100),c=r.fractionHeuristic>=an&&r.total>=3;console.log(` Recent titles ${c?E.err(`${o}% heuristic`):E.ok(`${o}% heuristic`)} (${r.heuristicOnly}/${r.total} sessions in last ${nt}h fell back to first-message title)`)}if(i.flags.length>0){console.log("");for(let o of i.flags)console.log(` ${E.warn("!")} ${o}`)}if(console.log(""),console.log(E.dim("\u2014 Tab-name invariant \u2014")),t.length===0)return console.log(E.ok(` \u2713 holds across ${s.length.toLocaleString()} aliased session${s.length===1?"":"s"}`)),console.log(E.dim(" No fabricated origin labels (`VS Code \xB7 cwd \xB7 branch`) found. No deprecated cwd-branch synthesis found.")),n.db.integrity==="ok"?0:1;console.log(E.err(`\u2717 ${t.length} invariant violation${t.length===1?"":"s"} found across ${s.length.toLocaleString()} aliased sessions`)),console.log("");let a=new Map;for(let o of t){let c=a.get(o.violation)??[];c.push(o),a.set(o.violation,c)}for(let[o,c]of a){console.log(E.warn(` ${o} (${c.length})`));for(let l of c.slice(0,10))console.log(` ${l.session_id.slice(0,8)} ${E.dim("\u2192")} ${JSON.stringify(l.alias)}`);c.length>10&&console.log(E.dim(` \u2026 and ${c.length-10} more (rerun with --json for the full list)`)),console.log("")}return console.log(E.dim('Remediation: `recall name <id-prefix> ""` clears a bad alias so the heuristic title takes over,\nor `recall name <id-prefix> "<actual tab name>"` sets it to the real value.')),1}function go(){try{let e=sn(S);return Number(e.bavail)*Number(e.bsize)}catch{return 0}}var ro,oo,on,nt,an,un=H(()=>{"use strict";st();L();F();ro=["VS Code","VS Code Insiders","Cursor","Windsurf","Warp","iTerm","Terminal","WezTerm","Windows Terminal","Kitty","Alacritty"],oo=new RegExp(`^(${ro.map(e=>e.replace(/ /g,"\\s")).join("|")})\\s\xB7\\s`);on=5*6e4,nt=24,an=.5});var pn={};Ie(pn,{runOptimize:()=>To});import{existsSync as _o,readFileSync as fo}from"node:fs";import{join as ho}from"node:path";function Eo(){let e=ho(S,"daemon.pid");if(!_o(e))return!1;try{let s=parseInt(fo(e,"utf-8").trim(),10);return!Number.isFinite(s)||s<=0?!1:(process.kill(s,0),!0)}catch{return!1}}async function ue(e,s){let t=Date.now();try{return s(),{step:e,ok:!0,durationMs:Date.now()-t}}catch(n){return{step:e,ok:!1,durationMs:Date.now()-t,error:n.message}}}async function To(e={}){let s=p(),t=[];if(e.vacuum&&Eo())return e.json?(process.stdout.write(JSON.stringify({ok:!1,error:"daemon-running",message:"VACUUM requires the daemon to be stopped. Run `recall stop`, then re-run with --vacuum."},null,2)+`
656
+ `),2):(console.error(E.err("\u2717 VACUUM requires the daemon to be stopped. Run `recall stop` first, then re-run with --vacuum.")),2);t.push(await ue("wal_checkpoint(TRUNCATE)",()=>{s.pragma("wal_checkpoint(TRUNCATE)")})),t.push(await ue("messages_fts optimize",()=>{s.exec("INSERT INTO messages_fts(messages_fts) VALUES('optimize');")})),t.push(await ue("sessions_fts optimize",()=>{s.exec("INSERT INTO sessions_fts(sessions_fts) VALUES('optimize');")})),t.push(await ue("PRAGMA optimize",()=>{s.exec("PRAGMA optimize")})),e.vacuum&&t.push(await ue("VACUUM",()=>{s.exec("VACUUM")}));let n=t.filter(i=>!i.ok);if(e.json)return process.stdout.write(JSON.stringify({ok:n.length===0,steps:t,vacuum:!!e.vacuum},null,2)+`
657
+ `),n.length===0?0:1;for(let i of t){let r=i.ok?E.ok("\u2713"):E.err("\u2717"),a=`${i.durationMs} ms`;console.log(` ${r} ${i.step.padEnd(28)} ${E.dim(a)}`),i.error&&console.log(` ${E.err(i.error)}`)}return n.length===0?(console.log(""),console.log(E.ok("All maintenance passes completed.")),e.vacuum||console.log(E.dim(" Run `recall optimize --vacuum` (with the daemon stopped) to reclaim disk pages from deleted rows.")),0):(console.log(""),console.log(E.warn(`${n.length} step(s) failed \u2014 review the errors above.`)),1)}var mn=H(()=>{"use strict";st();L();F()});L();import{McpServer as So}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as bo}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as T}from"zod";var Tn=/<(local-command-caveat|local-command-stdout|command-name|command-message|command-args|system-reminder|user-prompt-submit-hook|task-notification)[\s\S]*?<\/\1>/gi,Sn=/⚡ \*\*Tool call · `[^`]+`\*\*\s*\n+```json\n[\s\S]*?\n```/g,bn=/\*\*Tool result\*\*\s*\n+```\n[\s\S]*?\n```/g;function Rn(e){return e.replace(Tn,"").trim()}function Nn(e){let s=e.replace(Sn,"[tool call]");return s=s.replace(bn,"[tool result]"),s=s.replace(/_\(unknown block: thinking\)_/g,""),s=s.replace(/(?:\[tool call\]|\[tool result\])(?:\s*(?:\[tool call\]|\[tool result\]))+/g,"[tool activity]"),s=s.replace(/\n{3,}/g,`
658
+
659
+ `),s.trim()}function yn(e){return e.role??e.type??"message"}function ut(e,s,t={}){let n=t.mode??"condensed",i=t.includeSidechain===!0,r=t.since?Date.parse(t.since):0,a=s.filter(d=>!(!i&&d.is_sidechain===1||r&&d.timestamp&&Date.parse(d.timestamp)<r)),o=[];t.prelude&&(o.push(t.prelude.trim()),o.push("")),o.push(`# Claude Recall, past session context (${n})`),o.push(""),o.push(`- **Project**: ${e.project_name}`),e.decoded_path&&o.push(`- **Path**: \`${e.decoded_path}\``),o.push(`- **Session ID**: \`${e.id}\``),e.started_at&&o.push(`- **Started**: ${e.started_at}`),e.ended_at&&o.push(`- **Ended**: ${e.ended_at}`),e.git_branch&&o.push(`- **Branch**: \`${e.git_branch}\``),o.push(`- **Messages**: ${a.length}`),o.push(""),o.push("> This is a transcript of a previous Claude Code session, included for context. Refer back to it when the user asks about past decisions, code written, or problems debugged in this work."),o.push(""),o.push("---"),o.push("");let c=0,l=0;for(let d of a){let u=d.content_text??"",m=Rn(u);n==="condensed"&&(m=Nn(m));let g=m.length>0,_=!!d.tool_names&&d.tool_names.length>0;if(!g&&!_){l+=1;continue}let f=yn(d),h=d.timestamp?` \`${d.timestamp}\``:"";o.push(`## ${f}${h}`),o.push(""),_&&n==="condensed"&&(o.push(`_tools used: ${d.tool_names}_`),o.push("")),g&&(o.push(m),o.push("")),c+=1}return o.push("---"),o.push(""),o.push(`_${c} messages included_`+(l?`, ${l} empty skipped`:"")+(i?"":", subagent/sidechain hidden")+"."),o.join(`
660
+ `)}fe();Ce();import{existsSync as In,mkdirSync as qo,readFileSync as xn,writeFileSync as Qo,chmodSync as Zo}from"node:fs";import{homedir as Cn}from"node:os";import{join as _t}from"node:path";import{z}from"zod";function vn(){return process.env.RECALL_HOME??_t(Cn(),".recall")}function Dn(){return _t(vn(),"config.json")}var kn=z.object({enabled:z.boolean().default(!1),backend:z.enum(["api","mcp"]).default("api"),apiKey:z.string().optional(),model:z.string().default("claude-opus-4-7"),maxTagsPerSession:z.number().int().min(1).max(10).default(4),minTagsPerSession:z.number().int().min(1).max(10).default(2),autopilot:z.boolean().default(!1)}),ve={enabled:!1,backend:"api",model:"claude-opus-4-7",maxTagsPerSession:4,minTagsPerSession:2,autopilot:!1};function Fn(){let e=Dn();if(!In(e))return{};try{return JSON.parse(xn(e,"utf8"))}catch(s){return console.error("[auto-tag-config] failed to parse config.json, using defaults:",s),{}}}function De(){let e=Fn().autoTag;if(!e)return{...ve};let s=kn.safeParse({...ve,...e});return s.success?s.data:{...ve}}Fe();L();fe();import{z as I}from"zod";L();F();import{writeFileSync as zn}from"node:fs";import{join as Kn}from"node:path";var Jn=Kn(S,"aliases.json");function ht(e){try{let s=JSON.parse(e);if(Array.isArray(s))return s}catch{}return[]}function Vn(){return p().prepare("SELECT session_id, alias, updated_at, previous_aliases FROM session_aliases").all().map(s=>({session_id:s.session_id,alias:s.alias,updated_at:s.updated_at,previous_aliases:ht(s.previous_aliases)}))}function Et(e,s){let t=s.trim();if(!t)throw new Error("alias must be non-empty");let n=p(),i=new Date().toISOString(),r=n.prepare("SELECT alias, previous_aliases FROM session_aliases WHERE session_id = ?").get(e),a=[];return r&&(a=ht(r.previous_aliases),r.alias!==t&&a.push({alias:r.alias,replaced_at:i})),n.prepare(`INSERT INTO session_aliases (session_id, alias, updated_at, previous_aliases)
661
+ VALUES (?, ?, ?, ?)
662
+ ON CONFLICT(session_id) DO UPDATE SET
663
+ alias = excluded.alias,
664
+ updated_at = excluded.updated_at,
665
+ previous_aliases = excluded.previous_aliases`).run(e,t,i,JSON.stringify(a)),qn(),{session_id:e,alias:t,updated_at:i,previous_aliases:a}}function qn(){try{x();let e=Vn(),s={schema:"claude-recall.aliases.v1",backed_up_at:new Date().toISOString(),aliases:e};zn(Jn,JSON.stringify(s,null,2))}catch(e){console.error("[aliases] backup failed:",e)}}L();F();import{writeFileSync as Qn,mkdirSync as Zn,existsSync as ei}from"node:fs";import{join as Tt}from"node:path";var Me=Tt(S,"notes");function St(e){try{let s=JSON.parse(e);if(Array.isArray(s))return s}catch{}return[]}function ti(e){if(!e)return[];try{let s=JSON.parse(e);return Array.isArray(s)?s.filter(t=>!!t&&typeof t=="object"&&typeof t.synopsis=="string"&&typeof t.replaced_at=="string"):[]}catch{return[]}}function si(){x(),ei(Me)||Zn(Me,{recursive:!0})}function ni(e){return{session_id:e.session_id,content:e.content,updated_at:e.updated_at,previous_versions:St(e.previous_versions),auto_synopsis:e.auto_synopsis??null,auto_synopsis_generated_at:e.auto_synopsis_generated_at??null,auto_synopsis_history:ti(e.auto_synopsis_history)}}var ii="session_id, content, updated_at, previous_versions, auto_synopsis, auto_synopsis_generated_at, auto_synopsis_history";function Ue(e){let s=p().prepare(`SELECT ${ii} FROM session_notes WHERE session_id = ?`).get(e);return s?ni(s):null}function bt(e,s){let t=p(),n=new Date().toISOString(),i=t.prepare("SELECT content, previous_versions FROM session_notes WHERE session_id = ?").get(e),r=[];return i&&(r=St(i.previous_versions),i.content!==s&&i.content.length>0&&r.push({content:i.content,replaced_at:n})),t.prepare(`INSERT INTO session_notes (session_id, content, updated_at, previous_versions)
666
+ VALUES (?, ?, ?, ?)
667
+ ON CONFLICT(session_id) DO UPDATE SET
668
+ content = excluded.content,
669
+ updated_at = excluded.updated_at,
670
+ previous_versions = excluded.previous_versions`).run(e,s,n,JSON.stringify(r)),ri(e,s,n),Ue(e)??{session_id:e,content:s,updated_at:n,previous_versions:r,auto_synopsis:null,auto_synopsis_generated_at:null,auto_synopsis_history:[]}}function ri(e,s,t){try{si();let n=Tt(Me,`${e}.md`),i=`<!-- Claude Recall note \xB7 session ${e} \xB7 updated ${t} -->
671
+ `;Qn(n,i+s)}catch(n){console.error("[notes] mirror write failed:",n)}}L();F();import{randomUUID as oi}from"node:crypto";import{writeFileSync as ai,readFileSync as Ea,existsSync as Ta}from"node:fs";import{join as ci}from"node:path";var li=ci(S,"collections.json"),Rt=8;function di(e){return{...e}}function Pe(e,s,t,n=null,i=new Date().toISOString()){p().prepare(`INSERT INTO collection_events (collection_id, session_id, action, payload, at)
672
+ VALUES (?, ?, ?, ?, ?)`).run(e,n,s,t?JSON.stringify(t):null,i)}function ui(e){let s=p().prepare("SELECT * FROM collections WHERE id = ?").get(e);if(!s)throw new Error(`collection not found: ${e}`);return s}function pi(e){if(!e)return 0;let s=0,t=e,n=new Set,i=p();for(;t;){if(n.has(t))throw new Error("collection cycle detected");n.add(t);let r=i.prepare("SELECT parent_id FROM collections WHERE id = ?").get(t);if(!r)break;s+=1,t=r.parent_id}return s}function Nt(e){let s=p().prepare("SELECT * FROM collections WHERE id = ?").get(e);return s?di(s):null}function yt(e){let s=(e.name??"").trim();if(!s)throw new Error("name required");if(s.length>120)throw new Error("name too long (max 120 chars)");let t=p(),n=new Date().toISOString(),i=oi();if(e.parent_id){if(!Nt(e.parent_id))throw new Error("parent collection not found");if(pi(e.parent_id)>=Rt-1)throw new Error(`max collection depth is ${Rt}`)}return t.transaction(()=>{t.prepare(`INSERT INTO collections
673
+ (id, name, description, icon, color, parent_id, sort_key, created_at, updated_at, archived_at)
674
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(i,s,e.description??null,e.icon??null,e.color??null,e.parent_id??null,e.sort_key??"",n,n),Pe(i,"create",{name:s,parent_id:e.parent_id??null,icon:e.icon??null,color:e.color??null},null,n)})(),je(),Nt(i)}function wt(e,s,t=null,n={}){let i=p();if(ui(e),!i.prepare("SELECT 1 FROM sessions WHERE id = ?").get(s))throw new Error(`session not found: ${s}`);if(i.prepare("SELECT 1 FROM collection_sessions WHERE collection_id = ? AND session_id = ?").get(e,s))return{added:!1};let o=n.source??"manual",c=n.rule_id??null;if(o==="auto"&&!c)throw new Error("auto membership requires a rule_id");let l=new Date().toISOString();return i.transaction(()=>{i.prepare(`INSERT INTO collection_sessions (collection_id, session_id, added_at, note, source, rule_id)
675
+ VALUES (?, ?, ?, ?, ?, ?)`).run(e,s,l,t,o,c),Pe(e,"add",{note:t,source:o,rule_id:c},s,l)})(),je(),{added:!0}}function Lt(e,s){let t=p();if(!t.prepare("SELECT 1 FROM collection_sessions WHERE collection_id = ? AND session_id = ?").get(e,s))return{removed:!1};let i=new Date().toISOString();return t.transaction(()=>{t.prepare("DELETE FROM collection_sessions WHERE collection_id = ? AND session_id = ?").run(e,s),Pe(e,"remove",null,s,i)})(),je(),{removed:!0}}function mi(){return p().prepare(`SELECT id, collection_id, session_id, action, payload, at
676
+ FROM collection_events
677
+ ORDER BY at ASC, id ASC`).all()}function je(){try{x();let e=p(),s=e.prepare(`SELECT id, name, description, icon, color, parent_id, sort_key,
678
+ created_at, updated_at, archived_at
679
+ FROM collections
680
+ ORDER BY COALESCE(parent_id, ''), sort_key, LOWER(name)`).all(),t=e.prepare(`SELECT collection_id, session_id, added_at, note, source, rule_id
681
+ FROM collection_sessions
682
+ ORDER BY collection_id, added_at`).all(),n=mi(),i={schema:"claude-recall.collections.v1",backed_up_at:new Date().toISOString(),collections:s,memberships:t,events:n};ai(li,JSON.stringify(i,null,2))}catch(e){console.error("[collections] backup failed:",e)}}L();var Xe=60,gi=6e4,se=class extends Error{retryAfterMs;constructor(s){super(`MCP write rate limit exceeded \u2014 try again in ${Math.ceil(s/1e3)}s`),this.name="RateLimitError",this.retryAfterMs=s}};function $e(e){let s=new Date().toISOString(),t;try{t=JSON.stringify(e.args??null)}catch{t='"<unserializable>"'}p().prepare(`INSERT INTO mcp_audit_events (tool, args_json, result, error_message, caller, at)
683
+ VALUES (?, ?, ?, ?, ?, ?)`).run(e.tool,t,e.result,e.errorMessage??null,e.caller??null,s)}var K=class{capacity;windowMs;hits=[];constructor(s=Xe,t=gi){this.capacity=s,this.windowMs=t}consume(s=Date.now()){if(this.evict(s),this.hits.length>=this.capacity){let t=this.hits[0],n=Math.max(1,t+this.windowMs-s);throw new se(n)}this.hits.push(s)}remaining(s=Date.now()){return this.evict(s),Math.max(0,this.capacity-this.hits.length)}reset(){this.hits.length=0}evict(s){let t=s-this.windowMs;for(;this.hits.length>0&&this.hits[0]<t;)this.hits.shift()}};async function y(e){try{e.limiter.consume()}catch(s){throw s instanceof se&&$e({tool:e.tool,args:e.args,result:"rate_limited",errorMessage:s.message,caller:e.caller}),s}try{let s=await e.run();return $e({tool:e.tool,args:e.args,result:"ok",caller:e.caller}),s}catch(s){let t=s instanceof Error?s.message:String(s);throw $e({tool:e.tool,args:e.args,result:"error",errorMessage:t,caller:e.caller}),s}}L();import{z as _i}from"zod";function $(e){let s=e.trim();if(s.length<4)return null;let t=p();if(s.length>=32)return t.prepare("SELECT id FROM sessions WHERE id = ?").get(s)?.id??null;let n=t.prepare("SELECT id FROM sessions WHERE id LIKE ? LIMIT 2").all(`${s}%`);return n.length===1?n[0].id:null}function Ee(e){return{content:[{type:"text",text:e}],isError:!0}}function R(e){return e instanceof se?Ee(e.message):e instanceof _i.ZodError?Ee(`invalid input: ${e.message}`):e instanceof Error&&e.message.startsWith("SQLITE_")?Ee("database constraint error"):Ee(e instanceof Error?e.message:String(e))}function Q(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function J(e){return{content:[{type:"text",text:e}],isError:!0}}var fi=`
684
+
685
+ ---
686
+
687
+ `,hi=5e5;function At(e,s={}){let t=s.limiter??new K;e.registerTool("add_tag",{title:"Add tag to a session",description:"Apply a single tag to a session. Tags are normalized server-side (lowercase, hyphens, strip #). Idempotent.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),tag:I.string().min(1).describe("Tag to add. Normalized: lowercase, dashes, max 40 chars.")}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let r=await y({tool:"add_tag",args:{sessionId:i,tag:n.tag},limiter:t,run:()=>ge(i,n.tag)});return Q({sessionId:i,...r})}catch(i){return R(i)}}),e.registerTool("remove_tag",{title:"Remove tag from a session",description:"Remove a single tag from a session. The removal is recorded in tag_events (append-only) so history is preserved.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),tag:I.string().min(1).describe("Tag to remove (normalized server-side).")}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let r=q(n.tag);if(!r)return J("tag must contain at least one alphanumeric character");let a=await y({tool:"remove_tag",args:{sessionId:i,tag:r},limiter:t,run:()=>pt(i,r)});return Q({sessionId:i,...a})}catch(i){return R(i)}}),e.registerTool("set_alias",{title:"Set session alias",description:"Set a human-friendly alias for a session. Previous alias is archived to previous_aliases (never destroyed). Session UUID remains the immutable primary key.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),alias:I.string().min(1).max(120).describe("New alias (non-empty, max 120 chars).")}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let r=await y({tool:"set_alias",args:{sessionId:i,alias:n.alias},limiter:t,run:()=>Et(i,n.alias)});return Q(r)}catch(i){return R(i)}}),e.registerTool("append_note",{title:"Append to session note",description:"Append markdown to a session note. If a note already exists, the new content is added below a `---` separator. Previous version is archived in previous_versions.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),markdown:I.string().min(1).max(5e4).describe("Markdown to append.")}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let r=await y({tool:"append_note",args:{sessionId:i,length:n.markdown.length},limiter:t,run:()=>{let a=Ue(i),o=a&&a.content.length>0?`${a.content}${fi}${n.markdown}`:n.markdown;if(o.length>hi)throw new Error(`note would exceed the 500 KB cumulative limit (current: ${a?.content.length??0} bytes, adding: ${n.markdown.length} bytes)`);return bt(i,o)}});return Q(r)}catch(i){return R(i)}}),e.registerTool("create_collection",{title:"Create a collection",description:"Create a new collection for grouping sessions. Optionally nest under a parent. Soft-deletion only.",inputSchema:{name:I.string().min(1).max(120),parent_id:I.string().optional().describe("Parent collection id (omit for root)."),icon:I.string().max(32).optional(),color:I.string().max(32).optional(),description:I.string().max(2e3).optional()}},async n=>{try{let i=await y({tool:"create_collection",args:n,limiter:t,run:()=>yt({name:n.name,parent_id:n.parent_id??null,icon:n.icon??null,color:n.color??null,description:n.description??null})});return Q(i)}catch(i){return R(i)}}),e.registerTool("add_session_to_collection",{title:"Add session to collection",description:"Add a session to a collection. Idempotent: re-adding returns added=false.",inputSchema:{collectionId:I.string().min(1),sessionId:I.string().describe("Full session UUID or 8+-character prefix."),note:I.string().max(2e3).optional()}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);if(!p().prepare("SELECT 1 FROM collections WHERE id = ?").get(n.collectionId))return J("collection not found");let o=await y({tool:"add_session_to_collection",args:{collectionId:n.collectionId,sessionId:i,note:n.note??null},limiter:t,run:()=>wt(n.collectionId,i,n.note??null)});return Q({collectionId:n.collectionId,sessionId:i,...o})}catch(i){return R(i)}}),e.registerTool("remove_session_from_collection",{title:"Remove session from collection",description:"Remove a session from a collection. The removal is recorded in collection_events (append-only); the underlying session is untouched.",inputSchema:{collectionId:I.string().min(1),sessionId:I.string().describe("Full session UUID or 8+-character prefix.")}},async n=>{try{let i=$(n.sessionId);if(!i)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let r=await y({tool:"remove_session_from_collection",args:{collectionId:n.collectionId,sessionId:i},limiter:t,run:()=>Lt(n.collectionId,i)});return Q({collectionId:n.collectionId,sessionId:i,...r})}catch(i){return R(i)}})}import{z as M}from"zod";L();F();import{randomUUID as Ot}from"node:crypto";import{writeFileSync as It,readFileSync as Xa,existsSync as Ei,mkdirSync as Ti}from"node:fs";import{join as He}from"node:path";var Te=He(S,"threads"),Si=He(Te,"index.json");function xt(){x(),Ei(Te)||Ti(Te,{recursive:!0})}function Be(e,s,t){return{id:e.id,name:e.name,summary:e.summary,created_at:e.created_at,closed_at:e.closed_at,archived:e.archived===1,session_count:s.session_count,origin_count:s.origin_count,project:t?.project??null,project_count:t?.project_count??0,folder_id:e.folder_id??null}}function Ct(e){let s=new Map;if(e.length===0)return s;let t=p(),n=e.map(()=>"?").join(","),i=t.prepare(`SELECT te.thread_id AS thread_id,
688
+ p.name AS project,
689
+ COUNT(*) AS n,
690
+ SUM(CASE WHEN te.role = 'origin' THEN 1 ELSE 0 END) AS origin_n
691
+ FROM thread_edges te
692
+ LEFT JOIN sessions s ON s.id = te.session_id
693
+ LEFT JOIN projects p ON p.id = s.project_id
694
+ WHERE te.thread_id IN (${n})
695
+ GROUP BY te.thread_id, p.name`).all(...e),r=new Map;for(let a of i){let o=r.get(a.thread_id);o||(o=[],r.set(a.thread_id,o)),o.push(a)}for(let[a,o]of r){let c=o.filter(u=>u.project!==null),l=c.length,d=null;c.length>0&&(d=[...c].sort((m,g)=>g.n-m.n||g.origin_n-m.origin_n||(m.project??"").localeCompare(g.project??""))[0].project),s.set(a,{project:d,project_count:l})}return s}function vt(e){let s=e.auto_title_source;return{thread_id:e.thread_id,session_id:e.session_id,parent_session_id:e.parent_session_id,role:e.role,confidence:e.confidence,source:e.source,added_at:e.added_at,alias:e.alias,auto_title:e.auto_title,auto_title_source:s==="agent"||s==="heuristic"?s:null,alias_source:e.alias?"manual":null,first_user_message:e.first_user_message,project:e.project}}function Dt(e){let t=p().prepare(`SELECT NULLIF(sa.alias, '') AS alias,
696
+ s.auto_title AS auto_title,
697
+ s.auto_title_source AS auto_title_source,
698
+ s.first_user_message AS first_user_message,
699
+ p.name AS project
700
+ FROM (SELECT ? AS sid) q
701
+ LEFT JOIN sessions s ON s.id = q.sid
702
+ LEFT JOIN session_aliases sa ON sa.session_id = q.sid
703
+ LEFT JOIN projects p ON p.id = s.project_id`).get(e),n=t?.auto_title_source??null;return{alias:t?.alias??null,auto_title:t?.auto_title??null,auto_title_source:n==="agent"||n==="heuristic"?n:null,first_user_message:t?.first_user_message??null,project:t?.project??null}}function We(e){let t=p().prepare(`SELECT
704
+ COUNT(*) AS session_count,
705
+ SUM(CASE WHEN role = 'origin' THEN 1 ELSE 0 END) AS origin_count
706
+ FROM thread_edges WHERE thread_id = ?`).get(e);return{session_count:t?.session_count??0,origin_count:t?.origin_count??0}}function B(e){let s=D(e);s&&(xt(),It(He(Te,`${e}.json`),JSON.stringify(s,null,2)),kt())}function kt(){xt();let e=Ye({includeArchived:!0});It(Si,JSON.stringify({threads:e},null,2))}function Se(e){let s=e.name.trim();if(!s)throw new Error("thread name cannot be empty");let t=p(),n=Ot(),i=new Date().toISOString();t.prepare("INSERT INTO threads (id, name, summary, created_at) VALUES (?, ?, ?, ?)").run(n,s,e.summary?.trim()||null,i),e.originSessionId&&t.prepare(`INSERT INTO thread_edges (thread_id, session_id, parent_session_id, role, confidence, source, added_at)
707
+ VALUES (?, ?, NULL, 'origin', 1.0, 'manual', ?)`).run(n,e.originSessionId,i),B(n);let r=D(n);if(!r)throw new Error("thread creation succeeded but read-back failed");return r}function Ye(e={}){let s=p(),t=e.includeArchived?"":"WHERE archived = 0",n=s.prepare(`SELECT * FROM threads ${t} ORDER BY created_at DESC`).all(),i=Ct(n.map(r=>r.id));return n.map(r=>Be(r,We(r.id),i.get(r.id)))}function D(e){let s=p(),t=s.prepare("SELECT * FROM threads WHERE id = ?").get(e);if(!t)return null;let n=s.prepare(`SELECT e.*,
708
+ NULLIF(sa.alias, '') AS alias,
709
+ s.auto_title AS auto_title,
710
+ s.auto_title_source AS auto_title_source,
711
+ s.first_user_message AS first_user_message,
712
+ p.name AS project
713
+ FROM thread_edges e
714
+ LEFT JOIN sessions s ON s.id = e.session_id
715
+ LEFT JOIN session_aliases sa ON sa.session_id = e.session_id
716
+ LEFT JOIN projects p ON p.id = s.project_id
717
+ WHERE e.thread_id = ?
718
+ ORDER BY e.added_at ASC`).all(e).map(vt),i=Ct([e]).get(e);return{...Be(t,We(t.id),i),edges:n}}function Ft(e){return p().prepare(`SELECT t.* FROM threads t
719
+ JOIN thread_edges e ON e.thread_id = t.id
720
+ WHERE e.session_id = ? AND t.archived = 0
721
+ ORDER BY t.created_at DESC`).all(e).map(n=>Be(n,We(n.id)))}function be(e){let s=p();if(!s.prepare("SELECT * FROM threads WHERE id = ?").get(e.threadId))throw new Error(`thread ${e.threadId} not found`);let n=new Date().toISOString(),i=e.parentSessionId??null,r=e.role??(i?"child":"origin"),a=e.confidence??1,o=e.source??"manual";if(a<0||a>1)throw new Error("confidence must be 0..1");s.prepare(`INSERT INTO thread_edges
722
+ (thread_id, session_id, parent_session_id, role, confidence, source, added_at)
723
+ VALUES (?, ?, ?, ?, ?, ?, ?)
724
+ ON CONFLICT(thread_id, session_id) DO UPDATE SET
725
+ parent_session_id = excluded.parent_session_id,
726
+ role = excluded.role,
727
+ confidence = excluded.confidence,
728
+ source = excluded.source,
729
+ added_at = excluded.added_at`).run(e.threadId,e.sessionId,i,r,a,o,n),B(e.threadId);let c=Dt(e.sessionId);return{thread_id:e.threadId,session_id:e.sessionId,parent_session_id:i,role:r,confidence:a,source:o,added_at:n,alias:c.alias,auto_title:c.auto_title,auto_title_source:c.auto_title_source,alias_source:c.alias?"manual":null,first_user_message:c.first_user_message,project:c.project}}function Mt(e,s){let n=p().prepare("DELETE FROM thread_edges WHERE thread_id = ? AND session_id = ?").run(e,s);return n.changes>0&&B(e),{removed:n.changes}}function ae(e,s,t){let n=p(),i=n.prepare("SELECT * FROM thread_edges WHERE thread_id = ? AND session_id = ?").get(e,s);if(!i)throw new Error("edge not found; add the session first");if(t!==null){if(t===s)throw new Error("cycle detected: session cannot be its own parent");let o=n.prepare("SELECT parent_session_id FROM thread_edges WHERE thread_id = ? AND session_id = ?"),c=t,l=new Set;for(;c!==null;){if(c===s)throw new Error(`cycle detected: setting parent of ${s} to ${t} would create a loop`);if(l.has(c))break;l.add(c),c=o.get(e,c)?.parent_session_id??null}}let r=t?"child":"origin";n.prepare(`UPDATE thread_edges
730
+ SET parent_session_id = ?, role = ?, added_at = ?
731
+ WHERE thread_id = ? AND session_id = ?`).run(t,r,new Date().toISOString(),e,s),B(e);let a=Dt(s);return vt({...i,parent_session_id:t,role:r,added_at:new Date().toISOString(),alias:a.alias,auto_title:a.auto_title,auto_title_source:a.auto_title_source,first_user_message:a.first_user_message,project:a.project})}function Ut(e,s){let t=s.trim();if(!t)throw new Error("name cannot be empty");p().prepare("UPDATE threads SET name = ? WHERE id = ?").run(t,e),B(e);let i=D(e);if(!i)throw new Error(`thread ${e} not found`);return i}function Pt(e){p().prepare("UPDATE threads SET closed_at = ? WHERE id = ?").run(new Date().toISOString(),e),B(e);let t=D(e);if(!t)throw new Error(`thread ${e} not found`);return t}function jt(e){p().prepare("UPDATE threads SET closed_at = NULL WHERE id = ?").run(e),B(e);let t=D(e);if(!t)throw new Error(`thread ${e} not found`);return t}function $t(e){p().prepare("UPDATE threads SET archived = 1 WHERE id = ?").run(e),B(e);let t=D(e);if(!t)throw new Error(`thread ${e} not found`);return t}function Xt(e,s){if(e===s)throw new Error("cannot merge a thread into itself");let t=p(),n=new Date().toISOString();t.transaction(()=>{let r=t.prepare("SELECT * FROM thread_edges WHERE thread_id = ?").all(e);for(let a of r)t.prepare(`INSERT INTO thread_edges
732
+ (thread_id, session_id, parent_session_id, role, confidence, source, added_at)
733
+ VALUES (?, ?, ?, ?, ?, ?, ?)
734
+ ON CONFLICT(thread_id, session_id) DO UPDATE SET
735
+ parent_session_id = COALESCE(thread_edges.parent_session_id, excluded.parent_session_id),
736
+ role = CASE WHEN thread_edges.role = 'origin' OR excluded.role = 'origin' THEN 'origin' ELSE 'child' END,
737
+ confidence = MAX(thread_edges.confidence, excluded.confidence),
738
+ source = thread_edges.source`).run(s,a.session_id,a.parent_session_id,a.role,a.confidence,a.source,n);t.prepare("DELETE FROM threads WHERE id = ?").run(e)})(),B(s),kt();let i=D(s);if(!i)throw new Error("merge destination disappeared");return i}function Ht(e){if(e.sessionIds.length===0)throw new Error("no sessions to split off");let s=p(),t=new Date().toISOString(),n=Ot();s.transaction(()=>{s.prepare("INSERT INTO threads (id, name, summary, created_at) VALUES (?, ?, NULL, ?)").run(n,e.newThreadName.trim(),t);for(let r of e.sessionIds){let a=s.prepare("SELECT * FROM thread_edges WHERE thread_id = ? AND session_id = ?").get(e.threadId,r);a&&(s.prepare(`INSERT INTO thread_edges
739
+ (thread_id, session_id, parent_session_id, role, confidence, source, added_at)
740
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(n,r,a.parent_session_id,a.role,a.confidence,a.source,t),s.prepare("DELETE FROM thread_edges WHERE thread_id = ? AND session_id = ?").run(e.threadId,r))}})(),B(e.threadId),B(n);let i=D(n);if(!i)throw new Error("split destination disappeared");return i}L();F();import{writeFileSync as Ni,mkdirSync as yi,existsSync as wi}from"node:fs";import{join as Gt}from"node:path";L();var Bt=80;function Wt(e){if(e.alias&&e.alias.trim())return e.alias.trim();if(e.auto_title&&e.auto_title.trim())return e.auto_title.trim();let s=(e.first_user_message??"").trim();if(!s)return e.id.slice(0,8);let t=s.split(`
741
+ `)[0].trim();return t.length>Bt?t.slice(0,Bt)+"\u2026":t}function bi(e){return p().prepare(`SELECT s.id AS id,
742
+ sa.alias AS alias,
743
+ s.auto_title AS auto_title,
744
+ s.first_user_message AS first_user_message
745
+ FROM sessions s
746
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
747
+ WHERE s.id = ?`).get(e)??null}function Ri(e){let s=bi(e);return s?Wt(s):e.slice(0,8)}function Yt(e){if(!e)return null;let s=p(),t=s.prepare(`SELECT e.thread_id AS thread_id,
748
+ t.name AS thread_name,
749
+ e.parent_session_id AS parent_session_id,
750
+ e.added_at AS added_at
751
+ FROM thread_edges e
752
+ JOIN threads t ON t.id = e.thread_id
753
+ WHERE e.session_id = ?
754
+ AND t.archived = 0
755
+ ORDER BY e.added_at DESC
756
+ LIMIT 1`).get(e);if(!t)return null;let n=t.parent_session_id?{id:t.parent_session_id,title:Ri(t.parent_session_id)}:null,i=t.parent_session_id?[e,t.parent_session_id]:[e],r=i.map(()=>"?").join(", "),o=s.prepare(`SELECT e.session_id AS session_id,
757
+ s.id AS id,
758
+ sa.alias AS alias,
759
+ s.auto_title AS auto_title,
760
+ s.first_user_message AS first_user_message
761
+ FROM thread_edges e
762
+ LEFT JOIN sessions s ON s.id = e.session_id
763
+ LEFT JOIN session_aliases sa ON sa.session_id = e.session_id
764
+ WHERE e.thread_id = ?
765
+ AND e.session_id NOT IN (${r})
766
+ ORDER BY e.added_at ASC`).all(t.thread_id,...i).map(c=>({id:c.session_id,title:c.id?Wt(c):c.session_id.slice(0,8)}));return{thread_id:t.thread_id,thread_name:t.thread_name,parent_session:n,siblings:o}}var ze=Gt(S,"titles");var ce=5,Re=15,Li=500;function zt(e){if(!e)return[];try{let s=JSON.parse(e);if(Array.isArray(s))return s.filter(t=>!!t&&typeof t=="object"&&typeof t.title=="string"&&typeof t.replaced_at=="string")}catch{}return[]}function Kt(e){let s=p(),t=s.prepare(`SELECT rowid AS rid, content_text
767
+ FROM messages
768
+ WHERE session_id = ? AND role = 'user' AND is_sidechain = 0
769
+ AND content_text IS NOT NULL AND content_text != ''
770
+ ORDER BY COALESCE(timestamp, ''), rowid ASC
771
+ LIMIT ?`).all(e,ce),n=s.prepare(`SELECT rowid AS rid, content_text
772
+ FROM messages
773
+ WHERE session_id = ? AND role = 'user' AND is_sidechain = 0
774
+ AND content_text IS NOT NULL AND content_text != ''
775
+ ORDER BY COALESCE(timestamp, '') DESC, rowid DESC
776
+ LIMIT ?`).all(e,Re),i=new Map;for(let d of t)i.set(d.rid,d.content_text);for(let d of n)i.set(d.rid,d.content_text);if(i.size===0)throw new Error("no user messages available to summarise");let r=Array.from(i.entries()).sort((d,u)=>d[0]-u[0]).map(([,d])=>({content_text:d})),a=t.length===ce&&n.length===Re&&i.size===ce+Re,o=r.map((d,u)=>{let m=(d.content_text??"").slice(0,Li);return a&&u===ce?`--- (middle of session omitted) ---
777
+ ${u+1}. ${m}`:`${u+1}. ${m}`}).join(`
778
+ `),c=null;try{c=Yt(e)}catch(d){console.error("[autoTitle] thread context resolution failed:",d),c=null}let l=[];return c&&(l.push(Ai(c)),l.push("")),l.push(`You will receive a sample of user messages from a Claude Code session: the first ${ce}`,`messages (initial intent) and the last ${Re} messages (current direction).`,"Write a single descriptive title, max 50 characters, focused on what the user is","currently trying to accomplish. If initial intent and current direction differ, prefer","the current direction. Output ONLY the title, with no quotes and no trailing punctuation.","","Messages:",o),l.join(`
779
+ `)}var Ge=5;function Ai(e){let s=[];s.push("Thread context:"),s.push(`- This session is part of thread "${e.thread_name}".`),e.parent_session&&s.push(`- Parent session: "${e.parent_session.title}"`);let t=e.siblings.length;if(t>0){let i=e.siblings.slice(0,Ge).map(a=>`"${a.title}"`).join(", "),r=t>Ge?`, and ${t-Ge} more`:"";s.push(`- Sibling sessions (${t}): ${i}${r}`)}return s.push(""),s.push("Generate a title that reflects this session's role in the thread."),s.push('If siblings use a pattern like "Phase A / Phase B" or "Wave 1 of 4",'),s.push("follow the same pattern."),s.join(`
780
+ `)}function Jt(e,s,t){let n=s.trim();if(!n)return;let i=p(),r=i.prepare(`SELECT auto_title, auto_title_source, auto_title_generated_at, auto_title_history
781
+ FROM sessions WHERE id = ?`).get(e);if(!r||t==="heuristic"&&r.auto_title_source==="agent"&&r.auto_title||r.auto_title===n&&r.auto_title_source===t)return;let a=zt(r.auto_title_history),o=new Date().toISOString();r.auto_title&&r.auto_title_source&&a.push({title:r.auto_title,source:r.auto_title_source,replaced_at:o}),i.prepare(`UPDATE sessions
782
+ SET auto_title = ?,
783
+ auto_title_source = ?,
784
+ auto_title_generated_at = ?,
785
+ auto_title_history = ?
786
+ WHERE id = ?`).run(n,t,Date.now(),JSON.stringify(a),e),Ii(e,n,t,o)}function Vt(e){let s=p().prepare(`SELECT auto_title, auto_title_source, auto_title_generated_at, auto_title_history
787
+ FROM sessions WHERE id = ?`).get(e);return s?{auto_title:s.auto_title,auto_title_source:s.auto_title_source??null,auto_title_generated_at:s.auto_title_generated_at,auto_title_history:zt(s.auto_title_history)}:null}function Oi(){x(),wi(ze)||yi(ze,{recursive:!0})}function Ii(e,s,t,n){try{Oi();let i=Gt(ze,`${e}.txt`),r=`# Claude Recall auto-title \xB7 session ${e} \xB7 source ${t} \xB7 updated ${n}
788
+ `;Ni(i,r+s+`
789
+ `)}catch(i){console.error("[autoTitle] mirror write failed:",i)}}var Bi=50;function Wi(e){if(e.length===0)return[];let s=[...e].sort((c,l)=>c.added_at<l.added_at?-1:c.added_at>l.added_at?1:0),t=new Map,n=new Set,i=[];for(let c of s)if(c.parent_session_id){let l=t.get(c.parent_session_id)??[];l.push(c),t.set(c.parent_session_id,l)}let r=s.filter(c=>c.role==="origin"),o=[...r.length>0?r:s.filter(c=>!c.parent_session_id)];for(;o.length>0;){let c=o.shift();if(n.has(c.session_id))continue;n.add(c.session_id),i.push(c.session_id);let l=t.get(c.session_id)??[];for(let d of l)n.has(d.session_id)||o.push(d)}for(let c of s)n.has(c.session_id)||(n.add(c.session_id),i.push(c.session_id));return i}function Yi(e){let s=Kt(e.sessionId),t=["",`BULK CONTEXT: You are titling session ${e.current} of ${e.total} in this thread.`,"The parent and earlier siblings already have titles (shown above). YOUR JOB:","",'1. Identify the naming pattern. If parent is "Build Feature X" and earlier',' siblings are "Phase A: API design" and "Phase B: client wiring", the pattern',' is "Phase {LETTER}: {topic}".',"2. If no pattern is yet established (this is the first child), INVENT a pattern",' that will scale to N children. Good patterns: "Phase A/B/C", "Wave 1 of M",',' "Step N", or a domain-specific structure if the thread name suggests one.',"3. Output a title that follows the pattern AND describes what THIS session"," does in 50 characters max.","","Output ONLY the title, no quotes, no trailing punctuation."].join(`
790
+ `);return`${s}
791
+ ${t}`}function Gi(e){return Vt(e)?.auto_title_source??null}async function zi(e){let{spawnClaudePrompt:s,isClaudeCliAvailable:t}=await Promise.resolve().then(()=>(ss(),ts));if(!t())throw new Error("claude CLI not found on PATH");let n=await s(e.prompt,[],{model:e.model});if(!n.success)throw new Error(`claude CLI exited ${n.exitCode}: ${n.stderr.slice(-500)}`);let i=Ki(n.stdout);if(!i)throw new Error("claude CLI returned an empty title");return i.slice(0,Bi)}function Ki(e){let s=e.trim();if(!s)return"";try{let t=JSON.parse(s);if(t&&typeof t=="object"){let n=t.result;if(typeof n=="string")return ns(n)}}catch{}return ns(s)}function ns(e){return e.trim().replace(/^["'`]+|["'`]+$/g,"").replace(/\s+/g," ").replace(/[.!?]+$/g,"").trim()}async function rs(e,s={}){let t=D(e);if(!t)throw new Error(`thread not found: ${e}`);let n=Wi(t.edges),i=n.length,r=s.force??!1,a=s.signal,o=[],c=[],l=[];for(let d=0;d<n.length;d++){let u=n[d],m=d+1;if(a?.aborted){let _={sessionId:u,reason:"cancelled"};c.push(_),s.onSkipped?.(_);continue}if(!r&&Gi(u)==="agent"){let _={sessionId:u,reason:"already-titled"};c.push(_),s.onSkipped?.(_);continue}let g;try{if(is)g=await is({sessionId:u,current:m,total:i});else{let _=Yi({sessionId:u,current:m,total:i});g=await zi({prompt:_,model:s.model})}}catch(_){let f=_ instanceof Error?_.message:String(_??"unknown error"),h={sessionId:u,error:f};l.push(h),s.onFailed?.(h);continue}try{Jt(u,g,"agent")}catch(_){let f=_ instanceof Error?_.message:String(_??"unknown error"),h={sessionId:u,error:`setAutoTitle failed: ${f}`};l.push(h),s.onFailed?.(h);continue}o.push(u),s.onProgress?.({current:m,total:i,sessionId:u,title:g})}return{generated:o,skipped:c,failed:l}}var is=null;L();import{execFile as lr}from"node:child_process";import{promisify as dr}from"node:util";import{readlink as ur,readFile as us}from"node:fs/promises";import{platform as ye}from"node:os";import{readFileSync as Ji,statSync as Vi}from"node:fs";var qi=200*1024*1024;var Je=.5,cs=Je,Qi=[{maxGapMs:3600*1e3,weight:.7,label:"<1h gap"},{maxGapMs:14400*1e3,weight:.4,label:"<4h gap"},{maxGapMs:1440*60*1e3,weight:.2,label:"<24h gap"}],Zi=["let's commit","lets commit","now commit","inspect the diff","inspect this diff","review the diff","review this diff","diff of all changes","diff of changes","based on what we just did","based on the things done","based on all the things","continue from","continuing from","pick up where","next step","now fix","now lets","now let's","from the previous session","from our last session","from the last session"];function os(e){if(!e)return null;if(e.startsWith("/")){let t=e.split(" \xB7 ");if(t.length>1)return t[1].trim().toLowerCase()}let s=e.split(" \xB7 ");return s.length>1?s[s.length-1].trim().toLowerCase():null}function er(e,s){let t=s-e;if(t<0)return{weight:0,label:null};for(let n of Qi)if(t<=n.maxGapMs)return{weight:n.weight,label:n.label};return{weight:0,label:null}}function tr(e){if(e.length===0)return{weight:0,matched:null,matchedIndex:-1};let s=[.4,.35,.3,.25,.2,.15,.1],t=Math.min(e.length,s.length);for(let n=0;n<t;n++){let i=e[n];if(!i)continue;let r=i.toLowerCase();for(let a of Zi)if(r.includes(a))return{weight:s[n],matched:a,matchedIndex:n}}return{weight:0,matched:null,matchedIndex:-1}}function Ke(e,s){if(!e||!s||e.length!==s.length)return 0;let t=0;for(let n=0;n<e.length;n++)t+=e[n]*s[n];return t<-1?-1:t>1?1:t}function sr(e,s){if(e.length===0||s.length===0)return 0;let t=0;for(let n of e)for(let i of s){let r=Ke(n,i);r>t&&(t=r)}return t}function nr(e,s){let t=Ke(e.mean_embedding,s.mean_embedding),n=Ke(e.tail_pool,s.head_pool),i=sr(e.sample_chunks,s.sample_chunks),r=0,a=null;if(t>r&&(r=t,a="mean"),n>r&&(r=n,a="asymmetric"),i>r&&(r=i,a="max_pool"),r<.65)return{weight:0,cosine:r,mode:null};if(r>=.85)return{weight:.3,cosine:r,mode:a};let o=(r-.65)/.2*.3;return{weight:Math.round(o*100)/100,cosine:r,mode:a}}function ir(e,s){return e.cluster_id===null||s.cluster_id===null?{weight:0,same:!1}:e.cluster_id!==s.cluster_id?{weight:0,same:!1}:{weight:.05,same:!0}}function rr(e,s){if(e.size===0||s.size===0)return{weight:0,count:0};let t=0;for(let i of s)e.has(i)&&t++;return t===0?{weight:0,count:0}:{weight:Math.min(.4,t*.1),count:t}}function or(e,s){let t=os(e),n=os(s);return t&&n&&t===n?{weight:.1,brand:t}:{weight:0,brand:null}}function as(e){return e.replace(/\s+/g," ").trim().toLowerCase()}function ar(e,s){let t=0,n=null,i=!1;if(e.authored_paths.size>0){let r=s.recent_user_messages.slice(0,3).join(`
792
+ `).toLowerCase();for(let a of e.authored_paths){let o=a.toLowerCase();if(s.touched_files.has(a)||r.includes(o)){t+=.5,n=a;break}}}if(e.authored_content.length>0&&s.recent_user_messages[0]){let r=as(s.recent_user_messages[0]);if(r.length>=200)for(let a of e.authored_content){let o=as(a);if(o.length<200)continue;let c=Math.min(o.length,240),l=o.slice(0,c);if(r.includes(l)){t+=.4,i=!0;break}}}return{weight:Math.min(.6,t),pathMatch:n,contentMatch:i}}function cr(e,s,t=cs){if(s.started_at_ms<=e.started_at_ms)return null;let n=e.ended_at_ms??e.started_at_ms;if(s.started_at_ms<n)return null;let i=er(n,s.started_at_ms),r=s.recent_user_messages.length>0?s.recent_user_messages:s.first_user_message?[s.first_user_message]:[],a=tr(r),o=rr(e.touched_files,s.touched_files),c=or(e.auto_title,s.auto_title),l=nr(e,s),d=ir(e,s),u=ar(e,s),m=i.weight+a.weight+o.weight+c.weight+l.weight+d.weight+u.weight;if(m<t)return null;let g=[];if(i.label&&g.push(`temporal ${i.label} (+${i.weight})`),a.matched){let _=a.matchedIndex===0?"opening message":`message #${a.matchedIndex+1}`;g.push(`continuation phrase "${a.matched}" in ${_} (+${a.weight})`)}if(o.count>0&&g.push(`${o.count} file${o.count===1?"":"s"} overlap (+${o.weight.toFixed(1)})`),c.brand&&g.push(`shared brand "${c.brand}" (+${c.weight})`),l.weight>0&&l.mode&&g.push(`semantic ${l.mode==="asymmetric"?"tail\u2192head":l.mode==="max_pool"?"best-chunk":"mean"} ${l.cosine.toFixed(2)} (+${l.weight.toFixed(2)})`),d.same&&g.push(`same cluster (+${d.weight})`),u.weight>0){let _=[];u.pathMatch&&_.push(`opened authored path "${u.pathMatch.split("/").pop()}"`),u.contentMatch&&_.push("verbatim-paste of authored content"),g.push(`doc-authorship: ${_.join(" + ")} (+${u.weight.toFixed(2)})`)}return{parent_id:e.id,child_id:s.id,confidence:Math.min(1,m),signals:{temporal:i.weight,continuation:a.weight,file_overlap:o.weight,same_brand:c.weight,semantic:l.weight,cluster:d.weight,doc_authorship:u.weight},reasons:g}}function ls(e,s=cs){let t=[];for(let n=0;n<e.length;n++){let i=e[n],r=null;for(let a=0;a<n;a++){let o=e[a],c=cr(o,i,s);c&&(!r||c.confidence>r.confidence)&&(r=c)}r&&t.push(r)}return t}function ds(e,s={}){let t=s.maxUserMessages??5,n=s.userMessageMaxLen??2e3,i=new Set,r=[],a=new Set,o=[],c;try{if(Vi(e).size>qi)return{touched_files:i,recent_user_messages:r,authored_paths:a,authored_content:o};c=Ji(e,"utf8")}catch{return{touched_files:i,recent_user_messages:r,authored_paths:a,authored_content:o}}let l=0;for(;l<c.length;){let d=c.indexOf(`
793
+ `,l),u=d===-1?c.length:d,m=c.slice(l,u);if(l=d===-1?c.length:d+1,!m.trim())continue;let g;try{g=JSON.parse(m)}catch{continue}let _=g;if(_.type==="user"&&_.message?.role==="user"&&typeof _.message.content=="string"&&r.length<t){let h=_.message.content.trim();h&&r.push(h.length>n?h.slice(0,n):h)}let f=_.message?.content;if(Array.isArray(f))for(let h of f){if(!h||typeof h!="object")continue;let N=h;if(N.type!=="tool_use")continue;let w=N.input??{},U=typeof w.file_path=="string"?w.file_path:null;if(U){let b=Ne(U);b&&i.add(b)}if((N.name==="Write"||N.name==="Edit"||N.name==="MultiEdit")&&U){let b=Ne(U);b&&a.add(b);let G=typeof w.content=="string"?w.content:typeof w.new_string=="string"?w.new_string:null;G&&G.length>=200&&o.push(G.length>4096?G.slice(0,4096):G)}if(N.name==="Bash"&&typeof w.command=="string")for(let b of w.command.matchAll(/(?:^|[\s'"`(=])((?:\.\.?\/|\/|src\/|test\/|docs\/|site\/)[A-Za-z0-9_./-]+)/g)){let G=Ne(b[1]);G&&i.add(G)}if((N.name==="Glob"||N.name==="Grep")&&typeof w.pattern=="string"){let b=Ne(w.pattern);b&&!b.includes("*")&&i.add(b)}}}return{touched_files:i,recent_user_messages:r,authored_paths:a,authored_content:o}}function Ne(e){let s=e.trim().replace(/^['"]|['"]$/g,"");return!s||s.length<4||/[<>|;&\$`]/.test(s)?null:s}var qe=dr(lr),pr=6,ps="Active ",ms=" sessions \u2014 ",mr=6e4;async function gr(){if(ye()==="win32")return[];for(let s of["/bin/ps","/usr/bin/ps"])try{let{stdout:t}=await qe(s,["-eo","pid=,comm="],{timeout:2e3}),n=[];for(let i of t.split(`
794
+ `)){let r=i.trim().match(/^(\d+)\s+(.+)$/);if(!r)continue;let a=Number(r[1]),o=r[2].trim();(o==="claude"||o.endsWith("/claude")||o.endsWith("/bin/claude"))&&Number.isFinite(a)&&n.push(a)}return n}catch{continue}return[]}async function _r(e){let s=ye();if(s==="linux")try{return(await ur(`/proc/${e}/cwd`)).replace(/\/+$/,"")}catch{return null}if(s==="darwin"||s==="freebsd"||s==="openbsd")for(let t of["/usr/sbin/lsof","/usr/bin/lsof"])try{let{stdout:n}=await qe(t,["-a","-p",String(e),"-d","cwd","-Fn"],{timeout:2e3});for(let i of n.split(`
795
+ `))if(i.startsWith("n"))return i.slice(1).replace(/\/+$/,"");return null}catch{continue}return null}async function fr(e){let s=ye();if(s==="linux")try{let t=await us(`/proc/${e}/stat`,"utf8"),n=t.lastIndexOf(")");if(n===-1)return null;let i=t.slice(n+1).trim().split(/\s+/),r=Number(i[19]);if(!Number.isFinite(r))return null;let a=await us("/proc/uptime","utf8"),o=Number(a.split(/\s+/)[0]);return Number.isFinite(o)?Date.now()-o*1e3+r/100*1e3:null}catch{return null}if(s==="darwin"||s==="freebsd"||s==="openbsd")for(let t of["/bin/ps","/usr/bin/ps"])try{let{stdout:n}=await qe(t,["-o","lstart=","-p",String(e)],{timeout:2e3}),i=Date.parse(n.trim());return Number.isFinite(i)?i:null}catch{continue}return null}async function hr(e,s){let t=await gr();if(t.length===0)return null;let n=e.replace(/\/+$/,""),i=[];for(let a of t){let o=await _r(a);if(o&&(o===n||o.startsWith(n+"/"))){let c=await fr(a);i.push({pid:a,startMs:c})}}if(i.length===0)return new Set;let r=new Set;for(let{startMs:a}of i){if(a==null)continue;let o=null,c=mr;for(let l of s){if(r.has(l.session_id)||!l.started_at)continue;let d=Date.parse(l.started_at);if(!Number.isFinite(d))continue;let u=Math.abs(d-a);u<c&&(c=u,o=l)}o&&r.add(o.session_id)}return r}function Er(e){let t=p().prepare("SELECT id, name, decoded_path FROM projects WHERE id = ? LIMIT 1").get(e);if(!t)throw new Error(`project ${e} not found`);return t}function Tr(e,s){let t=p(),n=`${ps}${s}${ms}%`,i=t.prepare(`SELECT t.id
796
+ FROM threads t
797
+ WHERE t.archived = 0
798
+ AND t.name LIKE ?
799
+ ORDER BY t.created_at DESC`).all(n);for(let a of i){let o=D(a.id);if(o&&o.project===s)return o}let r=t.prepare(`SELECT DISTINCT te.thread_id AS id
800
+ FROM thread_edges te
801
+ JOIN sessions s ON s.id = te.session_id
802
+ JOIN threads t ON t.id = te.thread_id
803
+ WHERE s.project_id = ?
804
+ AND t.archived = 0
805
+ AND t.name LIKE ?
806
+ LIMIT 1`).get(e,n);return r?D(r.id):null}function gs(e){let s=e?new Date(e):new Date,t=s.getFullYear(),n=String(s.getMonth()+1).padStart(2,"0"),i=String(s.getDate()).padStart(2,"0");return`${t}-${n}-${i}`}function Ve(e,s){let t=p(),n=s>0,i=n?Date.now()-s*60*60*1e3:0;return n?t.prepare(`SELECT s.id AS session_id,
807
+ sa.alias AS alias,
808
+ s.auto_title AS auto_title,
809
+ s.first_user_message AS first_user_message,
810
+ s.started_at AS started_at,
811
+ s.file_mtime AS file_mtime
812
+ FROM sessions s
813
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
814
+ WHERE s.project_id = ?
815
+ AND s.file_mtime IS NOT NULL
816
+ AND s.file_mtime > ?
817
+ ORDER BY s.started_at ASC`).all(e,i):t.prepare(`SELECT s.id AS session_id,
818
+ sa.alias AS alias,
819
+ s.auto_title AS auto_title,
820
+ s.first_user_message AS first_user_message,
821
+ s.started_at AS started_at,
822
+ s.file_mtime AS file_mtime
823
+ FROM sessions s
824
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
825
+ WHERE s.project_id = ?
826
+ ORDER BY s.started_at ASC`).all(e)}function Sr(e){let s=p(),t=[];for(let n of e){if(!n.started_at)continue;let i=Date.parse(n.started_at);if(!Number.isFinite(i))continue;let r=s.prepare("SELECT file_path, ended_at FROM sessions WHERE id = ?").get(n.session_id);if(!r?.file_path)continue;let a=r.ended_at?Date.parse(r.ended_at):null,o=ds(r.file_path,{maxUserMessages:7});t.push({id:n.session_id,started_at_ms:i,ended_at_ms:Number.isFinite(a)?a:null,first_user_message:n.first_user_message,recent_user_messages:o.recent_user_messages,auto_title:n.auto_title,touched_files:o.touched_files,mean_embedding:null,head_pool:null,tail_pool:null,sample_chunks:[],cluster_id:null,authored_paths:o.authored_paths,authored_content:o.authored_content})}return t}function br(e){let t=p().prepare(`SELECT session_id, parent_session_id, source
827
+ FROM thread_edges
828
+ WHERE thread_id = ?`).all(e),n=new Map;for(let i of t)n.set(i.session_id,{parent_session_id:i.parent_session_id,source:i.source});return n}async function Qe(e,s={}){let t=Er(e),n=s.windowHours??pr,i=s.scoreThreshold??Je,r=s.useLivePids??!0,a=[],o=[];if(r&&t.decoded_path){let f=Ve(e,0),h=await hr(t.decoded_path,f);if(h===null){let w=ye()==="win32"?"Windows live-PID detection is not yet supported \u2014 falling back to the rolling mtime window.":"No live `claude` processes detected \u2014 falling back to the rolling mtime window. Output may include sessions that are no longer open.";a.push(w),o=Ve(e,n)}else h.size===0?(a.push(`No active terminals open in ${t.name} (cwd=${t.decoded_path}). Open a Claude terminal in this repo and re-run.`),o=[]):o=f.filter(N=>h.has(N.session_id))}else o=Ve(e,n);o.length===0&&!a.length&&a.push(`No active sessions in ${t.name} within the last ${n}h.`);let c=Tr(e,t.name),l=new Set(c?c.edges.map(f=>f.session_id):[]),d=o.filter(f=>!l.has(f.session_id)),u=Sr(o);u.sort((f,h)=>f.started_at_ms-h.started_at_ms);let m=ls(u,i),g=c?c.edges.filter(f=>f.source!=="auto-active"&&(f.parent_session_id||f.role==="origin")).map(f=>({session_id:f.session_id,parent_session_id:f.parent_session_id})):[],_=c?c.name:`${ps}${t.name}${ms}${gs(s.todayIso)}`;return{project:t,thread:{id:c?.id??null,name:_,exists:!!c,existing_session_count:c?.edges.length??0},candidates:o,proposed_additions:d,proposed_edges:m,preserved_manual_edges:g,warnings:a}}function _s(e){let s={thread_id:"",added:0,edges_set:0,preserved_manual:e.preserved_manual_edges.length},t;e.thread.exists&&e.thread.id?t=e.thread.id:t=Se({name:e.thread.name,summary:`Auto-captured by sync-active on ${gs()}. Members are sessions in ${e.project.name} that were active within the rolling window. Re-runnable: subsequent runs append new active sessions and never overwrite manual edges.`}).id,s.thread_id=t;let n=br(t);for(let a of e.candidates)n.has(a.session_id)||(be({threadId:t,sessionId:a.session_id,source:"auto-active",confidence:.5}),s.added++,n.set(a.session_id,{parent_session_id:null,source:"auto-active"}));for(let a of e.proposed_edges){let o=n.get(a.child_id);if(o&&o.source==="auto-active"&&o.parent_session_id!==a.parent_id&&n.has(a.parent_id))try{ae(t,a.child_id,a.parent_id),s.edges_set++,n.set(a.child_id,{parent_session_id:a.parent_id,source:o.source})}catch{}}let r=p().prepare(`SELECT session_id FROM thread_edges
829
+ WHERE thread_id = ?
830
+ AND source = 'auto-active'
831
+ AND parent_session_id IS NULL
832
+ AND role = 'child'`).all(t);for(let a of r)try{ae(t,a.session_id,null)}catch{}return s}function v(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function Rr(e){return{content:[{type:"text",text:e}],isError:!0}}function k(e,s){return M.object(e).strict().parse(s)}var Os=M.string().uuid(),W=Os.describe("Thread id (UUID)."),V=Os.describe("Session UUID (exact, not a prefix)."),fs={include_archived:M.boolean().optional().describe("Include archived threads (default false).")},hs={id:W},Es={session_id:V},Ts={name:M.string().min(1).max(200).describe("Human-readable thread name."),summary:M.string().max(4e3).optional().describe("Optional short description."),origin_session_id:V.optional().describe("Seed the thread with this session as its origin.")},Ss={thread_id:W,session_id:V,parent_session_id:V.optional().describe("If present, the edge is role=child with this parent; otherwise role=origin."),role:M.enum(["origin","child"]).optional()},bs={thread_id:W,session_id:V,parent_session_id:V.nullable().describe("New parent (null to clear and promote the edge back to role=origin).")},Rs={thread_id:W,session_id:V},Ns={thread_id:W,name:M.string().min(1).max(200)},ne={thread_id:W},ys={source_id:W.describe("Thread to dissolve \u2014 its edges move into dest_id."),dest_id:W.describe("Thread that absorbs source_id.")},ws={thread_id:W,session_ids:M.array(V).min(1).max(500),new_thread_name:M.string().min(1).max(200)},Ls={thread_id:W,force:M.boolean().optional().describe("When true, regenerate titles for sessions that already have an agent-sourced title. Default false skips them.")},As={project_id:M.number().int().positive().describe("Numeric project id from list_projects. The sync is repo-scoped and never crosses projects."),mode:M.enum(["preflight","apply"]).describe("preflight returns the proposed plan without writing; apply writes the thread + edges and is idempotent."),window_hours:M.number().min(.5).max(168).optional().describe("Rolling activity window in hours. Default 6.")};function Is(e){e.registerTool("thread_list",{title:"List threads",description:"All threads (v0.15a intent groups), newest first. Excludes archived threads unless include_archived is true.",inputSchema:fs},async s=>{try{let{include_archived:t}=k(fs,s);return v(Ye({includeArchived:t??!1}))}catch(t){return R(t)}}),e.registerTool("thread_get",{title:"Get thread with edges",description:"Full thread detail including every session edge (origin + children).",inputSchema:hs},async s=>{try{let{id:t}=k(hs,s),n=D(t);return n?v(n):Rr(`thread not found: ${t}`)}catch(t){return R(t)}}),e.registerTool("thread_for_session",{title:"List threads containing a session",description:"Return non-archived threads that reference this session (as origin or child).",inputSchema:Es},async s=>{try{let{session_id:t}=k(Es,s);return v(Ft(t))}catch(t){return R(t)}})}function xs(e,s={}){let t=s.limiter??new K;e.registerTool("thread_create",{title:"Create a thread",description:"Create a new thread. Optionally seed it with an origin session. Name is required; summary is optional.",inputSchema:Ts},async n=>{try{let i=k(Ts,n),r=await y({tool:"thread_create",args:i,limiter:t,run:()=>Se({name:i.name,summary:i.summary??null,originSessionId:i.origin_session_id})});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_add_session",{title:"Add session to thread",description:"Attach a session to a thread. If parent_session_id is provided the edge is role=child; otherwise role=origin. Upsert: re-adding updates the edge.",inputSchema:Ss},async n=>{try{let i=k(Ss,n),r=await y({tool:"thread_add_session",args:i,limiter:t,run:()=>be({threadId:i.thread_id,sessionId:i.session_id,parentSessionId:i.parent_session_id??null,role:i.role,source:"manual",confidence:1})});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_set_parent",{title:"Set edge parent within thread",description:"Change the parent session of an existing thread edge. Pass null to clear the parent and promote the edge to role=origin.",inputSchema:bs},async n=>{try{let i=k(bs,n),r=await y({tool:"thread_set_parent",args:i,limiter:t,run:()=>ae(i.thread_id,i.session_id,i.parent_session_id)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_remove_session",{title:"Remove session from thread",description:"Detach a session from a thread. The session itself is untouched; only the edge is removed. The updated thread is re-mirrored to disk.",inputSchema:Rs},async n=>{try{let i=k(Rs,n),r=await y({tool:"thread_remove_session",args:i,limiter:t,run:()=>Mt(i.thread_id,i.session_id)});return v({thread_id:i.thread_id,session_id:i.session_id,...r})}catch(i){return R(i)}}),e.registerTool("thread_rename",{title:"Rename thread",description:"Change the display name of a thread.",inputSchema:Ns},async n=>{try{let i=k(Ns,n),r=await y({tool:"thread_rename",args:i,limiter:t,run:()=>Ut(i.thread_id,i.name)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_close",{title:"Close thread",description:"Mark the thread as closed (sets closed_at). Thread remains listed; reopen to clear.",inputSchema:ne},async n=>{try{let i=k(ne,n),r=await y({tool:"thread_close",args:i,limiter:t,run:()=>Pt(i.thread_id)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_reopen",{title:"Reopen thread",description:"Clear closed_at on a closed thread.",inputSchema:ne},async n=>{try{let i=k(ne,n),r=await y({tool:"thread_reopen",args:i,limiter:t,run:()=>jt(i.thread_id)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_archive",{title:"Archive thread",description:"Soft-delete a thread by setting archived=1. Archived threads are hidden from thread_list by default but never hard-deleted; data is preserved in SQLite and the JSON mirror.",inputSchema:ne},async n=>{try{let i=k(ne,n),r=await y({tool:"thread_archive",args:i,limiter:t,run:()=>$t(i.thread_id)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_merge",{title:"Merge two threads",description:"Move every edge from source_id into dest_id, then delete source_id. Origin roles are preserved when present on either side.",inputSchema:ys},async n=>{try{let i=k(ys,n),r=await y({tool:"thread_merge",args:i,limiter:t,run:()=>Xt(i.source_id,i.dest_id)});return v(r)}catch(i){return R(i)}}),e.registerTool("thread_split",{title:"Split sessions into a new thread",description:"Peel the specified session_ids out of thread_id into a brand-new thread named new_thread_name. Non-member session_ids are silently skipped.",inputSchema:ws},async n=>{try{let i=k(ws,n),r=await y({tool:"thread_split",args:i,limiter:t,run:()=>Ht({threadId:i.thread_id,sessionIds:i.session_ids,newThreadName:i.new_thread_name})});return v(r)}catch(i){return R(i)}}),e.registerTool("sync_active_sessions",{title:"Sync currently active sessions in a repo into a thread",description:'Captures the Captain Code Pattern: scans every Claude session whose JSONL was touched within the rolling window in the given project, infers parent/child edges via the 7-signal scorer (temporal, continuation, file overlap, brand, semantic, cluster, doc-authorship), and stitches them into one thread. Idempotent: re-running reuses the existing "Active <project> sessions \u2014 *" thread, only appends new sessions, and never overwrites edges with source=manual. Use mode=preflight first to show the user the proposed plan, then mode=apply to write.',inputSchema:As},async n=>{try{let i=k(As,n);if(i.mode==="preflight"){let a=await Qe(i.project_id,{windowHours:i.window_hours});return v({plan:a})}let r=await y({tool:"sync_active_sessions",args:i,limiter:t,run:async()=>{let a=await Qe(i.project_id,{windowHours:i.window_hours}),o=_s(a);return{plan:a,result:o}}});return v(r)}catch(i){return R(i)}}),e.registerTool("generate_thread_titles",{title:"Generate titles for every session in a thread",description:"Walk a thread DAG in topology order (origins first by added_at, then children breadth-first) and generate a coherent agent title for each session. Each successive title sees already-titled ancestors and earlier siblings as strong context so a naming pattern emerges. Set force=true to regenerate already-titled sessions; default false skips them. Returns the final {generated, skipped, failed} summary after the whole walk completes.",inputSchema:Ls},async n=>{try{let i=k(Ls,n),r=await y({tool:"generate_thread_titles",args:i,limiter:t,run:async()=>{let a=await rs(i.thread_id,{force:i.force??!1});if(a.failed.length>0&&a.generated.length===0&&a.skipped.length===0)throw new Error(`all ${a.failed.length} session(s) failed title generation`);return a}});return v(r)}catch(i){return R(i)}})}F();var Nr="BAAI/bge-base-en-v1.5",yr=768,Cs=16,wr="Represent this sentence for searching relevant passages: ",vs=null,Lr=!1;function de(){return{loaded:Lr,modelId:Nr,dim:yr}}async function Ds(e){if(!vs)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");let s=[];for(let t=0;t<e.length;t+=Cs){let n=e.slice(t,t+Cs),r=(await vs(n,{pooling:"cls",normalize:!0})).tolist();for(let a=0;a<n.length;a++){let o=r[a],c=Array.isArray(o[0])?o[0]:o;s.push(new Float32Array(c))}}return s}async function ks(e){let s=wr+e,[t]=await Ds([s]);return t}L();F();import{writeFileSync as Ar}from"node:fs";import{join as Or}from"node:path";var Ir=Or(S,"recall-events.json");function Fs(e,s,t,n="cli"){p().prepare(`
833
+ INSERT INTO recall_events (session_id, recalled_at, token_count, mode, caller)
834
+ VALUES (?, datetime('now'), ?, ?, ?)
835
+ `).run(e,s,t,n),xr()}function xr(){x();let s=p().prepare("SELECT id, session_id, recalled_at, token_count, mode, caller FROM recall_events ORDER BY recalled_at DESC").all();Ar(Ir,JSON.stringify(s,null,2)+`
836
+ `,"utf-8")}L();async function Ms(e,s=50){let t=await ks(e),n=p(),i=Buffer.from(t.buffer,t.byteOffset,t.byteLength);return n.prepare(`SELECT v.rowid, v.distance, cm.session_id, cm.text, cm.message_uuids
837
+ FROM vec_chunks v JOIN chunk_meta cm ON cm.rowid = v.rowid
838
+ WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(i,s).map(a=>({sessionId:a.session_id,chunkRowid:a.rowid,distance:a.distance,text:a.text,messageUuids:JSON.parse(a.message_uuids)}))}async function Us(e,s=10,t=.65){let n=p(),i=n.prepare("SELECT rowid FROM chunk_meta WHERE session_id = ? ORDER BY rowid LIMIT 1").get(e);if(!i)return[];let r=n.prepare("SELECT embedding FROM vec_chunks WHERE rowid = ?").get(i.rowid);if(!r)return[];let a=n.prepare(`SELECT v.rowid, v.distance, cm.session_id FROM vec_chunks v JOIN chunk_meta cm ON cm.rowid = v.rowid
839
+ WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(r.embedding,s*5),o=new Map;for(let l of a){if(l.session_id===e)continue;let d=o.get(l.session_id);(d===void 0||l.distance<d)&&o.set(l.session_id,l.distance)}let c=[];for(let[l,d]of o){let u=1-d;u>=t&&c.push({sessionId:l,similarity:u})}return c.sort((l,d)=>d.similarity-l.similarity),c.slice(0,s)}function Cr(){let e=process.env.RECALL_RRF_K;if(e){let s=parseInt(e,10);if(!isNaN(s)&&s>=1&&s<=1e3)return s}return 60}function Ps(e){let s=Cr(),t=new Map;for(let i of e)for(let r=0;r<i.length;r++){let a=i[r],o=1/(s+r+1),c=t.get(a.id);c?(c.score+=o,c.lanes.push(a.lane),r+1<c.bestRank&&(c.bestRank=r+1,c.bestData=a.data)):t.set(a.id,{score:o,lanes:[a.lane],bestRank:r+1,bestData:a.data})}let n=[];for(let[i,r]of t)n.push({id:i,score:r.score,lanes:r.lanes,data:r.bestData});return n.sort((i,r)=>r.score-i.score),n}L();L();var vr=!1,Dr=null;function kr(){return p().prepare("SELECT COUNT(*) AS n FROM chunk_queue").get().n}function js(){return{running:vr,queueDepth:kr(),lastProcessedAt:Dr}}import{existsSync as Fr,mkdirSync as Zc,rmSync as el,createWriteStream as tl,statSync as sl}from"node:fs";import{join as $s}from"node:path";F();var Mr=[{path:"config.json",sha256:"bc00af31a4a31b74040d73370aa83b62da34c90b75eb77bfa7db039d90abd591"},{path:"tokenizer.json",sha256:"d241a60d5e8f04cc1b2b3e9ef7a4921b27bf526d9f6050ab90f9267a1f9e5c66"},{path:"tokenizer_config.json",sha256:"9261e7d79b44c8195c1cada2b453e55b00aeb81e907a6664974b4d7776172ab3"},{path:"onnx/model.onnx",sha256:"9bc579acdba21c253c62a9bf866891355a63ffa3442b52c8a37d75b2ccb91848"}];function Ur(){return $s(S,"models","BAAI","bge-base-en-v1.5")}function Xs(){let e=Ur();return Mr.every(s=>Fr($s(e,s.path)))}L();L();F();import{join as Ze}from"node:path";var Pr=new Set(["pending","approved","rejected"]),jr=new Set(["L1","L2","L3","L4","user"]),ll=Ze(S,"links"),$r=Ze(S,"suggestions"),dl=Ze($r,"index.json");function Hs(e){try{return JSON.parse(e)}catch{return e}}function Xr(e){return{id:e.id,source_session_id:e.source_session_id,target_session_id:e.target_session_id,link_type:e.link_type,confidence:e.confidence,source:e.source,evidence:Hs(e.evidence),approved:e.approved===1,created_at:e.created_at,updated_at:e.updated_at}}function Hr(e){return{id:e.id,source_session_id:e.source_session_id,target_session_id:e.target_session_id,link_type:e.link_type,confidence:e.confidence,evidence:Hs(e.evidence),status:e.status,inferred_by:e.inferred_by,created_at:e.created_at,decided_at:e.decided_at}}function Br(e){if(!jr.has(e))throw new Error(`invalid inferred_by: ${e}`)}function et(e){return p().prepare(`SELECT * FROM session_links
840
+ WHERE source_session_id = ? OR target_session_id = ?
841
+ ORDER BY confidence DESC, updated_at DESC`).all(e,e).map(Xr)}function tt(e={}){let s=p(),t=[],n=[];if(e.status){if(!Pr.has(e.status))throw new Error(`invalid status: ${e.status}`);t.push("status = ?"),n.push(e.status)}e.sourceSessionId&&(t.push("source_session_id = ?"),n.push(e.sourceSessionId)),e.targetSessionId&&(t.push("target_session_id = ?"),n.push(e.targetSessionId)),e.inferredBy&&(Br(e.inferredBy),t.push("inferred_by = ?"),n.push(e.inferredBy));let i=t.length?`WHERE ${t.join(" AND ")}`:"",r=Math.max(1,Math.min(5e3,e.limit??1e3));return s.prepare(`SELECT * FROM session_link_suggestions ${i}
842
+ ORDER BY confidence DESC, created_at DESC
843
+ LIMIT ?`).all(...n,r).map(Hr)}var Wr=4e3,Yr=2,Gr=30,zr=.2,Kr={citation:1,wiki_link:.9,similar:.7,skill_track:.5,bug_pattern:.4,temporal_proximity:.3};function we(e){return e?Math.ceil(e.length/4):0}function Bs(e){if(!Number.isFinite(e)||e<0)return 1;let s=Math.exp(-e/Gr);return Math.max(zr,s)}function Ws(e,s){if(!e||!s)return 0;let t=Date.parse(e),n=Date.parse(s);return!Number.isFinite(t)||!Number.isFinite(n)?0:Math.abs(n-t)/(1e3*60*60*24)}function Ys(e){return p().prepare(`SELECT s.id,
844
+ NULLIF(sa.alias, '') AS alias,
845
+ s.auto_title,
846
+ s.auto_title_source,
847
+ s.first_user_message,
848
+ s.project_id,
849
+ p.name AS project,
850
+ s.started_at
851
+ FROM sessions s
852
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
853
+ LEFT JOIN projects p ON p.id = s.project_id
854
+ WHERE s.id = ?`).get(e)??null}function Gs(e){let t=p().prepare("SELECT summary FROM session_semantic WHERE session_id = ?").get(e);if(!t||!t.summary)return null;let n=t.summary.trim();return n.length>0?n:null}function zs(e){let s=e.alias?.trim(),t=e.auto_title?.trim(),n=e.first_user_message?.trim();return t&&e.auto_title_source==="agent"?t:s||t||(n?n.slice(0,80):e.id.slice(0,8))}function Jr(e){let t=p().prepare(`SELECT id, auto_title, started_at
855
+ FROM sessions
856
+ WHERE project_id = ?
857
+ ORDER BY COALESCE(started_at, ''), id`).all(e),n=new Set,i=new Set,r=[];for(let m of t){if(!m.auto_title||!m.auto_title.startsWith("/")){r.push({id:m.id,brand:null,skill:null});continue}let g=m.auto_title.split(" \xB7 "),_=g[0].trim(),f=g.length>1?g.slice(1).join(" \xB7 ").trim():null;r.push({id:m.id,brand:f||null,skill:_||null}),f&&n.add(f),_&&i.add(_)}let a=[...n].sort(),o=new Map;a.forEach((m,g)=>o.set(m,g));let c=[...i].sort(),l=new Map;c.forEach((m,g)=>l.set(m,g));let d=new Map,u=new Map;for(let m of r){if(!m.brand||!m.skill)continue;let g=o.get(m.brand),_=l.get(m.skill);if(g===void 0||_===void 0)continue;let f=`${g}.${_}`,h=(d.get(f)??0)+1;d.set(f,h),u.set(m.id,`${g}.${_}.${h}`)}return{byId:u}}function Vr(e){return{table:e!==null?Jr(e):null,originProjectId:e,cache:new Map}}function Le(e,s){let t=e.cache.get(s);if(t)return t;let n=Ys(s);if(!n)return null;let i=e.table&&n.project_id===e.originProjectId?e.table.byId.get(s)??null:null,r={session_id:n.id,title:zs(n),decimal:i,summary:Gs(n.id),project:n.project,started_at:n.started_at};return e.cache.set(s,r),r}function qr(e,s){let n=p().prepare(`SELECT DISTINCT te.parent_session_id AS pid
858
+ FROM thread_edges te
859
+ WHERE te.session_id = ?
860
+ AND te.parent_session_id IS NOT NULL`).all(s),i=[];for(let r of n){if(!r.pid)continue;let a=Le(e,r.pid);a&&i.push(a)}return i}function Qr(e,s){let n=p().prepare(`SELECT DISTINCT te.session_id AS sid
861
+ FROM thread_edges te
862
+ WHERE te.parent_session_id = ?`).all(s),i=[];for(let r of n){if(!r.sid)continue;let a=Le(e,r.sid);a&&i.push(a)}return i}function Ks(e){let s=Kr[e.linkType]??.5,t=ie(e.confidence),n=s*t,i=Bs(e.daysApart),r=e.embeddingCosine??.5,a=ie(e.pagerank);if(e.scoring==="pagerank")return ie(a);if(e.scoring==="embedding-rerank")return e.embeddingCosine===null?ie(n):ie(r);let o=.35*n+.2*i+.2*r+.25*a;return ie(o)}function ie(e){return!Number.isFinite(e)||e<0?0:e>1?1:e}function Zr(e,s,t,n,i){let r=new Map;function a(o,c){if(o===c)return;let l=r.get(o);l||(l=new Set,r.set(o,l)),l.add(c)}for(let o of s)a(o.source_session_id,o.target_session_id),a(o.target_session_id,o.source_session_id);for(let o of t)a(e,o.session_id);for(let o of t)a(o.session_id,e);for(let o of n)a(e,o.session_id);for(let o of n)a(o.session_id,e);if(i>1){let o=new Set([e]),c=new Set([e]);for(let l=1;l<i;l++){let d=new Set;for(let u of o){let m=r.get(u);if(m)for(let g of m){if(c.has(g))continue;let _=et(g).filter(f=>f.approved);for(let f of _)a(f.source_session_id,f.target_session_id),a(f.target_session_id,f.source_session_id);c.add(g),d.add(g)}}if(d.size===0)break;for(let u of d)o.add(u)}}return{edges:r}}function eo(e,s={}){let t=s.iterations??12,n=s.damping??.85,i=Array.from(e.edges.keys());if(i.length===0)return new Map;let r=1/i.length,a=new Map(i.map(l=>[l,r]));for(let l=0;l<t;l++){let d=new Map(i.map(u=>[u,(1-n)/i.length]));for(let u of i){let m=e.edges.get(u);if(!m||m.size===0)continue;let g=(a.get(u)??0)/m.size;for(let _ of m)d.set(_,(d.get(_)??0)+n*g)}a=d}let o=0;for(let l of a.values())l>o&&(o=l);if(o<=0)return a;let c=new Map;for(let[l,d]of a)c.set(l,d/o);return c}var Js=240;function Vs(e,s){let t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}\u2026`}function to(e){let s=e.decimal?`${e.decimal} `:"",t=e.session_id.slice(0,8),n="evidence"in e&&e.evidence?` \u2014 ${e.evidence}`:"",i=`- ${s}${e.title} (${t})${n}`;if(e.summary){let r=Vs(e.summary,Js);return`${i}
863
+ ${r}`}return i}function so(e,s,t){let n=[],i=[],r=0,a=e.decimal?`${e.decimal}: `:"",o=`# Neighborhood for ${e.session_id} (${a}${e.title})`;if(n.push(o),r+=we(o),e.summary){let c=Vs(e.summary,Js*4);n.push(c),r+=we(c)}n.push("");for(let c of s){if(c.refs.length===0)continue;let l=`## ${c.heading}`,d=we(l),u=[],m=0;for(let g of c.refs){let _=to(g),f=we(_);if(r+d+m+f>t){i.push({session_id:g.session_id,title:g.title,decimal:g.decimal,summary:g.summary,project:g.project,started_at:g.started_at});continue}u.push(_),m+=f}if(u.length>0){n.push(l);for(let g of u)n.push(g);n.push(""),r+=d+m}}for(;n.length>0&&n[n.length-1]==="";)n.pop();return{bundle:n.join(`
864
+ `)+`
865
+ `,budgetUsed:r,truncated:i}}function no(e,s,t,n,i,r){let a=[];for(let o of t){if(n&&!n.has(o.link_type))continue;let c=null;if(o.source_session_id===s.session_id?c=o.target_session_id:o.target_session_id===s.session_id&&(c=o.source_session_id),!c)continue;let l=Le(e,c);if(!l)continue;let d=Ws(s.started_at,l.started_at),u=Ks({confidence:o.confidence,linkType:o.link_type,daysApart:d,embeddingCosine:null,pagerank:r.get(c)??0,scoring:i});a.push({...l,score:u,evidence:`(suggestion, ${o.inferred_by}) confidence=${o.confidence.toFixed(2)} ${Math.round(d)}d apart`,link_type:o.link_type})}return a}function qs(e,s={}){let t=Math.max(100,Math.floor(s.budget??Wr)),n=s.scoring??"hybrid",i=Math.max(1,Math.min(5,s.maxDepth??Yr)),r=s.includeWikiLinks??!0,a=s.includeSuggestions??!1,o=s.edgeTypes?new Set(s.edgeTypes):null,c=Ys(e);if(!c)throw new Error(`session not found: ${e}`);let l=Vr(c.project_id),d={session_id:c.id,title:zs(c),decimal:l.table?.byId.get(c.id)??null,summary:Gs(c.id),project:c.project,started_at:c.started_at};l.cache.set(c.id,d);let u=qr(l,e),m=Qr(l,e),g=et(e).filter(A=>A.approved).filter(A=>!o||o.has(A.link_type)).filter(A=>r||A.link_type!=="wiki_link"),_=Zr(e,g,u,m,i),f=eo(_),h=[],N=[],w=[],U=[];for(let A of g){let ee=A.source_session_id===e?A.target_session_id:A.source_session_id,re=Le(l,ee);if(!re)continue;let te=Ws(d.started_at,re.started_at),Ae=Ks({confidence:A.confidence,linkType:A.link_type,daysApart:te,embeddingCosine:null,pagerank:f.get(ee)??0,scoring:n}),Oe=Bs(te),P=`${A.link_type} confidence=${A.confidence.toFixed(2)} recency=${Oe.toFixed(2)} (${Math.round(te)}d apart)`,me={...re,score:Ae,evidence:P,link_type:A.link_type};A.link_type==="citation"?h.push(me):A.link_type==="similar"?N.push(me):A.link_type==="wiki_link"?U.push(me):w.push(me)}if(a){let A=tt({sourceSessionId:e,status:"pending",limit:100}),ee=tt({targetSessionId:e,status:"pending",limit:100}),re=[...A,...ee],te=new Set,Ae=re.filter(P=>te.has(P.id)?!1:(te.add(P.id),!0)),Oe=no(l,d,Ae,o,n,f);for(let P of Oe)P.link_type==="citation"?h.push(P):P.link_type==="similar"?N.push(P):P.link_type==="wiki_link"?U.push(P):w.push(P)}let b=(A,ee)=>ee.score-A.score;h.sort(b),N.sort(b),w.sort(b),U.sort(b);let pe=so(d,[{heading:"Parents",refs:u},{heading:"Children",refs:m},{heading:"Citations (approved)",refs:h},{heading:"Similar sessions",refs:N},{heading:"Cousins (skill track + temporal)",refs:w},{heading:"Wiki links (manual)",refs:U}],t);return{origin:d,parents:u,children:m,citations:h,similar:N,cousins:w,wikiLinks:U,bundle:pe.bundle,budgetUsed:pe.budgetUsed,budgetRemaining:Math.max(0,t-pe.budgetUsed),truncated:pe.truncated}}var Ro="0.12.0";function No(){let e=process.env.RECALL_MCP_ALLOW_WRITES;return e==="1"||e==="true"}function C(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function it(e){return{content:[{type:"text",text:e}]}}function X(e){return{content:[{type:"text",text:e}],isError:!0}}function yo(){let e=new So({name:"claude-recall",version:Ro}),s=No();if(e.registerTool("list_projects",{title:"List projects",description:"Every Claude Code project currently indexed by Recall, with session and message counts and the most recent activity timestamp.",inputSchema:{}},async()=>{let n=p().prepare(`SELECT p.name,
866
+ COUNT(s.id) AS session_count,
867
+ COALESCE(SUM(s.message_count), 0) AS message_count,
868
+ MAX(s.started_at) AS latest
869
+ FROM projects p
870
+ LEFT JOIN sessions s ON s.project_id = p.id
871
+ GROUP BY p.id
872
+ ORDER BY MAX(COALESCE(s.started_at, '')) DESC`).all();return C(n)}),e.registerTool("list_sessions",{title:"List sessions",description:"Recent sessions with alias, tags, and message counts. Optional filters for project, tag, and date range.",inputSchema:{project:T.string().optional().describe("Substring match against project name or decoded filesystem path."),tag:T.string().optional().describe("Only sessions carrying this tag (leading # optional)."),since:T.string().optional().describe("Only sessions started at or after this ISO timestamp or YYYY-MM-DD date."),until:T.string().optional().describe("Only sessions started at or before this ISO timestamp or YYYY-MM-DD date."),limit:T.number().int().min(1).max(500).optional()}},async({project:t,tag:n,since:i,until:r,limit:a})=>{let o=p(),c={limit:a??100},l="s.message_count > 2";if(t&&(l+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",c.proj=`%${t}%`),i&&(l+=" AND s.started_at >= @since",c.since=i),r&&(l+=" AND s.started_at <= @until",c.until=/^\d{4}-\d{2}-\d{2}$/.test(r)?`${r}T23:59:59.999Z`:r),n){let m=q(n);m&&(l+=" AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag)",c.tag=m)}let u=o.prepare(`SELECT s.id, p.name AS project, s.started_at, s.ended_at,
873
+ s.message_count, s.first_user_message, s.git_branch,
874
+ NULLIF(sa.alias, '') AS alias,
875
+ CASE WHEN sn.content IS NOT NULL AND sn.content != '' THEN 1 ELSE 0 END AS has_notes,
876
+ COALESCE(
877
+ (SELECT GROUP_CONCAT(tag, ',')
878
+ FROM (SELECT tag FROM session_tags WHERE session_id = s.id ORDER BY tag)),
879
+ ''
880
+ ) AS tags_csv
881
+ FROM sessions s
882
+ JOIN projects p ON p.id = s.project_id
883
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
884
+ LEFT JOIN session_notes sn ON sn.session_id = s.id
885
+ WHERE ${l}
886
+ ORDER BY COALESCE(s.started_at, '') DESC
887
+ LIMIT @limit`).all(c).map(({tags_csv:m,...g})=>({...g,tags:m?m.split(","):[]}));return C(u)}),e.registerTool("list_tags",{title:"List tags",description:"Every tag currently applied to a session, with its count, most popular first.",inputSchema:{}},async()=>C(mt())),e.registerTool("search",{title:"Search messages",description:"Full-text search over every message in every indexed session. Use `#tag-name` tokens inside the query string to also filter by tag; plain terms are ANDed together.",inputSchema:{query:T.string().describe("Text to find. Supports inline `#tag-name` tokens to narrow to sessions with the tag."),project:T.string().optional().describe("Substring match against project name or path."),limit:T.number().int().min(1).max(200).optional()}},async({query:t,project:n,limit:i})=>{let r=p(),a=t.trim();if(!a)return C({query:"",hits:[],tags:[]});let o=a.split(/\s+/).filter(Boolean),c=o.filter(f=>f.startsWith("#")).map(q).filter(f=>!!f),d=o.filter(f=>!f.startsWith("#")).map(f=>`"${f.replace(/"/g,"")}"`).join(" "),u=Math.max(1,Math.min(200,i??30));if(!d&&c.length>0){let f=`
888
+ SELECT s.id AS session_id,
889
+ s.id AS message_uuid,
890
+ p.name AS project,
891
+ s.started_at,
892
+ COALESCE(s.first_user_message, '') AS snippet,
893
+ CAST(NULL AS TEXT) AS role,
894
+ CAST(NULL AS TEXT) AS timestamp,
895
+ NULLIF(sa.alias, '') AS alias
896
+ FROM sessions s
897
+ JOIN projects p ON p.id = s.project_id
898
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
899
+ WHERE 1=1
900
+ `,h={limit:u};return n&&(f+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",h.proj=`%${n}%`),c.forEach((N,w)=>{f+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${w})`,h[`tag_${w}`]=N}),f+=" ORDER BY COALESCE(s.started_at, '') DESC LIMIT @limit",C({query:a,hits:r.prepare(f).all(h),tags:c})}if(!d)return C({query:a,hits:[],tags:c});let m=`
901
+ SELECT m.session_id AS session_id,
902
+ m.uuid AS message_uuid,
903
+ p.name AS project,
904
+ s.started_at,
905
+ snippet(messages_fts, 0, '<<', '>>', '\u2026', 20) AS snippet,
906
+ m.role,
907
+ m.timestamp,
908
+ NULLIF(sa.alias, '') AS alias
909
+ FROM messages_fts
910
+ JOIN messages m ON m.rowid = messages_fts.rowid
911
+ JOIN sessions s ON s.id = m.session_id
912
+ JOIN projects p ON p.id = s.project_id
913
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
914
+ WHERE messages_fts MATCH @fts
915
+ `,g={fts:d,limit:u};n&&(m+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",g.proj=`%${n}%`),c.forEach((f,h)=>{m+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${h})`,g[`tag_${h}`]=f}),m+=" ORDER BY bm25(messages_fts) LIMIT @limit";let _=r.prepare(m).all(g);if(de().loaded)try{let f=await Ms(a,u),h=_.map(b=>({id:String(b.session_id),data:b,lane:"bm25"})),N=f.map(b=>({id:b.sessionId,data:{session_id:b.sessionId,snippet:b.text,matched_via:"vector"},lane:"vector"})),U=Ps([h,N]).slice(0,u).map(b=>({...b.data,session_id:b.id,rrf_score:b.score,lanes:b.lanes}));return C({query:a,hits:U,tags:c,fusion:"rrf"})}catch{}return C({query:a,hits:_,tags:c})}),e.registerTool("find_similar_sessions",{title:"Find similar sessions",description:"Find sessions semantically similar to a given session using vector embeddings (Pro-only). Returns related sessions ranked by cosine similarity.",inputSchema:{session_id:T.string().uuid().describe("Session UUID to find similar sessions for."),limit:T.number().int().min(1).max(50).optional().describe("Max results (default 10)."),min_cosine:T.number().min(0).max(1).optional().describe("Minimum cosine similarity threshold (default 0.65).")}},async({session_id:t,limit:n,min_cosine:i})=>{if(!de().loaded)return C({upgrade_required:!0,reason:"Semantic vector search requires Pro with the embedding model installed.",buy_url:"https://clauderecall.com/pro"});try{let r=await Us(t,n??10,i??.65);return C({session_id:t,similar:r})}catch(r){return X(r instanceof Error?r.message:"vector search failed")}}),e.registerTool("semantic_status",{title:"Semantic search status",description:"Health snapshot of the semantic vector search tier: model status, worker status, queue depth.",inputSchema:{}},async()=>{let t=de(),n=js(),i=Xs();return C({embedder:t,worker:n,modelInstalled:i})}),e.registerTool("get_session",{title:"Get session transcript",description:"Return the full metadata and ordered messages for a session. Accepts a full UUID or an 8+-character id prefix.",inputSchema:{id:T.string().describe("Session id (full UUID or 8+-character prefix).")}},async({id:t})=>{let n=$(t);if(!n)return X(`session not found or prefix ambiguous: ${t}`);let i=p(),r=i.prepare(`SELECT s.id, s.project_id, s.started_at, s.ended_at,
916
+ s.message_count, s.user_message_count, s.assistant_message_count,
917
+ s.first_user_message, s.git_branch, s.version, s.indexed_at,
918
+ p.name AS project_name,
919
+ NULLIF(sa.alias, '') AS alias
920
+ FROM sessions s
921
+ JOIN projects p ON p.id = s.project_id
922
+ LEFT JOIN session_aliases sa ON sa.session_id = s.id
923
+ WHERE s.id = ?`).get(n);if(!r)return X(`session metadata missing for ${n}`);let a=_e(n),o=i.prepare(`SELECT uuid, type, role, timestamp, is_sidechain, content_text, tool_names
924
+ FROM messages WHERE session_id = ?
925
+ ORDER BY COALESCE(timestamp, ''), rowid`).all(n);return C({session:{...r,tags:a},messages:o})}),e.registerTool("context_for_session",{title:"Export session as context",description:"Render a past session as markdown ready to paste into a fresh Claude conversation. This is the flagship Recall operation: pipe any previous session back into a new chat as memory.",inputSchema:{id:T.string().describe("Session id (full UUID or 8+-character prefix)."),mode:T.enum(["condensed","full"]).optional().describe("`condensed` (default) strips tool-call JSON; `full` keeps everything."),includeSidechain:T.boolean().optional().describe("Include subagent / sidechain messages (default false)."),prelude:T.string().max(1e4).optional().describe("Optional header prepended above the transcript (max 10 000 chars)."),since:T.string().optional().describe("Only messages at or after this ISO timestamp.")}},async({id:t,mode:n,includeSidechain:i,prelude:r,since:a})=>{let o=$(t);if(!o)return X(`session not found or prefix ambiguous: ${t}`);let c=p(),l=c.prepare(`SELECT s.id, p.name AS project_name,
926
+ s.started_at, s.ended_at, s.message_count, s.git_branch
927
+ FROM sessions s JOIN projects p ON p.id = s.project_id
928
+ WHERE s.id = ?`).get(o);if(!l)return X(`session metadata missing for ${o}`);let d=c.prepare(`SELECT uuid, type, role, timestamp, is_sidechain, content_text, tool_names
929
+ FROM messages WHERE session_id = ?
930
+ ORDER BY COALESCE(timestamp, ''), rowid`).all(o),u=ut(l,d,{mode:n??"condensed",includeSidechain:i??!1,prelude:r??null,since:a??null}),m=a?"since":n??"condensed";return Fs(o,Math.ceil(u.length/4),m,"mcp"),it(u)}),e.registerTool("recall_neighborhood",{title:"Recall: neighborhood context bundle",description:"Build a ranked, budget-bounded markdown bundle for a session: parents + children from thread_edges, plus approved citations / similar / cousins / wiki_links from the cognitive graph. Pipe-friendly output ready to seed a fresh conversation. Reads only approved edges by default \u2014 pending suggestions are opt-in.",inputSchema:{session_id:T.string().describe("Session UUID or 8+-character prefix."),budget:T.number().int().min(100).max(5e4).optional().describe("Token budget for the assembled bundle. Default 4000."),scoring:T.enum(["pagerank","embedding-rerank","hybrid"]).optional().describe("Scoring mode (default hybrid)."),edge_types:T.array(T.enum(["citation","similar","skill_track","bug_pattern","wiki_link","temporal_proximity"])).optional().describe("Restrict to certain link types. Default: all approved types."),max_depth:T.number().int().min(1).max(5).optional().describe("Pagerank traversal depth on the local subgraph (default 2)."),include_wiki_links:T.boolean().optional().describe("Include manual wiki_link rows. Default true."),include_suggestions:T.boolean().optional().describe("Surface pending session_link_suggestions. Debug only; default false."),format:T.enum(["markdown","json"]).optional().describe("markdown (default) returns the bundle as text; json returns the full NeighborhoodResult.")}},async({session_id:t,budget:n,scoring:i,edge_types:r,max_depth:a,include_wiki_links:o,include_suggestions:c,format:l})=>{let d=$(t);if(!d)return X(`session not found or prefix ambiguous: ${t}`);try{let u=qs(d,{budget:n,scoring:i,edgeTypes:r,maxDepth:a,includeWikiLinks:o,includeSuggestions:c});return l==="json"?C(u):it(u.bundle)}catch(u){return X(u instanceof Error?u.message:String(u))}}),e.registerTool("doctor",{title:"Health check",description:"Read-only diagnostic snapshot of the local Claude Recall database: total size, WAL size, free pages, FTS5 fragmentation for messages and sessions, vector index row count, free disk space, integrity check, and row counts per surface table. Returns structured JSON. Equivalent to running `recall doctor --json` from the shell. Safe to call as often as needed; no side effects.",inputSchema:{}},async()=>{let{buildHealthReport:t}=await Promise.resolve().then(()=>(un(),dn));return C(t())}),Is(e),s){let t=Number(process.env.RECALL_MCP_RATE_LIMIT),n=Number.isFinite(t)&&t>0?t:Xe,i=p(),r=new Date(Date.now()-6e4).toISOString(),a=i.prepare("SELECT at FROM mcp_audit_events WHERE at >= ? ORDER BY at ASC").all(r),o=new K(n);for(let c of a){let l=new Date(c.at).getTime();if(Number.isFinite(l))try{o.consume(l)}catch{break}}e.registerTool("list_sessions_to_tag",{title:"List sessions to tag",description:"Return session summaries and sampled messages formatted for an agent to propose tags. Respects the same scope filters used by the Recall UI scan. Only returns data if auto-tagging is enabled in config.",inputSchema:{untaggedOnly:T.boolean().optional().describe("Only sessions with zero tags (default false)."),project:T.string().optional().describe("Exact project name match."),collectionId:T.string().optional().describe("Only sessions in this collection."),limit:T.number().int().min(1).max(200).optional()}},async c=>{let l=De();if(!l.enabled)return X("auto-tagging is disabled; enable it in Recall settings before scanning");let d=he({untaggedOnly:c.untaggedOnly,project:c.project,collectionId:c.collectionId,limit:c.limit??50});return C({count:d.length,sessions:d,guidance:`Produce ${l.minTagsPerSession}-${l.maxTagsPerSession} tags per session. Prefer existing tags from list_tags for consistency. Use apply_tags to write results.`})}),e.registerTool("apply_tags",{title:"Apply tags to a session",description:"Add one or more tags to a session (merge-mode, never removes existing). Only works when auto-tagging is enabled. Accepts full UUID or 8+-character id prefix.",inputSchema:{sessionId:T.string().describe("Full session UUID or 8+-character prefix."),tags:T.array(T.string()).min(1).max(10).describe("Tags to add. Normalized server-side (lowercase, hyphens, strip #).")}},async({sessionId:c,tags:l})=>{if(!De().enabled)return X("auto-tagging is disabled; enable it in Recall settings before writing tags");let u=$(c);if(!u)return X(`session not found or prefix ambiguous: ${c}`);try{let m=await y({tool:"apply_tags",args:{sessionId:u,tags:l},limiter:o,run:()=>{let g=[],_=[];for(let f of l)try{let{tag:h,added:N}=ge(u,f);N?g.push(h):_.push({tag:h,reason:"already present"})}catch(h){_.push({tag:f,reason:h instanceof Error?h.message:String(h)})}return{sessionId:u,applied:g,skipped:_}}});return C(m)}catch(m){return m instanceof Error&&m.message.startsWith("SQLITE_")?X("database constraint error"):X(m instanceof Error?m.message:String(m))}}),e.registerTool("optimize",{title:"Optimize the database",description:"Run the local maintenance pass: WAL checkpoint (TRUNCATE), FTS5 segment merge for messages and sessions, refresh planner stats. Equivalent to `recall optimize` from the shell. Safe while the daemon is running. Set vacuum=true to also reclaim free pages \u2014 but VACUUM rewrites the entire DB and requires the daemon to be stopped, so it errors out if the daemon is up. Write-mode only.",inputSchema:{vacuum:T.boolean().optional().describe("Also run VACUUM. Requires the daemon to be stopped.")}},async({vacuum:c})=>{let{runOptimize:l}=await Promise.resolve().then(()=>(mn(),pn)),d=process.stdout.write.bind(process.stdout),u="";process.stdout.write=m=>(u+=typeof m=="string"?m:Buffer.from(m).toString("utf-8"),!0);try{await l({vacuum:!!c,json:!0})}finally{process.stdout.write=d}try{return C(JSON.parse(u.trim()))}catch{return it(u.trim())}}),At(e,{limiter:o}),xs(e,{limiter:o}),console.error(`[claude-recall-mcp] MCP writes ENABLED \u2014 write tools registered (rate limit ${n}/min)`),console.error("[claude-recall-mcp] MCP thread writes ENABLED")}else console.error("[claude-recall-mcp] MCP writes DISABLED (read-only) \u2014 pass --allow-writes to enable"),console.error("[claude-recall-mcp] MCP thread writes DISABLED (read-only)");for(let t of ft)e.registerPrompt(t.name,{title:t.title,description:t.description,argsSchema:t.argsSchema},async n=>({messages:[{role:"user",content:{type:"text",text:t.build(n)}}]}));return e}async function wo(){let e=yo(),s=new bo;await e.connect(s);let t=async()=>{try{await e.close()}catch{}dt(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t)}var Lo=(()=>{try{let e=process.argv[1];return e?import.meta.url===new URL(`file://${e}`).href:!1}catch{return!1}})();Lo&&wo().catch(e=>{console.error("[claude-recall-mcp] fatal:",e),process.exit(1)});export{yo as buildServer};