@fleox-agent-crm/agentcrm 0.0.0-stage → 0.3.1

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 (129) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +223 -2
  3. package/changelog.json +76 -0
  4. package/dist/admin/admin-files.json +26 -0
  5. package/dist/admin/assets/chartsImpl-B45AAqnr.js +91 -0
  6. package/dist/admin/assets/firebase-C1x_l9P9.js +2 -0
  7. package/dist/admin/assets/gitJson-CAUSxl8d.js +3 -0
  8. package/dist/admin/assets/index-ChFauO8y.js +1181 -0
  9. package/dist/admin/assets/index-TcjySLVg.css +1 -0
  10. package/dist/admin/assets/index.esm-BFc7vwnl.js +400 -0
  11. package/dist/admin/assets/index.esm-CtEQZRb1.js +1651 -0
  12. package/dist/admin/assets/index.esm-Dum0J07z.js +16 -0
  13. package/dist/admin/assets/index.esm2017-EOzKbpUS.js +390 -0
  14. package/dist/admin/assets/lucide-react.min--rznIkmQ.js +6 -0
  15. package/dist/admin/assets/sdk-DgzLACmL.js +1717 -0
  16. package/dist/admin/config.js +3 -0
  17. package/dist/admin/favicon.svg +1 -0
  18. package/dist/admin/index.html +139 -0
  19. package/dist/admin/runtime/agentcrm-runtime.js +104 -0
  20. package/dist/admin/runtime/lucide-react.js +791 -0
  21. package/dist/admin/runtime/react-dom-client.js +17 -0
  22. package/dist/admin/runtime/react-dom.js +20 -0
  23. package/dist/admin/runtime/react-i18next.js +19 -0
  24. package/dist/admin/runtime/react-jsx-runtime.js +17 -0
  25. package/dist/admin/runtime/react.js +22 -0
  26. package/dist/agent/agentcrm.mjs +4057 -0
  27. package/dist/agent/chunks/_commonjsHelpers-ClciMIcB.mjs +14 -0
  28. package/dist/agent/chunks/activities-Cj_SIOay.mjs +227 -0
  29. package/dist/agent/chunks/api-Cz0Ubj-B.mjs +98 -0
  30. package/dist/agent/chunks/appearance-BloHbiLA.mjs +174 -0
  31. package/dist/agent/chunks/automations-DwWruxmu.mjs +698 -0
  32. package/dist/agent/chunks/backup-W3CjgkAJ.mjs +116 -0
  33. package/dist/agent/chunks/batch-CVETiJj5.mjs +142 -0
  34. package/dist/agent/chunks/compact-BGHI92tm.mjs +69 -0
  35. package/dist/agent/chunks/context-CxFfqS4D.mjs +65 -0
  36. package/dist/agent/chunks/core-Dy7vY9H6.mjs +255 -0
  37. package/dist/agent/chunks/dedupe-BepmsaHv.mjs +123 -0
  38. package/dist/agent/chunks/encoding-C9MzdP95.mjs +36 -0
  39. package/dist/agent/chunks/exportCsv-C6e6XEmB.mjs +130 -0
  40. package/dist/agent/chunks/extCheck-GgHG1dYB.mjs +200 -0
  41. package/dist/agent/chunks/extPackage-DuZ6GZhp.mjs +115 -0
  42. package/dist/agent/chunks/extScaffold-Aqyjf1nr.mjs +320 -0
  43. package/dist/agent/chunks/extensionActions-D2HtpQz4.mjs +48 -0
  44. package/dist/agent/chunks/extensions-Bp72uZ9a.mjs +164 -0
  45. package/dist/agent/chunks/featureRegistry-DSsZbhrh.mjs +149 -0
  46. package/dist/agent/chunks/fields-B0bitgmr.mjs +476 -0
  47. package/dist/agent/chunks/files-CjsPJSR6.mjs +158 -0
  48. package/dist/agent/chunks/files-D1daDgY0.mjs +367 -0
  49. package/dist/agent/chunks/firebase-DxGVQV9E.mjs +454 -0
  50. package/dist/agent/chunks/firebaseRules-iori-s7U.mjs +221 -0
  51. package/dist/agent/chunks/forms-BfuWaQjA.mjs +847 -0
  52. package/dist/agent/chunks/gitJson-fjr8mcHe.mjs +528 -0
  53. package/dist/agent/chunks/hosting-Bme7LxPc.mjs +144 -0
  54. package/dist/agent/chunks/http-Bk9ICb7I.mjs +127 -0
  55. package/dist/agent/chunks/ids-CvnmrMJX.mjs +59 -0
  56. package/dist/agent/chunks/ids-u-noYcy5.mjs +14 -0
  57. package/dist/agent/chunks/importCsv-LVmMTaWU.mjs +334 -0
  58. package/dist/agent/chunks/index-B-4W0-P4.mjs +535 -0
  59. package/dist/agent/chunks/index-BM8qeGOb.mjs +2489 -0
  60. package/dist/agent/chunks/index-Bnmav-Oh.mjs +311 -0
  61. package/dist/agent/chunks/index-CRkw3_iF.mjs +147 -0
  62. package/dist/agent/chunks/index-CWrrF1wl.mjs +111 -0
  63. package/dist/agent/chunks/index-DUSdTBkz.mjs +2988 -0
  64. package/dist/agent/chunks/index-RCmo-uld.mjs +26 -0
  65. package/dist/agent/chunks/index-w03ReRs4.mjs +9780 -0
  66. package/dist/agent/chunks/install-CbpD_7gd.mjs +188 -0
  67. package/dist/agent/chunks/jsx-runtime-Ca2uwGDr.mjs +76 -0
  68. package/dist/agent/chunks/loader-MPD9iAvh.mjs +184 -0
  69. package/dist/agent/chunks/lucide-react.min-Bt3gtepY.mjs +64 -0
  70. package/dist/agent/chunks/memory-WNR3VHUJ.mjs +56 -0
  71. package/dist/agent/chunks/migrate-CsClnnN8.mjs +182 -0
  72. package/dist/agent/chunks/modules-clWdE-K2.mjs +202 -0
  73. package/dist/agent/chunks/nodeImport-ylfRDvqt.mjs +38 -0
  74. package/dist/agent/chunks/package-B7mI6HGi.mjs +194 -0
  75. package/dist/agent/chunks/papaparse-Df7i1dHr.mjs +1309 -0
  76. package/dist/agent/chunks/pipelines-BAKpPk1w.mjs +234 -0
  77. package/dist/agent/chunks/plan-BmTat6-T.mjs +254 -0
  78. package/dist/agent/chunks/pluginRegistry-DyHtlkcr.mjs +83 -0
  79. package/dist/agent/chunks/query-C5nJB_3C.mjs +192 -0
  80. package/dist/agent/chunks/records-DcHFelq5.mjs +2195 -0
  81. package/dist/agent/chunks/registry-COfJrSmg.mjs +182 -0
  82. package/dist/agent/chunks/registry-DijAKZ24.mjs +431 -0
  83. package/dist/agent/chunks/relations-B9TynBmA.mjs +191 -0
  84. package/dist/agent/chunks/reports-BACHGccj.mjs +189 -0
  85. package/dist/agent/chunks/runtime-C0Xf0Axi.mjs +14873 -0
  86. package/dist/agent/chunks/runtimeConfig-CCOFg1RM.mjs +250 -0
  87. package/dist/agent/chunks/s3-Dz50-NuA.mjs +903 -0
  88. package/dist/agent/chunks/schema-C4lSslXv.mjs +373 -0
  89. package/dist/agent/chunks/schema-DnLj8dUO.mjs +213 -0
  90. package/dist/agent/chunks/sdk-whvfg786.mjs +43644 -0
  91. package/dist/agent/chunks/seed-6MRmQQBR.mjs +541 -0
  92. package/dist/agent/chunks/server-DC3t8r30.mjs +242 -0
  93. package/dist/agent/chunks/settings-Di8nzNEL.mjs +111 -0
  94. package/dist/agent/chunks/standardObjects-CheqfDqa.mjs +243 -0
  95. package/dist/agent/chunks/stats-BfuLWhVV.mjs +51 -0
  96. package/dist/agent/chunks/store-5gqRSKXg.mjs +122 -0
  97. package/dist/agent/chunks/themes-DPZFv3yc.mjs +43 -0
  98. package/dist/agent/chunks/tools-iSH-yMpl.mjs +884 -0
  99. package/dist/agent/chunks/types-BOy9PUl7.mjs +44 -0
  100. package/dist/agent/chunks/users-DI1BvS0f.mjs +107 -0
  101. package/dist/agent/chunks/utils-DmI8FtXP.mjs +83 -0
  102. package/dist/agent/chunks/views-DH1n9Lrt.mjs +109 -0
  103. package/dist/version.json +7 -0
  104. package/package.json +97 -4
  105. package/plugin/.claude-plugin/plugin.json +17 -0
  106. package/plugin/skills/agentcrm-admin/SKILL.md +191 -0
  107. package/plugin/skills/agentcrm-admin/references/appearance.md +26 -0
  108. package/plugin/skills/agentcrm-admin/references/import.md +16 -0
  109. package/plugin/skills/agentcrm-admin/references/records.md +30 -0
  110. package/plugin/skills/agentcrm-admin/references/schema.md +59 -0
  111. package/plugin/skills/agentcrm-extensions/SKILL.md +93 -0
  112. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +213 -0
  113. package/plugin/skills/agentcrm-extensions/references/runtime-api.md +41 -0
  114. package/plugin/skills/agentcrm-extensions/references/testing.md +78 -0
  115. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +65 -0
  116. package/plugin/skills/agentcrm-install/SKILL.md +116 -0
  117. package/plugin/skills/agentcrm-install/references/firebase.md +18 -0
  118. package/plugin/skills/agentcrm-install/references/github.md +20 -0
  119. package/plugin/skills/agentcrm-install/references/mcp.md +17 -0
  120. package/plugin/skills/agentcrm-install/references/plans.md +45 -0
  121. package/plugin/skills/agentcrm-install/references/s3.md +24 -0
  122. package/plugin/skills/agentcrm-manage/SKILL.md +108 -0
  123. package/plugin/skills/agentcrm-manage/references/cron.md +11 -0
  124. package/plugin/skills/agentcrm-manage/references/doctor.md +18 -0
  125. package/plugin/skills/agentcrm-sales/SKILL.md +92 -0
  126. package/plugin/skills/agentcrm-sales/references/enrichment.md +13 -0
  127. package/plugin/skills/agentcrm-sales/references/playbooks.md +29 -0
  128. package/plugin/skills/agentcrm-sales/references/weekly-report.md +27 -0
  129. package/types/agentcrm-runtime.d.ts +894 -0
