@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.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Workflow Auto-Suggest — Analyzes M13 awareness patterns to suggest workflows
3
+ *
4
+ * Detects:
5
+ * - Repeated app switches (e.g., copy from browser → paste in editor)
6
+ * - Recurring errors in the same app
7
+ * - Regular-interval manual tasks (e.g., checking email every 30 min)
8
+ * - Repeated tool usage patterns
9
+ */
10
+
11
+ import type { NodeRegistry } from './nodes/registry.ts';
12
+ import type { WorkflowDefinition } from './types.ts';
13
+
14
+ export type WorkflowSuggestion = {
15
+ id: string;
16
+ title: string;
17
+ description: string;
18
+ confidence: number; // 0-1
19
+ category: 'repetitive_action' | 'error_response' | 'scheduled_task' | 'app_pattern';
20
+ previewDefinition: WorkflowDefinition;
21
+ patternEvidence: string[];
22
+ };
23
+
24
+ type AwarenessPattern = {
25
+ type: string;
26
+ data: Record<string, unknown>;
27
+ timestamp: number;
28
+ };
29
+
30
+ export class WorkflowAutoSuggest {
31
+ private nodeRegistry: NodeRegistry;
32
+ private llmManager: any;
33
+ private patterns: AwarenessPattern[] = [];
34
+ private suggestions: WorkflowSuggestion[] = [];
35
+ private lastAnalysis = 0;
36
+ private analysisCooldownMs = 300_000; // 5 min
37
+
38
+ constructor(nodeRegistry: NodeRegistry, llmManager: unknown) {
39
+ this.nodeRegistry = nodeRegistry;
40
+ this.llmManager = llmManager;
41
+ }
42
+
43
+ /**
44
+ * Feed awareness events into the pattern buffer
45
+ */
46
+ addPattern(event: AwarenessPattern): void {
47
+ this.patterns.push(event);
48
+ // Keep last 500 events
49
+ if (this.patterns.length > 500) {
50
+ this.patterns = this.patterns.slice(-500);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Generate workflow suggestions from accumulated patterns
56
+ */
57
+ async generateSuggestions(): Promise<WorkflowSuggestion[]> {
58
+ const now = Date.now();
59
+ // Don't analyze too frequently
60
+ if (now - this.lastAnalysis < this.analysisCooldownMs && this.suggestions.length > 0) {
61
+ return this.suggestions;
62
+ }
63
+ this.lastAnalysis = now;
64
+
65
+ if (this.patterns.length < 10) {
66
+ return this.suggestions;
67
+ }
68
+
69
+ const detectedPatterns: WorkflowSuggestion[] = [];
70
+
71
+ // Detect repeated app switches
72
+ const appSwitches = this.detectAppSwitchPatterns();
73
+ detectedPatterns.push(...appSwitches);
74
+
75
+ // Detect recurring errors
76
+ const errorPatterns = this.detectErrorPatterns();
77
+ detectedPatterns.push(...errorPatterns);
78
+
79
+ // Detect scheduled-like behavior
80
+ const scheduledPatterns = this.detectScheduledPatterns();
81
+ detectedPatterns.push(...scheduledPatterns);
82
+
83
+ // Use LLM for more complex pattern detection if we have enough data
84
+ if (this.patterns.length > 50) {
85
+ try {
86
+ const llmSuggestions = await this.llmAnalyzePatterns();
87
+ detectedPatterns.push(...llmSuggestions);
88
+ } catch {
89
+ // LLM analysis is best-effort
90
+ }
91
+ }
92
+
93
+ // Deduplicate by title
94
+ const seen = new Set<string>();
95
+ this.suggestions = detectedPatterns.filter(s => {
96
+ if (seen.has(s.title)) return false;
97
+ seen.add(s.title);
98
+ return true;
99
+ }).slice(0, 10);
100
+
101
+ return this.suggestions;
102
+ }
103
+
104
+ /**
105
+ * Dismiss a suggestion
106
+ */
107
+ dismiss(id: string): void {
108
+ this.suggestions = this.suggestions.filter(s => s.id !== id);
109
+ }
110
+
111
+ // ── Pattern detection ──
112
+
113
+ private detectAppSwitchPatterns(): WorkflowSuggestion[] {
114
+ const suggestions: WorkflowSuggestion[] = [];
115
+ const appSwitchEvents = this.patterns.filter(p => p.type === 'context_change');
116
+
117
+ // Look for repeated A→B→A switches (copy-paste patterns)
118
+ const switchPairs: Map<string, number> = new Map();
119
+ for (let i = 1; i < appSwitchEvents.length; i++) {
120
+ const from = String(appSwitchEvents[i - 1]!.data.appName ?? '');
121
+ const to = String(appSwitchEvents[i]!.data.appName ?? '');
122
+ if (from && to && from !== to) {
123
+ const key = `${from}→${to}`;
124
+ switchPairs.set(key, (switchPairs.get(key) ?? 0) + 1);
125
+ }
126
+ }
127
+
128
+ for (const [pair, count] of switchPairs) {
129
+ if (count >= 5) {
130
+ const [from, to] = pair.split('→');
131
+ suggestions.push({
132
+ id: `suggest-switch-${Date.now()}-${from}-${to}`,
133
+ title: `Automate ${from} → ${to} workflow`,
134
+ description: `You frequently switch between ${from} and ${to} (${count} times recently). Consider automating this.`,
135
+ confidence: Math.min(count / 20, 0.9),
136
+ category: 'app_pattern',
137
+ patternEvidence: [`${count} switches between ${from} and ${to}`],
138
+ previewDefinition: {
139
+ nodes: [
140
+ { id: 'trigger-1', type: 'trigger.screen', label: `Watch ${from}`, position: { x: 100, y: 200 }, config: { condition_type: 'app_active', app_name: from } },
141
+ { id: 'action-1', type: 'action.notification', label: `Notify about ${to}`, position: { x: 400, y: 200 }, config: { message: `Ready to switch to ${to}?` } },
142
+ ],
143
+ edges: [
144
+ { id: 'e-1', source: 'trigger-1', target: 'action-1' },
145
+ ],
146
+ settings: { maxRetries: 1, retryDelayMs: 5000, timeoutMs: 60000, parallelism: 'sequential', onError: 'stop' },
147
+ },
148
+ });
149
+ }
150
+ }
151
+
152
+ return suggestions;
153
+ }
154
+
155
+ private detectErrorPatterns(): WorkflowSuggestion[] {
156
+ const suggestions: WorkflowSuggestion[] = [];
157
+ const errorEvents = this.patterns.filter(p => p.type === 'error_detected');
158
+
159
+ // Group errors by app
160
+ const errorsByApp: Map<string, number> = new Map();
161
+ for (const evt of errorEvents) {
162
+ const app = String(evt.data.appName ?? 'unknown');
163
+ errorsByApp.set(app, (errorsByApp.get(app) ?? 0) + 1);
164
+ }
165
+
166
+ for (const [app, count] of errorsByApp) {
167
+ if (count >= 3) {
168
+ suggestions.push({
169
+ id: `suggest-error-${Date.now()}-${app}`,
170
+ title: `Auto-respond to ${app} errors`,
171
+ description: `${app} has had ${count} errors recently. Set up automatic error handling.`,
172
+ confidence: Math.min(count / 10, 0.85),
173
+ category: 'error_response',
174
+ patternEvidence: [`${count} errors in ${app}`],
175
+ previewDefinition: {
176
+ nodes: [
177
+ { id: 'trigger-1', type: 'trigger.screen', label: `Detect ${app} error`, position: { x: 100, y: 200 }, config: { condition_type: 'text_present', text: 'error' } },
178
+ { id: 'action-1', type: 'action.agent_task', label: 'Research fix', position: { x: 400, y: 200 }, config: { task: `Research and suggest a fix for the error in ${app}` } },
179
+ { id: 'action-2', type: 'action.notification', label: 'Notify solution', position: { x: 700, y: 200 }, config: { message: '{{$node["action-1"].data.response}}' } },
180
+ ],
181
+ edges: [
182
+ { id: 'e-1', source: 'trigger-1', target: 'action-1' },
183
+ { id: 'e-2', source: 'action-1', target: 'action-2' },
184
+ ],
185
+ settings: { maxRetries: 2, retryDelayMs: 5000, timeoutMs: 120000, parallelism: 'sequential', onError: 'continue' },
186
+ },
187
+ });
188
+ }
189
+ }
190
+
191
+ return suggestions;
192
+ }
193
+
194
+ private detectScheduledPatterns(): WorkflowSuggestion[] {
195
+ const suggestions: WorkflowSuggestion[] = [];
196
+
197
+ // Look for events that happen at roughly the same time daily
198
+ const eventsByHour: Map<number, AwarenessPattern[]> = new Map();
199
+ for (const p of this.patterns) {
200
+ const hour = new Date(p.timestamp).getHours();
201
+ const existing = eventsByHour.get(hour) ?? [];
202
+ existing.push(p);
203
+ eventsByHour.set(hour, existing);
204
+ }
205
+
206
+ for (const [hour, events] of eventsByHour) {
207
+ // Group by type within the hour
208
+ const typeCount: Map<string, number> = new Map();
209
+ for (const e of events) {
210
+ typeCount.set(e.type, (typeCount.get(e.type) ?? 0) + 1);
211
+ }
212
+
213
+ for (const [type, count] of typeCount) {
214
+ if (count >= 3 && type === 'context_change') {
215
+ const apps = [...new Set(events.filter(e => e.type === type).map(e => String(e.data.appName ?? '')))].filter(Boolean);
216
+ if (apps.length > 0) {
217
+ suggestions.push({
218
+ id: `suggest-scheduled-${Date.now()}-${hour}`,
219
+ title: `Schedule ${hour}:00 routine`,
220
+ description: `You regularly use ${apps.slice(0, 3).join(', ')} around ${hour}:00. Automate this routine.`,
221
+ confidence: Math.min(count / 10, 0.7),
222
+ category: 'scheduled_task',
223
+ patternEvidence: [`${count} occurrences around ${hour}:00`],
224
+ previewDefinition: {
225
+ nodes: [
226
+ { id: 'trigger-1', type: 'trigger.cron', label: `Daily at ${hour}:00`, position: { x: 100, y: 200 }, config: { expression: `0 ${hour} * * *` } },
227
+ { id: 'action-1', type: 'action.notification', label: 'Start routine', position: { x: 400, y: 200 }, config: { message: `Time for your ${hour}:00 routine with ${apps[0]}` } },
228
+ ],
229
+ edges: [
230
+ { id: 'e-1', source: 'trigger-1', target: 'action-1' },
231
+ ],
232
+ settings: { maxRetries: 1, retryDelayMs: 5000, timeoutMs: 60000, parallelism: 'sequential', onError: 'stop' },
233
+ },
234
+ });
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ return suggestions;
241
+ }
242
+
243
+ private async llmAnalyzePatterns(): Promise<WorkflowSuggestion[]> {
244
+ // Summarize recent patterns for LLM analysis
245
+ const recentPatterns = this.patterns.slice(-100).map(p => ({
246
+ type: p.type,
247
+ app: p.data.appName ?? p.data.app ?? undefined,
248
+ time: new Date(p.timestamp).toLocaleTimeString(),
249
+ }));
250
+
251
+ const prompt = [
252
+ {
253
+ role: 'system' as const,
254
+ content: `You analyze user behavior patterns and suggest automation workflows. Respond with a JSON array of suggestions. Each suggestion should have: title (string), description (string), confidence (0-1), category (repetitive_action|error_response|scheduled_task|app_pattern). Only suggest if you see clear patterns. Return [] if no strong patterns.`,
255
+ },
256
+ {
257
+ role: 'user' as const,
258
+ content: `Here are recent user activity patterns:\n${JSON.stringify(recentPatterns, null, 2)}\n\nWhat workflow automations would you suggest?`,
259
+ },
260
+ ];
261
+
262
+ const response = await this.llmManager.chat(prompt, { temperature: 0.3, max_tokens: 2000 });
263
+ const text = response.content;
264
+
265
+ try {
266
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
267
+ if (!jsonMatch) return [];
268
+ const parsed = JSON.parse(jsonMatch[0]);
269
+ if (!Array.isArray(parsed)) return [];
270
+
271
+ return parsed.slice(0, 3).map((s: any, i: number) => ({
272
+ id: `suggest-llm-${Date.now()}-${i}`,
273
+ title: s.title ?? 'Suggested workflow',
274
+ description: s.description ?? '',
275
+ confidence: Math.min(s.confidence ?? 0.5, 0.9),
276
+ category: s.category ?? 'repetitive_action',
277
+ patternEvidence: ['Detected by AI analysis'],
278
+ previewDefinition: {
279
+ nodes: [
280
+ { id: 'trigger-1', type: 'trigger.manual', label: 'Manual Trigger', position: { x: 100, y: 200 }, config: {} },
281
+ ],
282
+ edges: [],
283
+ settings: { maxRetries: 3, retryDelayMs: 5000, timeoutMs: 300000, parallelism: 'parallel', onError: 'stop' },
284
+ },
285
+ }));
286
+ } catch {
287
+ return [];
288
+ }
289
+ }
290
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Workflow Engine — Service that orchestrates workflow execution
3
+ */
4
+
5
+ import type { Service, ServiceStatus } from '../daemon/services.ts';
6
+ import type { NodeRegistry, ExecutionContext, StepLogger, NodeInput } from './nodes/registry.ts';
7
+ import type { ToolRegistry } from '../actions/tools/registry.ts';
8
+ import type { WorkflowDefinition, WorkflowExecution, ExecutionStatus } from './types.ts';
9
+ import type { WorkflowEvent } from './events.ts';
10
+ import { VariableScope } from './variables.ts';
11
+ import { topologicalSort, getOutgoingEdges, executeNode } from './executor.ts';
12
+ import type { TemplateContext } from './template.ts';
13
+ import * as vault from '../vault/workflows.ts';
14
+
15
+ export class WorkflowEngine implements Service {
16
+ name = 'workflow-engine';
17
+ private _status: ServiceStatus = 'stopped';
18
+ private nodeRegistry: NodeRegistry;
19
+ private toolRegistry: ToolRegistry;
20
+ private llmManager: unknown;
21
+ private activeExecutions: Map<string, AbortController> = new Map();
22
+ private eventCallback: ((event: WorkflowEvent) => void) | null = null;
23
+
24
+ constructor(
25
+ nodeRegistry: NodeRegistry,
26
+ toolRegistry: ToolRegistry,
27
+ llmManager: unknown,
28
+ ) {
29
+ this.nodeRegistry = nodeRegistry;
30
+ this.toolRegistry = toolRegistry;
31
+ this.llmManager = llmManager;
32
+ }
33
+
34
+ async start(): Promise<void> {
35
+ this._status = 'running';
36
+ }
37
+
38
+ async stop(): Promise<void> {
39
+ // Cancel all active executions
40
+ for (const [id, controller] of this.activeExecutions) {
41
+ controller.abort();
42
+ }
43
+ this.activeExecutions.clear();
44
+ this._status = 'stopped';
45
+ }
46
+
47
+ status(): ServiceStatus {
48
+ return this._status;
49
+ }
50
+
51
+ setEventCallback(cb: (event: WorkflowEvent) => void): void {
52
+ this.eventCallback = cb;
53
+ }
54
+
55
+ getActiveCount(): number {
56
+ return this.activeExecutions.size;
57
+ }
58
+
59
+ /**
60
+ * Execute a workflow by ID.
61
+ */
62
+ async execute(
63
+ workflowId: string,
64
+ triggerType: string,
65
+ triggerData?: Record<string, unknown>,
66
+ ): Promise<WorkflowExecution> {
67
+ const workflow = vault.getWorkflow(workflowId);
68
+ if (!workflow) throw new Error(`Workflow '${workflowId}' not found`);
69
+ if (!workflow.enabled) throw new Error(`Workflow '${workflow.name}' is disabled`);
70
+
71
+ const version = vault.getLatestVersion(workflowId);
72
+ if (!version) throw new Error(`No versions found for workflow '${workflow.name}'`);
73
+
74
+ const definition = version.definition;
75
+
76
+ // Create execution record
77
+ const execution = vault.createExecution(workflowId, version.version, triggerType, triggerData);
78
+
79
+ this.emit({
80
+ type: 'execution_started',
81
+ workflowId,
82
+ executionId: execution.id,
83
+ data: { triggerType, triggerData: triggerData ?? {}, workflowName: workflow.name },
84
+ timestamp: Date.now(),
85
+ });
86
+
87
+ // Run in background (don't block the caller)
88
+ this.runExecution(execution.id, workflowId, definition, triggerType, triggerData ?? {}).catch(err => {
89
+ console.error(`[WorkflowEngine] Execution ${execution.id} crashed: ${err.message}`);
90
+ });
91
+
92
+ return execution;
93
+ }
94
+
95
+ /**
96
+ * Cancel a running execution.
97
+ */
98
+ async cancel(executionId: string): Promise<void> {
99
+ const controller = this.activeExecutions.get(executionId);
100
+ if (controller) {
101
+ controller.abort();
102
+ vault.updateExecution(executionId, { status: 'cancelled', completed_at: Date.now() });
103
+ this.activeExecutions.delete(executionId);
104
+
105
+ const exec = vault.getExecution(executionId);
106
+ this.emit({
107
+ type: 'execution_cancelled',
108
+ workflowId: exec?.workflow_id ?? '',
109
+ executionId,
110
+ data: {},
111
+ timestamp: Date.now(),
112
+ });
113
+ }
114
+ }
115
+
116
+ // ── Internal ──
117
+
118
+ private async runExecution(
119
+ executionId: string,
120
+ workflowId: string,
121
+ definition: WorkflowDefinition,
122
+ triggerType: string,
123
+ triggerData: Record<string, unknown>,
124
+ ): Promise<void> {
125
+ const abortController = new AbortController();
126
+ this.activeExecutions.set(executionId, abortController);
127
+
128
+ const startedAt = Date.now();
129
+ const variables = new VariableScope(workflowId);
130
+ const nodeMap = new Map(definition.nodes.map(n => [n.id, n]));
131
+ const nodeOutputs = new Map<string, Record<string, unknown>>();
132
+
133
+ // Build template context
134
+ const templateCtx: TemplateContext = {
135
+ variables: variables.toObject(),
136
+ nodeOutputs,
137
+ triggerData,
138
+ env: process.env as Record<string, string>,
139
+ };
140
+
141
+ try {
142
+ // Run graph execution using BFS with routing
143
+ const levels = topologicalSort(definition);
144
+
145
+ // Find trigger node(s) — they already fired, seed their output
146
+ const triggerNodes = definition.nodes.filter(n => n.type.startsWith('trigger.'));
147
+ for (const tn of triggerNodes) {
148
+ nodeOutputs.set(tn.id, { ...triggerData, timestamp: Date.now() });
149
+ // Also store by label for $node["label"] references
150
+ nodeOutputs.set(tn.label, { ...triggerData, timestamp: Date.now() });
151
+ }
152
+
153
+ // Track which nodes should be skipped (not on active route)
154
+ const activeNodes = new Set<string>(triggerNodes.map(n => n.id));
155
+
156
+ // Seed: all nodes directly connected from trigger nodes are active
157
+ for (const tn of triggerNodes) {
158
+ for (const targetId of getOutgoingEdges(definition, tn.id)) {
159
+ activeNodes.add(targetId);
160
+ }
161
+ }
162
+
163
+ for (const level of levels) {
164
+ if (abortController.signal.aborted) break;
165
+
166
+ // Filter to only active nodes at this level
167
+ const toExecute = level.filter(id => activeNodes.has(id) && !triggerNodes.some(t => t.id === id));
168
+ if (toExecute.length === 0) continue;
169
+
170
+ const settings = definition.settings;
171
+ const isParallel = settings.parallelism === 'parallel';
172
+
173
+ if (isParallel) {
174
+ await Promise.all(toExecute.map(nodeId => this.executeStep(
175
+ executionId, workflowId, nodeId, nodeMap, nodeOutputs, variables,
176
+ templateCtx, definition, abortController, activeNodes,
177
+ )));
178
+ } else {
179
+ for (const nodeId of toExecute) {
180
+ if (abortController.signal.aborted) break;
181
+ await this.executeStep(
182
+ executionId, workflowId, nodeId, nodeMap, nodeOutputs, variables,
183
+ templateCtx, definition, abortController, activeNodes,
184
+ );
185
+ }
186
+ }
187
+ }
188
+
189
+ // Complete
190
+ const duration = Date.now() - startedAt;
191
+ vault.updateExecution(executionId, {
192
+ status: 'completed',
193
+ completed_at: Date.now(),
194
+ duration_ms: duration,
195
+ variables: variables.toObject(),
196
+ });
197
+
198
+ this.emit({
199
+ type: 'execution_completed',
200
+ workflowId,
201
+ executionId,
202
+ data: { duration_ms: duration },
203
+ timestamp: Date.now(),
204
+ });
205
+ } catch (err) {
206
+ const errMsg = err instanceof Error ? err.message : String(err);
207
+ const duration = Date.now() - startedAt;
208
+
209
+ vault.updateExecution(executionId, {
210
+ status: 'failed',
211
+ error_message: errMsg,
212
+ completed_at: Date.now(),
213
+ duration_ms: duration,
214
+ variables: variables.toObject(),
215
+ });
216
+
217
+ this.emit({
218
+ type: 'execution_failed',
219
+ workflowId,
220
+ executionId,
221
+ data: { error: errMsg, duration_ms: duration },
222
+ timestamp: Date.now(),
223
+ });
224
+ } finally {
225
+ this.activeExecutions.delete(executionId);
226
+ }
227
+ }
228
+
229
+ private async executeStep(
230
+ executionId: string,
231
+ workflowId: string,
232
+ nodeId: string,
233
+ nodeMap: Map<string, import('./types.ts').WorkflowNode>,
234
+ nodeOutputs: Map<string, Record<string, unknown>>,
235
+ variables: VariableScope,
236
+ templateCtx: TemplateContext,
237
+ definition: WorkflowDefinition,
238
+ abortController: AbortController,
239
+ activeNodes: Set<string>,
240
+ ): Promise<void> {
241
+ const node = nodeMap.get(nodeId);
242
+ if (!node) return;
243
+
244
+ const stepResult = vault.createStepResult(executionId, nodeId, node.type);
245
+ vault.updateStepResult(stepResult.id, { status: 'running', started_at: Date.now() });
246
+
247
+ this.emit({
248
+ type: 'step_started',
249
+ workflowId,
250
+ executionId,
251
+ nodeId,
252
+ data: { nodeType: node.type, label: node.label },
253
+ timestamp: Date.now(),
254
+ });
255
+
256
+ // Collect input from incoming edges
257
+ const incomingEdges = definition.edges.filter(e => e.target === nodeId);
258
+ const inputData: Record<string, unknown> = {};
259
+ for (const edge of incomingEdges) {
260
+ const sourceOutput = nodeOutputs.get(edge.source);
261
+ if (sourceOutput) Object.assign(inputData, sourceOutput);
262
+ }
263
+
264
+ const input: NodeInput = {
265
+ data: inputData,
266
+ variables: variables.toObject(),
267
+ executionId,
268
+ };
269
+
270
+ const logger: StepLogger = {
271
+ info: (msg) => console.log(`[Workflow:${nodeId}] ${msg}`),
272
+ warn: (msg) => console.warn(`[Workflow:${nodeId}] ${msg}`),
273
+ error: (msg) => console.error(`[Workflow:${nodeId}] ${msg}`),
274
+ };
275
+
276
+ const ctx: ExecutionContext = {
277
+ executionId,
278
+ workflowId,
279
+ toolRegistry: this.toolRegistry,
280
+ llmManager: this.llmManager,
281
+ variables,
282
+ logger,
283
+ abortSignal: abortController.signal,
284
+ nodeRegistry: this.nodeRegistry,
285
+ broadcast: (type, data) => this.emit({
286
+ type: type as any,
287
+ workflowId,
288
+ executionId,
289
+ data,
290
+ timestamp: Date.now(),
291
+ }),
292
+ };
293
+
294
+ // Refresh template context
295
+ templateCtx.variables = variables.toObject();
296
+
297
+ try {
298
+ const startTime = Date.now();
299
+ const output = await executeNode(node, input, this.nodeRegistry, ctx, templateCtx, definition.settings);
300
+ const duration = Date.now() - startTime;
301
+
302
+ // Store output
303
+ nodeOutputs.set(nodeId, output.data);
304
+ nodeOutputs.set(node.label, output.data); // For $node["label"] references
305
+
306
+ vault.updateStepResult(stepResult.id, {
307
+ status: 'completed',
308
+ input_data: inputData,
309
+ output_data: output.data,
310
+ completed_at: Date.now(),
311
+ duration_ms: duration,
312
+ });
313
+
314
+ this.emit({
315
+ type: 'step_completed',
316
+ workflowId,
317
+ executionId,
318
+ nodeId,
319
+ data: { duration_ms: duration, output: output.data },
320
+ timestamp: Date.now(),
321
+ });
322
+
323
+ // Activate downstream nodes based on route
324
+ const targets = getOutgoingEdges(definition, nodeId, output.route);
325
+ for (const t of targets) {
326
+ activeNodes.add(t);
327
+ }
328
+ } catch (err) {
329
+ const errMsg = err instanceof Error ? err.message : String(err);
330
+
331
+ vault.updateStepResult(stepResult.id, {
332
+ status: 'failed',
333
+ input_data: inputData,
334
+ error_message: errMsg,
335
+ completed_at: Date.now(),
336
+ duration_ms: Date.now() - (stepResult.started_at ?? Date.now()),
337
+ });
338
+
339
+ this.emit({
340
+ type: 'step_failed',
341
+ workflowId,
342
+ executionId,
343
+ nodeId,
344
+ data: { error: errMsg, nodeType: node.type },
345
+ timestamp: Date.now(),
346
+ });
347
+
348
+ // Check fallback
349
+ if (node.fallbackNodeId && nodeMap.has(node.fallbackNodeId)) {
350
+ logger.info(`Falling back to node: ${node.fallbackNodeId}`);
351
+ activeNodes.add(node.fallbackNodeId);
352
+ return;
353
+ }
354
+
355
+ // If onError is 'stop', throw to halt execution
356
+ if (definition.settings.onError === 'stop') {
357
+ throw err;
358
+ }
359
+ // 'continue' — log and move on
360
+ }
361
+ }
362
+
363
+ private emit(event: WorkflowEvent): void {
364
+ this.eventCallback?.(event);
365
+ }
366
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Workflow Event Types — for real-time WebSocket broadcasting
3
+ */
4
+
5
+ export type WorkflowEventType =
6
+ | 'execution_started'
7
+ | 'execution_completed'
8
+ | 'execution_failed'
9
+ | 'execution_cancelled'
10
+ | 'step_started'
11
+ | 'step_completed'
12
+ | 'step_failed'
13
+ | 'variable_changed'
14
+ | 'workflow_enabled'
15
+ | 'workflow_disabled';
16
+
17
+ export type WorkflowEvent = {
18
+ type: WorkflowEventType;
19
+ workflowId: string;
20
+ executionId?: string;
21
+ nodeId?: string;
22
+ data: Record<string, unknown>;
23
+ timestamp: number;
24
+ };