@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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObserverBridge — thin adapter between the Observer layer and workflow triggers
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to ObserverManager events and routes them to the trigger system,
|
|
5
|
+
* mapping observer event types to canonical workflow trigger event types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ObserverManager, ObserverEvent } from '../../observers/index.ts';
|
|
9
|
+
|
|
10
|
+
// ── Types ──
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Canonical workflow trigger event types sourced from the Observer layer.
|
|
14
|
+
*/
|
|
15
|
+
export type ObserverTriggerType =
|
|
16
|
+
| 'file_change'
|
|
17
|
+
| 'clipboard'
|
|
18
|
+
| 'process'
|
|
19
|
+
| 'email'
|
|
20
|
+
| 'calendar'
|
|
21
|
+
| 'notification'
|
|
22
|
+
| 'screen';
|
|
23
|
+
|
|
24
|
+
export type TriggerCallback = (eventType: ObserverTriggerType, data: Record<string, unknown>) => void;
|
|
25
|
+
|
|
26
|
+
// ── Event type mapping ──
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps raw observer event type strings (observer.name + "." + event sub-type)
|
|
30
|
+
* to canonical workflow trigger types.
|
|
31
|
+
*
|
|
32
|
+
* Observer names and event types observed in the codebase:
|
|
33
|
+
* - FileWatcher: file_changed, file_created, file_deleted
|
|
34
|
+
* - ClipboardMonitor: clipboard_changed
|
|
35
|
+
* - ProcessMonitor: process_started, process_stopped, process_changed
|
|
36
|
+
* - EmailSync: email_received, email_sent
|
|
37
|
+
* - CalendarSync: calendar_event_created, calendar_event_updated, calendar_event_reminder
|
|
38
|
+
* - NotificationListener: notification_received
|
|
39
|
+
* - Screen/awareness: screen_changed, screen_text_detected
|
|
40
|
+
*/
|
|
41
|
+
const EVENT_TYPE_MAP: Record<string, ObserverTriggerType> = {
|
|
42
|
+
// File watcher
|
|
43
|
+
file_changed: 'file_change',
|
|
44
|
+
file_created: 'file_change',
|
|
45
|
+
file_deleted: 'file_change',
|
|
46
|
+
file_modified: 'file_change',
|
|
47
|
+
|
|
48
|
+
// Clipboard
|
|
49
|
+
clipboard_changed: 'clipboard',
|
|
50
|
+
clipboard_copied: 'clipboard',
|
|
51
|
+
|
|
52
|
+
// Process monitor
|
|
53
|
+
process_started: 'process',
|
|
54
|
+
process_stopped: 'process',
|
|
55
|
+
process_changed: 'process',
|
|
56
|
+
process_exited: 'process',
|
|
57
|
+
|
|
58
|
+
// Email
|
|
59
|
+
email_received: 'email',
|
|
60
|
+
email_sent: 'email',
|
|
61
|
+
email_new: 'email',
|
|
62
|
+
|
|
63
|
+
// Calendar
|
|
64
|
+
calendar_event_created: 'calendar',
|
|
65
|
+
calendar_event_updated: 'calendar',
|
|
66
|
+
calendar_event_reminder: 'calendar',
|
|
67
|
+
calendar_event_deleted: 'calendar',
|
|
68
|
+
|
|
69
|
+
// Notifications
|
|
70
|
+
notification_received: 'notification',
|
|
71
|
+
|
|
72
|
+
// Screen / awareness
|
|
73
|
+
screen_changed: 'screen',
|
|
74
|
+
screen_text_detected: 'screen',
|
|
75
|
+
screen_app_switched: 'screen',
|
|
76
|
+
ocr_text_changed: 'screen',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── ObserverBridge ──
|
|
80
|
+
|
|
81
|
+
export class ObserverBridge {
|
|
82
|
+
private observerManager: ObserverManager;
|
|
83
|
+
private triggerCallback: TriggerCallback | null = null;
|
|
84
|
+
private running = false;
|
|
85
|
+
|
|
86
|
+
constructor(observerManager: ObserverManager) {
|
|
87
|
+
this.observerManager = observerManager;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set the callback invoked when an observer event arrives.
|
|
92
|
+
*/
|
|
93
|
+
setTriggerCallback(cb: TriggerCallback): void {
|
|
94
|
+
this.triggerCallback = cb;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start the bridge — subscribe to all observer events.
|
|
99
|
+
*/
|
|
100
|
+
start(): void {
|
|
101
|
+
if (this.running) return;
|
|
102
|
+
|
|
103
|
+
this.observerManager.setEventHandler((event: ObserverEvent) => {
|
|
104
|
+
this.routeEvent(event);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.running = true;
|
|
108
|
+
console.log('[ObserverBridge] Started — listening to observer events');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop the bridge — clear the event handler.
|
|
113
|
+
* Observers continue running; we just stop routing their events.
|
|
114
|
+
*/
|
|
115
|
+
stop(): void {
|
|
116
|
+
if (!this.running) return;
|
|
117
|
+
|
|
118
|
+
// Replace the handler with a no-op so we stop routing events
|
|
119
|
+
this.observerManager.setEventHandler(() => {});
|
|
120
|
+
|
|
121
|
+
this.running = false;
|
|
122
|
+
console.log('[ObserverBridge] Stopped');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
isRunning(): boolean {
|
|
126
|
+
return this.running;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Internal ──
|
|
130
|
+
|
|
131
|
+
private routeEvent(event: ObserverEvent): void {
|
|
132
|
+
if (!this.triggerCallback) return;
|
|
133
|
+
|
|
134
|
+
const triggerType = this.mapEventType(event.type);
|
|
135
|
+
if (!triggerType) {
|
|
136
|
+
// Unknown event type — skip silently
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data: Record<string, unknown> = {
|
|
141
|
+
...event.data,
|
|
142
|
+
_observer: {
|
|
143
|
+
originalType: event.type,
|
|
144
|
+
timestamp: event.timestamp,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
this.triggerCallback(triggerType, data);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[ObserverBridge] Trigger callback threw for event type "${triggerType}":`, err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private mapEventType(rawType: string): ObserverTriggerType | null {
|
|
156
|
+
// Direct match
|
|
157
|
+
if (rawType in EVENT_TYPE_MAP) {
|
|
158
|
+
return EVENT_TYPE_MAP[rawType]!;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Prefix match (e.g. "file_" -> file_change)
|
|
162
|
+
if (rawType.startsWith('file_')) return 'file_change';
|
|
163
|
+
if (rawType.startsWith('clipboard_')) return 'clipboard';
|
|
164
|
+
if (rawType.startsWith('process_')) return 'process';
|
|
165
|
+
if (rawType.startsWith('email_')) return 'email';
|
|
166
|
+
if (rawType.startsWith('calendar_')) return 'calendar';
|
|
167
|
+
if (rawType.startsWith('notification_')) return 'notification';
|
|
168
|
+
if (rawType.startsWith('screen_') || rawType.startsWith('ocr_')) return 'screen';
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PollingEngine — outbound HTTP polling with deduplication
|
|
3
|
+
*
|
|
4
|
+
* Registers named polling jobs that periodically fetch a URL and invoke
|
|
5
|
+
* a callback when new data is detected (deduplicated by a configured field).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Types ──
|
|
9
|
+
|
|
10
|
+
export type PollConfig = {
|
|
11
|
+
/** Target URL to poll */
|
|
12
|
+
url: string;
|
|
13
|
+
/** How often to poll in milliseconds */
|
|
14
|
+
intervalMs: number;
|
|
15
|
+
/** HTTP method (default: GET) */
|
|
16
|
+
method?: string;
|
|
17
|
+
/** Additional request headers */
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
/** Request body (for POST/PUT) */
|
|
20
|
+
body?: string;
|
|
21
|
+
/**
|
|
22
|
+
* JSON path (dot-notation) into the response used for deduplication.
|
|
23
|
+
* If set, the callback is only fired when this value changes.
|
|
24
|
+
* Example: "data.updatedAt" or "id"
|
|
25
|
+
*/
|
|
26
|
+
deduplicateField?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PollCallback = (data: unknown, meta: PollMeta) => void;
|
|
30
|
+
|
|
31
|
+
export type PollMeta = {
|
|
32
|
+
id: string;
|
|
33
|
+
url: string;
|
|
34
|
+
status: number;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PollJob = {
|
|
39
|
+
id: string;
|
|
40
|
+
config: PollConfig;
|
|
41
|
+
callback: PollCallback;
|
|
42
|
+
handle: ReturnType<typeof setInterval>;
|
|
43
|
+
lastDeduplicateValue: unknown;
|
|
44
|
+
lastPolledAt: number | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── Helpers ──
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Traverse a dot-separated path on an object.
|
|
51
|
+
*/
|
|
52
|
+
function getNestedValue(obj: unknown, path: string): unknown {
|
|
53
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
54
|
+
const parts = path.split('.');
|
|
55
|
+
let current: unknown = obj;
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
if (current === null || current === undefined) return undefined;
|
|
58
|
+
if (typeof current !== 'object') return undefined;
|
|
59
|
+
current = (current as Record<string, unknown>)[part];
|
|
60
|
+
}
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── PollingEngine ──
|
|
65
|
+
|
|
66
|
+
export class PollingEngine {
|
|
67
|
+
private jobs: Map<string, PollJob> = new Map();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register and start a polling job.
|
|
71
|
+
*/
|
|
72
|
+
register(id: string, config: PollConfig, callback: PollCallback): void {
|
|
73
|
+
if (this.jobs.has(id)) {
|
|
74
|
+
this.unregister(id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (config.intervalMs < 1000) {
|
|
78
|
+
throw new Error(`Poll interval for "${id}" must be at least 1000ms`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const job: PollJob = {
|
|
82
|
+
id,
|
|
83
|
+
config,
|
|
84
|
+
callback,
|
|
85
|
+
handle: null as unknown as ReturnType<typeof setInterval>,
|
|
86
|
+
lastDeduplicateValue: Symbol('unset'), // Sentinel: never equal to real data
|
|
87
|
+
lastPolledAt: null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handle = setInterval(() => this.poll(id), config.intervalMs);
|
|
91
|
+
job.handle = handle;
|
|
92
|
+
this.jobs.set(id, job);
|
|
93
|
+
|
|
94
|
+
console.log(`[PollingEngine] Registered poll job "${id}" -> ${config.url} (every ${config.intervalMs}ms)`);
|
|
95
|
+
|
|
96
|
+
// Run immediately on first registration
|
|
97
|
+
this.poll(id).catch(err => {
|
|
98
|
+
console.error(`[PollingEngine] Initial poll for "${id}" failed:`, err);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop and remove a polling job.
|
|
104
|
+
*/
|
|
105
|
+
unregister(id: string): void {
|
|
106
|
+
const job = this.jobs.get(id);
|
|
107
|
+
if (job) {
|
|
108
|
+
clearInterval(job.handle);
|
|
109
|
+
this.jobs.delete(id);
|
|
110
|
+
console.log(`[PollingEngine] Unregistered poll job "${id}"`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stop and remove all polling jobs.
|
|
116
|
+
*/
|
|
117
|
+
unregisterAll(): void {
|
|
118
|
+
for (const job of this.jobs.values()) {
|
|
119
|
+
clearInterval(job.handle);
|
|
120
|
+
}
|
|
121
|
+
this.jobs.clear();
|
|
122
|
+
console.log('[PollingEngine] All poll jobs unregistered');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns IDs of all active poll jobs.
|
|
127
|
+
*/
|
|
128
|
+
getJobIds(): string[] {
|
|
129
|
+
return Array.from(this.jobs.keys());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Internal ──
|
|
133
|
+
|
|
134
|
+
private async poll(id: string): Promise<void> {
|
|
135
|
+
const job = this.jobs.get(id);
|
|
136
|
+
if (!job) return;
|
|
137
|
+
|
|
138
|
+
const { config, callback } = job;
|
|
139
|
+
const timestamp = Date.now();
|
|
140
|
+
job.lastPolledAt = timestamp;
|
|
141
|
+
|
|
142
|
+
let response: Response;
|
|
143
|
+
try {
|
|
144
|
+
response = await fetch(config.url, {
|
|
145
|
+
method: config.method ?? 'GET',
|
|
146
|
+
headers: config.headers,
|
|
147
|
+
body: config.body,
|
|
148
|
+
signal: AbortSignal.timeout(30_000),
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[PollingEngine] Fetch failed for "${id}" (${config.url}):`, err);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
console.warn(`[PollingEngine] Poll "${id}" got HTTP ${response.status}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let data: unknown;
|
|
161
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (contentType.includes('application/json')) {
|
|
165
|
+
data = await response.json();
|
|
166
|
+
} else {
|
|
167
|
+
data = await response.text();
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`[PollingEngine] Failed to parse response for "${id}":`, err);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Deduplication check
|
|
175
|
+
if (config.deduplicateField) {
|
|
176
|
+
const currentValue = getNestedValue(data, config.deduplicateField);
|
|
177
|
+
const serialized = JSON.stringify(currentValue);
|
|
178
|
+
const lastSerialized = JSON.stringify(job.lastDeduplicateValue);
|
|
179
|
+
|
|
180
|
+
if (serialized === lastSerialized) {
|
|
181
|
+
// No change — skip callback
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
job.lastDeduplicateValue = currentValue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const meta: PollMeta = {
|
|
189
|
+
id,
|
|
190
|
+
url: config.url,
|
|
191
|
+
status: response.status,
|
|
192
|
+
timestamp,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
callback(data, meta);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(`[PollingEngine] Callback for "${id}" threw:`, err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenConditionEvaluator — evaluates visual conditions for screen-based triggers
|
|
3
|
+
*
|
|
4
|
+
* Supports instant text/app conditions as well as LLM-backed visual checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── Types ──
|
|
8
|
+
|
|
9
|
+
export type ScreenConditionType =
|
|
10
|
+
| 'text_present'
|
|
11
|
+
| 'text_absent'
|
|
12
|
+
| 'app_active'
|
|
13
|
+
| 'visual_match'
|
|
14
|
+
| 'llm_check';
|
|
15
|
+
|
|
16
|
+
export type ScreenCondition = {
|
|
17
|
+
type: ScreenConditionType;
|
|
18
|
+
/** For text_present / text_absent: the text substring to look for */
|
|
19
|
+
text?: string;
|
|
20
|
+
/** For app_active: the application name to check */
|
|
21
|
+
appName?: string;
|
|
22
|
+
/** For visual_match: a description of what should be visible */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** For llm_check: a natural-language prompt sent to the LLM */
|
|
25
|
+
prompt?: string;
|
|
26
|
+
/** Optional: case-sensitive matching (default: false) */
|
|
27
|
+
caseSensitive?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ConditionResult = {
|
|
31
|
+
matched: boolean;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ── ScreenConditionEvaluator ──
|
|
36
|
+
|
|
37
|
+
export class ScreenConditionEvaluator {
|
|
38
|
+
private llmManager: unknown;
|
|
39
|
+
|
|
40
|
+
constructor(llmManager: unknown) {
|
|
41
|
+
this.llmManager = llmManager;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Evaluate a screen condition.
|
|
46
|
+
*
|
|
47
|
+
* - text_present / text_absent: instant, no LLM needed
|
|
48
|
+
* - app_active: instant, checks appName against current active app
|
|
49
|
+
* - visual_match / llm_check: defers to LLM (async, requires llmManager)
|
|
50
|
+
*
|
|
51
|
+
* @param condition - The condition to evaluate
|
|
52
|
+
* @param ocrText - Current screen OCR text (for text-based conditions)
|
|
53
|
+
* @param appName - Currently active application name (for app_active)
|
|
54
|
+
* @returns boolean result
|
|
55
|
+
*/
|
|
56
|
+
async evaluate(
|
|
57
|
+
condition: ScreenCondition,
|
|
58
|
+
ocrText?: string,
|
|
59
|
+
appName?: string,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
switch (condition.type) {
|
|
62
|
+
case 'text_present':
|
|
63
|
+
return this.evaluateTextPresent(condition, ocrText ?? '');
|
|
64
|
+
|
|
65
|
+
case 'text_absent':
|
|
66
|
+
return this.evaluateTextAbsent(condition, ocrText ?? '');
|
|
67
|
+
|
|
68
|
+
case 'app_active':
|
|
69
|
+
return this.evaluateAppActive(condition, appName ?? '');
|
|
70
|
+
|
|
71
|
+
case 'visual_match':
|
|
72
|
+
return this.evaluateVisualMatch(condition, ocrText ?? '', appName ?? '');
|
|
73
|
+
|
|
74
|
+
case 'llm_check':
|
|
75
|
+
return this.evaluateLlmCheck(condition, ocrText ?? '', appName ?? '');
|
|
76
|
+
|
|
77
|
+
default: {
|
|
78
|
+
// TypeScript exhaustiveness guard
|
|
79
|
+
const _exhaustive: never = condition.type;
|
|
80
|
+
console.warn(`[ScreenConditionEvaluator] Unknown condition type: ${String(_exhaustive)}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Instant evaluators ──
|
|
87
|
+
|
|
88
|
+
private evaluateTextPresent(condition: ScreenCondition, ocrText: string): boolean {
|
|
89
|
+
if (!condition.text) {
|
|
90
|
+
console.warn('[ScreenConditionEvaluator] text_present condition missing "text" field');
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (condition.caseSensitive) {
|
|
95
|
+
return ocrText.includes(condition.text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ocrText.toLowerCase().includes(condition.text.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private evaluateTextAbsent(condition: ScreenCondition, ocrText: string): boolean {
|
|
102
|
+
return !this.evaluateTextPresent({ ...condition, type: 'text_present' }, ocrText);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private evaluateAppActive(condition: ScreenCondition, activeApp: string): boolean {
|
|
106
|
+
if (!condition.appName) {
|
|
107
|
+
console.warn('[ScreenConditionEvaluator] app_active condition missing "appName" field');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// No active app is running — condition cannot match
|
|
112
|
+
if (!activeApp) return false;
|
|
113
|
+
|
|
114
|
+
const target = condition.caseSensitive
|
|
115
|
+
? condition.appName
|
|
116
|
+
: condition.appName.toLowerCase();
|
|
117
|
+
|
|
118
|
+
const current = condition.caseSensitive
|
|
119
|
+
? activeApp
|
|
120
|
+
: activeApp.toLowerCase();
|
|
121
|
+
|
|
122
|
+
// Support partial match (e.g. "Chrome" matches "Google Chrome")
|
|
123
|
+
return current.includes(target) || target.includes(current);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── LLM-backed evaluators ──
|
|
127
|
+
|
|
128
|
+
private async evaluateVisualMatch(
|
|
129
|
+
condition: ScreenCondition,
|
|
130
|
+
ocrText: string,
|
|
131
|
+
appName: string,
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
if (!condition.description) {
|
|
134
|
+
console.warn('[ScreenConditionEvaluator] visual_match condition missing "description" field');
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!this.llmManager) {
|
|
139
|
+
console.warn('[ScreenConditionEvaluator] No LLM manager available for visual_match — returning false');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const prompt = [
|
|
144
|
+
`You are evaluating a screen condition for workflow automation.`,
|
|
145
|
+
``,
|
|
146
|
+
`Current screen state:`,
|
|
147
|
+
`- Active application: ${appName || '(unknown)'}`,
|
|
148
|
+
`- OCR text on screen: ${ocrText ? `"${ocrText.slice(0, 2000)}"` : '(none)'}`,
|
|
149
|
+
``,
|
|
150
|
+
`Condition to check: "${condition.description}"`,
|
|
151
|
+
``,
|
|
152
|
+
`Does the current screen state match this description? Answer with ONLY "yes" or "no".`,
|
|
153
|
+
].join('\n');
|
|
154
|
+
|
|
155
|
+
return this.askLlm(prompt);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async evaluateLlmCheck(
|
|
159
|
+
condition: ScreenCondition,
|
|
160
|
+
ocrText: string,
|
|
161
|
+
appName: string,
|
|
162
|
+
): Promise<boolean> {
|
|
163
|
+
if (!condition.prompt) {
|
|
164
|
+
console.warn('[ScreenConditionEvaluator] llm_check condition missing "prompt" field');
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!this.llmManager) {
|
|
169
|
+
console.warn('[ScreenConditionEvaluator] No LLM manager available for llm_check — returning false');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const systemContext = [
|
|
174
|
+
`You are a screen state evaluator for workflow automation.`,
|
|
175
|
+
`Current screen state:`,
|
|
176
|
+
`- Active application: ${appName || '(unknown)'}`,
|
|
177
|
+
`- OCR text visible: ${ocrText ? `"${ocrText.slice(0, 2000)}"` : '(none)'}`,
|
|
178
|
+
``,
|
|
179
|
+
`Answer the following question about the screen state with ONLY "yes" or "no".`,
|
|
180
|
+
].join('\n');
|
|
181
|
+
|
|
182
|
+
const fullPrompt = `${systemContext}\n\n${condition.prompt}`;
|
|
183
|
+
return this.askLlm(fullPrompt);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Ask the LLM manager a yes/no question and parse the response.
|
|
188
|
+
*/
|
|
189
|
+
private async askLlm(prompt: string): Promise<boolean> {
|
|
190
|
+
try {
|
|
191
|
+
// llmManager is loosely typed (unknown) to avoid circular deps.
|
|
192
|
+
// We cast it to access the expected interface.
|
|
193
|
+
const mgr = this.llmManager as {
|
|
194
|
+
complete?: (prompt: string, opts?: Record<string, unknown>) => Promise<{ text: string }>;
|
|
195
|
+
chat?: (messages: Array<{ role: string; content: string }>) => Promise<{ content: string }>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let responseText: string | null = null;
|
|
199
|
+
|
|
200
|
+
if (typeof mgr.complete === 'function') {
|
|
201
|
+
const result = await mgr.complete(prompt, { max_tokens: 10, temperature: 0 });
|
|
202
|
+
responseText = result.text;
|
|
203
|
+
} else if (typeof mgr.chat === 'function') {
|
|
204
|
+
const result = await mgr.chat([{ role: 'user', content: prompt }]);
|
|
205
|
+
responseText = result.content;
|
|
206
|
+
} else {
|
|
207
|
+
console.warn('[ScreenConditionEvaluator] llmManager has no usable interface');
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const normalized = (responseText ?? '').trim().toLowerCase();
|
|
212
|
+
return normalized.startsWith('yes');
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('[ScreenConditionEvaluator] LLM evaluation failed:', err);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|