@usejarvis/brain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronScheduler — lightweight cron expression parser and scheduler
|
|
3
|
+
*
|
|
4
|
+
* Supports standard 5-field cron: minute hour dayOfMonth month dayOfWeek
|
|
5
|
+
* Special characters: * / - and comma-separated lists
|
|
6
|
+
* No external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Types ──
|
|
10
|
+
|
|
11
|
+
export type CronJob = {
|
|
12
|
+
id: string;
|
|
13
|
+
expression: string;
|
|
14
|
+
callback: () => void;
|
|
15
|
+
lastRun: number | null;
|
|
16
|
+
nextRun: number;
|
|
17
|
+
handle: ReturnType<typeof setInterval>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CronJobInfo = {
|
|
21
|
+
id: string;
|
|
22
|
+
expression: string;
|
|
23
|
+
lastRun: number | null;
|
|
24
|
+
nextRun: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ── Parser helpers ──
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a single cron field value into a sorted array of matching integers.
|
|
31
|
+
*/
|
|
32
|
+
function parseField(field: string, min: number, max: number): number[] {
|
|
33
|
+
const values = new Set<number>();
|
|
34
|
+
|
|
35
|
+
for (const part of field.split(',')) {
|
|
36
|
+
const trimmed = part.trim();
|
|
37
|
+
|
|
38
|
+
// Wildcard: *
|
|
39
|
+
if (trimmed === '*') {
|
|
40
|
+
for (let i = min; i <= max; i++) values.add(i);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step: */n or start/n
|
|
45
|
+
if (trimmed.includes('/')) {
|
|
46
|
+
const [rangeStr, stepStr] = trimmed.split('/');
|
|
47
|
+
const step = parseInt(stepStr!, 10);
|
|
48
|
+
if (isNaN(step) || step <= 0) throw new Error(`Invalid step in cron field: "${trimmed}"`);
|
|
49
|
+
|
|
50
|
+
let rangeMin = min;
|
|
51
|
+
let rangeMax = max;
|
|
52
|
+
|
|
53
|
+
if (rangeStr !== '*') {
|
|
54
|
+
if (rangeStr!.includes('-')) {
|
|
55
|
+
const [a, b] = rangeStr!.split('-').map(s => parseInt(s, 10));
|
|
56
|
+
rangeMin = a!;
|
|
57
|
+
rangeMax = b!;
|
|
58
|
+
} else {
|
|
59
|
+
rangeMin = parseInt(rangeStr!, 10);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (let i = rangeMin; i <= rangeMax; i += step) values.add(i);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Range: a-b
|
|
68
|
+
if (trimmed.includes('-')) {
|
|
69
|
+
const [a, b] = trimmed.split('-').map(s => parseInt(s, 10));
|
|
70
|
+
if (isNaN(a!) || isNaN(b!)) throw new Error(`Invalid range in cron field: "${trimmed}"`);
|
|
71
|
+
for (let i = a!; i <= b!; i += 1) values.add(i);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Literal value
|
|
76
|
+
const val = parseInt(trimmed, 10);
|
|
77
|
+
if (isNaN(val)) throw new Error(`Invalid value in cron field: "${trimmed}"`);
|
|
78
|
+
values.add(val);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Array.from(values).sort((a, b) => a - b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a full 5-field cron expression into its component arrays.
|
|
86
|
+
*/
|
|
87
|
+
function parseExpression(expression: string): {
|
|
88
|
+
minutes: number[];
|
|
89
|
+
hours: number[];
|
|
90
|
+
daysOfMonth: number[];
|
|
91
|
+
months: number[];
|
|
92
|
+
daysOfWeek: number[];
|
|
93
|
+
} {
|
|
94
|
+
const parts = expression.trim().split(/\s+/);
|
|
95
|
+
if (parts.length !== 5) {
|
|
96
|
+
throw new Error(`Invalid cron expression "${expression}": expected 5 fields, got ${parts.length}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const [minField, hourField, domField, monthField, dowField] = parts;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
minutes: parseField(minField!, 0, 59),
|
|
103
|
+
hours: parseField(hourField!, 0, 23),
|
|
104
|
+
daysOfMonth: parseField(domField!, 1, 31),
|
|
105
|
+
months: parseField(monthField!, 1, 12),
|
|
106
|
+
daysOfWeek: parseField(dowField!, 0, 6), // 0 = Sunday
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── CronScheduler ──
|
|
111
|
+
|
|
112
|
+
export class CronScheduler {
|
|
113
|
+
private jobs: Map<string, CronJob> = new Map();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a cron expression matches a given date.
|
|
117
|
+
*/
|
|
118
|
+
static matches(expression: string, date: Date = new Date()): boolean {
|
|
119
|
+
try {
|
|
120
|
+
const { minutes, hours, daysOfMonth, months, daysOfWeek } = parseExpression(expression);
|
|
121
|
+
|
|
122
|
+
const minute = date.getMinutes();
|
|
123
|
+
const hour = date.getHours();
|
|
124
|
+
const dom = date.getDate();
|
|
125
|
+
const month = date.getMonth() + 1; // getMonth() is 0-based
|
|
126
|
+
const dow = date.getDay(); // 0 = Sunday
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
minutes.includes(minute) &&
|
|
130
|
+
hours.includes(hour) &&
|
|
131
|
+
daysOfMonth.includes(dom) &&
|
|
132
|
+
months.includes(month) &&
|
|
133
|
+
daysOfWeek.includes(dow)
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate the next execution time for a cron expression.
|
|
142
|
+
* @param expression - 5-field cron expression
|
|
143
|
+
* @param from - start searching from this date (default: now)
|
|
144
|
+
* @returns Date of next execution, or null if none found within 1 year
|
|
145
|
+
*/
|
|
146
|
+
static nextRun(expression: string, from: Date = new Date()): Date | null {
|
|
147
|
+
try {
|
|
148
|
+
const { minutes, hours, daysOfMonth, months, daysOfWeek } = parseExpression(expression);
|
|
149
|
+
|
|
150
|
+
// Start from the next minute
|
|
151
|
+
const start = new Date(from);
|
|
152
|
+
start.setSeconds(0, 0);
|
|
153
|
+
start.setMinutes(start.getMinutes() + 1);
|
|
154
|
+
|
|
155
|
+
// Search up to 1 year ahead (minute-by-minute is too slow; step by minute smartly)
|
|
156
|
+
const limit = new Date(from);
|
|
157
|
+
limit.setFullYear(limit.getFullYear() + 1);
|
|
158
|
+
|
|
159
|
+
const candidate = new Date(start);
|
|
160
|
+
|
|
161
|
+
while (candidate < limit) {
|
|
162
|
+
const month = candidate.getMonth() + 1;
|
|
163
|
+
const dom = candidate.getDate();
|
|
164
|
+
const hour = candidate.getHours();
|
|
165
|
+
const minute = candidate.getMinutes();
|
|
166
|
+
const dow = candidate.getDay();
|
|
167
|
+
|
|
168
|
+
if (!months.includes(month)) {
|
|
169
|
+
// Advance to next valid month
|
|
170
|
+
candidate.setMonth(candidate.getMonth() + 1);
|
|
171
|
+
candidate.setDate(1);
|
|
172
|
+
candidate.setHours(0, 0, 0, 0);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!daysOfMonth.includes(dom) || !daysOfWeek.includes(dow)) {
|
|
177
|
+
// Advance to next day
|
|
178
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
179
|
+
candidate.setHours(0, 0, 0, 0);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!hours.includes(hour)) {
|
|
184
|
+
// Find next valid hour
|
|
185
|
+
const nextHour = hours.find(h => h > hour);
|
|
186
|
+
if (nextHour !== undefined) {
|
|
187
|
+
candidate.setHours(nextHour, 0, 0, 0);
|
|
188
|
+
} else {
|
|
189
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
190
|
+
candidate.setHours(0, 0, 0, 0);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!minutes.includes(minute)) {
|
|
196
|
+
// Find next valid minute in this hour
|
|
197
|
+
const nextMinute = minutes.find(m => m > minute);
|
|
198
|
+
if (nextMinute !== undefined) {
|
|
199
|
+
candidate.setMinutes(nextMinute, 0, 0);
|
|
200
|
+
} else {
|
|
201
|
+
// Advance to next valid hour
|
|
202
|
+
const nextHour = hours.find(h => h > hour);
|
|
203
|
+
if (nextHour !== undefined) {
|
|
204
|
+
candidate.setHours(nextHour, 0, 0, 0);
|
|
205
|
+
} else {
|
|
206
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
207
|
+
candidate.setHours(0, 0, 0, 0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// All fields match
|
|
214
|
+
return new Date(candidate);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Schedule a recurring callback based on a cron expression.
|
|
225
|
+
* Uses setInterval to check every 30 seconds whether the expression matches.
|
|
226
|
+
*/
|
|
227
|
+
schedule(id: string, expression: string, callback: () => void): void {
|
|
228
|
+
if (this.jobs.has(id)) {
|
|
229
|
+
this.cancel(id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Validate expression eagerly
|
|
233
|
+
parseExpression(expression);
|
|
234
|
+
|
|
235
|
+
const nextRun = CronScheduler.nextRun(expression);
|
|
236
|
+
if (!nextRun) {
|
|
237
|
+
throw new Error(`Cron expression "${expression}" has no upcoming execution times`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let lastTickMinute = -1;
|
|
241
|
+
|
|
242
|
+
const handle = setInterval(() => {
|
|
243
|
+
const now = new Date();
|
|
244
|
+
const currentMinute = now.getFullYear() * 525960 + (now.getMonth() + 1) * 43830 + now.getDate() * 1440 + now.getHours() * 60 + now.getMinutes();
|
|
245
|
+
|
|
246
|
+
// Only evaluate once per minute
|
|
247
|
+
if (currentMinute === lastTickMinute) return;
|
|
248
|
+
lastTickMinute = currentMinute;
|
|
249
|
+
|
|
250
|
+
if (CronScheduler.matches(expression, now)) {
|
|
251
|
+
const job = this.jobs.get(id);
|
|
252
|
+
if (job) {
|
|
253
|
+
job.lastRun = Date.now();
|
|
254
|
+
const next = CronScheduler.nextRun(expression, now);
|
|
255
|
+
job.nextRun = next ? next.getTime() : Date.now();
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
callback();
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(`[CronScheduler] Job "${id}" threw an error:`, err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}, 30_000);
|
|
264
|
+
|
|
265
|
+
this.jobs.set(id, {
|
|
266
|
+
id,
|
|
267
|
+
expression,
|
|
268
|
+
callback,
|
|
269
|
+
lastRun: null,
|
|
270
|
+
nextRun: nextRun.getTime(),
|
|
271
|
+
handle,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
console.log(`[CronScheduler] Scheduled job "${id}" (${expression}), next run: ${nextRun.toISOString()}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Cancel a specific scheduled job.
|
|
279
|
+
*/
|
|
280
|
+
cancel(id: string): void {
|
|
281
|
+
const job = this.jobs.get(id);
|
|
282
|
+
if (job) {
|
|
283
|
+
clearInterval(job.handle);
|
|
284
|
+
this.jobs.delete(id);
|
|
285
|
+
console.log(`[CronScheduler] Cancelled job "${id}"`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Cancel all scheduled jobs.
|
|
291
|
+
*/
|
|
292
|
+
cancelAll(): void {
|
|
293
|
+
for (const job of this.jobs.values()) {
|
|
294
|
+
clearInterval(job.handle);
|
|
295
|
+
}
|
|
296
|
+
this.jobs.clear();
|
|
297
|
+
console.log('[CronScheduler] All jobs cancelled');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns info about all active jobs (without the handle or callback).
|
|
302
|
+
*/
|
|
303
|
+
getJobs(): CronJobInfo[] {
|
|
304
|
+
return Array.from(this.jobs.values()).map(({ id, expression, lastRun, nextRun }) => ({
|
|
305
|
+
id,
|
|
306
|
+
expression,
|
|
307
|
+
lastRun,
|
|
308
|
+
nextRun,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TriggerManager — ties together all trigger mechanisms and maps them to the WorkflowEngine
|
|
3
|
+
*
|
|
4
|
+
* Implements the Service interface and acts as the single entry point for:
|
|
5
|
+
* - Cron-based scheduling (trigger.cron)
|
|
6
|
+
* - Inbound webhooks (trigger.webhook)
|
|
7
|
+
* - Outbound HTTP polling (trigger.poll)
|
|
8
|
+
* - Observer-based events (trigger.file_change, trigger.clipboard, etc.)
|
|
9
|
+
* - Manual triggers (fired externally via fireTrigger)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Service, ServiceStatus } from '../../daemon/services.ts';
|
|
13
|
+
import type { WorkflowEngine } from '../engine.ts';
|
|
14
|
+
import type { WorkflowDefinition, WorkflowNode } from '../types.ts';
|
|
15
|
+
import { CronScheduler } from './cron.ts';
|
|
16
|
+
import { WebhookManager } from './webhook.ts';
|
|
17
|
+
import { PollingEngine } from './poller.ts';
|
|
18
|
+
import type { PollConfig } from './poller.ts';
|
|
19
|
+
import * as vault from '../../vault/workflows.ts';
|
|
20
|
+
|
|
21
|
+
// ── Types ──
|
|
22
|
+
|
|
23
|
+
/** All trigger node types recognised by the TriggerManager */
|
|
24
|
+
const TRIGGER_TYPES = new Set([
|
|
25
|
+
'trigger.cron',
|
|
26
|
+
'trigger.webhook',
|
|
27
|
+
'trigger.poll',
|
|
28
|
+
'trigger.manual',
|
|
29
|
+
'trigger.file_change',
|
|
30
|
+
'trigger.clipboard',
|
|
31
|
+
'trigger.process',
|
|
32
|
+
'trigger.email',
|
|
33
|
+
'trigger.calendar',
|
|
34
|
+
'trigger.notification',
|
|
35
|
+
'trigger.screen',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// ── TriggerManager ──
|
|
39
|
+
|
|
40
|
+
export class TriggerManager implements Service {
|
|
41
|
+
readonly name = 'trigger-manager';
|
|
42
|
+
|
|
43
|
+
private _status: ServiceStatus = 'stopped';
|
|
44
|
+
private engine: WorkflowEngine;
|
|
45
|
+
|
|
46
|
+
private cron: CronScheduler;
|
|
47
|
+
private webhooks: WebhookManager;
|
|
48
|
+
private poller: PollingEngine;
|
|
49
|
+
|
|
50
|
+
/** workflowId → set of registered trigger identifiers */
|
|
51
|
+
private registrations: Map<string, Set<string>> = new Map();
|
|
52
|
+
|
|
53
|
+
constructor(workflowEngine: WorkflowEngine) {
|
|
54
|
+
this.engine = workflowEngine;
|
|
55
|
+
this.cron = new CronScheduler();
|
|
56
|
+
this.webhooks = new WebhookManager();
|
|
57
|
+
this.poller = new PollingEngine();
|
|
58
|
+
|
|
59
|
+
// Wire webhook callbacks
|
|
60
|
+
this.webhooks.setTriggerCallback((workflowId, data) => {
|
|
61
|
+
this.fire(workflowId, 'webhook', data);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Service lifecycle ──
|
|
66
|
+
|
|
67
|
+
async start(): Promise<void> {
|
|
68
|
+
this._status = 'starting';
|
|
69
|
+
try {
|
|
70
|
+
await this.reloadAll();
|
|
71
|
+
this._status = 'running';
|
|
72
|
+
console.log('[TriggerManager] Started');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this._status = 'error';
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stop(): Promise<void> {
|
|
80
|
+
this._status = 'stopping';
|
|
81
|
+
|
|
82
|
+
this.cron.cancelAll();
|
|
83
|
+
this.poller.unregisterAll();
|
|
84
|
+
this.registrations.clear();
|
|
85
|
+
// Webhooks are stateless HTTP handlers; nothing to teardown at the transport level
|
|
86
|
+
|
|
87
|
+
this._status = 'stopped';
|
|
88
|
+
console.log('[TriggerManager] Stopped');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
status(): ServiceStatus {
|
|
92
|
+
return this._status;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Public API ──
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register all trigger nodes found in a workflow definition.
|
|
99
|
+
*/
|
|
100
|
+
registerWorkflow(workflowId: string, definition: WorkflowDefinition): void {
|
|
101
|
+
// Remove any existing triggers first (idempotent)
|
|
102
|
+
this.unregisterWorkflow(workflowId);
|
|
103
|
+
|
|
104
|
+
const triggerNodes = definition.nodes.filter(n => TRIGGER_TYPES.has(n.type));
|
|
105
|
+
const ids = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const node of triggerNodes) {
|
|
108
|
+
const nodeKey = `${workflowId}:${node.id}`;
|
|
109
|
+
this.registerTriggerNode(workflowId, node, nodeKey);
|
|
110
|
+
ids.add(nodeKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ids.size > 0) {
|
|
114
|
+
this.registrations.set(workflowId, ids);
|
|
115
|
+
console.log(`[TriggerManager] Registered ${ids.size} trigger(s) for workflow "${workflowId}"`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Unregister all triggers associated with a workflow.
|
|
121
|
+
*/
|
|
122
|
+
unregisterWorkflow(workflowId: string): void {
|
|
123
|
+
const ids = this.registrations.get(workflowId);
|
|
124
|
+
if (!ids) return;
|
|
125
|
+
|
|
126
|
+
for (const id of ids) {
|
|
127
|
+
this.cron.cancel(id);
|
|
128
|
+
this.poller.unregister(id);
|
|
129
|
+
// Webhook is keyed by workflowId, unregister once
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.webhooks.unregister(workflowId);
|
|
133
|
+
this.registrations.delete(workflowId);
|
|
134
|
+
console.log(`[TriggerManager] Unregistered triggers for workflow "${workflowId}"`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Load all enabled workflows from the vault and register their triggers.
|
|
139
|
+
* Safe to call multiple times (idempotent — clears first).
|
|
140
|
+
*/
|
|
141
|
+
async reloadAll(): Promise<void> {
|
|
142
|
+
// Clear existing
|
|
143
|
+
this.cron.cancelAll();
|
|
144
|
+
this.poller.unregisterAll();
|
|
145
|
+
this.registrations.clear();
|
|
146
|
+
|
|
147
|
+
const workflows = vault.findWorkflows({ enabled: true });
|
|
148
|
+
let registered = 0;
|
|
149
|
+
|
|
150
|
+
for (const wf of workflows) {
|
|
151
|
+
const version = vault.getLatestVersion(wf.id);
|
|
152
|
+
if (!version) continue;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
this.registerWorkflow(wf.id, version.definition);
|
|
156
|
+
registered++;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(`[TriggerManager] Failed to register triggers for workflow "${wf.name}":`, err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`[TriggerManager] Loaded triggers for ${registered}/${workflows.length} workflows`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Manually fire a trigger for a workflow (useful for manual/test triggers).
|
|
167
|
+
*/
|
|
168
|
+
fireTrigger(workflowId: string, triggerType: string, data?: Record<string, unknown>): void {
|
|
169
|
+
this.fire(workflowId, triggerType, data ?? {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Accessors ──
|
|
173
|
+
|
|
174
|
+
getCronScheduler(): CronScheduler {
|
|
175
|
+
return this.cron;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getWebhookManager(): WebhookManager {
|
|
179
|
+
return this.webhooks;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getPollingEngine(): PollingEngine {
|
|
183
|
+
return this.poller;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Internal ──
|
|
187
|
+
|
|
188
|
+
private registerTriggerNode(workflowId: string, node: WorkflowNode, nodeKey: string): void {
|
|
189
|
+
switch (node.type) {
|
|
190
|
+
case 'trigger.cron':
|
|
191
|
+
this.registerCronTrigger(workflowId, node, nodeKey);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'trigger.webhook':
|
|
195
|
+
this.registerWebhookTrigger(workflowId, node);
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case 'trigger.poll':
|
|
199
|
+
this.registerPollTrigger(workflowId, node, nodeKey);
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'trigger.manual':
|
|
203
|
+
// Manual triggers are fired programmatically via fireTrigger() — no setup needed
|
|
204
|
+
console.log(`[TriggerManager] Manual trigger registered for workflow "${workflowId}" (node: ${node.id})`);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
// Observer-sourced triggers — registered via ObserverBridge externally,
|
|
208
|
+
// but we still track them in registrations for cleanup purposes
|
|
209
|
+
case 'trigger.file_change':
|
|
210
|
+
case 'trigger.clipboard':
|
|
211
|
+
case 'trigger.process':
|
|
212
|
+
case 'trigger.email':
|
|
213
|
+
case 'trigger.calendar':
|
|
214
|
+
case 'trigger.notification':
|
|
215
|
+
case 'trigger.screen':
|
|
216
|
+
console.log(`[TriggerManager] Observer trigger "${node.type}" registered for workflow "${workflowId}"`);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
console.warn(`[TriggerManager] Unknown trigger type "${node.type}" in workflow "${workflowId}"`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private registerCronTrigger(workflowId: string, node: WorkflowNode, key: string): void {
|
|
225
|
+
const expression = node.config.expression as string | undefined;
|
|
226
|
+
if (!expression) {
|
|
227
|
+
console.warn(`[TriggerManager] Cron trigger node "${node.id}" in workflow "${workflowId}" has no expression`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
this.cron.schedule(key, expression, () => {
|
|
233
|
+
this.fire(workflowId, 'cron', { expression, nodeId: node.id });
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(`[TriggerManager] Failed to schedule cron "${expression}" for workflow "${workflowId}":`, err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private registerWebhookTrigger(workflowId: string, node: WorkflowNode): void {
|
|
241
|
+
const secret = node.config.secret as string | undefined;
|
|
242
|
+
const path = this.webhooks.register(workflowId, secret);
|
|
243
|
+
console.log(`[TriggerManager] Webhook trigger for workflow "${workflowId}" registered at ${path}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private registerPollTrigger(workflowId: string, node: WorkflowNode, key: string): void {
|
|
247
|
+
const url = node.config.url as string | undefined;
|
|
248
|
+
if (!url) {
|
|
249
|
+
console.warn(`[TriggerManager] Poll trigger node "${node.id}" in workflow "${workflowId}" has no url`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const config: PollConfig = {
|
|
254
|
+
url,
|
|
255
|
+
intervalMs: (node.config.intervalMs as number | undefined) ?? 60_000,
|
|
256
|
+
method: (node.config.method as string | undefined) ?? 'GET',
|
|
257
|
+
headers: node.config.headers as Record<string, string> | undefined,
|
|
258
|
+
body: node.config.body as string | undefined,
|
|
259
|
+
deduplicateField: node.config.deduplicateField as string | undefined,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
this.poller.register(key, config, (data, meta) => {
|
|
264
|
+
this.fire(workflowId, 'poll', {
|
|
265
|
+
data,
|
|
266
|
+
url: meta.url,
|
|
267
|
+
status: meta.status,
|
|
268
|
+
timestamp: meta.timestamp,
|
|
269
|
+
nodeId: node.id,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(`[TriggerManager] Failed to register poll trigger for workflow "${workflowId}":`, err);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Fire the WorkflowEngine for a given workflow trigger.
|
|
279
|
+
*/
|
|
280
|
+
private fire(workflowId: string, triggerType: string, data: Record<string, unknown>): void {
|
|
281
|
+
this.engine.execute(workflowId, triggerType, data).catch(err => {
|
|
282
|
+
console.error(`[TriggerManager] Execution failed for workflow "${workflowId}" (trigger: ${triggerType}):`, err);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|