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