@percena/weft 0.4.0-next.1 → 0.4.0-next.3

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.
@@ -1,3044 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/automations.ts
21
- var automations_exports = {};
22
- __export(automations_exports, {
23
- AGENT_EVENTS: () => AGENT_EVENTS,
24
- APP_EVENTS: () => APP_EVENTS,
25
- AUTOMATIONS_CONFIG_FILE: () => AUTOMATIONS_CONFIG_FILE,
26
- AUTOMATIONS_HISTORY_FILE: () => AUTOMATIONS_HISTORY_FILE,
27
- AUTOMATIONS_RETRY_QUEUE_FILE: () => AUTOMATIONS_RETRY_QUEUE_FILE,
28
- AUTOMATION_HISTORY_MAX_ENTRIES: () => AUTOMATION_HISTORY_MAX_ENTRIES,
29
- AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER: () => AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER,
30
- AutomationConditionSchema: () => AutomationConditionSchema,
31
- AutomationEventLogger: () => AutomationEventLogger,
32
- AutomationSystem: () => AutomationSystem,
33
- AutomationsConfigSchema: () => AutomationsConfigSchema,
34
- EventLogHandler: () => EventLogHandler,
35
- HISTORY_FIELD_MAX_LENGTH: () => HISTORY_FIELD_MAX_LENGTH,
36
- PromptHandler: () => PromptHandler,
37
- RetryScheduler: () => RetryScheduler,
38
- StateConditionSchema: () => StateConditionSchema,
39
- TimeConditionSchema: () => TimeConditionSchema,
40
- VALID_EVENTS: () => VALID_EVENTS,
41
- WebhookHandler: () => WebhookHandler,
42
- WorkspaceEventBus: () => WorkspaceEventBus,
43
- appendAutomationHistoryEntry: () => appendAutomationHistoryEntry,
44
- automationHistoryInputForPromptResult: () => automationHistoryInputForPromptResult,
45
- buildEnvFromSdkInput: () => buildEnvFromSdkInput,
46
- compactAutomationHistory: () => compactAutomationHistory,
47
- compactAutomationHistorySync: () => compactAutomationHistorySync,
48
- createAutomationRuntimeGuard: () => createAutomationRuntimeGuard,
49
- createAutomationSchedulerHost: () => createAutomationSchedulerHost,
50
- createAutomationTimelineBridge: () => createAutomationTimelineBridge,
51
- createAutomationsConfigDoctorReport: () => createAutomationsConfigDoctorReport,
52
- createInMemoryAutomationHistoryStore: () => createInMemoryAutomationHistoryStore,
53
- createPromptHistoryEntry: () => createPromptHistoryEntry,
54
- createRuntimeAutomationBridge: () => createRuntimeAutomationBridge,
55
- createWebhookHistoryEntry: () => createWebhookHistoryEntry,
56
- evaluateAutoLabels: () => evaluateAutoLabels,
57
- evaluateConditions: () => evaluateConditions,
58
- executeAutomationPrompt: () => executeAutomationPrompt,
59
- executeWebhookRequest: () => executeWebhookRequest,
60
- executeWithRetry: () => executeWithRetry,
61
- extractLabelId: () => extractLabelId,
62
- generateShortId: () => generateShortId,
63
- loadAutomationsConfig: () => loadAutomationsConfig,
64
- matchesCron: () => matchesCron,
65
- normalizeNumberValue: () => normalizeNumberValue,
66
- parsePromptReferences: () => parsePromptReferences,
67
- projectTimelineEnvelopeToAutomationInput: () => projectTimelineEnvelopeToAutomationInput,
68
- resolveAutomationsConfigPath: () => resolveAutomationsConfigPath,
69
- sanitizeForShell: () => sanitizeForShell,
70
- saveAutomationsConfig: () => saveAutomationsConfig,
71
- validateAutoLabelPattern: () => validateAutoLabelPattern,
72
- validateAutomations: () => validateAutomations,
73
- validateAutomationsConfig: () => validateAutomationsConfig,
74
- validateAutomationsContent: () => validateAutomationsContent,
75
- zodErrorToIssues: () => zodErrorToIssues
76
- });
77
- module.exports = __toCommonJS(automations_exports);
78
-
79
- // ../packages/automations/dist/index.js
80
- var import_fs = require("fs");
81
- var import_crypto = require("crypto");
82
- var import_path = require("path");
83
- var import_zod = require("zod");
84
-
85
- // ../packages/core/dist/index.js
86
- var THINKING_LEVEL_IDS = [
87
- "off",
88
- "low",
89
- "medium",
90
- "high",
91
- "xhigh",
92
- "max"
93
- ];
94
- function isValidThinkingLevel(value) {
95
- return typeof value === "string" && THINKING_LEVEL_IDS.includes(value);
96
- }
97
- function normalizeThinkingLevel(value) {
98
- if (value === "think") return "medium";
99
- if (isValidThinkingLevel(value)) return value;
100
- return void 0;
101
- }
102
-
103
- // ../packages/automations/dist/index.js
104
- var import_croner = require("croner");
105
- var import_croner2 = require("croner");
106
- var import_promises = require("fs/promises");
107
- var import_path2 = require("path");
108
- var import_promises2 = require("fs/promises");
109
- var import_fs2 = require("fs");
110
- var import_path3 = require("path");
111
- var import_fs3 = require("fs");
112
- var import_fs4 = require("fs");
113
- var APP_EVENTS = [
114
- "LabelAdd",
115
- "LabelRemove",
116
- "LabelConfigChange",
117
- "PermissionModeChange",
118
- "FlagChange",
119
- "SessionStatusChange",
120
- "SchedulerTick"
121
- ];
122
- var AGENT_EVENTS = [
123
- "PreToolUse",
124
- "PostToolUse",
125
- "PostToolUseFailure",
126
- "Notification",
127
- "UserPromptSubmit",
128
- "SessionStart",
129
- "SessionEnd",
130
- "Stop",
131
- "SubagentStart",
132
- "SubagentStop",
133
- "PreCompact",
134
- "PermissionRequest",
135
- "Setup"
136
- ];
137
- var AUTOMATIONS_CONFIG_FILE = "automations.json";
138
- var AUTOMATIONS_HISTORY_FILE = "automations-history.jsonl";
139
- var AUTOMATIONS_RETRY_QUEUE_FILE = "automations-retry-queue.jsonl";
140
- var DEFAULT_WEBHOOK_METHOD = "POST";
141
- var HISTORY_FIELD_MAX_LENGTH = 2e3;
142
- var AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER = 20;
143
- var AUTOMATION_HISTORY_MAX_ENTRIES = 1e3;
144
- function generateShortId() {
145
- return (0, import_crypto.randomBytes)(3).toString("hex");
146
- }
147
- function resolveAutomationsConfigPath(workspaceRoot) {
148
- return (0, import_path.join)(workspaceRoot, AUTOMATIONS_CONFIG_FILE);
149
- }
150
- var ThinkingLevelInputSchema = import_zod.z.enum(["off", "low", "medium", "high", "xhigh", "max", "think"]).transform((value) => normalizeThinkingLevel(value)).optional();
151
- var PromptActionSchema = import_zod.z.object({
152
- type: import_zod.z.literal("prompt"),
153
- prompt: import_zod.z.string().min(1, "Prompt cannot be empty"),
154
- llmConnection: import_zod.z.string().min(1).optional(),
155
- model: import_zod.z.string().min(1).optional(),
156
- thinkingLevel: ThinkingLevelInputSchema
157
- });
158
- var WebhookActionSchema = import_zod.z.object({
159
- type: import_zod.z.literal("webhook"),
160
- url: import_zod.z.string().min(1, "URL cannot be empty").refine(
161
- (url) => {
162
- if (url.includes("$")) return true;
163
- try {
164
- const parsed = new URL(url);
165
- return parsed.protocol === "http:" || parsed.protocol === "https:";
166
- } catch {
167
- return false;
168
- }
169
- },
170
- "URL must be a valid http/https URL or contain $VAR templates"
171
- ),
172
- method: import_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
173
- headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
174
- bodyFormat: import_zod.z.enum(["json", "form", "raw"]).optional(),
175
- body: import_zod.z.unknown().optional(),
176
- captureResponse: import_zod.z.boolean().optional(),
177
- auth: import_zod.z.union([
178
- import_zod.z.object({
179
- type: import_zod.z.literal("basic"),
180
- username: import_zod.z.string().min(1),
181
- password: import_zod.z.string()
182
- }),
183
- import_zod.z.object({
184
- type: import_zod.z.literal("bearer"),
185
- token: import_zod.z.string().min(1)
186
- })
187
- ]).optional()
188
- });
189
- var ActionDefinitionSchema = import_zod.z.union([
190
- PromptActionSchema,
191
- WebhookActionSchema,
192
- import_zod.z.object({ type: import_zod.z.string() }).passthrough()
193
- ]);
194
- var VALID_WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
195
- var TimeConditionSchema = import_zod.z.object({
196
- condition: import_zod.z.literal("time"),
197
- after: import_zod.z.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format").optional(),
198
- before: import_zod.z.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format").optional(),
199
- weekday: import_zod.z.array(import_zod.z.enum(VALID_WEEKDAYS)).optional(),
200
- timezone: import_zod.z.string().optional()
201
- });
202
- var StateConditionSchema = import_zod.z.object({
203
- condition: import_zod.z.literal("state"),
204
- field: import_zod.z.string().min(1, "Field name cannot be empty"),
205
- value: import_zod.z.unknown().optional(),
206
- from: import_zod.z.unknown().optional(),
207
- to: import_zod.z.unknown().optional(),
208
- contains: import_zod.z.string().optional(),
209
- not_value: import_zod.z.unknown().optional()
210
- }).superRefine((data, ctx) => {
211
- const hasValue = data.value !== void 0;
212
- const hasFromOrTo = data.from !== void 0 || data.to !== void 0;
213
- const hasContains = data.contains !== void 0;
214
- const hasNotValue = data.not_value !== void 0;
215
- const operatorCount = (hasValue ? 1 : 0) + (hasFromOrTo ? 1 : 0) + (hasContains ? 1 : 0) + (hasNotValue ? 1 : 0);
216
- if (operatorCount === 0) {
217
- ctx.addIssue({
218
- code: import_zod.z.ZodIssueCode.custom,
219
- message: "State condition must have at least one operator (value, from/to, contains, or not_value)",
220
- path: ["field"]
221
- });
222
- return;
223
- }
224
- if (operatorCount > 1) {
225
- ctx.addIssue({
226
- code: import_zod.z.ZodIssueCode.custom,
227
- message: "State condition must use exactly one operator group (value, from/to, contains, or not_value)",
228
- path: ["field"]
229
- });
230
- }
231
- });
232
- var AutomationConditionSchema = import_zod.z.lazy(
233
- () => import_zod.z.union([
234
- TimeConditionSchema,
235
- StateConditionSchema,
236
- import_zod.z.object({
237
- condition: import_zod.z.enum(["and", "or", "not"]),
238
- conditions: import_zod.z.array(AutomationConditionSchema).min(1, "Logical condition must have at least one sub-condition")
239
- })
240
- ])
241
- );
242
- var AutomationMatcherSchema = import_zod.z.object({
243
- id: import_zod.z.string().optional(),
244
- name: import_zod.z.string().optional(),
245
- matcher: import_zod.z.string().optional(),
246
- cron: import_zod.z.string().optional(),
247
- timezone: import_zod.z.string().optional(),
248
- permissionMode: import_zod.z.enum(["safe", "ask", "allow-all"]).optional(),
249
- labels: import_zod.z.array(import_zod.z.string()).optional(),
250
- enabled: import_zod.z.boolean().optional(),
251
- conditions: import_zod.z.array(AutomationConditionSchema).optional(),
252
- // Telegram forum-topic name (1–128 chars). Silently ignored at runtime when
253
- // no supergroup is paired or the Telegram adapter is not connected.
254
- telegramTopic: import_zod.z.string().min(1).max(128).optional(),
255
- actions: import_zod.z.array(ActionDefinitionSchema).min(1, "At least one action required")
256
- });
257
- var DEPRECATED_EVENT_ALIASES = {
258
- "TodoStateChange": "SessionStatusChange"
259
- };
260
- var VALID_EVENTS = [
261
- ...APP_EVENTS,
262
- ...AGENT_EVENTS,
263
- ...Object.keys(DEPRECATED_EVENT_ALIASES)
264
- ];
265
- var AutomationsConfigSchema = import_zod.z.object({
266
- version: import_zod.z.number().optional(),
267
- automations: import_zod.z.record(import_zod.z.string(), import_zod.z.array(AutomationMatcherSchema)).optional()
268
- }).transform((data) => {
269
- const automations = data.automations ?? {};
270
- const validAutomations = {};
271
- for (const [event, matchers] of Object.entries(automations)) {
272
- if (VALID_EVENTS.includes(event)) {
273
- const canonical = DEPRECATED_EVENT_ALIASES[event];
274
- if (canonical) {
275
- validAutomations[canonical] = [...validAutomations[canonical] ?? [], ...matchers];
276
- } else {
277
- validAutomations[event] = [...validAutomations[event] ?? [], ...matchers];
278
- }
279
- }
280
- }
281
- return { version: data.version, automations: validAutomations };
282
- });
283
- function zodErrorToIssues(error, file) {
284
- return error.issues.map((issue) => ({
285
- file,
286
- path: issue.path.join(".") || "root",
287
- message: issue.message,
288
- severity: "error"
289
- }));
290
- }
291
- function isValidLabelId(_workspaceRoot, _labelId) {
292
- return true;
293
- }
294
- function extractLabelId(label) {
295
- const separatorIndex = label.indexOf("::");
296
- if (separatorIndex !== -1) {
297
- return label.slice(0, separatorIndex);
298
- }
299
- return label;
300
- }
301
- function getLlmConnection(_slug) {
302
- return null;
303
- }
304
- function getDefaultModelsForConnection(_providerType, _piAuthProvider) {
305
- return [];
306
- }
307
- var MAX_CONDITION_DEPTH_EXCLUSIVE = 8;
308
- var CONDITION_DEPTH_WARNING_THRESHOLD = 4;
309
- function validateAutomationsConfig(content) {
310
- const result = AutomationsConfigSchema.safeParse(content);
311
- if (!result.success) {
312
- const errors = result.error.issues.map((issue) => {
313
- const path = issue.path.join(".");
314
- return path ? `${path}: ${issue.message}` : issue.message;
315
- });
316
- return { valid: false, errors, config: null };
317
- }
318
- const schemaConfig = result.data;
319
- const semanticErrors = [];
320
- runMatcherSemanticValidations(schemaConfig, AUTOMATIONS_CONFIG_FILE, semanticErrors, []);
321
- if (semanticErrors.length > 0) {
322
- const errors = semanticErrors.map((issue) => issue.path ? `${issue.path}: ${issue.message}` : issue.message);
323
- return { valid: false, errors, config: null };
324
- }
325
- return { valid: true, errors: [], config: schemaConfig };
326
- }
327
- function runMatcherSemanticValidations(config, file, errors, warnings) {
328
- for (const [event, matchers] of Object.entries(config.automations)) {
329
- if (!matchers) continue;
330
- for (let i = 0; i < matchers.length; i++) {
331
- const matcher = matchers[i];
332
- if (!matcher) continue;
333
- if (matcher.permissionMode === "allow-all") {
334
- warnings.push({
335
- file,
336
- path: `automations.${event}[${i}].permissionMode`,
337
- message: 'permissionMode "allow-all" bypasses all security checks \u2014 use with caution',
338
- severity: "warning",
339
- suggestion: 'Consider using "safe" or "ask" permission mode instead'
340
- });
341
- }
342
- if (matcher.matcher) {
343
- const MAX_REGEX_LENGTH = 500;
344
- if (matcher.matcher.length > MAX_REGEX_LENGTH) {
345
- errors.push({
346
- file,
347
- path: `automations.${event}[${i}].matcher`,
348
- message: `Regex pattern too long (${matcher.matcher.length} chars, max ${MAX_REGEX_LENGTH})`,
349
- severity: "error",
350
- suggestion: "Simplify the regex pattern or split into multiple matchers"
351
- });
352
- } else {
353
- try {
354
- new RegExp(matcher.matcher);
355
- const nestedQuantifiers = /\([^)]*[+*][^)]*\)[+*{]/;
356
- const riskyPatterns = /(\.\*){2,}|(\.\+){2,}|\([^)]*\|[^)]*\)[+*{]/;
357
- if (nestedQuantifiers.test(matcher.matcher) || riskyPatterns.test(matcher.matcher)) {
358
- errors.push({
359
- file,
360
- path: `automations.${event}[${i}].matcher`,
361
- message: "Regex pattern rejected: potential catastrophic backtracking (ReDoS)",
362
- severity: "error",
363
- suggestion: "Avoid nested quantifiers like (a+)+, (.*)+, (.+)*, ([a-z]+)+, and repeated alternation like (a|a)+"
364
- });
365
- }
366
- } catch (e) {
367
- errors.push({
368
- file,
369
- path: `automations.${event}[${i}].matcher`,
370
- message: `Invalid regex pattern: ${e instanceof Error ? e.message : "Unknown error"}`,
371
- severity: "error",
372
- suggestion: "Fix the regex pattern or remove the matcher to match all events"
373
- });
374
- }
375
- }
376
- }
377
- if (matcher.cron) {
378
- try {
379
- new import_croner.Cron(matcher.cron);
380
- } catch (e) {
381
- errors.push({
382
- file,
383
- path: `automations.${event}[${i}].cron`,
384
- message: `Invalid cron expression: ${e instanceof Error ? e.message : "Unknown error"}`,
385
- severity: "error",
386
- suggestion: "Use standard 5-field cron format: minute hour day-of-month month day-of-week"
387
- });
388
- }
389
- }
390
- if (matcher.timezone) {
391
- try {
392
- Intl.DateTimeFormat(void 0, { timeZone: matcher.timezone });
393
- } catch {
394
- errors.push({
395
- file,
396
- path: `automations.${event}[${i}].timezone`,
397
- message: `Invalid timezone: ${matcher.timezone}`,
398
- severity: "error",
399
- suggestion: 'Use IANA timezone format like "Europe/Budapest" or "America/New_York"'
400
- });
401
- }
402
- }
403
- if (matcher.actions) {
404
- for (let j = 0; j < matcher.actions.length; j++) {
405
- const action = matcher.actions[j];
406
- if (action && typeof action === "object" && "type" in action && action.type === "webhook" && "url" in action && typeof action.url === "string" && action.url.includes("$")) {
407
- warnings.push({
408
- file,
409
- path: `automations.${event}[${i}].actions[${j}].url`,
410
- message: "Webhook URL contains variable templates \u2014 will be validated at runtime after expansion",
411
- severity: "warning",
412
- suggestion: "Ensure the referenced WEFT_WH_* variables are set in your shell profile"
413
- });
414
- }
415
- }
416
- }
417
- if (matcher.cron && event !== "SchedulerTick") {
418
- warnings.push({
419
- file,
420
- path: `automations.${event}[${i}].cron`,
421
- message: "Cron expressions are only used for SchedulerTick events",
422
- severity: "warning",
423
- suggestion: "Move this automation to the SchedulerTick event or use matcher instead"
424
- });
425
- }
426
- if (matcher.conditions && Array.isArray(matcher.conditions)) {
427
- validateConditionsArray(matcher.conditions, `automations.${event}[${i}].conditions`, event, file, errors, warnings, 0);
428
- }
429
- }
430
- }
431
- }
432
- function validateAutomationsContent(jsonString, fileName) {
433
- const file = fileName ?? AUTOMATIONS_CONFIG_FILE;
434
- const errors = [];
435
- const warnings = [];
436
- let content;
437
- try {
438
- content = JSON.parse(jsonString);
439
- } catch (e) {
440
- return {
441
- valid: false,
442
- errors: [{
443
- file,
444
- path: "",
445
- message: `Invalid JSON: ${e instanceof Error ? e.message : "Unknown error"}`,
446
- severity: "error"
447
- }],
448
- warnings: []
449
- };
450
- }
451
- const result = AutomationsConfigSchema.safeParse(content);
452
- if (!result.success) {
453
- errors.push(...zodErrorToIssues(result.error, file));
454
- return { valid: false, errors, warnings };
455
- }
456
- const config = result.data;
457
- const matcherCount = Object.values(config.automations).reduce(
458
- (sum, matchers) => sum + (matchers?.length ?? 0),
459
- 0
460
- );
461
- if (matcherCount === 0) {
462
- warnings.push({
463
- file,
464
- path: "automations",
465
- message: "No automations configured",
466
- severity: "warning",
467
- suggestion: "Add automation definitions under event names like SessionStatusChange, LabelAdd, etc."
468
- });
469
- }
470
- try {
471
- const rawConfig = JSON.parse(jsonString);
472
- if (rawConfig.automations) {
473
- for (const event of Object.keys(rawConfig.automations)) {
474
- const canonical = DEPRECATED_EVENT_ALIASES[event];
475
- if (canonical) {
476
- warnings.push({
477
- file,
478
- path: `automations.${event}`,
479
- message: `Event '${event}' has been renamed to '${canonical}'. The old name still works but is deprecated.`,
480
- severity: "warning",
481
- suggestion: `Rename '${event}' to '${canonical}' in your config`
482
- });
483
- }
484
- }
485
- }
486
- } catch {
487
- }
488
- runMatcherSemanticValidations(config, file, errors, warnings);
489
- return {
490
- valid: errors.length === 0,
491
- errors,
492
- warnings
493
- };
494
- }
495
- function createAutomationsConfigDoctorReport(jsonString, fileName) {
496
- const diagnostics = validateAutomationsContent(jsonString, fileName);
497
- const summary = summarizeAutomationsConfig(jsonString);
498
- return {
499
- domain: "automations",
500
- valid: diagnostics.valid,
501
- errors: diagnostics.errors,
502
- warnings: diagnostics.warnings,
503
- summary
504
- };
505
- }
506
- function validateAutomations(workspaceRoot) {
507
- const configPath = resolveAutomationsConfigPath(workspaceRoot);
508
- const file = "automations.json";
509
- if (!(0, import_fs.existsSync)(configPath)) {
510
- return {
511
- valid: true,
512
- errors: [],
513
- warnings: [{
514
- file,
515
- path: "",
516
- message: "No automations configuration found (no automations configured)",
517
- severity: "warning"
518
- }]
519
- };
520
- }
521
- let raw;
522
- try {
523
- raw = (0, import_fs.readFileSync)(configPath, "utf-8");
524
- } catch (e) {
525
- return {
526
- valid: false,
527
- errors: [{
528
- file,
529
- path: "",
530
- message: `Cannot read file: ${e instanceof Error ? e.message : "Unknown error"}`,
531
- severity: "error"
532
- }],
533
- warnings: []
534
- };
535
- }
536
- let content;
537
- try {
538
- content = JSON.parse(raw);
539
- } catch (e) {
540
- return {
541
- valid: false,
542
- errors: [{
543
- file,
544
- path: "",
545
- message: `Invalid JSON: ${e instanceof Error ? e.message : "Unknown error"}`,
546
- severity: "error"
547
- }],
548
- warnings: []
549
- };
550
- }
551
- const contentResult = validateAutomationsContent(raw);
552
- if (!contentResult.valid) {
553
- return contentResult;
554
- }
555
- const errors = [];
556
- const warnings = [...contentResult.warnings];
557
- try {
558
- const config = content;
559
- const labelEntries = config.automations;
560
- if (labelEntries) {
561
- for (const [event, matchers] of Object.entries(labelEntries)) {
562
- if (!matchers) continue;
563
- for (let i = 0; i < matchers.length; i++) {
564
- const matcher = matchers[i];
565
- if (matcher?.labels) {
566
- for (const label of matcher.labels) {
567
- const labelId = extractLabelId(label);
568
- if (!isValidLabelId(workspaceRoot, labelId)) {
569
- warnings.push({
570
- file,
571
- path: `automations.${event}[${i}].labels`,
572
- message: `Label "${labelId}" does not exist in workspace`,
573
- severity: "warning",
574
- suggestion: `Create this label in labels/config.json or use an existing label ID`
575
- });
576
- }
577
- }
578
- }
579
- const actions = matcher?.actions;
580
- if (actions) {
581
- for (const action of actions) {
582
- if (action.type !== "prompt") continue;
583
- if (action.llmConnection) {
584
- const connection = getLlmConnection(action.llmConnection);
585
- if (!connection) {
586
- errors.push({
587
- file,
588
- path: `automations.${event}[${i}].actions`,
589
- message: `LLM connection "${action.llmConnection}" not found in config`,
590
- severity: "error",
591
- suggestion: "Check the connection slug in AI Settings or config.json"
592
- });
593
- } else if (action.model) {
594
- const availableModels = connection.models ?? getDefaultModelsForConnection(connection.providerType, connection.piAuthProvider);
595
- const modelIds = availableModels.map((m) => typeof m === "string" ? m : m.id);
596
- const modelValue = action.model;
597
- const isAvailable = modelIds.some(
598
- (id) => id === modelValue || id.endsWith(`/${modelValue}`) || // Also match short aliases: "haiku" → any id containing "haiku", "sonnet" → "sonnet", etc.
599
- id.toLowerCase().includes(modelValue.toLowerCase())
600
- );
601
- if (!isAvailable) {
602
- warnings.push({
603
- file,
604
- path: `automations.${event}[${i}].actions`,
605
- message: `Model "${modelValue}" may not be available on connection "${action.llmConnection}" (${connection.providerType})`,
606
- severity: "warning",
607
- suggestion: `Available models: ${modelIds.slice(0, 5).join(", ")}${modelIds.length > 5 ? `, ... (${modelIds.length} total)` : ""}`
608
- });
609
- }
610
- }
611
- }
612
- }
613
- }
614
- }
615
- }
616
- }
617
- } catch {
618
- }
619
- const allErrors = [...contentResult.errors, ...errors];
620
- return {
621
- valid: allErrors.length === 0,
622
- errors: allErrors,
623
- warnings
624
- };
625
- }
626
- function summarizeAutomationsConfig(jsonString) {
627
- try {
628
- const parsed = JSON.parse(jsonString);
629
- let matcherCount = 0;
630
- let actionCount = 0;
631
- for (const matchers of Object.values(parsed.automations ?? {})) {
632
- if (!Array.isArray(matchers)) continue;
633
- matcherCount += matchers.length;
634
- actionCount += matchers.reduce((sum, matcher) => {
635
- const actions = matcher && typeof matcher === "object" ? matcher.actions : void 0;
636
- return sum + (Array.isArray(actions) ? actions.length : 0);
637
- }, 0);
638
- }
639
- return { matcherCount, actionCount };
640
- } catch {
641
- return { matcherCount: 0, actionCount: 0 };
642
- }
643
- }
644
- var VALID_WEEKDAYS2 = /* @__PURE__ */ new Set(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]);
645
- var HH_MM_RE = /^\d{2}:\d{2}$/;
646
- var TRANSITION_EVENTS = /* @__PURE__ */ new Set(["PermissionModeChange", "SessionStatusChange"]);
647
- function validateConditionsArray(conditions, basePath, event, file, errors, warnings, depth) {
648
- if (depth > CONDITION_DEPTH_WARNING_THRESHOLD) {
649
- warnings.push({
650
- file,
651
- path: basePath,
652
- message: `Condition nesting depth ${depth} \u2014 consider simplifying`,
653
- severity: "warning"
654
- });
655
- }
656
- if (depth >= MAX_CONDITION_DEPTH_EXCLUSIVE) {
657
- errors.push({
658
- file,
659
- path: basePath,
660
- message: `Condition nesting exceeds maximum depth of ${MAX_CONDITION_DEPTH_EXCLUSIVE}`,
661
- severity: "error"
662
- });
663
- return;
664
- }
665
- for (let j = 0; j < conditions.length; j++) {
666
- const cond = conditions[j];
667
- if (!cond || typeof cond !== "object") continue;
668
- const path = `${basePath}[${j}]`;
669
- switch (cond.condition) {
670
- case "time":
671
- validateTimeCondition(cond, path, file, errors);
672
- break;
673
- case "state":
674
- validateStateCondition(cond, path, event, file, errors, warnings);
675
- break;
676
- case "and":
677
- case "or":
678
- case "not":
679
- if (Array.isArray(cond.conditions) && cond.conditions.length > 0) {
680
- validateConditionsArray(cond.conditions, `${path}.conditions`, event, file, errors, warnings, depth + 1);
681
- }
682
- break;
683
- }
684
- }
685
- }
686
- function validateTimeCondition(cond, path, file, errors) {
687
- if (cond.after !== void 0 && typeof cond.after === "string") {
688
- if (!HH_MM_RE.test(cond.after)) {
689
- errors.push({ file, path: `${path}.after`, message: `Invalid time format: "${cond.after}" (expected HH:MM)`, severity: "error" });
690
- } else {
691
- const [h, m] = cond.after.split(":").map(Number);
692
- if ((h ?? 0) > 23 || (m ?? 0) > 59) {
693
- errors.push({ file, path: `${path}.after`, message: `Invalid time value: "${cond.after}"`, severity: "error" });
694
- }
695
- }
696
- }
697
- if (cond.before !== void 0 && typeof cond.before === "string") {
698
- if (!HH_MM_RE.test(cond.before)) {
699
- errors.push({ file, path: `${path}.before`, message: `Invalid time format: "${cond.before}" (expected HH:MM)`, severity: "error" });
700
- } else {
701
- const [h, m] = cond.before.split(":").map(Number);
702
- if ((h ?? 0) > 23 || (m ?? 0) > 59) {
703
- errors.push({ file, path: `${path}.before`, message: `Invalid time value: "${cond.before}"`, severity: "error" });
704
- }
705
- }
706
- }
707
- if (cond.weekday !== void 0 && Array.isArray(cond.weekday)) {
708
- for (const day of cond.weekday) {
709
- if (typeof day === "string" && !VALID_WEEKDAYS2.has(day)) {
710
- errors.push({ file, path: `${path}.weekday`, message: `Invalid weekday: "${day}" (expected mon-sun)`, severity: "error" });
711
- }
712
- }
713
- }
714
- if (cond.timezone !== void 0 && typeof cond.timezone === "string") {
715
- try {
716
- Intl.DateTimeFormat(void 0, { timeZone: cond.timezone });
717
- } catch {
718
- errors.push({ file, path: `${path}.timezone`, message: `Invalid timezone: "${cond.timezone}"`, severity: "error" });
719
- }
720
- }
721
- }
722
- function validateStateCondition(cond, path, event, file, errors, warnings) {
723
- const hasValue = cond.value !== void 0;
724
- const hasFrom = cond.from !== void 0;
725
- const hasTo = cond.to !== void 0;
726
- const hasContains = cond.contains !== void 0;
727
- const hasNotValue = cond.not_value !== void 0;
728
- const operatorCount = (hasValue ? 1 : 0) + (hasFrom || hasTo ? 1 : 0) + (hasContains ? 1 : 0) + (hasNotValue ? 1 : 0);
729
- if (operatorCount === 0) {
730
- errors.push({ file, path, message: "State condition must have at least one operator (value, from/to, contains, or not_value)", severity: "error" });
731
- } else if (operatorCount > 1) {
732
- errors.push({ file, path, message: "State condition must use exactly one operator group (value, from/to, contains, or not_value)", severity: "error" });
733
- }
734
- if ((hasFrom || hasTo) && !TRANSITION_EVENTS.has(event)) {
735
- warnings.push({
736
- file,
737
- path,
738
- message: `from/to transition checks are typically used with PermissionModeChange or SessionStatusChange events, not ${event}`,
739
- severity: "warning"
740
- });
741
- }
742
- }
743
- function sanitizeForShell(value) {
744
- return value.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
745
- }
746
- function createLogger(scope) {
747
- return {
748
- debug: (...args) => console.debug(`[${scope}]`, ...args),
749
- info: (...args) => console.info(`[${scope}]`, ...args),
750
- warn: (...args) => console.warn(`[${scope}]`, ...args),
751
- error: (...args) => console.error(`[${scope}]`, ...args)
752
- };
753
- }
754
- var log = createLogger("cron-matcher");
755
- function matchesCron(cronExpr, timezone) {
756
- try {
757
- const options = timezone ? { timezone } : {};
758
- const job = new import_croner2.Cron(cronExpr, options);
759
- const now = /* @__PURE__ */ new Date();
760
- const startOfMinute = new Date(now);
761
- startOfMinute.setSeconds(0, 0);
762
- const checkFrom = new Date(startOfMinute.getTime() - 1e3);
763
- const nextRun = job.nextRun(checkFrom);
764
- log.debug(`[matchesCron] cron=${cronExpr}, tz=${timezone || "default"}`);
765
- log.debug(`[matchesCron] now=${now.toISOString()}, startOfMinute=${startOfMinute.toISOString()}`);
766
- log.debug(`[matchesCron] checkFrom=${checkFrom.toISOString()}, nextRun=${nextRun?.toISOString() || "null"}`);
767
- if (!nextRun) {
768
- log.debug(`[matchesCron] No nextRun, returning false`);
769
- return false;
770
- }
771
- const matches = nextRun.getTime() >= startOfMinute.getTime() && nextRun.getTime() < startOfMinute.getTime() + 6e4;
772
- log.debug(`[matchesCron] matches=${matches} (nextRun ${nextRun.getTime()} vs startOfMinute ${startOfMinute.getTime()} to ${startOfMinute.getTime() + 6e4})`);
773
- return matches;
774
- } catch (e) {
775
- console.error(`[matchesCron] Error:`, e);
776
- return false;
777
- }
778
- }
779
- var TRANSITION_FIELDS = {
780
- permissionMode: { to: "newMode", from: "oldMode" },
781
- sessionStatus: { to: "newState", from: "oldState" }
782
- };
783
- var WEEKDAY_MAP = {
784
- mon: 1,
785
- tue: 2,
786
- wed: 3,
787
- thu: 4,
788
- fri: 5,
789
- sat: 6,
790
- sun: 7
791
- };
792
- function evaluateConditions(conditions, context) {
793
- if (conditions.length === 0) return true;
794
- for (const condition of conditions) {
795
- if (!evaluateCondition(condition, context, 0)) return false;
796
- }
797
- return true;
798
- }
799
- function evaluateCondition(condition, context, depth) {
800
- if (depth >= MAX_CONDITION_DEPTH_EXCLUSIVE) return false;
801
- switch (condition.condition) {
802
- case "time":
803
- return evaluateTimeCondition(condition, context);
804
- case "state":
805
- return evaluateStateCondition(condition, context);
806
- case "and":
807
- case "or":
808
- case "not":
809
- return evaluateLogicalCondition(condition, context, depth);
810
- default:
811
- return false;
812
- }
813
- }
814
- function evaluateTimeCondition(condition, context) {
815
- const now = context.now ?? /* @__PURE__ */ new Date();
816
- const tz = condition.timezone ?? context.matcherTimezone;
817
- const { hours, minutes, weekdayNum } = getTimeInTimezone(now, tz);
818
- if (condition.weekday && condition.weekday.length > 0) {
819
- const allowed = new Set(condition.weekday.map((d) => WEEKDAY_MAP[d]));
820
- if (!allowed.has(weekdayNum)) return false;
821
- }
822
- const hasAfter = condition.after !== void 0;
823
- const hasBefore = condition.before !== void 0;
824
- if (!hasAfter && !hasBefore) return true;
825
- const currentMinutes = hours * 60 + minutes;
826
- const afterMinutes = hasAfter ? parseTimeToMinutes(condition.after) : 0;
827
- const beforeMinutes = hasBefore ? parseTimeToMinutes(condition.before) : 0;
828
- if (hasAfter && hasBefore) {
829
- if (afterMinutes <= beforeMinutes) {
830
- return currentMinutes >= afterMinutes && currentMinutes < beforeMinutes;
831
- } else {
832
- return currentMinutes >= afterMinutes || currentMinutes < beforeMinutes;
833
- }
834
- }
835
- if (hasAfter) return currentMinutes >= afterMinutes;
836
- return currentMinutes < beforeMinutes;
837
- }
838
- function parseTimeToMinutes(time) {
839
- const [h, m] = time.split(":").map(Number);
840
- return (h ?? 0) * 60 + (m ?? 0);
841
- }
842
- function getTimeInTimezone(date, timezone) {
843
- if (timezone) {
844
- try {
845
- const formatter = new Intl.DateTimeFormat("en-US", {
846
- timeZone: timezone,
847
- hour: "numeric",
848
- minute: "numeric",
849
- weekday: "short",
850
- hour12: false
851
- });
852
- const parts = formatter.formatToParts(date);
853
- const hours2 = Number(parts.find((p) => p.type === "hour")?.value ?? 0);
854
- const minutes2 = Number(parts.find((p) => p.type === "minute")?.value ?? 0);
855
- const weekdayStr = parts.find((p) => p.type === "weekday")?.value?.toLowerCase().slice(0, 3) ?? "";
856
- const weekdayNum2 = WEEKDAY_MAP[weekdayStr] ?? 0;
857
- return { hours: hours2, minutes: minutes2, weekdayNum: weekdayNum2 };
858
- } catch {
859
- }
860
- }
861
- const hours = date.getHours();
862
- const minutes = date.getMinutes();
863
- const jsDay = date.getDay();
864
- const weekdayNum = jsDay === 0 ? 7 : jsDay;
865
- return { hours, minutes, weekdayNum };
866
- }
867
- function evaluateStateCondition(condition, context) {
868
- const { field } = condition;
869
- const { payload } = context;
870
- const hasFrom = condition.from !== void 0;
871
- const hasTo = condition.to !== void 0;
872
- if (hasFrom || hasTo) {
873
- const mapping = TRANSITION_FIELDS[field];
874
- const toKey = mapping?.to ?? field;
875
- const fromKey = mapping?.from ?? field;
876
- if (hasTo && payload[toKey] !== condition.to) return false;
877
- if (hasFrom && payload[fromKey] !== condition.from) return false;
878
- return true;
879
- }
880
- if (condition.contains !== void 0) {
881
- const arr = payload[field];
882
- if (!Array.isArray(arr)) return false;
883
- return arr.includes(condition.contains);
884
- }
885
- if (condition.not_value !== void 0) {
886
- const fieldValue = payload[field];
887
- if (fieldValue === void 0) return false;
888
- return fieldValue !== condition.not_value;
889
- }
890
- if (condition.value !== void 0) {
891
- return payload[field] === condition.value;
892
- }
893
- return false;
894
- }
895
- function evaluateLogicalCondition(condition, context, depth) {
896
- const { conditions } = condition;
897
- switch (condition.condition) {
898
- case "and":
899
- for (const sub of conditions) {
900
- if (!evaluateCondition(sub, context, depth + 1)) return false;
901
- }
902
- return true;
903
- case "or":
904
- for (const sub of conditions) {
905
- if (evaluateCondition(sub, context, depth + 1)) return true;
906
- }
907
- return false;
908
- case "not":
909
- for (const sub of conditions) {
910
- if (evaluateCondition(sub, context, depth + 1)) return false;
911
- }
912
- return true;
913
- default:
914
- return false;
915
- }
916
- }
917
- function toSnakeCase(str) {
918
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
919
- }
920
- function expandEnvVars(str, env) {
921
- return str.replace(/\$\{([^}]+)\}/g, (_, varName) => env[varName] ?? "").replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, varName) => env[varName] ?? "");
922
- }
923
- function parsePromptReferences(prompt) {
924
- const mentions = [];
925
- const matches = prompt.matchAll(/(?:^|[\s(])@([a-zA-Z][a-zA-Z0-9-]*)/g);
926
- for (const match of matches) {
927
- const captured = match[1];
928
- if (captured) {
929
- const mention = captured.toLowerCase();
930
- if (!mentions.includes(mention)) {
931
- mentions.push(mention);
932
- }
933
- }
934
- }
935
- return { mentions };
936
- }
937
- function getMatchValue(event, data) {
938
- switch (event) {
939
- case "LabelAdd":
940
- case "LabelRemove":
941
- return String(data.label ?? "");
942
- case "LabelConfigChange":
943
- return "";
944
- // Always matches
945
- case "PermissionModeChange":
946
- return String(data.newMode ?? "");
947
- case "FlagChange":
948
- return String(data.isFlagged ?? false);
949
- case "SessionStatusChange":
950
- return String(data.newStatus ?? data.newState ?? "");
951
- case "PreToolUse":
952
- case "PostToolUse":
953
- return String(data.toolName ?? data.data?.tool_name ?? "");
954
- case "SchedulerTick":
955
- return "";
956
- default:
957
- return JSON.stringify(data);
958
- }
959
- }
960
- function getMatchValueForSdkInput(event, input) {
961
- switch (event) {
962
- case "PreToolUse":
963
- case "PostToolUse":
964
- case "PostToolUseFailure":
965
- case "PermissionRequest":
966
- return input.tool_name ?? "";
967
- case "Notification":
968
- return input.message ?? "";
969
- case "SessionStart":
970
- return input.source ?? "";
971
- case "SubagentStart":
972
- case "SubagentStop":
973
- return input.agent_type ?? "";
974
- default:
975
- return "";
976
- }
977
- }
978
- function matchesBasePredicate(matcher, event, matchValue) {
979
- if (matcher.enabled === false) return false;
980
- if (event === "SchedulerTick") {
981
- return !!matcher.cron && matchesCron(matcher.cron, matcher.timezone);
982
- }
983
- if (!matcher.matcher) return true;
984
- try {
985
- return new RegExp(matcher.matcher).test(matchValue);
986
- } catch {
987
- return false;
988
- }
989
- }
990
- function matcherMatchesWithContext(matcher, event, context) {
991
- if (!matchesBasePredicate(matcher, event, context.matchValue)) return false;
992
- if (matcher.conditions?.length) {
993
- return evaluateConditions(matcher.conditions, {
994
- payload: context.payload,
995
- matcherTimezone: context.matcherTimezone ?? matcher.timezone
996
- });
997
- }
998
- return true;
999
- }
1000
- function matcherMatches(matcher, event, data) {
1001
- return matcherMatchesWithContext(matcher, event, {
1002
- matchValue: getMatchValue(event, data),
1003
- payload: data,
1004
- matcherTimezone: matcher.timezone
1005
- });
1006
- }
1007
- function matcherMatchesSdk(matcher, event, input) {
1008
- return matcherMatchesWithContext(matcher, event, {
1009
- matchValue: getMatchValueForSdkInput(event, input),
1010
- payload: input,
1011
- matcherTimezone: matcher.timezone
1012
- });
1013
- }
1014
- function cleanEnv() {
1015
- return Object.fromEntries(
1016
- Object.entries(process.env).filter((e) => e[1] !== void 0)
1017
- );
1018
- }
1019
- var PAYLOAD_SKIP_KEYS = /* @__PURE__ */ new Set(["sessionId", "sessionName", "workspaceId", "timestamp"]);
1020
- function buildBaseEventEnv(event, payload) {
1021
- const env = {
1022
- WEFT_EVENT: event,
1023
- WEFT_EVENT_DATA: JSON.stringify(payload)
1024
- };
1025
- if (payload.sessionId) env.WEFT_SESSION_ID = payload.sessionId;
1026
- if (payload.sessionName) env.WEFT_SESSION_NAME = payload.sessionName;
1027
- if (payload.workspaceId) env.WEFT_WORKSPACE_ID = payload.workspaceId;
1028
- const sessionMetadata = {};
1029
- if (payload.sessionId) sessionMetadata.id = payload.sessionId;
1030
- if (payload.sessionName) sessionMetadata.name = payload.sessionName;
1031
- if (Object.keys(sessionMetadata).length > 0) {
1032
- env.WEFT_SESSION_METADATA = JSON.stringify(sessionMetadata);
1033
- }
1034
- if (event === "SchedulerTick") {
1035
- const now = /* @__PURE__ */ new Date();
1036
- env.WEFT_LOCAL_TIME = now.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" });
1037
- env.WEFT_LOCAL_DATE = now.toISOString().split("T")[0];
1038
- }
1039
- for (const [key, value] of Object.entries(payload)) {
1040
- if (PAYLOAD_SKIP_KEYS.has(key)) continue;
1041
- const envKey = `WEFT_${toSnakeCase(key).toUpperCase()}`;
1042
- env[envKey] = typeof value === "string" ? value : String(value);
1043
- }
1044
- return env;
1045
- }
1046
- function buildEnvFromPayload(event, payload) {
1047
- const base = buildBaseEventEnv(event, payload);
1048
- const env = { ...cleanEnv(), ...base };
1049
- if (payload.sessionName) env.WEFT_SESSION_NAME = sanitizeForShell(payload.sessionName);
1050
- for (const [key, value] of Object.entries(payload)) {
1051
- if (PAYLOAD_SKIP_KEYS.has(key)) continue;
1052
- const envKey = `WEFT_${toSnakeCase(key).toUpperCase()}`;
1053
- env[envKey] = typeof value === "string" ? sanitizeForShell(value) : String(value);
1054
- }
1055
- return env;
1056
- }
1057
- function buildWebhookEnv(event, payload) {
1058
- const env = buildBaseEventEnv(event, payload);
1059
- for (const [key, value] of Object.entries(process.env)) {
1060
- if (key.startsWith("WEFT_WH_") && value !== void 0) {
1061
- env[key] = value;
1062
- }
1063
- }
1064
- return env;
1065
- }
1066
- function buildEnvFromSdkInput(event, input) {
1067
- const env = {
1068
- ...cleanEnv(),
1069
- WEFT_EVENT: event
1070
- };
1071
- switch (event) {
1072
- case "PreToolUse":
1073
- case "PostToolUse":
1074
- if (input.tool_name) env.WEFT_TOOL_NAME = input.tool_name;
1075
- if (input.tool_input) env.WEFT_TOOL_INPUT = sanitizeForShell(JSON.stringify(input.tool_input));
1076
- if (input.tool_response) env.WEFT_TOOL_RESPONSE = sanitizeForShell(input.tool_response);
1077
- break;
1078
- case "PostToolUseFailure":
1079
- if (input.tool_name) env.WEFT_TOOL_NAME = input.tool_name;
1080
- if (input.tool_input) env.WEFT_TOOL_INPUT = sanitizeForShell(JSON.stringify(input.tool_input));
1081
- if (input.error) env.WEFT_ERROR = sanitizeForShell(input.error);
1082
- break;
1083
- case "UserPromptSubmit":
1084
- if (input.prompt) env.WEFT_PROMPT = sanitizeForShell(input.prompt);
1085
- break;
1086
- case "SessionStart":
1087
- if (input.source) env.WEFT_SOURCE = input.source;
1088
- if (input.model) env.WEFT_MODEL = input.model;
1089
- break;
1090
- case "SubagentStart":
1091
- case "SubagentStop":
1092
- if (input.agent_id) env.WEFT_AGENT_ID = input.agent_id;
1093
- if (input.agent_type) env.WEFT_AGENT_TYPE = input.agent_type;
1094
- break;
1095
- case "Notification":
1096
- if (input.message) env.WEFT_MESSAGE = sanitizeForShell(input.message);
1097
- if (input.title) env.WEFT_TITLE = sanitizeForShell(input.title);
1098
- break;
1099
- // SessionEnd, Stop, PreCompact, PermissionRequest, Setup have no additional fields
1100
- default:
1101
- break;
1102
- }
1103
- return env;
1104
- }
1105
- function createAutomationRuntimeGuard() {
1106
- const seen = /* @__PURE__ */ new Set();
1107
- return {
1108
- shouldDispatch(key) {
1109
- return !seen.has(keyString(key));
1110
- },
1111
- remember(key) {
1112
- seen.add(keyString(key));
1113
- }
1114
- };
1115
- }
1116
- async function executeAutomationPrompt(options) {
1117
- const automationId = automationIdForPrompt(options.prompt);
1118
- const origin = { type: "automation", id: automationId };
1119
- const dispatchKey = {
1120
- runtimeSessionId: options.runtime.sessionId,
1121
- automationId,
1122
- prompt: options.prompt.prompt
1123
- };
1124
- const triggered = createTriggeredItem(options.prompt, automationId, origin);
1125
- if (options.guard && !options.guard.shouldDispatch(dispatchKey)) {
1126
- return {
1127
- skipped: true,
1128
- timelineItems: [
1129
- triggered,
1130
- createResultItem(options.prompt, automationId, false, {
1131
- skipped: true,
1132
- reason: "automation loop guard blocked duplicate prompt dispatch"
1133
- })
1134
- ]
1135
- };
1136
- }
1137
- try {
1138
- await options.runtime.commands.sendMessage(options.prompt.prompt, {
1139
- commandOrigin: origin,
1140
- ...options.prompt.permissionMode ? { permissionMode: options.prompt.permissionMode } : {}
1141
- });
1142
- options.guard?.remember(dispatchKey);
1143
- return {
1144
- skipped: false,
1145
- timelineItems: [
1146
- triggered,
1147
- createResultItem(options.prompt, automationId, true)
1148
- ]
1149
- };
1150
- } catch (error) {
1151
- return {
1152
- skipped: false,
1153
- timelineItems: [
1154
- triggered,
1155
- createResultItem(options.prompt, automationId, false, {
1156
- reason: error instanceof Error ? error.message : String(error)
1157
- })
1158
- ]
1159
- };
1160
- }
1161
- }
1162
- function automationIdForPrompt(prompt) {
1163
- return prompt.matcherId ?? prompt.automationName ?? "automation";
1164
- }
1165
- function createTriggeredItem(prompt, automationId, origin) {
1166
- return {
1167
- type: "automation_triggered",
1168
- automation: {
1169
- automationId,
1170
- origin,
1171
- detail: {
1172
- name: prompt.automationName,
1173
- mentions: prompt.mentions,
1174
- labels: prompt.labels,
1175
- permissionMode: prompt.permissionMode
1176
- }
1177
- }
1178
- };
1179
- }
1180
- function createResultItem(prompt, automationId, success, extras = {}) {
1181
- return {
1182
- type: "automation_action_result",
1183
- result: {
1184
- type: "prompt",
1185
- automationId,
1186
- success,
1187
- sessionId: prompt.sessionId,
1188
- ...extras
1189
- }
1190
- };
1191
- }
1192
- function keyString(key) {
1193
- return `${key.runtimeSessionId}:${key.automationId}:${key.prompt}`;
1194
- }
1195
- function createAutomationTimelineBridge(options) {
1196
- return {
1197
- handleTimelineEnvelope(envelope) {
1198
- for (const event of projectTimelineEnvelopeToAutomationInput(envelope)) {
1199
- options.publish(event);
1200
- }
1201
- }
1202
- };
1203
- }
1204
- function projectTimelineEnvelopeToAutomationInput(envelope) {
1205
- const { item, sessionId, timestamp } = envelope;
1206
- switch (item.type) {
1207
- case "permission_requested":
1208
- return [{
1209
- event: "PermissionRequest",
1210
- source: "timeline",
1211
- sessionId,
1212
- matchValue: item.request.toolName,
1213
- payload: compactRecord({
1214
- requestId: item.request.requestId,
1215
- toolName: item.request.toolName,
1216
- reason: item.request.reason,
1217
- input: item.request.input,
1218
- scope: item.request.scope
1219
- }),
1220
- timestamp
1221
- }];
1222
- case "permission_resolved":
1223
- return [{
1224
- event: "PermissionResolved",
1225
- source: "policy",
1226
- sessionId,
1227
- matchValue: item.resolution.allowed ? "allowed" : "denied",
1228
- payload: {
1229
- requestId: item.requestId,
1230
- resolution: item.resolution
1231
- },
1232
- timestamp
1233
- }];
1234
- case "source_state_changed": {
1235
- const source = asRecord(item.source);
1236
- const sourceSlug = stringValue(source.sourceSlug) ?? stringValue(source.slug) ?? stringValue(source.id);
1237
- return [{
1238
- event: "SourceStateChange",
1239
- source: "source",
1240
- sessionId,
1241
- matchValue: sourceSlug,
1242
- payload: item.source,
1243
- timestamp
1244
- }];
1245
- }
1246
- case "skill_activated": {
1247
- const skill = asRecord(item.skill);
1248
- const skillSlug = stringValue(skill.skillSlug) ?? stringValue(skill.slug) ?? stringValue(skill.id);
1249
- return [{
1250
- event: "SkillActivated",
1251
- source: "skill",
1252
- sessionId,
1253
- matchValue: skillSlug,
1254
- payload: item.skill,
1255
- timestamp
1256
- }];
1257
- }
1258
- case "host_state_changed":
1259
- return hostStateEvents(item.state, sessionId, timestamp);
1260
- case "turn_started":
1261
- return [{
1262
- event: "SessionStart",
1263
- source: "timeline",
1264
- sessionId,
1265
- matchValue: item.turnId,
1266
- payload: item,
1267
- timestamp
1268
- }];
1269
- case "turn_completed":
1270
- return [{
1271
- event: "SessionEnd",
1272
- source: "timeline",
1273
- sessionId,
1274
- matchValue: item.turnId,
1275
- payload: item,
1276
- timestamp
1277
- }];
1278
- default:
1279
- return [];
1280
- }
1281
- }
1282
- function hostStateEvents(state, fallbackSessionId, timestamp) {
1283
- const record = asRecord(state);
1284
- const sessionId = stringValue(record.sessionId) ?? fallbackSessionId;
1285
- const events = [];
1286
- if (typeof record.status === "string") {
1287
- events.push({
1288
- event: "SessionStatusChange",
1289
- source: "host",
1290
- sessionId,
1291
- matchValue: record.status,
1292
- payload: state,
1293
- timestamp
1294
- });
1295
- }
1296
- if (typeof record.flagged === "boolean") {
1297
- events.push({
1298
- event: "FlagChange",
1299
- source: "host",
1300
- sessionId,
1301
- matchValue: String(record.flagged),
1302
- payload: state,
1303
- timestamp
1304
- });
1305
- }
1306
- if (Array.isArray(record.labels)) {
1307
- for (const label of record.labels) {
1308
- if (typeof label !== "string") continue;
1309
- events.push({
1310
- event: "LabelAdd",
1311
- source: "host",
1312
- sessionId,
1313
- matchValue: label,
1314
- payload: state,
1315
- timestamp
1316
- });
1317
- }
1318
- }
1319
- return events;
1320
- }
1321
- function compactRecord(record) {
1322
- return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
1323
- }
1324
- function asRecord(value) {
1325
- return value && typeof value === "object" ? value : {};
1326
- }
1327
- function stringValue(value) {
1328
- return typeof value === "string" ? value : void 0;
1329
- }
1330
- function createInMemoryAutomationHistoryStore(options = {}) {
1331
- const now = options.now ?? Date.now;
1332
- const maxEntries = options.maxEntries ?? Number.POSITIVE_INFINITY;
1333
- const records = [];
1334
- return {
1335
- async record(input) {
1336
- const record = {
1337
- ...cloneHistoryInput(input),
1338
- recordId: `automation-history:${records.length + 1}`,
1339
- attempt: input.attempt ?? 1,
1340
- timestamp: now()
1341
- };
1342
- records.push(record);
1343
- while (records.length > maxEntries) records.shift();
1344
- return cloneHistoryRecord(record);
1345
- },
1346
- async list(filter = {}) {
1347
- return filterHistoryRecords(records, filter).map(cloneHistoryRecord);
1348
- },
1349
- async getLastExecuted(matcherId) {
1350
- const record = [...records].reverse().find((candidate) => candidate.matcherId === matcherId && candidate.status !== "skipped");
1351
- return record ? cloneHistoryRecord(record) : void 0;
1352
- },
1353
- getSnapshot() {
1354
- return { records: records.map(cloneHistoryRecord) };
1355
- }
1356
- };
1357
- }
1358
- function automationHistoryInputForPromptResult(prompt, result) {
1359
- const actionResult = result.timelineItems.map((item) => item.type === "automation_action_result" ? item.result : void 0).find(Boolean);
1360
- const success = actionResult?.success === true;
1361
- const skipped = result.skipped || actionResult?.skipped === true;
1362
- const error = typeof actionResult?.reason === "string" ? actionResult.reason : void 0;
1363
- return {
1364
- matcherId: prompt.matcherId ?? prompt.automationName ?? "automation",
1365
- automationName: prompt.automationName,
1366
- sessionId: prompt.sessionId,
1367
- actionType: "prompt",
1368
- status: skipped ? "skipped" : success ? "success" : "failed",
1369
- skipped,
1370
- attempt: 1,
1371
- promptPreview: redactAutomationText(prompt.prompt),
1372
- ...error ? { error: redactAutomationText(error) } : {},
1373
- timelineItemCount: result.timelineItems.length
1374
- };
1375
- }
1376
- function filterHistoryRecords(records, filter) {
1377
- return records.filter((record) => !filter.matcherId || record.matcherId === filter.matcherId).filter((record) => !filter.sessionId || record.sessionId === filter.sessionId).filter((record) => !filter.status || record.status === filter.status).slice(0, filter.limit ?? Number.POSITIVE_INFINITY);
1378
- }
1379
- function cloneHistoryInput(input) {
1380
- return {
1381
- ...input,
1382
- ...input.metadata ? { metadata: { ...input.metadata } } : {}
1383
- };
1384
- }
1385
- function cloneHistoryRecord(record) {
1386
- return {
1387
- ...record,
1388
- ...record.metadata ? { metadata: { ...record.metadata } } : {}
1389
- };
1390
- }
1391
- function redactAutomationText(text) {
1392
- return text.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[^\s,;]+/gi, (_match, prefix, bearer = "") => `${prefix}${bearer}[REDACTED]`).replace(/(token\s*[:=]\s*)[^\s,;]+/gi, (_match, prefix) => `${prefix}[REDACTED]`).replace(/(api[_-]?key\s*[:=]\s*)[^\s,;]+/gi, (_match, prefix) => `${prefix}[REDACTED]`).replace(/(secret\s*[:=]\s*)[^\s,;]+/gi, (_match, prefix) => `${prefix}[REDACTED]`);
1393
- }
1394
- function createAutomationSchedulerHost(options) {
1395
- const now = options.now ?? Date.now;
1396
- const schedules = /* @__PURE__ */ new Map();
1397
- return {
1398
- start(input) {
1399
- const registration = { ...input };
1400
- schedules.set(input.schedulerId, registration);
1401
- return {
1402
- ...registration,
1403
- state: "started",
1404
- timestamp: now()
1405
- };
1406
- },
1407
- stop(schedulerId) {
1408
- const registration = schedules.get(schedulerId);
1409
- if (!registration) return void 0;
1410
- schedules.delete(schedulerId);
1411
- return {
1412
- ...registration,
1413
- state: "stopped",
1414
- timestamp: now()
1415
- };
1416
- },
1417
- tick(schedulerId, tickOptions = {}) {
1418
- const registration = schedules.get(schedulerId);
1419
- if (!registration) return false;
1420
- void Promise.resolve(options.publish({
1421
- event: "SchedulerTick",
1422
- source: "host",
1423
- sessionId: registration.workspaceId,
1424
- matchValue: registration.schedulerId,
1425
- payload: {
1426
- ...registration,
1427
- ...tickOptions.reason ? { reason: tickOptions.reason } : {}
1428
- },
1429
- timestamp: now()
1430
- }));
1431
- return true;
1432
- },
1433
- getSnapshot() {
1434
- return {
1435
- schedules: [...schedules.values()].map((schedule) => ({ ...schedule }))
1436
- };
1437
- }
1438
- };
1439
- }
1440
- function createRuntimeAutomationBridge(options) {
1441
- const guard = options.guard ?? createAutomationRuntimeGuard();
1442
- const timelineBridge = createAutomationTimelineBridge({
1443
- publish(event) {
1444
- void Promise.resolve(options.publish(event)).catch((error) => {
1445
- options.onError?.(toError(error));
1446
- });
1447
- }
1448
- });
1449
- options.runtime.events.connect(
1450
- (envelope) => timelineBridge.handleTimelineEnvelope(envelope),
1451
- (error) => options.onError?.(error)
1452
- );
1453
- return {
1454
- async handlePromptsReady(prompts) {
1455
- const results = [];
1456
- for (const prompt of prompts) {
1457
- const result = await executeAutomationPrompt({
1458
- runtime: options.runtime,
1459
- prompt,
1460
- guard
1461
- });
1462
- results.push(result);
1463
- if (options.historyStore) {
1464
- await options.historyStore.record(automationHistoryInputForPromptResult(prompt, result));
1465
- }
1466
- if (!options.emitTimelineItem) continue;
1467
- for (const item of result.timelineItems) {
1468
- await options.emitTimelineItem(item);
1469
- }
1470
- }
1471
- return results;
1472
- },
1473
- dispose() {
1474
- options.runtime.events.disconnect();
1475
- }
1476
- };
1477
- }
1478
- function toError(error) {
1479
- return error instanceof Error ? error : new Error(String(error));
1480
- }
1481
- var AutomationEventLogger = class {
1482
- /** Callback when an event is lost due to buffer overflow */
1483
- onEventLost = null;
1484
- constructor(_workspaceRootPath) {
1485
- }
1486
- /**
1487
- * Log an automation event.
1488
- * Stub: no persistence in OSS package.
1489
- */
1490
- log(input) {
1491
- }
1492
- /**
1493
- * Get the path to the event log file.
1494
- * Stub: returns empty string.
1495
- */
1496
- getLogPath() {
1497
- return "";
1498
- }
1499
- /**
1500
- * Dispose the logger, flushing buffered events.
1501
- * Stub: no-op.
1502
- */
1503
- async dispose() {
1504
- }
1505
- };
1506
- function redactUrl(url) {
1507
- try {
1508
- const parsed = new URL(url);
1509
- if (parsed.pathname.length > 20) {
1510
- return `${parsed.origin}${parsed.pathname.slice(0, 15)}...`;
1511
- }
1512
- return `${parsed.origin}${parsed.pathname}`;
1513
- } catch {
1514
- return url.slice(0, 30) + "...";
1515
- }
1516
- }
1517
- function createWebhookHistoryEntry(opts) {
1518
- return {
1519
- id: opts.matcherId,
1520
- ts: Date.now(),
1521
- ok: opts.ok,
1522
- webhook: {
1523
- method: opts.method ?? DEFAULT_WEBHOOK_METHOD,
1524
- url: redactUrl(opts.url),
1525
- statusCode: opts.statusCode,
1526
- durationMs: opts.durationMs,
1527
- ...opts.attempts && opts.attempts > 1 ? { attempts: opts.attempts } : {},
1528
- ...opts.error ? { error: opts.error.slice(0, HISTORY_FIELD_MAX_LENGTH) } : {},
1529
- ...opts.responseBody ? { responseBody: opts.responseBody.slice(0, HISTORY_FIELD_MAX_LENGTH) } : {}
1530
- }
1531
- };
1532
- }
1533
- function createPromptHistoryEntry(opts) {
1534
- return {
1535
- id: opts.matcherId,
1536
- ts: Date.now(),
1537
- ok: opts.ok,
1538
- ...opts.sessionId ? { sessionId: opts.sessionId } : {},
1539
- ...opts.prompt ? { prompt: opts.prompt.slice(0, HISTORY_FIELD_MAX_LENGTH) } : {},
1540
- ...opts.error ? { error: opts.error.slice(0, HISTORY_FIELD_MAX_LENGTH) } : {}
1541
- };
1542
- }
1543
- function expandWebhookAction(action, env) {
1544
- const expanded = {
1545
- ...action,
1546
- url: expandEnvVars(action.url, env)
1547
- };
1548
- if (action.headers) {
1549
- expanded.headers = {};
1550
- for (const [key, value] of Object.entries(action.headers)) {
1551
- expanded.headers[key] = expandEnvVars(value, env);
1552
- }
1553
- }
1554
- if (typeof action.body === "string") {
1555
- expanded.body = expandEnvVars(action.body, env);
1556
- } else if (action.body !== void 0 && typeof action.body === "object" && action.body !== null) {
1557
- expanded.body = JSON.parse(expandEnvVars(JSON.stringify(action.body), env));
1558
- }
1559
- if (action.auth) {
1560
- if (action.auth.type === "basic") {
1561
- expanded.auth = {
1562
- type: "basic",
1563
- username: expandEnvVars(action.auth.username, env),
1564
- password: expandEnvVars(action.auth.password, env)
1565
- };
1566
- } else if (action.auth.type === "bearer") {
1567
- expanded.auth = {
1568
- type: "bearer",
1569
- token: expandEnvVars(action.auth.token, env)
1570
- };
1571
- }
1572
- }
1573
- return expanded;
1574
- }
1575
- var DEFAULT_TIMEOUT_MS = 3e4;
1576
- async function executeWebhookRequest(action, options) {
1577
- const env = options?.env;
1578
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1579
- const method = action.method ?? DEFAULT_WEBHOOK_METHOD;
1580
- const url = env ? expandEnvVars(action.url, env) : action.url;
1581
- try {
1582
- const parsed = new URL(url);
1583
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1584
- return {
1585
- type: "webhook",
1586
- url,
1587
- statusCode: 0,
1588
- success: false,
1589
- error: `Invalid URL scheme "${parsed.protocol}" \u2014 only http and https are allowed`,
1590
- durationMs: 0
1591
- };
1592
- }
1593
- } catch {
1594
- return {
1595
- type: "webhook",
1596
- url,
1597
- statusCode: 0,
1598
- success: false,
1599
- error: `Invalid URL after variable expansion: "${url.slice(0, 50)}"`,
1600
- durationMs: 0
1601
- };
1602
- }
1603
- const start = Date.now();
1604
- const controller = new AbortController();
1605
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1606
- try {
1607
- const headers = {};
1608
- if (action.auth) {
1609
- if (action.auth.type === "basic") {
1610
- const user = env ? expandEnvVars(action.auth.username, env) : action.auth.username;
1611
- const pass = env ? expandEnvVars(action.auth.password, env) : action.auth.password;
1612
- headers["Authorization"] = `Basic ${btoa(`${user}:${pass}`)}`;
1613
- } else if (action.auth.type === "bearer") {
1614
- const token = env ? expandEnvVars(action.auth.token, env) : action.auth.token;
1615
- headers["Authorization"] = `Bearer ${token}`;
1616
- }
1617
- }
1618
- if (action.headers) {
1619
- for (const [key, value] of Object.entries(action.headers)) {
1620
- headers[key] = env ? expandEnvVars(value, env) : value;
1621
- }
1622
- }
1623
- let requestBody;
1624
- if (method !== "GET" && action.body !== void 0) {
1625
- const bodyFormat = action.bodyFormat ?? "json";
1626
- if (bodyFormat === "json") {
1627
- if (!headers["Content-Type"] && !headers["content-type"]) {
1628
- headers["Content-Type"] = "application/json";
1629
- }
1630
- if (typeof action.body === "string") {
1631
- requestBody = env ? expandEnvVars(action.body, env) : action.body;
1632
- } else {
1633
- const raw = JSON.stringify(action.body);
1634
- requestBody = env ? expandEnvVars(raw, env) : raw;
1635
- }
1636
- } else if (bodyFormat === "form") {
1637
- if (!headers["Content-Type"] && !headers["content-type"]) {
1638
- headers["Content-Type"] = "application/x-www-form-urlencoded";
1639
- }
1640
- if (typeof action.body === "object" && action.body !== null) {
1641
- const params = new URLSearchParams();
1642
- for (const [k, v] of Object.entries(action.body)) {
1643
- const val = String(v ?? "");
1644
- params.append(k, env ? expandEnvVars(val, env) : val);
1645
- }
1646
- requestBody = params.toString();
1647
- } else {
1648
- const raw = String(action.body);
1649
- requestBody = env ? expandEnvVars(raw, env) : raw;
1650
- }
1651
- } else {
1652
- const raw = String(action.body);
1653
- requestBody = env ? expandEnvVars(raw, env) : raw;
1654
- }
1655
- }
1656
- const response = await fetch(url, {
1657
- method,
1658
- headers,
1659
- body: requestBody,
1660
- signal: controller.signal
1661
- });
1662
- const success = response.status >= 200 && response.status < 300;
1663
- const MAX_RESPONSE_SIZE = 4096;
1664
- let responseBody;
1665
- try {
1666
- const text = await response.text();
1667
- if (action.captureResponse) {
1668
- responseBody = text.length > MAX_RESPONSE_SIZE ? text.slice(0, MAX_RESPONSE_SIZE) + "...(truncated)" : text;
1669
- }
1670
- } catch {
1671
- }
1672
- return {
1673
- type: "webhook",
1674
- url,
1675
- statusCode: response.status,
1676
- success,
1677
- error: success ? void 0 : `HTTP ${response.status} ${response.statusText}`,
1678
- durationMs: Date.now() - start,
1679
- ...responseBody !== void 0 ? { responseBody } : {}
1680
- };
1681
- } catch (err) {
1682
- const isTimeout = err instanceof DOMException && err.name === "AbortError";
1683
- const error = isTimeout ? `Request timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : "Unknown error";
1684
- return {
1685
- type: "webhook",
1686
- url,
1687
- statusCode: 0,
1688
- success: false,
1689
- error,
1690
- durationMs: Date.now() - start
1691
- };
1692
- } finally {
1693
- clearTimeout(timeoutId);
1694
- }
1695
- }
1696
- function isTransientFailure(result) {
1697
- if (result.success) return false;
1698
- if (result.statusCode >= 400 && result.statusCode < 500) return false;
1699
- return true;
1700
- }
1701
- async function executeWithRetry(action, options) {
1702
- const maxAttempts = options?.retry?.maxAttempts ?? 0;
1703
- if (maxAttempts <= 0) {
1704
- const result = await executeWebhookRequest(action, options);
1705
- return { ...result, attempts: 1 };
1706
- }
1707
- const initialDelay = options?.retry?.initialDelayMs ?? 1e3;
1708
- const maxDelay = options?.retry?.maxDelayMs ?? 1e4;
1709
- const totalStart = Date.now();
1710
- let lastResult;
1711
- for (let attempt = 0; attempt <= maxAttempts; attempt++) {
1712
- lastResult = await executeWebhookRequest(action, options);
1713
- if (!isTransientFailure(lastResult)) {
1714
- return {
1715
- ...lastResult,
1716
- attempts: attempt + 1,
1717
- durationMs: Date.now() - totalStart
1718
- };
1719
- }
1720
- if (attempt === maxAttempts) break;
1721
- const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
1722
- const jitter = delay * 0.1 * (Math.random() * 2 - 1);
1723
- await new Promise((r) => setTimeout(r, delay + jitter));
1724
- }
1725
- return {
1726
- ...lastResult,
1727
- attempts: maxAttempts + 1,
1728
- durationMs: Date.now() - totalStart
1729
- };
1730
- }
1731
- var log2 = createLogger("history-store");
1732
- var mutexes = /* @__PURE__ */ new Map();
1733
- function withMutex(key, fn) {
1734
- const prev = mutexes.get(key) ?? Promise.resolve();
1735
- const next = prev.then(fn, fn);
1736
- mutexes.set(key, next.then(() => {
1737
- }, () => {
1738
- }));
1739
- return next;
1740
- }
1741
- var appendCounters = /* @__PURE__ */ new Map();
1742
- async function appendAutomationHistoryEntry(workspaceRootPath, entry) {
1743
- const historyPath = (0, import_path3.join)(workspaceRootPath, AUTOMATIONS_HISTORY_FILE);
1744
- await withMutex(workspaceRootPath, async () => {
1745
- await (0, import_promises2.appendFile)(historyPath, JSON.stringify(entry) + "\n", "utf-8");
1746
- const count = (appendCounters.get(workspaceRootPath) ?? 0) + 1;
1747
- appendCounters.set(workspaceRootPath, count);
1748
- if (count >= AUTOMATION_HISTORY_MAX_ENTRIES) {
1749
- appendCounters.set(workspaceRootPath, 0);
1750
- await runCompaction(historyPath);
1751
- }
1752
- });
1753
- }
1754
- async function compactAutomationHistory(workspaceRootPath, maxPerMatcher = AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER, maxTotal = AUTOMATION_HISTORY_MAX_ENTRIES) {
1755
- const historyPath = (0, import_path3.join)(workspaceRootPath, AUTOMATIONS_HISTORY_FILE);
1756
- await withMutex(workspaceRootPath, () => runCompaction(historyPath, maxPerMatcher, maxTotal));
1757
- }
1758
- function compactAutomationHistorySync(workspaceRootPath, maxPerMatcher = AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER, maxTotal = AUTOMATION_HISTORY_MAX_ENTRIES) {
1759
- const historyPath = (0, import_path3.join)(workspaceRootPath, AUTOMATIONS_HISTORY_FILE);
1760
- if (!(0, import_fs2.existsSync)(historyPath)) return;
1761
- let content;
1762
- try {
1763
- content = (0, import_fs2.readFileSync)(historyPath, "utf-8");
1764
- } catch {
1765
- return;
1766
- }
1767
- const result = compactEntries(content, maxPerMatcher, maxTotal);
1768
- if (!result) return;
1769
- (0, import_fs2.writeFileSync)(historyPath, result, "utf-8");
1770
- log2.debug(`[HistoryStore] Startup compaction complete`);
1771
- }
1772
- async function runCompaction(historyPath, maxPerMatcher = AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER, maxTotal = AUTOMATION_HISTORY_MAX_ENTRIES) {
1773
- let content;
1774
- try {
1775
- if (!(0, import_fs2.existsSync)(historyPath)) return;
1776
- content = await (0, import_promises2.readFile)(historyPath, "utf-8");
1777
- } catch {
1778
- return;
1779
- }
1780
- const result = compactEntries(content, maxPerMatcher, maxTotal);
1781
- if (!result) return;
1782
- await (0, import_promises2.writeFile)(historyPath, result, "utf-8");
1783
- log2.debug(`[HistoryStore] Compacted history`);
1784
- }
1785
- function compactEntries(content, maxPerMatcher, maxTotal) {
1786
- const lines = content.trim().split("\n").filter(Boolean);
1787
- if (lines.length === 0) return null;
1788
- const entries = [];
1789
- for (const line of lines) {
1790
- try {
1791
- const parsed = JSON.parse(line);
1792
- entries.push({ raw: line, id: parsed.id ?? "" });
1793
- } catch {
1794
- }
1795
- }
1796
- const originalLineCount = lines.length;
1797
- const byId = /* @__PURE__ */ new Map();
1798
- for (let i = 0; i < entries.length; i++) {
1799
- const id = entries[i].id;
1800
- let group = byId.get(id);
1801
- if (!group) {
1802
- group = [];
1803
- byId.set(id, group);
1804
- }
1805
- group.push(i);
1806
- }
1807
- const keepIndices = /* @__PURE__ */ new Set();
1808
- for (const indices of byId.values()) {
1809
- const kept = indices.slice(-maxPerMatcher);
1810
- for (const idx of kept) {
1811
- keepIndices.add(idx);
1812
- }
1813
- }
1814
- let trimmed = entries.filter((_, i) => keepIndices.has(i));
1815
- if (trimmed.length > maxTotal) {
1816
- trimmed = trimmed.slice(-maxTotal);
1817
- }
1818
- if (trimmed.length === originalLineCount) return null;
1819
- return trimmed.map((e) => e.raw).join("\n") + "\n";
1820
- }
1821
- var log3 = createLogger("retry-scheduler");
1822
- var DEFERRED_DELAYS_MS = [
1823
- 5 * 6e4,
1824
- // 5 minutes
1825
- 30 * 6e4,
1826
- // 30 minutes
1827
- 60 * 6e4
1828
- // 1 hour
1829
- ];
1830
- var MAX_DEFERRED_ATTEMPTS = DEFERRED_DELAYS_MS.length;
1831
- var TICK_INTERVAL_MS = 6e4;
1832
- var RetryScheduler = class {
1833
- workspaceRootPath;
1834
- timer = null;
1835
- processing = false;
1836
- constructor(options) {
1837
- this.workspaceRootPath = options.workspaceRootPath;
1838
- }
1839
- /**
1840
- * Start the scheduler. Checks queue every minute.
1841
- */
1842
- start() {
1843
- if (this.timer) return;
1844
- this.timer = setInterval(() => this.tick(), TICK_INTERVAL_MS);
1845
- log3.debug("[RetryScheduler] Started");
1846
- setTimeout(() => this.tick(), 5e3);
1847
- }
1848
- /**
1849
- * Stop the scheduler and clean up.
1850
- */
1851
- dispose() {
1852
- if (this.timer) {
1853
- clearInterval(this.timer);
1854
- this.timer = null;
1855
- }
1856
- log3.debug("[RetryScheduler] Disposed");
1857
- }
1858
- /**
1859
- * Enqueue a failed webhook for deferred retry.
1860
- * Called by WebhookHandler when immediate retries are exhausted.
1861
- */
1862
- async enqueue(matcherId, action, expandedUrl, lastError) {
1863
- const entry = {
1864
- id: `${matcherId}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
1865
- matcherId,
1866
- action,
1867
- expandedUrl,
1868
- deferredAttempt: 0,
1869
- nextRetryAt: Date.now() + DEFERRED_DELAYS_MS[0],
1870
- createdAt: Date.now(),
1871
- lastError
1872
- };
1873
- const queuePath = (0, import_path2.join)(this.workspaceRootPath, AUTOMATIONS_RETRY_QUEUE_FILE);
1874
- await (0, import_promises.appendFile)(queuePath, JSON.stringify(entry) + "\n", "utf-8");
1875
- log3.debug(`[RetryScheduler] Enqueued ${entry.id} \u2014 next retry in ${DEFERRED_DELAYS_MS[0] / 6e4}m`);
1876
- }
1877
- /**
1878
- * Process the queue: read entries, retry those that are due, rewrite the queue.
1879
- */
1880
- async tick() {
1881
- if (this.processing) return;
1882
- this.processing = true;
1883
- try {
1884
- const queuePath = (0, import_path2.join)(this.workspaceRootPath, AUTOMATIONS_RETRY_QUEUE_FILE);
1885
- let raw;
1886
- try {
1887
- raw = await (0, import_promises.readFile)(queuePath, "utf-8");
1888
- } catch {
1889
- return;
1890
- }
1891
- const lines = raw.trim().split("\n").filter(Boolean);
1892
- if (lines.length === 0) return;
1893
- const entries = [];
1894
- for (const line of lines) {
1895
- try {
1896
- entries.push(JSON.parse(line));
1897
- } catch {
1898
- }
1899
- }
1900
- if (entries.length === 0) return;
1901
- const now = Date.now();
1902
- const remaining = [];
1903
- for (const entry of entries) {
1904
- if (entry.nextRetryAt > now) {
1905
- remaining.push(entry);
1906
- continue;
1907
- }
1908
- log3.debug(`[RetryScheduler] Retrying ${entry.id} (deferred attempt ${entry.deferredAttempt + 1}/${MAX_DEFERRED_ATTEMPTS})`);
1909
- let result;
1910
- try {
1911
- result = await executeWebhookRequest(entry.action, { timeoutMs: 3e4 });
1912
- } catch (err) {
1913
- result = {
1914
- type: "webhook",
1915
- url: entry.expandedUrl,
1916
- statusCode: 0,
1917
- success: false,
1918
- error: err instanceof Error ? err.message : "Unknown error"
1919
- };
1920
- }
1921
- if (result.success) {
1922
- log3.debug(`[RetryScheduler] ${entry.id} succeeded on deferred attempt ${entry.deferredAttempt + 1}`);
1923
- const historyEntry = createWebhookHistoryEntry({
1924
- matcherId: entry.matcherId,
1925
- ok: true,
1926
- method: entry.action.method,
1927
- url: entry.expandedUrl,
1928
- statusCode: result.statusCode,
1929
- durationMs: result.durationMs ?? 0,
1930
- attempts: entry.deferredAttempt + 1
1931
- });
1932
- try {
1933
- await appendAutomationHistoryEntry(this.workspaceRootPath, historyEntry);
1934
- } catch (e) {
1935
- log3.debug(`[RetryScheduler] Failed to write history: ${e}`);
1936
- }
1937
- } else if (entry.deferredAttempt + 1 >= MAX_DEFERRED_ATTEMPTS) {
1938
- log3.debug(`[RetryScheduler] ${entry.id} permanently failed after ${MAX_DEFERRED_ATTEMPTS} deferred attempts`);
1939
- const historyEntry = createWebhookHistoryEntry({
1940
- matcherId: entry.matcherId,
1941
- ok: false,
1942
- method: entry.action.method,
1943
- url: entry.expandedUrl,
1944
- statusCode: result.statusCode,
1945
- durationMs: result.durationMs ?? 0,
1946
- attempts: entry.deferredAttempt + 1,
1947
- error: result.error ?? "Unknown error"
1948
- });
1949
- try {
1950
- await appendAutomationHistoryEntry(this.workspaceRootPath, historyEntry);
1951
- } catch (e) {
1952
- log3.debug(`[RetryScheduler] Failed to write history: ${e}`);
1953
- }
1954
- } else {
1955
- const nextDelay = DEFERRED_DELAYS_MS[entry.deferredAttempt + 1];
1956
- remaining.push({
1957
- ...entry,
1958
- deferredAttempt: entry.deferredAttempt + 1,
1959
- nextRetryAt: Date.now() + nextDelay,
1960
- lastError: result.error
1961
- });
1962
- log3.debug(`[RetryScheduler] ${entry.id} failed \u2014 next retry in ${nextDelay / 6e4}m`);
1963
- }
1964
- }
1965
- if (remaining.length === 0) {
1966
- await (0, import_promises.writeFile)(queuePath, "", "utf-8");
1967
- } else {
1968
- const content = remaining.map((e) => JSON.stringify(e)).join("\n") + "\n";
1969
- await (0, import_promises.writeFile)(queuePath, content, "utf-8");
1970
- }
1971
- } catch (err) {
1972
- log3.debug(`[RetryScheduler] Tick error: ${err}`);
1973
- } finally {
1974
- this.processing = false;
1975
- }
1976
- }
1977
- };
1978
- function loadAutomationsConfig(workspaceRootPath) {
1979
- const configPath = resolveAutomationsConfigPath(workspaceRootPath);
1980
- if (!(0, import_fs3.existsSync)(configPath)) {
1981
- return { config: null, configPath };
1982
- }
1983
- try {
1984
- const raw = JSON.parse((0, import_fs3.readFileSync)(configPath, "utf-8"));
1985
- return { config: raw, configPath };
1986
- } catch {
1987
- return { config: null, configPath };
1988
- }
1989
- }
1990
- function saveAutomationsConfig(workspaceRootPath, config) {
1991
- const validation = validateAutomationsConfig(config);
1992
- if (!validation.valid) {
1993
- return { ok: false, automationCount: 0, errors: validation.errors };
1994
- }
1995
- const configPath = resolveAutomationsConfigPath(workspaceRootPath);
1996
- (0, import_fs3.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1997
- const automationCount = validation.config ? Object.values(validation.config.automations).reduce((sum, matchers) => sum + (matchers?.length ?? 0), 0) : 0;
1998
- return { ok: true, automationCount, errors: [] };
1999
- }
2000
- var log4 = createLogger("event-bus");
2001
- var DEFAULT_RATE_LIMIT = 10;
2002
- var SCHEDULER_RATE_LIMIT = 60;
2003
- var RATE_WINDOW_MS = 6e4;
2004
- function getRateLimit(event) {
2005
- return event === "SchedulerTick" ? SCHEDULER_RATE_LIMIT : DEFAULT_RATE_LIMIT;
2006
- }
2007
- var WorkspaceEventBus = class {
2008
- workspaceId;
2009
- handlers = /* @__PURE__ */ new Map();
2010
- anyHandlers = /* @__PURE__ */ new Set();
2011
- rateCounts = /* @__PURE__ */ new Map();
2012
- disposed = false;
2013
- constructor(workspaceId) {
2014
- this.workspaceId = workspaceId;
2015
- log4.debug(`[EventBus] Created for workspace: ${workspaceId}`);
2016
- }
2017
- /**
2018
- * Emit an event to all registered handlers.
2019
- * Handlers are called in parallel, errors are caught and logged.
2020
- */
2021
- async emit(event, payload) {
2022
- if (this.disposed) {
2023
- log4.warn(`[EventBus] Attempted to emit after disposal: ${event}`);
2024
- return;
2025
- }
2026
- const now = Date.now();
2027
- const rateWindow = this.rateCounts.get(event) ?? { count: 0, windowStart: now };
2028
- if (now - rateWindow.windowStart >= RATE_WINDOW_MS) {
2029
- rateWindow.count = 0;
2030
- rateWindow.windowStart = now;
2031
- }
2032
- const limit = getRateLimit(event);
2033
- if (rateWindow.count >= limit) {
2034
- log4.warn(
2035
- `[EventBus] Rate limit: ${event} fired ${rateWindow.count} times in ${Math.round((now - rateWindow.windowStart) / 1e3)}s (limit: ${limit}/min), dropping`
2036
- );
2037
- return;
2038
- }
2039
- rateWindow.count++;
2040
- this.rateCounts.set(event, rateWindow);
2041
- log4.debug(`[EventBus] Emitting: ${event}`);
2042
- const eventHandlers = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
2043
- const anyHandlersCopy = new Set(this.anyHandlers);
2044
- const eventPromises = Array.from(eventHandlers).map(async (handler) => {
2045
- try {
2046
- await handler(payload);
2047
- } catch (error) {
2048
- log4.error(`[EventBus] Handler error for ${event}:`, error);
2049
- }
2050
- });
2051
- const anyPromises = Array.from(anyHandlersCopy).map(async (handler) => {
2052
- try {
2053
- await handler(event, payload);
2054
- } catch (error) {
2055
- log4.error(`[EventBus] Any-handler error for ${event}:`, error);
2056
- }
2057
- });
2058
- await Promise.all([...eventPromises, ...anyPromises]);
2059
- log4.debug(`[EventBus] Emitted: ${event} (${eventHandlers.size} handlers, ${anyHandlersCopy.size} any-handlers)`);
2060
- }
2061
- /**
2062
- * Register a handler for a specific event type.
2063
- */
2064
- on(event, handler) {
2065
- if (this.disposed) {
2066
- log4.warn(`[EventBus] Attempted to register handler after disposal: ${event}`);
2067
- return;
2068
- }
2069
- if (!this.handlers.has(event)) {
2070
- this.handlers.set(event, /* @__PURE__ */ new Set());
2071
- }
2072
- this.handlers.get(event).add(handler);
2073
- log4.debug(`[EventBus] Registered handler for: ${event}`);
2074
- }
2075
- /**
2076
- * Unregister a handler for a specific event type.
2077
- */
2078
- off(event, handler) {
2079
- const eventHandlers = this.handlers.get(event);
2080
- if (eventHandlers) {
2081
- eventHandlers.delete(handler);
2082
- log4.debug(`[EventBus] Unregistered handler for: ${event}`);
2083
- }
2084
- }
2085
- /**
2086
- * Register a handler for all events.
2087
- * Useful for logging, metrics, or debugging.
2088
- */
2089
- onAny(handler) {
2090
- if (this.disposed) {
2091
- log4.warn(`[EventBus] Attempted to register any-handler after disposal`);
2092
- return;
2093
- }
2094
- this.anyHandlers.add(handler);
2095
- log4.debug(`[EventBus] Registered any-handler`);
2096
- }
2097
- /**
2098
- * Unregister an all-events handler.
2099
- */
2100
- offAny(handler) {
2101
- this.anyHandlers.delete(handler);
2102
- log4.debug(`[EventBus] Unregistered any-handler`);
2103
- }
2104
- /**
2105
- * Clean up all handlers and mark as disposed.
2106
- */
2107
- dispose() {
2108
- if (this.disposed) return;
2109
- log4.debug(`[EventBus] Disposing for workspace: ${this.workspaceId}`);
2110
- this.handlers.clear();
2111
- this.anyHandlers.clear();
2112
- this.rateCounts.clear();
2113
- this.disposed = true;
2114
- }
2115
- /**
2116
- * Check if the bus has been disposed.
2117
- */
2118
- isDisposed() {
2119
- return this.disposed;
2120
- }
2121
- /**
2122
- * Get the workspace ID this bus belongs to.
2123
- */
2124
- getWorkspaceId() {
2125
- return this.workspaceId;
2126
- }
2127
- /**
2128
- * Get handler count for debugging.
2129
- */
2130
- getHandlerCount(event) {
2131
- if (event) {
2132
- return this.handlers.get(event)?.size ?? 0;
2133
- }
2134
- let total = this.anyHandlers.size;
2135
- for (const handlers of this.handlers.values()) {
2136
- total += handlers.size;
2137
- }
2138
- return total;
2139
- }
2140
- };
2141
- function deriveAutomationName(event, matcher) {
2142
- if (matcher.name) return matcher.name;
2143
- const firstAction = matcher.actions[0];
2144
- if (!firstAction) return event;
2145
- if (firstAction.type === "webhook") {
2146
- const label = `Webhook ${firstAction.method ?? "POST"} ${firstAction.url}`;
2147
- return label.length > 40 ? label.slice(0, 40) + "..." : label;
2148
- }
2149
- const mentionMatch = firstAction.prompt.match(/@(\S+)/);
2150
- if (mentionMatch) return `${mentionMatch[1]} prompt`;
2151
- return firstAction.prompt.length > 40 ? firstAction.prompt.slice(0, 40) + "..." : firstAction.prompt;
2152
- }
2153
- var log5 = createLogger("prompt-handler");
2154
- var PromptHandler = class {
2155
- options;
2156
- configProvider;
2157
- bus = null;
2158
- boundHandler = null;
2159
- constructor(options, configProvider) {
2160
- this.options = options;
2161
- this.configProvider = configProvider;
2162
- }
2163
- /**
2164
- * Subscribe to App events on the bus.
2165
- */
2166
- subscribe(bus) {
2167
- this.bus = bus;
2168
- this.boundHandler = this.handleEvent.bind(this);
2169
- bus.onAny(this.boundHandler);
2170
- log5.debug(`[PromptHandler] Subscribed to event bus`);
2171
- }
2172
- /**
2173
- * Handle an event by processing matching prompt actions.
2174
- */
2175
- async handleEvent(event, payload) {
2176
- if (!APP_EVENTS.includes(event)) {
2177
- return;
2178
- }
2179
- const matchers = this.configProvider.getMatchersForEvent(event);
2180
- if (matchers.length === 0) return;
2181
- const matcherPrompts = [];
2182
- for (const matcher of matchers) {
2183
- if (!matcherMatches(matcher, event, payload)) continue;
2184
- const prompts = [];
2185
- for (const action of matcher.actions) {
2186
- if (action.type === "prompt") {
2187
- prompts.push({ prompt: action, labels: matcher.labels, permissionMode: matcher.permissionMode });
2188
- }
2189
- }
2190
- if (prompts.length > 0) {
2191
- const telegramTopic = matcher.telegramTopic?.trim();
2192
- matcherPrompts.push({
2193
- matcherId: matcher.id,
2194
- automationName: deriveAutomationName(event, matcher),
2195
- telegramTopic: telegramTopic && telegramTopic.length > 0 ? telegramTopic : void 0,
2196
- prompts
2197
- });
2198
- }
2199
- }
2200
- if (matcherPrompts.length === 0) return;
2201
- const totalPrompts = matcherPrompts.reduce((s, m) => s + m.prompts.length, 0);
2202
- log5.debug(`[PromptHandler] Processing ${totalPrompts} prompts for ${event}`);
2203
- const env = buildEnvFromPayload(event, payload);
2204
- const pendingPrompts = [];
2205
- for (const { matcherId, automationName, telegramTopic, prompts } of matcherPrompts) {
2206
- const expandedTopic = telegramTopic ? expandEnvVars(telegramTopic, env).trim() : void 0;
2207
- const finalTopic = expandedTopic && expandedTopic.length > 0 ? expandedTopic : void 0;
2208
- for (const { prompt, labels, permissionMode } of prompts) {
2209
- const expandedPrompt = expandEnvVars(prompt.prompt, env);
2210
- const references = parsePromptReferences(expandedPrompt);
2211
- const expandedLabels = labels?.map((label) => expandEnvVars(label, env));
2212
- pendingPrompts.push({
2213
- sessionId: this.options.sessionId,
2214
- matcherId,
2215
- automationName,
2216
- prompt: expandedPrompt,
2217
- mentions: references.mentions,
2218
- labels: expandedLabels,
2219
- permissionMode,
2220
- llmConnection: prompt.llmConnection,
2221
- model: prompt.model,
2222
- thinkingLevel: prompt.thinkingLevel,
2223
- telegramTopic: finalTopic
2224
- });
2225
- }
2226
- }
2227
- if (pendingPrompts.length > 0 && this.options.onPromptsReady) {
2228
- log5.debug(`[PromptHandler] Delivering ${pendingPrompts.length} prompts`);
2229
- this.options.onPromptsReady(pendingPrompts);
2230
- }
2231
- }
2232
- /**
2233
- * Clean up resources.
2234
- */
2235
- dispose() {
2236
- if (this.bus && this.boundHandler) {
2237
- this.bus.offAny(this.boundHandler);
2238
- this.boundHandler = null;
2239
- }
2240
- this.bus = null;
2241
- log5.debug(`[PromptHandler] Disposed`);
2242
- }
2243
- };
2244
- var log6 = createLogger("event-log-handler");
2245
- var EventLogHandler = class {
2246
- options;
2247
- logger;
2248
- bus = null;
2249
- boundHandler = null;
2250
- constructor(options) {
2251
- this.options = options;
2252
- this.logger = new AutomationEventLogger(options.workspaceRootPath);
2253
- if (options.onEventLost) {
2254
- this.logger.onEventLost = (_loggedEvent) => {
2255
- };
2256
- }
2257
- }
2258
- /**
2259
- * Subscribe to all events on the bus.
2260
- */
2261
- subscribe(bus) {
2262
- this.bus = bus;
2263
- this.boundHandler = this.handleEvent.bind(this);
2264
- bus.onAny(this.boundHandler);
2265
- log6.debug(`[EventLogHandler] Subscribed to event bus, logging to ${this.logger.getLogPath()}`);
2266
- }
2267
- /**
2268
- * Handle an event by logging it.
2269
- */
2270
- async handleEvent(event, payload) {
2271
- const startTime = payload.timestamp;
2272
- const durationMs = Date.now() - startTime;
2273
- this.logger.log({
2274
- event,
2275
- sessionId: payload.sessionId,
2276
- workspaceId: this.options.workspaceId,
2277
- data: { ...payload }
2278
- });
2279
- log6.debug(`[EventLogHandler] Logged: ${event}`);
2280
- }
2281
- /**
2282
- * Get the path to the event log file.
2283
- */
2284
- getLogPath() {
2285
- return this.logger.getLogPath();
2286
- }
2287
- /**
2288
- * Clean up resources.
2289
- */
2290
- async dispose() {
2291
- if (this.bus && this.boundHandler) {
2292
- this.bus.offAny(this.boundHandler);
2293
- this.boundHandler = null;
2294
- }
2295
- this.bus = null;
2296
- await this.logger.dispose();
2297
- log6.debug(`[EventLogHandler] Disposed`);
2298
- }
2299
- };
2300
- var log7 = createLogger("webhook-handler");
2301
- var EndpointRateLimiter = class {
2302
- windows = /* @__PURE__ */ new Map();
2303
- maxPerMinute;
2304
- cleanupTimer = null;
2305
- constructor(maxPerMinute = 30) {
2306
- this.maxPerMinute = maxPerMinute;
2307
- this.cleanupTimer = setInterval(() => {
2308
- const cutoff = Date.now() - 12e4;
2309
- for (const [origin, timestamps] of this.windows) {
2310
- if (timestamps.every((t) => t < cutoff)) {
2311
- this.windows.delete(origin);
2312
- }
2313
- }
2314
- }, 3e5);
2315
- }
2316
- /** Returns true if the request is allowed */
2317
- allow(url) {
2318
- const origin = this.getOrigin(url);
2319
- const now = Date.now();
2320
- const windowStart = now - 6e4;
2321
- let timestamps = this.windows.get(origin);
2322
- if (timestamps) {
2323
- timestamps = timestamps.filter((t) => t > windowStart);
2324
- } else {
2325
- timestamps = [];
2326
- }
2327
- if (timestamps.length >= this.maxPerMinute) {
2328
- return false;
2329
- }
2330
- timestamps.push(now);
2331
- this.windows.set(origin, timestamps);
2332
- return true;
2333
- }
2334
- getOrigin(url) {
2335
- try {
2336
- return new URL(url).origin;
2337
- } catch {
2338
- return url;
2339
- }
2340
- }
2341
- dispose() {
2342
- if (this.cleanupTimer) {
2343
- clearInterval(this.cleanupTimer);
2344
- this.cleanupTimer = null;
2345
- }
2346
- this.windows.clear();
2347
- }
2348
- };
2349
- var WebhookHandler = class {
2350
- options;
2351
- configProvider;
2352
- rateLimiter = new EndpointRateLimiter(30);
2353
- retryScheduler;
2354
- bus = null;
2355
- boundHandler = null;
2356
- constructor(options, configProvider) {
2357
- this.options = options;
2358
- this.configProvider = configProvider;
2359
- this.retryScheduler = new RetryScheduler({ workspaceRootPath: options.workspaceRootPath });
2360
- }
2361
- /**
2362
- * Subscribe to App events on the bus.
2363
- */
2364
- subscribe(bus) {
2365
- this.bus = bus;
2366
- this.boundHandler = this.handleEvent.bind(this);
2367
- bus.onAny(this.boundHandler);
2368
- this.retryScheduler.start();
2369
- log7.debug(`[WebhookHandler] Subscribed to event bus`);
2370
- }
2371
- /**
2372
- * Handle an event by processing matching webhook actions.
2373
- */
2374
- async handleEvent(event, payload) {
2375
- if (!APP_EVENTS.includes(event)) {
2376
- return;
2377
- }
2378
- const matchers = this.configProvider.getMatchersForEvent(event);
2379
- if (matchers.length === 0) return;
2380
- const webhookTasks = [];
2381
- for (const matcher of matchers) {
2382
- if (!matcherMatches(matcher, event, payload)) continue;
2383
- for (const action of matcher.actions) {
2384
- if (action.type === "webhook") {
2385
- webhookTasks.push({ action, matcherId: matcher.id ?? "unknown" });
2386
- }
2387
- }
2388
- }
2389
- if (webhookTasks.length === 0) return;
2390
- log7.debug(`[WebhookHandler] Processing ${webhookTasks.length} webhooks for ${event}`);
2391
- const env = buildWebhookEnv(event, payload);
2392
- const results = new Array(webhookTasks.length);
2393
- const toExecute = [];
2394
- for (let i = 0; i < webhookTasks.length; i++) {
2395
- const task = webhookTasks[i];
2396
- const resolvedUrl = expandEnvVars(task.action.url, env);
2397
- if (!this.rateLimiter.allow(resolvedUrl)) {
2398
- log7.debug(`[WebhookHandler] Rate-limited: ${redactUrl(resolvedUrl)}`);
2399
- results[i] = {
2400
- type: "webhook",
2401
- url: resolvedUrl,
2402
- statusCode: 0,
2403
- success: false,
2404
- error: "Rate-limited: too many requests to this endpoint",
2405
- durationMs: 0,
2406
- attempts: 0
2407
- };
2408
- } else {
2409
- toExecute.push({ index: i, task });
2410
- }
2411
- }
2412
- if (toExecute.length > 0) {
2413
- const webhookOpts = { env, retry: { maxAttempts: 2 } };
2414
- const outcomes = await Promise.allSettled(
2415
- toExecute.map(({ task }) => executeWithRetry(task.action, webhookOpts))
2416
- );
2417
- for (let j = 0; j < outcomes.length; j++) {
2418
- const outcome = outcomes[j];
2419
- const { index, task } = toExecute[j];
2420
- if (outcome.status === "fulfilled") {
2421
- results[index] = outcome.value;
2422
- } else {
2423
- results[index] = {
2424
- type: "webhook",
2425
- url: task.action.url,
2426
- statusCode: 0,
2427
- success: false,
2428
- error: outcome.reason?.message ?? "Unknown error"
2429
- };
2430
- }
2431
- }
2432
- }
2433
- for (let i = 0; i < results.length; i++) {
2434
- const result = results[i];
2435
- const task = webhookTasks[i];
2436
- if (!result.success) {
2437
- log7.debug(`[WebhookHandler] ${result.url} \u2192 ${result.error}`);
2438
- }
2439
- const entry = createWebhookHistoryEntry({
2440
- matcherId: task.matcherId,
2441
- ok: result.success,
2442
- method: task.action.method,
2443
- url: result.url,
2444
- statusCode: result.statusCode,
2445
- durationMs: result.durationMs ?? 0,
2446
- attempts: result.attempts,
2447
- error: result.error,
2448
- responseBody: result.responseBody
2449
- });
2450
- try {
2451
- await appendAutomationHistoryEntry(this.options.workspaceRootPath, entry);
2452
- } catch (e) {
2453
- log7.debug(`[WebhookHandler] Failed to write history: ${e}`);
2454
- }
2455
- if (isTransientFailure(result)) {
2456
- if (result.attempts && result.attempts > 1) {
2457
- const expandedAction = expandWebhookAction(task.action, env);
2458
- this.retryScheduler.enqueue(task.matcherId, expandedAction, result.url, result.error).catch((e) => log7.debug(`[WebhookHandler] Failed to enqueue for deferred retry: ${e}`));
2459
- }
2460
- }
2461
- }
2462
- if (results.length > 0 && this.options.onWebhookResults) {
2463
- log7.debug(`[WebhookHandler] Delivering ${results.length} webhook results`);
2464
- this.options.onWebhookResults(results);
2465
- }
2466
- }
2467
- /**
2468
- * Clean up resources.
2469
- */
2470
- dispose() {
2471
- if (this.bus && this.boundHandler) {
2472
- this.bus.offAny(this.boundHandler);
2473
- this.boundHandler = null;
2474
- }
2475
- this.bus = null;
2476
- this.rateLimiter.dispose();
2477
- this.retryScheduler.dispose();
2478
- log7.debug(`[WebhookHandler] Disposed`);
2479
- }
2480
- };
2481
- var SchedulerService = class {
2482
- callback;
2483
- constructor(callback) {
2484
- this.callback = callback;
2485
- }
2486
- start() {
2487
- }
2488
- stop() {
2489
- }
2490
- };
2491
- var log8 = createLogger("automation-system");
2492
- var AutomationSystem = class {
2493
- eventBus;
2494
- options;
2495
- config = null;
2496
- promptHandler = null;
2497
- webhookHandler = null;
2498
- eventLogHandler = null;
2499
- scheduler = null;
2500
- disposed = false;
2501
- // Session metadata tracking (moved from SessionManager)
2502
- lastKnownMetadata = /* @__PURE__ */ new Map();
2503
- constructor(options) {
2504
- this.options = options;
2505
- this.eventBus = new WorkspaceEventBus(options.workspaceId);
2506
- this.loadConfig();
2507
- this.createHandlers();
2508
- if (options.enableScheduler) {
2509
- this.startScheduler();
2510
- }
2511
- log8.debug(`[AutomationSystem] Created for workspace: ${options.workspaceId}`);
2512
- }
2513
- // ============================================================================
2514
- // Configuration
2515
- // ============================================================================
2516
- /**
2517
- * Read, parse, and validate automations.json. Shared pipeline for loadConfig/reloadConfig.
2518
- * Returns the raw parsed JSON alongside validation results (avoids re-reading for backfillIds).
2519
- */
2520
- readAndValidateConfig(configPath) {
2521
- const raw = JSON.parse((0, import_fs4.readFileSync)(configPath, "utf-8"));
2522
- const validation = validateAutomationsConfig(raw);
2523
- return { raw, validation };
2524
- }
2525
- /**
2526
- * Load automations configuration from automations.json.
2527
- */
2528
- loadConfig() {
2529
- const configPath = resolveAutomationsConfigPath(this.options.workspaceRootPath);
2530
- if (!(0, import_fs4.existsSync)(configPath)) {
2531
- log8.debug(`[AutomationSystem] No automations config found at ${configPath}`);
2532
- this.config = { automations: {} };
2533
- return;
2534
- }
2535
- try {
2536
- const { raw, validation } = this.readAndValidateConfig(configPath);
2537
- if (!validation.valid) {
2538
- console.warn("[AutomationSystem] Invalid automations config:", validation.errors);
2539
- this.config = { automations: {} };
2540
- return;
2541
- }
2542
- this.config = validation.config;
2543
- this.backfillIds(configPath, raw);
2544
- this.rotateHistory();
2545
- const actionCount = this.getActionCount();
2546
- log8.debug(`[AutomationSystem] Loaded ${actionCount} actions from ${configPath}`);
2547
- } catch (e) {
2548
- const error = e instanceof Error ? e.message : "Unknown error";
2549
- console.warn("[AutomationSystem] Failed to load automations config:", error);
2550
- this.config = { automations: {} };
2551
- }
2552
- }
2553
- /**
2554
- * Reload automations configuration.
2555
- * Call this when automations.json changes.
2556
- */
2557
- reloadConfig() {
2558
- const configPath = resolveAutomationsConfigPath(this.options.workspaceRootPath);
2559
- if (!(0, import_fs4.existsSync)(configPath)) {
2560
- this.config = { automations: {} };
2561
- return { success: true, automationCount: 0, errors: [] };
2562
- }
2563
- try {
2564
- const { raw, validation } = this.readAndValidateConfig(configPath);
2565
- if (!validation.valid) {
2566
- return { success: false, automationCount: 0, errors: validation.errors };
2567
- }
2568
- this.config = validation.config;
2569
- this.backfillIds(configPath, raw);
2570
- const actionCount = this.getActionCount();
2571
- log8.debug(`[AutomationSystem] Reloaded ${actionCount} actions`);
2572
- return { success: true, automationCount: actionCount, errors: [] };
2573
- } catch (e) {
2574
- const error = e instanceof Error ? e.message : "Unknown error";
2575
- return { success: false, automationCount: 0, errors: [`Failed to parse JSON: ${error}`] };
2576
- }
2577
- }
2578
- /**
2579
- * Backfill missing IDs on matchers in the raw config.
2580
- * Operates on the already-parsed raw JSON to avoid re-reading from disk.
2581
- * Only writes if IDs were actually missing — no-op on subsequent loads.
2582
- */
2583
- backfillIds(configPath, raw) {
2584
- try {
2585
- const obj = raw;
2586
- const eventMap = obj.automations ?? obj.tasks ?? obj.hooks;
2587
- if (!eventMap) return;
2588
- let changed = false;
2589
- for (const matchers of Object.values(eventMap)) {
2590
- if (!Array.isArray(matchers)) continue;
2591
- for (const m of matchers) {
2592
- if (!m.id) {
2593
- m.id = generateShortId();
2594
- changed = true;
2595
- }
2596
- }
2597
- }
2598
- if (changed) {
2599
- (0, import_fs4.writeFileSync)(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2600
- log8.debug("[AutomationSystem] Backfilled missing matcher IDs");
2601
- }
2602
- } catch {
2603
- }
2604
- }
2605
- /**
2606
- * Compact automations-history.jsonl on startup: two-tier retention.
2607
- * 1) Keep only the last N entries per automation ID.
2608
- * 2) If total still exceeds the global cap, drop oldest globally.
2609
- * Runs synchronously during init — single-threaded, no race with concurrent appends.
2610
- */
2611
- rotateHistory() {
2612
- try {
2613
- compactAutomationHistorySync(this.options.workspaceRootPath);
2614
- } catch {
2615
- }
2616
- }
2617
- /**
2618
- * Get total number of actions.
2619
- */
2620
- getActionCount() {
2621
- if (!this.config) return 0;
2622
- return Object.values(this.config.automations).reduce(
2623
- (sum, matchers) => sum + (matchers?.reduce((s, m) => s + m.actions.length, 0) ?? 0),
2624
- 0
2625
- );
2626
- }
2627
- // ============================================================================
2628
- // AutomationsConfigProvider Implementation
2629
- // ============================================================================
2630
- getConfig() {
2631
- return this.config;
2632
- }
2633
- getMatchersForEvent(event) {
2634
- return this.config?.automations[event] ?? [];
2635
- }
2636
- // ============================================================================
2637
- // Handlers
2638
- // ============================================================================
2639
- /**
2640
- * Create and register all handlers.
2641
- */
2642
- createHandlers() {
2643
- this.promptHandler = new PromptHandler(
2644
- {
2645
- workspaceId: this.options.workspaceId,
2646
- workspaceRootPath: this.options.workspaceRootPath,
2647
- onPromptsReady: this.options.onPromptsReady,
2648
- onError: this.options.onError
2649
- },
2650
- this
2651
- );
2652
- this.promptHandler.subscribe(this.eventBus);
2653
- this.webhookHandler = new WebhookHandler(
2654
- {
2655
- workspaceId: this.options.workspaceId,
2656
- workspaceRootPath: this.options.workspaceRootPath,
2657
- onWebhookResults: this.options.onWebhookResults,
2658
- onError: this.options.onError
2659
- },
2660
- this
2661
- );
2662
- this.webhookHandler.subscribe(this.eventBus);
2663
- this.eventLogHandler = new EventLogHandler({
2664
- workspaceRootPath: this.options.workspaceRootPath,
2665
- workspaceId: this.options.workspaceId,
2666
- onEventLost: this.options.onEventLost
2667
- });
2668
- this.eventLogHandler.subscribe(this.eventBus);
2669
- log8.debug(`[AutomationSystem] Handlers created and subscribed`);
2670
- }
2671
- // ============================================================================
2672
- // Scheduler
2673
- // ============================================================================
2674
- /**
2675
- * Start the scheduler service.
2676
- */
2677
- startScheduler() {
2678
- if (this.scheduler) return;
2679
- this.scheduler = new SchedulerService(async (payload) => {
2680
- await this.eventBus.emit("SchedulerTick", {
2681
- workspaceId: this.options.workspaceId,
2682
- timestamp: Date.now(),
2683
- localTime: payload.localTime,
2684
- utcTime: payload.utcTime
2685
- });
2686
- });
2687
- this.scheduler.start();
2688
- log8.debug(`[AutomationSystem] Scheduler started`);
2689
- }
2690
- /**
2691
- * Stop the scheduler service.
2692
- */
2693
- stopScheduler() {
2694
- if (this.scheduler) {
2695
- this.scheduler.stop();
2696
- this.scheduler = null;
2697
- log8.debug(`[AutomationSystem] Scheduler stopped`);
2698
- }
2699
- }
2700
- // ============================================================================
2701
- // Session Metadata Diffing
2702
- // ============================================================================
2703
- /**
2704
- * Update session metadata and emit events for changes.
2705
- *
2706
- * This replaces the diffing logic that was in SessionManager.
2707
- * Call this whenever session metadata changes.
2708
- *
2709
- * @param sessionId - The session ID
2710
- * @param next - The new metadata snapshot
2711
- * @returns The events that were emitted
2712
- */
2713
- async updateSessionMetadata(sessionId, next) {
2714
- const prev = this.lastKnownMetadata.get(sessionId) ?? {};
2715
- const emittedEvents = [];
2716
- const timestamp = Date.now();
2717
- const sessionName = next.sessionName;
2718
- const labels = next.labels ?? [];
2719
- if (prev.permissionMode !== next.permissionMode) {
2720
- await this.eventBus.emit("PermissionModeChange", {
2721
- sessionId,
2722
- sessionName,
2723
- workspaceId: this.options.workspaceId,
2724
- timestamp,
2725
- labels,
2726
- oldMode: prev.permissionMode ?? "",
2727
- newMode: next.permissionMode ?? ""
2728
- });
2729
- emittedEvents.push("PermissionModeChange");
2730
- }
2731
- const prevLabels = new Set(prev.labels ?? []);
2732
- const nextLabels = new Set(next.labels ?? []);
2733
- for (const label of nextLabels) {
2734
- if (!prevLabels.has(label)) {
2735
- await this.eventBus.emit("LabelAdd", {
2736
- sessionId,
2737
- sessionName,
2738
- workspaceId: this.options.workspaceId,
2739
- timestamp,
2740
- labels: [...nextLabels],
2741
- label
2742
- });
2743
- emittedEvents.push("LabelAdd");
2744
- }
2745
- }
2746
- for (const label of prevLabels) {
2747
- if (!nextLabels.has(label)) {
2748
- await this.eventBus.emit("LabelRemove", {
2749
- sessionId,
2750
- sessionName,
2751
- workspaceId: this.options.workspaceId,
2752
- timestamp,
2753
- labels: [...nextLabels],
2754
- label
2755
- });
2756
- emittedEvents.push("LabelRemove");
2757
- }
2758
- }
2759
- const wasFlagged = prev.isFlagged ?? false;
2760
- const isFlagged = next.isFlagged ?? false;
2761
- if (wasFlagged !== isFlagged) {
2762
- await this.eventBus.emit("FlagChange", {
2763
- sessionId,
2764
- sessionName,
2765
- workspaceId: this.options.workspaceId,
2766
- timestamp,
2767
- labels,
2768
- isFlagged
2769
- });
2770
- emittedEvents.push("FlagChange");
2771
- }
2772
- if (prev.sessionStatus !== next.sessionStatus) {
2773
- await this.eventBus.emit("SessionStatusChange", {
2774
- sessionId,
2775
- sessionName,
2776
- workspaceId: this.options.workspaceId,
2777
- timestamp,
2778
- labels,
2779
- oldState: prev.sessionStatus ?? "",
2780
- newState: next.sessionStatus ?? ""
2781
- });
2782
- emittedEvents.push("SessionStatusChange");
2783
- }
2784
- this.lastKnownMetadata.set(sessionId, { ...next });
2785
- if (emittedEvents.length > 0) {
2786
- log8.debug(`[AutomationSystem] Emitted ${emittedEvents.length} events for session ${sessionId}: ${emittedEvents.join(", ")}`);
2787
- }
2788
- return emittedEvents;
2789
- }
2790
- /**
2791
- * Remove session metadata tracking.
2792
- * Call this when a session is deleted.
2793
- */
2794
- removeSessionMetadata(sessionId) {
2795
- this.lastKnownMetadata.delete(sessionId);
2796
- log8.debug(`[AutomationSystem] Removed metadata for session ${sessionId}`);
2797
- }
2798
- /**
2799
- * Get stored metadata for a session.
2800
- */
2801
- getSessionMetadata(sessionId) {
2802
- return this.lastKnownMetadata.get(sessionId);
2803
- }
2804
- /**
2805
- * Set initial metadata for a session (without emitting events).
2806
- * Call this when loading existing sessions.
2807
- */
2808
- setInitialSessionMetadata(sessionId, metadata) {
2809
- this.lastKnownMetadata.set(sessionId, { ...metadata });
2810
- }
2811
- // ============================================================================
2812
- // Direct Event Emission
2813
- // ============================================================================
2814
- /**
2815
- * Emit a LabelConfigChange event.
2816
- * Call this when labels/config.json changes.
2817
- */
2818
- async emitLabelConfigChange() {
2819
- await this.eventBus.emit("LabelConfigChange", {
2820
- workspaceId: this.options.workspaceId,
2821
- timestamp: Date.now()
2822
- });
2823
- }
2824
- /**
2825
- * Emit an event directly (for edge cases).
2826
- */
2827
- async emit(event, payload) {
2828
- await this.eventBus.emit(event, payload);
2829
- }
2830
- // ============================================================================
2831
- // Agent Event Execution (Backend-Agnostic)
2832
- // ============================================================================
2833
- /**
2834
- * Execute agent event automations directly (without going through the Claude SDK).
2835
- * This is the backend-agnostic entry point for non-Claude backends (Codex, Copilot, Pi)
2836
- * to fire agent events from automations.json.
2837
- *
2838
- * For each matching automation matcher, builds env vars and evaluates matching.
2839
- * Command execution has been removed — all automation actions now go through prompt-based
2840
- * execution (creating agent sessions via PromptHandler).
2841
- * Catches all errors — automations must never break the agent flow.
2842
- *
2843
- * @param signal - Optional AbortSignal for cancelling automation execution on abort
2844
- * @returns Number of matched matchers (for diagnostics/testing)
2845
- */
2846
- async executeAgentEvent(event, input, signal) {
2847
- if (!this.config) return 0;
2848
- const matchers = this.config.automations[event];
2849
- if (!matchers?.length) return 0;
2850
- let matchedCount = 0;
2851
- for (const matcher of matchers) {
2852
- if (!matcherMatchesSdk(matcher, event, input)) continue;
2853
- matchedCount++;
2854
- log8.debug(`[AutomationSystem] Matched ${event} automation (prompt-based execution pending)`);
2855
- }
2856
- return matchedCount;
2857
- }
2858
- // ============================================================================
2859
- // SDK Automation Integration
2860
- // ============================================================================
2861
- /**
2862
- * Build SDK hook callbacks from automations.json definitions.
2863
- *
2864
- * Command execution has been removed — all automation actions now go through prompt-based
2865
- * execution (creating agent sessions via PromptHandler). Agent event automations are not
2866
- * currently supported via prompts, so this returns empty.
2867
- */
2868
- buildSdkHooks() {
2869
- return {};
2870
- }
2871
- // ============================================================================
2872
- // Lifecycle
2873
- // ============================================================================
2874
- /**
2875
- * Check if the system has been disposed.
2876
- */
2877
- isDisposed() {
2878
- return this.disposed;
2879
- }
2880
- /**
2881
- * Dispose the automation system, cleaning up all resources.
2882
- */
2883
- async dispose() {
2884
- if (this.disposed) return;
2885
- log8.debug(`[AutomationSystem] Disposing for workspace: ${this.options.workspaceId}`);
2886
- this.stopScheduler();
2887
- this.promptHandler?.dispose();
2888
- this.webhookHandler?.dispose();
2889
- await this.eventLogHandler?.dispose();
2890
- this.eventBus.dispose();
2891
- this.lastKnownMetadata.clear();
2892
- this.disposed = true;
2893
- log8.debug(`[AutomationSystem] Disposed`);
2894
- }
2895
- };
2896
- var CURRENCY_PREFIX = /^[$€£¥]/;
2897
- var COMMA_SEPARATOR = /,/g;
2898
- var SUFFIX_MULTIPLIERS = {
2899
- k: 1e3,
2900
- m: 1e6,
2901
- b: 1e9
2902
- };
2903
- function normalizeNumberValue(raw) {
2904
- let cleaned = raw.trim().replace(CURRENCY_PREFIX, "").replace(COMMA_SEPARATOR, "");
2905
- const suffixMatch = cleaned.match(/^(-?[\d.]+)([kmb])$/i);
2906
- if (suffixMatch) {
2907
- const base = parseFloat(suffixMatch[1]);
2908
- const multiplier = SUFFIX_MULTIPLIERS[suffixMatch[2].toLowerCase()];
2909
- const result = base * multiplier;
2910
- return Number.isInteger(result) ? String(result) : result.toFixed(2);
2911
- }
2912
- const num = parseFloat(cleaned);
2913
- if (isNaN(num)) return raw.trim();
2914
- return String(num);
2915
- }
2916
- var MAX_MATCHES_PER_MESSAGE = 10;
2917
- var CODE_BLOCK_PATTERN = /```[\s\S]*?```|`[^`]+`/g;
2918
- function evaluateAutoLabels(text, configs) {
2919
- const stripped = text.replace(CODE_BLOCK_PATTERN, "");
2920
- const matches = [];
2921
- const seen = /* @__PURE__ */ new Set();
2922
- for (const config of configs) {
2923
- for (const rule of config.rules) {
2924
- if (matches.length >= MAX_MATCHES_PER_MESSAGE) return matches;
2925
- let flags = rule.flags ?? "gi";
2926
- if (!flags.includes("g")) flags = `g${flags}`;
2927
- let regex;
2928
- try {
2929
- regex = new RegExp(rule.pattern, flags);
2930
- } catch {
2931
- continue;
2932
- }
2933
- let match;
2934
- while ((match = regex.exec(stripped)) !== null) {
2935
- if (matches.length >= MAX_MATCHES_PER_MESSAGE) return matches;
2936
- let value;
2937
- if (rule.valueTemplate) {
2938
- value = substituteCaptures(rule.valueTemplate, match);
2939
- } else {
2940
- value = match[1] ?? match[0];
2941
- }
2942
- value = value.trim();
2943
- if (!value) continue;
2944
- const dedupeKey = `${config.labelId}::${value}`;
2945
- if (seen.has(dedupeKey)) continue;
2946
- seen.add(dedupeKey);
2947
- matches.push({
2948
- labelId: config.labelId,
2949
- value: normalizeNumberValue(value),
2950
- matchedText: match[0]
2951
- });
2952
- }
2953
- }
2954
- }
2955
- return matches;
2956
- }
2957
- function substituteCaptures(template, match) {
2958
- return template.replace(/\$(\d+)/g, (_, index) => {
2959
- const i = parseInt(index, 10);
2960
- return match[i] ?? "";
2961
- });
2962
- }
2963
- var NESTED_QUANTIFIER = /(\([^)]*[+*][^)]*\))[+*]|\(\?[^)]*[+*][^)]*\)[+*]/;
2964
- function validateAutoLabelPattern(pattern, flags) {
2965
- const errors = [];
2966
- const warnings = [];
2967
- try {
2968
- new RegExp(pattern, flags ?? "gi");
2969
- } catch (err) {
2970
- errors.push(`Invalid regex: ${String(err)}`);
2971
- return { errors, warnings };
2972
- }
2973
- if (NESTED_QUANTIFIER.test(pattern)) {
2974
- errors.push(`Pattern contains nested quantifiers that may cause catastrophic backtracking: ${pattern}`);
2975
- }
2976
- try {
2977
- const re = new RegExp(pattern, flags ?? "gi");
2978
- if (!re.global) {
2979
- warnings.push('Pattern should have the "g" flag to avoid infinite loops');
2980
- }
2981
- } catch {
2982
- }
2983
- const captureGroupCount = (pattern.match(/\((?!\?)/g) || []).length;
2984
- if (captureGroupCount === 0) {
2985
- warnings.push("Pattern has no capture groups \u2014 the entire match will be used as the value");
2986
- }
2987
- return { errors, warnings };
2988
- }
2989
- // Annotate the CommonJS export names for ESM import in node:
2990
- 0 && (module.exports = {
2991
- AGENT_EVENTS,
2992
- APP_EVENTS,
2993
- AUTOMATIONS_CONFIG_FILE,
2994
- AUTOMATIONS_HISTORY_FILE,
2995
- AUTOMATIONS_RETRY_QUEUE_FILE,
2996
- AUTOMATION_HISTORY_MAX_ENTRIES,
2997
- AUTOMATION_HISTORY_MAX_RUNS_PER_MATCHER,
2998
- AutomationConditionSchema,
2999
- AutomationEventLogger,
3000
- AutomationSystem,
3001
- AutomationsConfigSchema,
3002
- EventLogHandler,
3003
- HISTORY_FIELD_MAX_LENGTH,
3004
- PromptHandler,
3005
- RetryScheduler,
3006
- StateConditionSchema,
3007
- TimeConditionSchema,
3008
- VALID_EVENTS,
3009
- WebhookHandler,
3010
- WorkspaceEventBus,
3011
- appendAutomationHistoryEntry,
3012
- automationHistoryInputForPromptResult,
3013
- buildEnvFromSdkInput,
3014
- compactAutomationHistory,
3015
- compactAutomationHistorySync,
3016
- createAutomationRuntimeGuard,
3017
- createAutomationSchedulerHost,
3018
- createAutomationTimelineBridge,
3019
- createAutomationsConfigDoctorReport,
3020
- createInMemoryAutomationHistoryStore,
3021
- createPromptHistoryEntry,
3022
- createRuntimeAutomationBridge,
3023
- createWebhookHistoryEntry,
3024
- evaluateAutoLabels,
3025
- evaluateConditions,
3026
- executeAutomationPrompt,
3027
- executeWebhookRequest,
3028
- executeWithRetry,
3029
- extractLabelId,
3030
- generateShortId,
3031
- loadAutomationsConfig,
3032
- matchesCron,
3033
- normalizeNumberValue,
3034
- parsePromptReferences,
3035
- projectTimelineEnvelopeToAutomationInput,
3036
- resolveAutomationsConfigPath,
3037
- sanitizeForShell,
3038
- saveAutomationsConfig,
3039
- validateAutoLabelPattern,
3040
- validateAutomations,
3041
- validateAutomationsConfig,
3042
- validateAutomationsContent,
3043
- zodErrorToIssues
3044
- });