openbot 0.2.2 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +54 -141
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +170 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +339 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +549 -31
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +8 -7
package/README.md CHANGED
@@ -25,7 +25,7 @@ You can talk to OpenBot using a simple POST request:
25
25
  ```bash
26
26
  curl -N \
27
27
  -H "Content-Type: application/json" \
28
- -d '{"event":{"type":"user:text","data":{"content":"Hello!"}}}' \
28
+ -d '{"event":{"type":"agent:input","data":{"content":"Hello!"}}}' \
29
29
  http://localhost:4001/api/chat
30
30
  ```
31
31
 
@@ -1,35 +1,74 @@
1
+ import path from "node:path";
1
2
  import { llmPlugin } from "../plugins/llm/index.js";
2
3
  import { fileSystemToolDefinitions, fileSystemPlugin } from "../plugins/file-system/index.js";
3
4
  import { DEFAULT_BASE_DIR, resolvePath } from "../config.js";
4
5
  export const agentCreatorAgent = (options) => (builder) => {
5
6
  const { model } = options;
6
- const agentsDir = resolvePath(`${DEFAULT_BASE_DIR}/agents`);
7
+ const baseDir = resolvePath(DEFAULT_BASE_DIR);
8
+ const agentsDir = path.join(baseDir, "agents");
7
9
  builder
8
- .use(fileSystemPlugin({ baseDir: agentsDir }))
10
+ .use(fileSystemPlugin({ baseDir }))
9
11
  .use(llmPlugin({
10
12
  model,
11
- system: `You are the OpenBot Agent Creator. Your job is to help users build custom OpenBot agents via natural language.
13
+ system: `You are the OpenBot Agent Creator. Your job is to help users create AND update custom OpenBot agents via natural language.
12
14
 
13
- An OpenBot agent is defined by a folder in ${agentsDir} containing an \`agent.yaml\` file.
15
+ Configuration storage:
16
+ 1. The Default Agent (the main orchestrator, usually named "OpenBot") is defined in: ${baseDir}/AGENT.md.
17
+ 2. Custom Agents live in their own subdirectories: ${agentsDir}/<agent-name>/AGENT.md.
14
18
 
15
- The YAML format is:
19
+ The AGENT.md file uses Markdown with a YAML frontmatter block at the top.
20
+
21
+ Frontmatter fields (required):
22
+ ---
16
23
  name: <slug-name>
17
24
  description: <short description>
18
25
  plugins:
19
- - name: file-system
20
- - name: shell
21
- - name: browser # (if they need web access)
22
- systemPrompt: |
23
- <detailed instructions for the agent>
24
-
25
- Your workflow:
26
- 1. Ask the user what kind of agent they want to build (if they haven't provided enough detail).
27
- 2. Suggest a name, description, required plugins, and a draft system prompt.
28
- 3. Once the user approves, use the file-system tools to create the directory ${agentsDir}/<name> and write the \`agent.yaml\` file inside it.
29
- 4. Tell the user they may need to restart the OpenBot server for the new agent to be registered.`,
26
+ - shell
27
+ - file-system
28
+ - name: approval
29
+ config:
30
+ rules: []
31
+ ---
32
+
33
+ Optional fields:
34
+ - model: <provider/model-id>
35
+ - subscribe: [<event-type>, ...]
36
+
37
+ The Markdown body below the frontmatter contains the agent's persona and detailed behavioral instructions.
38
+
39
+ Official plugin catalog (prefer these):
40
+ - shell: execute shell commands
41
+ - file-system: read/write/list/delete files
42
+ - approval: require user approval before risky actions
43
+ - browser-tools: web automation
44
+ - search: search/retrieval tools
45
+
46
+ Tool Usage:
47
+ 1. Use \`listFiles\` to explore existing agents in ${agentsDir}.
48
+ 2. Use \`readFile\` to inspect an existing agent's AGENT.md before updating it.
49
+ 3. Use \`writeFile\` to create or update an agent's AGENT.md.
50
+
51
+ Rules:
52
+ 1. Do not write files until user explicitly approves the proposed changes.
53
+ 2. For updates, ALWAYS read the current AGENT.md first using \`readFile\`, then propose changes.
54
+ 3. For the Default Agent, ALWAYS use ${baseDir}/AGENT.md.
55
+ 4. For Custom Agents, ALWAYS use ${agentsDir}/<name>/AGENT.md.
56
+ 5. If you're unsure which agent to update, use \`listFiles\` on ${agentsDir} to see available agents.
57
+ 6. Prefer official plugins.
58
+ 7. Keep frontmatter minimal; include only meaningful fields.
59
+ 8. If required info is missing, ask focused follow-up questions.
60
+ 9. After writing, confirm that the correct AGENT.md was updated.
61
+ 10. The server hot-reloads ~/.openbot changes.
62
+ 11. ALWAYS use the consolidated AGENT.md format. Do NOT create agent.yaml files anymore.
63
+
64
+ Workflow:
65
+ 1. Determine whether this is create or update, and if it's for the Default Agent or a Custom Agent.
66
+ 2. If it's an update, use \`readFile\` to get the current configuration.
67
+ 3. If the agent name is ambiguous, use \`listFiles\` to find the correct one.
68
+ 4. Collect missing requirements from the user.
69
+ 5. Show a proposed AGENT.md content (frontmatter + instructions) and request explicit approval.
70
+ 6. On approval, write the appropriate AGENT.md using \`writeFile\`.
71
+ 7. Return a short completion summary.`,
30
72
  toolDefinitions: fileSystemToolDefinitions, // Give it access to write files
31
- promptInputType: "agent:agent-creator:input",
32
- actionResultInputType: "agent:agent-creator:result",
33
- completionEventType: "agent:agent-creator:output",
34
73
  }));
35
74
  };
