@usejarvis/brain 0.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,356 @@
1
+ import { test, expect, beforeEach, describe } from 'bun:test';
2
+ import { initDatabase } from './schema.ts';
3
+ import {
4
+ buildExtractionPrompt,
5
+ parseExtractionResponse,
6
+ extractAndStore,
7
+ type ExtractionResult,
8
+ } from './extractor.ts';
9
+ import { findEntities } from './entities.ts';
10
+ import { findFacts } from './facts.ts';
11
+ import { findRelationships } from './relationships.ts';
12
+ import { findCommitments } from './commitments.ts';
13
+ import type { LLMProvider, LLMMessage, LLMOptions, LLMResponse } from '../llm/provider.ts';
14
+
15
+ describe('Vault Extractor', () => {
16
+ beforeEach(() => {
17
+ // Initialize in-memory database for each test
18
+ initDatabase(':memory:');
19
+ });
20
+
21
+ describe('buildExtractionPrompt', () => {
22
+ test('should build prompt with user and assistant messages', () => {
23
+ const userMessage = "My sister Anna's birthday is March 15th";
24
+ const assistantResponse = "I'll remember that Anna's birthday is March 15th!";
25
+
26
+ const prompt = buildExtractionPrompt(userMessage, assistantResponse);
27
+
28
+ expect(prompt).toContain('USER MESSAGE:');
29
+ expect(prompt).toContain(userMessage);
30
+ expect(prompt).toContain('ASSISTANT RESPONSE:');
31
+ expect(prompt).toContain(assistantResponse);
32
+ expect(prompt).toContain('entities');
33
+ expect(prompt).toContain('facts');
34
+ expect(prompt).toContain('relationships');
35
+ expect(prompt).toContain('commitments');
36
+ });
37
+ });
38
+
39
+ describe('parseExtractionResponse', () => {
40
+ test('should parse valid JSON response', () => {
41
+ const response = JSON.stringify({
42
+ entities: [
43
+ { name: 'Anna', type: 'person' },
44
+ ],
45
+ facts: [
46
+ { subject: 'Anna', predicate: 'birthday_is', object: 'March 15', confidence: 1.0 },
47
+ ],
48
+ relationships: [],
49
+ commitments: [],
50
+ });
51
+
52
+ const result = parseExtractionResponse(response);
53
+
54
+ expect(result.entities).toHaveLength(1);
55
+ expect(result.entities[0]!.name).toBe('Anna');
56
+ expect(result.facts).toHaveLength(1);
57
+ expect(result.facts[0]!.predicate).toBe('birthday_is');
58
+ });
59
+
60
+ test('should handle markdown code blocks', () => {
61
+ const response = `\`\`\`json
62
+ {
63
+ "entities": [{ "name": "Bob", "type": "person" }],
64
+ "facts": [],
65
+ "relationships": [],
66
+ "commitments": []
67
+ }
68
+ \`\`\``;
69
+
70
+ const result = parseExtractionResponse(response);
71
+
72
+ expect(result.entities).toHaveLength(1);
73
+ expect(result.entities[0]!.name).toBe('Bob');
74
+ });
75
+
76
+ test('should return empty result on invalid JSON', () => {
77
+ const response = 'This is not JSON';
78
+
79
+ const result = parseExtractionResponse(response);
80
+
81
+ expect(result.entities).toHaveLength(0);
82
+ expect(result.facts).toHaveLength(0);
83
+ expect(result.relationships).toHaveLength(0);
84
+ expect(result.commitments).toHaveLength(0);
85
+ });
86
+
87
+ test('should handle missing fields', () => {
88
+ const response = JSON.stringify({
89
+ entities: [{ name: 'Test', type: 'person' }],
90
+ // Missing facts, relationships, commitments
91
+ });
92
+
93
+ const result = parseExtractionResponse(response);
94
+
95
+ expect(result.entities).toHaveLength(1);
96
+ expect(result.facts).toHaveLength(0);
97
+ expect(result.relationships).toHaveLength(0);
98
+ expect(result.commitments).toHaveLength(0);
99
+ });
100
+ });
101
+
102
+ describe('extractAndStore', () => {
103
+ test('should return empty result when no provider given', async () => {
104
+ const result = await extractAndStore('Hello', 'Hi there');
105
+
106
+ expect(result.entities).toHaveLength(0);
107
+ expect(result.facts).toHaveLength(0);
108
+ });
109
+
110
+ test('should extract and store entities', async () => {
111
+ const mockProvider: LLMProvider = {
112
+ name: 'mock',
113
+ async chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse> {
114
+ return {
115
+ content: JSON.stringify({
116
+ entities: [
117
+ { name: 'Alice', type: 'person' },
118
+ { name: 'Project Phoenix', type: 'project' },
119
+ ],
120
+ facts: [],
121
+ relationships: [],
122
+ commitments: [],
123
+ }),
124
+ tool_calls: [],
125
+ usage: { input_tokens: 100, output_tokens: 50 },
126
+ model: 'mock-model',
127
+ finish_reason: 'stop',
128
+ };
129
+ },
130
+ async *stream() {
131
+ yield { type: 'done', response: {} as any };
132
+ },
133
+ async listModels() {
134
+ return ['mock-model'];
135
+ },
136
+ };
137
+
138
+ const result = await extractAndStore(
139
+ 'Alice is working on Project Phoenix',
140
+ 'Got it!',
141
+ mockProvider
142
+ );
143
+
144
+ expect(result.entities).toHaveLength(2);
145
+
146
+ // Verify entities were stored in database
147
+ const entities = findEntities({});
148
+ expect(entities).toHaveLength(2);
149
+ expect(entities.find((e) => e.name === 'Alice')).toBeDefined();
150
+ expect(entities.find((e) => e.name === 'Project Phoenix')).toBeDefined();
151
+ });
152
+
153
+ test('should extract and store facts', async () => {
154
+ const mockProvider: LLMProvider = {
155
+ name: 'mock',
156
+ async chat(): Promise<LLMResponse> {
157
+ return {
158
+ content: JSON.stringify({
159
+ entities: [
160
+ { name: 'Bob', type: 'person' },
161
+ ],
162
+ facts: [
163
+ { subject: 'Bob', predicate: 'email_is', object: 'bob@example.com', confidence: 1.0 },
164
+ ],
165
+ relationships: [],
166
+ commitments: [],
167
+ }),
168
+ tool_calls: [],
169
+ usage: { input_tokens: 100, output_tokens: 50 },
170
+ model: 'mock-model',
171
+ finish_reason: 'stop',
172
+ };
173
+ },
174
+ async *stream() {
175
+ yield { type: 'done', response: {} as any };
176
+ },
177
+ async listModels() {
178
+ return ['mock-model'];
179
+ },
180
+ };
181
+
182
+ await extractAndStore(
183
+ "Bob's email is bob@example.com",
184
+ 'Noted!',
185
+ mockProvider
186
+ );
187
+
188
+ // Verify fact was stored
189
+ const facts = findFacts({});
190
+ expect(facts).toHaveLength(1);
191
+ expect(facts[0]!.predicate).toBe('email_is');
192
+ expect(facts[0]!.object).toBe('bob@example.com');
193
+ });
194
+
195
+ test('should extract and store relationships', async () => {
196
+ const mockProvider: LLMProvider = {
197
+ name: 'mock',
198
+ async chat(): Promise<LLMResponse> {
199
+ return {
200
+ content: JSON.stringify({
201
+ entities: [
202
+ { name: 'Alice', type: 'person' },
203
+ { name: 'Bob', type: 'person' },
204
+ ],
205
+ facts: [],
206
+ relationships: [
207
+ { from: 'Alice', to: 'Bob', type: 'manages' },
208
+ ],
209
+ commitments: [],
210
+ }),
211
+ tool_calls: [],
212
+ usage: { input_tokens: 100, output_tokens: 50 },
213
+ model: 'mock-model',
214
+ finish_reason: 'stop',
215
+ };
216
+ },
217
+ async *stream() {
218
+ yield { type: 'done', response: {} as any };
219
+ },
220
+ async listModels() {
221
+ return ['mock-model'];
222
+ },
223
+ };
224
+
225
+ await extractAndStore(
226
+ 'Alice manages Bob',
227
+ 'Understood!',
228
+ mockProvider
229
+ );
230
+
231
+ // Verify relationship was stored
232
+ const relationships = findRelationships({});
233
+ expect(relationships).toHaveLength(1);
234
+ expect(relationships[0]!.type).toBe('manages');
235
+ });
236
+
237
+ test('should extract and store commitments', async () => {
238
+ const mockProvider: LLMProvider = {
239
+ name: 'mock',
240
+ async chat(): Promise<LLMResponse> {
241
+ return {
242
+ content: JSON.stringify({
243
+ entities: [],
244
+ facts: [],
245
+ relationships: [],
246
+ commitments: [
247
+ {
248
+ what: 'Remind about meeting',
249
+ when_due: '2026-03-15T10:00:00Z',
250
+ priority: 'high',
251
+ },
252
+ ],
253
+ }),
254
+ tool_calls: [],
255
+ usage: { input_tokens: 100, output_tokens: 50 },
256
+ model: 'mock-model',
257
+ finish_reason: 'stop',
258
+ };
259
+ },
260
+ async *stream() {
261
+ yield { type: 'done', response: {} as any };
262
+ },
263
+ async listModels() {
264
+ return ['mock-model'];
265
+ },
266
+ };
267
+
268
+ await extractAndStore(
269
+ 'Remind me about the meeting on March 15',
270
+ 'Will do!',
271
+ mockProvider
272
+ );
273
+
274
+ // Verify commitment was stored
275
+ const commitments = findCommitments({});
276
+ expect(commitments).toHaveLength(1);
277
+ expect(commitments[0]!.what).toBe('Remind about meeting');
278
+ expect(commitments[0]!.priority).toBe('high');
279
+ expect(commitments[0]!.when_due).toBeTruthy();
280
+ });
281
+
282
+ test('should reuse existing entities', async () => {
283
+ const mockProvider: LLMProvider = {
284
+ name: 'mock',
285
+ async chat(): Promise<LLMResponse> {
286
+ return {
287
+ content: JSON.stringify({
288
+ entities: [
289
+ { name: 'Charlie', type: 'person' },
290
+ ],
291
+ facts: [
292
+ { subject: 'Charlie', predicate: 'location_is', object: 'NYC', confidence: 1.0 },
293
+ ],
294
+ relationships: [],
295
+ commitments: [],
296
+ }),
297
+ tool_calls: [],
298
+ usage: { input_tokens: 100, output_tokens: 50 },
299
+ model: 'mock-model',
300
+ finish_reason: 'stop',
301
+ };
302
+ },
303
+ async *stream() {
304
+ yield { type: 'done', response: {} as any };
305
+ },
306
+ async listModels() {
307
+ return ['mock-model'];
308
+ },
309
+ };
310
+
311
+ // First extraction
312
+ await extractAndStore('Charlie lives in NYC', 'Got it!', mockProvider);
313
+
314
+ // Second extraction with same entity
315
+ await extractAndStore('Charlie works remotely', 'Noted!', mockProvider);
316
+
317
+ // Should only have one Charlie entity
318
+ const entities = findEntities({ name: 'Charlie' });
319
+ expect(entities).toHaveLength(1);
320
+ });
321
+
322
+ test('should handle invalid entity types', async () => {
323
+ const mockProvider: LLMProvider = {
324
+ name: 'mock',
325
+ async chat(): Promise<LLMResponse> {
326
+ return {
327
+ content: JSON.stringify({
328
+ entities: [
329
+ { name: 'Invalid', type: 'invalid_type' },
330
+ ],
331
+ facts: [],
332
+ relationships: [],
333
+ commitments: [],
334
+ }),
335
+ tool_calls: [],
336
+ usage: { input_tokens: 100, output_tokens: 50 },
337
+ model: 'mock-model',
338
+ finish_reason: 'stop',
339
+ };
340
+ },
341
+ async *stream() {
342
+ yield { type: 'done', response: {} as any };
343
+ },
344
+ async listModels() {
345
+ return ['mock-model'];
346
+ },
347
+ };
348
+
349
+ await extractAndStore('Test', 'Test', mockProvider);
350
+
351
+ // Entity should not be created
352
+ const entities = findEntities({});
353
+ expect(entities).toHaveLength(0);
354
+ });
355
+ });
356
+ });
@@ -0,0 +1,345 @@
1
+ import type { LLMProvider } from '../llm/provider.ts';
2
+ import { createEntity, findEntities } from './entities.ts';
3
+ import { createFact } from './facts.ts';
4
+ import { createRelationship } from './relationships.ts';
5
+ import { createCommitment } from './commitments.ts';
6
+
7
+ export type ExtractionResult = {
8
+ entities: Array<{ name: string; type: string; properties?: Record<string, unknown> }>;
9
+ facts: Array<{ subject: string; predicate: string; object: string; confidence: number }>;
10
+ relationships: Array<{ from: string; to: string; type: string }>;
11
+ commitments: Array<{ what: string; when_due?: string; priority?: string }>;
12
+ };
13
+
14
+ /**
15
+ * Build extraction prompt for LLM
16
+ */
17
+ export function buildExtractionPrompt(userMessage: string, assistantResponse: string): string {
18
+ return `You are an expert at extracting structured information from conversations. Analyze the following conversation and extract entities, facts, relationships, and commitments.
19
+
20
+ USER MESSAGE:
21
+ ${userMessage}
22
+
23
+ ASSISTANT RESPONSE:
24
+ ${assistantResponse}
25
+
26
+ Extract the following information and return ONLY valid JSON (no markdown, no explanation):
27
+
28
+ {
29
+ "entities": [
30
+ {
31
+ "name": "Entity name",
32
+ "type": "person|project|tool|place|concept|event",
33
+ "properties": {}
34
+ }
35
+ ],
36
+ "facts": [
37
+ {
38
+ "subject": "Entity name",
39
+ "predicate": "property_name",
40
+ "object": "value",
41
+ "confidence": 0.0-1.0
42
+ }
43
+ ],
44
+ "relationships": [
45
+ {
46
+ "from": "Entity A name",
47
+ "to": "Entity B name",
48
+ "type": "relationship_type"
49
+ }
50
+ ],
51
+ "commitments": [
52
+ {
53
+ "what": "Description of commitment",
54
+ "when_due": "ISO date string (optional)",
55
+ "priority": "low|normal|high|critical (optional)"
56
+ }
57
+ ]
58
+ }
59
+
60
+ GUIDELINES:
61
+ - Extract only concrete, verifiable information
62
+ - For entities: identify people, projects, tools, places, concepts, events
63
+ - For facts: extract attributes about entities (e.g., "birthday_is", "works_at", "location_is")
64
+ - For relationships: extract connections between entities (e.g., "sister_of", "manages", "part_of")
65
+ - For commitments: extract any promises, tasks, or reminders mentioned
66
+ - Use snake_case for predicates and relationship types
67
+ - Set confidence lower (0.5-0.8) for implied or uncertain information
68
+ - If no information to extract, return empty arrays
69
+ - Respond with ONLY the JSON object, no other text`;
70
+ }
71
+
72
+ /**
73
+ * Parse LLM response into ExtractionResult
74
+ */
75
+ export function parseExtractionResponse(llmResponse: string): ExtractionResult {
76
+ // Clean up response - remove markdown code blocks if present
77
+ let cleaned = llmResponse.trim();
78
+
79
+ // Remove markdown JSON code blocks
80
+ if (cleaned.startsWith('```json')) {
81
+ cleaned = cleaned.replace(/^```json\s*/, '').replace(/\s*```$/, '');
82
+ } else if (cleaned.startsWith('```')) {
83
+ cleaned = cleaned.replace(/^```\s*/, '').replace(/\s*```$/, '');
84
+ }
85
+
86
+ try {
87
+ const parsed = JSON.parse(cleaned);
88
+
89
+ // Validate and normalize the structure
90
+ const result: ExtractionResult = {
91
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
92
+ facts: Array.isArray(parsed.facts) ? parsed.facts : [],
93
+ relationships: Array.isArray(parsed.relationships) ? parsed.relationships : [],
94
+ commitments: Array.isArray(parsed.commitments) ? parsed.commitments : [],
95
+ };
96
+
97
+ return result;
98
+ } catch (_error) {
99
+
100
+ // Return empty result on parse failure
101
+ return {
102
+ entities: [],
103
+ facts: [],
104
+ relationships: [],
105
+ commitments: [],
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Parse ISO date string to timestamp, return null if invalid
112
+ */
113
+ function parseDate(dateStr?: string): number | null {
114
+ if (!dateStr) return null;
115
+
116
+ try {
117
+ const timestamp = new Date(dateStr).getTime();
118
+ return isNaN(timestamp) ? null : timestamp;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Validate entity type
126
+ */
127
+ function isValidEntityType(type: string): type is 'person' | 'project' | 'tool' | 'place' | 'concept' | 'event' {
128
+ return ['person', 'project', 'tool', 'place', 'concept', 'event'].includes(type);
129
+ }
130
+
131
+ /**
132
+ * High-level: extract and store in vault
133
+ */
134
+ export async function extractAndStore(
135
+ userMessage: string,
136
+ assistantResponse: string,
137
+ provider?: LLMProvider
138
+ ): Promise<ExtractionResult> {
139
+ // If no provider, return empty result
140
+ if (!provider) {
141
+ return {
142
+ entities: [],
143
+ facts: [],
144
+ relationships: [],
145
+ commitments: [],
146
+ };
147
+ }
148
+
149
+ try {
150
+ // Build prompt
151
+ const prompt = buildExtractionPrompt(userMessage, assistantResponse);
152
+
153
+ // Call LLM
154
+ const response = await provider.chat([
155
+ { role: 'user', content: prompt },
156
+ ], {
157
+ temperature: 0.1, // Low temperature for consistent extraction
158
+ max_tokens: 2000,
159
+ });
160
+
161
+ // Parse response
162
+ const extraction = parseExtractionResponse(response.content);
163
+
164
+ // Store entities
165
+ const entityMap = new Map<string, string>(); // name -> id
166
+
167
+ for (const entityData of extraction.entities) {
168
+ const { name, type, properties } = entityData;
169
+
170
+ // Validate type
171
+ if (!isValidEntityType(type)) {
172
+ console.warn(`Invalid entity type: ${type}, skipping entity ${name}`);
173
+ continue;
174
+ }
175
+
176
+ // Check if entity already exists
177
+ const existing = findEntities({ name, type });
178
+
179
+ if (existing.length > 0) {
180
+ // Use existing entity ID
181
+ entityMap.set(name, existing[0]!.id);
182
+ } else {
183
+ // Create new entity
184
+ const entity = createEntity(
185
+ type,
186
+ name,
187
+ properties,
188
+ 'llm_extraction'
189
+ );
190
+ entityMap.set(name, entity.id);
191
+ }
192
+ }
193
+
194
+ // Store facts
195
+ for (const factData of extraction.facts) {
196
+ const { subject, predicate, object, confidence } = factData;
197
+
198
+ // Get subject entity ID
199
+ const subjectId = entityMap.get(subject);
200
+ if (!subjectId) {
201
+ console.warn(`Subject entity not found: ${subject}, skipping fact`);
202
+ continue;
203
+ }
204
+
205
+ createFact(subjectId, predicate, object, {
206
+ confidence: confidence ?? 1.0,
207
+ source: 'llm_extraction',
208
+ });
209
+ }
210
+
211
+ // Store relationships
212
+ for (const relData of extraction.relationships) {
213
+ const { from, to, type } = relData;
214
+
215
+ // Get entity IDs
216
+ const fromId = entityMap.get(from);
217
+ const toId = entityMap.get(to);
218
+
219
+ if (!fromId || !toId) {
220
+ console.warn(`Relationship entities not found: ${from} -> ${to}, skipping`);
221
+ continue;
222
+ }
223
+
224
+ createRelationship(fromId, toId, type);
225
+ }
226
+
227
+ // Store commitments
228
+ for (const commitmentData of extraction.commitments) {
229
+ const { what, when_due, priority } = commitmentData;
230
+
231
+ const whenDueTimestamp = parseDate(when_due);
232
+
233
+ createCommitment(what, {
234
+ when_due: whenDueTimestamp ?? undefined,
235
+ priority: (priority as any) ?? 'normal',
236
+ context: `Extracted from conversation`,
237
+ created_from: 'llm_extraction',
238
+ });
239
+ }
240
+
241
+ return extraction;
242
+ } catch (error) {
243
+ console.error('Failed to extract and store:', error);
244
+
245
+ // Return empty result on error
246
+ return {
247
+ entities: [],
248
+ facts: [],
249
+ relationships: [],
250
+ commitments: [],
251
+ };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Extract a completed goal as a vault entity with performance facts.
257
+ * Called when a goal is completed/failed/killed to build historical data
258
+ * for future estimation.
259
+ */
260
+ export function extractGoalCompletion(goal: {
261
+ id: string;
262
+ title: string;
263
+ level: string;
264
+ score: number;
265
+ status: string;
266
+ estimated_hours: number | null;
267
+ actual_hours: number;
268
+ created_at: number;
269
+ completed_at: number | null;
270
+ tags: string[];
271
+ }): void {
272
+ try {
273
+ // Create or find entity for this goal
274
+ const existing = findEntities({ name: goal.title, type: 'concept' });
275
+ let entityId: string;
276
+
277
+ if (existing.length > 0) {
278
+ entityId = existing[0]!.id;
279
+ } else {
280
+ const entity = createEntity('concept', goal.title, {
281
+ goal_id: goal.id,
282
+ goal_level: goal.level,
283
+ }, 'goal_completion');
284
+ entityId = entity.id;
285
+ }
286
+
287
+ // Store performance facts
288
+ createFact(entityId, 'goal_final_score', goal.score.toFixed(2), {
289
+ confidence: 1.0,
290
+ source: 'goal_completion',
291
+ });
292
+
293
+ createFact(entityId, 'goal_outcome', goal.status, {
294
+ confidence: 1.0,
295
+ source: 'goal_completion',
296
+ });
297
+
298
+ createFact(entityId, 'goal_level', goal.level, {
299
+ confidence: 1.0,
300
+ source: 'goal_completion',
301
+ });
302
+
303
+ if (goal.estimated_hours !== null) {
304
+ createFact(entityId, 'estimated_hours', goal.estimated_hours.toString(), {
305
+ confidence: 1.0,
306
+ source: 'goal_completion',
307
+ });
308
+ }
309
+
310
+ if (goal.actual_hours > 0) {
311
+ createFact(entityId, 'actual_hours', goal.actual_hours.toFixed(1), {
312
+ confidence: 1.0,
313
+ source: 'goal_completion',
314
+ });
315
+ }
316
+
317
+ // Time to complete
318
+ if (goal.completed_at) {
319
+ const durationDays = Math.ceil((goal.completed_at - goal.created_at) / 86400000);
320
+ createFact(entityId, 'days_to_complete', durationDays.toString(), {
321
+ confidence: 1.0,
322
+ source: 'goal_completion',
323
+ });
324
+ }
325
+
326
+ // Estimation accuracy
327
+ if (goal.estimated_hours !== null && goal.actual_hours > 0) {
328
+ const accuracy = (goal.estimated_hours / goal.actual_hours).toFixed(2);
329
+ createFact(entityId, 'estimation_accuracy', accuracy, {
330
+ confidence: 1.0,
331
+ source: 'goal_completion',
332
+ });
333
+ }
334
+
335
+ // Tags
336
+ if (goal.tags.length > 0) {
337
+ createFact(entityId, 'goal_tags', goal.tags.join(', '), {
338
+ confidence: 1.0,
339
+ source: 'goal_completion',
340
+ });
341
+ }
342
+ } catch (err) {
343
+ console.error('[Extractor] Failed to extract goal completion:', err);
344
+ }
345
+ }