@@ -0,0 +1,698 @@
1
+ import { createRequire as __acrmRequire } from "node:module";
2
+ import { fileURLToPath as __acrmPath } from "node:url";
3
+ import { dirname as __acrmDirname } from "node:path";
4
+ const require2 = __acrmRequire(import.meta.url);
5
+ const __filename = __acrmPath(import.meta.url);
6
+ const __dirname = __acrmDirname(__filename);
7
+ import { toIsoDate, recordLabel, formatFieldValue, normalizeEmail } from "./fields-B0bitgmr.mjs";
8
+ import { n as newId } from "./ids-CvnmrMJX.mjs";
9
+ import { plugins } from "./pluginRegistry-DyHtlkcr.mjs";
10
+ import { r as resolveField, m as matchesFilter } from "./query-C5nJB_3C.mjs";
11
+ import { queryContext, setRecordOwner, updateRecord } from "./records-DcHFelq5.mjs";
12
+ import { getSettings } from "./settings-Di8nzNEL.mjs";
13
+ import { userOf, automationRunsOf, asDoc, requireRecord, requireObject, automationsOf, objectOf, recordsOf, thaw, recordOf, usersOf } from "./store-5gqRSKXg.mjs";
14
+ import { F as FILTER_OPS } from "../agentcrm.mjs";
15
+ import { lastActivityAt, addNote, createTask } from "./activities-Cj_SIOay.mjs";
16
+ import { OpsError, assertCan } from "./context-CxFfqS4D.mjs";
17
+ import { pipelineOfRecord } from "./pipelines-BAKpPk1w.mjs";
18
+ import { getAutomationAction, listAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
19
+ import { registerAutomationAction, subscribeAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
20
+ import "./standardObjects-CheqfDqa.mjs";
21
+ import "./relations-B9TynBmA.mjs";
22
+ import "./runtimeConfig-CCOFg1RM.mjs";
23
+ import "node:util";
24
+ import "node:fs";
25
+ import "node:path";
26
+ import "node:url";
27
+ import "node:crypto";
28
+ import "node:os";
29
+ import "node:child_process";
30
+ import "node:http";
31
+ const AUTOMATION_TEMPLATES = [
32
+ {
33
+ id: "deal-won-onboarding",
34
+ name: "Deal won → onboarding task",
35
+ description: "When a deal reaches a won stage, create an onboarding task for its owner due in 2 days and log a note.",
36
+ requires: ["deals"],
37
+ build: () => ({
38
+ name: "Deal won → onboarding task",
39
+ object: "deals",
40
+ trigger: { type: "stage.changed", to: "won" },
41
+ conditions: [],
42
+ actions: [
43
+ { type: "createTask", title: "Kick off onboarding for {{label}}", dueInDays: 2, assignTo: "owner" },
44
+ { type: "addNote", body: "🎉 Deal won ({{amount}}). Onboarding task created for {{owner}}." }
45
+ ]
46
+ })
47
+ },
48
+ {
49
+ id: "new-lead-round-robin",
50
+ name: "New lead → assign round robin",
51
+ description: "When a person is created as a lead, assign an owner in turn among active admins and members, and create a first-touch task.",
52
+ requires: ["people"],
53
+ build: () => ({
54
+ name: "New lead → assign round robin",
55
+ object: "people",
56
+ trigger: { type: "record.created" },
57
+ conditions: [{ field: "lifecycleStage", op: "eq", value: "lead" }],
58
+ actions: [
59
+ { type: "assignOwner", userId: "roundRobin" },
60
+ { type: "createTask", title: "First touch: {{label}}", dueInDays: 1, assignTo: "owner" }
61
+ ]
62
+ })
63
+ },
64
+ {
65
+ id: "stale-deal-followup",
66
+ name: "Deal stale 14 days → follow-up task",
67
+ description: "Every check, open deals without any activity for 14 days get a follow-up task for their owner (once per idle period).",
68
+ requires: ["deals"],
69
+ build: () => ({
70
+ name: "Deal stale 14 days → follow-up task",
71
+ object: "deals",
72
+ trigger: { type: "inactivity", days: 14 },
73
+ conditions: [{ field: "stage", op: "notIn", value: ["won", "lost"] }],
74
+ actions: [{ type: "createTask", title: "Follow up on {{label}} (no activity for 14 days)", dueInDays: 1, assignTo: "owner" }]
75
+ })
76
+ },
77
+ {
78
+ id: "deal-lost-note",
79
+ name: "Deal lost → log the reason",
80
+ description: "When a deal moves to a lost stage, log a note with the lost reason on its timeline.",
81
+ requires: ["deals"],
82
+ build: () => ({
83
+ name: "Deal lost → log the reason",
84
+ object: "deals",
85
+ trigger: { type: "stage.changed", to: "lost" },
86
+ conditions: [],
87
+ actions: [{ type: "addNote", body: "Deal lost. Reason: {{lostReason}}" }]
88
+ })
89
+ },
90
+ {
91
+ id: "customer-welcome",
92
+ name: "New customer → welcome task",
93
+ description: "When a person's lifecycle stage becomes Customer, create a task to send the welcome pack.",
94
+ requires: ["people"],
95
+ build: () => ({
96
+ name: "New customer → welcome task",
97
+ object: "people",
98
+ trigger: { type: "field.changed", field: "lifecycleStage", to: "customer" },
99
+ conditions: [],
100
+ actions: [{ type: "createTask", title: "Send the welcome pack to {{label}}", dueInDays: 3, assignTo: "owner" }]
101
+ })
102
+ },
103
+ {
104
+ id: "new-person-webhook",
105
+ name: "New person → webhook",
106
+ description: "POST every new person as JSON to a URL you own (Zapier, Make, n8n, Slack workflow…). Set the URL before enabling.",
107
+ requires: ["people"],
108
+ build: () => ({
109
+ name: "New person → webhook",
110
+ enabled: false,
111
+ object: "people",
112
+ trigger: { type: "record.created" },
113
+ conditions: [],
114
+ actions: [{ type: "webhook", url: "https://example.com/your-webhook" }]
115
+ })
116
+ }
117
+ ];
118
+ function findAutomationTemplate(id) {
119
+ return AUTOMATION_TEMPLATES.find((t) => t.id === id);
120
+ }
121
+ const PLACEHOLDER = /\{\{\s*([\w.-]+)\s*\}\}/g;
122
+ function templatePlaceholders(template) {
123
+ return [...template.matchAll(PLACEHOLDER)].map((m) => m[1]);
124
+ }
125
+ function renderTemplate(ctx, template, record, object) {
126
+ if (!template.includes("{{")) return template;
127
+ let locale;
128
+ let currency;
129
+ try {
130
+ const s = getSettings({ ...ctx, user: null });
131
+ locale = s.locale;
132
+ currency = s.defaultCurrency;
133
+ } catch {
134
+ }
135
+ const fmt = object ? { ...queryContext(ctx, object), locale, currency } : { locale, currency };
136
+ return template.replace(PLACEHOLDER, (_m, key) => {
137
+ switch (key) {
138
+ case "label":
139
+ case "recordLabel":
140
+ return recordLabel(object, record);
141
+ case "object":
142
+ return object?.labelSingular ?? record.object;
143
+ case "id":
144
+ return record.id;
145
+ case "today":
146
+ return toIsoDate(new Date(ctx.now()));
147
+ case "owner":
148
+ return record.ownerId ? userOf(ctx.store, record.ownerId)?.name ?? "" : "";
149
+ case "owner.email":
150
+ return record.ownerId ? userOf(ctx.store, record.ownerId)?.email ?? "" : "";
151
+ case "owner.id":
152
+ return record.ownerId ?? "";
153
+ }
154
+ const field = object ? resolveField(object, key) : void 0;
155
+ const value = key in record.fields ? record.fields[key] : record[key];
156
+ if (value === void 0 || value === null) return "";
157
+ if (!field) return typeof value === "object" ? JSON.stringify(value) : String(value);
158
+ return formatFieldValue(field, value, fmt);
159
+ });
160
+ }
161
+ function summarizeForWebhook(ctx, record, object) {
162
+ const qctx = queryContext(ctx, object);
163
+ const display = {};
164
+ for (const field of object.fields) {
165
+ const v = record.fields[field.id];
166
+ if (v === void 0 || v === null) continue;
167
+ display[field.id] = formatFieldValue(field, v, qctx);
168
+ }
169
+ const owner = record.ownerId ? userOf(ctx.store, record.ownerId) : void 0;
170
+ return {
171
+ id: record.id,
172
+ object: record.object,
173
+ label: recordLabel(object, record),
174
+ fields: record.fields,
175
+ display,
176
+ ...owner ? { owner: { id: owner.id, name: owner.name, email: owner.email } } : {},
177
+ createdAt: new Date(record.createdAt).toISOString(),
178
+ updatedAt: new Date(record.updatedAt).toISOString()
179
+ };
180
+ }
181
+ const MAX_AUTOMATION_DEPTH = 3;
182
+ const MAX_AUTOMATION_RUNS = 1e3;
183
+ const DAY = 864e5;
184
+ const HOUR = 36e5;
185
+ const EVENT_TRIGGERS = ["record.created", "record.updated", "field.changed", "stage.changed"];
186
+ const SCHEDULED_TRIGGERS = ["schedule", "inactivity"];
187
+ const TRIGGER_TYPES = [...EVENT_TRIGGERS, ...SCHEDULED_TRIGGERS];
188
+ const ACTION_TYPES = ["createTask", "updateField", "addNote", "assignOwner", "webhook", "extension"];
189
+ let webhookFetch = null;
190
+ function setAutomationFetch(fn) {
191
+ webhookFetch = fn;
192
+ }
193
+ function listAutomations(ctx) {
194
+ return ctx.store.list("automations");
195
+ }
196
+ function getAutomation(ctx, id) {
197
+ const a = ctx.store.get("automations", id);
198
+ if (!a) throw new OpsError("notFound", `Automation "${id}" not found.`);
199
+ return a;
200
+ }
201
+ function listAutomationRuns(ctx, opts = {}) {
202
+ let runs = automationRunsOf(ctx.store);
203
+ if (opts.automationId) runs = runs.filter((r) => r.automationId === opts.automationId);
204
+ if (opts.recordId) runs = runs.filter((r) => r.recordId === opts.recordId);
205
+ if (opts.status) runs = runs.filter((r) => r.status === opts.status);
206
+ runs = [...runs].sort((a, b) => b.startedAt - a.startedAt || (a.id < b.id ? 1 : -1));
207
+ return opts.limit ? runs.slice(0, opts.limit) : runs;
208
+ }
209
+ function fieldExists(object, id) {
210
+ return !!resolveField(object, id);
211
+ }
212
+ function validateAutomation(ctx, input) {
213
+ const errors = [];
214
+ const name = (input.name ?? "").trim();
215
+ if (!name) errors.push("A name is required.");
216
+ const object = objectOf(ctx.store, input.object);
217
+ if (!object) errors.push(`Unknown object "${input.object}".`);
218
+ const trigger = { ...input.trigger ?? { type: "record.created" } };
219
+ if (!TRIGGER_TYPES.includes(trigger.type)) errors.push(`Unknown trigger "${String(trigger.type)}". Triggers: ${TRIGGER_TYPES.join(", ")}.`);
220
+ if (trigger.type === "field.changed" && !trigger.field) errors.push("A field.changed trigger needs a field.");
221
+ if (trigger.field && object && !fieldExists(object, trigger.field)) errors.push(`Trigger field "${trigger.field}" doesn't exist on ${object.labelPlural}.`);
222
+ if (trigger.type === "inactivity" && !(typeof trigger.days === "number" && trigger.days >= 1)) errors.push("An inactivity trigger needs days ≥ 1.");
223
+ if (trigger.type === "schedule" && !(typeof trigger.everyHours === "number" && trigger.everyHours >= 1)) errors.push("A schedule trigger needs everyHours ≥ 1.");
224
+ if (trigger.type === "stage.changed" && object && !ctx.store.list("pipelines").some((p) => p.object === object.id)) errors.push(`${object.labelPlural} have no pipeline.`);
225
+ const conditions = [];
226
+ for (const c of input.conditions ?? []) {
227
+ if (!c || typeof c.field !== "string") {
228
+ errors.push("Invalid condition.");
229
+ continue;
230
+ }
231
+ if (!FILTER_OPS.includes(c.op)) errors.push(`Unknown condition operator "${String(c.op)}".`);
232
+ if (object && !fieldExists(object, c.field)) errors.push(`Condition field "${c.field}" doesn't exist on ${object.labelPlural}.`);
233
+ conditions.push({ field: c.field, op: c.op, ...c.value !== void 0 ? { value: c.value } : {} });
234
+ }
235
+ const actions = [];
236
+ if (!input.actions?.length) errors.push("Add at least one action.");
237
+ for (const [i, a] of (input.actions ?? []).entries()) {
238
+ const n = `Action ${i + 1}`;
239
+ switch (a?.type) {
240
+ case "createTask":
241
+ if (!a.title?.trim()) errors.push(`${n}: a task title is required.`);
242
+ if (a.dueInDays !== void 0 && !(typeof a.dueInDays === "number" && a.dueInDays >= 0)) errors.push(`${n}: dueInDays must be ≥ 0.`);
243
+ if (a.assignTo && a.assignTo !== "owner" && !userOf(ctx.store, a.assignTo)) errors.push(`${n}: unknown user "${a.assignTo}".`);
244
+ actions.push({ type: "createTask", title: a.title?.trim() ?? "", ...a.dueInDays !== void 0 ? { dueInDays: a.dueInDays } : {}, ...a.assignTo ? { assignTo: a.assignTo } : {} });
245
+ break;
246
+ case "updateField":
247
+ if (!a.field) errors.push(`${n}: pick a field.`);
248
+ else if (object && !object.fields.some((f) => f.id === a.field)) errors.push(`${n}: field "${a.field}" doesn't exist on ${object.labelPlural}.`);
249
+ actions.push({ type: "updateField", field: a.field, value: a.value });
250
+ break;
251
+ case "addNote":
252
+ if (!a.body?.trim()) errors.push(`${n}: the note is empty.`);
253
+ actions.push({ type: "addNote", body: a.body ?? "" });
254
+ break;
255
+ case "assignOwner":
256
+ if (!a.userId) errors.push(`${n}: pick a user or round robin.`);
257
+ else if (a.userId !== "roundRobin" && !userOf(ctx.store, a.userId)) errors.push(`${n}: unknown user "${a.userId}".`);
258
+ actions.push({ type: "assignOwner", userId: a.userId });
259
+ break;
260
+ case "webhook":
261
+ if (!/^https?:\/\/\S+$/i.test(a.url ?? "")) errors.push(`${n}: the webhook URL must start with https:// (or http://).`);
262
+ actions.push({ type: "webhook", url: (a.url ?? "").trim() });
263
+ break;
264
+ case "extension": {
265
+ const def = typeof a.action === "string" ? getAutomationAction(a.action) : void 0;
266
+ const params = a.params && typeof a.params === "object" && !Array.isArray(a.params) ? { ...a.params } : {};
267
+ if (!def) {
268
+ const known = listAutomationActions().map((d) => d.id);
269
+ errors.push(`${n}: unknown extension action "${String(a.action)}"${known.length ? `. Available: ${known.join(", ")}` : " (is its extension installed and enabled?)"}.`);
270
+ } else {
271
+ if (object && def.objects?.length && !def.objects.includes(object.id)) errors.push(`${n}: "${def.id}" doesn't apply to ${object.labelPlural}.`);
272
+ for (const p of def.params ?? []) {
273
+ if (p.required && (params[p.id] === void 0 || params[p.id] === null || params[p.id] === "")) errors.push(`${n}: "${p.label}" is required.`);
274
+ }
275
+ if (object && def.validate) errors.push(...def.validate(params, object).map((e) => `${n}: ${e}`));
276
+ }
277
+ actions.push({ type: "extension", action: String(a.action ?? ""), params });
278
+ break;
279
+ }
280
+ default:
281
+ errors.push(`${n}: unknown action "${String(a?.type)}". Actions: ${ACTION_TYPES.join(", ")}.`);
282
+ }
283
+ }
284
+ if (errors.length) throw new OpsError("validation", errors.join(" "), errors);
285
+ return { name, enabled: input.enabled ?? true, object: input.object, trigger, conditions, actions };
286
+ }
287
+ async function createAutomation(ctx, input, opts = {}) {
288
+ assertCan(ctx, "automations.write");
289
+ const valid = validateAutomation(ctx, input);
290
+ const now = ctx.now();
291
+ const id = opts.id ?? newId(now);
292
+ if (ctx.store.get("automations", id)) throw new OpsError("conflict", `Automation "${id}" already exists.`);
293
+ const doc = { id, name: valid.name, enabled: valid.enabled ?? true, object: valid.object, trigger: valid.trigger, conditions: valid.conditions ?? [], actions: valid.actions, createdAt: now, updatedAt: now };
294
+ await ctx.store.write([{ op: "set", collection: "automations", doc: asDoc(doc) }], { message: `Create automation ${valid.name}` });
295
+ return getAutomation(ctx, id);
296
+ }
297
+ async function updateAutomation(ctx, id, patch) {
298
+ assertCan(ctx, "automations.write");
299
+ const current = thaw(getAutomation(ctx, id));
300
+ const valid = validateAutomation(ctx, {
301
+ name: patch.name ?? current.name,
302
+ enabled: patch.enabled ?? current.enabled,
303
+ object: patch.object ?? current.object,
304
+ trigger: patch.trigger ?? current.trigger,
305
+ conditions: patch.conditions ?? current.conditions,
306
+ actions: patch.actions ?? current.actions
307
+ });
308
+ const next = { ...current, ...valid, enabled: valid.enabled ?? true, conditions: valid.conditions ?? [], updatedAt: ctx.now() };
309
+ await ctx.store.write([{ op: "set", collection: "automations", doc: asDoc(next) }], { message: `Update automation ${valid.name}` });
310
+ return getAutomation(ctx, id);
311
+ }
312
+ async function setAutomationEnabled(ctx, id, enabled) {
313
+ assertCan(ctx, "automations.write");
314
+ getAutomation(ctx, id);
315
+ await ctx.store.write([{ op: "patch", collection: "automations", id, fields: { enabled, updatedAt: ctx.now() } }], { message: `${enabled ? "Enable" : "Disable"} automation` });
316
+ return getAutomation(ctx, id);
317
+ }
318
+ async function deleteAutomation(ctx, id, opts = {}) {
319
+ assertCan(ctx, "automations.write");
320
+ getAutomation(ctx, id);
321
+ const runs = opts.keepRuns ? [] : automationRunsOf(ctx.store).filter((r) => r.automationId === id);
322
+ await ctx.store.write(
323
+ [{ op: "delete", collection: "automations", id }, ...runs.map((r) => ({ op: "delete", collection: "automationRuns", id: r.id }))],
324
+ { message: "Delete automation" }
325
+ );
326
+ return { id, deletedRuns: runs.length };
327
+ }
328
+ async function duplicateAutomation(ctx, id) {
329
+ const a = getAutomation(ctx, id);
330
+ return createAutomation(ctx, { name: `${a.name} (copy)`, enabled: false, object: a.object, trigger: thaw(a.trigger), conditions: thaw(a.conditions), actions: thaw(a.actions) });
331
+ }
332
+ async function createAutomationFromTemplate(ctx, templateId, overrides = {}) {
333
+ const template = findAutomationTemplate(templateId);
334
+ if (!template) throw new OpsError("notFound", `Unknown automation template "${templateId}". Templates: ${AUTOMATION_TEMPLATES.map((t) => t.id).join(", ")}.`);
335
+ return createAutomation(ctx, { ...template.build(), ...overrides });
336
+ }
337
+ async function clearAutomationRuns(ctx, opts = {}) {
338
+ assertCan(ctx, "automations.write");
339
+ const runs = automationRunsOf(ctx.store).filter((r) => !opts.automationId || r.automationId === opts.automationId);
340
+ if (runs.length) await ctx.store.write(runs.map((r) => ({ op: "delete", collection: "automationRuns", id: r.id })), { message: "Clear automation runs" });
341
+ return { deleted: runs.length };
342
+ }
343
+ async function pruneAutomationRuns(ctx, keep = MAX_AUTOMATION_RUNS) {
344
+ const runs = listAutomationRuns(ctx);
345
+ const doomed = runs.slice(keep);
346
+ if (doomed.length) await ctx.store.write(doomed.map((r) => ({ op: "delete", collection: "automationRuns", id: r.id })), { message: "Prune automation runs" });
347
+ return { deleted: doomed.length };
348
+ }
349
+ function conditionsMatch(ctx, automation, record, object) {
350
+ if (!automation.conditions?.length) return true;
351
+ const qctx = queryContext(ctx, object);
352
+ return automation.conditions.every((f) => matchesFilter(record, f, qctx));
353
+ }
354
+ function stageMatches(ctx, record, to, stageId) {
355
+ if (to === void 0 || to === null || to === "") return true;
356
+ const wanted = String(to).toLowerCase();
357
+ if (stageId.toLowerCase() === wanted) return true;
358
+ const pipeline = pipelineOfRecord(ctx, record);
359
+ const stage = pipeline?.stages.find((s) => s.id === stageId);
360
+ return !!stage && (stage.kind === wanted || stage.label.toLowerCase() === wanted);
361
+ }
362
+ function triggerMatches(ctx, automation, event) {
363
+ const t = automation.trigger;
364
+ const record = "record" in event.payload ? event.payload.record : void 0;
365
+ if (!record || record.object !== automation.object) return false;
366
+ switch (event.type) {
367
+ case "record.created":
368
+ return t.type === "record.created";
369
+ case "record.updated": {
370
+ const changed = event.payload.changed;
371
+ if (t.type === "record.updated") return !t.field || changed.includes(t.field);
372
+ if (t.type !== "field.changed" || !t.field || !changed.includes(t.field)) return false;
373
+ if (t.to === void 0 || t.to === null || t.to === "") return true;
374
+ const object = objectOf(ctx.store, record.object);
375
+ return !!object && matchesFilter(record, { field: t.field, op: "eq", value: t.to }, queryContext(ctx, object));
376
+ }
377
+ case "stage.changed":
378
+ return t.type === "stage.changed" && stageMatches(ctx, record, t.to, event.payload.to);
379
+ default:
380
+ return false;
381
+ }
382
+ }
383
+ function eligibleOwners(ctx) {
384
+ return usersOf(ctx.store).filter((u) => !u.disabled && u.role !== "viewer").sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
385
+ }
386
+ function describeAction(ctx, action, record, object) {
387
+ switch (action.type) {
388
+ case "createTask": {
389
+ const assignee = action.assignTo && action.assignTo !== "owner" ? action.assignTo : record.ownerId;
390
+ return `Create task "${renderTemplate(ctx, action.title, record, object)}"${action.dueInDays !== void 0 ? ` due in ${action.dueInDays} day(s)` : ""}${assignee ? ` for ${userOf(ctx.store, assignee)?.name ?? assignee}` : ""}`;
391
+ }
392
+ case "updateField":
393
+ return `Set ${resolveField(object, action.field)?.label ?? action.field} to ${JSON.stringify(typeof action.value === "string" ? renderTemplate(ctx, action.value, record, object) : action.value ?? null)}`;
394
+ case "addNote":
395
+ return `Add note "${renderTemplate(ctx, action.body, record, object).slice(0, 120)}"`;
396
+ case "assignOwner":
397
+ return action.userId === "roundRobin" ? "Assign owner (round robin)" : `Assign owner ${userOf(ctx.store, action.userId)?.name ?? action.userId}`;
398
+ case "webhook":
399
+ return `POST to ${action.url}`;
400
+ case "extension": {
401
+ const def = getAutomationAction(action.action);
402
+ if (!def) return `Run ${action.action} (not available)`;
403
+ const params = renderParams(ctx, def, action.params ?? {}, record, object);
404
+ return def.describe?.({ params, record, object }) ?? `Run ${def.label}`;
405
+ }
406
+ }
407
+ }
408
+ function renderParams(ctx, def, params, record, object) {
409
+ const out = { ...params };
410
+ for (const p of def.params ?? []) {
411
+ if (p.template && typeof out[p.id] === "string") out[p.id] = renderTemplate(ctx, out[p.id], record, object);
412
+ }
413
+ return out;
414
+ }
415
+ async function postWebhook(url, body) {
416
+ const fetchImpl = webhookFetch ?? (typeof fetch === "function" ? fetch : void 0);
417
+ if (!fetchImpl) throw new Error("fetch is not available in this environment.");
418
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
419
+ const timer = controller ? setTimeout(() => controller.abort(), 1e4) : void 0;
420
+ try {
421
+ const res = await fetchImpl(url, { method: "POST", headers: { "content-type": "text/plain;charset=UTF-8" }, body: JSON.stringify(body), signal: controller?.signal });
422
+ if (!res.ok && res.status !== 0) throw new Error(`Webhook answered HTTP ${res.status}.`);
423
+ } finally {
424
+ if (timer) clearTimeout(timer);
425
+ }
426
+ }
427
+ async function executeAction(ctx, actx, automation, action, recordId, object, trigger) {
428
+ const record = requireRecord(ctx.store, recordId);
429
+ switch (action.type) {
430
+ case "createTask": {
431
+ const assigneeId = action.assignTo && action.assignTo !== "owner" ? action.assignTo : record.ownerId;
432
+ const task = await createTask(actx, {
433
+ title: renderTemplate(ctx, action.title, record, object),
434
+ dueAt: action.dueInDays !== void 0 ? ctx.now() + action.dueInDays * DAY : void 0,
435
+ assigneeId: assigneeId && userOf(ctx.store, assigneeId) ? assigneeId : void 0,
436
+ targets: [{ object: record.object, id: record.id }]
437
+ });
438
+ return `Task ${task.id} created`;
439
+ }
440
+ case "updateField": {
441
+ const value = typeof action.value === "string" ? renderTemplate(ctx, action.value, record, object) : action.value;
442
+ await updateRecord(actx, record.id, { [action.field]: value ?? null });
443
+ return `${action.field} updated`;
444
+ }
445
+ case "addNote": {
446
+ const note = await addNote(actx, { object: record.object, id: record.id }, renderTemplate(ctx, action.body, record, object), automation.name);
447
+ return `Note ${note.id} added`;
448
+ }
449
+ case "assignOwner": {
450
+ let userId = action.userId;
451
+ if (userId === "roundRobin") {
452
+ const pool = eligibleOwners(ctx);
453
+ if (!pool.length) throw new Error("No active admin or member to assign.");
454
+ const doc = ctx.store.get("automations", automation.id);
455
+ const cursor = doc?.roundRobinCursor ?? 0;
456
+ userId = pool[cursor % pool.length].id;
457
+ await ctx.store.write([{ op: "patch", collection: "automations", id: automation.id, fields: { roundRobinCursor: (cursor + 1) % pool.length } }], { message: "Automation round robin" });
458
+ }
459
+ await setRecordOwner(actx, record.id, userId);
460
+ return `Owner set to ${userOf(ctx.store, userId)?.name ?? userId}`;
461
+ }
462
+ case "webhook": {
463
+ await postWebhook(action.url, {
464
+ automation: { id: automation.id, name: automation.name },
465
+ trigger,
466
+ firedAt: new Date(ctx.now()).toISOString(),
467
+ record: summarizeForWebhook(ctx, record, object)
468
+ });
469
+ return `POST ${action.url} ok`;
470
+ }
471
+ case "extension": {
472
+ const def = getAutomationAction(action.action);
473
+ if (!def) throw new Error(`Action "${action.action}" is not available (is its extension installed and enabled?).`);
474
+ const params = renderParams(ctx, def, action.params ?? {}, record, object);
475
+ const message = await def.run(actx, { params, record, object, automation, trigger });
476
+ return typeof message === "string" && message ? message : `${def.id} done`;
477
+ }
478
+ }
479
+ }
480
+ async function runOne(ctx, input) {
481
+ const { automation, record, object, dryRun } = input;
482
+ const startedAt = ctx.now();
483
+ const run = {
484
+ id: newId(startedAt),
485
+ automationId: automation.id,
486
+ recordId: record.id,
487
+ startedAt,
488
+ status: "ok",
489
+ trigger: automation.trigger.type,
490
+ ...input.key ? { key: input.key } : {},
491
+ ...dryRun ? { dryRun: true } : {}
492
+ };
493
+ const results = [];
494
+ if (dryRun) {
495
+ const plans = automation.actions.map((a) => ({ action: a, describe: describeAction(ctx, a, record, object) }));
496
+ run.actions = plans.map((p) => ({ type: p.action.type, ok: true, message: p.describe }));
497
+ run.finishedAt = ctx.now();
498
+ run.message = "Dry run — nothing was written.";
499
+ return run;
500
+ }
501
+ const actx = { ...ctx, user: null, depth: (ctx.depth ?? 0) + 1 };
502
+ for (const action of automation.actions) {
503
+ try {
504
+ const message = await executeAction(ctx, actx, automation, action, record.id, object, String(input.trigger));
505
+ results.push({ type: action.type, ok: true, message });
506
+ } catch (err) {
507
+ results.push({ type: action.type, ok: false, message: err.message ?? String(err) });
508
+ }
509
+ }
510
+ run.actions = results;
511
+ run.finishedAt = ctx.now();
512
+ const failed = results.filter((r) => !r.ok);
513
+ if (failed.length) {
514
+ run.status = "error";
515
+ run.message = failed.map((f) => `${f.type}: ${f.message}`).join(" · ");
516
+ }
517
+ await ctx.store.write(
518
+ [
519
+ { op: "set", collection: "automationRuns", doc: asDoc(run) },
520
+ { op: "patch", collection: "automations", id: automation.id, fields: { lastRunAt: run.finishedAt } }
521
+ ],
522
+ { message: `Automation run: ${automation.name}` }
523
+ );
524
+ ctx.log?.(`automation "${automation.name}" on ${record.id}: ${run.status}`);
525
+ return run;
526
+ }
527
+ async function logSkipped(ctx, automation, recordId, message) {
528
+ const now = ctx.now();
529
+ const run = { id: newId(now), automationId: automation.id, ...recordId ? { recordId } : {}, startedAt: now, finishedAt: now, status: "skipped", message, trigger: automation.trigger.type };
530
+ await ctx.store.write([{ op: "set", collection: "automationRuns", doc: asDoc(run) }], { message: `Automation skipped: ${automation.name}` });
531
+ return run;
532
+ }
533
+ function tally(runs) {
534
+ return {
535
+ ran: runs.filter((r) => r.status === "ok").length,
536
+ skipped: runs.filter((r) => r.status === "skipped").length,
537
+ errors: runs.filter((r) => r.status === "error").length,
538
+ runs
539
+ };
540
+ }
541
+ async function runAutomationEvent(ctx, event) {
542
+ const record = "record" in event.payload ? event.payload.record : void 0;
543
+ if (!record || record.deletedAt) return tally([]);
544
+ const candidates = automationsOf(ctx.store).filter((a) => a.enabled && EVENT_TRIGGERS.includes(a.trigger.type) && triggerMatches(ctx, a, event));
545
+ if (!candidates.length) return tally([]);
546
+ const object = objectOf(ctx.store, record.object);
547
+ if (!object) return tally([]);
548
+ const runs = [];
549
+ const depth = ctx.depth ?? 0;
550
+ for (const automation of candidates) {
551
+ if (depth >= MAX_AUTOMATION_DEPTH) {
552
+ runs.push(await logSkipped(ctx, automation, record.id, `Loop protection: nested automation depth ${depth} reached the limit (${MAX_AUTOMATION_DEPTH}).`));
553
+ continue;
554
+ }
555
+ const current = recordOf(ctx.store, record.id);
556
+ if (!current || current.deletedAt) continue;
557
+ if (!conditionsMatch(ctx, automation, current, object)) continue;
558
+ runs.push(await runOne(ctx, { automation, record: current, object, trigger: event.type }));
559
+ }
560
+ return tally(runs);
561
+ }
562
+ function installAutomations(registry = plugins) {
563
+ const types = ["record.created", "record.updated", "stage.changed"];
564
+ const offs = types.map(
565
+ (type) => registry.addAction(type, async (ctx, payload) => {
566
+ await runAutomationEvent(ctx, { type, payload });
567
+ }, 50)
568
+ );
569
+ return () => offs.forEach((off) => off());
570
+ }
571
+ function lastTouchedAt(ctx, record) {
572
+ return Math.max(lastActivityAt(ctx, record.id) ?? 0, record.updatedAt, record.createdAt);
573
+ }
574
+ async function runScheduled(ctx, opts = {}) {
575
+ assertCan(ctx, "automations.run");
576
+ const now = ctx.now();
577
+ const automations = automationsOf(ctx.store).filter(
578
+ (a) => SCHEDULED_TRIGGERS.includes(a.trigger.type) && (opts.automationId ? a.id === opts.automationId : a.enabled)
579
+ );
580
+ if (opts.automationId && !automations.length) {
581
+ const a = getAutomation(ctx, opts.automationId);
582
+ throw new OpsError("invalid", `Automation "${a.name}" has a ${a.trigger.type} trigger — only schedule / inactivity automations run on a schedule.`);
583
+ }
584
+ const knownKeys = new Set(automationRunsOf(ctx.store).map((r) => r.key).filter((k) => !!k));
585
+ const runs = [];
586
+ for (const automation of automations) {
587
+ const object = objectOf(ctx.store, automation.object);
588
+ if (!object) continue;
589
+ const t = automation.trigger;
590
+ let slotKey = null;
591
+ if (t.type === "schedule") {
592
+ const every = Math.max(1, t.everyHours ?? 24) * HOUR;
593
+ if (!opts.force && automation.lastRunAt && now - automation.lastRunAt < every) continue;
594
+ slotKey = `${automation.id}:schedule:${Math.floor(now / every)}`;
595
+ }
596
+ const sysCtx = { ...ctx, depth: ctx.depth ?? 0 };
597
+ let matched = 0;
598
+ for (const record of recordsOf(ctx.store, object.id)) {
599
+ let key;
600
+ if (t.type === "inactivity") {
601
+ const since = lastTouchedAt(ctx, record);
602
+ if (now - since < (t.days ?? 14) * DAY) continue;
603
+ key = `${automation.id}:inactivity:${record.id}:${since}`;
604
+ } else {
605
+ key = `${slotKey}:${record.id}${opts.force ? `:${now}` : ""}`;
606
+ }
607
+ if (knownKeys.has(key)) continue;
608
+ if (!conditionsMatch(ctx, automation, record, object)) continue;
609
+ matched++;
610
+ const run = await runOne(sysCtx, { automation, record, object, trigger: t.type, key, dryRun: opts.dryRun });
611
+ knownKeys.add(key);
612
+ runs.push(run);
613
+ }
614
+ if (t.type === "schedule" && !matched && !opts.dryRun) {
615
+ await ctx.store.write([{ op: "patch", collection: "automations", id: automation.id, fields: { lastRunAt: now } }], { message: "Automation schedule tick" });
616
+ }
617
+ }
618
+ if (!opts.dryRun && automationRunsOf(ctx.store).length > MAX_AUTOMATION_RUNS) await pruneAutomationRuns(ctx);
619
+ return tally(runs);
620
+ }
621
+ async function testAutomation(ctx, automation, recordId, opts = {}) {
622
+ assertCan(ctx, "automations.run");
623
+ const dryRun = opts.dryRun !== false;
624
+ if (!dryRun) assertCan(ctx, "records.write");
625
+ const record = requireRecord(ctx.store, recordId);
626
+ const object = requireObject(ctx.store, record.object);
627
+ const def = {
628
+ id: automation.id ?? "test",
629
+ name: automation.name || "Test",
630
+ enabled: true,
631
+ object: automation.object,
632
+ trigger: automation.trigger,
633
+ conditions: automation.conditions ?? [],
634
+ actions: automation.actions
635
+ };
636
+ if (def.object !== record.object) throw new OpsError("validation", `This automation applies to ${def.object}; the record is a ${object.labelSingular}.`);
637
+ const qctx = queryContext(ctx, object);
638
+ const failedConditions = def.conditions.filter((f) => !matchesFilter(record, f, qctx));
639
+ if (failedConditions.length) {
640
+ const now = ctx.now();
641
+ return {
642
+ matched: false,
643
+ failedConditions,
644
+ run: { id: "test", automationId: def.id, recordId, startedAt: now, finishedAt: now, status: "skipped", message: "Conditions don't match this record.", dryRun }
645
+ };
646
+ }
647
+ const run = await runOne(ctx, { automation: def, record, object, trigger: "manual", dryRun });
648
+ return { matched: true, run, failedConditions: [] };
649
+ }
650
+ function scheduledCandidates(ctx, automationId) {
651
+ const a = getAutomation(ctx, automationId);
652
+ const object = objectOf(ctx.store, a.object);
653
+ if (!object) return [];
654
+ return recordsOf(ctx.store, object.id).filter((r) => {
655
+ if (a.trigger.type === "inactivity" && ctx.now() - lastTouchedAt(ctx, r) < (a.trigger.days ?? 14) * DAY) return false;
656
+ return conditionsMatch(ctx, a, r, object);
657
+ });
658
+ }
659
+ function automationAssignees(ctx) {
660
+ return eligibleOwners(ctx).map((u) => ({ id: u.id, name: u.name, email: normalizeEmail(u.email) }));
661
+ }
662
+ export {
663
+ ACTION_TYPES,
664
+ AUTOMATION_TEMPLATES,
665
+ EVENT_TRIGGERS,
666
+ MAX_AUTOMATION_DEPTH,
667
+ MAX_AUTOMATION_RUNS,
668
+ SCHEDULED_TRIGGERS,
669
+ TRIGGER_TYPES,
670
+ automationAssignees,
671
+ clearAutomationRuns,
672
+ createAutomation,
673
+ createAutomationFromTemplate,
674
+ deleteAutomation,
675
+ duplicateAutomation,
676
+ findAutomationTemplate,
677
+ getAutomation,
678
+ getAutomationAction,
679
+ installAutomations,
680
+ lastTouchedAt,
681
+ listAutomationActions,
682
+ listAutomationRuns,
683
+ listAutomations,
684
+ pruneAutomationRuns,
685
+ registerAutomationAction,
686
+ renderTemplate,
687
+ runAutomationEvent,
688
+ runScheduled,
689
+ scheduledCandidates,
690
+ setAutomationEnabled,
691
+ setAutomationFetch,
692
+ subscribeAutomationActions,
693
+ templatePlaceholders,
694
+ testAutomation,
695
+ triggerMatches,
696
+ updateAutomation,
697
+ validateAutomation
698
+ };