@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,260 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export const CONTENT_STAGES = [
4
+ 'idea', 'research', 'outline', 'draft', 'assets', 'review', 'scheduled', 'published',
5
+ ] as const;
6
+
7
+ export type ContentStage = typeof CONTENT_STAGES[number];
8
+
9
+ export const CONTENT_TYPES = [
10
+ 'youtube', 'blog', 'twitter', 'instagram', 'tiktok', 'linkedin',
11
+ 'podcast', 'newsletter', 'short_form', 'other',
12
+ ] as const;
13
+
14
+ export type ContentType = typeof CONTENT_TYPES[number];
15
+
16
+ export type ContentItem = {
17
+ id: string;
18
+ title: string;
19
+ body: string;
20
+ content_type: ContentType;
21
+ stage: ContentStage;
22
+ tags: string[];
23
+ scheduled_at: number | null;
24
+ published_at: number | null;
25
+ published_url: string | null;
26
+ created_by: string;
27
+ sort_order: number;
28
+ created_at: number;
29
+ updated_at: number;
30
+ };
31
+
32
+ export type ContentStageNote = {
33
+ id: string;
34
+ content_id: string;
35
+ stage: ContentStage;
36
+ note: string;
37
+ author: string;
38
+ created_at: number;
39
+ };
40
+
41
+ export type ContentAttachment = {
42
+ id: string;
43
+ content_id: string;
44
+ filename: string;
45
+ disk_path: string;
46
+ mime_type: string;
47
+ size_bytes: number;
48
+ label: string | null;
49
+ created_at: number;
50
+ };
51
+
52
+ type ContentRow = Omit<ContentItem, 'tags'> & { tags: string | null };
53
+
54
+ function parseRow(row: ContentRow): ContentItem {
55
+ return {
56
+ ...row,
57
+ tags: row.tags ? JSON.parse(row.tags) : [],
58
+ };
59
+ }
60
+
61
+ // --- Content Items CRUD ---
62
+
63
+ export function createContent(title: string, opts?: {
64
+ body?: string;
65
+ content_type?: ContentType;
66
+ stage?: ContentStage;
67
+ tags?: string[];
68
+ created_by?: string;
69
+ }): ContentItem {
70
+ const db = getDb();
71
+ const id = generateId();
72
+ const now = Date.now();
73
+
74
+ db.prepare(
75
+ `INSERT INTO content_items (id, title, body, content_type, stage, tags, created_by, created_at, updated_at)
76
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
77
+ ).run(
78
+ id,
79
+ title,
80
+ opts?.body ?? '',
81
+ opts?.content_type ?? 'blog',
82
+ opts?.stage ?? 'idea',
83
+ opts?.tags ? JSON.stringify(opts.tags) : null,
84
+ opts?.created_by ?? 'user',
85
+ now,
86
+ now,
87
+ );
88
+
89
+ return {
90
+ id, title,
91
+ body: opts?.body ?? '',
92
+ content_type: (opts?.content_type ?? 'blog') as ContentType,
93
+ stage: (opts?.stage ?? 'idea') as ContentStage,
94
+ tags: opts?.tags ?? [],
95
+ scheduled_at: null,
96
+ published_at: null,
97
+ published_url: null,
98
+ created_by: opts?.created_by ?? 'user',
99
+ sort_order: 0,
100
+ created_at: now,
101
+ updated_at: now,
102
+ };
103
+ }
104
+
105
+ export function getContent(id: string): ContentItem | null {
106
+ const db = getDb();
107
+ const row = db.prepare('SELECT * FROM content_items WHERE id = ?').get(id) as ContentRow | null;
108
+ return row ? parseRow(row) : null;
109
+ }
110
+
111
+ export function findContent(query: {
112
+ stage?: ContentStage;
113
+ content_type?: ContentType;
114
+ tag?: string;
115
+ }): ContentItem[] {
116
+ const db = getDb();
117
+ const conditions: string[] = [];
118
+ const params: unknown[] = [];
119
+
120
+ if (query.stage) {
121
+ conditions.push('stage = ?');
122
+ params.push(query.stage);
123
+ }
124
+ if (query.content_type) {
125
+ conditions.push('content_type = ?');
126
+ params.push(query.content_type);
127
+ }
128
+ if (query.tag) {
129
+ conditions.push("tags LIKE ?");
130
+ params.push(`%"${query.tag}"%`);
131
+ }
132
+
133
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
134
+ const rows = db.prepare(
135
+ `SELECT * FROM content_items ${where} ORDER BY sort_order ASC, updated_at DESC`
136
+ ).all(...params as any[]) as ContentRow[];
137
+
138
+ return rows.map(parseRow);
139
+ }
140
+
141
+ export function updateContent(id: string, updates: {
142
+ title?: string;
143
+ body?: string;
144
+ content_type?: ContentType;
145
+ stage?: ContentStage;
146
+ tags?: string[];
147
+ scheduled_at?: number | null;
148
+ published_at?: number | null;
149
+ published_url?: string | null;
150
+ sort_order?: number;
151
+ }): ContentItem | null {
152
+ const db = getDb();
153
+ const existing = getContent(id);
154
+ if (!existing) return null;
155
+
156
+ const sets: string[] = ['updated_at = ?'];
157
+ const params: unknown[] = [Date.now()];
158
+
159
+ if (updates.title !== undefined) { sets.push('title = ?'); params.push(updates.title); }
160
+ if (updates.body !== undefined) { sets.push('body = ?'); params.push(updates.body); }
161
+ if (updates.content_type !== undefined) { sets.push('content_type = ?'); params.push(updates.content_type); }
162
+ if (updates.stage !== undefined) { sets.push('stage = ?'); params.push(updates.stage); }
163
+ if (updates.tags !== undefined) { sets.push('tags = ?'); params.push(JSON.stringify(updates.tags)); }
164
+ if (updates.scheduled_at !== undefined) { sets.push('scheduled_at = ?'); params.push(updates.scheduled_at); }
165
+ if (updates.published_at !== undefined) { sets.push('published_at = ?'); params.push(updates.published_at); }
166
+ if (updates.published_url !== undefined) { sets.push('published_url = ?'); params.push(updates.published_url); }
167
+ if (updates.sort_order !== undefined) { sets.push('sort_order = ?'); params.push(updates.sort_order); }
168
+
169
+ params.push(id);
170
+ db.prepare(`UPDATE content_items SET ${sets.join(', ')} WHERE id = ?`).run(...params as any[]);
171
+
172
+ return getContent(id);
173
+ }
174
+
175
+ export function deleteContent(id: string): boolean {
176
+ const db = getDb();
177
+ const result = db.prepare('DELETE FROM content_items WHERE id = ?').run(id);
178
+ return result.changes > 0;
179
+ }
180
+
181
+ export function advanceStage(id: string): ContentItem | null {
182
+ const item = getContent(id);
183
+ if (!item) return null;
184
+ const idx = CONTENT_STAGES.indexOf(item.stage);
185
+ if (idx >= CONTENT_STAGES.length - 1) return null;
186
+ return updateContent(id, { stage: CONTENT_STAGES[idx + 1] });
187
+ }
188
+
189
+ export function regressStage(id: string): ContentItem | null {
190
+ const item = getContent(id);
191
+ if (!item) return null;
192
+ const idx = CONTENT_STAGES.indexOf(item.stage);
193
+ if (idx <= 0) return null;
194
+ return updateContent(id, { stage: CONTENT_STAGES[idx - 1] });
195
+ }
196
+
197
+ // --- Stage Notes ---
198
+
199
+ export function addStageNote(
200
+ contentId: string,
201
+ stage: ContentStage,
202
+ note: string,
203
+ author: string = 'user'
204
+ ): ContentStageNote {
205
+ const db = getDb();
206
+ const id = generateId();
207
+ const now = Date.now();
208
+
209
+ db.prepare(
210
+ 'INSERT INTO content_stage_notes (id, content_id, stage, note, author, created_at) VALUES (?, ?, ?, ?, ?, ?)'
211
+ ).run(id, contentId, stage, note, author, now);
212
+
213
+ return { id, content_id: contentId, stage, note, author, created_at: now };
214
+ }
215
+
216
+ export function getStageNotes(contentId: string, stage?: ContentStage): ContentStageNote[] {
217
+ const db = getDb();
218
+ if (stage) {
219
+ return db.prepare(
220
+ 'SELECT * FROM content_stage_notes WHERE content_id = ? AND stage = ? ORDER BY created_at ASC'
221
+ ).all(contentId, stage) as ContentStageNote[];
222
+ }
223
+ return db.prepare(
224
+ 'SELECT * FROM content_stage_notes WHERE content_id = ? ORDER BY created_at ASC'
225
+ ).all(contentId) as ContentStageNote[];
226
+ }
227
+
228
+ // --- Attachments ---
229
+
230
+ export function addAttachment(
231
+ contentId: string,
232
+ filename: string,
233
+ diskPath: string,
234
+ mimeType: string,
235
+ sizeBytes: number,
236
+ label?: string
237
+ ): ContentAttachment {
238
+ const db = getDb();
239
+ const id = generateId();
240
+ const now = Date.now();
241
+
242
+ db.prepare(
243
+ 'INSERT INTO content_attachments (id, content_id, filename, disk_path, mime_type, size_bytes, label, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
244
+ ).run(id, contentId, filename, diskPath, mimeType, sizeBytes, label ?? null, now);
245
+
246
+ return { id, content_id: contentId, filename, disk_path: diskPath, mime_type: mimeType, size_bytes: sizeBytes, label: label ?? null, created_at: now };
247
+ }
248
+
249
+ export function getAttachments(contentId: string): ContentAttachment[] {
250
+ const db = getDb();
251
+ return db.prepare(
252
+ 'SELECT * FROM content_attachments WHERE content_id = ? ORDER BY created_at ASC'
253
+ ).all(contentId) as ContentAttachment[];
254
+ }
255
+
256
+ export function deleteAttachment(id: string): boolean {
257
+ const db = getDb();
258
+ const result = db.prepare('DELETE FROM content_attachments WHERE id = ?').run(id);
259
+ return result.changes > 0;
260
+ }
@@ -0,0 +1,173 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export type MessageRole = 'user' | 'assistant' | 'system';
4
+
5
+ export type ConversationMessage = {
6
+ id: string;
7
+ conversation_id: string;
8
+ role: MessageRole;
9
+ content: string;
10
+ tool_calls: unknown[] | null;
11
+ created_at: number;
12
+ };
13
+
14
+ export type Conversation = {
15
+ id: string;
16
+ agent_id: string | null;
17
+ channel: string | null;
18
+ started_at: number;
19
+ last_message_at: number;
20
+ message_count: number;
21
+ metadata: Record<string, unknown> | null;
22
+ };
23
+
24
+ type ConversationRow = {
25
+ id: string;
26
+ agent_id: string | null;
27
+ channel: string | null;
28
+ started_at: number;
29
+ last_message_at: number;
30
+ message_count: number;
31
+ metadata: string | null;
32
+ };
33
+
34
+ type MessageRow = {
35
+ id: string;
36
+ conversation_id: string;
37
+ role: MessageRole;
38
+ content: string;
39
+ tool_calls: string | null;
40
+ created_at: number;
41
+ };
42
+
43
+ function parseConversation(row: ConversationRow): Conversation {
44
+ return {
45
+ ...row,
46
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
47
+ };
48
+ }
49
+
50
+ function parseMessage(row: MessageRow): ConversationMessage {
51
+ return {
52
+ ...row,
53
+ tool_calls: row.tool_calls ? JSON.parse(row.tool_calls) : null,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Get or create the active conversation for a channel.
59
+ * Returns the most recent conversation for the channel, or creates a new one.
60
+ */
61
+ export function getOrCreateConversation(channel: string): Conversation {
62
+ const db = getDb();
63
+ const now = Date.now();
64
+
65
+ // Look for a recent conversation on this channel (within last 4 hours)
66
+ const cutoff = now - 4 * 60 * 60 * 1000;
67
+ const existing = db.prepare(
68
+ 'SELECT * FROM conversations WHERE channel = ? AND last_message_at > ? ORDER BY last_message_at DESC LIMIT 1'
69
+ ).get(channel, cutoff) as ConversationRow | null;
70
+
71
+ if (existing) {
72
+ return parseConversation(existing);
73
+ }
74
+
75
+ // Create new conversation
76
+ const id = generateId();
77
+ db.prepare(
78
+ 'INSERT INTO conversations (id, channel, started_at, last_message_at, message_count) VALUES (?, ?, ?, ?, 0)'
79
+ ).run(id, channel, now, now);
80
+
81
+ return {
82
+ id,
83
+ agent_id: null,
84
+ channel,
85
+ started_at: now,
86
+ last_message_at: now,
87
+ message_count: 0,
88
+ metadata: null,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Add a message to a conversation.
94
+ * Updates conversation metadata (last_message_at, message_count).
95
+ */
96
+ export function addMessage(
97
+ conversationId: string,
98
+ msg: { role: MessageRole; content: string; tool_calls?: unknown[] }
99
+ ): ConversationMessage {
100
+ const db = getDb();
101
+ const id = generateId();
102
+ const now = Date.now();
103
+
104
+ db.prepare(
105
+ 'INSERT INTO conversation_messages (id, conversation_id, role, content, tool_calls, created_at) VALUES (?, ?, ?, ?, ?, ?)'
106
+ ).run(
107
+ id,
108
+ conversationId,
109
+ msg.role,
110
+ msg.content,
111
+ msg.tool_calls ? JSON.stringify(msg.tool_calls) : null,
112
+ now
113
+ );
114
+
115
+ // Update conversation
116
+ db.prepare(
117
+ 'UPDATE conversations SET last_message_at = ?, message_count = message_count + 1 WHERE id = ?'
118
+ ).run(now, conversationId);
119
+
120
+ return {
121
+ id,
122
+ conversation_id: conversationId,
123
+ role: msg.role,
124
+ content: msg.content,
125
+ tool_calls: msg.tool_calls ?? null,
126
+ created_at: now,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Get messages for a conversation, ordered by time ascending.
132
+ */
133
+ export function getMessages(
134
+ conversationId: string,
135
+ opts?: { limit?: number; before?: number }
136
+ ): ConversationMessage[] {
137
+ const db = getDb();
138
+ const limit = opts?.limit ?? 100;
139
+
140
+ if (opts?.before) {
141
+ const rows = db.prepare(
142
+ 'SELECT * FROM conversation_messages WHERE conversation_id = ? AND created_at < ? ORDER BY created_at ASC LIMIT ?'
143
+ ).all(conversationId, opts.before, limit) as MessageRow[];
144
+ return rows.map(parseMessage);
145
+ }
146
+
147
+ // Get the last N messages, ordered ascending
148
+ const rows = db.prepare(
149
+ 'SELECT * FROM (SELECT * FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT ?) ORDER BY created_at ASC'
150
+ ).all(conversationId, limit) as MessageRow[];
151
+
152
+ return rows.map(parseMessage);
153
+ }
154
+
155
+ /**
156
+ * Get the most recent conversation for a channel, with its messages.
157
+ */
158
+ export function getRecentConversation(channel: string): {
159
+ conversation: Conversation;
160
+ messages: ConversationMessage[];
161
+ } | null {
162
+ const db = getDb();
163
+ const row = db.prepare(
164
+ 'SELECT * FROM conversations WHERE channel = ? ORDER BY last_message_at DESC LIMIT 1'
165
+ ).get(channel) as ConversationRow | null;
166
+
167
+ if (!row) return null;
168
+
169
+ const conversation = parseConversation(row);
170
+ const messages = getMessages(conversation.id);
171
+
172
+ return { conversation, messages };
173
+ }
@@ -0,0 +1,180 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export type EntityType = 'person' | 'project' | 'tool' | 'place' | 'concept' | 'event';
4
+
5
+ export type Entity = {
6
+ id: string;
7
+ type: EntityType;
8
+ name: string;
9
+ properties: Record<string, unknown> | null;
10
+ created_at: number;
11
+ updated_at: number;
12
+ source: string | null;
13
+ };
14
+
15
+ type EntityRow = {
16
+ id: string;
17
+ type: EntityType;
18
+ name: string;
19
+ properties: string | null;
20
+ created_at: number;
21
+ updated_at: number;
22
+ source: string | null;
23
+ };
24
+
25
+ /**
26
+ * Parse entity row from database, deserializing JSON fields
27
+ */
28
+ function parseEntity(row: EntityRow): Entity {
29
+ return {
30
+ ...row,
31
+ properties: row.properties ? JSON.parse(row.properties) : null,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Create a new entity in the knowledge graph
37
+ */
38
+ export function createEntity(
39
+ type: EntityType,
40
+ name: string,
41
+ properties?: Record<string, unknown>,
42
+ source?: string
43
+ ): Entity {
44
+ const db = getDb();
45
+ const id = generateId();
46
+ const now = Date.now();
47
+
48
+ const stmt = db.prepare(
49
+ 'INSERT INTO entities (id, type, name, properties, created_at, updated_at, source) VALUES (?, ?, ?, ?, ?, ?, ?)'
50
+ );
51
+
52
+ stmt.run(
53
+ id,
54
+ type,
55
+ name,
56
+ properties ? JSON.stringify(properties) : null,
57
+ now,
58
+ now,
59
+ source ?? null
60
+ );
61
+
62
+ return {
63
+ id,
64
+ type,
65
+ name,
66
+ properties: properties ?? null,
67
+ created_at: now,
68
+ updated_at: now,
69
+ source: source ?? null,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Get an entity by ID
75
+ */
76
+ export function getEntity(id: string): Entity | null {
77
+ const db = getDb();
78
+ const stmt = db.prepare('SELECT * FROM entities WHERE id = ?');
79
+ const row = stmt.get(id) as EntityRow | null;
80
+
81
+ if (!row) return null;
82
+
83
+ return parseEntity(row);
84
+ }
85
+
86
+ /**
87
+ * Find entities matching query criteria
88
+ */
89
+ export function findEntities(query: {
90
+ type?: EntityType;
91
+ name?: string;
92
+ nameContains?: string;
93
+ }): Entity[] {
94
+ const db = getDb();
95
+ const conditions: string[] = [];
96
+ const params: unknown[] = [];
97
+
98
+ if (query.type) {
99
+ conditions.push('type = ?');
100
+ params.push(query.type);
101
+ }
102
+
103
+ if (query.name) {
104
+ conditions.push('name = ?');
105
+ params.push(query.name);
106
+ }
107
+
108
+ if (query.nameContains) {
109
+ conditions.push('name LIKE ?');
110
+ params.push(`%${query.nameContains}%`);
111
+ }
112
+
113
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
114
+ const stmt = db.prepare(`SELECT * FROM entities ${where} ORDER BY updated_at DESC`);
115
+ const rows = stmt.all(...params as any[]) as EntityRow[];
116
+
117
+ return rows.map(parseEntity);
118
+ }
119
+
120
+ /**
121
+ * Update an entity's properties
122
+ */
123
+ export function updateEntity(
124
+ id: string,
125
+ updates: Partial<Pick<Entity, 'name' | 'properties' | 'type'>>
126
+ ): Entity | null {
127
+ const db = getDb();
128
+ const entity = getEntity(id);
129
+ if (!entity) return null;
130
+
131
+ const fields: string[] = [];
132
+ const params: unknown[] = [];
133
+
134
+ if (updates.name !== undefined) {
135
+ fields.push('name = ?');
136
+ params.push(updates.name);
137
+ }
138
+
139
+ if (updates.type !== undefined) {
140
+ fields.push('type = ?');
141
+ params.push(updates.type);
142
+ }
143
+
144
+ if (updates.properties !== undefined) {
145
+ fields.push('properties = ?');
146
+ params.push(JSON.stringify(updates.properties));
147
+ }
148
+
149
+ if (fields.length === 0) return entity;
150
+
151
+ fields.push('updated_at = ?');
152
+ params.push(Date.now());
153
+
154
+ params.push(id);
155
+
156
+ const stmt = db.prepare(`UPDATE entities SET ${fields.join(', ')} WHERE id = ?`);
157
+ stmt.run(...params as any[]);
158
+
159
+ return getEntity(id);
160
+ }
161
+
162
+ /**
163
+ * Delete an entity and all related facts/relationships (via cascade)
164
+ */
165
+ export function deleteEntity(id: string): boolean {
166
+ const db = getDb();
167
+ const stmt = db.prepare('DELETE FROM entities WHERE id = ?');
168
+ const result = stmt.run(id);
169
+ return result.changes > 0;
170
+ }
171
+
172
+ /**
173
+ * Search entities by name using LIKE query
174
+ */
175
+ export function searchEntitiesByName(query: string): Entity[] {
176
+ const db = getDb();
177
+ const stmt = db.prepare('SELECT * FROM entities WHERE name LIKE ? ORDER BY name');
178
+ const rows = stmt.all(`%${query}%`) as EntityRow[];
179
+ return rows.map(parseEntity);
180
+ }