@vellumai/assistant 0.3.3 → 0.3.5

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 (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -0,0 +1,759 @@
1
+ /**
2
+ * Media asset storage and processing stage tracking.
3
+ *
4
+ * Provides CRUD operations for the media_assets and processing_stages tables.
5
+ * Uses content-hash deduplication (same pattern as attachments-store.ts).
6
+ */
7
+
8
+ import { and, eq, inArray, gte, desc, asc } from 'drizzle-orm';
9
+ import { v4 as uuid } from 'uuid';
10
+ import { getDb } from './db.js';
11
+ import { mediaAssets, processingStages, mediaKeyframes, mediaVisionOutputs, mediaTimelines, mediaEvents, mediaTrackingProfiles } from './schema.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type MediaAssetStatus = 'registered' | 'processing' | 'indexed' | 'failed' | 'cancelled';
18
+ export type MediaType = 'video' | 'audio' | 'image';
19
+ export type StageStatus = 'pending' | 'running' | 'completed' | 'failed';
20
+
21
+ export interface MediaAsset {
22
+ id: string;
23
+ title: string;
24
+ filePath: string;
25
+ mimeType: string;
26
+ durationSeconds: number | null;
27
+ fileHash: string;
28
+ status: MediaAssetStatus;
29
+ mediaType: MediaType;
30
+ metadata: Record<string, unknown> | null;
31
+ createdAt: number;
32
+ updatedAt: number;
33
+ }
34
+
35
+ export interface ProcessingStage {
36
+ id: string;
37
+ assetId: string;
38
+ stage: string;
39
+ status: StageStatus;
40
+ progress: number;
41
+ lastError: string | null;
42
+ startedAt: number | null;
43
+ completedAt: number | null;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Content hashing
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Compute a content hash for deduplication. Uses Bun.hash (wyhash) for speed,
52
+ * encoded as base-36 for compact storage.
53
+ */
54
+ export function computeFileHash(data: Buffer | Uint8Array): string {
55
+ return Bun.hash(data).toString(36);
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Media asset CRUD
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export function registerMediaAsset(params: {
63
+ title: string;
64
+ filePath: string;
65
+ mimeType: string;
66
+ durationSeconds: number | null;
67
+ fileHash: string;
68
+ mediaType: MediaType;
69
+ metadata?: Record<string, unknown>;
70
+ }): MediaAsset {
71
+ const db = getDb();
72
+
73
+ // Dedup: if an asset with the same content hash already exists, return it
74
+ const existing = db
75
+ .select()
76
+ .from(mediaAssets)
77
+ .where(eq(mediaAssets.fileHash, params.fileHash))
78
+ .get();
79
+
80
+ if (existing) {
81
+ return parseAssetRow(existing);
82
+ }
83
+
84
+ const now = Date.now();
85
+ const record = {
86
+ id: uuid(),
87
+ title: params.title,
88
+ filePath: params.filePath,
89
+ mimeType: params.mimeType,
90
+ durationSeconds: params.durationSeconds,
91
+ fileHash: params.fileHash,
92
+ status: 'registered' as const,
93
+ mediaType: params.mediaType,
94
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
95
+ createdAt: now,
96
+ updatedAt: now,
97
+ };
98
+
99
+ db.insert(mediaAssets).values(record).run();
100
+
101
+ return {
102
+ ...record,
103
+ metadata: params.metadata ?? null,
104
+ };
105
+ }
106
+
107
+ export function getMediaAssetById(id: string): MediaAsset | null {
108
+ const db = getDb();
109
+ const row = db.select().from(mediaAssets).where(eq(mediaAssets.id, id)).get();
110
+ return row ? parseAssetRow(row) : null;
111
+ }
112
+
113
+ export function getMediaAssetByFilePath(filePath: string): MediaAsset | null {
114
+ const db = getDb();
115
+ const row = db.select().from(mediaAssets).where(eq(mediaAssets.filePath, filePath)).get();
116
+ return row ? parseAssetRow(row) : null;
117
+ }
118
+
119
+ export function getMediaAssetByHash(fileHash: string): MediaAsset | null {
120
+ const db = getDb();
121
+ const row = db.select().from(mediaAssets).where(eq(mediaAssets.fileHash, fileHash)).get();
122
+ return row ? parseAssetRow(row) : null;
123
+ }
124
+
125
+ export function getMediaAssetsByStatus(status: MediaAssetStatus): MediaAsset[] {
126
+ const db = getDb();
127
+ const rows = db.select().from(mediaAssets).where(eq(mediaAssets.status, status)).all();
128
+ return rows.map(parseAssetRow);
129
+ }
130
+
131
+ export function updateMediaAssetStatus(id: string, status: MediaAssetStatus): void {
132
+ const db = getDb();
133
+ db.update(mediaAssets)
134
+ .set({ status, updatedAt: Date.now() })
135
+ .where(eq(mediaAssets.id, id))
136
+ .run();
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Processing stage CRUD
141
+ // ---------------------------------------------------------------------------
142
+
143
+ export function createProcessingStage(params: {
144
+ assetId: string;
145
+ stage: string;
146
+ }): ProcessingStage {
147
+ const db = getDb();
148
+ const record = {
149
+ id: uuid(),
150
+ assetId: params.assetId,
151
+ stage: params.stage,
152
+ status: 'pending' as const,
153
+ progress: 0,
154
+ lastError: null,
155
+ startedAt: null,
156
+ completedAt: null,
157
+ };
158
+
159
+ db.insert(processingStages).values(record).run();
160
+ return record;
161
+ }
162
+
163
+ export function getProcessingStagesForAsset(assetId: string): ProcessingStage[] {
164
+ const db = getDb();
165
+ const rows = db
166
+ .select()
167
+ .from(processingStages)
168
+ .where(eq(processingStages.assetId, assetId))
169
+ .all();
170
+ return rows.map(parseStageRow);
171
+ }
172
+
173
+ export function updateProcessingStage(
174
+ id: string,
175
+ updates: Partial<Pick<ProcessingStage, 'status' | 'progress' | 'lastError' | 'startedAt' | 'completedAt'>>,
176
+ ): void {
177
+ const db = getDb();
178
+ db.update(processingStages)
179
+ .set(updates)
180
+ .where(eq(processingStages.id, id))
181
+ .run();
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Row parsing
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function parseAssetRow(row: typeof mediaAssets.$inferSelect): MediaAsset {
189
+ let metadata: Record<string, unknown> | null = null;
190
+ if (row.metadata) {
191
+ try {
192
+ metadata = JSON.parse(row.metadata) as Record<string, unknown>;
193
+ } catch {
194
+ metadata = null;
195
+ }
196
+ }
197
+ return {
198
+ id: row.id,
199
+ title: row.title,
200
+ filePath: row.filePath,
201
+ mimeType: row.mimeType,
202
+ durationSeconds: row.durationSeconds,
203
+ fileHash: row.fileHash,
204
+ status: row.status as MediaAssetStatus,
205
+ mediaType: row.mediaType as MediaType,
206
+ metadata,
207
+ createdAt: row.createdAt,
208
+ updatedAt: row.updatedAt,
209
+ };
210
+ }
211
+
212
+ function parseStageRow(row: typeof processingStages.$inferSelect): ProcessingStage {
213
+ return {
214
+ id: row.id,
215
+ assetId: row.assetId,
216
+ stage: row.stage,
217
+ status: row.status as StageStatus,
218
+ progress: row.progress,
219
+ lastError: row.lastError,
220
+ startedAt: row.startedAt,
221
+ completedAt: row.completedAt,
222
+ };
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Keyframe types & CRUD
227
+ // ---------------------------------------------------------------------------
228
+
229
+ export interface MediaKeyframe {
230
+ id: string;
231
+ assetId: string;
232
+ timestamp: number;
233
+ filePath: string;
234
+ metadata: Record<string, unknown> | null;
235
+ createdAt: number;
236
+ }
237
+
238
+ export function insertKeyframe(params: {
239
+ assetId: string;
240
+ timestamp: number;
241
+ filePath: string;
242
+ metadata?: Record<string, unknown>;
243
+ }): MediaKeyframe {
244
+ const db = getDb();
245
+ const now = Date.now();
246
+ const record = {
247
+ id: uuid(),
248
+ assetId: params.assetId,
249
+ timestamp: params.timestamp,
250
+ filePath: params.filePath,
251
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
252
+ createdAt: now,
253
+ };
254
+ db.insert(mediaKeyframes).values(record).run();
255
+ return { ...record, metadata: params.metadata ?? null };
256
+ }
257
+
258
+ export function insertKeyframesBatch(
259
+ rows: Array<{
260
+ assetId: string;
261
+ timestamp: number;
262
+ filePath: string;
263
+ metadata?: Record<string, unknown>;
264
+ }>,
265
+ ): MediaKeyframe[] {
266
+ const db = getDb();
267
+ const now = Date.now();
268
+ const records = rows.map((r) => ({
269
+ id: uuid(),
270
+ assetId: r.assetId,
271
+ timestamp: r.timestamp,
272
+ filePath: r.filePath,
273
+ metadata: r.metadata ? JSON.stringify(r.metadata) : null,
274
+ createdAt: now,
275
+ }));
276
+ if (records.length > 0) {
277
+ db.insert(mediaKeyframes).values(records).run();
278
+ }
279
+ return records.map((rec, i) => ({
280
+ ...rec,
281
+ metadata: rows[i].metadata ?? null,
282
+ }));
283
+ }
284
+
285
+ export function getKeyframesForAsset(assetId: string): MediaKeyframe[] {
286
+ const db = getDb();
287
+ const rows = db
288
+ .select()
289
+ .from(mediaKeyframes)
290
+ .where(eq(mediaKeyframes.assetId, assetId))
291
+ .all();
292
+ return rows.map(parseKeyframeRow);
293
+ }
294
+
295
+ export function deleteKeyframesForAsset(assetId: string): void {
296
+ const db = getDb();
297
+ db.delete(mediaKeyframes).where(eq(mediaKeyframes.assetId, assetId)).run();
298
+ }
299
+
300
+ export function getKeyframeById(id: string): MediaKeyframe | null {
301
+ const db = getDb();
302
+ const row = db.select().from(mediaKeyframes).where(eq(mediaKeyframes.id, id)).get();
303
+ return row ? parseKeyframeRow(row) : null;
304
+ }
305
+
306
+ function parseKeyframeRow(row: typeof mediaKeyframes.$inferSelect): MediaKeyframe {
307
+ let metadata: Record<string, unknown> | null = null;
308
+ if (row.metadata) {
309
+ try { metadata = JSON.parse(row.metadata) as Record<string, unknown>; } catch { metadata = null; }
310
+ }
311
+ return {
312
+ id: row.id,
313
+ assetId: row.assetId,
314
+ timestamp: row.timestamp,
315
+ filePath: row.filePath,
316
+ metadata,
317
+ createdAt: row.createdAt,
318
+ };
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // Vision output types & CRUD
323
+ // ---------------------------------------------------------------------------
324
+
325
+ export interface MediaVisionOutput {
326
+ id: string;
327
+ assetId: string;
328
+ keyframeId: string;
329
+ analysisType: string;
330
+ output: Record<string, unknown>;
331
+ confidence: number | null;
332
+ createdAt: number;
333
+ }
334
+
335
+ export function insertVisionOutput(params: {
336
+ assetId: string;
337
+ keyframeId: string;
338
+ analysisType: string;
339
+ output: Record<string, unknown>;
340
+ confidence?: number;
341
+ }): MediaVisionOutput {
342
+ const db = getDb();
343
+ const now = Date.now();
344
+ const record = {
345
+ id: uuid(),
346
+ assetId: params.assetId,
347
+ keyframeId: params.keyframeId,
348
+ analysisType: params.analysisType,
349
+ output: JSON.stringify(params.output),
350
+ confidence: params.confidence ?? null,
351
+ createdAt: now,
352
+ };
353
+ db.insert(mediaVisionOutputs).values(record).run();
354
+ return { ...record, output: params.output };
355
+ }
356
+
357
+ export function insertVisionOutputsBatch(
358
+ rows: Array<{
359
+ assetId: string;
360
+ keyframeId: string;
361
+ analysisType: string;
362
+ output: Record<string, unknown>;
363
+ confidence?: number;
364
+ }>,
365
+ ): MediaVisionOutput[] {
366
+ const db = getDb();
367
+ const now = Date.now();
368
+ const records = rows.map((r) => ({
369
+ id: uuid(),
370
+ assetId: r.assetId,
371
+ keyframeId: r.keyframeId,
372
+ analysisType: r.analysisType,
373
+ output: JSON.stringify(r.output),
374
+ confidence: r.confidence ?? null,
375
+ createdAt: now,
376
+ }));
377
+ if (records.length > 0) {
378
+ db.insert(mediaVisionOutputs).values(records).run();
379
+ }
380
+ return records.map((rec, i) => ({
381
+ ...rec,
382
+ output: rows[i].output,
383
+ }));
384
+ }
385
+
386
+ export function getVisionOutputsForAsset(assetId: string, analysisType?: string): MediaVisionOutput[] {
387
+ const db = getDb();
388
+ const conditions = [eq(mediaVisionOutputs.assetId, assetId)];
389
+ if (analysisType) {
390
+ conditions.push(eq(mediaVisionOutputs.analysisType, analysisType));
391
+ }
392
+ const rows = db
393
+ .select()
394
+ .from(mediaVisionOutputs)
395
+ .where(and(...conditions))
396
+ .all();
397
+ return rows.map(parseVisionOutputRow);
398
+ }
399
+
400
+ export function getVisionOutputsByKeyframeIds(keyframeIds: string[]): MediaVisionOutput[] {
401
+ if (keyframeIds.length === 0) return [];
402
+ const db = getDb();
403
+ const rows = db
404
+ .select()
405
+ .from(mediaVisionOutputs)
406
+ .where(inArray(mediaVisionOutputs.keyframeId, keyframeIds))
407
+ .all();
408
+ return rows.map(parseVisionOutputRow);
409
+ }
410
+
411
+ function parseVisionOutputRow(row: typeof mediaVisionOutputs.$inferSelect): MediaVisionOutput {
412
+ let output: Record<string, unknown> = {};
413
+ try { output = JSON.parse(row.output) as Record<string, unknown>; } catch { output = {}; }
414
+ return {
415
+ id: row.id,
416
+ assetId: row.assetId,
417
+ keyframeId: row.keyframeId,
418
+ analysisType: row.analysisType,
419
+ output,
420
+ confidence: row.confidence,
421
+ createdAt: row.createdAt,
422
+ };
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // Timeline types & CRUD
427
+ // ---------------------------------------------------------------------------
428
+
429
+ export interface MediaTimeline {
430
+ id: string;
431
+ assetId: string;
432
+ startTime: number;
433
+ endTime: number;
434
+ segmentType: string;
435
+ attributes: Record<string, unknown> | null;
436
+ confidence: number | null;
437
+ createdAt: number;
438
+ }
439
+
440
+ export function insertTimelineSegment(params: {
441
+ assetId: string;
442
+ startTime: number;
443
+ endTime: number;
444
+ segmentType: string;
445
+ attributes?: Record<string, unknown>;
446
+ confidence?: number;
447
+ }): MediaTimeline {
448
+ const db = getDb();
449
+ const now = Date.now();
450
+ const record = {
451
+ id: uuid(),
452
+ assetId: params.assetId,
453
+ startTime: params.startTime,
454
+ endTime: params.endTime,
455
+ segmentType: params.segmentType,
456
+ attributes: params.attributes ? JSON.stringify(params.attributes) : null,
457
+ confidence: params.confidence ?? null,
458
+ createdAt: now,
459
+ };
460
+ db.insert(mediaTimelines).values(record).run();
461
+ return { ...record, attributes: params.attributes ?? null };
462
+ }
463
+
464
+ export function insertTimelineSegmentsBatch(
465
+ rows: Array<{
466
+ assetId: string;
467
+ startTime: number;
468
+ endTime: number;
469
+ segmentType: string;
470
+ attributes?: Record<string, unknown>;
471
+ confidence?: number;
472
+ }>,
473
+ ): MediaTimeline[] {
474
+ const db = getDb();
475
+ const now = Date.now();
476
+ const records = rows.map((r) => ({
477
+ id: uuid(),
478
+ assetId: r.assetId,
479
+ startTime: r.startTime,
480
+ endTime: r.endTime,
481
+ segmentType: r.segmentType,
482
+ attributes: r.attributes ? JSON.stringify(r.attributes) : null,
483
+ confidence: r.confidence ?? null,
484
+ createdAt: now,
485
+ }));
486
+ if (records.length > 0) {
487
+ db.insert(mediaTimelines).values(records).run();
488
+ }
489
+ return records.map((rec, i) => ({
490
+ ...rec,
491
+ attributes: rows[i].attributes ?? null,
492
+ }));
493
+ }
494
+
495
+ export function getTimelineForAsset(assetId: string): MediaTimeline[] {
496
+ const db = getDb();
497
+ const rows = db
498
+ .select()
499
+ .from(mediaTimelines)
500
+ .where(eq(mediaTimelines.assetId, assetId))
501
+ .all();
502
+ return rows.map(parseTimelineRow);
503
+ }
504
+
505
+ export function deleteTimelineForAsset(assetId: string): void {
506
+ const db = getDb();
507
+ db.delete(mediaTimelines).where(eq(mediaTimelines.assetId, assetId)).run();
508
+ }
509
+
510
+ function parseTimelineRow(row: typeof mediaTimelines.$inferSelect): MediaTimeline {
511
+ let attributes: Record<string, unknown> | null = null;
512
+ if (row.attributes) {
513
+ try { attributes = JSON.parse(row.attributes) as Record<string, unknown>; } catch { attributes = null; }
514
+ }
515
+ return {
516
+ id: row.id,
517
+ assetId: row.assetId,
518
+ startTime: row.startTime,
519
+ endTime: row.endTime,
520
+ segmentType: row.segmentType,
521
+ attributes,
522
+ confidence: row.confidence,
523
+ createdAt: row.createdAt,
524
+ };
525
+ }
526
+
527
+ // ---------------------------------------------------------------------------
528
+ // Media event types & CRUD
529
+ // ---------------------------------------------------------------------------
530
+
531
+ export interface MediaEvent {
532
+ id: string;
533
+ assetId: string;
534
+ eventType: string;
535
+ startTime: number;
536
+ endTime: number;
537
+ confidence: number;
538
+ reasons: string[];
539
+ metadata: Record<string, unknown> | null;
540
+ createdAt: number;
541
+ }
542
+
543
+ export function insertEvent(params: {
544
+ assetId: string;
545
+ eventType: string;
546
+ startTime: number;
547
+ endTime: number;
548
+ confidence: number;
549
+ reasons: string[];
550
+ metadata?: Record<string, unknown>;
551
+ }): MediaEvent {
552
+ const db = getDb();
553
+ const now = Date.now();
554
+ const record = {
555
+ id: uuid(),
556
+ assetId: params.assetId,
557
+ eventType: params.eventType,
558
+ startTime: params.startTime,
559
+ endTime: params.endTime,
560
+ confidence: params.confidence,
561
+ reasons: JSON.stringify(params.reasons),
562
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
563
+ createdAt: now,
564
+ };
565
+ db.insert(mediaEvents).values(record).run();
566
+ return { ...record, reasons: params.reasons, metadata: params.metadata ?? null };
567
+ }
568
+
569
+ export function insertEventsBatch(
570
+ rows: Array<{
571
+ assetId: string;
572
+ eventType: string;
573
+ startTime: number;
574
+ endTime: number;
575
+ confidence: number;
576
+ reasons: string[];
577
+ metadata?: Record<string, unknown>;
578
+ }>,
579
+ ): MediaEvent[] {
580
+ const db = getDb();
581
+ const now = Date.now();
582
+ const records = rows.map((r) => ({
583
+ id: uuid(),
584
+ assetId: r.assetId,
585
+ eventType: r.eventType,
586
+ startTime: r.startTime,
587
+ endTime: r.endTime,
588
+ confidence: r.confidence,
589
+ reasons: JSON.stringify(r.reasons),
590
+ metadata: r.metadata ? JSON.stringify(r.metadata) : null,
591
+ createdAt: now,
592
+ }));
593
+ if (records.length > 0) {
594
+ db.insert(mediaEvents).values(records).run();
595
+ }
596
+ return records.map((rec, i) => ({
597
+ ...rec,
598
+ reasons: rows[i].reasons,
599
+ metadata: rows[i].metadata ?? null,
600
+ }));
601
+ }
602
+
603
+ export function getEventsForAsset(
604
+ assetId: string,
605
+ filters?: {
606
+ eventType?: string;
607
+ minConfidence?: number;
608
+ limit?: number;
609
+ sortBy?: 'confidence' | 'startTime';
610
+ },
611
+ ): MediaEvent[] {
612
+ const db = getDb();
613
+ const conditions = [eq(mediaEvents.assetId, assetId)];
614
+ if (filters?.eventType) {
615
+ conditions.push(eq(mediaEvents.eventType, filters.eventType));
616
+ }
617
+ if (filters?.minConfidence !== undefined) {
618
+ conditions.push(gte(mediaEvents.confidence, filters.minConfidence));
619
+ }
620
+
621
+ let query = db
622
+ .select()
623
+ .from(mediaEvents)
624
+ .where(and(...conditions))
625
+ .$dynamic();
626
+
627
+ if (filters?.sortBy === 'confidence') {
628
+ query = query.orderBy(desc(mediaEvents.confidence));
629
+ } else {
630
+ query = query.orderBy(asc(mediaEvents.startTime));
631
+ }
632
+
633
+ if (filters?.limit) {
634
+ query = query.limit(filters.limit);
635
+ }
636
+
637
+ const rows = query.all();
638
+ return rows.map(parseEventRow);
639
+ }
640
+
641
+ export function getEventById(id: string): MediaEvent | null {
642
+ const db = getDb();
643
+ const row = db.select().from(mediaEvents).where(eq(mediaEvents.id, id)).get();
644
+ return row ? parseEventRow(row) : null;
645
+ }
646
+
647
+ export function deleteEventsForAsset(assetId: string): void {
648
+ const db = getDb();
649
+ db.delete(mediaEvents).where(eq(mediaEvents.assetId, assetId)).run();
650
+ }
651
+
652
+ export function deleteEventsForAssetByType(assetId: string, eventType: string): void {
653
+ const db = getDb();
654
+ db.delete(mediaEvents)
655
+ .where(and(eq(mediaEvents.assetId, assetId), eq(mediaEvents.eventType, eventType)))
656
+ .run();
657
+ }
658
+
659
+ function parseEventRow(row: typeof mediaEvents.$inferSelect): MediaEvent {
660
+ let reasons: string[] = [];
661
+ try { reasons = JSON.parse(row.reasons) as string[]; } catch { reasons = []; }
662
+ let metadata: Record<string, unknown> | null = null;
663
+ if (row.metadata) {
664
+ try { metadata = JSON.parse(row.metadata) as Record<string, unknown>; } catch { metadata = null; }
665
+ }
666
+ return {
667
+ id: row.id,
668
+ assetId: row.assetId,
669
+ eventType: row.eventType,
670
+ startTime: row.startTime,
671
+ endTime: row.endTime,
672
+ confidence: row.confidence,
673
+ reasons,
674
+ metadata,
675
+ createdAt: row.createdAt,
676
+ };
677
+ }
678
+
679
+ // ---------------------------------------------------------------------------
680
+ // Tracking profile types & CRUD
681
+ // ---------------------------------------------------------------------------
682
+
683
+ export type CapabilityTier = 'ready' | 'beta' | 'experimental';
684
+
685
+ export interface CapabilityProfileEntry {
686
+ enabled: boolean;
687
+ tier: CapabilityTier;
688
+ }
689
+
690
+ export type CapabilityProfile = Record<string, CapabilityProfileEntry>;
691
+
692
+ export interface TrackingProfile {
693
+ id: string;
694
+ assetId: string;
695
+ capabilities: CapabilityProfile;
696
+ createdAt: number;
697
+ }
698
+
699
+ /**
700
+ * Upsert a tracking profile for a media asset. If a profile already exists
701
+ * for the given assetId, it is replaced.
702
+ */
703
+ export function setTrackingProfile(assetId: string, capabilities: CapabilityProfile): TrackingProfile {
704
+ const db = getDb();
705
+ const now = Date.now();
706
+
707
+ // Check for existing profile by assetId
708
+ const existing = db
709
+ .select()
710
+ .from(mediaTrackingProfiles)
711
+ .where(eq(mediaTrackingProfiles.assetId, assetId))
712
+ .get();
713
+
714
+ if (existing) {
715
+ db.update(mediaTrackingProfiles)
716
+ .set({ capabilities: JSON.stringify(capabilities), createdAt: now })
717
+ .where(eq(mediaTrackingProfiles.id, existing.id))
718
+ .run();
719
+ return { id: existing.id, assetId, capabilities, createdAt: now };
720
+ }
721
+
722
+ const id = uuid();
723
+ db.insert(mediaTrackingProfiles).values({
724
+ id,
725
+ assetId,
726
+ capabilities: JSON.stringify(capabilities),
727
+ createdAt: now,
728
+ }).run();
729
+
730
+ return { id, assetId, capabilities, createdAt: now };
731
+ }
732
+
733
+ /**
734
+ * Get the current tracking profile for a media asset, if one exists.
735
+ */
736
+ export function getTrackingProfile(assetId: string): TrackingProfile | null {
737
+ const db = getDb();
738
+ const row = db
739
+ .select()
740
+ .from(mediaTrackingProfiles)
741
+ .where(eq(mediaTrackingProfiles.assetId, assetId))
742
+ .get();
743
+
744
+ if (!row) return null;
745
+
746
+ let capabilities: CapabilityProfile = {};
747
+ try {
748
+ capabilities = JSON.parse(row.capabilities) as CapabilityProfile;
749
+ } catch {
750
+ capabilities = {};
751
+ }
752
+
753
+ return {
754
+ id: row.id,
755
+ assetId: row.assetId,
756
+ capabilities,
757
+ createdAt: row.createdAt,
758
+ };
759
+ }