total-recall-brain 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * /api/sessions/* routes
3
+ *
4
+ * - GET /api/sessions list (limit, offset)
5
+ * - GET /api/sessions/:id read full session
6
+ * - POST /api/sessions/ingest ingest (raw file or pre-parsed)
7
+ * - DELETE /api/sessions/:id delete
8
+ */
9
+
10
+ import express from 'express';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import crypto from 'node:crypto';
14
+ import { requireAuth, requireScope, ingestRateLimiter } from '../auth.mjs';
15
+ import {
16
+ parseSessionFile,
17
+ sessionToEmbedChunks,
18
+ getEmbedding,
19
+ saveSessionEmbeddingToIndex,
20
+ removeSessionEmbeddingFromIndex,
21
+ } from '../../core/embeddings.mjs';
22
+ import {
23
+ AGENT_DIR,
24
+ BRAIN_DIR,
25
+ SESSIONS_DIR,
26
+ DERIVED_DIR,
27
+ notFound,
28
+ badRequest,
29
+ serverError,
30
+ } from './_shared.mjs';
31
+
32
+ const router = express.Router();
33
+
34
+ const activeEmbeddings = new Set();
35
+
36
+ async function drainActiveEmbeddings() {
37
+ if (activeEmbeddings.size === 0) return;
38
+ await Promise.allSettled(Array.from(activeEmbeddings));
39
+ }
40
+
41
+
42
+ function listSessionFiles() {
43
+ if (!fs.existsSync(SESSIONS_DIR)) return [];
44
+ return fs.readdirSync(SESSIONS_DIR)
45
+ .filter(f => f.endsWith('.jsonl') || f.endsWith('.json') || f.endsWith('.md'))
46
+ .sort()
47
+ .reverse();
48
+ }
49
+
50
+ function readSessionFile(filename) {
51
+ const filePath = path.join(SESSIONS_DIR, filename);
52
+ if (!fs.existsSync(filePath)) return null;
53
+ const raw = fs.readFileSync(filePath, 'utf8').trim();
54
+
55
+ const id = filename.replace(/\.(jsonl|json|md)$/, '');
56
+ let meta = {};
57
+
58
+ // Parse SSSS frontmatter from .md files
59
+ let body = raw;
60
+ if (filename.endsWith('.md') && raw.startsWith('---')) {
61
+ const endIdx = raw.indexOf('---', 3);
62
+ if (endIdx !== -1) {
63
+ const frontmatter = raw.slice(3, endIdx).trim();
64
+ body = raw.slice(endIdx + 3).trim();
65
+ // Simple YAML parser for known fields
66
+ for (const line of frontmatter.split('\n')) {
67
+ const match = line.match(/^(\w+):\s*"?(.+?)"?\s*$/);
68
+ if (match) meta[match[1]] = match[2];
69
+ }
70
+ }
71
+ }
72
+
73
+ const lines = body.split('\n').filter(Boolean);
74
+ const entries = lines.map(l => {
75
+ try { return JSON.parse(l); } catch { return null; }
76
+ }).filter(Boolean);
77
+
78
+ let exchanges = entries;
79
+ if (entries.length === 1 && entries[0] && Array.isArray(entries[0].messages)) {
80
+ exchanges = entries[0].messages.map(m => ({
81
+ ...m,
82
+ session_id: m.session_id || entries[0].id || entries[0].session_id || id
83
+ }));
84
+ } else {
85
+ exchanges = entries.map(m => ({
86
+ ...m,
87
+ session_id: m.session_id || id
88
+ }));
89
+ }
90
+
91
+ return {
92
+ id,
93
+ filename,
94
+ title: meta.title || id,
95
+ date: meta.date || null,
96
+ source: meta.source || null,
97
+ project: meta.project || null,
98
+ entries,
99
+ exchanges,
100
+ count: exchanges.length
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Extract human/assistant turns from raw IDE session file content.
106
+ * Handles JSONL (Claude Code, Codex, Cursor, VS Code) and plaintext (Antigravity overview).
107
+ */
108
+ function parseRawSessionContent(content, source) {
109
+ const messages = [];
110
+ const lines = content.split('\n').filter(Boolean);
111
+
112
+ for (const line of lines) {
113
+ let rec;
114
+ try { rec = JSON.parse(line); } catch { continue; }
115
+
116
+ if (source === 'antigravity' && (rec.type === 'USER_INPUT' || (rec.source === 'MODEL' && (rec.type === 'PLANNER_RESPONSE' || rec.type === 'RESPONSE' || rec.type === 'model_response')))) {
117
+ let role = rec.type === 'USER_INPUT' ? 'user' : 'assistant';
118
+ let text = rec.content || '';
119
+ if (role === 'user' && text.includes('<USER_REQUEST>')) {
120
+ const match = text.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
121
+ if (match) text = match[1].trim();
122
+ }
123
+ if (text.trim()) messages.push({ role, content: text.slice(0, 8000) });
124
+ continue;
125
+ }
126
+
127
+ if (rec.role && (rec.content || rec.message)) {
128
+ const text = typeof rec.content === 'string' ? rec.content
129
+ : Array.isArray(rec.content) ? rec.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
130
+ : typeof rec.message?.text === 'string' ? rec.message.text
131
+ : '';
132
+ if (text.trim()) messages.push({ role: rec.role, content: text.slice(0, 8000) });
133
+ continue;
134
+ }
135
+
136
+ if (rec.kind === 2 && Array.isArray(rec.v)) {
137
+ for (const req of rec.v) {
138
+ const user = req.message?.text || '';
139
+ const asst = req.response?.response?.value || req.response?.value || '';
140
+ if (user) messages.push({ role: 'user', content: user.slice(0, 8000) });
141
+ if (asst) messages.push({ role: 'assistant', content: asst.slice(0, 8000) });
142
+ }
143
+ }
144
+ }
145
+
146
+ if (messages.length === 0 && content.trim()) {
147
+ if (source === 'antigravity') {
148
+ for (const line of content.split('\n').filter(Boolean)) {
149
+ const trimmed = line.trim();
150
+ if (!trimmed) continue;
151
+ let role = 'assistant';
152
+ if (trimmed.startsWith('USER:') || trimmed.startsWith('User:') || trimmed.startsWith('[user]')) {
153
+ role = 'user';
154
+ } else if (trimmed.startsWith('TOOL:') || trimmed.startsWith('[tool]') || trimmed.includes('tool_call')) {
155
+ role = 'tool';
156
+ }
157
+ messages.push({ role, content: trimmed.slice(0, 8000) });
158
+ }
159
+ } else {
160
+ messages.push({ role: 'assistant', content: content.slice(0, 20000) });
161
+ }
162
+ }
163
+
164
+ return messages;
165
+ }
166
+
167
+ router.get('/api/sessions', requireAuth, requireScope('memory:read'), (req, res) => {
168
+ try {
169
+ const files = listSessionFiles();
170
+ const { limit = '50', offset = '0' } = req.query;
171
+ const off = parseInt(offset, 10) || 0;
172
+ const lim = Math.min(200, parseInt(limit, 10) || 50);
173
+ const page = files.slice(off, off + lim).map(f => {
174
+ const data = readSessionFile(f);
175
+ const stat = fs.statSync(path.join(SESSIONS_DIR, f));
176
+ return {
177
+ id: data?.id,
178
+ filename: f,
179
+ title: data?.title || data?.id,
180
+ date: data?.date || stat.mtime.toISOString(),
181
+ source: data?.source || null,
182
+ project: data?.project || null,
183
+ count: data?.count || 0,
184
+ modified: stat.mtime.toISOString(),
185
+ size: stat.size
186
+ };
187
+ });
188
+ res.json({ total: files.length, offset: off, limit: lim, sessions: page });
189
+ } catch (err) {
190
+ serverError(res, err);
191
+ }
192
+ });
193
+
194
+ router.get('/api/sessions/:id', requireAuth, requireScope('memory:read'), (req, res) => {
195
+ try {
196
+ const files = listSessionFiles();
197
+ const match = files.find(f => f.startsWith(req.params.id));
198
+ if (!match) return notFound(res, `Session not found: ${req.params.id}`);
199
+ res.json(readSessionFile(match));
200
+ } catch (err) {
201
+ serverError(res, err);
202
+ }
203
+ });
204
+
205
+ router.post('/api/sessions/ingest', ingestRateLimiter(), requireAuth, requireScope('memory:write'), (req, res) => {
206
+ try {
207
+ const body = req.body || {};
208
+ const source = body.source || 'api';
209
+
210
+ let messages;
211
+ if (body.content && typeof body.content === 'string') {
212
+ messages = parseRawSessionContent(body.content, source);
213
+ } else if (Array.isArray(body.messages)) {
214
+ messages = body.messages;
215
+ } else {
216
+ return badRequest(res, 'Provide either {content} (raw file) or {messages:[{role,content}]}');
217
+ }
218
+
219
+ if (messages.length === 0) {
220
+ return res.status(200).json({ ingested: false, reason: 'no extractable messages' });
221
+ }
222
+
223
+ // Deduplicate by sha256 of the raw content (relay always sends sha256)
224
+ if (body.sha256) {
225
+ const hashIndex = path.join(BRAIN_DIR, 'memory-derived', 'relay-hashes.jsonl');
226
+ fs.mkdirSync(path.dirname(hashIndex), { recursive: true });
227
+ const seen = new Set(
228
+ fs.existsSync(hashIndex)
229
+ ? fs.readFileSync(hashIndex, 'utf8').split('\n').filter(Boolean).map(l => {
230
+ try { return JSON.parse(l).sha256; } catch { return ''; }
231
+ })
232
+ : []
233
+ );
234
+ if (seen.has(body.sha256)) {
235
+ return res.status(200).json({ ingested: false, reason: 'duplicate' });
236
+ }
237
+ fs.appendFileSync(hashIndex, JSON.stringify({ sha256: body.sha256, ts: new Date().toISOString(), source }) + '\n');
238
+ }
239
+
240
+ // Extract title from first user message
241
+ const firstUserMsg = messages.find(m => m.role === 'user');
242
+ const rawTitle = firstUserMsg?.content?.slice(0, 120)?.split('\n')[0]?.trim() || 'untitled session';
243
+ const cleanTitle = rawTitle
244
+ .replace(/<[^>]+>/g, '') // strip XML/HTML tags
245
+ .replace(/[^a-zA-Z0-9\s-]/g, '') // keep only alphanumeric, spaces, hyphens
246
+ .trim()
247
+ .slice(0, 80);
248
+
249
+ // Generate date-prefixed slug
250
+ const now = new Date();
251
+ const datePrefix = now.toISOString().slice(0, 10); // 2026-05-25
252
+ const titleSlug = cleanTitle.toLowerCase().replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'untitled';
253
+ const uniqueSuffix = crypto.randomBytes(3).toString('hex');
254
+ const slug = `${datePrefix}-${source}-${titleSlug}-${uniqueSuffix}`;
255
+ const filename = `${slug}.md`;
256
+
257
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
258
+ const filePath = path.join(SESSIONS_DIR, filename);
259
+
260
+ // Detect project from relay metadata or path
261
+ const project = body.project || (body.path ? path.basename(path.resolve(body.path, '..', '..', '..')) : null);
262
+
263
+ // Build SSSS frontmatter
264
+ const frontmatter = [
265
+ '---',
266
+ 'type: session',
267
+ `slug: ${slug}`,
268
+ `title: "${cleanTitle || 'Untitled session'}"`,
269
+ `source: ${source}`,
270
+ `date: ${now.toISOString()}`,
271
+ project ? `project: ${project}` : null,
272
+ 'category: sessions',
273
+ 'schema_version: 2',
274
+ `message_count: ${messages.length}`,
275
+ '---',
276
+ ].filter(Boolean).join('\n');
277
+
278
+ // Build session body as JSONL
279
+ const sessionObj = {
280
+ id: slug,
281
+ source,
282
+ messages: messages.map(m => ({
283
+ role: m.role,
284
+ content: m.content,
285
+ source,
286
+ session_id: slug,
287
+ timestamp: m.timestamp || now.toISOString(),
288
+ }))
289
+ };
290
+
291
+ fs.writeFileSync(filePath, frontmatter + '\n' + JSON.stringify(sessionObj) + '\n', 'utf8');
292
+
293
+ // Best-effort: embed session immediately so it's searchable right away
294
+ const promise = (async () => {
295
+ if (process.isShuttingDown) return;
296
+ try {
297
+ const msgs = parseSessionFile(filePath);
298
+ const chunks = sessionToEmbedChunks(msgs);
299
+ for (let i = 0; i < chunks.length; i++) {
300
+ if (process.isShuttingDown) return;
301
+ const key = chunks.length === 1 ? slug : `${slug}:chunk-${i}`;
302
+ const embedding = await getEmbedding(chunks[i]);
303
+ if (process.isShuttingDown) return;
304
+ saveSessionEmbeddingToIndex(DERIVED_DIR, key, chunks[i], embedding);
305
+ }
306
+ } catch (err) {
307
+ /* Ollama may not be running — non-fatal */
308
+ }
309
+ })();
310
+
311
+ activeEmbeddings.add(promise);
312
+ promise.finally(() => {
313
+ activeEmbeddings.delete(promise);
314
+ });
315
+
316
+ res.status(200).json({ ok: true, ingested: true, id: slug, filename, count: messages.length });
317
+ } catch (err) {
318
+ serverError(res, err);
319
+ }
320
+ });
321
+
322
+ router.delete('/api/sessions/:id', requireAuth, requireScope('memory:write'), (req, res) => {
323
+ try {
324
+ const files = listSessionFiles();
325
+ const match = files.find(f => f.startsWith(req.params.id));
326
+ if (!match) return notFound(res, `Session not found: ${req.params.id}`);
327
+ fs.unlinkSync(path.join(SESSIONS_DIR, match));
328
+ try { removeSessionEmbeddingFromIndex(DERIVED_DIR, req.params.id); } catch { /* non-fatal */ }
329
+ res.json({ deleted: true, id: req.params.id });
330
+ } catch (err) {
331
+ serverError(res, err);
332
+ }
333
+ });
334
+
335
+ export { router as sessionsRouter, drainActiveEmbeddings };
336
+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * /api/share routes
3
+ *
4
+ * POST /api/share — Universal share-to-brain endpoint.
5
+ * Accepts content from Chrome extension, CLI, or any external client
6
+ * and routes it to the correct action (remember or research).
7
+ */
8
+
9
+ import express from 'express';
10
+ import crypto from 'node:crypto';
11
+ import { createMemoryNode, writeNode } from '../../core/vault.mjs';
12
+ import { invalidate } from '../../core/vault-cache.mjs';
13
+ import { addToQueue } from '../../core/research-queue.mjs';
14
+ import { requireAuth, requireScope } from '../auth.mjs';
15
+ import {
16
+ VAULT_DIR,
17
+ badRequest,
18
+ serverError,
19
+ resolveVaultFromQuery,
20
+ } from './_shared.mjs';
21
+
22
+ const router = express.Router();
23
+
24
+ /**
25
+ * Resolve the effective action from the auto-routing heuristic.
26
+ *
27
+ * - Has URL + no excerpt → 'research'
28
+ * - Has excerpt < 500 chars → 'remember', category = 'facts'
29
+ * - Has excerpt >= 500 chars → 'remember', category = 'concepts'
30
+ * - Has URL + excerpt → 'remember' with URL as citation
31
+ */
32
+ function resolveAction({ url, excerpt, action }) {
33
+ if (action && action !== 'auto') {
34
+ // Explicit action — determine category based on excerpt length
35
+ if (action === 'remember') {
36
+ const category = excerpt && excerpt.length >= 500 ? 'concepts' : 'facts';
37
+ return { action: 'remember', category };
38
+ }
39
+ return { action, category: null };
40
+ }
41
+
42
+ // Auto-routing heuristic
43
+ if (url && !excerpt) {
44
+ return { action: 'research', category: null };
45
+ }
46
+ if (excerpt && excerpt.length >= 500) {
47
+ return { action: 'remember', category: 'concepts' };
48
+ }
49
+ if (excerpt) {
50
+ return { action: 'remember', category: 'facts' };
51
+ }
52
+ // URL + excerpt (caught by excerpt checks above) or fallback
53
+ if (url) {
54
+ return { action: 'research', category: null };
55
+ }
56
+ return { action: 'remember', category: 'facts' };
57
+ }
58
+
59
+ /**
60
+ * POST /api/share
61
+ * Body: { url, title, excerpt, source, action, brainId, tags }
62
+ */
63
+ router.post('/api/share', requireAuth, requireScope('memory:write'), (req, res) => {
64
+ try {
65
+ const { url, title, excerpt, source, action: rawAction, tags } = req.body || {};
66
+
67
+ // Validate: at least a URL or excerpt is required
68
+ if (!url && !excerpt) {
69
+ return badRequest(res, 'At least one of url or excerpt is required');
70
+ }
71
+
72
+ const { action, category } = resolveAction({ url, excerpt, action: rawAction });
73
+
74
+ if (action === 'research') {
75
+ // Queue a research project
76
+ const topic = title || url || 'Untitled research';
77
+ const item = addToQueue({
78
+ topic,
79
+ priority: 'medium',
80
+ notes: excerpt || '',
81
+ });
82
+
83
+ return res.status(201).json({
84
+ action_taken: 'research',
85
+ id: item.id,
86
+ slug: null,
87
+ message: `Queued research: "${topic}"`,
88
+ });
89
+ }
90
+
91
+ // action === 'remember'
92
+ const vaultDir = resolveVaultFromQuery(req);
93
+ const slugBase = category || 'facts';
94
+ const slug = `${slugBase}-${crypto.randomBytes(4).toString('hex')}`;
95
+ const nodeTitle = title || (excerpt ? excerpt.slice(0, 80) : url || 'Untitled share');
96
+
97
+ const node = createMemoryNode({
98
+ slug,
99
+ title: nodeTitle,
100
+ category: category || 'facts',
101
+ content: excerpt || url || '',
102
+ });
103
+
104
+ // Override source type from request
105
+ node.source.type = source || 'api';
106
+
107
+ // Set tags if provided
108
+ if (tags) {
109
+ node.tags = Array.isArray(tags) ? tags : String(tags).split(',').map(t => t.trim());
110
+ }
111
+
112
+ // Add URL as citation if provided
113
+ if (url) {
114
+ node.x_citations = [{
115
+ url,
116
+ title: title || url,
117
+ source: 'share-to-brain',
118
+ published: node.created,
119
+ relevance: 1.0,
120
+ accessed: node.created,
121
+ }];
122
+ }
123
+
124
+ writeNode(node, vaultDir);
125
+ invalidate();
126
+
127
+ return res.status(201).json({
128
+ action_taken: 'remember',
129
+ id: node.slug,
130
+ slug: `${category || 'facts'}/${node.slug}.md`,
131
+ message: `Saved as ${category || 'facts'}: ${category || 'facts'}/${node.slug}.md`,
132
+ });
133
+ } catch (err) {
134
+ serverError(res, err);
135
+ }
136
+ });
137
+
138
+ export { router as shareRouter };