@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,297 @@
1
+ /**
2
+ * CommitmentExecutor — Notify-Then-Execute Engine
3
+ *
4
+ * Detects due commitments, announces pending execution to the UI
5
+ * with a cancel window, then forces the agent to execute if not cancelled.
6
+ *
7
+ * Aggressiveness modes:
8
+ * passive: announce only, never auto-execute
9
+ * moderate: 30s cancel window (default)
10
+ * aggressive: 5s cancel window
11
+ */
12
+
13
+ import { getDueCommitments, getUpcoming, updateCommitmentStatus } from '../vault/commitments.ts';
14
+ import type { Commitment } from '../vault/commitments.ts';
15
+ import type { IAgentService } from './agent-service-interface.ts';
16
+ import type { WSMessage } from '../comms/websocket.ts';
17
+
18
+ export type Aggressiveness = 'passive' | 'moderate' | 'aggressive';
19
+
20
+ export type ExecutionState = {
21
+ commitmentId: string;
22
+ what: string;
23
+ announcedAt: number;
24
+ cancelDeadline: number;
25
+ cancelled: boolean;
26
+ executed: boolean;
27
+ };
28
+
29
+ export type BroadcastFn = (msg: WSMessage) => void;
30
+
31
+ const CANCEL_WINDOW: Record<Aggressiveness, number> = {
32
+ passive: Infinity,
33
+ moderate: 30_000,
34
+ aggressive: 5_000,
35
+ };
36
+
37
+ export class CommitmentExecutor {
38
+ private agentService: IAgentService | null = null;
39
+ private broadcast: BroadcastFn | null = null;
40
+ private pending: Map<string, ExecutionState> = new Map();
41
+ private executedIds: Set<string> = new Set();
42
+ private checkTimer: Timer | null = null;
43
+ private tickTimer: Timer | null = null;
44
+ private aggressiveness: Aggressiveness;
45
+ private running = false;
46
+
47
+ constructor(aggressiveness: Aggressiveness = 'moderate') {
48
+ this.aggressiveness = aggressiveness;
49
+ }
50
+
51
+ setAgentService(agent: IAgentService): void {
52
+ this.agentService = agent;
53
+ }
54
+
55
+ setBroadcast(fn: BroadcastFn): void {
56
+ this.broadcast = fn;
57
+ }
58
+
59
+ start(): void {
60
+ if (this.running) return;
61
+ this.running = true;
62
+
63
+ // Check for due commitments every 60 seconds
64
+ this.checkTimer = setInterval(() => {
65
+ this.checkAndAnnounce();
66
+ }, 60_000);
67
+
68
+ // Tick pending executions every 5 seconds
69
+ this.tickTimer = setInterval(() => {
70
+ this.tickExecutions().catch((err) =>
71
+ console.error('[Executor] Tick error:', err)
72
+ );
73
+ }, 5_000);
74
+
75
+ // Run an immediate check
76
+ this.checkAndAnnounce();
77
+
78
+ console.log(`[Executor] Started (mode: ${this.aggressiveness})`);
79
+ }
80
+
81
+ stop(): void {
82
+ this.running = false;
83
+ if (this.checkTimer) {
84
+ clearInterval(this.checkTimer);
85
+ this.checkTimer = null;
86
+ }
87
+ if (this.tickTimer) {
88
+ clearInterval(this.tickTimer);
89
+ this.tickTimer = null;
90
+ }
91
+ console.log('[Executor] Stopped');
92
+ }
93
+
94
+ /**
95
+ * Check for commitments that are due or due within 2 minutes.
96
+ * Announce each one as a pending execution.
97
+ */
98
+ checkAndAnnounce(): void {
99
+ try {
100
+ const now = Date.now();
101
+ const dueNow = getDueCommitments(); // when_due <= now
102
+ const upcoming = getUpcoming(20); // all upcoming with when_due
103
+
104
+ // Filter upcoming to those due within 2 minutes
105
+ const dueSoon = upcoming.filter(
106
+ (c) => c.when_due && c.when_due > now && c.when_due <= now + 2 * 60_000
107
+ );
108
+
109
+ const candidates = [...dueNow, ...dueSoon];
110
+
111
+ for (const commitment of candidates) {
112
+ // Skip if already announced, executed, or terminal status
113
+ if (this.pending.has(commitment.id)) continue;
114
+ if (this.executedIds.has(commitment.id)) continue;
115
+ if (commitment.status === 'completed' || commitment.status === 'failed') continue;
116
+
117
+ this.announceExecution(commitment);
118
+ }
119
+ } catch (err) {
120
+ console.error('[Executor] Check error:', err);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Cancel a pending execution. Returns true if successfully cancelled.
126
+ */
127
+ cancelExecution(commitmentId: string): boolean {
128
+ const state = this.pending.get(commitmentId);
129
+ if (!state || state.executed || state.cancelled) return false;
130
+
131
+ state.cancelled = true;
132
+ console.log(`[Executor] Cancelled execution: ${state.what}`);
133
+
134
+ // Broadcast cancellation confirmation
135
+ this.broadcast?.({
136
+ type: 'notification',
137
+ payload: {
138
+ source: 'commitment_executor',
139
+ action: 'execution_cancelled',
140
+ commitmentId,
141
+ what: state.what,
142
+ },
143
+ timestamp: Date.now(),
144
+ });
145
+
146
+ // Clean up
147
+ this.pending.delete(commitmentId);
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Get all pending executions (for UI display).
153
+ */
154
+ getPending(): ExecutionState[] {
155
+ return Array.from(this.pending.values()).filter((s) => !s.cancelled && !s.executed);
156
+ }
157
+
158
+ // --- Private ---
159
+
160
+ private announceExecution(commitment: Commitment): void {
161
+ const now = Date.now();
162
+ const cancelWindow = CANCEL_WINDOW[this.aggressiveness];
163
+
164
+ const state: ExecutionState = {
165
+ commitmentId: commitment.id,
166
+ what: commitment.what,
167
+ announcedAt: now,
168
+ cancelDeadline: cancelWindow === Infinity ? Infinity : now + cancelWindow,
169
+ cancelled: false,
170
+ executed: false,
171
+ };
172
+
173
+ this.pending.set(commitment.id, state);
174
+
175
+ if (this.aggressiveness === 'passive') {
176
+ console.log(`[Executor] Announced (passive, no auto-execute): ${commitment.what}`);
177
+ } else {
178
+ const windowSec = Math.round(cancelWindow / 1000);
179
+ console.log(`[Executor] Announced: "${commitment.what}" — executing in ${windowSec}s unless cancelled`);
180
+ }
181
+
182
+ // Broadcast announcement to all WebSocket clients
183
+ this.broadcast?.({
184
+ type: 'notification',
185
+ payload: {
186
+ source: 'commitment_executor',
187
+ action: 'pending_execution',
188
+ commitmentId: commitment.id,
189
+ what: commitment.what,
190
+ executeAt: state.cancelDeadline === Infinity ? null : state.cancelDeadline,
191
+ cancelWindowMs: cancelWindow === Infinity ? null : cancelWindow,
192
+ },
193
+ timestamp: now,
194
+ });
195
+
196
+ // Also broadcast as a chat message so the user sees it
197
+ this.broadcast?.({
198
+ type: 'chat',
199
+ payload: {
200
+ text: this.aggressiveness === 'passive'
201
+ ? `Task due: "${commitment.what}". Waiting for your instruction to proceed.`
202
+ : `Executing "${commitment.what}" in ${Math.round(cancelWindow / 1000)}s. Send cancel to abort.`,
203
+ source: 'proactive',
204
+ },
205
+ priority: 'urgent',
206
+ timestamp: now,
207
+ });
208
+ }
209
+
210
+ private async tickExecutions(): Promise<void> {
211
+ if (!this.agentService) return;
212
+
213
+ const now = Date.now();
214
+
215
+ for (const [id, state] of this.pending) {
216
+ if (state.cancelled || state.executed) {
217
+ this.pending.delete(id);
218
+ continue;
219
+ }
220
+
221
+ // Check if cancel window has expired
222
+ if (now < state.cancelDeadline) continue;
223
+
224
+ // Execute!
225
+ state.executed = true;
226
+ this.pending.delete(id);
227
+ this.executedIds.add(id);
228
+
229
+ // Cap executedIds memory
230
+ if (this.executedIds.size > 500) {
231
+ const arr = Array.from(this.executedIds);
232
+ this.executedIds = new Set(arr.slice(arr.length - 250));
233
+ }
234
+
235
+ try {
236
+ await this.executeCommitment(state);
237
+ } catch (err) {
238
+ console.error(`[Executor] Failed to execute "${state.what}":`, err);
239
+ try {
240
+ const reason = err instanceof Error ? err.message : 'Execution failed';
241
+ updateCommitmentStatus(state.commitmentId, 'failed', reason);
242
+ } catch { /* ignore */ }
243
+ }
244
+ }
245
+ }
246
+
247
+ private async executeCommitment(state: ExecutionState): Promise<void> {
248
+ console.log(`[Executor] Executing: "${state.what}"`);
249
+
250
+ // Mark as active
251
+ try {
252
+ updateCommitmentStatus(state.commitmentId, 'active');
253
+ } catch { /* ignore */ }
254
+
255
+ // Build a mandatory execution prompt
256
+ const prompt = [
257
+ '[COMMITMENT EXECUTION — MANDATORY]',
258
+ '',
259
+ `You previously committed to: "${state.what}"`,
260
+ 'This commitment is now due. Execute it NOW using your tools.',
261
+ '',
262
+ 'Instructions:',
263
+ '1. Use your available tools (browser, terminal, file operations) to complete this task.',
264
+ '2. Be thorough — actually perform the work, don\'t just describe it.',
265
+ '3. After completion, summarize what you did.',
266
+ '4. If the task is impossible or unclear, explain why and suggest alternatives.',
267
+ '',
268
+ 'BEGIN EXECUTION.',
269
+ ].join('\n');
270
+
271
+ const response = await this.agentService!.handleMessage(prompt, 'system');
272
+
273
+ // Broadcast the execution result
274
+ this.broadcast?.({
275
+ type: 'chat',
276
+ payload: {
277
+ text: response ?? 'Task executed (no response).',
278
+ source: 'proactive',
279
+ },
280
+ priority: 'normal',
281
+ timestamp: Date.now(),
282
+ });
283
+
284
+ // Mark commitment as completed
285
+ const resultSummary = response
286
+ ? response.length > 500 ? response.slice(0, 497) + '...' : response
287
+ : 'Executed successfully';
288
+
289
+ try {
290
+ updateCommitmentStatus(state.commitmentId, 'completed', resultSummary);
291
+ } catch (err) {
292
+ console.error('[Executor] Failed to update commitment status:', err);
293
+ }
294
+
295
+ console.log(`[Executor] Completed: "${state.what}"`);
296
+ }
297
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Event Classifier — Priority Router
3
+ *
4
+ * Classifies ObserverEvents into priority levels using rule-based logic.
5
+ * No LLM calls — must be instant. Critical/high events trigger immediate
6
+ * reactions; normal/low events get batched for the next heartbeat.
7
+ */
8
+
9
+ import type { ObserverEvent } from '../observers/index.ts';
10
+ import { getDueCommitments, getUpcoming } from '../vault/commitments.ts';
11
+
12
+ export type EventPriority = 'critical' | 'high' | 'normal' | 'low';
13
+
14
+ export type ClassifiedEvent = {
15
+ event: ObserverEvent;
16
+ priority: EventPriority;
17
+ reason: string;
18
+ };
19
+
20
+ // Patterns that suggest high-intent clipboard content
21
+ const URL_PATTERN = /https?:\/\/\S+/i;
22
+ const EMAIL_PATTERN = /[\w.-]+@[\w.-]+\.\w{2,}/;
23
+ const PHONE_PATTERN = /(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/;
24
+
25
+ // Directories where file changes are low-priority noise
26
+ const LOW_PRIORITY_DIRS = ['/tmp', '/var/tmp', '/dev/shm', 'node_modules', '.git', '__pycache__', '.cache'];
27
+
28
+ /**
29
+ * Classify an observer event into a priority level.
30
+ * Rules are evaluated top-down; first match wins.
31
+ */
32
+ export function classifyEvent(event: ObserverEvent): ClassifiedEvent {
33
+ const { type, data } = event;
34
+
35
+ // --- Commitment-related events (injected by heartbeat checks) ---
36
+ if (type === 'commitment_overdue') {
37
+ return { event, priority: 'critical', reason: `Commitment overdue: ${data.what}` };
38
+ }
39
+
40
+ if (type === 'commitment_due_soon') {
41
+ return { event, priority: 'high', reason: `Commitment due within 15 min: ${data.what}` };
42
+ }
43
+
44
+ // --- Clipboard events ---
45
+ if (type === 'clipboard') {
46
+ const text = String(data.content ?? '');
47
+
48
+ if (text.length < 3) {
49
+ return { event, priority: 'low', reason: 'Clipboard: trivial content' };
50
+ }
51
+
52
+ if (URL_PATTERN.test(text)) {
53
+ return { event, priority: 'high', reason: 'Clipboard contains URL — possible intent to browse/research' };
54
+ }
55
+
56
+ if (EMAIL_PATTERN.test(text)) {
57
+ return { event, priority: 'high', reason: 'Clipboard contains email address — possible intent to contact' };
58
+ }
59
+
60
+ if (PHONE_PATTERN.test(text)) {
61
+ return { event, priority: 'high', reason: 'Clipboard contains phone number' };
62
+ }
63
+
64
+ if (text.length > 200) {
65
+ return { event, priority: 'normal', reason: 'Clipboard: substantial text copied' };
66
+ }
67
+
68
+ return { event, priority: 'low', reason: 'Clipboard: short text' };
69
+ }
70
+
71
+ // --- File change events ---
72
+ if (type === 'file_change') {
73
+ const filePath = String(data.path ?? '');
74
+ const changeType = String(data.changeType ?? data.type ?? '');
75
+
76
+ // Check if file is in a low-priority directory
77
+ if (LOW_PRIORITY_DIRS.some(dir => filePath.includes(dir))) {
78
+ return { event, priority: 'low', reason: `File change in noisy directory: ${filePath}` };
79
+ }
80
+
81
+ // Large file deletions are noteworthy
82
+ if (changeType === 'delete' || changeType === 'rename') {
83
+ return { event, priority: 'high', reason: `File ${changeType}: ${filePath}` };
84
+ }
85
+
86
+ return { event, priority: 'normal', reason: `File modified: ${filePath}` };
87
+ }
88
+
89
+ // --- Process events ---
90
+ if (type === 'process_started') {
91
+ const name = String(data.name ?? data.command ?? '');
92
+
93
+ // Interesting process launches
94
+ if (/chrome|firefox|code|slack|discord|telegram|zoom/i.test(name)) {
95
+ return { event, priority: 'normal', reason: `Notable app launched: ${name}` };
96
+ }
97
+
98
+ return { event, priority: 'low', reason: `Process started: ${name}` };
99
+ }
100
+
101
+ if (type === 'process_stopped') {
102
+ return { event, priority: 'low', reason: `Process stopped: ${data.name ?? data.pid}` };
103
+ }
104
+
105
+ // --- Notification events ---
106
+ if (type === 'notification') {
107
+ const urgency = String(data.urgency ?? '');
108
+
109
+ if (urgency === 'critical') {
110
+ return { event, priority: 'critical', reason: `System notification (critical): ${data.summary}` };
111
+ }
112
+
113
+ return { event, priority: 'normal', reason: `System notification: ${data.summary}` };
114
+ }
115
+
116
+ // --- Calendar / Email events ---
117
+ if (type === 'calendar') {
118
+ return { event, priority: 'high', reason: `Calendar event: ${data.summary ?? data.title}` };
119
+ }
120
+
121
+ if (type === 'email') {
122
+ const subject = String(data.subject ?? '');
123
+ const labels = Array.isArray(data.labels) ? data.labels as string[] : [];
124
+
125
+ // IMPORTANT/STARRED labels → high priority
126
+ if (labels.includes('IMPORTANT') || labels.includes('STARRED')) {
127
+ return { event, priority: 'high', reason: `Important email: ${subject}` };
128
+ }
129
+
130
+ // Urgent keywords in subject
131
+ const urgentKeywords = /\b(urgent|asap|critical|emergency|action required|immediate|deadline)\b/i;
132
+ if (urgentKeywords.test(subject)) {
133
+ return { event, priority: 'high', reason: `Urgent email: ${subject}` };
134
+ }
135
+
136
+ return { event, priority: 'normal', reason: `New email: ${subject || 'no subject'}` };
137
+ }
138
+
139
+ // --- Awareness events (M13) ---
140
+ if (type === 'error_detected') {
141
+ return { event, priority: 'high', reason: `Screen error detected: ${data.errorText} in ${data.appName}` };
142
+ }
143
+
144
+ if (type === 'struggle_detected') {
145
+ const score = data.compositeScore as number;
146
+ const priority = score >= 0.7 ? 'high' : 'normal';
147
+ return { event, priority, reason: `User struggling in ${data.appName} (score: ${score?.toFixed(2)}, ${data.appCategory})` };
148
+ }
149
+
150
+ if (type === 'stuck_detected') {
151
+ return { event, priority: 'normal', reason: `User appears stuck in ${data.appName} (${Math.round((data.durationMs as number) / 1000)}s)` };
152
+ }
153
+
154
+ if (type === 'context_changed') {
155
+ return { event, priority: 'low', reason: `Switched from ${data.fromApp} to ${data.toApp}` };
156
+ }
157
+
158
+ if (type === 'session_started' || type === 'session_ended') {
159
+ return { event, priority: 'low', reason: `Activity session ${type === 'session_started' ? 'started' : 'ended'}` };
160
+ }
161
+
162
+ if (type === 'suggestion_ready') {
163
+ return { event, priority: 'normal', reason: `Awareness suggestion: ${data.title}` };
164
+ }
165
+
166
+ if (type === 'screen_capture') {
167
+ return { event, priority: 'low', reason: `Screen captured (${Math.round((data.pixelChangePct as number) * 100)}% change)` };
168
+ }
169
+
170
+ // --- Sidecar events ---
171
+ if (type === 'sidecar_register') {
172
+ return { event, priority: 'normal', reason: `Sidecar registered: ${data.name ?? data.sidecar_id}` };
173
+ }
174
+
175
+ if (type === 'sidecar_disconnect') {
176
+ return { event, priority: 'normal', reason: `Sidecar disconnected: ${data.name ?? data.sidecar_id}` };
177
+ }
178
+
179
+ if (type === 'sidecar_rpc_error') {
180
+ return { event, priority: 'high', reason: `Sidecar RPC error: ${data.error ?? data.method} on ${data.name ?? data.sidecar_id}` };
181
+ }
182
+
183
+ if (type === 'sidecar_rpc_complete') {
184
+ return { event, priority: 'low', reason: `Sidecar RPC complete: ${data.method} on ${data.name ?? data.sidecar_id}` };
185
+ }
186
+
187
+ if (type.startsWith('sidecar_')) {
188
+ return { event, priority: 'normal', reason: `Sidecar event: ${type}` };
189
+ }
190
+
191
+ // --- Default ---
192
+ return { event, priority: 'low', reason: `Unclassified event: ${type}` };
193
+ }
194
+
195
+ /**
196
+ * Check commitments and generate synthetic events for due/overdue items.
197
+ * Called periodically (e.g., every heartbeat) to inject commitment-awareness.
198
+ */
199
+ export function checkCommitments(): ClassifiedEvent[] {
200
+ const events: ClassifiedEvent[] = [];
201
+
202
+ try {
203
+ // Check overdue commitments
204
+ const overdue = getDueCommitments();
205
+ for (const c of overdue) {
206
+ events.push({
207
+ event: {
208
+ type: 'commitment_overdue',
209
+ data: { id: c.id, what: c.what, when_due: c.when_due, priority: c.priority },
210
+ timestamp: Date.now(),
211
+ },
212
+ priority: 'critical',
213
+ reason: `Commitment overdue: ${c.what}`,
214
+ });
215
+ }
216
+
217
+ // Check commitments due within 15 minutes
218
+ const upcoming = getUpcoming(10);
219
+ const fifteenMinFromNow = Date.now() + 15 * 60 * 1000;
220
+
221
+ for (const c of upcoming) {
222
+ if (c.when_due && c.when_due <= fifteenMinFromNow && c.when_due > Date.now()) {
223
+ events.push({
224
+ event: {
225
+ type: 'commitment_due_soon',
226
+ data: { id: c.id, what: c.what, when_due: c.when_due, priority: c.priority },
227
+ timestamp: Date.now(),
228
+ },
229
+ priority: 'high',
230
+ reason: `Commitment due within 15 min: ${c.what}`,
231
+ });
232
+ }
233
+ }
234
+ } catch (err) {
235
+ console.error('[EventClassifier] Error checking commitments:', err);
236
+ }
237
+
238
+ return events;
239
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Event Coalescer — Batch Buffer
3
+ *
4
+ * Collects normal/low priority events in memory and flushes them
5
+ * as a formatted summary string at heartbeat time. Groups events
6
+ * by type for clean LLM consumption.
7
+ */
8
+
9
+ import type { ClassifiedEvent, EventPriority } from './event-classifier.ts';
10
+
11
+ const MAX_BUFFER_SIZE = 100;
12
+
13
+ export class EventCoalescer {
14
+ private buffer: ClassifiedEvent[] = [];
15
+
16
+ /**
17
+ * Add a classified event to the buffer.
18
+ * Oldest events are dropped if buffer exceeds max size.
19
+ */
20
+ addEvent(event: ClassifiedEvent): void {
21
+ this.buffer.push(event);
22
+
23
+ if (this.buffer.length > MAX_BUFFER_SIZE) {
24
+ // Drop oldest events
25
+ const overflow = this.buffer.length - MAX_BUFFER_SIZE;
26
+ this.buffer.splice(0, overflow);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get the current buffer size.
32
+ */
33
+ get size(): number {
34
+ return this.buffer.length;
35
+ }
36
+
37
+ /**
38
+ * Flush the buffer and return a formatted summary string.
39
+ * Returns empty string if no events buffered.
40
+ */
41
+ flush(): string {
42
+ if (this.buffer.length === 0) {
43
+ return '';
44
+ }
45
+
46
+ const events = [...this.buffer];
47
+ this.buffer = [];
48
+
49
+ // Group by event type
50
+ const groups = new Map<string, ClassifiedEvent[]>();
51
+ for (const evt of events) {
52
+ const type = evt.event.type;
53
+ if (!groups.has(type)) {
54
+ groups.set(type, []);
55
+ }
56
+ groups.get(type)!.push(evt);
57
+ }
58
+
59
+ // Format each group
60
+ const lines: string[] = [];
61
+ lines.push(`## Recent Activity (${events.length} events since last check)`);
62
+ lines.push('');
63
+
64
+ for (const [type, groupEvents] of groups) {
65
+ const label = formatEventType(type);
66
+ lines.push(`**${label}** (${groupEvents.length}):`);
67
+
68
+ // Show up to 5 details per group, summarize the rest
69
+ const shown = groupEvents.slice(0, 5);
70
+ for (const evt of shown) {
71
+ lines.push(` - ${evt.reason}`);
72
+ }
73
+
74
+ if (groupEvents.length > 5) {
75
+ lines.push(` - ... and ${groupEvents.length - 5} more`);
76
+ }
77
+
78
+ lines.push('');
79
+ }
80
+
81
+ return lines.join('\n');
82
+ }
83
+
84
+ /**
85
+ * Peek at the buffer without clearing it.
86
+ */
87
+ peek(): ClassifiedEvent[] {
88
+ return [...this.buffer];
89
+ }
90
+
91
+ /**
92
+ * Clear the buffer without returning anything.
93
+ */
94
+ clear(): void {
95
+ this.buffer = [];
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Human-readable label for event types.
101
+ */
102
+ function formatEventType(type: string): string {
103
+ switch (type) {
104
+ case 'file_change': return 'File Changes';
105
+ case 'clipboard': return 'Clipboard Activity';
106
+ case 'process_started': return 'Apps Launched';
107
+ case 'process_stopped': return 'Apps Closed';
108
+ case 'notification': return 'System Notifications';
109
+ case 'calendar': return 'Calendar Events';
110
+ case 'email': return 'Emails';
111
+ case 'browser': return 'Browser Activity';
112
+ case 'commitment_overdue': return 'Overdue Commitments';
113
+ case 'commitment_due_soon': return 'Upcoming Commitments';
114
+ case 'screen_capture': return 'Screen Captures';
115
+ case 'context_changed': return 'Context Switches';
116
+ case 'error_detected': return 'Screen Errors';
117
+ case 'stuck_detected': return 'Stuck Detection';
118
+ case 'session_started': return 'Sessions Started';
119
+ case 'session_ended': return 'Sessions Ended';
120
+ case 'suggestion_ready': return 'Awareness Suggestions';
121
+ default: return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
122
+ }
123
+ }