chainlesschain 0.51.0 → 0.81.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 (70) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
  4. package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
  5. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
  6. package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
  7. package/src/assets/web-panel/index.html +2 -2
  8. package/src/commands/a2a.js +380 -0
  9. package/src/commands/agent-network.js +785 -0
  10. package/src/commands/automation.js +654 -0
  11. package/src/commands/bi.js +348 -0
  12. package/src/commands/crosschain.js +218 -0
  13. package/src/commands/dao.js +565 -0
  14. package/src/commands/did-v2.js +620 -0
  15. package/src/commands/dlp.js +341 -0
  16. package/src/commands/economy.js +578 -0
  17. package/src/commands/evolution.js +391 -0
  18. package/src/commands/evomap.js +394 -0
  19. package/src/commands/federation.js +283 -0
  20. package/src/commands/hmemory.js +442 -0
  21. package/src/commands/inference.js +318 -0
  22. package/src/commands/lowcode.js +356 -0
  23. package/src/commands/marketplace.js +256 -0
  24. package/src/commands/perf.js +433 -0
  25. package/src/commands/pipeline.js +449 -0
  26. package/src/commands/plugin-ecosystem.js +517 -0
  27. package/src/commands/privacy.js +321 -0
  28. package/src/commands/reputation.js +261 -0
  29. package/src/commands/sandbox.js +401 -0
  30. package/src/commands/siem.js +246 -0
  31. package/src/commands/sla.js +259 -0
  32. package/src/commands/social.js +311 -0
  33. package/src/commands/sso.js +798 -0
  34. package/src/commands/stress.js +230 -0
  35. package/src/commands/terraform.js +245 -0
  36. package/src/commands/workflow.js +320 -0
  37. package/src/commands/zkp.js +562 -1
  38. package/src/index.js +21 -0
  39. package/src/lib/a2a-protocol.js +451 -0
  40. package/src/lib/agent-economy.js +479 -0
  41. package/src/lib/agent-network.js +1121 -0
  42. package/src/lib/app-builder.js +239 -0
  43. package/src/lib/automation-engine.js +948 -0
  44. package/src/lib/bi-engine.js +338 -0
  45. package/src/lib/cross-chain.js +345 -0
  46. package/src/lib/dao-governance.js +569 -0
  47. package/src/lib/did-v2-manager.js +1127 -0
  48. package/src/lib/dlp-engine.js +389 -0
  49. package/src/lib/evolution-system.js +453 -0
  50. package/src/lib/evomap-federation.js +177 -0
  51. package/src/lib/evomap-governance.js +276 -0
  52. package/src/lib/federation-hardening.js +259 -0
  53. package/src/lib/hierarchical-memory.js +481 -0
  54. package/src/lib/inference-network.js +330 -0
  55. package/src/lib/perf-tuning.js +734 -0
  56. package/src/lib/pipeline-orchestrator.js +928 -0
  57. package/src/lib/plugin-ecosystem.js +1109 -0
  58. package/src/lib/privacy-computing.js +427 -0
  59. package/src/lib/reputation-optimizer.js +299 -0
  60. package/src/lib/sandbox-v2.js +306 -0
  61. package/src/lib/siem-exporter.js +333 -0
  62. package/src/lib/skill-marketplace.js +325 -0
  63. package/src/lib/sla-manager.js +275 -0
  64. package/src/lib/social-graph-analytics.js +707 -0
  65. package/src/lib/sso-manager.js +841 -0
  66. package/src/lib/stress-tester.js +330 -0
  67. package/src/lib/terraform-manager.js +363 -0
  68. package/src/lib/workflow-engine.js +454 -1
  69. package/src/lib/zkp-engine.js +523 -20
  70. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