@@ -15,7 +15,7 @@ export const osAgent = (options) => (builder) => {
15
15
  .use(approvalPlugin({
16
16
  rules: [
17
17
  { action: "action:executeCommand", message: "The agent wants to execute a terminal command. Please review carefully." },
18
- { action: "action:writeFile", message: "The agent wants to write to a file." },
18
+ // { action: "action:writeFile", message: "The agent wants to write to a file." },
19
19
  { action: "action:deleteFile", message: "The agent wants to delete a file." },
20
20
  ],
21
21
  }))
@@ -26,9 +26,6 @@ export const osAgent = (options) => (builder) => {
26
26
  ...shellToolDefinitions,
27
27
  ...fileSystemToolDefinitions
28
28
  },
29
- promptInputType: "agent:os:input",
30
- actionResultInputType: "agent:os:result",
31
- completionEventType: "agent:os:output",
32
29
  }));
33
30
  // NOTE: Bridge-back to the manager is handled generically by open-bot.ts.
34
31
  // No per-agent boilerplate needed.
@@ -0,0 +1,32 @@
1
+ import { llmPlugin } from "../plugins/llm/index.js";
2
+ const PLANNER_SYSTEM_PROMPT = `You are a planning specialist agent.
3
+
4
+ Your job:
5
+ - Convert a user intent into a short, executable plan.
6
+ - Keep plans practical and concise.
7
+ - Prefer 2-5 steps.
8
+ - Each step should specify the best agent to execute it when relevant.
9
+
10
+ Available agent naming convention:
11
+ - Use exact agent names when assigning (e.g. os, browser, topic, planner-agent, etc.).
12
+
13
+ Output format:
14
+ - Return strict JSON only.
15
+ - Shape:
16
+ {
17
+ "goal": "...",
18
+ "steps": [
19
+ { "id": "step_1", "agent": "agent-name-or-manager", "task": "..." }
20
+ ]
21
+ }
22
+
23
+ Rules:
24
+ - If no specialist agent is needed for a step, set "agent" to "manager".
25
+ - Do not include markdown fences.
26
+ - Do not include explanatory prose outside JSON.`;
27
+ export const plannerAgent = (options) => (builder) => {
28
+ builder.use(llmPlugin({
29
+ model: options.model,
30
+ system: PLANNER_SYSTEM_PROMPT,
31
+ }));
32
+ };
@@ -1,6 +1,6 @@
1
1
  import { generateText } from "ai";
