@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,396 @@
1
+ /**
2
+ * Background Agent Service — Independent Monitoring Brain
3
+ *
4
+ * Runs heartbeats, event reactions, and commitment executions on a
5
+ * SEPARATE agent with its own browser instance (CDP port 9223).
6
+ * User chat on the main AgentService is never blocked.
7
+ *
8
+ * Shares: LLMManager (same API keys), SQLite vault (same DB)
9
+ * Separate: BrowserController, AgentOrchestrator, ToolRegistry, conversation history
10
+ */
11
+
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import type { Service, ServiceStatus } from './services.ts';
15
+ import type { IAgentService } from './agent-service-interface.ts';
16
+ import type { JarvisConfig } from '../config/types.ts';
17
+ import type { RoleDefinition } from '../roles/types.ts';
18
+ import type { LLMManager } from '../llm/manager.ts';
19
+ import type { ResearchQueue } from './research-queue.ts';
20
+
21
+ import { AgentOrchestrator } from '../agents/orchestrator.ts';
22
+ import { loadRole } from '../roles/loader.ts';
23
+ import { ToolRegistry } from '../actions/tools/registry.ts';
24
+ import { NON_BROWSER_TOOLS, createBrowserTools } from '../actions/tools/builtin.ts';
25
+ import { BrowserController } from '../actions/browser/session.ts';
26
+ import { DESKTOP_TOOLS } from '../actions/tools/desktop.ts';
27
+ import { commitmentsTool } from '../actions/tools/commitments.ts';
28
+ import { researchQueueTool } from '../actions/tools/research.ts';
29
+ import { buildSystemPrompt, type PromptContext } from '../roles/prompt-builder.ts';
30
+ import { getDueCommitments, getUpcoming } from '../vault/commitments.ts';
31
+ import { getRecentObservations } from '../vault/observations.ts';
32
+ import { findContent } from '../vault/content-pipeline.ts';
33
+ import { getRecentConversation, getMessages } from '../vault/conversations.ts';
34
+ import { getActiveGoalsSummary } from '../vault/retrieval.ts';
35
+
36
+ const BG_CDP_PORT = 9223;
37
+ const BG_PROFILE_DIR = join(homedir(), '.jarvis', 'browser', 'bg-profile');
38
+
39
+ export class BackgroundAgentService implements Service, IAgentService {
40
+ name = 'background-agent';
41
+ private _status: ServiceStatus = 'stopped';
42
+ private config: JarvisConfig;
43
+ private llmManager: LLMManager;
44
+ private orchestrator: AgentOrchestrator;
45
+ private bgBrowser: BrowserController;
46
+ private role: RoleDefinition | null = null;
47
+ private researchQueue: ResearchQueue | null = null;
48
+ private busy = false;
49
+
50
+ constructor(config: JarvisConfig, llmManager: LLMManager) {
51
+ this.config = config;
52
+ this.llmManager = llmManager;
53
+ this.orchestrator = new AgentOrchestrator();
54
+ this.bgBrowser = new BrowserController(BG_CDP_PORT, BG_PROFILE_DIR);
55
+ }
56
+
57
+ setResearchQueue(queue: ResearchQueue): void {
58
+ this.researchQueue = queue;
59
+ }
60
+
61
+ async start(): Promise<void> {
62
+ this._status = 'starting';
63
+
64
+ try {
65
+ // 1. Wire shared LLM manager
66
+ this.orchestrator.setLLMManager(this.llmManager);
67
+
68
+ // 2. Load the same role as the main agent
69
+ this.role = this.loadActiveRole();
70
+
71
+ // 3. Build tool registry with background browser
72
+ const toolRegistry = new ToolRegistry();
73
+
74
+ for (const tool of NON_BROWSER_TOOLS) {
75
+ toolRegistry.register(tool);
76
+ }
77
+
78
+ const bgBrowserTools = createBrowserTools(this.bgBrowser);
79
+ for (const tool of bgBrowserTools) {
80
+ toolRegistry.register(tool);
81
+ }
82
+
83
+ // Desktop tools (routed via sidecar RPC)
84
+ for (const tool of DESKTOP_TOOLS) {
85
+ toolRegistry.register(tool);
86
+ }
87
+
88
+ toolRegistry.register(commitmentsTool);
89
+ toolRegistry.register(researchQueueTool);
90
+
91
+ this.orchestrator.setToolRegistry(toolRegistry);
92
+
93
+ // 4. Create primary agent for background operations
94
+ this.orchestrator.createPrimary(this.role);
95
+
96
+ this._status = 'running';
97
+ console.log(`[BackgroundAgent] Started with role: ${this.role.name}, browser on port ${BG_CDP_PORT}`);
98
+ } catch (error) {
99
+ this._status = 'error';
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ async stop(): Promise<void> {
105
+ this._status = 'stopping';
106
+ const primary = this.orchestrator.getPrimary();
107
+ if (primary) {
108
+ this.orchestrator.terminateAgent(primary.id);
109
+ }
110
+
111
+ if (this.bgBrowser.connected) {
112
+ await this.bgBrowser.disconnect();
113
+ }
114
+
115
+ this._status = 'stopped';
116
+ console.log('[BackgroundAgent] Stopped');
117
+ }
118
+
119
+ status(): ServiceStatus {
120
+ return this._status;
121
+ }
122
+
123
+ get isBusy(): boolean {
124
+ return this.busy;
125
+ }
126
+
127
+ /**
128
+ * Handle periodic heartbeat with full tool access.
129
+ * Returns null if busy (non-blocking for the caller).
130
+ */
131
+ async handleHeartbeat(coalescedEvents?: string): Promise<string | null> {
132
+ if (this.busy) {
133
+ console.log('[BackgroundAgent] Skipping heartbeat — already busy');
134
+ return null;
135
+ }
136
+
137
+ this.busy = true;
138
+ try {
139
+ const systemPrompt = this.buildHeartbeatPrompt(coalescedEvents);
140
+ const parts: string[] = ['[HEARTBEAT] Periodic check-in. Review your responsibilities and take action.'];
141
+ if (coalescedEvents) {
142
+ parts.push('', coalescedEvents);
143
+ }
144
+
145
+ const response = await this.orchestrator.processMessage(systemPrompt, parts.join('\n'));
146
+ if (response && response.trim().length > 0) {
147
+ return response;
148
+ }
149
+ return null;
150
+ } catch (err) {
151
+ console.error('[BackgroundAgent] Heartbeat error:', err);
152
+ return null;
153
+ } finally {
154
+ this.busy = false;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Handle a reactive event message (from EventReactor / CommitmentExecutor).
160
+ */
161
+ async handleMessage(text: string, channel: string = 'system'): Promise<string> {
162
+ // Wait if busy — event reactor already has its own queue, so this is a safety net
163
+ const waitStart = Date.now();
164
+ while (this.busy && Date.now() - waitStart < 60_000) {
165
+ await new Promise(r => setTimeout(r, 1000));
166
+ }
167
+
168
+ this.busy = true;
169
+ try {
170
+ const systemPrompt = this.buildSystemPrompt(channel);
171
+ return await this.orchestrator.processMessage(systemPrompt, text);
172
+ } catch (err) {
173
+ console.error('[BackgroundAgent] Message error:', err);
174
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
175
+ } finally {
176
+ this.busy = false;
177
+ }
178
+ }
179
+
180
+ // --- Private methods ---
181
+
182
+ private buildSystemPrompt(channel: string): string {
183
+ if (!this.role) return '';
184
+ const context = this.buildPromptContext();
185
+ return buildSystemPrompt(this.role, context);
186
+ }
187
+
188
+ /**
189
+ * Get the last N messages from the most recent chat conversation.
190
+ * Returns formatted chat transcript and staleness info.
191
+ */
192
+ private getRecentChatContext(messageCount: number = 20): {
193
+ transcript: string | null;
194
+ lastUserMessageAt: number | null;
195
+ lastAssistantMessageAt: number | null;
196
+ minutesSinceLastUserMessage: number | null;
197
+ } {
198
+ try {
199
+ const recent = getRecentConversation('websocket');
200
+ if (!recent) return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
201
+
202
+ const messages = getMessages(recent.conversation.id, { limit: messageCount });
203
+ if (messages.length === 0) return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
204
+
205
+ // Find timestamps for staleness detection
206
+ const now = Date.now();
207
+ let lastUserAt: number | null = null;
208
+ let lastAssistantAt: number | null = null;
209
+ for (let i = messages.length - 1; i >= 0; i--) {
210
+ const msg = messages[i]!;
211
+ if (!lastUserAt && msg.role === 'user') lastUserAt = msg.created_at;
212
+ if (!lastAssistantAt && msg.role === 'assistant') lastAssistantAt = msg.created_at;
213
+ if (lastUserAt && lastAssistantAt) break;
214
+ }
215
+
216
+ // Format transcript
217
+ const lines = messages
218
+ .filter(m => m.role === 'user' || m.role === 'assistant')
219
+ .map(m => {
220
+ const time = new Date(m.created_at).toLocaleTimeString();
221
+ const role = m.role === 'user' ? 'USER' : 'JARVIS';
222
+ // Truncate long messages to keep context manageable
223
+ const content = m.content.length > 500 ? m.content.slice(0, 500) + '...' : m.content;
224
+ return `[${time}] ${role}: ${content}`;
225
+ });
226
+
227
+ return {
228
+ transcript: lines.join('\n'),
229
+ lastUserMessageAt: lastUserAt,
230
+ lastAssistantMessageAt: lastAssistantAt,
231
+ minutesSinceLastUserMessage: lastUserAt ? Math.round((now - lastUserAt) / 60_000) : null,
232
+ };
233
+ } catch (err) {
234
+ console.error('[BackgroundAgent] Error loading chat context:', err);
235
+ return { transcript: null, lastUserMessageAt: null, lastAssistantMessageAt: null, minutesSinceLastUserMessage: null };
236
+ }
237
+ }
238
+
239
+ private buildHeartbeatPrompt(coalescedEvents?: string): string {
240
+ if (!this.role) return '';
241
+
242
+ const context = this.buildPromptContext();
243
+ const rolePrompt = buildSystemPrompt(this.role, context);
244
+
245
+ const parts = [rolePrompt, '', '# Heartbeat Check', this.role.heartbeat_instructions];
246
+
247
+ // --- RECENT CHAT CONTEXT ---
248
+ const chat = this.getRecentChatContext(20);
249
+ if (chat.transcript) {
250
+ parts.push('', '# RECENT CHAT (last 20 messages)');
251
+ parts.push('Review this conversation for unfulfilled promises, unanswered questions, or implicit commitments.');
252
+ parts.push('');
253
+ parts.push(chat.transcript);
254
+
255
+ // Staleness warning
256
+ if (chat.minutesSinceLastUserMessage !== null && chat.minutesSinceLastUserMessage >= 120) {
257
+ parts.push('');
258
+ parts.push(`⚠ CONVERSATION STALE: Last user message was ${chat.minutesSinceLastUserMessage} minutes ago.`);
259
+ parts.push('Consider a gentle proactive check-in if appropriate during active hours.');
260
+ }
261
+
262
+ // Detect if JARVIS was last to speak (may have promised something)
263
+ if (chat.lastAssistantMessageAt && chat.lastUserMessageAt && chat.lastAssistantMessageAt > chat.lastUserMessageAt) {
264
+ parts.push('');
265
+ parts.push('NOTE: JARVIS was the last to speak. Check if that last message contained any promises, "I\'ll do X" statements, or tasks that may not have been completed.');
266
+ }
267
+ }
268
+
269
+ // --- ACTIVE GOALS ---
270
+ try {
271
+ const goalsSummary = getActiveGoalsSummary();
272
+ if (goalsSummary) {
273
+ parts.push('', '# ACTIVE GOALS');
274
+ parts.push('Cross-reference these with the recent chat. If goals were discussed but not updated, flag it.');
275
+ parts.push('');
276
+ parts.push(goalsSummary);
277
+ }
278
+ } catch (err) {
279
+ console.error('[BackgroundAgent] Error loading goals summary:', err);
280
+ }
281
+
282
+ if (coalescedEvents) {
283
+ parts.push('', '# Recent System Events', coalescedEvents);
284
+ }
285
+
286
+ parts.push('', '# COMMITMENT EXECUTION');
287
+ parts.push('If any commitments are overdue or due soon, EXECUTE them now using your tools.');
288
+ parts.push('Do not just mention them — actually perform the work. Use browse, terminal, file operations as needed.');
289
+
290
+ if (this.researchQueue && this.researchQueue.queuedCount() > 0) {
291
+ const next = this.researchQueue.getNext();
292
+ if (next) {
293
+ parts.push('', '# BACKGROUND RESEARCH');
294
+ parts.push(`You have a research topic queued: "${next.topic}"`);
295
+ parts.push(`Reason: ${next.reason}`);
296
+ parts.push(`Research ID: ${next.id}`);
297
+ parts.push('If nothing urgent needs your attention, research this topic now.');
298
+ parts.push('Use your browser and tools to gather information, then use the research_queue tool with action "complete" to save your findings.');
299
+ }
300
+ } else {
301
+ parts.push('', '# IDLE MODE');
302
+ parts.push('No research topics queued. If nothing urgent, you may:');
303
+ parts.push('- Check news or trends relevant to the user');
304
+ parts.push('- Review and organize pending tasks');
305
+ parts.push('- Or simply report "All clear" if nothing needs attention');
306
+ }
307
+
308
+ parts.push('', '# Important', 'You have full tool access during this heartbeat. If you need to take action (browse the web, run commands, check files), DO IT. Be proactive and aggressive about helping.');
309
+
310
+ return parts.join('\n');
311
+ }
312
+
313
+ private buildPromptContext(): PromptContext {
314
+ const context: PromptContext = {
315
+ currentTime: new Date().toISOString(),
316
+ };
317
+
318
+ // Get due commitments
319
+ try {
320
+ const due = getDueCommitments();
321
+ const upcoming = getUpcoming(5);
322
+ const allCommitments = [...due, ...upcoming];
323
+
324
+ if (allCommitments.length > 0) {
325
+ context.activeCommitments = allCommitments.map((c) => {
326
+ const dueStr = c.when_due
327
+ ? ` (due: ${new Date(c.when_due).toLocaleString()})`
328
+ : '';
329
+ return `[${c.priority}] ${c.what}${dueStr} — ${c.status}`;
330
+ });
331
+ }
332
+ } catch (err) {
333
+ console.error('[BackgroundAgent] Error loading commitments:', err);
334
+ }
335
+
336
+ // Get active content pipeline items
337
+ try {
338
+ const activeContent = findContent({}).filter(
339
+ (c) => c.stage !== 'published'
340
+ ).slice(0, 10);
341
+ if (activeContent.length > 0) {
342
+ context.contentPipeline = activeContent.map((c) => {
343
+ const tags = c.tags.length > 0 ? ` [${c.tags.join(', ')}]` : '';
344
+ return `"${c.title}" (${c.content_type}) — ${c.stage}${tags}`;
345
+ });
346
+ }
347
+ } catch (err) {
348
+ console.error('[BackgroundAgent] Error loading content pipeline:', err);
349
+ }
350
+
351
+ // Get recent observations
352
+ try {
353
+ const observations = getRecentObservations(undefined, 10);
354
+ if (observations.length > 0) {
355
+ context.recentObservations = observations.map((o) => {
356
+ const time = new Date(o.created_at).toLocaleTimeString();
357
+ return `[${time}] ${o.type}: ${JSON.stringify(o.data).slice(0, 200)}`;
358
+ });
359
+ }
360
+ } catch (err) {
361
+ console.error('[BackgroundAgent] Error loading observations:', err);
362
+ }
363
+
364
+ return context;
365
+ }
366
+
367
+ private loadActiveRole(): RoleDefinition {
368
+ const roleName = this.config.active_role;
369
+
370
+ // Package-root-relative paths for global install compatibility
371
+ const pkgRoot = join(import.meta.dir, '../..');
372
+ const paths = [
373
+ join(pkgRoot, `roles/${roleName}.yaml`),
374
+ join(pkgRoot, `roles/${roleName}.yml`),
375
+ join(pkgRoot, `config/roles/${roleName}.yaml`),
376
+ join(pkgRoot, `config/roles/${roleName}.yml`),
377
+ // Also try CWD-relative for local dev
378
+ `roles/${roleName}.yaml`,
379
+ `roles/${roleName}.yml`,
380
+ ];
381
+
382
+ for (const rolePath of paths) {
383
+ try {
384
+ const role = loadRole(rolePath);
385
+ console.log(`[BackgroundAgent] Loaded role '${role.name}' from ${rolePath}`);
386
+ return role;
387
+ } catch {
388
+ // Try next path
389
+ }
390
+ }
391
+
392
+ throw new Error(
393
+ `[BackgroundAgent] Could not load role '${roleName}'. Searched: ${paths.join(', ')}`
394
+ );
395
+ }
396
+ }
@@ -0,0 +1,78 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { createBrowserTools, NON_BROWSER_TOOLS, BUILTIN_TOOLS } from '../actions/tools/builtin.ts';
3
+ import { BrowserController } from '../actions/browser/session.ts';
4
+
5
+ describe('NON_BROWSER_TOOLS', () => {
6
+ test('contains 9 non-browser tools', () => {
7
+ expect(NON_BROWSER_TOOLS).toHaveLength(9);
8
+ const names = NON_BROWSER_TOOLS.map(t => t.name);
9
+ expect(names).toContain('run_command');
10
+ expect(names).toContain('read_file');
11
+ expect(names).toContain('write_file');
12
+ expect(names).toContain('list_directory');
13
+ expect(names).toContain('list_sidecars');
14
+ expect(names).toContain('get_clipboard');
15
+ expect(names).toContain('set_clipboard');
16
+ expect(names).toContain('capture_screen');
17
+ expect(names).toContain('get_system_info');
18
+ });
19
+
20
+ test('none have browser category', () => {
21
+ for (const tool of NON_BROWSER_TOOLS) {
22
+ expect(tool.category).not.toBe('browser');
23
+ }
24
+ });
25
+ });
26
+
27
+ describe('createBrowserTools', () => {
28
+ test('returns 7 browser tools', () => {
29
+ const ctrl = new BrowserController(9999);
30
+ const tools = createBrowserTools(ctrl);
31
+ expect(tools).toHaveLength(7);
32
+ });
33
+
34
+ test('all tools have browser category', () => {
35
+ const ctrl = new BrowserController(9999);
36
+ const tools = createBrowserTools(ctrl);
37
+ for (const tool of tools) {
38
+ expect(tool.category).toBe('browser');
39
+ }
40
+ });
41
+
42
+ test('tool names match expected browser tools', () => {
43
+ const ctrl = new BrowserController(9999);
44
+ const tools = createBrowserTools(ctrl);
45
+ const names = tools.map(t => t.name).sort();
46
+ expect(names).toEqual([
47
+ 'browser_click',
48
+ 'browser_evaluate',
49
+ 'browser_navigate',
50
+ 'browser_screenshot',
51
+ 'browser_scroll',
52
+ 'browser_snapshot',
53
+ 'browser_type',
54
+ ]);
55
+ });
56
+
57
+ test('BUILTIN_TOOLS = NON_BROWSER_TOOLS + 7 browser + 9 desktop tools', () => {
58
+ expect(BUILTIN_TOOLS).toHaveLength(NON_BROWSER_TOOLS.length + 7 + 9);
59
+ });
60
+ });
61
+
62
+ describe('BrowserController parameterization', () => {
63
+ test('accepts custom port', () => {
64
+ const ctrl = new BrowserController(9223);
65
+ // Should not throw — port is stored internally
66
+ expect(ctrl).toBeDefined();
67
+ });
68
+
69
+ test('accepts custom port and profile dir', () => {
70
+ const ctrl = new BrowserController(9223, '/tmp/test-bg-profile');
71
+ expect(ctrl).toBeDefined();
72
+ });
73
+
74
+ test('defaults work (no args)', () => {
75
+ const ctrl = new BrowserController();
76
+ expect(ctrl).toBeDefined();
77
+ });
78
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Channel Service — External Communication Channels
3
+ *
4
+ * Manages Telegram, Discord (and future) channel adapters.
5
+ * Routes all external messages through the same AgentService (same brain),
6
+ * persists conversations to the vault (unified history), and handles
7
+ * proactive broadcasts to all connected channels.
8
+ */
9
+
10
+ import type { Service, ServiceStatus } from './services.ts';
11
+ import type { AgentService } from './agent-service.ts';
12
+ import type { JarvisConfig } from '../config/types.ts';
13
+ import type { ChannelMessage } from '../comms/channels/telegram.ts';
14
+ import type { STTProvider } from '../comms/voice.ts';
15
+
16
+ import { ChannelManager } from '../comms/index.ts';
17
+ import { TelegramAdapter } from '../comms/channels/telegram.ts';
18
+ import { DiscordAdapter } from '../comms/channels/discord.ts';
19
+ import { createSTTProvider } from '../comms/voice.ts';
20
+ import { getOrCreateConversation, addMessage } from '../vault/conversations.ts';
21
+
22
+ export type ApprovalCommandHandler = (action: 'approve' | 'deny', shortId: string, channel: string) => Promise<string>;
23
+
24
+ export class ChannelService implements Service {
25
+ name = 'channels';
26
+ private _status: ServiceStatus = 'stopped';
27
+ private config: JarvisConfig;
28
+ private agentService: AgentService;
29
+ private manager: ChannelManager;
30
+ private sttProvider: STTProvider | null = null;
31
+ /** Track last message sender per channel for proactive broadcasts */
32
+ private lastRecipients = new Map<string, string>();
33
+ /** Handler for approval commands (approve/deny) from external channels */
34
+ private approvalHandler: ApprovalCommandHandler | null = null;
35
+
36
+ constructor(config: JarvisConfig, agentService: AgentService) {
37
+ this.config = config;
38
+ this.agentService = agentService;
39
+ this.manager = new ChannelManager();
40
+ }
41
+
42
+ setApprovalHandler(handler: ApprovalCommandHandler): void {
43
+ this.approvalHandler = handler;
44
+ }
45
+
46
+ async start(): Promise<void> {
47
+ this._status = 'starting';
48
+
49
+ try {
50
+ // 1. Create STT provider if configured
51
+ if (this.config.stt) {
52
+ this.sttProvider = createSTTProvider(this.config.stt);
53
+ if (this.sttProvider) {
54
+ console.log(`[ChannelService] STT provider: ${this.config.stt.provider}`);
55
+ } else {
56
+ console.log('[ChannelService] STT configured but no valid credentials — voice messages disabled');
57
+ }
58
+ }
59
+
60
+ // 2. Create & register adapters from config
61
+ const channels = this.config.channels;
62
+
63
+ if (channels?.telegram?.enabled && channels.telegram.bot_token) {
64
+ const telegram = new TelegramAdapter(channels.telegram.bot_token, {
65
+ sttProvider: this.sttProvider ?? undefined,
66
+ allowedUsers: channels.telegram.allowed_users,
67
+ });
68
+ this.manager.register(telegram);
69
+ }
70
+
71
+ if (channels?.discord?.enabled && channels.discord.bot_token) {
72
+ const discord = new DiscordAdapter(channels.discord.bot_token, {
73
+ sttProvider: this.sttProvider ?? undefined,
74
+ allowedUsers: channels.discord.allowed_users,
75
+ guildId: channels.discord.guild_id,
76
+ });
77
+ this.manager.register(discord);
78
+ }
79
+
80
+ // 3. Set unified message handler — same brain for all channels
81
+ this.manager.setHandler(async (msg: ChannelMessage): Promise<string> => {
82
+ return this.handleChannelMessage(msg);
83
+ });
84
+
85
+ // 4. Connect all registered channels (Promise.allSettled — one failure doesn't block others)
86
+ const channelList = this.manager.listChannels();
87
+ if (channelList.length > 0) {
88
+ await this.manager.connectAll();
89
+ console.log(`[ChannelService] Active channels: ${channelList.join(', ')}`);
90
+ } else {
91
+ console.log('[ChannelService] No channels configured — enable in Dashboard Settings or config.yaml');
92
+ }
93
+
94
+ this._status = 'running';
95
+ console.log('[ChannelService] Started');
96
+ } catch (error) {
97
+ this._status = 'error';
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ async stop(): Promise<void> {
103
+ this._status = 'stopping';
104
+ await this.manager.disconnectAll();
105
+ this._status = 'stopped';
106
+ console.log('[ChannelService] Stopped');
107
+ }
108
+
109
+ status(): ServiceStatus {
110
+ return this._status;
111
+ }
112
+
113
+ /** Expose manager for direct adapter access if needed */
114
+ getManager(): ChannelManager {
115
+ return this.manager;
116
+ }
117
+
118
+ /** Get connection status of all channels */
119
+ getChannelStatus(): Record<string, boolean> {
120
+ return this.manager.getStatus();
121
+ }
122
+
123
+ /**
124
+ * Send a message to a specific channel.
125
+ * Used for targeted proactive notifications.
126
+ */
127
+ async sendToChannel(channelName: string, recipientId: string, text: string): Promise<void> {
128
+ const adapter = this.manager.getChannel(channelName);
129
+ if (!adapter || !adapter.isConnected()) {
130
+ console.warn(`[ChannelService] Cannot send to ${channelName}: not connected`);
131
+ return;
132
+ }
133
+ try {
134
+ await adapter.sendMessage(recipientId, text);
135
+ } catch (err) {
136
+ console.error(`[ChannelService] Failed to send to ${channelName}:`, err);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Broadcast a message to ALL connected external channels.
142
+ * Uses the last known recipient per channel (from most recent inbound message).
143
+ */
144
+ async broadcastToAll(text: string): Promise<void> {
145
+ for (const name of this.manager.listChannels()) {
146
+ const adapter = this.manager.getChannel(name);
147
+ if (!adapter?.isConnected()) continue;
148
+
149
+ const lastRecipient = this.lastRecipients.get(name);
150
+ if (!lastRecipient) {
151
+ console.log(`[ChannelService] No known recipient for ${name}, skipping broadcast`);
152
+ continue;
153
+ }
154
+
155
+ try {
156
+ await adapter.sendMessage(lastRecipient, text);
157
+ } catch (err) {
158
+ console.error(`[ChannelService] Broadcast to ${name} failed:`, err);
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Core message handler: receives from any channel, routes to AgentService,
165
+ * persists to vault (unified history), returns response.
166
+ */
167
+ private async handleChannelMessage(msg: ChannelMessage): Promise<string> {
168
+ const channelTag = msg.channel; // 'telegram' | 'discord'
169
+
170
+ // Track recipient for future broadcasts
171
+ const recipientId = String(msg.metadata.chatId ?? msg.metadata.channelId ?? msg.from);
172
+ this.lastRecipients.set(channelTag, recipientId);
173
+
174
+ // Check for approval commands: "approve <id>" or "deny <id>"
175
+ const trimmed = msg.text.trim().toLowerCase();
176
+ const approveMatch = trimmed.match(/^approve\s+([a-f0-9-]+)/i);
177
+ const denyMatch = trimmed.match(/^deny\s+([a-f0-9-]+)/i);
178
+
179
+ if (this.approvalHandler && (approveMatch || denyMatch)) {
180
+ const action = approveMatch ? 'approve' : 'deny';
181
+ const shortId = (approveMatch ?? denyMatch)![1];
182
+ try {
183
+ return await this.approvalHandler(action as 'approve' | 'deny', shortId!, channelTag);
184
+ } catch (err) {
185
+ return `Error processing approval: ${err instanceof Error ? err.message : String(err)}`;
186
+ }
187
+ }
188
+
189
+ // 1. Persist inbound user message to vault
190
+ const conversation = getOrCreateConversation(channelTag);
191
+ addMessage(conversation.id, { role: 'user', content: msg.text });
192
+
193
+ // 2. Route to AgentService (non-streaming — external channels are request/response)
194
+ const response = await this.agentService.handleMessage(msg.text, channelTag);
195
+
196
+ // 3. Persist assistant response to vault
197
+ addMessage(conversation.id, { role: 'assistant', content: response });
198
+
199
+ return response;
200
+ }
201
+ }