@@ -0,0 +1,948 @@
1
+ /**
2
+ * Automation Engine — CLI port of Phase 96 工作流自动化引擎.
3
+ *
4
+ * 12 built-in SaaS connectors (Gmail/Slack/GitHub/Jira/Notion/Trello/Discord/
5
+ * Teams/Airtable/Figma/Linear/Confluence) + 5 trigger types
6
+ * (webhook/schedule/event/condition/manual) + simulated flow execution.
7
+ *
8
+ * CLI is record-keeping + simulator only: no real API calls, no credential
9
+ * storage, no outbound HTTP. Each node execution produces a mock output
10
+ * shape derived from connector metadata so downstream nodes can chain.
11
+ */
12
+
13
+ import crypto from "crypto";
14
+
15
+ /* ── Constants ─────────────────────────────────────────────── */
16
+
17
+ export const FLOW_STATUS = Object.freeze({
18
+ DRAFT: "draft",
19
+ ACTIVE: "active",
20
+ PAUSED: "paused",
21
+ ARCHIVED: "archived",
22
+ });
23
+
24
+ export const EXECUTION_STATUS = Object.freeze({
25
+ RUNNING: "running",
26
+ SUCCESS: "success",
27
+ FAILED: "failed",
28
+ CANCELLED: "cancelled",
29
+ });
30
+
31
+ export const TRIGGER_TYPE = Object.freeze({
32
+ WEBHOOK: "webhook",
33
+ SCHEDULE: "schedule",
34
+ EVENT: "event",
35
+ CONDITION: "condition",
36
+ MANUAL: "manual",
37
+ });
38
+
39
+ export const NODE_TYPE = Object.freeze({
40
+ ACTION: "action",
41
+ CONDITION: "condition",
42
+ PARALLEL: "parallel",
43
+ LOOP: "loop",
44
+ });
45
+
46
+ /* ── Built-in connector catalog ────────────────────────────── */
47
+
48
+ export const CONNECTOR_CATALOG = Object.freeze([
49
+ {
50
+ id: "gmail",
51
+ displayName: "Gmail",
52
+ category: "email",
53
+ actions: ["send", "search", "markRead", "archive"],
54
+ },
55
+ {
56
+ id: "slack",
57
+ displayName: "Slack",
58
+ category: "messaging",
59
+ actions: ["postMessage", "createChannel", "listEvents"],
60
+ },
61
+ {
62
+ id: "github",
63
+ displayName: "GitHub",
64
+ category: "dev",
65
+ actions: ["createIssue", "createPR", "addWebhook", "createRelease"],
66
+ },
67
+ {
68
+ id: "jira",
69
+ displayName: "Jira",
70
+ category: "project",
71
+ actions: ["createIssue", "updateStatus", "jql"],
72
+ },
73
+ {
74
+ id: "notion",
75
+ displayName: "Notion",
76
+ category: "knowledge",
77
+ actions: ["createPage", "updateDatabase", "query"],
78
+ },
79
+ {
80
+ id: "trello",
81
+ displayName: "Trello",
82
+ category: "kanban",
83
+ actions: ["createCard", "moveCard", "addComment"],
84
+ },
85
+ {
86
+ id: "discord",
87
+ displayName: "Discord",
88
+ category: "community",
89
+ actions: ["postMessage", "manageChannel", "botReply"],
90
+ },
91
+ {
92
+ id: "teams",
93
+ displayName: "Teams",
94
+ category: "collab",
95
+ actions: ["postMessage", "createMeeting", "shareFile"],
96
+ },
97
+ {
98
+ id: "airtable",
99
+ displayName: "Airtable",
100
+ category: "data",
101
+ actions: ["listRecords", "createRecord", "updateRecord", "deleteRecord"],
102
+ },
103
+ {
104
+ id: "figma",
105
+ displayName: "Figma",
106
+ category: "design",
107
+ actions: ["exportAsset", "listenChange", "addComment"],
108
+ },
109
+ {
110
+ id: "linear",
111
+ displayName: "Linear",
112
+ category: "project",
113
+ actions: ["createIssue", "updateStatus", "query"],
114
+ },
115
+ {
116
+ id: "confluence",
117
+ displayName: "Confluence",
118
+ category: "docs",
119
+ actions: ["createPage", "update", "search"],
120
+ },
121
+ ]);
122
+
123
+ const CONNECTOR_INDEX = new Map(CONNECTOR_CATALOG.map((c) => [c.id, c]));
124
+
125
+ export function listConnectors() {
126
+ return CONNECTOR_CATALOG.map((c) => ({ ...c, actions: [...c.actions] }));
127
+ }
128
+
129
+ export function getConnector(id) {
130
+ const c = CONNECTOR_INDEX.get(id);
131
+ return c ? { ...c, actions: [...c.actions] } : null;
132
+ }
133
+
134
+ /* ── Built-in flow templates ───────────────────────────────── */
135
+
136
+ export const FLOW_TEMPLATES = Object.freeze([
137
+ {
138
+ id: "github-issue-to-slack",
139
+ name: "GitHub Issue → Slack notify",
140
+ description: "Post to #dev when a GitHub issue is opened",
141
+ nodes: [
142
+ {
143
+ id: "n1",
144
+ type: "action",
145
+ connector: "github",
146
+ action: "addWebhook",
147
+ params: { events: ["issues"] },
148
+ },
149
+ {
150
+ id: "n2",
151
+ type: "action",
152
+ connector: "slack",
153
+ action: "postMessage",
154
+ params: { channel: "#dev" },
155
+ },
156
+ ],
157
+ edges: [{ from: "n1", to: "n2" }],
158
+ },
159
+ {
160
+ id: "daily-standup-digest",
161
+ name: "Daily standup digest",
162
+ description: "9:00 AM daily — pull Jira tickets and email team",
163
+ nodes: [
164
+ {
165
+ id: "n1",
166
+ type: "action",
167
+ connector: "jira",
168
+ action: "jql",
169
+ params: { jql: "assignee = currentUser()" },
170
+ },
171
+ {
172
+ id: "n2",
173
+ type: "action",
174
+ connector: "gmail",
175
+ action: "send",
176
+ params: { subject: "Daily standup" },
177
+ },
178
+ ],
179
+ edges: [{ from: "n1", to: "n2" }],
180
+ },
181
+ {
182
+ id: "figma-to-notion",
183
+ name: "Figma export → Notion page",
184
+ description:
185
+ "When a Figma file changes, export the latest asset to a Notion page",
186
+ nodes: [
187
+ {
188
+ id: "n1",
189
+ type: "action",
190
+ connector: "figma",
191
+ action: "listenChange",
192
+ params: {},
193
+ },
194
+ {
195
+ id: "n2",
196
+ type: "action",
197
+ connector: "figma",
198
+ action: "exportAsset",
199
+ params: { format: "png" },
200
+ },
201
+ {
202
+ id: "n3",
203
+ type: "action",
204
+ connector: "notion",
205
+ action: "createPage",
206
+ params: { database: "design-assets" },
207
+ },
208
+ ],
209
+ edges: [
210
+ { from: "n1", to: "n2" },
211
+ { from: "n2", to: "n3" },
212
+ ],
213
+ },
214
+ {
215
+ id: "error-rate-alert",
216
+ name: "Error-rate alert",
217
+ description: "When error rate > 5% fire Slack + Teams alert",
218
+ nodes: [
219
+ {
220
+ id: "n1",
221
+ type: "condition",
222
+ expression: "ctx.errorRate > 0.05",
223
+ },
224
+ {
225
+ id: "n2",
226
+ type: "action",
227
+ connector: "slack",
228
+ action: "postMessage",
229
+ params: { channel: "#ops" },
230
+ },
231
+ {
232
+ id: "n3",
233
+ type: "action",
234
+ connector: "teams",
235
+ action: "postMessage",
236
+ params: { channel: "ops" },
237
+ },
238
+ ],
239
+ edges: [
240
+ { from: "n1", to: "n2" },
241
+ { from: "n1", to: "n3" },
242
+ ],
243
+ },
244
+ ]);
245
+
246
+ const TEMPLATE_INDEX = new Map(FLOW_TEMPLATES.map((t) => [t.id, t]));
247
+
248
+ export function listFlowTemplates() {
249
+ return FLOW_TEMPLATES.map((t) => ({
250
+ ...t,
251
+ nodes: t.nodes.map((n) => ({ ...n })),
252
+ edges: t.edges.map((e) => ({ ...e })),
253
+ }));
254
+ }
255
+
256
+ export function getFlowTemplate(id) {
257
+ const t = TEMPLATE_INDEX.get(id);
258
+ if (!t) return null;
259
+ return {
260
+ ...t,
261
+ nodes: t.nodes.map((n) => ({ ...n })),
262
+ edges: t.edges.map((e) => ({ ...e })),
263
+ };
264
+ }
265
+
266
+ /* ── Schema ────────────────────────────────────────────────── */
267
+
268
+ export function ensureAutomationTables(db) {
269
+ db.exec(`
270
+ CREATE TABLE IF NOT EXISTS auto_flows (
271
+ id TEXT PRIMARY KEY,
272
+ name TEXT NOT NULL,
273
+ description TEXT,
274
+ nodes TEXT,
275
+ edges TEXT,
276
+ status TEXT DEFAULT 'draft',
277
+ schedule TEXT,
278
+ created_by TEXT,
279
+ shared_with TEXT DEFAULT '[]',
280
+ created_at TEXT DEFAULT (datetime('now')),
281
+ updated_at TEXT DEFAULT (datetime('now'))
282
+ )
283
+ `);
284
+ db.exec(`
285
+ CREATE TABLE IF NOT EXISTS auto_executions (
286
+ id TEXT PRIMARY KEY,
287
+ flow_id TEXT NOT NULL,
288
+ trigger_type TEXT,
289
+ input_data TEXT,
290
+ output_data TEXT,
291
+ status TEXT DEFAULT 'running',
292
+ steps_log TEXT DEFAULT '[]',
293
+ duration_ms INTEGER DEFAULT 0,
294
+ error TEXT,
295
+ test_mode INTEGER DEFAULT 0,
296
+ started_at TEXT DEFAULT (datetime('now')),
297
+ completed_at TEXT
298
+ )
299
+ `);
300
+ db.exec(`
301
+ CREATE TABLE IF NOT EXISTS auto_triggers (
302
+ id TEXT PRIMARY KEY,
303
+ flow_id TEXT NOT NULL,
304
+ type TEXT NOT NULL,
305
+ config TEXT,
306
+ enabled INTEGER DEFAULT 1,
307
+ last_triggered_at TEXT,
308
+ trigger_count INTEGER DEFAULT 0,
309
+ created_at TEXT DEFAULT (datetime('now'))
310
+ )
311
+ `);
312
+ }
313
+
314
+ /* ── Helpers ───────────────────────────────────────────────── */
315
+
316
+ function _genId(prefix) {
317
+ return `${prefix}-${crypto.randomBytes(6).toString("hex")}`;
318
+ }
319
+
320
+ function _now() {
321
+ return new Date().toISOString();
322
+ }
323
+
324
+ function _parseJSON(value, fallback) {
325
+ if (value == null) return fallback;
326
+ if (typeof value !== "string") return value;
327
+ try {
328
+ return JSON.parse(value);
329
+ } catch (_e) {
330
+ return fallback;
331
+ }
332
+ }
333
+
334
+ function _rowToFlow(row) {
335
+ if (!row) return null;
336
+ return {
337
+ id: row.id,
338
+ name: row.name,
339
+ description: row.description || "",
340
+ nodes: _parseJSON(row.nodes, []),
341
+ edges: _parseJSON(row.edges, []),
342
+ status: row.status,
343
+ schedule: row.schedule || null,
344
+ createdBy: row.created_by || null,
345
+ sharedWith: _parseJSON(row.shared_with, []),
346
+ createdAt: row.created_at,
347
+ updatedAt: row.updated_at,
348
+ };
349
+ }
350
+
351
+ function _rowToExecution(row) {
352
+ if (!row) return null;
353
+ return {
354
+ id: row.id,
355
+ flowId: row.flow_id,
356
+ triggerType: row.trigger_type,
357
+ inputData: _parseJSON(row.input_data, null),
358
+ outputData: _parseJSON(row.output_data, null),
359
+ status: row.status,
360
+ stepsLog: _parseJSON(row.steps_log, []),
361
+ durationMs: row.duration_ms || 0,
362
+ error: row.error || null,
363
+ testMode: row.test_mode === 1,
364
+ startedAt: row.started_at,
365
+ completedAt: row.completed_at || null,
366
+ };
367
+ }
368
+
369
+ function _rowToTrigger(row) {
370
+ if (!row) return null;
371
+ return {
372
+ id: row.id,
373
+ flowId: row.flow_id,
374
+ type: row.type,
375
+ config: _parseJSON(row.config, {}),
376
+ enabled: row.enabled === 1,
377
+ lastTriggeredAt: row.last_triggered_at || null,
378
+ triggerCount: row.trigger_count || 0,
379
+ createdAt: row.created_at,
380
+ };
381
+ }
382
+
383
+ function _requireFlow(db, flowId) {
384
+ const row = db.prepare(`SELECT * FROM auto_flows WHERE id = ?`).get(flowId);
385
+ if (!row) throw new Error(`Flow not found: ${flowId}`);
386
+ return row;
387
+ }
388
+
389
+ function _validateNodes(nodes) {
390
+ if (!Array.isArray(nodes)) throw new Error("nodes must be an array");
391
+ for (const n of nodes) {
392
+ if (!n || typeof n !== "object") throw new Error("node must be an object");
393
+ if (!n.id) throw new Error("node.id required");
394
+ const type = n.type || "action";
395
+ if (!Object.values(NODE_TYPE).includes(type)) {
396
+ throw new Error(`Unknown node type: ${type}`);
397
+ }
398
+ if (type === "action") {
399
+ if (!n.connector) throw new Error(`node ${n.id}: connector required`);
400
+ if (!CONNECTOR_INDEX.has(n.connector)) {
401
+ throw new Error(`Unknown connector: ${n.connector}`);
402
+ }
403
+ const conn = CONNECTOR_INDEX.get(n.connector);
404
+ if (!n.action) throw new Error(`node ${n.id}: action required`);
405
+ if (!conn.actions.includes(n.action)) {
406
+ throw new Error(`Unknown action for ${n.connector}: ${n.action}`);
407
+ }
408
+ }
409
+ if (type === "condition" && !n.expression) {
410
+ throw new Error(`node ${n.id}: expression required for condition`);
411
+ }
412
+ }
413
+ }
414
+
415
+ function _validateEdges(edges, nodes) {
416
+ if (!Array.isArray(edges)) throw new Error("edges must be an array");
417
+ const ids = new Set(nodes.map((n) => n.id));
418
+ for (const e of edges) {
419
+ if (!e || !e.from || !e.to) throw new Error("edge needs from/to");
420
+ if (!ids.has(e.from)) throw new Error(`edge.from unknown node: ${e.from}`);
421
+ if (!ids.has(e.to)) throw new Error(`edge.to unknown node: ${e.to}`);
422
+ }
423
+ }
424
+
425
+ /* ── Flow CRUD ─────────────────────────────────────────────── */
426
+
427
+ export function createFlow(db, options = {}) {
428
+ const {
429
+ name,
430
+ description,
431
+ nodes = [],
432
+ edges = [],
433
+ createdBy,
434
+ schedule,
435
+ } = options;
436
+ if (!name) throw new Error("name is required");
437
+ _validateNodes(nodes);
438
+ _validateEdges(edges, nodes);
439
+
440
+ const id = _genId("flow");
441
+ const now = _now();
442
+ db.prepare(
443
+ `INSERT INTO auto_flows
444
+ (id, name, description, nodes, edges, status, schedule, created_by, shared_with, created_at, updated_at)
445
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
446
+ ).run(
447
+ id,
448
+ name,
449
+ description || "",
450
+ JSON.stringify(nodes),
451
+ JSON.stringify(edges),
452
+ FLOW_STATUS.DRAFT,
453
+ schedule || null,
454
+ createdBy || null,
455
+ JSON.stringify([]),
456
+ now,
457
+ now,
458
+ );
459
+
460
+ return _rowToFlow(
461
+ db.prepare(`SELECT * FROM auto_flows WHERE id = ?`).get(id),
462
+ );
463
+ }
464
+
465
+ export function getFlow(db, flowId) {
466
+ return _rowToFlow(
467
+ db.prepare(`SELECT * FROM auto_flows WHERE id = ?`).get(flowId),
468
+ );
469
+ }
470
+
471
+ export function listFlows(db, filters = {}) {
472
+ const { status, limit = 50 } = filters;
473
+ const all = db.data?.get("auto_flows") || [];
474
+ let rows;
475
+ if (status) {
476
+ rows = db
477
+ .prepare(
478
+ `SELECT * FROM auto_flows WHERE status = ? ORDER BY created_at DESC LIMIT ?`,
479
+ )
480
+ .all(status, limit);
481
+ } else {
482
+ rows = db
483
+ .prepare(`SELECT * FROM auto_flows ORDER BY created_at DESC LIMIT ?`)
484
+ .all(limit);
485
+ }
486
+ // Defensive: if db helper returns nothing for some reason, fall back to raw data
487
+ if ((!rows || rows.length === 0) && all.length > 0) {
488
+ let fallback = [...all];
489
+ if (status) fallback = fallback.filter((r) => r.status === status);
490
+ fallback.sort((a, b) => (a.created_at > b.created_at ? -1 : 1));
491
+ rows = fallback.slice(0, limit);
492
+ }
493
+ return rows.map(_rowToFlow);
494
+ }
495
+
496
+ export function updateFlowStatus(db, flowId, status) {
497
+ const validStatuses = Object.values(FLOW_STATUS);
498
+ if (!validStatuses.includes(status)) {
499
+ throw new Error(`Invalid flow status: ${status}`);
500
+ }
501
+ _requireFlow(db, flowId);
502
+ db.prepare(
503
+ `UPDATE auto_flows SET status = ?, updated_at = ? WHERE id = ?`,
504
+ ).run(status, _now(), flowId);
505
+ return _rowToFlow(
506
+ db.prepare(`SELECT * FROM auto_flows WHERE id = ?`).get(flowId),
507
+ );
508
+ }
509
+
510
+ export function deleteFlow(db, flowId) {
511
+ _requireFlow(db, flowId);
512
+ db.prepare(`DELETE FROM auto_triggers WHERE flow_id = ?`).run(flowId);
513
+ db.prepare(`DELETE FROM auto_executions WHERE flow_id = ?`).run(flowId);
514
+ db.prepare(`DELETE FROM auto_flows WHERE id = ?`).run(flowId);
515
+ return true;
516
+ }
517
+
518
+ export function scheduleFlow(db, flowId, cron) {
519
+ if (!cron || typeof cron !== "string") {
520
+ throw new Error("cron expression required");
521
+ }
522
+ _requireFlow(db, flowId);
523
+ db.prepare(
524
+ `UPDATE auto_flows SET schedule = ?, updated_at = ? WHERE id = ?`,
525
+ ).run(cron, _now(), flowId);
526
+ return getFlow(db, flowId);
527
+ }
528
+
529
+ export function shareFlow(db, flowId, targetOrg) {
530
+ if (!targetOrg) throw new Error("targetOrg required");
531
+ const row = _requireFlow(db, flowId);
532
+ const flow = _rowToFlow(row);
533
+ const set = new Set(flow.sharedWith);
534
+ set.add(targetOrg);
535
+ const shared = [...set];
536
+ db.prepare(
537
+ `UPDATE auto_flows SET shared_with = ?, updated_at = ? WHERE id = ?`,
538
+ ).run(JSON.stringify(shared), _now(), flowId);
539
+ return getFlow(db, flowId);
540
+ }
541
+
542
+ export function importTemplate(db, templateId, options = {}) {
543
+ const template = getFlowTemplate(templateId);
544
+ if (!template) throw new Error(`Template not found: ${templateId}`);
545
+ const name = options.name || template.name;
546
+ return createFlow(db, {
547
+ name,
548
+ description: options.description || template.description,
549
+ nodes: template.nodes,
550
+ edges: template.edges,
551
+ createdBy: options.createdBy || null,
552
+ });
553
+ }
554
+
555
+ /* ── Triggers ──────────────────────────────────────────────── */
556
+
557
+ export function addTrigger(db, flowId, options = {}) {
558
+ const { type, config = {} } = options;
559
+ if (!type) throw new Error("trigger type required");
560
+ if (!Object.values(TRIGGER_TYPE).includes(type)) {
561
+ throw new Error(`Unknown trigger type: ${type}`);
562
+ }
563
+ _requireFlow(db, flowId);
564
+
565
+ if (type === TRIGGER_TYPE.SCHEDULE && !config.cron) {
566
+ throw new Error("schedule trigger requires config.cron");
567
+ }
568
+ if (type === TRIGGER_TYPE.WEBHOOK && !config.url) {
569
+ throw new Error("webhook trigger requires config.url");
570
+ }
571
+ if (type === TRIGGER_TYPE.EVENT && !config.event) {
572
+ throw new Error("event trigger requires config.event");
573
+ }
574
+ if (type === TRIGGER_TYPE.CONDITION && !config.expression) {
575
+ throw new Error("condition trigger requires config.expression");
576
+ }
577
+
578
+ const id = _genId("trig");
579
+ const now = _now();
580
+ db.prepare(
581
+ `INSERT INTO auto_triggers
582
+ (id, flow_id, type, config, enabled, last_triggered_at, trigger_count, created_at)
583
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
584
+ ).run(id, flowId, type, JSON.stringify(config), 1, null, 0, now);
585
+
586
+ return _rowToTrigger(
587
+ db.prepare(`SELECT * FROM auto_triggers WHERE id = ?`).get(id),
588
+ );
589
+ }
590
+
591
+ export function listTriggers(db, flowId) {
592
+ const rows = flowId
593
+ ? db
594
+ .prepare(
595
+ `SELECT * FROM auto_triggers WHERE flow_id = ? ORDER BY created_at DESC`,
596
+ )
597
+ .all(flowId)
598
+ : db.prepare(`SELECT * FROM auto_triggers ORDER BY created_at DESC`).all();
599
+ return rows.map(_rowToTrigger);
600
+ }
601
+
602
+ export function getTrigger(db, triggerId) {
603
+ return _rowToTrigger(
604
+ db.prepare(`SELECT * FROM auto_triggers WHERE id = ?`).get(triggerId),
605
+ );
606
+ }
607
+
608
+ export function setTriggerEnabled(db, triggerId, enabled) {
609
+ const trig = getTrigger(db, triggerId);
610
+ if (!trig) throw new Error(`Trigger not found: ${triggerId}`);
611
+ db.prepare(`UPDATE auto_triggers SET enabled = ? WHERE id = ?`).run(
612
+ enabled ? 1 : 0,
613
+ triggerId,
614
+ );
615
+ return getTrigger(db, triggerId);
616
+ }
617
+
618
+ /* ── Flow execution (simulation) ───────────────────────────── */
619
+
620
+ function _simulateNodeOutput(node, input) {
621
+ if (node.type === "condition") {
622
+ const result = _evalCondition(node.expression, input);
623
+ return { branch: result ? "true" : "false", expression: node.expression };
624
+ }
625
+ if (node.type === "parallel") {
626
+ return { parallel: true, branches: node.branches || [] };
627
+ }
628
+ if (node.type === "loop") {
629
+ return { loop: true, iterations: node.iterations || 1 };
630
+ }
631
+ const conn = CONNECTOR_INDEX.get(node.connector);
632
+ const action = node.action || "noop";
633
+ return {
634
+ simulated: true,
635
+ connector: conn?.displayName || node.connector,
636
+ action,
637
+ input,
638
+ output: {
639
+ status: "ok",
640
+ resourceId: _genId(action),
641
+ timestamp: _now(),
642
+ },
643
+ };
644
+ }
645
+
646
+ function _evalCondition(expression, ctx) {
647
+ // Safe-ish numeric comparison evaluator.
648
+ // Supports: <, <=, >, >=, ==, != between ctx.<path> and a numeric literal,
649
+ // or between two ctx.<path> refs. Anything else returns false.
650
+ if (!expression || typeof expression !== "string") return false;
651
+ const opMatch = expression.match(
652
+ /^\s*ctx\.([\w.]+)\s*(<=|>=|==|!=|<|>)\s*([\w.]+|-?\d+(?:\.\d+)?)\s*$/,
653
+ );
654
+ if (!opMatch) return false;
655
+ const [, lhsPath, op, rhsRaw] = opMatch;
656
+ const lhs = _getByPath(ctx, lhsPath);
657
+ let rhs;
658
+ if (/^-?\d+(\.\d+)?$/.test(rhsRaw)) {
659
+ rhs = parseFloat(rhsRaw);
660
+ } else if (rhsRaw.startsWith("ctx.")) {
661
+ rhs = _getByPath(ctx, rhsRaw.slice(4));
662
+ } else {
663
+ rhs = rhsRaw;
664
+ }
665
+ switch (op) {
666
+ case "<":
667
+ return lhs < rhs;
668
+ case "<=":
669
+ return lhs <= rhs;
670
+ case ">":
671
+ return lhs > rhs;
672
+ case ">=":
673
+ return lhs >= rhs;
674
+ case "==":
675
+ return lhs == rhs; // eslint-disable-line eqeqeq
676
+ case "!=":
677
+ return lhs != rhs; // eslint-disable-line eqeqeq
678
+ default:
679
+ return false;
680
+ }
681
+ }
682
+
683
+ function _getByPath(obj, path) {
684
+ if (!obj || !path) return undefined;
685
+ let cur = obj;
686
+ for (const part of path.split(".")) {
687
+ if (cur == null) return undefined;
688
+ cur = cur[part];
689
+ }
690
+ return cur;
691
+ }
692
+
693
+ function _topoOrder(nodes, edges) {
694
+ const idSet = new Set(nodes.map((n) => n.id));
695
+ const incoming = new Map();
696
+ const outgoing = new Map();
697
+ for (const n of nodes) {
698
+ incoming.set(n.id, new Set());
699
+ outgoing.set(n.id, new Set());
700
+ }
701
+ for (const e of edges) {
702
+ if (idSet.has(e.from) && idSet.has(e.to)) {
703
+ incoming.get(e.to).add(e.from);
704
+ outgoing.get(e.from).add(e.to);
705
+ }
706
+ }
707
+ const result = [];
708
+ const queue = [];
709
+ for (const n of nodes) {
710
+ if ((incoming.get(n.id)?.size || 0) === 0) queue.push(n.id);
711
+ }
712
+ while (queue.length > 0) {
713
+ const id = queue.shift();
714
+ result.push(id);
715
+ for (const nextId of outgoing.get(id) || []) {
716
+ incoming.get(nextId).delete(id);
717
+ if (incoming.get(nextId).size === 0) queue.push(nextId);
718
+ }
719
+ }
720
+ if (result.length !== nodes.length) {
721
+ throw new Error("Flow has cycles — cannot execute");
722
+ }
723
+ const indexById = new Map(nodes.map((n) => [n.id, n]));
724
+ return result.map((id) => indexById.get(id));
725
+ }
726
+
727
+ export function executeFlow(db, flowId, options = {}) {
728
+ const flow = getFlow(db, flowId);
729
+ if (!flow) throw new Error(`Flow not found: ${flowId}`);
730
+
731
+ const {
732
+ inputData = {},
733
+ triggerType = TRIGGER_TYPE.MANUAL,
734
+ testMode = false,
735
+ } = options;
736
+ const execId = _genId("exec");
737
+ const startedAt = _now();
738
+ const startMs = Date.now();
739
+
740
+ db.prepare(
741
+ `INSERT INTO auto_executions
742
+ (id, flow_id, trigger_type, input_data, output_data, status, steps_log, duration_ms, error, test_mode, started_at, completed_at)
743
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
744
+ ).run(
745
+ execId,
746
+ flowId,
747
+ triggerType,
748
+ JSON.stringify(inputData),
749
+ null,
750
+ EXECUTION_STATUS.RUNNING,
751
+ JSON.stringify([]),
752
+ 0,
753
+ null,
754
+ testMode ? 1 : 0,
755
+ startedAt,
756
+ null,
757
+ );
758
+
759
+ const stepsLog = [];
760
+ let finalStatus = EXECUTION_STATUS.SUCCESS;
761
+ let finalError = null;
762
+ let outputData = null;
763
+
764
+ try {
765
+ const ordered = _topoOrder(flow.nodes, flow.edges);
766
+ const stepInputs = new Map();
767
+ stepInputs.set("__initial__", inputData);
768
+
769
+ for (const node of ordered) {
770
+ const stepStart = Date.now();
771
+ const parentEdges = flow.edges.filter((e) => e.to === node.id);
772
+ let merged;
773
+ if (parentEdges.length === 0) {
774
+ merged = inputData;
775
+ } else if (parentEdges.length === 1) {
776
+ merged = stepInputs.get(parentEdges[0].from) || {};
777
+ } else {
778
+ merged = {};
779
+ for (const pe of parentEdges) {
780
+ Object.assign(merged, stepInputs.get(pe.from) || {});
781
+ }
782
+ }
783
+ const output = _simulateNodeOutput(node, merged);
784
+ stepInputs.set(node.id, output.output || output);
785
+ stepsLog.push({
786
+ nodeId: node.id,
787
+ nodeType: node.type || "action",
788
+ connector: node.connector || null,
789
+ action: node.action || null,
790
+ status: "success",
791
+ durationMs: Date.now() - stepStart,
792
+ output,
793
+ });
794
+ outputData = output;
795
+ }
796
+ } catch (err) {
797
+ finalStatus = EXECUTION_STATUS.FAILED;
798
+ finalError = err.message;
799
+ }
800
+
801
+ const durationMs = Date.now() - startMs;
802
+ const completedAt = _now();
803
+ db.prepare(
804
+ `UPDATE auto_executions SET output_data = ?, status = ?, steps_log = ?, duration_ms = ?, error = ?, completed_at = ? WHERE id = ?`,
805
+ ).run(
806
+ JSON.stringify(outputData),
807
+ finalStatus,
808
+ JSON.stringify(stepsLog),
809
+ durationMs,
810
+ finalError,
811
+ completedAt,
812
+ execId,
813
+ );
814
+
815
+ return _rowToExecution(
816
+ db.prepare(`SELECT * FROM auto_executions WHERE id = ?`).get(execId),
817
+ );
818
+ }
819
+
820
+ export function fireTrigger(db, triggerId, inputData = {}) {
821
+ const trig = getTrigger(db, triggerId);
822
+ if (!trig) throw new Error(`Trigger not found: ${triggerId}`);
823
+ if (!trig.enabled) throw new Error(`Trigger disabled: ${triggerId}`);
824
+
825
+ const flow = getFlow(db, trig.flowId);
826
+ if (!flow) throw new Error(`Flow not found: ${trig.flowId}`);
827
+ if (flow.status === FLOW_STATUS.ARCHIVED) {
828
+ throw new Error(`Flow archived: ${trig.flowId}`);
829
+ }
830
+ if (flow.status === FLOW_STATUS.PAUSED) {
831
+ throw new Error(`Flow paused: ${trig.flowId}`);
832
+ }
833
+
834
+ const now = _now();
835
+ // trigger_count increment read-modify-write (MockDatabase can't parse `col = col + 1`)
836
+ const currentTrig = db
837
+ .prepare(`SELECT * FROM auto_triggers WHERE id = ?`)
838
+ .get(triggerId);
839
+ const newCount = (currentTrig?.trigger_count || 0) + 1;
840
+ db.prepare(
841
+ `UPDATE auto_triggers SET last_triggered_at = ?, trigger_count = ? WHERE id = ?`,
842
+ ).run(now, newCount, triggerId);
843
+
844
+ return executeFlow(db, trig.flowId, {
845
+ inputData,
846
+ triggerType: trig.type,
847
+ });
848
+ }
849
+
850
+ export function getExecution(db, execId) {
851
+ return _rowToExecution(
852
+ db.prepare(`SELECT * FROM auto_executions WHERE id = ?`).get(execId),
853
+ );
854
+ }
855
+
856
+ export function listExecutions(db, filters = {}) {
857
+ const { flowId, status, limit = 50 } = filters;
858
+ let rows;
859
+ if (flowId && status) {
860
+ rows = db
861
+ .prepare(
862
+ `SELECT * FROM auto_executions WHERE flow_id = ? AND status = ? ORDER BY started_at DESC LIMIT ?`,
863
+ )
864
+ .all(flowId, status, limit);
865
+ } else if (flowId) {
866
+ rows = db
867
+ .prepare(
868
+ `SELECT * FROM auto_executions WHERE flow_id = ? ORDER BY started_at DESC LIMIT ?`,
869
+ )
870
+ .all(flowId, limit);
871
+ } else if (status) {
872
+ rows = db
873
+ .prepare(
874
+ `SELECT * FROM auto_executions WHERE status = ? ORDER BY started_at DESC LIMIT ?`,
875
+ )
876
+ .all(status, limit);
877
+ } else {
878
+ rows = db
879
+ .prepare(`SELECT * FROM auto_executions ORDER BY started_at DESC LIMIT ?`)
880
+ .all(limit);
881
+ }
882
+ return rows.map(_rowToExecution);
883
+ }
884
+
885
+ /* ── Stats ─────────────────────────────────────────────────── */
886
+
887
+ export function getStats(db) {
888
+ const flowsByStatus = {};
889
+ for (const status of Object.values(FLOW_STATUS)) flowsByStatus[status] = 0;
890
+ for (const row of db.data?.get("auto_flows") || []) {
891
+ flowsByStatus[row.status] = (flowsByStatus[row.status] || 0) + 1;
892
+ }
893
+
894
+ const execByStatus = {};
895
+ for (const status of Object.values(EXECUTION_STATUS))
896
+ execByStatus[status] = 0;
897
+ let totalDuration = 0;
898
+ let execCount = 0;
899
+ for (const row of db.data?.get("auto_executions") || []) {
900
+ execByStatus[row.status] = (execByStatus[row.status] || 0) + 1;
901
+ totalDuration += row.duration_ms || 0;
902
+ execCount++;
903
+ }
904
+
905
+ const triggersByType = {};
906
+ for (const type of Object.values(TRIGGER_TYPE)) triggersByType[type] = 0;
907
+ for (const row of db.data?.get("auto_triggers") || []) {
908
+ triggersByType[row.type] = (triggersByType[row.type] || 0) + 1;
909
+ }
910
+
911
+ const successRate =
912
+ execCount > 0
913
+ ? (execByStatus[EXECUTION_STATUS.SUCCESS] || 0) / execCount
914
+ : 0;
915
+ const avgDurationMs = execCount > 0 ? totalDuration / execCount : 0;
916
+
917
+ return {
918
+ flows: {
919
+ total: Object.values(flowsByStatus).reduce((a, b) => a + b, 0),
920
+ byStatus: flowsByStatus,
921
+ },
922
+ executions: {
923
+ total: execCount,
924
+ byStatus: execByStatus,
925
+ successRate,
926
+ avgDurationMs,
927
+ },
928
+ triggers: {
929
+ total: Object.values(triggersByType).reduce((a, b) => a + b, 0),
930
+ byType: triggersByType,
931
+ },
932
+ connectors: CONNECTOR_CATALOG.length,
933
+ templates: FLOW_TEMPLATES.length,
934
+ };
935
+ }
936
+
937
+ /* ── Config ────────────────────────────────────────────────── */
938
+
939
+ export function getConfig() {
940
+ return {
941
+ connectors: CONNECTOR_CATALOG.length,
942
+ templates: FLOW_TEMPLATES.length,
943
+ flowStatuses: Object.values(FLOW_STATUS),
944
+ executionStatuses: Object.values(EXECUTION_STATUS),
945
+ triggerTypes: Object.values(TRIGGER_TYPE),
946
+ nodeTypes: Object.values(NODE_TYPE),
947
+ };
948
+ }