@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,37 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const variableGetNode: NodeDefinition = {
4
+ type: 'logic.variable_get',
5
+ label: 'Get Variable',
6
+ description: 'Read a workflow variable and inject it into the data stream.',
7
+ category: 'logic',
8
+ icon: '📖',
9
+ color: '#f59e0b',
10
+ configSchema: {
11
+ key: {
12
+ type: 'string',
13
+ label: 'Key',
14
+ description: 'Variable name to read.',
15
+ required: true,
16
+ placeholder: 'my_variable',
17
+ },
18
+ },
19
+ inputs: ['default'],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ const key = String(config.key ?? '');
23
+ if (!key) throw new Error('key is required');
24
+
25
+ const value = ctx.variables.get(key);
26
+ ctx.logger.info(`Getting variable "${key}": ${JSON.stringify(value)?.slice(0, 80)}`);
27
+
28
+ return {
29
+ data: {
30
+ ...input.data,
31
+ [key]: value,
32
+ variable_key: key,
33
+ variable_value: value,
34
+ },
35
+ };
36
+ },
37
+ };
@@ -0,0 +1,59 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const variableSetNode: NodeDefinition = {
4
+ type: 'logic.variable_set',
5
+ label: 'Set Variable',
6
+ description: 'Set a workflow variable (in-memory or persistent across executions).',
7
+ category: 'logic',
8
+ icon: '📝',
9
+ color: '#f59e0b',
10
+ configSchema: {
11
+ key: {
12
+ type: 'string',
13
+ label: 'Key',
14
+ description: 'Variable name to set.',
15
+ required: true,
16
+ placeholder: 'my_variable',
17
+ },
18
+ value: {
19
+ type: 'template',
20
+ label: 'Value',
21
+ description: 'Value to assign. Supports template expressions.',
22
+ required: true,
23
+ placeholder: '{{data.result}}',
24
+ },
25
+ persistent: {
26
+ type: 'boolean',
27
+ label: 'Persistent',
28
+ description: 'If true, the variable persists across workflow executions.',
29
+ required: false,
30
+ default: false,
31
+ },
32
+ },
33
+ inputs: ['default'],
34
+ outputs: ['default'],
35
+ execute: async (input, config, ctx) => {
36
+ const key = String(config.key ?? '');
37
+ if (!key) throw new Error('key is required');
38
+
39
+ const value = config.value;
40
+ const persistent = config.persistent === true;
41
+
42
+ ctx.logger.info(`Setting variable "${key}" (persistent=${persistent})`);
43
+
44
+ if (persistent) {
45
+ ctx.variables.setPersistent(key, value);
46
+ } else {
47
+ ctx.variables.set(key, value);
48
+ }
49
+
50
+ return {
51
+ data: {
52
+ ...input.data,
53
+ variable_key: key,
54
+ variable_value: value,
55
+ persistent,
56
+ },
57
+ };
58
+ },
59
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Node Registry — mirrors ToolRegistry pattern
3
+ */
4
+
5
+ import type { NodeCategory, NodeConfigField } from '../types.ts';
6
+ import type { ToolRegistry } from '../../actions/tools/registry.ts';
7
+
8
+ // ── Node I/O ──
9
+
10
+ export type NodeInput = {
11
+ data: Record<string, unknown>;
12
+ variables: Record<string, unknown>;
13
+ executionId: string;
14
+ };
15
+
16
+ export type NodeOutput = {
17
+ data: Record<string, unknown>;
18
+ route?: string; // For conditional nodes: which handle to follow
19
+ };
20
+
21
+ // ── Execution Context ──
22
+
23
+ export type StepLogger = {
24
+ info(msg: string): void;
25
+ warn(msg: string): void;
26
+ error(msg: string): void;
27
+ };
28
+
29
+ export type ExecutionContext = {
30
+ executionId: string;
31
+ workflowId: string;
32
+ toolRegistry: ToolRegistry;
33
+ llmManager: unknown; // LLMManager — loosely typed to avoid circular deps
34
+ variables: VariableScopeInterface;
35
+ logger: StepLogger;
36
+ abortSignal: AbortSignal;
37
+ nodeRegistry?: NodeRegistry; // For self-heal: re-execute with corrected config
38
+ /** Broadcast a message to dashboard chat (used by send_message, notification nodes) */
39
+ broadcast?: (type: string, data: Record<string, unknown>) => void;
40
+ };
41
+
42
+ export interface VariableScopeInterface {
43
+ get(key: string): unknown;
44
+ set(key: string, value: unknown): void;
45
+ setPersistent(key: string, value: unknown): void;
46
+ toObject(): Record<string, unknown>;
47
+ }
48
+
49
+ // ── Node Definition ──
50
+
51
+ export type NodeDefinition = {
52
+ type: string; // e.g., 'trigger.cron'
53
+ label: string;
54
+ description: string;
55
+ category: NodeCategory;
56
+ icon: string;
57
+ color: string;
58
+ configSchema: Record<string, NodeConfigField>;
59
+ inputs: string[];
60
+ outputs: string[];
61
+ execute: (input: NodeInput, config: Record<string, unknown>, ctx: ExecutionContext) => Promise<NodeOutput>;
62
+ };
63
+
64
+ // ── Registry ──
65
+
66
+ export class NodeRegistry {
67
+ private nodes: Map<string, NodeDefinition> = new Map();
68
+
69
+ register(node: NodeDefinition): void {
70
+ if (this.nodes.has(node.type)) {
71
+ throw new Error(`Node type '${node.type}' is already registered`);
72
+ }
73
+ this.nodes.set(node.type, node);
74
+ }
75
+
76
+ get(type: string): NodeDefinition | undefined {
77
+ return this.nodes.get(type);
78
+ }
79
+
80
+ list(category?: NodeCategory): NodeDefinition[] {
81
+ const all = Array.from(this.nodes.values());
82
+ if (!category) return all;
83
+ return all.filter(n => n.category === category);
84
+ }
85
+
86
+ has(type: string): boolean {
87
+ return this.nodes.has(type);
88
+ }
89
+
90
+ getCategories(): NodeCategory[] {
91
+ const cats = new Set<NodeCategory>();
92
+ for (const n of this.nodes.values()) cats.add(n.category);
93
+ return Array.from(cats);
94
+ }
95
+
96
+ count(): number {
97
+ return this.nodes.size;
98
+ }
99
+ }
@@ -0,0 +1,99 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const aggregateTransform: NodeDefinition = {
4
+ type: 'transform.aggregate',
5
+ label: 'Aggregate',
6
+ description: 'Compute sum, average, count, min, or max over an array field.',
7
+ category: 'transform',
8
+ icon: '🔢',
9
+ color: '#10b981',
10
+ configSchema: {
11
+ operation: {
12
+ type: 'select',
13
+ label: 'Operation',
14
+ description: 'Aggregation operation to apply.',
15
+ required: true,
16
+ default: 'sum',
17
+ options: [
18
+ { label: 'Sum', value: 'sum' },
19
+ { label: 'Average', value: 'avg' },
20
+ { label: 'Count', value: 'count' },
21
+ { label: 'Min', value: 'min' },
22
+ { label: 'Max', value: 'max' },
23
+ ],
24
+ },
25
+ field: {
26
+ type: 'string',
27
+ label: 'Field',
28
+ description: 'Dot-path to the array field in data. For sum/avg/min/max, each element or sub-field is used as a number.',
29
+ required: true,
30
+ placeholder: 'items',
31
+ },
32
+ },
33
+ inputs: ['default'],
34
+ outputs: ['default'],
35
+ execute: async (input, config, ctx) => {
36
+ const operation = String(config.operation ?? 'sum');
37
+ const field = String(config.field ?? '');
38
+
39
+ if (!field) throw new Error('field is required');
40
+
41
+ // Resolve dot-path to get the array
42
+ const parts = field.split('.');
43
+ let arr: unknown = input.data as unknown;
44
+ for (const part of parts) {
45
+ if (arr && typeof arr === 'object') arr = (arr as Record<string, unknown>)[part];
46
+ else { arr = undefined; break; }
47
+ }
48
+
49
+ ctx.logger.info(`Aggregate: ${operation} on field "${field}"`);
50
+
51
+ if (operation === 'count') {
52
+ const count = Array.isArray(arr) ? arr.length : (arr !== undefined && arr !== null ? 1 : 0);
53
+ return { data: { ...input.data, aggregate_result: count, aggregate_operation: 'count' } };
54
+ }
55
+
56
+ if (!Array.isArray(arr)) {
57
+ throw new Error(`Field "${field}" must be an array for ${operation}`);
58
+ }
59
+
60
+ const numbers = arr.map(item => {
61
+ if (typeof item === 'number') return item;
62
+ if (typeof item === 'string') return parseFloat(item);
63
+ if (item !== null && typeof item === 'object') {
64
+ // If field path has sub-field like "items.value", the last segment after the array is handled here
65
+ // For simple arrays of numbers this branch is fine
66
+ const numVal = Object.values(item as Record<string, unknown>)[0];
67
+ return typeof numVal === 'number' ? numVal : parseFloat(String(numVal));
68
+ }
69
+ return NaN;
70
+ }).filter(n => !isNaN(n));
71
+
72
+ let result: number;
73
+ switch (operation) {
74
+ case 'sum':
75
+ result = numbers.reduce((a, b) => a + b, 0);
76
+ break;
77
+ case 'avg':
78
+ result = numbers.length > 0 ? numbers.reduce((a, b) => a + b, 0) / numbers.length : 0;
79
+ break;
80
+ case 'min':
81
+ result = numbers.length > 0 ? Math.min(...numbers) : 0;
82
+ break;
83
+ case 'max':
84
+ result = numbers.length > 0 ? Math.max(...numbers) : 0;
85
+ break;
86
+ default:
87
+ throw new Error(`Unknown operation: ${operation}`);
88
+ }
89
+
90
+ return {
91
+ data: {
92
+ ...input.data,
93
+ aggregate_result: result,
94
+ aggregate_operation: operation,
95
+ aggregate_count: numbers.length,
96
+ },
97
+ };
98
+ },
99
+ };
@@ -0,0 +1,70 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const csvParseTransform: NodeDefinition = {
4
+ type: 'transform.csv_parse',
5
+ label: 'CSV Parse',
6
+ description: 'Parse a CSV string into an array of row objects.',
7
+ category: 'transform',
8
+ icon: '📊',
9
+ color: '#10b981',
10
+ configSchema: {
11
+ input_field: {
12
+ type: 'string',
13
+ label: 'Input Field',
14
+ description: 'Dot-path to the field in data containing the CSV string.',
15
+ required: true,
16
+ placeholder: 'csv_content',
17
+ },
18
+ delimiter: {
19
+ type: 'string',
20
+ label: 'Delimiter',
21
+ description: 'Column delimiter character.',
22
+ required: false,
23
+ default: ',',
24
+ },
25
+ },
26
+ inputs: ['default'],
27
+ outputs: ['default'],
28
+ execute: async (input, config, ctx) => {
29
+ const inputField = String(config.input_field ?? '');
30
+ const delimiter = String(config.delimiter ?? ',');
31
+
32
+ if (!inputField) throw new Error('input_field is required');
33
+
34
+ // Resolve dot-path
35
+ const raw = inputField.split('.').reduce<unknown>((acc, key) => {
36
+ if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key];
37
+ return undefined;
38
+ }, input.data as unknown);
39
+
40
+ if (typeof raw !== 'string') {
41
+ throw new Error(`Field "${inputField}" must be a string, got ${typeof raw}`);
42
+ }
43
+
44
+ ctx.logger.info(`CSV parse: field "${inputField}", delimiter "${delimiter}"`);
45
+
46
+ const lines = raw.split('\n').map(l => l.trim()).filter(l => l.length > 0);
47
+ if (lines.length === 0) {
48
+ return { data: { ...input.data, csv_rows: [], csv_headers: [] } };
49
+ }
50
+
51
+ const headers = lines[0]!.split(delimiter).map(h => h.trim().replace(/^"|"$/g, ''));
52
+ const rows: Record<string, string>[] = [];
53
+
54
+ for (let i = 1; i < lines.length; i++) {
55
+ const values = lines[i]!.split(delimiter).map(v => v.trim().replace(/^"|"$/g, ''));
56
+ const row: Record<string, string> = {};
57
+ headers.forEach((h, idx) => { row[h] = values[idx] ?? ''; });
58
+ rows.push(row);
59
+ }
60
+
61
+ return {
62
+ data: {
63
+ ...input.data,
64
+ csv_rows: rows,
65
+ csv_headers: headers,
66
+ csv_count: rows.length,
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,63 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const jsonParseTransform: NodeDefinition = {
4
+ type: 'transform.json_parse',
5
+ label: 'JSON Parse',
6
+ description: 'Parse a JSON string field and store the result under a new key.',
7
+ category: 'transform',
8
+ icon: '{ }',
9
+ color: '#10b981',
10
+ configSchema: {
11
+ input_field: {
12
+ type: 'string',
13
+ label: 'Input Field',
14
+ description: 'Dot-path to the field in data containing the JSON string.',
15
+ required: true,
16
+ placeholder: 'body',
17
+ },
18
+ output_field: {
19
+ type: 'string',
20
+ label: 'Output Field',
21
+ description: 'Key to store the parsed result under in the output data.',
22
+ required: true,
23
+ placeholder: 'parsed',
24
+ },
25
+ },
26
+ inputs: ['default'],
27
+ outputs: ['default'],
28
+ execute: async (input, config, ctx) => {
29
+ const inputField = String(config.input_field ?? '');
30
+ const outputField = String(config.output_field ?? 'parsed');
31
+
32
+ if (!inputField) throw new Error('input_field is required');
33
+
34
+ // Resolve dot-path
35
+ const raw = inputField.split('.').reduce<unknown>((acc, key) => {
36
+ if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key];
37
+ return undefined;
38
+ }, input.data as unknown);
39
+
40
+ ctx.logger.info(`JSON parse: field "${inputField}" → "${outputField}"`);
41
+
42
+ let parsed: unknown;
43
+ if (typeof raw === 'string') {
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ } catch (err) {
47
+ throw new Error(`Failed to parse JSON in field "${inputField}": ${err instanceof Error ? err.message : String(err)}`);
48
+ }
49
+ } else if (raw !== undefined && raw !== null) {
50
+ // Already an object — pass through
51
+ parsed = raw;
52
+ } else {
53
+ throw new Error(`Field "${inputField}" is undefined or null`);
54
+ }
55
+
56
+ return {
57
+ data: {
58
+ ...input.data,
59
+ [outputField]: parsed,
60
+ },
61
+ };
62
+ },
63
+ };
@@ -0,0 +1,84 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+ import { safeEvaluateExpression } from '../../safe-eval.ts';
3
+
4
+ export const mapFilterTransform: NodeDefinition = {
5
+ type: 'transform.map_filter',
6
+ label: 'Map / Filter',
7
+ description: 'Map or filter an array using a JavaScript expression.',
8
+ category: 'transform',
9
+ icon: '🗂️',
10
+ color: '#10b981',
11
+ configSchema: {
12
+ items_field: {
13
+ type: 'string',
14
+ label: 'Items Field',
15
+ description: 'Dot-path to the array field in data to transform.',
16
+ required: true,
17
+ placeholder: 'items',
18
+ },
19
+ expression: {
20
+ type: 'template',
21
+ label: 'Expression',
22
+ description: 'JavaScript expression applied to each element. `item` is the current element, `index` is its position. For map: return transformed value. For filter: return truthy/falsy.',
23
+ required: true,
24
+ placeholder: 'item.value > 10',
25
+ },
26
+ mode: {
27
+ type: 'select',
28
+ label: 'Mode',
29
+ description: 'Whether to transform (map) or filter the array.',
30
+ required: true,
31
+ default: 'filter',
32
+ options: [
33
+ { label: 'Map', value: 'map' },
34
+ { label: 'Filter', value: 'filter' },
35
+ ],
36
+ },
37
+ },
38
+ inputs: ['default'],
39
+ outputs: ['default'],
40
+ execute: async (input, config, ctx) => {
41
+ const itemsField = String(config.items_field ?? '');
42
+ const expression = String(config.expression ?? '');
43
+ const mode = String(config.mode ?? 'filter');
44
+
45
+ if (!itemsField) throw new Error('items_field is required');
46
+ if (!expression) throw new Error('expression is required');
47
+
48
+ // Resolve dot-path
49
+ const parts = itemsField.split('.');
50
+ let arr: unknown = input.data as unknown;
51
+ for (const part of parts) {
52
+ if (arr && typeof arr === 'object') arr = (arr as Record<string, unknown>)[part];
53
+ else { arr = undefined; break; }
54
+ }
55
+
56
+ if (!Array.isArray(arr)) {
57
+ throw new Error(`Field "${itemsField}" must be an array, got ${typeof arr}`);
58
+ }
59
+
60
+ ctx.logger.info(`${mode} on field "${itemsField}" (${arr.length} items)`);
61
+
62
+ let result: unknown[];
63
+ if (mode === 'map') {
64
+ result = arr.map((item, index) => {
65
+ try { return safeEvaluateExpression(expression, { item, index, data: input.data }); }
66
+ catch (e) { ctx.logger.warn(`map expression error at index ${index}: ${e}`); return item; }
67
+ });
68
+ } else {
69
+ result = arr.filter((item, index) => {
70
+ try { return !!safeEvaluateExpression(expression, { item, index, data: input.data }); }
71
+ catch (e) { ctx.logger.warn(`filter expression error at index ${index}: ${e}`); return false; }
72
+ });
73
+ }
74
+
75
+ return {
76
+ data: {
77
+ ...input.data,
78
+ [itemsField.split('.').pop() ?? 'result']: result,
79
+ transform_count: result.length,
80
+ transform_mode: mode,
81
+ },
82
+ };
83
+ },
84
+ };
@@ -0,0 +1,89 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const regexMatchTransform: NodeDefinition = {
4
+ type: 'transform.regex_match',
5
+ label: 'Regex Match',
6
+ description: 'Apply a regular expression to a string field and capture matches.',
7
+ category: 'transform',
8
+ icon: '🔍',
9
+ color: '#10b981',
10
+ configSchema: {
11
+ input_field: {
12
+ type: 'string',
13
+ label: 'Input Field',
14
+ description: 'Dot-path to the field in data to run the regex against.',
15
+ required: true,
16
+ placeholder: 'body',
17
+ },
18
+ pattern: {
19
+ type: 'string',
20
+ label: 'Pattern',
21
+ description: 'Regular expression pattern (without slashes).',
22
+ required: true,
23
+ placeholder: '\\d+',
24
+ },
25
+ flags: {
26
+ type: 'string',
27
+ label: 'Flags',
28
+ description: 'Regex flags (e.g., "gi", "m"). Defaults to "g".',
29
+ required: false,
30
+ default: 'g',
31
+ },
32
+ },
33
+ inputs: ['default'],
34
+ outputs: ['default'],
35
+ execute: async (input, config, ctx) => {
36
+ const inputField = String(config.input_field ?? '');
37
+ const pattern = String(config.pattern ?? '');
38
+ const flags = String(config.flags ?? 'g');
39
+
40
+ if (!inputField) throw new Error('input_field is required');
41
+ if (!pattern) throw new Error('pattern is required');
42
+
43
+ // Resolve dot-path
44
+ const raw = inputField.split('.').reduce<unknown>((acc, key) => {
45
+ if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key];
46
+ return undefined;
47
+ }, input.data as unknown);
48
+
49
+ if (typeof raw !== 'string') {
50
+ throw new Error(`Field "${inputField}" must be a string, got ${typeof raw}`);
51
+ }
52
+
53
+ ctx.logger.info(`Regex match: /${pattern}/${flags} on field "${inputField}"`);
54
+
55
+ let regex: RegExp;
56
+ try {
57
+ regex = new RegExp(pattern, flags);
58
+ } catch (err) {
59
+ throw new Error(`Invalid regex pattern: ${err instanceof Error ? err.message : String(err)}`);
60
+ }
61
+
62
+ const matches: string[] = [];
63
+ const groups: Array<Record<string, string | undefined>> = [];
64
+
65
+ if (flags.includes('g')) {
66
+ let match: RegExpExecArray | null;
67
+ while ((match = regex.exec(raw)) !== null) {
68
+ matches.push(match[0]);
69
+ if (match.groups) groups.push(match.groups);
70
+ }
71
+ } else {
72
+ const match = regex.exec(raw);
73
+ if (match) {
74
+ matches.push(match[0]);
75
+ if (match.groups) groups.push(match.groups);
76
+ }
77
+ }
78
+
79
+ return {
80
+ data: {
81
+ ...input.data,
82
+ regex_matches: matches,
83
+ regex_groups: groups,
84
+ regex_count: matches.length,
85
+ regex_matched: matches.length > 0,
86
+ },
87
+ };
88
+ },
89
+ };
@@ -0,0 +1,33 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const calendarTrigger: NodeDefinition = {
4
+ type: 'trigger.calendar',
5
+ label: 'Calendar Trigger',
6
+ description: 'Fire a set number of minutes before a calendar event starts.',
7
+ category: 'trigger',
8
+ icon: '📅',
9
+ color: '#8b5cf6',
10
+ configSchema: {
11
+ minutes_before: {
12
+ type: 'number',
13
+ label: 'Minutes Before',
14
+ description: 'How many minutes before the event to fire.',
15
+ required: true,
16
+ default: 15,
17
+ },
18
+ },
19
+ inputs: [],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ const minutesBefore = typeof config.minutes_before === 'number' ? config.minutes_before : 15;
23
+ ctx.logger.info(`Calendar trigger fired — ${minutesBefore} minutes before event`);
24
+ return {
25
+ data: {
26
+ triggerType: 'calendar',
27
+ minutes_before: minutesBefore,
28
+ firedAt: Date.now(),
29
+ ...input.data,
30
+ },
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,32 @@
1
+ import type { NodeDefinition } from '../registry.ts';
2
+
3
+ export const clipboardTrigger: NodeDefinition = {
4
+ type: 'trigger.clipboard',
5
+ label: 'Clipboard Trigger',
6
+ description: 'Fire when clipboard content changes, optionally filtering by regex pattern.',
7
+ category: 'trigger',
8
+ icon: '📋',
9
+ color: '#8b5cf6',
10
+ configSchema: {
11
+ pattern: {
12
+ type: 'string',
13
+ label: 'Pattern Filter',
14
+ description: 'Optional regex pattern. Only fire if clipboard text matches.',
15
+ required: false,
16
+ placeholder: 'https?://',
17
+ },
18
+ },
19
+ inputs: [],
20
+ outputs: ['default'],
21
+ execute: async (input, config, ctx) => {
22
+ ctx.logger.info('Clipboard trigger fired');
23
+ return {
24
+ data: {
25
+ triggerType: 'clipboard',
26
+ pattern: config.pattern ?? null,
27
+ firedAt: Date.now(),
28
+ ...input.data,
29
+ },
30
+ };
31
+ },
32
+ };