2
2
  export const topicAgent = (options) => (builder) => {
3
- builder.on("manager:completion", async function* (event, { state }) {
3
+ builder.on("agent:output", async function* (event, { state }) {
4
4
  // Only title if it doesn't have one and there's history
5
5
  if (state.title || !state.messages || state.messages.length === 0) {
6
6
  return;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,151 @@
1
+ const DEFAULT_POLICY = {
2
+ maxRetries: 1,
3
+ stepTimeoutMs: 45000,
4
+ };
5
+ function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+ function toError(error) {
9
+ return error instanceof Error ? error.message : String(error);
10
+ }
11
+ function updateTrace(state, patch) {
12
+ const next = {
13
+ traceId: patch.traceId ?? state.execution?.traceId ?? `trace_${Date.now()}`,
14
+ state: patch.state ?? state.execution?.state ?? "RECEIVED",
15
+ intent: patch.intent ?? state.execution?.intent,
16
+ plan: patch.plan ?? state.execution?.plan,
17
+ currentStepId: patch.currentStepId ?? state.execution?.currentStepId,
18
+ error: patch.error ?? state.execution?.error,
19
+ updatedAt: nowIso(),
20
+ };
21
+ state.execution = next;
22
+ return next;
23
+ }
24
+ function toExecutionEvent(trace) {
25
+ return {
26
+ type: "execution:state",
27
+ data: {
28
+ traceId: trace.traceId,
29
+ state: trace.state,
30
+ currentStepId: trace.currentStepId,
31
+ error: trace.error,
32
+ intentType: trace.intent?.type,
33
+ planSteps: trace.plan?.steps.length,
34
+ },
35
+ };
36
+ }
37
+ function withTimeout(promise, timeoutMs) {
38
+ return new Promise((resolve, reject) => {
39
+ const timer = setTimeout(() => {
40
+ reject(new Error(`Step timed out after ${timeoutMs}ms`));
41
+ }, timeoutMs);
42
+ promise
43
+ .then((value) => {
44
+ clearTimeout(timer);
45
+ resolve(value);
46
+ })
47
+ .catch((error) => {
48
+ clearTimeout(timer);
49
+ reject(error);
50
+ });
51
+ });
52
+ }
53
+ function hasPendingApprovals(state) {
54
+ const agentStates = state.agentStates || {};
55
+ return Object.values(agentStates).some((agentState) => !!agentState.pendingApprovals &&
56
+ Object.keys(agentState.pendingApprovals).length > 0);
57
+ }
58
+ async function* executeStep(step, input, policy) {
59
+ let lastError;
60
+ for (let attempt = 0; attempt <= policy.maxRetries; attempt++) {
61
+ try {
62
+ const stream = step.kind === "delegate"
63
+ ? input.callbacks.runAgent(step.agent ?? "", step.task ?? "", step.attachments, input.state, input.runId)
64
+ : input.callbacks.runManager(step.content ?? "", step.attachments, input.state, input.runId);
65
+ while (true) {
66
+ const nextItem = await withTimeout(stream.next(), policy.stepTimeoutMs);
67
+ if (nextItem.done)
68
+ break;
69
+ const event = nextItem.value;
70
+ if (input.state.execution?.state === "WAITING_APPROVAL") {
71
+ const trace = updateTrace(input.state, {
72
+ state: "EXECUTING",
73
+ currentStepId: step.id,
74
+ });
75
+ yield toExecutionEvent(trace);
76
+ }
77
+ yield event;
78
+ }
79
+ return;
80
+ }
81
+ catch (error) {
82
+ lastError = error;
83
+ if (attempt < policy.maxRetries) {
84
+ const trace = updateTrace(input.state, {
85
+ state: "EXECUTING",
86
+ currentStepId: step.id,
87
+ error: `Retry ${attempt + 1}/${policy.maxRetries}: ${toError(error)}`,
88
+ });
89
+ yield toExecutionEvent(trace);
90
+ continue;
91
+ }
92
+ }
93
+ }
94
+ throw lastError instanceof Error ? lastError : new Error(toError(lastError));
95
+ }
96
+ /**
97
+ * Stateful execution engine for plan steps.
98
+ * The engine is the commit point for executing actions from a plan.
99
+ */
100
+ export async function* executePlan(input) {
101
+ const policy = {
102
+ ...DEFAULT_POLICY,
103
+ ...(input.policy ?? {}),
104
+ };
105
+ let trace = updateTrace(input.state, {
106
+ traceId: input.traceId,
107
+ state: "EXECUTING",
108
+ plan: input.plan,
109
+ error: undefined,
110
+ });
111
+ yield toExecutionEvent(trace);
112
+ try {
113
+ for (const step of input.plan.steps) {
114
+ trace = updateTrace(input.state, {
115
+ state: "EXECUTING",
116
+ currentStepId: step.id,
117
+ error: undefined,
118
+ });
119
+ yield toExecutionEvent(trace);
120
+ yield* executeStep(step, input, policy);
121
+ if (hasPendingApprovals(input.state)) {
122
+ trace = updateTrace(input.state, {
123
+ state: "WAITING_APPROVAL",
124
+ currentStepId: step.id,
125
+ });
126
+ yield toExecutionEvent(trace);
127
+ return;
128
+ }
129
+ }
130
+ trace = updateTrace(input.state, {
131
+ state: "COMPLETED",
132
+ currentStepId: undefined,
133
+ error: undefined,
134
+ });
135
+ yield toExecutionEvent(trace);
136
+ }
137
+ catch (error) {
138
+ trace = updateTrace(input.state, {
139
+ state: "FAILED",
140
+ error: toError(error),
141
+ });
142
+ yield toExecutionEvent(trace);
143
+ throw error;
144
+ }
145
+ }
146
+ export function setExecutionState(state, patch) {
147
+ return updateTrace(state, patch);
148
+ }
149
+ export function executionStateEvent(trace) {
150
+ return toExecutionEvent(trace);
151
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Classifies user input into a routing intent.
3
+ * Starts with deterministic rules; can later be upgraded to hybrid LLM+rules.
4
+ */
5
+ export function classifyIntent(input) {
6
+ const raw = input.content.trim();
7
+ if (!raw) {
8
+ return { type: "chat", confidence: 0.8 };
9
+ }
10
+ if (raw.startsWith("/") || raw.startsWith("@")) {
11
+ const firstSpace = raw.indexOf(" ");
12
+ const prefix = firstSpace === -1 ? raw.slice(1) : raw.slice(1, firstSpace);
13
+ if (input.knownAgents.has(prefix)) {
14
+ return {
15
+ type: "agent_direct",
16
+ confidence: 0.99,
17
+ targetAgent: prefix,
18
+ };
19
+ }
20
+ }
21
+ const looksLikeTask = /\b(create|build|write|fix|update|implement|run|execute|open|edit|delete|list)\b/i.test(raw);
22
+ if (looksLikeTask) {
23
+ return { type: "task", confidence: 0.75 };
24
+ }
25
+ return { type: "chat", confidence: 0.7 };
26
+ }
@@ -0,0 +1,106 @@
1
+ import { generateObject } from "ai";
2
+ import { z } from "zod";
3
+ function createStepId(index) {
4
+ return `step_${index + 1}`;
5
+ }
6
+ /**
7
+ * Strategic planner that emits explicit executable steps.
8
+ * Initial version is deterministic to keep behavior predictable.
9
+ */
10
+ const PlanStepSchema = z.object({
11
+ id: z.string(),
12
+ kind: z.enum(["delegate", "manager"]),
13
+ successCriteria: z.string(),
14
+ agent: z.string().optional(),
15
+ task: z.string().optional(),
16
+ content: z.string().optional(),
17
+ });
18
+ const PlanSchema = z.object({
19
+ goal: z.string(),
20
+ stopCondition: z.string(),
21
+ steps: z.array(PlanStepSchema).min(1).max(4),
22
+ });
23
+ function sanitizePlan(raw, input) {
24
+ const steps = raw.steps.map((step, index) => {
25
+ const normalized = {
26
+ id: step.id?.trim() ? step.id : createStepId(index),
27
+ kind: step.kind,
28
+ successCriteria: step.successCriteria?.trim() || "Step completed successfully.",
29
+ };
30
+ if (step.kind === "delegate") {
31
+ const chosenAgent = step.agent && input.knownAgents.includes(step.agent)
32
+ ? step.agent
33
+ : input.knownAgents[0];
34
+ normalized.agent = chosenAgent;
35
+ normalized.task = step.task?.trim() || input.content.trim();
36
+ }
37
+ else {
38
+ normalized.content = step.content?.trim() || input.content.trim();
39
+ }
40
+ return normalized;
41
+ });
42
+ if (steps[0]) {
43
+ steps[0].attachments = input.attachments;
44
+ }
45
+ return {
46
+ goal: raw.goal?.trim() || input.content || "Handle user request",
47
+ stopCondition: raw.stopCondition?.trim() || "All plan steps completed successfully.",
48
+ steps,
49
+ };
50
+ }
51
+ function buildDeterministicPlan(input) {
52
+ const steps = [];
53
+ const content = input.content.trim();
54
+ const attachments = input.attachments;
55
+ if (input.intent.type === "agent_direct" && input.intent.targetAgent) {
56
+ const firstSpace = content.indexOf(" ");
57
+ const delegatedTask = firstSpace === -1 ? "" : content.slice(firstSpace + 1).trim();
58
+ steps.push({
59
+ id: createStepId(0),
60
+ kind: "delegate",
61
+ agent: input.intent.targetAgent,
62
+ task: delegatedTask,
63
+ attachments,
64
+ successCriteria: "Agent returns an output event.",
65
+ });
66
+ }
67
+ else {
68
+ steps.push({
69
+ id: createStepId(0),
70
+ kind: "manager",
71
+ content,
72
+ attachments,
73
+ successCriteria: "Manager emits a completion message.",
74
+ });
75
+ }
76
+ return {
77
+ goal: content || "Handle user request",
78
+ steps,
79
+ stopCondition: "All plan steps completed successfully.",
80
+ };
81
+ }
82
+ export async function createPlan(input, options = {}) {
83
+ if (input.intent.type === "agent_direct") {
84
+ return buildDeterministicPlan(input);
85
+ }
86
+ if (!options.model) {
87
+ return buildDeterministicPlan(input);
88
+ }
89
+ try {
90
+ const { object } = await generateObject({
91
+ model: options.model,
92
+ schema: PlanSchema,
93
+ system: `You are a strategic planner for an AI orchestration system.
94
+ Output compact, executable plans.
95
+ Available agents: ${input.knownAgents.join(", ") || "none"}.
96
+ Use "delegate" only when an agent is clearly needed; otherwise use "manager".`,
97
+ prompt: `Intent: ${JSON.stringify(input.intent)}
98
+ User request: ${input.content}
99
+ Return a plan with 1-4 steps.`,
100
+ });
101
+ return sanitizePlan(object, input);
102
+ }
103
+ catch {
104
+ return buildDeterministicPlan(input);
105
+ }
106
+ }
@@ -0,0 +1,121 @@
1
+ function minuteKey(date) {
2
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}T${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
3
+ }
4
+ function parseNumber(value) {
5
+ if (!/^\d+$/.test(value))
6
+ return null;
7
+ return Number(value);
8
+ }
9
+ function matchToken(value, token, min, max) {
10
+ const [base, stepRaw] = token.split("/");
11
+ const step = stepRaw ? parseNumber(stepRaw) : null;
12
+ if (stepRaw && (!step || step <= 0))
13
+ return false;
14
+ const checkStep = (start) => {
15
+ if (!step)
16
+ return true;
17
+ return (value - start) % step === 0;
18
+ };
19
+ if (base === "*") {
20
+ return checkStep(min);
21
+ }
22
+ if (base.includes("-")) {
23
+ const [startRaw, endRaw] = base.split("-");
24
+ const start = parseNumber(startRaw);
25
+ const end = parseNumber(endRaw);
26
+ if (start === null || end === null || start < min || end > max || start > end)
27
+ return false;
28
+ if (value < start || value > end)
29
+ return false;
30
+ return checkStep(start);
31
+ }
32
+ const exact = parseNumber(base);
33
+ if (exact === null || exact < min || exact > max)
34
+ return false;
35
+ if (value !== exact)
36
+ return false;
37
+ return checkStep(exact);
38
+ }
39
+ function matchField(value, field, min, max) {
40
+ const tokens = field.split(",").map((t) => t.trim()).filter(Boolean);
41
+ if (tokens.length === 0)
42
+ return false;
43
+ return tokens.some((token) => matchToken(value, token, min, max));
44
+ }
45
+ export function isCronDue(cron, date) {
46
+ const fields = cron.trim().split(/\s+/);
47
+ if (fields.length !== 5)
48
+ return false;
49
+ const [minuteField, hourField, domField, monthField, dowField] = fields;
50
+ const minute = date.getMinutes();
51
+ const hour = date.getHours();
52
+ const dayOfMonth = date.getDate();
53
+ const month = date.getMonth() + 1;
54
+ const dayOfWeek = date.getDay();
55
+ const minuteMatch = matchField(minute, minuteField, 0, 59);
56
+ const hourMatch = matchField(hour, hourField, 0, 23);
57
+ const monthMatch = matchField(month, monthField, 1, 12);
58
+ const domMatch = matchField(dayOfMonth, domField, 1, 31);
59
+ const dowMatch = matchField(dayOfWeek, dowField, 0, 7) || (dayOfWeek === 0 && matchField(7, dowField, 0, 7));
60
+ const domWildcard = domField.trim() === "*";
61
+ const dowWildcard = dowField.trim() === "*";
62
+ const dayMatch = domWildcard && dowWildcard
63
+ ? true
64
+ : domWildcard
65
+ ? dowMatch
66
+ : dowWildcard
67
+ ? domMatch
68
+ : domMatch || dowMatch;
69
+ return minuteMatch && hourMatch && monthMatch && dayMatch;
70
+ }
71
+ export function startAutomationWorker(options) {
72
+ const { listAutomations, runAutomation, pollIntervalMs = 60000, logger = console, } = options;
73
+ const inFlightByAutomation = new Set();
74
+ const seenMinuteByAutomation = new Map();
75
+ let polling = false;
76
+ const tick = async () => {
77
+ if (polling)
78
+ return;
79
+ polling = true;
80
+ try {
81
+ const now = new Date();
82
+ const currentMinute = minuteKey(now);
83
+ const automations = await listAutomations();
84
+ for (const automation of automations) {
85
+ if (!automation.enabled)
86
+ continue;
87
+ if (inFlightByAutomation.has(automation.id))
88
+ continue;
89
+ if (seenMinuteByAutomation.get(automation.id) === currentMinute)
90
+ continue;
91
+ const due = isCronDue(automation.cron, now);
92
+ if (!due)
93
+ continue;
94
+ seenMinuteByAutomation.set(automation.id, currentMinute);
95
+ inFlightByAutomation.add(automation.id);
96
+ void runAutomation(automation, new Date(now))
97
+ .catch((err) => {
98
+ logger.error(`[automations] Failed run for "${automation.name}" (${automation.id}):`, err);
99
+ })
100
+ .finally(() => {
101
+ inFlightByAutomation.delete(automation.id);
102
+ });
103
+ }
104
+ }
105
+ catch (err) {
106
+ logger.error("[automations] Worker tick failed:", err);
107
+ }
108
+ finally {
109
+ polling = false;
110
+ }
111
+ };
112
+ const interval = setInterval(() => {
113
+ void tick();
114
+ }, pollIntervalMs);
115
+ logger.info(`[automations] Worker started (poll interval ${pollIntervalMs}ms)`);
116
+ void tick();
117
+ return () => {
118
+ clearInterval(interval);
119
+ logger.info("[automations] Worker stopped");
120
+ };
121
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from "./config.js";
4
+ function getAutomationsPath() {
5
+ const cfg = loadConfig();
6
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
7
+ const resolvedBaseDir = resolvePath(baseDir);
8
+ return path.join(resolvedBaseDir, "automations.json");
9
+ }
10
+ export async function listAutomations() {
11
+ const filePath = getAutomationsPath();
12
+ try {
13
+ const raw = await fs.readFile(filePath, "utf-8");
14
+ const parsed = JSON.parse(raw);
15
+ if (!Array.isArray(parsed))
16
+ return [];
17
+ return parsed
18
+ .filter((item) => item && typeof item === "object")
19
+ .map((item) => {
20
+ const targetType = item.targetType === "agent" ? "agent" : "orchestrator";
21
+ const agentName = typeof item.agentName === "string" && item.agentName.trim()
22
+ ? item.agentName.trim()
23
+ : undefined;
24
+ return {
25
+ id: typeof item.id === "string" ? item.id : "",
26
+ name: typeof item.name === "string" ? item.name : "",
27
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
28
+ cron: typeof item.cron === "string" ? item.cron : "",
29
+ targetType,
30
+ agentName: targetType === "agent" ? agentName : undefined,
31
+ enabled: Boolean(item.enabled),
32
+ createdAt: typeof item.createdAt === "string" ? item.createdAt : new Date().toISOString(),
33
+ updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : new Date().toISOString(),
34
+ };
35
+ })
36
+ .filter((item) => {
37
+ if (!item.id || !item.name || !item.prompt || !item.cron)
38
+ return false;
39
+ if (item.targetType === "agent" && !item.agentName)
40
+ return false;
41
+ return true;
42
+ });
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export async function saveAutomations(items) {
49
+ const filePath = getAutomationsPath();
50
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
51
+ await fs.writeFile(filePath, JSON.stringify(items, null, 2), "utf-8");
52
+ }