@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,87 @@
1
+ /**
2
+ * Vault Module - Data Access Layer for J.A.R.V.I.S. Knowledge Graph
3
+ *
4
+ * This module provides CRUD operations for all core data types in the knowledge graph:
5
+ * - Entities: People, projects, tools, places, concepts, events
6
+ * - Facts: Atomic pieces of knowledge with confidence scores
7
+ * - Relationships: Typed edges between entities
8
+ * - Commitments: Promises and tasks the AI needs to fulfill
9
+ * - Observations: Raw events from the observation layer
10
+ * - Vectors: Embeddings for semantic search
11
+ *
12
+ * All modules use Bun's SQLite API and handle JSON serialization automatically.
13
+ */
14
+
15
+ // Re-export schema utilities
16
+ export { initDatabase, getDb, closeDb, generateId } from './schema.ts';
17
+
18
+ // Re-export entities module
19
+ export type { Entity, EntityType } from './entities.ts';
20
+ export {
21
+ createEntity,
22
+ getEntity,
23
+ findEntities,
24
+ updateEntity,
25
+ deleteEntity,
26
+ searchEntitiesByName,
27
+ } from './entities.ts';
28
+
29
+ // Re-export facts module
30
+ export type { Fact } from './facts.ts';
31
+ export {
32
+ createFact,
33
+ getFact,
34
+ findFacts,
35
+ queryFact,
36
+ updateFact,
37
+ deleteFact,
38
+ verifyFact,
39
+ } from './facts.ts';
40
+
41
+ // Re-export relationships module
42
+ export type { Relationship } from './relationships.ts';
43
+ export {
44
+ createRelationship,
45
+ getRelationship,
46
+ findRelationships,
47
+ getEntityRelationships,
48
+ deleteRelationship,
49
+ } from './relationships.ts';
50
+
51
+ // Re-export commitments module
52
+ export type { Commitment, CommitmentPriority, CommitmentStatus, RetryPolicy } from './commitments.ts';
53
+ export {
54
+ createCommitment,
55
+ getCommitment,
56
+ findCommitments,
57
+ getUpcoming,
58
+ completeCommitment,
59
+ failCommitment,
60
+ escalateCommitment,
61
+ getDueCommitments,
62
+ } from './commitments.ts';
63
+
64
+ // Re-export observations module
65
+ export type { Observation, ObservationType } from './observations.ts';
66
+ export {
67
+ createObservation,
68
+ getUnprocessed,
69
+ markProcessed,
70
+ getRecentObservations,
71
+ } from './observations.ts';
72
+
73
+ // Re-export vectors module
74
+ export type { VectorRecord } from './vectors.ts';
75
+ export {
76
+ storeVector,
77
+ findSimilar,
78
+ deleteVectors,
79
+ } from './vectors.ts';
80
+
81
+ // Re-export extractor module
82
+ export type { ExtractionResult } from './extractor.ts';
83
+ export {
84
+ buildExtractionPrompt,
85
+ parseExtractionResponse,
86
+ extractAndStore,
87
+ } from './extractor.ts';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Encrypted secrets store for JARVIS.
3
+ *
4
+ * Stores secrets in an AES-256-GCM encrypted file (~/.jarvis/.secrets.enc)
5
+ * with a random key stored in ~/.jarvis/.secrets.key (chmod 600).
6
+ *
7
+ * This avoids depending on OS keychain daemons (which are unreliable on WSL2).
8
+ */
9
+
10
+ import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+
15
+ const JARVIS_DIR = join(homedir(), '.jarvis');
16
+ const KEY_PATH = join(JARVIS_DIR, '.secrets.key');
17
+ const SECRETS_PATH = join(JARVIS_DIR, '.secrets.enc');
18
+ const ALGORITHM = 'aes-256-gcm';
19
+ const IV_LENGTH = 12;
20
+ const TAG_LENGTH = 16;
21
+
22
+ function ensureDir(): void {
23
+ if (!existsSync(JARVIS_DIR)) {
24
+ mkdirSync(JARVIS_DIR, { recursive: true });
25
+ }
26
+ }
27
+
28
+ function getOrCreateKey(): Buffer {
29
+ ensureDir();
30
+ if (existsSync(KEY_PATH)) {
31
+ const hex = readFileSync(KEY_PATH, 'utf-8').trim();
32
+ return Buffer.from(hex, 'hex');
33
+ }
34
+ const key = randomBytes(32);
35
+ writeFileSync(KEY_PATH, key.toString('hex'), { mode: 0o600 });
36
+ try { chmodSync(KEY_PATH, 0o600); } catch {}
37
+ return key;
38
+ }
39
+
40
+ function encrypt(key: Buffer, plaintext: string): Buffer {
41
+ const iv = randomBytes(IV_LENGTH);
42
+ const cipher = createCipheriv(ALGORITHM, key, iv);
43
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
44
+ const tag = cipher.getAuthTag();
45
+ return Buffer.concat([iv, tag, encrypted]);
46
+ }
47
+
48
+ function decrypt(key: Buffer, data: Buffer): string {
49
+ const iv = data.subarray(0, IV_LENGTH);
50
+ const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
51
+ const encrypted = data.subarray(IV_LENGTH + TAG_LENGTH);
52
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
53
+ decipher.setAuthTag(tag);
54
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
55
+ }
56
+
57
+ function loadSecrets(): Record<string, string> {
58
+ if (!existsSync(SECRETS_PATH)) return {};
59
+ try {
60
+ const key = getOrCreateKey();
61
+ const raw = readFileSync(SECRETS_PATH);
62
+ const json = decrypt(key, raw);
63
+ return JSON.parse(json);
64
+ } catch (err) {
65
+ console.warn('[Keychain] Failed to decrypt secrets file, starting fresh:', err);
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function saveSecrets(secrets: Record<string, string>): void {
71
+ ensureDir();
72
+ const key = getOrCreateKey();
73
+ const json = JSON.stringify(secrets);
74
+ const encrypted = encrypt(key, json);
75
+ writeFileSync(SECRETS_PATH, encrypted, { mode: 0o600 });
76
+ try { chmodSync(SECRETS_PATH, 0o600); } catch {}
77
+ }
78
+
79
+ export function getSecret(name: string): string | null {
80
+ const secrets = loadSecrets();
81
+ return secrets[name] ?? null;
82
+ }
83
+
84
+ export function setSecret(name: string, value: string): void {
85
+ const secrets = loadSecrets();
86
+ secrets[name] = value;
87
+ saveSecrets(secrets);
88
+ }
89
+
90
+ export function deleteSecret(name: string): void {
91
+ const secrets = loadSecrets();
92
+ delete secrets[name];
93
+ saveSecrets(secrets);
94
+ }
95
+
96
+ export function hasSecret(name: string): boolean {
97
+ const secrets = loadSecrets();
98
+ return name in secrets;
99
+ }
@@ -0,0 +1,115 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export type ObservationType =
4
+ | 'file_change'
5
+ | 'notification'
6
+ | 'clipboard'
7
+ | 'app_activity'
8
+ | 'calendar'
9
+ | 'email'
10
+ | 'browser'
11
+ | 'process'
12
+ | 'screen_capture';
13
+
14
+ export type Observation = {
15
+ id: string;
16
+ type: ObservationType;
17
+ data: Record<string, unknown>;
18
+ processed: boolean;
19
+ created_at: number;
20
+ };
21
+
22
+ type ObservationRow = {
23
+ id: string;
24
+ type: ObservationType;
25
+ data: string;
26
+ processed: number;
27
+ created_at: number;
28
+ };
29
+
30
+ /**
31
+ * Parse observation row from database, deserializing JSON fields
32
+ */
33
+ function parseObservation(row: ObservationRow): Observation {
34
+ return {
35
+ id: row.id,
36
+ type: row.type,
37
+ data: JSON.parse(row.data),
38
+ processed: row.processed === 1,
39
+ created_at: row.created_at,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Create a new observation
45
+ */
46
+ export function createObservation(
47
+ type: ObservationType,
48
+ data: Record<string, unknown>
49
+ ): Observation {
50
+ const db = getDb();
51
+ const id = generateId();
52
+ const now = Date.now();
53
+
54
+ const stmt = db.prepare(
55
+ 'INSERT INTO observations (id, type, data, processed, created_at) VALUES (?, ?, ?, ?, ?)'
56
+ );
57
+
58
+ stmt.run(id, type, JSON.stringify(data), 0, now);
59
+
60
+ return {
61
+ id,
62
+ type,
63
+ data,
64
+ processed: false,
65
+ created_at: now,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Get unprocessed observations
71
+ */
72
+ export function getUnprocessed(limit: number = 100): Observation[] {
73
+ const db = getDb();
74
+ const stmt = db.prepare(
75
+ 'SELECT * FROM observations WHERE processed = 0 ORDER BY created_at ASC LIMIT ?'
76
+ );
77
+ const rows = stmt.all(limit) as ObservationRow[];
78
+
79
+ return rows.map(parseObservation);
80
+ }
81
+
82
+ /**
83
+ * Mark an observation as processed
84
+ */
85
+ export function markProcessed(id: string): void {
86
+ const db = getDb();
87
+ const stmt = db.prepare('UPDATE observations SET processed = 1 WHERE id = ?');
88
+ stmt.run(id);
89
+ }
90
+
91
+ /**
92
+ * Get recent observations, optionally filtered by type
93
+ */
94
+ export function getRecentObservations(
95
+ type?: ObservationType,
96
+ limit: number = 50
97
+ ): Observation[] {
98
+ const db = getDb();
99
+
100
+ let query = 'SELECT * FROM observations';
101
+ const params: unknown[] = [];
102
+
103
+ if (type) {
104
+ query += ' WHERE type = ?';
105
+ params.push(type);
106
+ }
107
+
108
+ query += ' ORDER BY created_at DESC LIMIT ?';
109
+ params.push(limit);
110
+
111
+ const stmt = db.prepare(query);
112
+ const rows = stmt.all(...params as any[]) as ObservationRow[];
113
+
114
+ return rows.map(parseObservation);
115
+ }
@@ -0,0 +1,178 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+ import type { Entity } from './entities.ts';
3
+
4
+ export type Relationship = {
5
+ id: string;
6
+ from_id: string;
7
+ to_id: string;
8
+ type: string;
9
+ properties: Record<string, unknown> | null;
10
+ created_at: number;
11
+ };
12
+
13
+ type RelationshipRow = {
14
+ id: string;
15
+ from_id: string;
16
+ to_id: string;
17
+ type: string;
18
+ properties: string | null;
19
+ created_at: number;
20
+ };
21
+
22
+ /**
23
+ * Parse relationship row from database, deserializing JSON fields
24
+ */
25
+ function parseRelationship(row: RelationshipRow): Relationship {
26
+ return {
27
+ ...row,
28
+ properties: row.properties ? JSON.parse(row.properties) : null,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Create a new relationship between entities
34
+ */
35
+ export function createRelationship(
36
+ from_id: string,
37
+ to_id: string,
38
+ type: string,
39
+ properties?: Record<string, unknown>
40
+ ): Relationship {
41
+ const db = getDb();
42
+ const id = generateId();
43
+ const now = Date.now();
44
+
45
+ const stmt = db.prepare(
46
+ 'INSERT INTO relationships (id, from_id, to_id, type, properties, created_at) VALUES (?, ?, ?, ?, ?, ?)'
47
+ );
48
+
49
+ stmt.run(id, from_id, to_id, type, properties ? JSON.stringify(properties) : null, now);
50
+
51
+ return {
52
+ id,
53
+ from_id,
54
+ to_id,
55
+ type,
56
+ properties: properties ?? null,
57
+ created_at: now,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Get a relationship by ID
63
+ */
64
+ export function getRelationship(id: string): Relationship | null {
65
+ const db = getDb();
66
+ const stmt = db.prepare('SELECT * FROM relationships WHERE id = ?');
67
+ const row = stmt.get(id) as RelationshipRow | null;
68
+
69
+ if (!row) return null;
70
+
71
+ return parseRelationship(row);
72
+ }
73
+
74
+ /**
75
+ * Find relationships matching query criteria
76
+ */
77
+ export function findRelationships(query: {
78
+ from_id?: string;
79
+ to_id?: string;
80
+ type?: string;
81
+ }): Relationship[] {
82
+ const db = getDb();
83
+ const conditions: string[] = [];
84
+ const params: unknown[] = [];
85
+
86
+ if (query.from_id) {
87
+ conditions.push('from_id = ?');
88
+ params.push(query.from_id);
89
+ }
90
+
91
+ if (query.to_id) {
92
+ conditions.push('to_id = ?');
93
+ params.push(query.to_id);
94
+ }
95
+
96
+ if (query.type) {
97
+ conditions.push('type = ?');
98
+ params.push(query.type);
99
+ }
100
+
101
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
102
+ const stmt = db.prepare(`SELECT * FROM relationships ${where} ORDER BY created_at DESC`);
103
+ const rows = stmt.all(...params as any[]) as RelationshipRow[];
104
+
105
+ return rows.map(parseRelationship);
106
+ }
107
+
108
+ /**
109
+ * Get all relationships for an entity (both incoming and outgoing) with full entity details
110
+ */
111
+ export function getEntityRelationships(
112
+ entityId: string
113
+ ): Array<Relationship & { from_entity: Entity; to_entity: Entity }> {
114
+ const db = getDb();
115
+
116
+ const stmt = db.prepare(`
117
+ SELECT
118
+ r.*,
119
+ e1.id as from_entity_id,
120
+ e1.type as from_entity_type,
121
+ e1.name as from_entity_name,
122
+ e1.properties as from_entity_properties,
123
+ e1.created_at as from_entity_created_at,
124
+ e1.updated_at as from_entity_updated_at,
125
+ e1.source as from_entity_source,
126
+ e2.id as to_entity_id,
127
+ e2.type as to_entity_type,
128
+ e2.name as to_entity_name,
129
+ e2.properties as to_entity_properties,
130
+ e2.created_at as to_entity_created_at,
131
+ e2.updated_at as to_entity_updated_at,
132
+ e2.source as to_entity_source
133
+ FROM relationships r
134
+ JOIN entities e1 ON r.from_id = e1.id
135
+ JOIN entities e2 ON r.to_id = e2.id
136
+ WHERE r.from_id = ? OR r.to_id = ?
137
+ ORDER BY r.created_at DESC
138
+ `);
139
+
140
+ const rows = stmt.all(entityId, entityId) as any[];
141
+
142
+ return rows.map((row) => ({
143
+ id: row.id,
144
+ from_id: row.from_id,
145
+ to_id: row.to_id,
146
+ type: row.type,
147
+ properties: row.properties ? JSON.parse(row.properties) : null,
148
+ created_at: row.created_at,
149
+ from_entity: {
150
+ id: row.from_entity_id,
151
+ type: row.from_entity_type,
152
+ name: row.from_entity_name,
153
+ properties: row.from_entity_properties ? JSON.parse(row.from_entity_properties) : null,
154
+ created_at: row.from_entity_created_at,
155
+ updated_at: row.from_entity_updated_at,
156
+ source: row.from_entity_source,
157
+ },
158
+ to_entity: {
159
+ id: row.to_entity_id,
160
+ type: row.to_entity_type,
161
+ name: row.to_entity_name,
162
+ properties: row.to_entity_properties ? JSON.parse(row.to_entity_properties) : null,
163
+ created_at: row.to_entity_created_at,
164
+ updated_at: row.to_entity_updated_at,
165
+ source: row.to_entity_source,
166
+ },
167
+ }));
168
+ }
169
+
170
+ /**
171
+ * Delete a relationship
172
+ */
173
+ export function deleteRelationship(id: string): boolean {
174
+ const db = getDb();
175
+ const stmt = db.prepare('DELETE FROM relationships WHERE id = ?');
176
+ const result = stmt.run(id);
177
+ return result.changes > 0;
178
+ }
@@ -0,0 +1,126 @@
1
+ import { test, expect, beforeEach } from 'bun:test';
2
+ import { initDatabase } from './schema.ts';
3
+ import { createEntity } from './entities.ts';
4
+ import { createFact } from './facts.ts';
5
+ import { createRelationship } from './relationships.ts';
6
+ import {
7
+ extractSearchTerms,
8
+ retrieveForMessage,
9
+ formatKnowledgeContext,
10
+ getKnowledgeForMessage,
11
+ } from './retrieval.ts';
12
+
13
+ beforeEach(() => {
14
+ initDatabase(':memory:');
15
+ });
16
+
17
+ // --- extractSearchTerms ---
18
+
19
+ test('extractSearchTerms filters stopwords', () => {
20
+ const terms = extractSearchTerms('Where does John work at Google?');
21
+ expect(terms).toContain('john');
22
+ expect(terms).toContain('google');
23
+ expect(terms).not.toContain('where');
24
+ expect(terms).not.toContain('does');
25
+ expect(terms).not.toContain('at');
26
+ });
27
+
28
+ test('extractSearchTerms deduplicates', () => {
29
+ const terms = extractSearchTerms('John and John went to Google Google');
30
+ const johnCount = terms.filter(t => t === 'john').length;
31
+ expect(johnCount).toBe(1);
32
+ });
33
+
34
+ test('extractSearchTerms handles empty input', () => {
35
+ expect(extractSearchTerms('')).toEqual([]);
36
+ expect(extractSearchTerms('the is a')).toEqual([]);
37
+ });
38
+
39
+ // --- retrieveForMessage ---
40
+
41
+ test('retrieveForMessage finds entities by name', () => {
42
+ createEntity('person', 'John', { role: 'engineer' });
43
+ createEntity('person', 'Anna');
44
+
45
+ const profiles = retrieveForMessage('Tell me about John');
46
+ expect(profiles.length).toBe(1);
47
+ expect(profiles[0]!.entity.name).toBe('John');
48
+ });
49
+
50
+ test('retrieveForMessage finds entities via fact objects', () => {
51
+ const john = createEntity('person', 'John');
52
+ createFact(john.id, 'works_at', 'Google');
53
+
54
+ // Search for "Google" — should find John because he has a fact with object "Google"
55
+ const profiles = retrieveForMessage('What do you know about Google?');
56
+ expect(profiles.length).toBeGreaterThanOrEqual(1);
57
+ const names = profiles.map(p => p.entity.name);
58
+ expect(names).toContain('John');
59
+ });
60
+
61
+ test('retrieveForMessage includes facts for matched entities', () => {
62
+ const john = createEntity('person', 'John');
63
+ createFact(john.id, 'works_at', 'Google');
64
+ createFact(john.id, 'birthday', 'March 15');
65
+
66
+ const profiles = retrieveForMessage('Tell me about John');
67
+ expect(profiles.length).toBe(1);
68
+ expect(profiles[0]!.facts.length).toBe(2);
69
+ });
70
+
71
+ test('retrieveForMessage includes relationships', () => {
72
+ const john = createEntity('person', 'John');
73
+ const google = createEntity('concept', 'Google');
74
+ createRelationship(john.id, google.id, 'works_at');
75
+
76
+ const profiles = retrieveForMessage('What about John?');
77
+ expect(profiles.length).toBeGreaterThanOrEqual(1);
78
+
79
+ const johnProfile = profiles.find(p => p.entity.name === 'John');
80
+ expect(johnProfile).toBeDefined();
81
+ expect(johnProfile!.relationships.length).toBeGreaterThanOrEqual(1);
82
+ });
83
+
84
+ test('retrieveForMessage returns empty for irrelevant query', () => {
85
+ createEntity('person', 'John');
86
+ const profiles = retrieveForMessage('the is a');
87
+ expect(profiles.length).toBe(0);
88
+ });
89
+
90
+ // --- formatKnowledgeContext ---
91
+
92
+ test('formatKnowledgeContext formats entity with facts', () => {
93
+ const john = createEntity('person', 'John');
94
+ createFact(john.id, 'works_at', 'Google');
95
+
96
+ const profiles = retrieveForMessage('John');
97
+ const context = formatKnowledgeContext(profiles);
98
+
99
+ expect(context).toContain('**John** (person)');
100
+ expect(context).toContain('works_at: Google');
101
+ });
102
+
103
+ test('formatKnowledgeContext returns empty for no profiles', () => {
104
+ expect(formatKnowledgeContext([])).toBe('');
105
+ });
106
+
107
+ // --- getKnowledgeForMessage (integration) ---
108
+
109
+ test('getKnowledgeForMessage end-to-end', () => {
110
+ const john = createEntity('person', 'John');
111
+ createFact(john.id, 'works_at', 'Google');
112
+ createFact(john.id, 'location', 'San Francisco');
113
+
114
+ const anna = createEntity('person', 'Anna');
115
+ createFact(anna.id, 'sister_of', 'John');
116
+
117
+ const context = getKnowledgeForMessage('Where does John live?');
118
+ expect(context).toContain('John');
119
+ expect(context).toContain('works_at: Google');
120
+ expect(context).toContain('location: San Francisco');
121
+ });
122
+
123
+ test('getKnowledgeForMessage handles no matches gracefully', () => {
124
+ const context = getKnowledgeForMessage('Tell me about quantum physics');
125
+ expect(context).toBe('');
126
+ });