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,333 @@
1
+ import { z } from 'zod';
2
+ import { MEMORY_LAYERS } from './memory-layers.mjs';
3
+
4
+ // gray-matter parses ISO 8601 strings into JS Date objects.
5
+ // This helper coerces Date → ISO string so Zod validators work
6
+ // regardless of whether input comes from parsed YAML or raw JSON.
7
+ const ssssDatetime = () =>
8
+ z.preprocess(
9
+ (val) => (val instanceof Date ? val.toISOString() : val),
10
+ z.string().datetime()
11
+ );
12
+
13
+ const ssssDatetimeNullable = () =>
14
+ z.preprocess(
15
+ (val) => (val instanceof Date ? val.toISOString() : val),
16
+ z.string().datetime().nullable()
17
+ );
18
+
19
+ // ─── Document Primitives (§5.1 of the SSSS spec) ───────────────────────────
20
+
21
+ export const MemoryNodeSchema = z.object({
22
+ type: z.literal('memory'),
23
+ slug: z.string(),
24
+ category: z.string(),
25
+ title: z.string(),
26
+ status: z.enum(['active', 'superseded', 'deprecated', 'draft']),
27
+ confidence: z.number().min(0).max(1),
28
+ importance: z.number().int().min(1).max(5),
29
+ created: ssssDatetime(),
30
+ updated: ssssDatetime(),
31
+ last_accessed: ssssDatetime(),
32
+ source: z.object({
33
+ type: z.string(),
34
+ session_id: z.string(),
35
+ agent: z.string().optional(),
36
+ evidence_count: z.number().int(),
37
+ }),
38
+ supersedes: z.array(z.string()),
39
+ superseded_by: z.string().nullable(),
40
+ contradicts: z.array(z.string()),
41
+ tags: z.array(z.string()),
42
+ related: z.array(z.string()),
43
+ routes_to_skills: z.array(z.string()),
44
+ sentiment_polarity: z.enum(['directive_must', 'directive_must_not', 'descriptive', 'preference']),
45
+ sentiment_target: z.string(),
46
+ modality: z.enum(['must', 'must_not', 'should', 'should_not']),
47
+ subject: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/),
48
+ predicate: z.string().regex(/^[a-zA-Z0-9_\s.-]+$/),
49
+ object: z.string(),
50
+ decay: z.object({
51
+ half_life_days: z.number(),
52
+ access_count: z.number().int(),
53
+ }),
54
+ schema_version: z.literal(2),
55
+ x_memory_layer: z.enum(MEMORY_LAYERS).optional(),
56
+ x_temporal_context: z.preprocess((val) => (val instanceof Date ? val.toISOString() : val), z.string()).optional(),
57
+ x_citations: z.array(
58
+ z.object({
59
+ source: z.string().optional(),
60
+ title: z.string().optional(),
61
+ url: z.string().optional(),
62
+ published: z.preprocess((val) => (val instanceof Date ? val.toISOString() : val), z.string()).optional(),
63
+ relevance: z.number().optional(),
64
+ accessed: z.preprocess((val) => (val instanceof Date ? val.toISOString() : val), z.string()).optional(),
65
+ })
66
+ ).optional(),
67
+ x_location: z.object({
68
+ lat: z.number(),
69
+ lon: z.number(),
70
+ label: z.string().optional(),
71
+ accuracy: z.number().optional(),
72
+ }).optional().nullable(),
73
+ x_media_refs: z.array(z.object({
74
+ path: z.string(),
75
+ type: z.enum(['image', 'audio', 'video', 'document']),
76
+ description: z.string().optional(),
77
+ size_bytes: z.number().optional(),
78
+ })).optional().nullable(),
79
+ x_browser_context: z.object({
80
+ url: z.string(),
81
+ domain: z.string().optional(),
82
+ title: z.string().optional(),
83
+ tab_group: z.string().optional(),
84
+ visit_count: z.number().optional(),
85
+ }).optional().nullable(),
86
+ // Temporal rule expiration
87
+ expires_at: ssssDatetimeNullable().optional(),
88
+ // Absolute Invariant Extensions
89
+ priority: z.literal('absolute').optional(),
90
+ immutable: z.boolean().optional()
91
+ });
92
+
93
+ export const ConflictRecordSchema = z.object({
94
+ type: z.literal('conflict'),
95
+ conflict_id: z.string(),
96
+ status: z.enum(['pending', 'resolved']),
97
+ new_slug: z.string(),
98
+ existing_slug: z.string(),
99
+ similarity: z.number(),
100
+ polarity_flip: z.boolean(),
101
+ detected_at: ssssDatetime(),
102
+ reason: z.string(),
103
+ resolution: z.string().nullable(),
104
+ resolved_at: ssssDatetimeNullable(),
105
+ });
106
+
107
+ export const TaskSchema = z.object({
108
+ type: z.literal('task'),
109
+ priority: z.number().int(),
110
+ category: z.enum([
111
+ 'memory-maintenance',
112
+ 'system2-deliberation',
113
+ 'skill-engineering',
114
+ 'proactive-research',
115
+ 'self-evaluation',
116
+ 'exploration'
117
+ ]),
118
+ status: z.enum(['pending', 'in_progress', 'done', 'failed']),
119
+ target: z.string().optional(),
120
+ estimated_calls: z.number().int().optional(),
121
+ deadline: z.preprocess(
122
+ (val) => (val instanceof Date ? val.toISOString() : val),
123
+ z.string()
124
+ ).optional(),
125
+ created_by: z.string().optional(),
126
+ reason: z.string().optional(),
127
+ workflow_id: z.string().optional(),
128
+ progress: z.number().optional(),
129
+ x_memory_layer: z.enum(MEMORY_LAYERS).optional(),
130
+ });
131
+
132
+ export const SkillSchema = z.object({
133
+ type: z.literal('skill').optional(), // Optional per spec: real skills may omit type
134
+ name: z.string(),
135
+ description: z.string(),
136
+ });
137
+
138
+ export const AssistantSchema = z.object({
139
+ type: z.literal('assistant'),
140
+ name: z.string(),
141
+ description: z.string().optional(),
142
+ model: z.string().optional(),
143
+ });
144
+
145
+ export const WorkflowSchema = z.object({
146
+ type: z.literal('workflow'),
147
+ name: z.string(),
148
+ description: z.string().optional(),
149
+ triggers: z.array(z.object({
150
+ type: z.string(),
151
+ cron: z.string().optional(),
152
+ })).optional(),
153
+ isActive: z.boolean().optional(),
154
+ });
155
+
156
+ export const RuleSchema = z.object({
157
+ type: z.literal('rule'),
158
+ name: z.string(),
159
+ description: z.string().optional(),
160
+ scope: z.string().optional(),
161
+ });
162
+
163
+ export const ModelSchema = z.object({
164
+ type: z.literal('model'),
165
+ model_id: z.string(),
166
+ provider: z.string(),
167
+ display_name: z.string().optional(),
168
+ });
169
+
170
+ export const ConversationSchema = z.object({
171
+ type: z.literal('conversation'),
172
+ thread_id: z.string(),
173
+ workspace_id: z.string().optional(),
174
+ user_id: z.string().optional(),
175
+ status: z.enum(['active', 'archived', 'closed']).optional(),
176
+ turn_count: z.number().int().optional(),
177
+ created_at: ssssDatetime().optional(),
178
+ });
179
+
180
+ export const RunSchema = z.object({
181
+ type: z.literal('run'),
182
+ run_id: z.string(),
183
+ workflow_id: z.string(),
184
+ status: z.enum(['pending', 'running', 'done', 'failed']).optional(),
185
+ step_count: z.number().int().optional(),
186
+ started_at: ssssDatetime().optional(),
187
+ });
188
+
189
+ export const ProposalSchema = z.object({
190
+ type: z.literal('proposal'),
191
+ proposal_id: z.string(),
192
+ category: z.enum([
193
+ 'memory-cleanup',
194
+ 'skill-improvement',
195
+ 'workflow-repair',
196
+ 'model-routing',
197
+ 'stale-knowledge-refresh',
198
+ 'tool-adapter',
199
+ 'schema-friction',
200
+ ]),
201
+ status: z.enum(['draft', 'pending', 'accepted', 'rejected', 'superseded']),
202
+ target_path: z.string().optional(),
203
+ summary: z.string(),
204
+ rationale: z.string().optional(),
205
+ proposed_by: z.string(),
206
+ proposed_at: ssssDatetime(),
207
+ reviewed_at: ssssDatetimeNullable().optional(),
208
+ reviewed_by: z.string().nullable().optional(),
209
+ rejection_reason: z.string().nullable().optional(),
210
+ });
211
+
212
+ export const SchemaProposalSchema = z.object({
213
+ type: z.literal('schema-proposal'),
214
+ proposal_id: z.string(),
215
+ status: z.enum(['draft', 'pending', 'accepted', 'rejected']),
216
+ from_version: z.number().int(),
217
+ to_version: z.number().int(),
218
+ summary: z.string(),
219
+ breaking: z.boolean(),
220
+ migration_path: z.string().optional(),
221
+ proposed_by: z.string(),
222
+ proposed_at: ssssDatetime(),
223
+ reviewed_at: ssssDatetimeNullable().optional(),
224
+ reviewed_by: z.string().nullable().optional(),
225
+ });
226
+
227
+ export const MigrationSchema = z.object({
228
+ type: z.literal('migration'),
229
+ migration_id: z.string(),
230
+ from_version: z.number().int(),
231
+ to_version: z.number().int(),
232
+ status: z.enum(['pending', 'applied', 'rolled-back', 'failed']),
233
+ description: z.string(),
234
+ applied_at: ssssDatetimeNullable().optional(),
235
+ checksum: z.string().optional(),
236
+ });
237
+
238
+ export const ReleaseSchema = z.object({
239
+ type: z.literal('release'),
240
+ release_id: z.string(),
241
+ version: z.string(),
242
+ schema_version: z.number().int(),
243
+ summary: z.string(),
244
+ released_at: ssssDatetime(),
245
+ signed_by: z.string().optional(),
246
+ signature: z.string().optional(),
247
+ changelog: z.string().optional(),
248
+ });
249
+
250
+ // ─── Contract Primitives (§5.2, §6 of the SSSS spec) ───────────────────────
251
+
252
+ export const OperationEnvelopeSchema = z.object({
253
+ type: z.literal('operation'),
254
+ idempotency_key: z.string().min(8),
255
+ path: z.string(),
256
+ workspace_id: z.string(),
257
+ content: z.string(),
258
+ lease_id: z.string().optional(),
259
+ intent: z.string().optional(),
260
+ dry_run: z.boolean().optional(),
261
+ });
262
+
263
+ export const PatchEnvelopeSchema = z.object({
264
+ type: z.literal('patch'),
265
+ idempotency_key: z.string().min(8),
266
+ path: z.string(),
267
+ workspace_id: z.string(),
268
+ patches: z.record(z.unknown()),
269
+ lease_id: z.string().optional(),
270
+ intent: z.string().optional(),
271
+ dry_run: z.boolean().optional(),
272
+ });
273
+
274
+ export const EventEnvelopeSchema = z.object({
275
+ type: z.literal('event'),
276
+ idempotency_key: z.string().min(8),
277
+ path: z.string(),
278
+ workspace_id: z.string(),
279
+ content: z.string(),
280
+ lease_id: z.string().optional(),
281
+ intent: z.string().optional(),
282
+ });
283
+
284
+ // ─── Operation Response (§6.4 of the SSSS spec) ────────────────────────────
285
+
286
+ export const OperationResponseSchema = z.object({
287
+ success: z.boolean(),
288
+ type: z.enum(['operation', 'patch', 'event']),
289
+ operation_id: z.string(),
290
+ path: z.string(),
291
+ committed_at: z.string().datetime().nullable(),
292
+ dry_run: z.boolean().optional(),
293
+ validation: z.object({
294
+ valid: z.boolean(),
295
+ type: z.string().optional(),
296
+ errors: z.array(z.string()),
297
+ warnings: z.array(z.string()),
298
+ }),
299
+ replay: z.record(z.unknown()).optional(),
300
+ repair: z.object({
301
+ field_errors: z.array(z.object({
302
+ field: z.string(),
303
+ issue: z.string(),
304
+ })),
305
+ }).optional(),
306
+ });
307
+
308
+ // ─── Derived / Utility Schemas ──────────────────────────────────────────────
309
+
310
+ export const SubQuerySchema = z.object({
311
+ queries: z.array(z.string().describe("Specific search queries to resolve the user's overarching research request")),
312
+ reasoning: z.string().describe("Explanation for why these sub-queries are necessary")
313
+ });
314
+
315
+ // ─── Schema Registry (§5 of the SSSS spec) ─────────────────────────────────
316
+
317
+ /** Map from SSSS `type` value to its Zod schema. Used by the operation validator. */
318
+ export const SSSS_SCHEMAS = {
319
+ memory: MemoryNodeSchema,
320
+ conflict: ConflictRecordSchema,
321
+ task: TaskSchema,
322
+ skill: SkillSchema,
323
+ assistant: AssistantSchema,
324
+ workflow: WorkflowSchema,
325
+ rule: RuleSchema,
326
+ model: ModelSchema,
327
+ conversation: ConversationSchema,
328
+ run: RunSchema,
329
+ proposal: ProposalSchema,
330
+ 'schema-proposal': SchemaProposalSchema,
331
+ migration: MigrationSchema,
332
+ release: ReleaseSchema,
333
+ };
@@ -0,0 +1,219 @@
1
+ /**
2
+ * src/core/search.mjs
3
+ *
4
+ * Canonical semantic search logic — searches vault nodes and session history
5
+ * by meaning via Ollama embeddings. Called by both the MCP layer and REST API.
6
+ *
7
+ * MCP and REST are thin wrappers; all logic lives here.
8
+ */
9
+
10
+ import { loadNodes } from './vault.mjs';
11
+ import {
12
+ getEmbedding,
13
+ cosineSimilarity,
14
+ loadEmbeddingsIndex,
15
+ loadSessionEmbeddingsIndex,
16
+ } from './embeddings.mjs';
17
+
18
+ /**
19
+ * Run a semantic search across vault nodes and/or session history.
20
+ *
21
+ * @param {string} query Natural language query
22
+ * @param {object} opts
23
+ * @param {string} opts.vaultDir
24
+ * @param {string} opts.derivedDir
25
+ * @param {number} [opts.top_k=5] Max results to return (capped at 20)
26
+ * @param {boolean} [opts.includeSessions=true] Include session chunk results
27
+ *
28
+ * @returns {Promise<Array<{
29
+ * type: 'vault' | 'session',
30
+ * score: number,
31
+ * // vault fields: slug, title, category, tags, ...
32
+ * // session fields: key, session_id, snippet, chunk, total_chunks
33
+ * }>>}
34
+ */
35
+ export async function semanticSearch(query, {
36
+ vaultDir,
37
+ derivedDir,
38
+ top_k = 5,
39
+ includeSessions = true,
40
+ category = null,
41
+ tags = null,
42
+ modality = null,
43
+ importance = null,
44
+ priority = null
45
+ } = {}) {
46
+ const k = Math.min(Number(top_k) || 5, 20);
47
+ let queryEmbedding = null;
48
+ try {
49
+ queryEmbedding = await getEmbedding(String(query));
50
+ } catch (err) {
51
+ // Gracefully ignore embedding generation errors to allow text search fallback
52
+ }
53
+ const results = [];
54
+
55
+ // If metadata filters are active, session log search is automatically bypassed
56
+ const hasMetaFilters = category || (tags && tags.length > 0) || modality || importance !== null || priority;
57
+ const activeIncludeSessions = hasMetaFilters ? false : includeSessions;
58
+
59
+ // ── Vault nodes ────────────────────────────────────────────────────────────
60
+ const vaultIndex = loadEmbeddingsIndex(derivedDir);
61
+ const vaultEntries = Object.entries(vaultIndex);
62
+
63
+ if (queryEmbedding && vaultEntries.length > 0) {
64
+ let filteredNodes = loadNodes(vaultDir);
65
+
66
+ if (category) {
67
+ const catLower = category.toLowerCase();
68
+ filteredNodes = filteredNodes.filter(n => n.category && n.category.toLowerCase() === catLower);
69
+ }
70
+ if (tags && tags.length > 0) {
71
+ filteredNodes = filteredNodes.filter(n => n.tags && tags.some(t => n.tags.includes(t)));
72
+ }
73
+ if (modality) {
74
+ const modLower = modality.toLowerCase();
75
+ filteredNodes = filteredNodes.filter(n => n.modality && n.modality.toLowerCase() === modLower);
76
+ }
77
+ if (importance !== null && importance !== undefined) {
78
+ const minImp = parseInt(importance, 10);
79
+ filteredNodes = filteredNodes.filter(n => {
80
+ const val = parseInt(n.importance, 10);
81
+ return !isNaN(val) && val >= minImp;
82
+ });
83
+ }
84
+ if (priority) {
85
+ const prioLower = priority.toLowerCase();
86
+ filteredNodes = filteredNodes.filter(n => n.priority && n.priority.toLowerCase() === prioLower);
87
+ }
88
+
89
+ const filteredSlugs = new Set(filteredNodes.map(n => n.slug));
90
+
91
+ const scored = vaultEntries
92
+ .filter(([slug]) => filteredSlugs.has(slug))
93
+ .map(([slug, { embedding }]) => ({ slug, score: cosineSimilarity(queryEmbedding, embedding) }))
94
+ .sort((a, b) => b.score - a.score)
95
+ .slice(0, k);
96
+
97
+ for (const { slug, score } of scored) {
98
+ const node = filteredNodes.find(n => n.slug === slug);
99
+ if (node) {
100
+ results.push({ ...node, type: 'vault', score: Math.round(score * 1000) / 1000 });
101
+ }
102
+ }
103
+ } else {
104
+ // ── Text Search Fallback for Vault Nodes ──
105
+ try {
106
+ let filteredNodes = loadNodes(vaultDir);
107
+
108
+ if (category) {
109
+ const catLower = category.toLowerCase();
110
+ filteredNodes = filteredNodes.filter(n => n.category && n.category.toLowerCase() === catLower);
111
+ }
112
+ if (tags && tags.length > 0) {
113
+ filteredNodes = filteredNodes.filter(n => n.tags && tags.some(t => n.tags.includes(t)));
114
+ }
115
+ if (modality) {
116
+ const modLower = modality.toLowerCase();
117
+ filteredNodes = filteredNodes.filter(n => n.modality && n.modality.toLowerCase() === modLower);
118
+ }
119
+ if (importance !== null && importance !== undefined) {
120
+ const minImp = parseInt(importance, 10);
121
+ filteredNodes = filteredNodes.filter(n => {
122
+ const val = parseInt(n.importance, 10);
123
+ return !isNaN(val) && val >= minImp;
124
+ });
125
+ }
126
+ if (priority) {
127
+ const prioLower = priority.toLowerCase();
128
+ filteredNodes = filteredNodes.filter(n => n.priority && n.priority.toLowerCase() === prioLower);
129
+ }
130
+
131
+ const qLower = String(query).toLowerCase();
132
+ const scoredText = [];
133
+
134
+ for (const node of filteredNodes) {
135
+ let score = 0;
136
+ const slug = String(node.slug || '').toLowerCase();
137
+ const title = String(node.title || '').toLowerCase();
138
+ const body = String(node.body || '').toLowerCase();
139
+ const nTags = Array.isArray(node.tags) ? node.tags.map(t => String(t).toLowerCase()) : [];
140
+
141
+ if (slug === qLower || title === qLower) {
142
+ score = 0.9;
143
+ } else if (slug.includes(qLower) || title.includes(qLower)) {
144
+ score = 0.7;
145
+ } else if (nTags.some(t => t.includes(qLower))) {
146
+ score = 0.6;
147
+ } else if (body.includes(qLower)) {
148
+ score = 0.5;
149
+ }
150
+
151
+ if (score > 0) {
152
+ scoredText.push({ node, score });
153
+ }
154
+ }
155
+
156
+ scoredText.sort((a, b) => b.score - a.score);
157
+ for (const { node, score } of scoredText.slice(0, k)) {
158
+ results.push({ ...node, type: 'vault', score });
159
+ }
160
+ } catch {
161
+ // Gracefully ignore filesystem reading issues
162
+ }
163
+ }
164
+
165
+ // ── Sessions ───────────────────────────────────────────────────────────────
166
+ if (activeIncludeSessions) {
167
+ const sessionIndex = loadSessionEmbeddingsIndex(derivedDir);
168
+ const sessionEntries = Object.entries(sessionIndex);
169
+
170
+ if (queryEmbedding && sessionEntries.length > 0) {
171
+ const scored = sessionEntries
172
+ .map(([key, entry]) => ({
173
+ key,
174
+ session_id: entry.session_id || key,
175
+ score: cosineSimilarity(queryEmbedding, entry.embedding),
176
+ snippet: entry.snippet,
177
+ chunk: entry.chunk,
178
+ total_chunks: entry.total_chunks,
179
+ }))
180
+ .sort((a, b) => b.score - a.score)
181
+ .slice(0, k);
182
+
183
+ for (const { key, session_id, score, snippet, chunk, total_chunks } of scored) {
184
+ results.push({ type: 'session', key, session_id, snippet, chunk, total_chunks, score: Math.round(score * 1000) / 1000 });
185
+ }
186
+ } else {
187
+ // ── Text Search Fallback for Sessions ──
188
+ try {
189
+ const qLower = String(query).toLowerCase();
190
+ const scoredText = [];
191
+ for (const [key, entry] of sessionEntries) {
192
+ const snippet = String(entry.snippet || '').toLowerCase();
193
+ if (snippet.includes(qLower)) {
194
+ scoredText.push({
195
+ type: 'session',
196
+ key,
197
+ session_id: entry.session_id || key,
198
+ score: 0.5,
199
+ snippet: entry.snippet,
200
+ chunk: entry.chunk,
201
+ total_chunks: entry.total_chunks
202
+ });
203
+ }
204
+ }
205
+ scoredText.sort((a, b) => b.score - a.score);
206
+ for (const r of scoredText.slice(0, k)) {
207
+ results.push(r);
208
+ }
209
+ } catch {}
210
+ }
211
+ }
212
+
213
+ results.sort((a, b) => b.score - a.score);
214
+ const finalResults = results.slice(0, k);
215
+ if (!queryEmbedding || vaultEntries.length === 0) {
216
+ finalResults.degradedTextSearch = true;
217
+ }
218
+ return finalResults;
219
+ }