amalgm 0.1.51 → 0.1.52

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 (70) hide show
  1. package/lib/tunnel-events.js +48 -23
  2. package/package.json +2 -2
  3. package/runtime/lib/harnesses.js +12 -4
  4. package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
  5. package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
  6. package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
  7. package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
  8. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
  9. package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
  10. package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
  11. package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
  13. package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
  14. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
  15. package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
  16. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
  17. package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
  18. package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
  19. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
  20. package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
  21. package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
  22. package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
  23. package/runtime/scripts/amalgm-mcp/config.js +33 -48
  24. package/runtime/scripts/amalgm-mcp/deps.js +1 -31
  25. package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
  26. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
  27. package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
  28. package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
  29. package/runtime/scripts/amalgm-mcp/index.js +12 -14
  30. package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
  31. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
  32. package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
  33. package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
  34. package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
  37. package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
  38. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
  39. package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
  40. package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
  41. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
  42. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
  43. package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
  44. package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
  45. package/runtime/scripts/chat-core/adapters/claude.js +2 -1
  46. package/runtime/scripts/chat-core/auth.js +82 -12
  47. package/runtime/scripts/chat-core/contract.js +5 -1
  48. package/runtime/scripts/chat-core/engine.js +103 -62
  49. package/runtime/scripts/chat-core/event-schema.js +8 -0
  50. package/runtime/scripts/chat-core/events.js +5 -0
  51. package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
  52. package/runtime/scripts/chat-core/parts.js +21 -6
  53. package/runtime/scripts/chat-core/sse.js +3 -0
  54. package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
  55. package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
  56. package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
  57. package/runtime/scripts/chat-core/tool-shape.js +4 -4
  58. package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
  59. package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
  60. package/runtime/scripts/local-gateway.js +34 -27
  61. package/runtime/scripts/platform-context.txt +76 -94
  62. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
  63. package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
  64. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
  65. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
  66. package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
  67. package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
  68. package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
  69. package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
  70. package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
