@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,119 @@
1
+ /**
2
+ * Built-in Node Library — Phase 3
3
+ *
4
+ * Imports all node definitions and registers them into a NodeRegistry instance.
5
+ * Call registerBuiltinNodes(registry) once at startup.
6
+ */
7
+
8
+ import type { NodeRegistry } from './registry.ts';
9
+
10
+ // ── Triggers ──────────────────────────────────────────────────────────────────
11
+ import { cronTrigger } from './triggers/cron.ts';
12
+ import { webhookTrigger } from './triggers/webhook.ts';
13
+ import { emailTrigger } from './triggers/email.ts';
14
+ import { fileChangeTrigger } from './triggers/file-change.ts';
15
+ import { screenEventTrigger } from './triggers/screen-event.ts';
16
+ import { manualTrigger } from './triggers/manual.ts';
17
+ import { gitTrigger } from './triggers/git.ts';
18
+ import { clipboardTrigger } from './triggers/clipboard.ts';
19
+ import { processTrigger } from './triggers/process.ts';
20
+ import { calendarTrigger } from './triggers/calendar.ts';
21
+ import { pollTrigger } from './triggers/poll.ts';
22
+
23
+ // ── Actions ───────────────────────────────────────────────────────────────────
24
+ import { sendMessageAction } from './actions/send-message.ts';
25
+ import { runToolAction } from './actions/run-tool.ts';
26
+ import { agentTaskAction } from './actions/agent-task.ts';
27
+ import { httpRequestAction } from './actions/http-request.ts';
28
+ import { fileWriteAction } from './actions/file-write.ts';
29
+ import { notificationAction } from './actions/notification.ts';
30
+ import { gmailAction } from './actions/gmail.ts';
31
+ import { calendarActionNode } from './actions/calendar-action.ts';
32
+ import { telegramAction } from './actions/telegram.ts';
33
+ import { discordAction } from './actions/discord.ts';
34
+ import { shellCommandAction } from './actions/shell-command.ts';
35
+ import { codeExecutionAction } from './actions/code-execution.ts';
36
+
37
+ // ── Logic ─────────────────────────────────────────────────────────────────────
38
+ import { ifElseNode } from './logic/if-else.ts';
39
+ import { switchNode } from './logic/switch.ts';
40
+ import { loopNode } from './logic/loop.ts';
41
+ import { delayNode } from './logic/delay.ts';
42
+ import { mergeNode } from './logic/merge.ts';
43
+ import { raceNode } from './logic/race.ts';
44
+ import { variableSetNode } from './logic/variable-set.ts';
45
+ import { variableGetNode } from './logic/variable-get.ts';
46
+ import { templateRenderNode } from './logic/template-render.ts';
47
+
48
+ // ── Transform ─────────────────────────────────────────────────────────────────
49
+ import { jsonParseTransform } from './transform/json-parse.ts';
50
+ import { csvParseTransform } from './transform/csv-parse.ts';
51
+ import { regexMatchTransform } from './transform/regex-match.ts';
52
+ import { aggregateTransform } from './transform/aggregate.ts';
53
+ import { mapFilterTransform } from './transform/map-filter.ts';
54
+
55
+ // ── Error ─────────────────────────────────────────────────────────────────────
56
+ import { errorHandlerNode } from './error/error-handler.ts';
57
+ import { retryNode } from './error/retry.ts';
58
+ import { fallbackNode } from './error/fallback.ts';
59
+
60
+ /**
61
+ * Register all built-in nodes into the provided NodeRegistry.
62
+ * Throws if any node type is already registered (duplicate detection).
63
+ */
64
+ export function registerBuiltinNodes(registry: NodeRegistry): void {
65
+ // Triggers
66
+ registry.register(cronTrigger);
67
+ registry.register(webhookTrigger);
68
+ registry.register(emailTrigger);
69
+ registry.register(fileChangeTrigger);
70
+ registry.register(screenEventTrigger);
71
+ registry.register(manualTrigger);
72
+ registry.register(gitTrigger);
73
+ registry.register(clipboardTrigger);
74
+ registry.register(processTrigger);
75
+ registry.register(calendarTrigger);
76
+ registry.register(pollTrigger);
77
+
78
+ // Actions
79
+ registry.register(sendMessageAction);
80
+ registry.register(runToolAction);
81
+ registry.register(agentTaskAction);
82
+ registry.register(httpRequestAction);
83
+ registry.register(fileWriteAction);
84
+ registry.register(notificationAction);
85
+ registry.register(gmailAction);
86
+ registry.register(calendarActionNode);
87
+ registry.register(telegramAction);
88
+ registry.register(discordAction);
89
+ registry.register(shellCommandAction);
90
+ registry.register(codeExecutionAction);
91
+
92
+ // Logic
93
+ registry.register(ifElseNode);
94
+ registry.register(switchNode);
95
+ registry.register(loopNode);
96
+ registry.register(delayNode);
97
+ registry.register(mergeNode);
98
+ registry.register(raceNode);
99
+ registry.register(variableSetNode);
100
+ registry.register(variableGetNode);
101
+ registry.register(templateRenderNode);
102
+
103
+ // Transform
104
+ registry.register(jsonParseTransform);
105
+ registry.register(csvParseTransform);
106
+ registry.register(regexMatchTransform);
107
+ registry.register(aggregateTransform);
108
+ registry.register(mapFilterTransform);
109
+
110
+ // Error
111
+ registry.register(errorHandlerNode);
112
+ registry.register(retryNode);
113
+ registry.register(fallbackNode);
114
+ }
115
+
116
+ /**
117
+ * Convenience: total number of built-in nodes across all categories.
118
+ */
119
+ export const BUILTIN_NODE_COUNT = 40;
@@ -0,0 +1,37 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const errorHandlerNode: NodeDefinition = {
4
+ type: 'error.error_handler',
5
+ label: 'Error Handler',
6
+ description: 'Catch errors from upstream nodes and continue with a custom message.',
7
+ category: 'error',
8
+ icon: '🛡️',
9
+ color: '#ef4444',
10
+ configSchema: {
11
+ message: {
12
+ type: 'template',
13
+ label: 'Message',
14
+ description: 'Custom error message to include in output. Supports template expressions.',
15
+ required: false,
16
+ placeholder: 'An error occurred: {{data.error}}',
17
+ },
18
+ },
19
+ inputs: ['default', 'error'],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ const message = config.message ? String(config.message) : 'An error was handled by the error handler node.';
23
+ ctx.logger.info(`Error handler triggered: ${message.slice(0, 120)}`);
24
+
25
+ const errorData = input.data['_error'] ?? input.data['error'] ?? null;
26
+
27
+ return {
28
+ data: {
29
+ ...input.data,
30
+ error_handled: true,
31
+ handler_message: message,
32
+ original_error: errorData,
33
+ handledAt: Date.now(),
34
+ },
35
+ };
36
+ },
37
+ };
@@ -0,0 +1,47 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const fallbackNode: NodeDefinition = {
4
+ type: 'error.fallback',
5
+ label: 'Fallback',
6
+ description: 'Provide a static fallback value when an upstream node fails.',
7
+ category: 'error',
8
+ icon: '🪂',
9
+ color: '#ef4444',
10
+ configSchema: {
11
+ fallback_value: {
12
+ type: 'json',
13
+ label: 'Fallback Value',
14
+ description: 'JSON value to merge into the data output when this node is reached via an error path.',
15
+ required: true,
16
+ default: { fallback: true },
17
+ },
18
+ },
19
+ inputs: ['default'],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ ctx.logger.info('Fallback node triggered — injecting fallback value');
23
+
24
+ let fallbackValue: unknown = config.fallback_value;
25
+ if (typeof fallbackValue === 'string') {
26
+ try {
27
+ fallbackValue = JSON.parse(fallbackValue);
28
+ } catch {
29
+ // keep as string
30
+ }
31
+ }
32
+
33
+ const fallbackData: Record<string, unknown> =
34
+ fallbackValue !== null && typeof fallbackValue === 'object' && !Array.isArray(fallbackValue)
35
+ ? (fallbackValue as Record<string, unknown>)
36
+ : { fallback_value: fallbackValue };
37
+
38
+ return {
39
+ data: {
40
+ ...input.data,
41
+ ...fallbackData,
42
+ _fallback_used: true,
43
+ fallbackAt: Date.now(),
44
+ },
45
+ };
46
+ },
47
+ };
@@ -0,0 +1,82 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const retryNode: NodeDefinition = {
4
+ type: 'error.retry',
5
+ label: 'Retry',
6
+ description: 'Automatically retry a failed upstream node with configurable backoff.',
7
+ category: 'error',
8
+ icon: '🔁',
9
+ color: '#ef4444',
10
+ configSchema: {
11
+ max_retries: {
12
+ type: 'number',
13
+ label: 'Max Retries',
14
+ description: 'Maximum number of retry attempts.',
15
+ required: true,
16
+ default: 3,
17
+ },
18
+ delay_ms: {
19
+ type: 'number',
20
+ label: 'Initial Delay (ms)',
21
+ description: 'Delay before the first retry. Subsequent delays increase with backoff.',
22
+ required: true,
23
+ default: 1000,
24
+ },
25
+ backoff: {
26
+ type: 'select',
27
+ label: 'Backoff Strategy',
28
+ description: 'How the delay grows between retry attempts.',
29
+ required: true,
30
+ default: 'exponential',
31
+ options: [
32
+ { label: 'Fixed', value: 'fixed' },
33
+ { label: 'Exponential', value: 'exponential' },
34
+ ],
35
+ },
36
+ },
37
+ inputs: ['default'],
38
+ outputs: ['default', 'failed'],
39
+ execute: async (input, config, ctx) => {
40
+ const maxRetries = typeof config.max_retries === 'number' ? config.max_retries : 3;
41
+ const delayMs = typeof config.delay_ms === 'number' ? config.delay_ms : 1000;
42
+ const backoff = String(config.backoff ?? 'exponential');
43
+
44
+ // The retry logic is orchestrated by the workflow executor using the node's RetryPolicy.
45
+ // This execute function is called on each attempt — it passes data through and records attempt info.
46
+ const currentAttempt = typeof input.data['_retry_attempt'] === 'number'
47
+ ? (input.data['_retry_attempt'] as number)
48
+ : 0;
49
+
50
+ ctx.logger.info(`Retry node: attempt ${currentAttempt + 1}/${maxRetries + 1}`);
51
+
52
+ const hasError = Boolean(input.data['_error'] || input.data['error']);
53
+
54
+ if (hasError && currentAttempt >= maxRetries) {
55
+ ctx.logger.warn(`Retry exhausted after ${maxRetries} attempts`);
56
+ return {
57
+ data: {
58
+ ...input.data,
59
+ retry_exhausted: true,
60
+ retry_attempts: currentAttempt,
61
+ max_retries: maxRetries,
62
+ },
63
+ route: 'failed',
64
+ };
65
+ }
66
+
67
+ const nextDelay = backoff === 'exponential'
68
+ ? delayMs * Math.pow(2, currentAttempt)
69
+ : delayMs;
70
+
71
+ return {
72
+ data: {
73
+ ...input.data,
74
+ _retry_attempt: currentAttempt + 1,
75
+ retry_delay_ms: nextDelay,
76
+ max_retries: maxRetries,
77
+ backoff,
78
+ },
79
+ route: 'default',
80
+ };
81
+ },
82
+ };
@@ -0,0 +1,42 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const delayNode: NodeDefinition = {
4
+ type: 'logic.delay',
5
+ label: 'Delay',
6
+ description: 'Pause workflow execution for a specified duration.',
7
+ category: 'logic',
8
+ icon: '⏳',
9
+ color: '#f59e0b',
10
+ configSchema: {
11
+ delay_ms: {
12
+ type: 'number',
13
+ label: 'Delay (ms)',
14
+ description: 'Number of milliseconds to wait before continuing.',
15
+ required: true,
16
+ default: 1000,
17
+ },
18
+ },
19
+ inputs: ['default'],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ const delayMs = typeof config.delay_ms === 'number' ? Math.max(0, config.delay_ms) : 1000;
23
+ ctx.logger.info(`Delaying for ${delayMs}ms`);
24
+
25
+ await new Promise<void>((resolve, reject) => {
26
+ if (ctx.abortSignal.aborted) { reject(new Error('Delay aborted')); return; }
27
+ const onAbort = () => { clearTimeout(timer); reject(new Error('Delay aborted')); };
28
+ const timer = setTimeout(() => {
29
+ ctx.abortSignal.removeEventListener('abort', onAbort);
30
+ resolve();
31
+ }, delayMs);
32
+ ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
33
+ });
34
+
35
+ return {
36
+ data: {
37
+ ...input.data,
38
+ delayed_ms: delayMs,
39
+ },
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,41 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+ import { safeEvaluateExpression } from '../../safe-eval.ts';
3
+
4
+ export const ifElseNode: NodeDefinition = {
5
+ type: 'logic.if_else',
6
+ label: 'If / Else',
7
+ description: 'Branch the workflow based on a JavaScript boolean expression.',
8
+ category: 'logic',
9
+ icon: '🔀',
10
+ color: '#f59e0b',
11
+ configSchema: {
12
+ condition: {
13
+ type: 'template',
14
+ label: 'Condition',
15
+ description: 'A JavaScript expression that evaluates to true or false. Has access to `data` (input data object).',
16
+ required: true,
17
+ placeholder: 'data.status === "ok" && data.count > 0',
18
+ },
19
+ },
20
+ inputs: ['default'],
21
+ outputs: ['true', 'false'],
22
+ execute: async (input, config, ctx) => {
23
+ const condition = String(config.condition ?? 'false');
24
+ ctx.logger.info(`Evaluating condition: ${condition.slice(0, 120)}`);
25
+
26
+ let result = false;
27
+ try {
28
+ result = !!safeEvaluateExpression(condition, { data: input.data, variables: input.variables });
29
+ } catch (err) {
30
+ ctx.logger.warn(`Condition evaluation error: ${err instanceof Error ? err.message : String(err)}`);
31
+ result = false;
32
+ }
33
+
34
+ ctx.logger.info(`Condition result: ${result}`);
35
+
36
+ return {
37
+ data: { ...input.data, condition_result: result },
38
+ route: result ? 'true' : 'false',
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,90 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const loopNode: NodeDefinition = {
4
+ type: 'logic.loop',
5
+ label: 'Loop',
6
+ description: 'Iterate over an array, executing downstream "item" nodes for each element, then emitting "done".',
7
+ category: 'logic',
8
+ icon: '🔁',
9
+ color: '#f59e0b',
10
+ configSchema: {
11
+ items_path: {
12
+ type: 'template',
13
+ label: 'Items Path / Expression',
14
+ description: 'A template expression that resolves to an array, e.g. "{{data.results}}".',
15
+ required: true,
16
+ placeholder: '{{data.items}}',
17
+ },
18
+ max_iterations: {
19
+ type: 'number',
20
+ label: 'Max Iterations',
21
+ description: 'Safety cap on the number of iterations.',
22
+ required: false,
23
+ default: 100,
24
+ },
25
+ },
26
+ inputs: ['default'],
27
+ outputs: ['item', 'done'],
28
+ execute: async (input, config, ctx) => {
29
+ const maxIterations = typeof config.max_iterations === 'number' ? config.max_iterations : 100;
30
+
31
+ // Resolve items from config (template engine resolves upstream)
32
+ let items: unknown[] = [];
33
+ const rawItems = config.items_path;
34
+ if (Array.isArray(rawItems)) {
35
+ items = rawItems;
36
+ } else if (typeof rawItems === 'string') {
37
+ // Try dot-path into input.data
38
+ const resolved = rawItems.split('.').reduce<unknown>((acc, key) => {
39
+ if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key];
40
+ return undefined;
41
+ }, input.data as unknown);
42
+ if (Array.isArray(resolved)) items = resolved;
43
+ }
44
+
45
+ const capped = items.slice(0, maxIterations);
46
+ if (items.length > maxIterations) {
47
+ ctx.logger.warn(`Loop capped at ${maxIterations} iterations (total items: ${items.length})`);
48
+ }
49
+
50
+ if (capped.length === 0) {
51
+ ctx.logger.info('Loop: no items to iterate');
52
+ return {
53
+ data: {
54
+ ...input.data,
55
+ loop_items: [],
56
+ loop_results: [],
57
+ loop_total: 0,
58
+ },
59
+ route: 'done',
60
+ };
61
+ }
62
+
63
+ ctx.logger.info(`Loop: iterating over ${capped.length} items`);
64
+
65
+ // Execute downstream "item" path for each element by collecting results.
66
+ // Since individual node execute() can't drive the graph executor,
67
+ // we emit each item's data with index metadata. The executor's routing
68
+ // sends this to the "item" output branch.
69
+ // For true per-item subgraph execution, we'd need executor-level loop support.
70
+ // Current approach: output all items with metadata so downstream nodes can process.
71
+ const results: unknown[] = [];
72
+ for (let i = 0; i < capped.length; i++) {
73
+ if (ctx.abortSignal.aborted) break;
74
+ const item = capped[i];
75
+ results.push(item);
76
+ }
77
+
78
+ return {
79
+ data: {
80
+ ...input.data,
81
+ loop_items: capped,
82
+ loop_results: results,
83
+ loop_total: capped.length,
84
+ loop_index: 0,
85
+ loop_current: capped[0],
86
+ },
87
+ route: 'item',
88
+ };
89
+ },
90
+ };
@@ -0,0 +1,38 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const mergeNode: NodeDefinition = {
4
+ type: 'logic.merge',
5
+ label: 'Merge',
6
+ description: 'Wait for two input branches and merge their data into one output.',
7
+ category: 'logic',
8
+ icon: '🔗',
9
+ color: '#f59e0b',
10
+ configSchema: {},
11
+ inputs: ['input_1', 'input_2'],
12
+ outputs: ['default'],
13
+ execute: async (input, _config, ctx) => {
14
+ ctx.logger.info('Merging inputs');
15
+
16
+ // The executor collects data from all incoming edges via Object.assign.
17
+ // We deep-merge by ensuring nested objects don't clobber each other.
18
+ const merged: Record<string, unknown> = {};
19
+ for (const [key, value] of Object.entries(input.data)) {
20
+ if (
21
+ key in merged &&
22
+ merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key]) &&
23
+ value && typeof value === 'object' && !Array.isArray(value)
24
+ ) {
25
+ merged[key] = { ...(merged[key] as Record<string, unknown>), ...(value as Record<string, unknown>) };
26
+ } else {
27
+ merged[key] = value;
28
+ }
29
+ }
30
+
31
+ merged.merged_at = Date.now();
32
+ merged.merge_source_count = Object.keys(input.data).length;
33
+
34
+ return {
35
+ data: merged,
36
+ };
37
+ },
38
+ };
@@ -0,0 +1,40 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const raceNode: NodeDefinition = {
4
+ type: 'logic.race',
5
+ label: 'Race',
6
+ description: 'Pass through whichever input branch arrives first; tag the winner with timing metadata.',
7
+ category: 'logic',
8
+ icon: '🏁',
9
+ color: '#f59e0b',
10
+ configSchema: {
11
+ timeout_ms: {
12
+ type: 'number',
13
+ label: 'Timeout (ms)',
14
+ description: 'If no branch completes within this time, the node fails.',
15
+ required: true,
16
+ default: 30000,
17
+ },
18
+ },
19
+ inputs: ['default'],
20
+ outputs: ['winner'],
21
+ execute: async (input, config, ctx) => {
22
+ const timeoutMs = typeof config.timeout_ms === 'number' ? config.timeout_ms : 30000;
23
+ const arrivedAt = Date.now();
24
+
25
+ ctx.logger.info(`Race node: input arrived (timeout was ${timeoutMs}ms)`);
26
+
27
+ // In graph execution, all inputs to this node are collected by the executor.
28
+ // The race semantics mean the first completed branch's data is what we receive.
29
+ // We tag the output with timing metadata.
30
+ return {
31
+ data: {
32
+ ...input.data,
33
+ race_winner: true,
34
+ race_arrived_at: arrivedAt,
35
+ race_timeout_ms: timeoutMs,
36
+ },
37
+ route: 'winner',
38
+ };
39
+ },
40
+ };
@@ -0,0 +1,59 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+ import { safeEvaluateExpression } from '../../safe-eval.ts';
3
+
4
+ export const switchNode: NodeDefinition = {
5
+ type: 'logic.switch',
6
+ label: 'Switch',
7
+ description: 'Route to one of multiple cases based on an expression value.',
8
+ category: 'logic',
9
+ icon: '🔃',
10
+ color: '#f59e0b',
11
+ configSchema: {
12
+ expression: {
13
+ type: 'template',
14
+ label: 'Expression',
15
+ description: 'JavaScript expression whose value is matched against cases. Has access to `data`.',
16
+ required: true,
17
+ placeholder: 'data.status',
18
+ },
19
+ cases: {
20
+ type: 'json',
21
+ label: 'Cases',
22
+ description: 'JSON array of string values to match, e.g. ["pending", "active", "done"]. Matched in order against case_0, case_1, ... case_N.',
23
+ required: true,
24
+ default: ['case_a', 'case_b', 'case_c'],
25
+ },
26
+ },
27
+ inputs: ['default'],
28
+ outputs: ['case_0', 'case_1', 'case_2', 'default'],
29
+ execute: async (input, config, ctx) => {
30
+ const expression = String(config.expression ?? '');
31
+
32
+ let exprValue: unknown;
33
+ try {
34
+ exprValue = safeEvaluateExpression(expression, { data: input.data, variables: input.variables });
35
+ } catch (err) {
36
+ ctx.logger.warn(`Switch expression evaluation error: ${err instanceof Error ? err.message : String(err)}`);
37
+ exprValue = undefined;
38
+ }
39
+
40
+ // Parse cases
41
+ let cases: unknown[] = [];
42
+ if (Array.isArray(config.cases)) {
43
+ cases = config.cases;
44
+ } else if (typeof config.cases === 'string') {
45
+ try { cases = JSON.parse(config.cases); } catch { cases = []; }
46
+ }
47
+
48
+ // Match against any number of cases (not limited to 3)
49
+ const matchIndex = cases.findIndex(c => String(c) === String(exprValue));
50
+ const route = matchIndex >= 0 ? `case_${matchIndex}` : 'default';
51
+
52
+ ctx.logger.info(`Switch: expression="${exprValue}", matched case ${matchIndex}, route="${route}"`);
53
+
54
+ return {
55
+ data: { ...input.data, switch_value: exprValue, switch_route: route },
56
+ route,
57
+ };
58
+ },
59
+ };
@@ -0,0 +1,53 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+ import { resolveTemplateString } from '../../template.ts';
3
+
4
+ export const templateRenderNode: NodeDefinition = {
5
+ type: 'logic.template_render',
6
+ label: 'Template Render',
7
+ description: 'Render a template string against current data and store the result.',
8
+ category: 'logic',
9
+ icon: '📄',
10
+ color: '#f59e0b',
11
+ configSchema: {
12
+ template: {
13
+ type: 'template',
14
+ label: 'Template',
15
+ description: 'Template string with {{...}} expressions to resolve.',
16
+ required: true,
17
+ placeholder: 'Hello {{data.name}}, you have {{data.count}} items.',
18
+ },
19
+ },
20
+ inputs: ['default'],
21
+ outputs: ['default'],
22
+ execute: async (input, config, ctx) => {
23
+ const template = String(config.template ?? '');
24
+ ctx.logger.info(`Rendering template (${template.length} chars)`);
25
+
26
+ // Build nodeOutputs from input data so $node["..."] references work
27
+ const nodeOutputs = new Map<string, Record<string, unknown>>();
28
+ // Populate with input.data entries that look like node outputs
29
+ for (const [key, value] of Object.entries(input.data)) {
30
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
31
+ nodeOutputs.set(key, value as Record<string, unknown>);
32
+ }
33
+ }
34
+
35
+ const templateCtx = {
36
+ variables: input.variables,
37
+ nodeOutputs,
38
+ triggerData: (input.data['$trigger'] as Record<string, unknown>) ?? input.data,
39
+ env: Object.fromEntries(
40
+ Object.entries(process.env).filter(([, v]) => v !== undefined) as [string, string][]
41
+ ),
42
+ };
43
+
44
+ const rendered = resolveTemplateString(template, templateCtx);
45
+
46
+ return {
47
+ data: {
48
+ ...input.data,
49
+ rendered_output: rendered,
50
+ },
51
+ };
52
+ },
53
+ };