@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,171 @@
1
+ import type { ContentBlock } from '../../llm/provider.ts';
2
+
3
+ export type ToolParameter = {
4
+ type: string;
5
+ description: string;
6
+ required: boolean;
7
+ };
8
+
9
+ export type ToolResult = {
10
+ content: ContentBlock[];
11
+ };
12
+
13
+ export function isToolResult(v: unknown): v is ToolResult {
14
+ return v !== null && typeof v === 'object' && 'content' in (v as object) && Array.isArray((v as ToolResult).content);
15
+ }
16
+
17
+ export type ToolDefinition = {
18
+ name: string;
19
+ description: string;
20
+ category: string;
21
+ parameters: Record<string, ToolParameter>;
22
+ execute: (params: Record<string, unknown>) => Promise<unknown>;
23
+ };
24
+
25
+ export class ToolRegistry {
26
+ private tools: Map<string, ToolDefinition> = new Map();
27
+
28
+ register(tool: ToolDefinition): void {
29
+ if (this.tools.has(tool.name)) {
30
+ throw new Error(`Tool '${tool.name}' is already registered`);
31
+ }
32
+
33
+ this.validateToolDefinition(tool);
34
+ this.tools.set(tool.name, tool);
35
+ }
36
+
37
+ get(name: string): ToolDefinition | undefined {
38
+ return this.tools.get(name);
39
+ }
40
+
41
+ list(category?: string): ToolDefinition[] {
42
+ const allTools = Array.from(this.tools.values());
43
+
44
+ if (!category) {
45
+ return allTools;
46
+ }
47
+
48
+ return allTools.filter(tool => tool.category === category);
49
+ }
50
+
51
+ has(name: string): boolean {
52
+ return this.tools.has(name);
53
+ }
54
+
55
+ async execute(name: string, params: Record<string, unknown>): Promise<unknown> {
56
+ const tool = this.tools.get(name);
57
+
58
+ if (!tool) {
59
+ throw new Error(`Tool '${name}' not found in registry`);
60
+ }
61
+
62
+ this.validateParameters(tool, params);
63
+
64
+ try {
65
+ return await tool.execute(params);
66
+ } catch (error) {
67
+ throw new Error(
68
+ `Tool '${name}' execution failed: ${error instanceof Error ? error.message : String(error)}`
69
+ );
70
+ }
71
+ }
72
+
73
+ unregister(name: string): boolean {
74
+ return this.tools.delete(name);
75
+ }
76
+
77
+ clear(): void {
78
+ this.tools.clear();
79
+ }
80
+
81
+ getCategories(): string[] {
82
+ const categories = new Set<string>();
83
+
84
+ for (const tool of this.tools.values()) {
85
+ categories.add(tool.category);
86
+ }
87
+
88
+ return Array.from(categories).sort();
89
+ }
90
+
91
+ count(): number {
92
+ return this.tools.size;
93
+ }
94
+
95
+ private validateToolDefinition(tool: ToolDefinition): void {
96
+ if (!tool.name || typeof tool.name !== 'string') {
97
+ throw new Error('Tool must have a valid name');
98
+ }
99
+
100
+ if (!tool.description || typeof tool.description !== 'string') {
101
+ throw new Error(`Tool '${tool.name}' must have a description`);
102
+ }
103
+
104
+ if (!tool.category || typeof tool.category !== 'string') {
105
+ throw new Error(`Tool '${tool.name}' must have a category`);
106
+ }
107
+
108
+ if (typeof tool.execute !== 'function') {
109
+ throw new Error(`Tool '${tool.name}' must have an execute function`);
110
+ }
111
+
112
+ if (typeof tool.parameters !== 'object' || tool.parameters === null) {
113
+ throw new Error(`Tool '${tool.name}' must have a parameters object`);
114
+ }
115
+
116
+ for (const [paramName, paramDef] of Object.entries(tool.parameters)) {
117
+ if (!paramDef.type || typeof paramDef.type !== 'string') {
118
+ throw new Error(`Parameter '${paramName}' in tool '${tool.name}' must have a type`);
119
+ }
120
+
121
+ if (!paramDef.description || typeof paramDef.description !== 'string') {
122
+ throw new Error(`Parameter '${paramName}' in tool '${tool.name}' must have a description`);
123
+ }
124
+
125
+ if (typeof paramDef.required !== 'boolean') {
126
+ throw new Error(`Parameter '${paramName}' in tool '${tool.name}' must specify if it's required`);
127
+ }
128
+ }
129
+ }
130
+
131
+ private validateParameters(tool: ToolDefinition, params: Record<string, unknown>): void {
132
+ for (const [paramName, paramDef] of Object.entries(tool.parameters)) {
133
+ const value = params[paramName];
134
+
135
+ if (paramDef.required && (value === undefined || value === null)) {
136
+ throw new Error(`Required parameter '${paramName}' missing for tool '${tool.name}'`);
137
+ }
138
+
139
+ if (value !== undefined && value !== null) {
140
+ const actualType = typeof value;
141
+ const expectedType = paramDef.type.toLowerCase();
142
+
143
+ if (expectedType === 'array' && !Array.isArray(value)) {
144
+ throw new Error(
145
+ `Parameter '${paramName}' for tool '${tool.name}' must be an array, got ${actualType}`
146
+ );
147
+ } else if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value))) {
148
+ throw new Error(
149
+ `Parameter '${paramName}' for tool '${tool.name}' must be an object, got ${actualType}`
150
+ );
151
+ } else if (
152
+ expectedType !== 'array' &&
153
+ expectedType !== 'object' &&
154
+ actualType !== expectedType &&
155
+ !(expectedType === 'number' && actualType === 'bigint')
156
+ ) {
157
+ throw new Error(
158
+ `Parameter '${paramName}' for tool '${tool.name}' must be ${expectedType}, got ${actualType}`
159
+ );
160
+ }
161
+ }
162
+ }
163
+
164
+ const unexpectedParams = Object.keys(params).filter(key => !tool.parameters[key]);
165
+ if (unexpectedParams.length > 0) {
166
+ console.warn(
167
+ `Unexpected parameters for tool '${tool.name}': ${unexpectedParams.join(', ')}`
168
+ );
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Research Queue Tool
3
+ *
4
+ * Lets the agent (or user via chat) manage the background research queue.
5
+ * Actions: add, list, remove.
6
+ */
7
+
8
+ import type { ToolDefinition } from './registry.ts';
9
+ import type { ResearchQueue, ResearchPriority } from '../../daemon/research-queue.ts';
10
+
11
+ let queueRef: ResearchQueue | null = null;
12
+
13
+ /**
14
+ * Wire the research queue instance. Must be called before tool use.
15
+ */
16
+ export function setResearchQueueRef(queue: ResearchQueue): void {
17
+ queueRef = queue;
18
+ }
19
+
20
+ export const researchQueueTool: ToolDefinition = {
21
+ name: 'research_queue',
22
+ description: 'Manage the background research queue. Add topics for JARVIS to research during idle time, list current queue, or remove topics.',
23
+ category: 'productivity',
24
+ parameters: {
25
+ action: {
26
+ type: 'string',
27
+ description: 'Action to perform: "add", "list", or "remove"',
28
+ required: true,
29
+ },
30
+ topic: {
31
+ type: 'string',
32
+ description: 'The research topic (required for "add")',
33
+ required: false,
34
+ },
35
+ reason: {
36
+ type: 'string',
37
+ description: 'Why this topic should be researched (required for "add")',
38
+ required: false,
39
+ },
40
+ priority: {
41
+ type: 'string',
42
+ description: 'Priority: "high", "normal", or "low" (default: "normal", for "add" only)',
43
+ required: false,
44
+ },
45
+ id: {
46
+ type: 'string',
47
+ description: 'Topic ID to remove (required for "remove")',
48
+ required: false,
49
+ },
50
+ status: {
51
+ type: 'string',
52
+ description: 'Filter by status for "list": "queued", "in_progress", "completed", "failed"',
53
+ required: false,
54
+ },
55
+ },
56
+ execute: async (params) => {
57
+ if (!queueRef) {
58
+ return { error: 'Research queue not initialized' };
59
+ }
60
+
61
+ const action = String(params.action ?? '');
62
+
63
+ switch (action) {
64
+ case 'add': {
65
+ const topic = String(params.topic ?? '');
66
+ const reason = String(params.reason ?? 'No reason provided');
67
+ const priority = (params.priority as ResearchPriority) ?? 'normal';
68
+
69
+ if (!topic) {
70
+ return { error: 'Missing "topic" parameter' };
71
+ }
72
+
73
+ const entry = queueRef.addTopic(topic, reason, 'agent', priority);
74
+ return {
75
+ success: true,
76
+ id: entry.id,
77
+ message: `Added to research queue: "${topic}" (${priority} priority)`,
78
+ };
79
+ }
80
+
81
+ case 'list': {
82
+ const status = params.status as any;
83
+ const topics = queueRef.list(status || undefined);
84
+ return {
85
+ count: topics.length,
86
+ topics: topics.map((t) => ({
87
+ id: t.id,
88
+ topic: t.topic,
89
+ reason: t.reason,
90
+ priority: t.priority,
91
+ status: t.status,
92
+ source: t.source,
93
+ result: t.result ? (t.result.length > 200 ? t.result.slice(0, 197) + '...' : t.result) : undefined,
94
+ })),
95
+ };
96
+ }
97
+
98
+ case 'remove': {
99
+ const id = String(params.id ?? '');
100
+ if (!id) {
101
+ return { error: 'Missing "id" parameter' };
102
+ }
103
+ const removed = queueRef.remove(id);
104
+ return { success: removed, message: removed ? 'Topic removed' : 'Topic not found' };
105
+ }
106
+
107
+ default:
108
+ return { error: `Unknown action: "${action}". Use "add", "list", or "remove".` };
109
+ }
110
+ },
111
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * List Sidecars Tool
3
+ *
4
+ * Returns live sidecar connection status. The AI calls this
5
+ * before routing commands to a remote machine via the `target` param.
6
+ */
7
+
8
+ import type { ToolDefinition } from './registry.ts';
9
+ import { getSidecarManager } from './sidecar-route.ts';
10
+
11
+ export const listSidecarsTool: ToolDefinition = {
12
+ name: 'list_sidecars',
13
+ description: 'List enrolled sidecar machines with live connection status, capabilities, and system info. Use this to discover available remote targets before using run_command, read_file, write_file, or list_directory with a "target" parameter.',
14
+ category: 'sidecar',
15
+ parameters: {
16
+ filter: {
17
+ type: 'string',
18
+ description: 'Optional filter string — only sidecars whose name or ID contain this string are returned (case-insensitive). Omit to list all.',
19
+ required: false,
20
+ },
21
+ },
22
+ execute: async (params) => {
23
+ const manager = getSidecarManager();
24
+ if (!manager) {
25
+ return 'Sidecar system not initialized.';
26
+ }
27
+
28
+ const filter = (params.filter as string | undefined)?.trim().toLowerCase();
29
+ let sidecars = manager.listSidecars();
30
+
31
+ if (filter) {
32
+ sidecars = sidecars.filter(
33
+ (s) => s.name.toLowerCase().includes(filter) || s.id.toLowerCase().includes(filter),
34
+ );
35
+ }
36
+
37
+ if (sidecars.length === 0) {
38
+ return filter
39
+ ? `No sidecars matching "${filter}".`
40
+ : 'No sidecars enrolled.';
41
+ }
42
+
43
+ return sidecars.map((s) => {
44
+ const status = s.connected ? 'CONNECTED' : 'OFFLINE';
45
+ const caps = s.capabilities?.length ? s.capabilities.join(', ') : 'none';
46
+ const host = s.hostname ?? 'unknown';
47
+ const os = s.os ?? 'unknown';
48
+ const lastSeen = s.last_seen_at ?? 'never';
49
+ let line = `[${status}] ${s.name} (${s.id})\n Host: ${host} | OS: ${os} | Capabilities: ${caps} | Last seen: ${lastSeen}`;
50
+ if (s.unavailable_capabilities?.length) {
51
+ const unavailLines = s.unavailable_capabilities.map(u => ` - ${u.name}: ${u.reason}`);
52
+ line += `\n Unavailable (missing deps):\n${unavailLines.join('\n')}`;
53
+ }
54
+ return line;
55
+ }).join('\n\n');
56
+ },
57
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Sidecar Routing — Transparent Remote Execution
3
+ *
4
+ * Holds a reference to the SidecarManager and provides a helper
5
+ * that existing tools call when a `target` parameter is present.
6
+ * The AI decides where to run a command by specifying (or omitting) a target.
7
+ */
8
+
9
+ import type { SidecarManager } from '../../sidecar/manager.ts';
10
+ import type { SidecarCapability, SidecarInfo } from '../../sidecar/types.ts';
11
+
12
+ let sidecarManager: SidecarManager | null = null;
13
+
14
+ /**
15
+ * Inject the sidecar manager at startup. Called once from the daemon.
16
+ */
17
+ export function setSidecarManagerRef(manager: SidecarManager): void {
18
+ sidecarManager = manager;
19
+ }
20
+
21
+ export function getSidecarManager(): SidecarManager | null {
22
+ return sidecarManager;
23
+ }
24
+
25
+ /**
26
+ * Find a sidecar by name or ID.
27
+ * Priority: exact ID → exact name (case-insensitive) → contains match.
28
+ */
29
+ function findSidecar(nameOrId: string, sidecars: SidecarInfo[]): SidecarInfo | null {
30
+ const query = nameOrId.trim();
31
+ if (!query) return null;
32
+
33
+ // Exact ID match
34
+ const byId = sidecars.find((s) => s.id === query);
35
+ if (byId) return byId;
36
+
37
+ // Exact name (case-insensitive)
38
+ const lower = query.toLowerCase();
39
+ const byName = sidecars.find((s) => s.name.toLowerCase() === lower);
40
+ if (byName) return byName;
41
+
42
+ // Contains match
43
+ const byContains = sidecars.find((s) => s.name.toLowerCase().includes(lower));
44
+ return byContains ?? null;
45
+ }
46
+
47
+ /**
48
+ * Route an RPC call to a sidecar. Returns the result string, or an error message.
49
+ *
50
+ * @param target - Sidecar name or ID
51
+ * @param method - RPC method name (e.g. "run_command", "read_file")
52
+ * @param params - RPC parameters
53
+ * @param requiredCapability - The sidecar must advertise this capability
54
+ */
55
+ export async function routeToSidecar(
56
+ target: string,
57
+ method: string,
58
+ params: Record<string, unknown>,
59
+ requiredCapability: SidecarCapability,
60
+ ): Promise<string> {
61
+ if (!sidecarManager) {
62
+ return 'Error: Sidecar system not initialized.';
63
+ }
64
+
65
+ const sidecars = sidecarManager.listSidecars();
66
+ const sidecar = findSidecar(target, sidecars);
67
+
68
+ if (!sidecar) {
69
+ const available = sidecars.map((s) => s.name).join(', ') || 'none';
70
+ return `Error: No sidecar found matching "${target}". Available: ${available}`;
71
+ }
72
+
73
+ if (!sidecar.connected) {
74
+ return `Error: Sidecar "${sidecar.name}" is offline.`;
75
+ }
76
+
77
+ // Check if capability is enabled but unavailable (missing system dependencies)
78
+ const unavail = sidecar.unavailable_capabilities?.find(u => u.name === requiredCapability);
79
+ if (unavail) {
80
+ return `Error: Sidecar "${sidecar.name}" has "${requiredCapability}" enabled but it is unavailable: ${unavail.reason}. Do NOT retry.`;
81
+ }
82
+
83
+ if (sidecar.capabilities && !sidecar.capabilities.includes(requiredCapability)) {
84
+ return `Error: Sidecar "${sidecar.name}" does not have the "${requiredCapability}" capability enabled. Available capabilities: ${sidecar.capabilities.join(', ')}. Do NOT retry — ask the user to enable it in the sidecar's config if needed.`;
85
+ }
86
+
87
+ try {
88
+ const result = await sidecarManager.dispatchRPC(sidecar.id, method, params);
89
+
90
+ if (result === 'detached') {
91
+ return `Task dispatched to "${sidecar.name}" and running in the background.`;
92
+ }
93
+
94
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
95
+ } catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+
98
+ // METHOD_NOT_FOUND means the capability is disabled — tell the LLM not to retry
99
+ if (msg.includes('METHOD_NOT_FOUND')) {
100
+ return `Error [${sidecar.name}]: Method "${method}" is not available. The "${requiredCapability}" capability is not enabled on this sidecar. Do NOT retry this call — ask the user to enable the capability in the sidecar's config if needed.`;
101
+ }
102
+
103
+ return `Error [${sidecar.name}]: ${msg}`;
104
+ }
105
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Manage Workflow Tool — Chat-Driven Workflow Automation
3
+ *
4
+ * Allows the primary agent to create, list, run, delete, enable/disable,
5
+ * and describe workflows directly from natural language chat.
6
+ * Uses the NLWorkflowBuilder to parse descriptions into workflow definitions.
7
+ */
8
+
9
+ import type { ToolDefinition } from './registry.ts';
10
+ import type { WorkflowEngine } from '../../workflows/engine.ts';
11
+ import type { NLWorkflowBuilder } from '../../workflows/nl-builder.ts';
12
+ import type { TriggerManager } from '../../workflows/triggers/manager.ts';
13
+ import * as vault from '../../vault/workflows.ts';
14
+
15
+ export type WorkflowToolDeps = {
16
+ workflowEngine: WorkflowEngine;
17
+ nlBuilder: NLWorkflowBuilder;
18
+ triggerManager: TriggerManager;
19
+ };
20
+
21
+ export function createManageWorkflowTool(deps: WorkflowToolDeps): ToolDefinition {
22
+ return {
23
+ name: 'manage_workflow',
24
+ description: [
25
+ 'Create, list, run, delete, enable/disable, and inspect workflow automations.',
26
+ 'Workflows are event-driven: "when X happens, do Y". They support triggers (cron, webhook, file changes, screen events),',
27
+ 'actions (HTTP requests, send messages, run tools, agent tasks), logic (if/else, loops, delays), and error handling.',
28
+ '',
29
+ 'Actions: create, list, run, delete, enable, disable, describe',
30
+ ].join('\n'),
31
+ category: 'automation',
32
+ parameters: {
33
+ action: {
34
+ type: 'string',
35
+ description: 'The action: "create", "list", "run", "delete", "enable", "disable", "describe"',
36
+ required: true,
37
+ },
38
+ name: {
39
+ type: 'string',
40
+ description: 'Workflow name (required for "create")',
41
+ required: false,
42
+ },
43
+ description: {
44
+ type: 'string',
45
+ description: 'Natural language description of what the workflow should do (for "create"). Example: "Every morning at 9am, check my email and send a Telegram summary"',
46
+ required: false,
47
+ },
48
+ workflow_id: {
49
+ type: 'string',
50
+ description: 'Target workflow ID (for "run", "delete", "enable", "disable", "describe")',
51
+ required: false,
52
+ },
53
+ },
54
+ execute: async (params) => {
55
+ const action = String(params.action ?? '').toLowerCase();
56
+
57
+ switch (action) {
58
+ case 'create': {
59
+ const name = params.name as string;
60
+ const description = params.description as string;
61
+ if (!description) return 'Error: "description" is required for create. Describe what the workflow should do in natural language.';
62
+
63
+ try {
64
+ // Use NL builder to parse description into a workflow definition
65
+ const definition = await deps.nlBuilder.parseDescription(description);
66
+
67
+ // Create the workflow in vault
68
+ const workflow = vault.createWorkflow(
69
+ name || 'Untitled Workflow',
70
+ { description },
71
+ );
72
+
73
+ // Create first version with the generated definition
74
+ vault.createVersion(workflow.id, definition, 'Created via chat');
75
+
76
+ // Register triggers so cron/webhook/poll triggers activate
77
+ try {
78
+ deps.triggerManager.registerWorkflow(workflow.id, definition);
79
+ } catch {
80
+ // Non-fatal — triggers can be registered later
81
+ }
82
+
83
+ const nodeTypes = definition.nodes.map(n => n.type).join(', ');
84
+ return `Workflow created successfully!\n\n` +
85
+ `- **ID**: ${workflow.id}\n` +
86
+ `- **Name**: ${name || 'Untitled Workflow'}\n` +
87
+ `- **Nodes**: ${definition.nodes.length} (${nodeTypes})\n` +
88
+ `- **Edges**: ${definition.edges.length}\n` +
89
+ `- **Version**: 1\n\n` +
90
+ `The workflow is enabled and its triggers are active. ` +
91
+ `You can view and edit it in the Workflows dashboard, or run it manually.`;
92
+ } catch (err) {
93
+ return `Failed to create workflow: ${err instanceof Error ? err.message : err}`;
94
+ }
95
+ }
96
+
97
+ case 'list': {
98
+ const workflows = vault.findWorkflows();
99
+ if (workflows.length === 0) {
100
+ return 'No workflows found. Use action "create" with a description to create one.';
101
+ }
102
+
103
+ const lines = workflows.map(wf => {
104
+ const status = wf.enabled ? 'Active' : 'Disabled';
105
+ const lastRun = wf.last_executed_at
106
+ ? new Date(wf.last_executed_at).toLocaleString()
107
+ : 'Never';
108
+ return `- **${wf.name}** (${wf.id})\n Status: ${status} | v${wf.current_version} | ${wf.execution_count} runs | Last: ${lastRun}`;
109
+ });
110
+
111
+ return `Found ${workflows.length} workflow(s):\n\n${lines.join('\n\n')}`;
112
+ }
113
+
114
+ case 'run': {
115
+ const id = params.workflow_id as string;
116
+ if (!id) return 'Error: "workflow_id" is required for run.';
117
+
118
+ try {
119
+ const execution = await deps.workflowEngine.execute(id, 'manual');
120
+ return `Workflow execution started.\n\n- **Execution ID**: ${execution.id}\n- **Status**: ${execution.status}\n\nThe workflow is running in the background. Check the Workflows dashboard for real-time progress.`;
121
+ } catch (err) {
122
+ return `Failed to run workflow: ${err instanceof Error ? err.message : err}`;
123
+ }
124
+ }
125
+
126
+ case 'delete': {
127
+ const id = params.workflow_id as string;
128
+ if (!id) return 'Error: "workflow_id" is required for delete.';
129
+
130
+ try {
131
+ const wf = vault.getWorkflow(id);
132
+ if (!wf) return `Workflow "${id}" not found.`;
133
+
134
+ deps.triggerManager.unregisterWorkflow(id);
135
+ vault.deleteWorkflow(id);
136
+ return `Workflow "${wf.name}" (${id}) has been deleted.`;
137
+ } catch (err) {
138
+ return `Failed to delete workflow: ${err instanceof Error ? err.message : err}`;
139
+ }
140
+ }
141
+
142
+ case 'enable': {
143
+ const id = params.workflow_id as string;
144
+ if (!id) return 'Error: "workflow_id" is required for enable.';
145
+
146
+ try {
147
+ vault.updateWorkflow(id, { enabled: true });
148
+ const version = vault.getLatestVersion(id);
149
+ if (version) {
150
+ deps.triggerManager.registerWorkflow(id, version.definition);
151
+ }
152
+ return `Workflow "${id}" is now enabled. Triggers are active.`;
153
+ } catch (err) {
154
+ return `Failed to enable workflow: ${err instanceof Error ? err.message : err}`;
155
+ }
156
+ }
157
+
158
+ case 'disable': {
159
+ const id = params.workflow_id as string;
160
+ if (!id) return 'Error: "workflow_id" is required for disable.';
161
+
162
+ try {
163
+ vault.updateWorkflow(id, { enabled: false });
164
+ deps.triggerManager.unregisterWorkflow(id);
165
+ return `Workflow "${id}" is now disabled. Triggers are deactivated.`;
166
+ } catch (err) {
167
+ return `Failed to disable workflow: ${err instanceof Error ? err.message : err}`;
168
+ }
169
+ }
170
+
171
+ case 'describe': {
172
+ const id = params.workflow_id as string;
173
+ if (!id) return 'Error: "workflow_id" is required for describe.';
174
+
175
+ try {
176
+ const wf = vault.getWorkflow(id);
177
+ if (!wf) return `Workflow "${id}" not found.`;
178
+
179
+ const version = vault.getLatestVersion(id);
180
+ const def = version?.definition;
181
+
182
+ let result = `**${wf.name}** (${wf.id})\n\n`;
183
+ if (wf.description) result += `${wf.description}\n\n`;
184
+ result += `- Status: ${wf.enabled ? 'Active' : 'Disabled'}\n`;
185
+ result += `- Version: ${wf.current_version}\n`;
186
+ result += `- Executions: ${wf.execution_count}\n`;
187
+ if (wf.tags.length > 0) result += `- Tags: ${wf.tags.join(', ')}\n`;
188
+
189
+ if (def) {
190
+ result += `\n**Nodes (${def.nodes.length}):**\n`;
191
+ for (const node of def.nodes) {
192
+ result += ` - ${node.label} (${node.type})\n`;
193
+ }
194
+ result += `\n**Connections (${def.edges.length}):**\n`;
195
+ for (const edge of def.edges) {
196
+ const srcNode = def.nodes.find(n => n.id === edge.source);
197
+ const tgtNode = def.nodes.find(n => n.id === edge.target);
198
+ result += ` - ${srcNode?.label ?? edge.source} → ${tgtNode?.label ?? edge.target}`;
199
+ if (edge.sourceHandle) result += ` [${edge.sourceHandle}]`;
200
+ result += '\n';
201
+ }
202
+ result += `\n**Settings:** retries=${def.settings.maxRetries}, timeout=${def.settings.timeoutMs}ms, onError=${def.settings.onError}`;
203
+ }
204
+
205
+ return result;
206
+ } catch (err) {
207
+ return `Failed to describe workflow: ${err instanceof Error ? err.message : err}`;
208
+ }
209
+ }
210
+
211
+ default:
212
+ return `Unknown action "${action}". Available actions: create, list, run, delete, enable, disable, describe`;
213
+ }
214
+ },
215
+ };
216
+ }