@@ -0,0 +1,1125 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const { loadCronParser } = require('../deps');
6
+ const { openLocalDb } = require('../state/db');
7
+ const { appendStateEvent, insertStateEvent, publishStateEvent } = require('../state/events');
8
+ const { normalizeTaskSchedule } = require('../tasks/schedule-normalization');
9
+ const { compileWorkflowText, splitEventRef } = require('../workflows/compiler');
10
+ const { getWebhookUrl } = require('../events/webhook-url');
11
+ const { validateCellReferences } = require('./cell-references');
12
+ const { validateWorkflowToolActions } = require('./tool-actions');
13
+
14
+ const LEGACY_MIGRATION_KEY = 'automations_legacy_migrated_v1';
15
+ const DEFAULT_RUN_LIMIT = 100;
16
+
17
+ let cronParser = null;
18
+
19
+ function cron() {
20
+ if (!cronParser) cronParser = loadCronParser();
21
+ return cronParser;
22
+ }
23
+
24
+ function nowIso() {
25
+ return new Date().toISOString();
26
+ }
27
+
28
+ function parseJson(value, fallback = null) {
29
+ if (typeof value !== 'string' || !value) return fallback;
30
+ try {
31
+ return JSON.parse(value);
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+
37
+ function stringifyJson(value, fallback) {
38
+ return JSON.stringify(value === undefined ? fallback : value);
39
+ }
40
+
41
+ function isObject(value) {
42
+ return !!value && typeof value === 'object' && !Array.isArray(value);
43
+ }
44
+
45
+ function cleanString(value) {
46
+ return typeof value === 'string' ? value.trim() : '';
47
+ }
48
+
49
+ function isoOrNull(value) {
50
+ if (!value) return null;
51
+ const date = new Date(value);
52
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
53
+ }
54
+
55
+ function validDate(value) {
56
+ if (!value) return null;
57
+ const date = new Date(value);
58
+ return Number.isNaN(date.getTime()) ? null : date;
59
+ }
60
+
61
+ function metaValue(db, key) {
62
+ return db.prepare('SELECT value FROM local_meta WHERE key = ?').get(key)?.value || null;
63
+ }
64
+
65
+ function setMetaValue(db, key, value) {
66
+ db.prepare(`
67
+ INSERT INTO local_meta (key, value, updated_at)
68
+ VALUES (?, ?, ?)
69
+ ON CONFLICT(key) DO UPDATE SET
70
+ value = excluded.value,
71
+ updated_at = excluded.updated_at
72
+ `).run(key, value, nowIso());
73
+ }
74
+
75
+ function defaultWorkflowText(label = 'Automation', triggerRef = 'automation.manual') {
76
+ const safeLabel = JSON.stringify(label || 'Automation');
77
+ return `export default workflow({
78
+ trigger: event(${JSON.stringify(triggerRef)}),
79
+ cells: [
80
+ code("start", async () => ({
81
+ ok: true,
82
+ message: ${safeLabel} + " has no workflow steps configured yet."
83
+ }))
84
+ ]
85
+ })`;
86
+ }
87
+
88
+ function workflowTriggerRefForInput(input = {}) {
89
+ const firstTrigger = Array.isArray(input.triggers) ? input.triggers[0] : input.trigger;
90
+ if (firstTrigger?.kind === 'scheduled') return 'scheduled.run';
91
+ if (firstTrigger?.source || firstTrigger?.event || input.source || input.event) {
92
+ const source = cleanString(firstTrigger?.source || input.source) || '*';
93
+ const event = cleanString(firstTrigger?.event || input.event) || '*';
94
+ return `${source}.${event}`;
95
+ }
96
+ return 'automation.manual';
97
+ }
98
+
99
+ function normalizeWorkflowRecord(input = {}, existing = null) {
100
+ const timestamp = nowIso();
101
+ const workflowText = cleanString(
102
+ input.workflowText ?? input.workflow ?? existing?.workflowText,
103
+ ) || defaultWorkflowText(input.name || existing?.name || 'Automation', workflowTriggerRefForInput(input));
104
+ let workflowIr = isObject(input.workflowIr)
105
+ ? input.workflowIr
106
+ : isObject(existing?.workflowIr)
107
+ ? existing.workflowIr
108
+ : null;
109
+ let compilerErrors = null;
110
+
111
+ try {
112
+ workflowIr = compileWorkflowText(workflowText);
113
+ const staticErrors = [
114
+ ...validateCellReferences(workflowIr),
115
+ ...validateWorkflowToolActions(workflowIr),
116
+ ];
117
+ if (staticErrors.length > 0) {
118
+ throw new Error(staticErrors.map((item) => item.message).join('\n'));
119
+ }
120
+ } catch (error) {
121
+ workflowIr = null;
122
+ compilerErrors = [error.message || String(error)];
123
+ }
124
+
125
+ return {
126
+ id: cleanString(input.id) || existing?.id || crypto.randomUUID(),
127
+ name: cleanString(input.name) || existing?.name || 'Untitled workflow',
128
+ description: input.description ?? existing?.description ?? '',
129
+ enabled: input.enabled ?? existing?.enabled ?? true,
130
+ projectPath: input.projectPath ?? existing?.projectPath ?? null,
131
+ workflowText,
132
+ workflowIr,
133
+ compilerErrors,
134
+ allowlist: isObject(input.allowlist) ? input.allowlist : existing?.allowlist || {},
135
+ limits: isObject(input.limits) ? input.limits : existing?.limits || {},
136
+ internal: input.internal ?? existing?.internal ?? false,
137
+ createdAt: isoOrNull(input.createdAt) || existing?.createdAt || timestamp,
138
+ updatedAt: existing ? timestamp : (isoOrNull(input.updatedAt) || null),
139
+ };
140
+ }
141
+
142
+ function workflowRowParams(workflow) {
143
+ return {
144
+ id: workflow.id,
145
+ name: workflow.name,
146
+ description: workflow.description || '',
147
+ enabled: workflow.enabled === false ? 0 : 1,
148
+ project_path: workflow.projectPath || null,
149
+ workflow_text: workflow.workflowText || '',
150
+ workflow_ir_json: workflow.workflowIr ? JSON.stringify(workflow.workflowIr) : null,
151
+ compiler_errors_json: workflow.compilerErrors ? JSON.stringify(workflow.compilerErrors) : null,
152
+ allowlist_json: stringifyJson(workflow.allowlist, {}),
153
+ limits_json: stringifyJson(workflow.limits, {}),
154
+ created_at: workflow.createdAt || nowIso(),
155
+ updated_at: workflow.updatedAt || workflow.createdAt || nowIso(),
156
+ workflow_json: JSON.stringify(workflow),
157
+ };
158
+ }
159
+
160
+ function rowToWorkflow(row) {
161
+ if (!row) return null;
162
+ const parsed = parseJson(row.workflow_json, {});
163
+ return normalizeWorkflowRecord({
164
+ ...parsed,
165
+ id: row.id,
166
+ name: row.name,
167
+ description: row.description || '',
168
+ enabled: row.enabled === 1,
169
+ projectPath: row.project_path || null,
170
+ workflowText: row.workflow_text || parsed.workflowText,
171
+ workflowIr: parseJson(row.workflow_ir_json, parsed.workflowIr || null),
172
+ compilerErrors: parseJson(row.compiler_errors_json, parsed.compilerErrors || null),
173
+ allowlist: parseJson(row.allowlist_json, parsed.allowlist || {}),
174
+ limits: parseJson(row.limits_json, parsed.limits || {}),
175
+ createdAt: row.created_at,
176
+ updatedAt: row.updated_at,
177
+ });
178
+ }
179
+
180
+ function upsertWorkflowRow(db, workflow) {
181
+ const params = workflowRowParams(workflow);
182
+ db.prepare(`
183
+ INSERT INTO automation_workflows (
184
+ id, name, description, enabled, project_path, workflow_text,
185
+ workflow_ir_json, compiler_errors_json, allowlist_json, limits_json,
186
+ created_at, updated_at, workflow_json
187
+ )
188
+ VALUES (
189
+ @id, @name, @description, @enabled, @project_path, @workflow_text,
190
+ @workflow_ir_json, @compiler_errors_json, @allowlist_json, @limits_json,
191
+ @created_at, @updated_at, @workflow_json
192
+ )
193
+ ON CONFLICT(id) DO UPDATE SET
194
+ name = excluded.name,
195
+ description = excluded.description,
196
+ enabled = excluded.enabled,
197
+ project_path = excluded.project_path,
198
+ workflow_text = excluded.workflow_text,
199
+ workflow_ir_json = excluded.workflow_ir_json,
200
+ compiler_errors_json = excluded.compiler_errors_json,
201
+ allowlist_json = excluded.allowlist_json,
202
+ limits_json = excluded.limits_json,
203
+ updated_at = excluded.updated_at,
204
+ workflow_json = excluded.workflow_json
205
+ `).run(params);
206
+ }
207
+
208
+ function normalizeSchedule(inputSchedule, existingSchedule = null) {
209
+ const candidate = inputSchedule || existingSchedule;
210
+ const result = normalizeTaskSchedule(candidate);
211
+ if (result.error) throw new Error(result.error);
212
+ return result.schedule;
213
+ }
214
+
215
+ function computeNextRunAt(trigger, afterDate = new Date()) {
216
+ if (!trigger || trigger.enabled === false || trigger.kind !== 'scheduled') return null;
217
+ const schedule = trigger.schedule;
218
+ if (!schedule) return null;
219
+ const after = validDate(afterDate) || new Date();
220
+
221
+ if (schedule.kind === 'once') {
222
+ const at = validDate(schedule.at);
223
+ if (!at) return null;
224
+ if (trigger.lastFiredAt && validDate(trigger.lastFiredAt) >= at) return null;
225
+ return at > after ? at.toISOString() : at.toISOString();
226
+ }
227
+
228
+ if (schedule.kind === 'interval') {
229
+ const ms = Number(schedule.ms);
230
+ if (!Number.isFinite(ms) || ms < 60_000) return null;
231
+ return new Date(after.getTime() + ms).toISOString();
232
+ }
233
+
234
+ if (schedule.kind === 'cron') {
235
+ try {
236
+ const interval = cron().CronExpressionParser.parse(schedule.expr, {
237
+ currentDate: after,
238
+ tz: schedule.tz || 'UTC',
239
+ });
240
+ return interval.next().toDate().toISOString();
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ return null;
247
+ }
248
+
249
+ function scheduleLabel(schedule) {
250
+ if (!schedule) return null;
251
+ if (schedule.kind === 'once') return schedule.at || 'Once';
252
+ if (schedule.kind === 'interval') {
253
+ const ms = Number(schedule.ms);
254
+ if (!Number.isFinite(ms)) return 'Interval';
255
+ if (ms >= 86_400_000) return `Every ${Math.round(ms / 86_400_000)}d`;
256
+ if (ms >= 3_600_000) return `Every ${Math.round(ms / 3_600_000)}h`;
257
+ return `Every ${Math.max(1, Math.round(ms / 60_000))}m`;
258
+ }
259
+ if (schedule.kind === 'cron') return schedule.expr || 'Cron';
260
+ return null;
261
+ }
262
+
263
+ function normalizeTriggerRecord(input = {}, automation, existing = null) {
264
+ const timestamp = nowIso();
265
+ const rawKind = cleanString(input.kind || existing?.kind || 'event').toLowerCase();
266
+ const kind = rawKind === 'webhook' ? 'event' : rawKind;
267
+ if (!['event', 'scheduled'].includes(kind)) {
268
+ throw new Error(`Unsupported trigger kind: ${rawKind}`);
269
+ }
270
+
271
+ const trigger = {
272
+ id: cleanString(input.id) || existing?.id || crypto.randomUUID(),
273
+ automationId: automation.id,
274
+ workflowId: automation.workflowId,
275
+ kind,
276
+ name: cleanString(input.name) || existing?.name || automation.name || 'Automation trigger',
277
+ description: input.description ?? existing?.description ?? '',
278
+ enabled: input.enabled ?? existing?.enabled ?? automation.enabled !== false,
279
+ projectPath: input.projectPath ?? existing?.projectPath ?? automation.projectPath ?? null,
280
+ createdAt: isoOrNull(input.createdAt) || existing?.createdAt || timestamp,
281
+ updatedAt: existing ? timestamp : (isoOrNull(input.updatedAt) || null),
282
+ lastFiredAt: isoOrNull(input.lastFiredAt) || existing?.lastFiredAt || null,
283
+ };
284
+
285
+ if (kind === 'scheduled') {
286
+ trigger.schedule = normalizeSchedule(input.schedule, existing?.schedule);
287
+ trigger.scheduleLabel = scheduleLabel(trigger.schedule);
288
+ trigger.nextRunAt = input.nextRunAt === null
289
+ ? null
290
+ : isoOrNull(input.nextRunAt) || existing?.nextRunAt || computeNextRunAt(trigger, trigger.createdAt);
291
+ trigger.source = null;
292
+ trigger.event = null;
293
+ trigger.sourceUrl = null;
294
+ trigger.webhookUrl = null;
295
+ trigger.secret = null;
296
+ } else {
297
+ const irTrigger = automation.workflow?.workflowIr?.trigger || {};
298
+ const source = cleanString(input.source ?? existing?.source ?? irTrigger.source) || '*';
299
+ const event = cleanString(input.event ?? input.eventName ?? existing?.event ?? irTrigger.event) || '*';
300
+ trigger.source = source;
301
+ trigger.event = event;
302
+ trigger.sourceUrl = input.sourceUrl ?? existing?.sourceUrl ?? null;
303
+ trigger.webhookUrl = input.webhookUrl || existing?.webhookUrl || getWebhookUrl();
304
+ trigger.secret = input.secret || existing?.secret || crypto.randomUUID().replace(/-/g, '');
305
+ trigger.schedule = null;
306
+ trigger.scheduleLabel = null;
307
+ trigger.nextRunAt = null;
308
+ }
309
+
310
+ return trigger;
311
+ }
312
+
313
+ function triggerRowParams(trigger) {
314
+ return {
315
+ id: trigger.id,
316
+ automation_id: trigger.automationId,
317
+ kind: trigger.kind,
318
+ name: trigger.name,
319
+ description: trigger.description || '',
320
+ enabled: trigger.enabled === false ? 0 : 1,
321
+ schedule_kind: trigger.schedule?.kind || null,
322
+ schedule_json: trigger.schedule ? JSON.stringify(trigger.schedule) : null,
323
+ next_run_at: trigger.nextRunAt || null,
324
+ last_fired_at: trigger.lastFiredAt || null,
325
+ source: trigger.source || null,
326
+ event: trigger.event || null,
327
+ source_url: trigger.sourceUrl || null,
328
+ webhook_url: trigger.webhookUrl || null,
329
+ secret: trigger.secret || null,
330
+ created_at: trigger.createdAt || nowIso(),
331
+ updated_at: trigger.updatedAt || trigger.createdAt || nowIso(),
332
+ trigger_json: JSON.stringify(trigger),
333
+ };
334
+ }
335
+
336
+ function rowToTrigger(row) {
337
+ if (!row) return null;
338
+ const parsed = parseJson(row.trigger_json, {});
339
+ return {
340
+ ...parsed,
341
+ id: row.id,
342
+ automationId: row.automation_id,
343
+ workflowId: parsed.workflowId || null,
344
+ kind: row.kind,
345
+ name: row.name,
346
+ description: row.description || '',
347
+ enabled: row.enabled === 1,
348
+ schedule: parseJson(row.schedule_json, parsed.schedule || null),
349
+ scheduleLabel: parsed.scheduleLabel || scheduleLabel(parseJson(row.schedule_json, parsed.schedule || null)),
350
+ nextRunAt: row.next_run_at || null,
351
+ lastFiredAt: row.last_fired_at || null,
352
+ source: row.source || null,
353
+ event: row.event || null,
354
+ sourceUrl: row.source_url || null,
355
+ webhookUrl: row.webhook_url || null,
356
+ secret: row.secret || null,
357
+ createdAt: row.created_at,
358
+ updatedAt: row.updated_at,
359
+ };
360
+ }
361
+
362
+ function upsertTriggerRow(db, trigger) {
363
+ const params = triggerRowParams(trigger);
364
+ db.prepare(`
365
+ INSERT INTO automation_triggers (
366
+ id, automation_id, kind, name, description, enabled, schedule_kind,
367
+ schedule_json, next_run_at, last_fired_at, source, event, source_url,
368
+ webhook_url, secret, created_at, updated_at, trigger_json
369
+ )
370
+ VALUES (
371
+ @id, @automation_id, @kind, @name, @description, @enabled, @schedule_kind,
372
+ @schedule_json, @next_run_at, @last_fired_at, @source, @event, @source_url,
373
+ @webhook_url, @secret, @created_at, @updated_at, @trigger_json
374
+ )
375
+ ON CONFLICT(id) DO UPDATE SET
376
+ automation_id = excluded.automation_id,
377
+ kind = excluded.kind,
378
+ name = excluded.name,
379
+ description = excluded.description,
380
+ enabled = excluded.enabled,
381
+ schedule_kind = excluded.schedule_kind,
382
+ schedule_json = excluded.schedule_json,
383
+ next_run_at = excluded.next_run_at,
384
+ last_fired_at = excluded.last_fired_at,
385
+ source = excluded.source,
386
+ event = excluded.event,
387
+ source_url = excluded.source_url,
388
+ webhook_url = excluded.webhook_url,
389
+ secret = excluded.secret,
390
+ updated_at = excluded.updated_at,
391
+ trigger_json = excluded.trigger_json
392
+ `).run(params);
393
+ }
394
+
395
+ function normalizeAutomationRecord(input = {}, workflow, existing = null) {
396
+ const timestamp = nowIso();
397
+ return {
398
+ id: cleanString(input.id) || existing?.id || crypto.randomUUID(),
399
+ name: cleanString(input.name) || existing?.name || workflow?.name || 'Untitled automation',
400
+ description: input.description ?? existing?.description ?? workflow?.description ?? '',
401
+ enabled: input.enabled ?? existing?.enabled ?? true,
402
+ projectPath: input.projectPath ?? existing?.projectPath ?? workflow?.projectPath ?? null,
403
+ workflowId: workflow.id,
404
+ createdAt: isoOrNull(input.createdAt) || existing?.createdAt || timestamp,
405
+ updatedAt: existing ? timestamp : (isoOrNull(input.updatedAt) || null),
406
+ };
407
+ }
408
+
409
+ function automationRowParams(automation) {
410
+ return {
411
+ id: automation.id,
412
+ name: automation.name,
413
+ description: automation.description || '',
414
+ enabled: automation.enabled === false ? 0 : 1,
415
+ project_path: automation.projectPath || null,
416
+ workflow_id: automation.workflowId,
417
+ created_at: automation.createdAt || nowIso(),
418
+ updated_at: automation.updatedAt || automation.createdAt || nowIso(),
419
+ automation_json: JSON.stringify(automation),
420
+ };
421
+ }
422
+
423
+ function rowToAutomationBase(row) {
424
+ if (!row) return null;
425
+ const parsed = parseJson(row.automation_json, {});
426
+ return {
427
+ ...parsed,
428
+ id: row.id,
429
+ name: row.name,
430
+ description: row.description || '',
431
+ enabled: row.enabled === 1,
432
+ projectPath: row.project_path || null,
433
+ workflowId: row.workflow_id,
434
+ createdAt: row.created_at,
435
+ updatedAt: row.updated_at,
436
+ };
437
+ }
438
+
439
+ function upsertAutomationRow(db, automation) {
440
+ const params = automationRowParams(automation);
441
+ db.prepare(`
442
+ INSERT INTO automation_definitions (
443
+ id, name, description, enabled, project_path, workflow_id,
444
+ created_at, updated_at, automation_json
445
+ )
446
+ VALUES (
447
+ @id, @name, @description, @enabled, @project_path, @workflow_id,
448
+ @created_at, @updated_at, @automation_json
449
+ )
450
+ ON CONFLICT(id) DO UPDATE SET
451
+ name = excluded.name,
452
+ description = excluded.description,
453
+ enabled = excluded.enabled,
454
+ project_path = excluded.project_path,
455
+ workflow_id = excluded.workflow_id,
456
+ updated_at = excluded.updated_at,
457
+ automation_json = excluded.automation_json
458
+ `).run(params);
459
+ }
460
+
461
+ function workflowStatus(workflow) {
462
+ if (!workflow || workflow.enabled === false) return 'paused';
463
+ if (Array.isArray(workflow.compilerErrors) && workflow.compilerErrors.length > 0) return 'error';
464
+ return 'idle';
465
+ }
466
+
467
+ function triggerStatus(trigger) {
468
+ if (!trigger || trigger.enabled === false) return 'paused';
469
+ return 'idle';
470
+ }
471
+
472
+ function automationStatus(automation, triggers, workflow) {
473
+ if (automation.enabled === false) return 'paused';
474
+ if (workflowStatus(workflow) === 'error') return 'error';
475
+ if (triggers.some((trigger) => triggerStatus(trigger) === 'error')) return 'error';
476
+ if (triggers.length > 0 && triggers.every((trigger) => trigger.enabled === false)) return 'paused';
477
+ return 'idle';
478
+ }
479
+
480
+ function composeAutomation(base, triggers, workflow) {
481
+ const normalizedTriggers = triggers.map((trigger) => ({
482
+ ...trigger,
483
+ workflowId: base.workflowId,
484
+ workflowIds: [base.workflowId],
485
+ status: triggerStatus(trigger),
486
+ backing: {
487
+ resource: 'triggers',
488
+ id: trigger.id,
489
+ },
490
+ }));
491
+ const normalizedWorkflow = workflow ? {
492
+ ...workflow,
493
+ triggerIds: normalizedTriggers.map((trigger) => trigger.id),
494
+ automationIds: [base.id],
495
+ status: workflowStatus(workflow),
496
+ } : null;
497
+
498
+ return {
499
+ ...base,
500
+ kind: normalizedTriggers[0]?.kind || 'automation',
501
+ status: automationStatus(base, normalizedTriggers, normalizedWorkflow),
502
+ triggerIds: normalizedTriggers.map((trigger) => trigger.id),
503
+ workflowIds: [base.workflowId],
504
+ triggers: normalizedTriggers,
505
+ workflow: normalizedWorkflow,
506
+ workflows: normalizedWorkflow ? [normalizedWorkflow] : [],
507
+ };
508
+ }
509
+
510
+ function readAutomationGraph(db = openLocalDb()) {
511
+ ensureAutomationsStore();
512
+ const automationRows = db.prepare('SELECT * FROM automation_definitions ORDER BY datetime(created_at) DESC, lower(name)').all();
513
+ const workflowRows = db.prepare('SELECT * FROM automation_workflows').all();
514
+ const triggerRows = db.prepare('SELECT * FROM automation_triggers ORDER BY datetime(created_at) DESC, lower(name)').all();
515
+
516
+ const workflowsById = new Map(workflowRows.map((row) => {
517
+ const workflow = rowToWorkflow(row);
518
+ return [workflow.id, workflow];
519
+ }));
520
+ const triggersByAutomation = new Map();
521
+ for (const row of triggerRows) {
522
+ const trigger = rowToTrigger(row);
523
+ if (!triggersByAutomation.has(trigger.automationId)) triggersByAutomation.set(trigger.automationId, []);
524
+ triggersByAutomation.get(trigger.automationId).push(trigger);
525
+ }
526
+
527
+ const automations = automationRows
528
+ .map(rowToAutomationBase)
529
+ .filter(Boolean)
530
+ .map((automation) => composeAutomation(
531
+ automation,
532
+ triggersByAutomation.get(automation.id) || [],
533
+ workflowsById.get(automation.workflowId) || null,
534
+ ));
535
+ const triggers = automations.flatMap((automation) => automation.triggers);
536
+ const usedWorkflowIds = new Set(automations.map((automation) => automation.workflowId));
537
+ const workflows = Array.from(workflowsById.values()).map((workflow) => ({
538
+ ...workflow,
539
+ triggerIds: triggers.filter((trigger) => trigger.workflowId === workflow.id).map((trigger) => trigger.id),
540
+ automationIds: automations.filter((automation) => automation.workflowId === workflow.id).map((automation) => automation.id),
541
+ status: workflowStatus(workflow),
542
+ orphaned: !usedWorkflowIds.has(workflow.id),
543
+ }));
544
+
545
+ return { automations, triggers, workflows };
546
+ }
547
+
548
+ function publishResources(source = 'automations:save') {
549
+ const resources = readAutomationGraph();
550
+ appendStateEvent({ resource: 'automations', op: 'replace', value: resources.automations, source });
551
+ appendStateEvent({ resource: 'triggers', op: 'replace', value: resources.triggers, source });
552
+ appendStateEvent({ resource: 'workflows', op: 'replace', value: resources.workflows, source });
553
+ }
554
+
555
+ function defaultTriggersForInput(input, automation) {
556
+ if (Array.isArray(input.triggers) && input.triggers.length > 0) return input.triggers;
557
+ if (input.trigger && typeof input.trigger === 'object') return [input.trigger];
558
+ if (input.schedule) {
559
+ return [{
560
+ kind: 'scheduled',
561
+ name: input.name,
562
+ description: input.description,
563
+ schedule: input.schedule,
564
+ enabled: input.enabled,
565
+ projectPath: input.projectPath,
566
+ }];
567
+ }
568
+ return [{
569
+ kind: 'event',
570
+ name: input.name,
571
+ description: input.description,
572
+ source: input.source,
573
+ event: input.event,
574
+ sourceUrl: input.sourceUrl,
575
+ enabled: input.enabled,
576
+ projectPath: input.projectPath,
577
+ }];
578
+ }
579
+
580
+ function insertAutomationBundle(db, input = {}) {
581
+ const workflowInput = {
582
+ ...(isObject(input.workflow) ? input.workflow : {}),
583
+ name: input.workflowName || input.name,
584
+ description: input.workflowDescription ?? input.description,
585
+ enabled: input.workflowEnabled ?? input.enabled,
586
+ projectPath: input.workflowProjectPath ?? input.projectPath,
587
+ workflowText: input.workflowText || (typeof input.workflow === 'string' ? input.workflow : input.workflow?.workflowText),
588
+ allowlist: input.allowlist || input.workflow?.allowlist,
589
+ limits: input.limits || input.workflow?.limits,
590
+ };
591
+ const workflow = normalizeWorkflowRecord(workflowInput);
592
+ if (workflow.compilerErrors?.length) throw new Error(workflow.compilerErrors.join('\n'));
593
+
594
+ const automation = normalizeAutomationRecord(input, workflow);
595
+ automation.workflow = workflow;
596
+ const triggerInputs = defaultTriggersForInput(input, automation);
597
+ const triggers = triggerInputs.map((triggerInput) => normalizeTriggerRecord(triggerInput, automation));
598
+ if (triggers.length === 0) throw new Error('Automation requires at least one trigger.');
599
+
600
+ upsertWorkflowRow(db, workflow);
601
+ upsertAutomationRow(db, automation);
602
+ for (const trigger of triggers) upsertTriggerRow(db, trigger);
603
+ return composeAutomation(automation, triggers, workflow);
604
+ }
605
+
606
+ function createAutomation(input = {}, options = {}) {
607
+ ensureAutomationsStore();
608
+ let automation = null;
609
+ const db = openLocalDb();
610
+ db.transaction(() => {
611
+ automation = insertAutomationBundle(db, input);
612
+ })();
613
+ publishResources(options.source || 'automations:create');
614
+ return automation;
615
+ }
616
+
617
+ function getAutomation(automationId) {
618
+ const id = cleanString(automationId);
619
+ if (!id) return null;
620
+ return readAutomationGraph().automations.find((automation) => automation.id === id) || null;
621
+ }
622
+
623
+ function getAutomationByTriggerId(triggerId) {
624
+ const id = cleanString(triggerId);
625
+ if (!id) return null;
626
+ return readAutomationGraph().automations.find((automation) => (
627
+ automation.triggers.some((trigger) => trigger.id === id)
628
+ )) || null;
629
+ }
630
+
631
+ function updateAutomation(automationId, updates = {}, options = {}) {
632
+ ensureAutomationsStore();
633
+ const existing = getAutomation(automationId);
634
+ if (!existing) throw new Error(`Automation not found: ${automationId}`);
635
+ let saved = null;
636
+ const db = openLocalDb();
637
+
638
+ db.transaction(() => {
639
+ const workflow = normalizeWorkflowRecord({
640
+ ...existing.workflow,
641
+ ...(isObject(updates.workflow) ? updates.workflow : {}),
642
+ ...(updates.workflowText !== undefined ? { workflowText: updates.workflowText } : {}),
643
+ ...(updates.workflow !== undefined && typeof updates.workflow === 'string' ? { workflowText: updates.workflow } : {}),
644
+ ...(updates.workflowName !== undefined ? { name: updates.workflowName } : {}),
645
+ ...(updates.allowlist !== undefined ? { allowlist: updates.allowlist } : {}),
646
+ ...(updates.limits !== undefined ? { limits: updates.limits } : {}),
647
+ }, existing.workflow);
648
+ if (workflow.compilerErrors?.length) throw new Error(workflow.compilerErrors.join('\n'));
649
+
650
+ const base = normalizeAutomationRecord({
651
+ ...existing,
652
+ ...updates,
653
+ id: existing.id,
654
+ }, workflow, existing);
655
+ base.workflow = workflow;
656
+ upsertWorkflowRow(db, workflow);
657
+ upsertAutomationRow(db, base);
658
+
659
+ let triggers = existing.triggers;
660
+ if (Array.isArray(updates.triggers)) {
661
+ db.prepare('DELETE FROM automation_triggers WHERE automation_id = ?').run(existing.id);
662
+ triggers = updates.triggers.map((triggerInput) => normalizeTriggerRecord(triggerInput, base));
663
+ for (const trigger of triggers) upsertTriggerRow(db, trigger);
664
+ } else if (isObject(updates.trigger) || updates.trigger_id || updates.triggerId) {
665
+ const triggerId = updates.trigger_id || updates.triggerId || updates.trigger?.id;
666
+ triggers = existing.triggers.map((trigger) => {
667
+ if (trigger.id !== triggerId) return trigger;
668
+ const next = normalizeTriggerRecord({
669
+ ...trigger,
670
+ ...(updates.trigger || {}),
671
+ id: trigger.id,
672
+ }, base, trigger);
673
+ upsertTriggerRow(db, next);
674
+ return next;
675
+ });
676
+ }
677
+
678
+ saved = composeAutomation(base, triggers, workflow);
679
+ })();
680
+
681
+ publishResources(options.source || 'automations:update');
682
+ return saved;
683
+ }
684
+
685
+ function updateAutomationByTriggerId(triggerId, updates = {}, options = {}) {
686
+ const automation = getAutomationByTriggerId(triggerId);
687
+ if (!automation) throw new Error(`Trigger not found: ${triggerId}`);
688
+ const {
689
+ trigger_id: _trigger_id,
690
+ triggerId: _triggerId,
691
+ id: _id,
692
+ kind,
693
+ name,
694
+ description,
695
+ enabled,
696
+ schedule,
697
+ source,
698
+ event,
699
+ eventName,
700
+ sourceUrl,
701
+ projectPath,
702
+ nextRunAt,
703
+ webhookUrl,
704
+ secret,
705
+ ...automationUpdates
706
+ } = updates || {};
707
+ const triggerUpdates = isObject(updates.trigger) ? updates.trigger : {};
708
+ return updateAutomation(automation.id, {
709
+ ...automationUpdates,
710
+ trigger: {
711
+ ...triggerUpdates,
712
+ id: triggerId,
713
+ kind: triggerUpdates.kind ?? kind,
714
+ name: triggerUpdates.name ?? name,
715
+ description: triggerUpdates.description ?? description,
716
+ enabled: triggerUpdates.enabled ?? enabled,
717
+ schedule: triggerUpdates.schedule ?? schedule,
718
+ source: triggerUpdates.source ?? source,
719
+ event: triggerUpdates.event ?? event,
720
+ eventName: triggerUpdates.eventName ?? eventName,
721
+ sourceUrl: triggerUpdates.sourceUrl ?? sourceUrl,
722
+ projectPath: triggerUpdates.projectPath ?? projectPath,
723
+ nextRunAt: triggerUpdates.nextRunAt ?? nextRunAt,
724
+ webhookUrl: triggerUpdates.webhookUrl ?? webhookUrl,
725
+ secret: triggerUpdates.secret ?? secret,
726
+ },
727
+ }, options);
728
+ }
729
+
730
+ function deleteAutomation(automationId, options = {}) {
731
+ ensureAutomationsStore();
732
+ const existing = getAutomation(automationId);
733
+ if (!existing) throw new Error(`Automation not found: ${automationId}`);
734
+ const db = openLocalDb();
735
+ db.transaction(() => {
736
+ db.prepare('DELETE FROM automation_definitions WHERE id = ?').run(existing.id);
737
+ db.prepare(`
738
+ DELETE FROM automation_workflows
739
+ WHERE id = ?
740
+ AND NOT EXISTS (
741
+ SELECT 1 FROM automation_definitions WHERE workflow_id = automation_workflows.id
742
+ )
743
+ `).run(existing.workflowId);
744
+ })();
745
+ publishResources(options.source || 'automations:delete');
746
+ return existing;
747
+ }
748
+
749
+ function deleteAutomationByTriggerId(triggerId, options = {}) {
750
+ const automation = getAutomationByTriggerId(triggerId);
751
+ if (!automation) throw new Error(`Trigger not found: ${triggerId}`);
752
+ return deleteAutomation(automation.id, options);
753
+ }
754
+
755
+ function listAutomations() {
756
+ return readAutomationGraph().automations;
757
+ }
758
+
759
+ function listTriggers() {
760
+ return readAutomationGraph().triggers;
761
+ }
762
+
763
+ function listWorkflows() {
764
+ return readAutomationGraph().workflows;
765
+ }
766
+
767
+ function listEventTriggersForIngress() {
768
+ return listTriggers().filter((trigger) => trigger.kind === 'event' && trigger.enabled !== false);
769
+ }
770
+
771
+ function markTriggerFired(triggerId, options = {}) {
772
+ ensureAutomationsStore();
773
+ const automation = getAutomationByTriggerId(triggerId);
774
+ if (!automation) return null;
775
+ const trigger = automation.triggers.find((item) => item.id === triggerId);
776
+ if (!trigger) return null;
777
+ const timestamp = options.firedAt || nowIso();
778
+ const next = {
779
+ ...trigger,
780
+ lastFiredAt: timestamp,
781
+ updatedAt: timestamp,
782
+ };
783
+ if (next.kind === 'scheduled') {
784
+ if (next.schedule?.kind === 'once') next.enabled = false;
785
+ next.nextRunAt = next.enabled === false ? null : computeNextRunAt(next, timestamp);
786
+ }
787
+ openLocalDb().transaction(() => {
788
+ upsertTriggerRow(openLocalDb(), next);
789
+ })();
790
+ publishResources(options.source || 'automations:trigger-fired');
791
+ return next;
792
+ }
793
+
794
+ function claimDueScheduledTriggers(now = new Date(), options = {}) {
795
+ ensureAutomationsStore();
796
+ const db = openLocalDb();
797
+ const nowDate = validDate(now) || new Date();
798
+ const timestamp = nowDate.toISOString();
799
+ const rows = db.prepare(`
800
+ SELECT automation_triggers.*
801
+ FROM automation_triggers
802
+ INNER JOIN automation_definitions
803
+ ON automation_definitions.id = automation_triggers.automation_id
804
+ INNER JOIN automation_workflows
805
+ ON automation_workflows.id = automation_definitions.workflow_id
806
+ WHERE automation_triggers.kind = 'scheduled'
807
+ AND automation_triggers.enabled = 1
808
+ AND automation_definitions.enabled = 1
809
+ AND automation_workflows.enabled = 1
810
+ AND automation_triggers.next_run_at IS NOT NULL
811
+ AND automation_triggers.next_run_at <= ?
812
+ ORDER BY datetime(automation_triggers.next_run_at) ASC
813
+ `).all(timestamp);
814
+
815
+ const claims = [];
816
+ db.transaction(() => {
817
+ for (const row of rows) {
818
+ const automation = getAutomation(row.automation_id);
819
+ if (!automation) continue;
820
+ const trigger = automation.triggers.find((item) => item.id === row.id);
821
+ if (!trigger) continue;
822
+ const fired = {
823
+ ...trigger,
824
+ lastFiredAt: timestamp,
825
+ updatedAt: timestamp,
826
+ };
827
+ if (fired.schedule?.kind === 'once') fired.enabled = false;
828
+ fired.nextRunAt = fired.enabled === false ? null : computeNextRunAt(fired, nowDate);
829
+ upsertTriggerRow(db, fired);
830
+ claims.push({
831
+ automation: composeAutomation(
832
+ automation,
833
+ automation.triggers.map((item) => (item.id === fired.id ? fired : item)),
834
+ automation.workflow,
835
+ ),
836
+ trigger: fired,
837
+ scheduledFor: row.next_run_at,
838
+ });
839
+ }
840
+ })();
841
+
842
+ if (claims.length > 0) publishResources(options.source || 'automations:scheduler-claim');
843
+ return claims;
844
+ }
845
+
846
+ function rowToAutomationRun(row) {
847
+ if (!row) return null;
848
+ const parsed = parseJson(row.run_json, {});
849
+ return {
850
+ ...parsed,
851
+ id: row.id,
852
+ runId: row.id,
853
+ automationId: row.automation_id,
854
+ triggerId: row.trigger_id || parsed.triggerId || null,
855
+ workflowId: row.workflow_id || parsed.workflowId || null,
856
+ status: row.status,
857
+ startedAt: row.started_at || parsed.startedAt || null,
858
+ finishedAt: row.finished_at || parsed.finishedAt || null,
859
+ projectPath: row.project_path || parsed.projectPath || null,
860
+ error: row.error || parsed.error || null,
861
+ createdAt: row.created_at,
862
+ updatedAt: row.updated_at,
863
+ };
864
+ }
865
+
866
+ function automationRunParams(run) {
867
+ const timestamp = nowIso();
868
+ const createdAt = run.createdAt || timestamp;
869
+ const updatedAt = run.updatedAt || timestamp;
870
+ const runJson = {
871
+ ...run,
872
+ id: run.id,
873
+ runId: run.id,
874
+ automationId: run.automationId,
875
+ triggerId: run.triggerId || null,
876
+ workflowId: run.workflowId || null,
877
+ status: run.status || 'running',
878
+ startedAt: run.startedAt || null,
879
+ finishedAt: run.finishedAt || null,
880
+ projectPath: run.projectPath || null,
881
+ error: run.error || null,
882
+ createdAt,
883
+ updatedAt,
884
+ };
885
+ return {
886
+ id: run.id,
887
+ automation_id: run.automationId,
888
+ trigger_id: run.triggerId || null,
889
+ workflow_id: run.workflowId || null,
890
+ status: run.status || 'running',
891
+ started_at: run.startedAt || null,
892
+ finished_at: run.finishedAt || null,
893
+ project_path: run.projectPath || null,
894
+ error: run.error || null,
895
+ run_json: JSON.stringify(runJson),
896
+ created_at: createdAt,
897
+ updated_at: updatedAt,
898
+ };
899
+ }
900
+
901
+ function upsertAutomationRunRow(db, run) {
902
+ const params = automationRunParams(run);
903
+ db.prepare(`
904
+ INSERT INTO automation_runs (
905
+ id, automation_id, trigger_id, workflow_id, status, started_at,
906
+ finished_at, project_path, error, run_json, created_at, updated_at
907
+ )
908
+ VALUES (
909
+ @id, @automation_id, @trigger_id, @workflow_id, @status, @started_at,
910
+ @finished_at, @project_path, @error, @run_json, @created_at, @updated_at
911
+ )
912
+ ON CONFLICT(id) DO UPDATE SET
913
+ automation_id = excluded.automation_id,
914
+ trigger_id = excluded.trigger_id,
915
+ workflow_id = excluded.workflow_id,
916
+ status = excluded.status,
917
+ started_at = excluded.started_at,
918
+ finished_at = excluded.finished_at,
919
+ project_path = excluded.project_path,
920
+ error = excluded.error,
921
+ run_json = excluded.run_json,
922
+ updated_at = excluded.updated_at
923
+ `).run(params);
924
+ }
925
+
926
+ function recordAutomationRun(automationId, entry, options = {}) {
927
+ if (!automationId || (!entry?.runId && !entry?.id)) return null;
928
+ ensureAutomationsStore();
929
+ const db = openLocalDb();
930
+ const runId = entry.runId || entry.id;
931
+ let event = null;
932
+ let run = null;
933
+ db.transaction(() => {
934
+ const existing = rowToAutomationRun(db.prepare('SELECT * FROM automation_runs WHERE id = ?').get(runId));
935
+ const timestamp = nowIso();
936
+ const events = Array.isArray(existing?.events) ? existing.events.slice(-99) : [];
937
+ events.push({ ...entry, recordedAt: timestamp });
938
+ run = {
939
+ ...(existing || {}),
940
+ id: runId,
941
+ automationId,
942
+ triggerId: entry.triggerId ?? existing?.triggerId ?? null,
943
+ workflowId: entry.workflowId ?? existing?.workflowId ?? null,
944
+ automationName: entry.automationName ?? existing?.automationName ?? null,
945
+ triggerName: entry.triggerName ?? existing?.triggerName ?? null,
946
+ workflowName: entry.workflowName ?? existing?.workflowName ?? null,
947
+ status: entry.status || existing?.status || 'running',
948
+ startedAt: entry.startedAt || existing?.startedAt || timestamp,
949
+ finishedAt: entry.finishedAt || existing?.finishedAt || null,
950
+ projectPath: entry.projectPath ?? existing?.projectPath ?? null,
951
+ error: entry.error || existing?.error || null,
952
+ output: entry.output !== undefined ? entry.output : existing?.output,
953
+ events,
954
+ createdAt: existing?.createdAt || timestamp,
955
+ updatedAt: timestamp,
956
+ };
957
+ upsertAutomationRunRow(db, run);
958
+ event = insertStateEvent(db, {
959
+ resource: 'automation_runs',
960
+ op: existing ? 'update' : 'insert',
961
+ id: run.id,
962
+ value: run,
963
+ source: options.source || 'automation_runs:record',
964
+ });
965
+ })();
966
+ publishStateEvent(event);
967
+ return run;
968
+ }
969
+
970
+ function listAutomationRuns(options = {}) {
971
+ ensureAutomationsStore();
972
+ const limit = Math.max(1, Math.min(Number(options.limit) || DEFAULT_RUN_LIMIT, 1000));
973
+ const db = openLocalDb();
974
+ const rows = options.automationId
975
+ ? db.prepare(`
976
+ SELECT * FROM automation_runs
977
+ WHERE automation_id = ?
978
+ ORDER BY datetime(updated_at) DESC
979
+ LIMIT ?
980
+ `).all(options.automationId, limit)
981
+ : db.prepare(`
982
+ SELECT * FROM automation_runs
983
+ ORDER BY datetime(updated_at) DESC
984
+ LIMIT ?
985
+ `).all(limit);
986
+ return rows.map(rowToAutomationRun).filter(Boolean);
987
+ }
988
+
989
+ function recordWorkflowCellRun(run, cell, options = {}) {
990
+ if (!run?.id || !cell?.name) return null;
991
+ ensureAutomationsStore();
992
+ const db = openLocalDb();
993
+ const timestamp = nowIso();
994
+ const cellRun = {
995
+ id: `${run.id}:${cell.name}`,
996
+ runId: run.id,
997
+ automationId: run.automationId,
998
+ triggerId: run.triggerId || null,
999
+ workflowId: run.workflowId || null,
1000
+ cellName: cell.name,
1001
+ cellKind: cell.kind || 'code',
1002
+ status: cell.status || 'running',
1003
+ startedAt: cell.startedAt || timestamp,
1004
+ finishedAt: cell.finishedAt || null,
1005
+ output: cell.output,
1006
+ error: cell.error || null,
1007
+ createdAt: cell.createdAt || timestamp,
1008
+ updatedAt: timestamp,
1009
+ };
1010
+ let event = null;
1011
+ db.transaction(() => {
1012
+ db.prepare(`
1013
+ INSERT INTO workflow_cell_runs (
1014
+ id, run_id, automation_id, trigger_id, workflow_id, cell_name,
1015
+ cell_kind, status, started_at, finished_at, output_json, error,
1016
+ cell_json, created_at, updated_at
1017
+ )
1018
+ VALUES (
1019
+ @id, @run_id, @automation_id, @trigger_id, @workflow_id, @cell_name,
1020
+ @cell_kind, @status, @started_at, @finished_at, @output_json, @error,
1021
+ @cell_json, @created_at, @updated_at
1022
+ )
1023
+ ON CONFLICT(id) DO UPDATE SET
1024
+ status = excluded.status,
1025
+ finished_at = excluded.finished_at,
1026
+ output_json = excluded.output_json,
1027
+ error = excluded.error,
1028
+ cell_json = excluded.cell_json,
1029
+ updated_at = excluded.updated_at
1030
+ `).run({
1031
+ id: cellRun.id,
1032
+ run_id: cellRun.runId,
1033
+ automation_id: cellRun.automationId,
1034
+ trigger_id: cellRun.triggerId,
1035
+ workflow_id: cellRun.workflowId,
1036
+ cell_name: cellRun.cellName,
1037
+ cell_kind: cellRun.cellKind,
1038
+ status: cellRun.status,
1039
+ started_at: cellRun.startedAt,
1040
+ finished_at: cellRun.finishedAt,
1041
+ output_json: cellRun.output === undefined ? null : JSON.stringify(cellRun.output),
1042
+ error: cellRun.error,
1043
+ cell_json: JSON.stringify(cellRun),
1044
+ created_at: cellRun.createdAt,
1045
+ updated_at: cellRun.updatedAt,
1046
+ });
1047
+ event = insertStateEvent(db, {
1048
+ resource: 'workflow_cell_runs',
1049
+ op: 'update',
1050
+ id: cellRun.id,
1051
+ value: cellRun,
1052
+ source: options.source || 'workflow_cell_runs:record',
1053
+ });
1054
+ })();
1055
+ publishStateEvent(event);
1056
+ return cellRun;
1057
+ }
1058
+
1059
+ function listWorkflowCellRuns(options = {}) {
1060
+ ensureAutomationsStore();
1061
+ const limit = Math.max(1, Math.min(Number(options.limit) || DEFAULT_RUN_LIMIT, 1000));
1062
+ const db = openLocalDb();
1063
+ const rows = options.runId
1064
+ ? db.prepare(`
1065
+ SELECT * FROM workflow_cell_runs
1066
+ WHERE run_id = ?
1067
+ ORDER BY datetime(created_at) ASC
1068
+ LIMIT ?
1069
+ `).all(options.runId, limit)
1070
+ : db.prepare(`
1071
+ SELECT * FROM workflow_cell_runs
1072
+ ORDER BY datetime(updated_at) DESC
1073
+ LIMIT ?
1074
+ `).all(limit);
1075
+ return rows.map((row) => parseJson(row.cell_json, null)).filter(Boolean);
1076
+ }
1077
+
1078
+ function migrateLegacyAutomations(db) {
1079
+ if (metaValue(db, LEGACY_MIGRATION_KEY)) return;
1080
+ setMetaValue(db, LEGACY_MIGRATION_KEY, nowIso());
1081
+ }
1082
+
1083
+ let ensuring = false;
1084
+ let ensured = false;
1085
+
1086
+ function ensureAutomationsStore() {
1087
+ if (ensured || ensuring) return;
1088
+ ensuring = true;
1089
+ try {
1090
+ const db = openLocalDb();
1091
+ migrateLegacyAutomations(db);
1092
+ ensured = true;
1093
+ } finally {
1094
+ ensuring = false;
1095
+ }
1096
+ }
1097
+
1098
+ function eventRefForTrigger(trigger) {
1099
+ return splitEventRef(`${trigger?.source || '*'}.${trigger?.event || '*'}`);
1100
+ }
1101
+
1102
+ module.exports = {
1103
+ claimDueScheduledTriggers,
1104
+ createAutomation,
1105
+ defaultWorkflowText,
1106
+ deleteAutomation,
1107
+ deleteAutomationByTriggerId,
1108
+ ensureAutomationsStore,
1109
+ eventRefForTrigger,
1110
+ getAutomation,
1111
+ getAutomationByTriggerId,
1112
+ listAutomationRuns,
1113
+ listAutomations,
1114
+ listEventTriggersForIngress,
1115
+ listTriggers,
1116
+ listWorkflowCellRuns,
1117
+ listWorkflows,
1118
+ markTriggerFired,
1119
+ normalizeWorkflowRecord,
1120
+ publishResources,
1121
+ recordAutomationRun,
1122
+ recordWorkflowCellRun,
1123
+ updateAutomation,
1124
+ updateAutomationByTriggerId,
1125
+ };