@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,283 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from 'bun:test';
2
+ import { join } from 'node:path';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
5
+ import {
6
+ acquireLock,
7
+ isLocked,
8
+ releaseLock,
9
+ readPid,
10
+ getPidPath,
11
+ getLogPath,
12
+ getLogDir,
13
+ } from './pid.ts';
14
+
15
+ const JARVIS_DIR = join(homedir(), '.jarvis');
16
+ const LOCK_PATH = join(JARVIS_DIR, 'jarvis.pid');
17
+ const PID_MODULE = join(import.meta.dir, 'pid.ts');
18
+ const READY_SIGNAL = join(tmpdir(), 'jarvis-test-lock-ready');
19
+ const HOLDER_SCRIPT = join(tmpdir(), 'jarvis-test-lock-holder.ts');
20
+
21
+ function cleanup(): void {
22
+ releaseLock();
23
+ try { unlinkSync(READY_SIGNAL); } catch {}
24
+ try { unlinkSync(HOLDER_SCRIPT); } catch {}
25
+ }
26
+
27
+ /**
28
+ * Spawn a child process that acquires the flock and holds it until killed.
29
+ * Returns once the child has confirmed it holds the lock.
30
+ */
31
+ async function spawnLockHolder(): Promise<{ proc: ReturnType<typeof Bun.spawn>; pid: number }> {
32
+ try { unlinkSync(READY_SIGNAL); } catch {}
33
+
34
+ writeFileSync(HOLDER_SCRIPT, `
35
+ import { acquireLock } from ${JSON.stringify(PID_MODULE)};
36
+ import { writeFileSync } from 'node:fs';
37
+ const ok = acquireLock(process.pid);
38
+ writeFileSync(${JSON.stringify(READY_SIGNAL)}, ok ? String(process.pid) : 'FAIL');
39
+ await Bun.sleep(60000);
40
+ `);
41
+
42
+ const proc = Bun.spawn(['bun', HOLDER_SCRIPT], {
43
+ stdio: ['ignore', 'ignore', 'ignore'],
44
+ });
45
+
46
+ for (let i = 0; i < 50; i++) {
47
+ await Bun.sleep(100);
48
+ if (existsSync(READY_SIGNAL)) {
49
+ const content = readFileSync(READY_SIGNAL, 'utf-8').trim();
50
+ if (content === 'FAIL') {
51
+ proc.kill();
52
+ await proc.exited;
53
+ throw new Error('Child process failed to acquire lock');
54
+ }
55
+ return { proc, pid: parseInt(content, 10) };
56
+ }
57
+ }
58
+ proc.kill();
59
+ await proc.exited;
60
+ throw new Error('Timed out waiting for child to acquire lock');
61
+ }
62
+
63
+ describe('Process Lock Manager', () => {
64
+ beforeEach(() => cleanup());
65
+ afterEach(() => cleanup());
66
+
67
+ // ── acquireLock ──────────────────────────────────────────────────
68
+
69
+ describe('acquireLock', () => {
70
+ test('returns true and creates lock file', () => {
71
+ expect(acquireLock(process.pid)).toBe(true);
72
+ expect(existsSync(LOCK_PATH)).toBe(true);
73
+ });
74
+
75
+ test('writes PID to lock file', () => {
76
+ acquireLock(process.pid);
77
+ const content = readFileSync(LOCK_PATH, 'utf-8').trim();
78
+ expect(content).toBe(String(process.pid));
79
+ });
80
+
81
+ test('creates ~/.jarvis dir if missing', () => {
82
+ // Dir almost certainly exists already, but acquireLock should not fail
83
+ expect(acquireLock(process.pid)).toBe(true);
84
+ });
85
+
86
+ test('second acquire in same process returns false', () => {
87
+ // First fd holds an exclusive flock; second open+flock is denied
88
+ expect(acquireLock(process.pid)).toBe(true);
89
+ expect(acquireLock(process.pid)).toBe(false);
90
+ });
91
+ });
92
+
93
+ // ── isLocked ─────────────────────────────────────────────────────
94
+
95
+ describe('isLocked', () => {
96
+ test('returns null when no lock file exists', () => {
97
+ expect(isLocked()).toBeNull();
98
+ });
99
+
100
+ test('returns null for stale file (file exists, no lock held)', () => {
101
+ mkdirSync(JARVIS_DIR, { recursive: true });
102
+ writeFileSync(LOCK_PATH, '99999');
103
+ // No flock held — probe should succeed → not locked
104
+ expect(isLocked()).toBeNull();
105
+ });
106
+
107
+ test('returns PID when lock is held by this process', () => {
108
+ acquireLock(process.pid);
109
+ // isLocked opens a second fd; flock is denied because our fd holds it
110
+ expect(isLocked()).toBe(process.pid);
111
+ });
112
+ });
113
+
114
+ // ── releaseLock ──────────────────────────────────────────────────
115
+
116
+ describe('releaseLock', () => {
117
+ test('releases lock and removes file', () => {
118
+ acquireLock(process.pid);
119
+ expect(existsSync(LOCK_PATH)).toBe(true);
120
+
121
+ releaseLock();
122
+ expect(existsSync(LOCK_PATH)).toBe(false);
123
+ expect(isLocked()).toBeNull();
124
+ });
125
+
126
+ test('is idempotent — safe to call without prior acquire', () => {
127
+ releaseLock();
128
+ releaseLock();
129
+ releaseLock();
130
+ // Should not throw
131
+ });
132
+
133
+ test('allows re-acquire after release', () => {
134
+ expect(acquireLock(process.pid)).toBe(true);
135
+ releaseLock();
136
+ expect(acquireLock(process.pid)).toBe(true);
137
+ });
138
+
139
+ test('multiple acquire/release cycles work', () => {
140
+ for (let i = 0; i < 5; i++) {
141
+ expect(acquireLock(process.pid)).toBe(true);
142
+ expect(isLocked()).toBe(process.pid);
143
+ releaseLock();
144
+ expect(isLocked()).toBeNull();
145
+ }
146
+ });
147
+ });
148
+
149
+ // ── readPid ──────────────────────────────────────────────────────
150
+
151
+ describe('readPid', () => {
152
+ test('returns null when no file exists', () => {
153
+ expect(readPid()).toBeNull();
154
+ });
155
+
156
+ test('returns PID from file', () => {
157
+ mkdirSync(JARVIS_DIR, { recursive: true });
158
+ writeFileSync(LOCK_PATH, '12345');
159
+ expect(readPid()).toBe(12345);
160
+ });
161
+
162
+ test('trims whitespace', () => {
163
+ mkdirSync(JARVIS_DIR, { recursive: true });
164
+ writeFileSync(LOCK_PATH, ' 42\n');
165
+ expect(readPid()).toBe(42);
166
+ });
167
+
168
+ test('returns null for non-numeric content', () => {
169
+ mkdirSync(JARVIS_DIR, { recursive: true });
170
+ writeFileSync(LOCK_PATH, 'not-a-pid');
171
+ expect(readPid()).toBeNull();
172
+ });
173
+
174
+ test('returns null for empty file', () => {
175
+ mkdirSync(JARVIS_DIR, { recursive: true });
176
+ writeFileSync(LOCK_PATH, '');
177
+ expect(readPid()).toBeNull();
178
+ });
179
+
180
+ test('returns null for zero', () => {
181
+ mkdirSync(JARVIS_DIR, { recursive: true });
182
+ writeFileSync(LOCK_PATH, '0');
183
+ expect(readPid()).toBeNull();
184
+ });
185
+
186
+ test('returns null for negative PID', () => {
187
+ mkdirSync(JARVIS_DIR, { recursive: true });
188
+ writeFileSync(LOCK_PATH, '-1');
189
+ expect(readPid()).toBeNull();
190
+ });
191
+ });
192
+
193
+ // ── path getters ─────────────────────────────────────────────────
194
+
195
+ describe('path getters', () => {
196
+ test('getPidPath returns ~/.jarvis/jarvis.pid', () => {
197
+ expect(getPidPath()).toBe(join(homedir(), '.jarvis', 'jarvis.pid'));
198
+ });
199
+
200
+ test('getLogPath returns path and creates logs dir', () => {
201
+ const logPath = getLogPath();
202
+ expect(logPath).toBe(join(JARVIS_DIR, 'logs', 'jarvis.log'));
203
+ expect(existsSync(join(JARVIS_DIR, 'logs'))).toBe(true);
204
+ });
205
+
206
+ test('getLogDir returns logs directory', () => {
207
+ expect(getLogDir()).toBe(join(JARVIS_DIR, 'logs'));
208
+ });
209
+ });
210
+
211
+ // ── cross-process locking ────────────────────────────────────────
212
+
213
+ describe('cross-process locking', () => {
214
+ let childProc: ReturnType<typeof Bun.spawn> | null = null;
215
+
216
+ afterEach(async () => {
217
+ if (childProc) {
218
+ childProc.kill();
219
+ await childProc.exited;
220
+ childProc = null;
221
+ }
222
+ cleanup();
223
+ });
224
+
225
+ test('isLocked detects lock held by another process', async () => {
226
+ const { proc, pid } = await spawnLockHolder();
227
+ childProc = proc;
228
+
229
+ const result = isLocked();
230
+ expect(result).toBe(pid);
231
+ }, { timeout: 15000 });
232
+
233
+ test('acquireLock fails when another process holds lock', async () => {
234
+ const { proc } = await spawnLockHolder();
235
+ childProc = proc;
236
+
237
+ expect(acquireLock(process.pid)).toBe(false);
238
+ }, { timeout: 15000 });
239
+
240
+ test('lock is released when holder is SIGKILLed', async () => {
241
+ const { proc } = await spawnLockHolder();
242
+ childProc = proc;
243
+
244
+ // Lock is held
245
+ expect(isLocked()).not.toBeNull();
246
+
247
+ // SIGKILL — OS closes all fds, releasing the flock
248
+ proc.kill(9);
249
+ await proc.exited;
250
+ childProc = null;
251
+
252
+ // Lock is now free
253
+ expect(isLocked()).toBeNull();
254
+ }, { timeout: 15000 });
255
+
256
+ test('can acquire lock after previous holder crashes', async () => {
257
+ const { proc } = await spawnLockHolder();
258
+ childProc = proc;
259
+
260
+ proc.kill(9);
261
+ await proc.exited;
262
+ childProc = null;
263
+
264
+ // New instance should succeed immediately
265
+ expect(acquireLock(process.pid)).toBe(true);
266
+ expect(readPid()).toBe(process.pid);
267
+ }, { timeout: 15000 });
268
+
269
+ test('lock survives SIGTERM of holder (graceful)', async () => {
270
+ const { proc, pid } = await spawnLockHolder();
271
+ childProc = proc;
272
+
273
+ // SIGTERM — child exits, OS releases flock
274
+ proc.kill(15);
275
+ await proc.exited;
276
+ childProc = null;
277
+
278
+ // Lock freed after process exits
279
+ expect(isLocked()).toBeNull();
280
+ expect(acquireLock(process.pid)).toBe(true);
281
+ }, { timeout: 15000 });
282
+ });
283
+ });
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Process Lock Manager for J.A.R.V.I.S. Daemon
3
+ *
4
+ * Uses flock()-based advisory locks to prevent duplicate daemon instances.
5
+ * Unlike PID-based checks, flock locks are automatically released by the OS
6
+ * when the process dies (even on SIGKILL, OOM, or crash), making this
7
+ * container-safe and race-free.
8
+ */
9
+
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import {
13
+ constants,
14
+ existsSync,
15
+ readFileSync,
16
+ unlinkSync,
17
+ mkdirSync,
18
+ openSync,
19
+ closeSync,
20
+ writeSync,
21
+ ftruncateSync,
22
+ } from 'node:fs';
23
+ import { cc } from 'bun:ffi';
24
+ import flockSource from './flock.c' with { type: 'file' };
25
+
26
+ const JARVIS_DIR = join(homedir(), '.jarvis');
27
+ const LOG_DIR = join(JARVIS_DIR, 'logs');
28
+ const LOCK_PATH = join(JARVIS_DIR, 'jarvis.pid');
29
+ const LOG_PATH = join(LOG_DIR, 'jarvis.log');
30
+
31
+ // ── flock() via Bun cc() ────────────────────────────────────────────
32
+ // Compiled at startup by Bun's embedded TinyCC. Resolves libc via
33
+ // system headers — works on Linux, macOS, and any POSIX platform
34
+ // without hardcoding a shared library path.
35
+
36
+ const isWindows = process.platform === 'win32';
37
+
38
+ let flock: any = null;
39
+ if (!isWindows) {
40
+ const lib = cc({
41
+ source: flockSource,
42
+ symbols: {
43
+ do_flock: { args: ['i32', 'i32'], returns: 'i32' },
44
+ },
45
+ });
46
+ flock = lib.symbols;
47
+ }
48
+
49
+ const LOCK_EX = 2; // Exclusive lock
50
+ const LOCK_NB = 4; // Non-blocking
51
+ const LOCK_UN = 8; // Unlock
52
+
53
+ // ── Lock state ───────────────────────────────────────────────────────
54
+
55
+ // The open FD that holds the flock — kept alive for the process lifetime.
56
+ // When the process exits (normally, SIGKILL, OOM, crash), the OS closes it
57
+ // and the advisory lock is automatically released.
58
+ let lockFd: number | null = null;
59
+
60
+ // ── Public API ───────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Acquire an exclusive lock on the lock file and write the PID.
64
+ * Returns true if the lock was acquired, false if another instance holds it.
65
+ */
66
+ export function acquireLock(pid: number): boolean {
67
+ if (lockFd !== null) return false;
68
+
69
+ try {
70
+ mkdirSync(JARVIS_DIR, { recursive: true });
71
+
72
+ if (isWindows) {
73
+ const existingPid = readPid();
74
+ if (existingPid !== null && existingPid !== pid) {
75
+ try {
76
+ process.kill(existingPid, 0);
77
+ return false;
78
+ } catch {
79
+ // Process not running, stale lock file
80
+ }
81
+ }
82
+ }
83
+
84
+ // Open (or create) the lock file — don't truncate before locking
85
+ const fd = openSync(LOCK_PATH, constants.O_WRONLY | constants.O_CREAT, 0o644);
86
+
87
+ // Try non-blocking exclusive lock
88
+ if (!isWindows) {
89
+ const result = flock.do_flock(fd, LOCK_EX | LOCK_NB);
90
+ if (result !== 0) {
91
+ closeSync(fd);
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Lock acquired — truncate and write PID for display purposes
97
+ ftruncateSync(fd, 0);
98
+ writeSync(fd, String(pid));
99
+
100
+ // Keep the FD open — closing it would release the lock
101
+ lockFd = fd;
102
+ return true;
103
+ } catch (err) {
104
+ console.error(`[PID] Failed to acquire lock: ${err}`);
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Check if the daemon lock is currently held.
111
+ * Returns the PID if locked (daemon running), null otherwise.
112
+ */
113
+ export function isLocked(): number | null {
114
+ if (!existsSync(LOCK_PATH)) return null;
115
+
116
+ let fd: number;
117
+ try {
118
+ fd = openSync(LOCK_PATH, constants.O_RDONLY);
119
+ } catch {
120
+ return null;
121
+ }
122
+
123
+ try {
124
+ if (!isWindows) {
125
+ // Try non-blocking exclusive lock to probe
126
+ const result = flock.do_flock(fd, LOCK_EX | LOCK_NB);
127
+ if (result === 0) {
128
+ // Lock acquired — no daemon running. Release immediately.
129
+ flock.do_flock(fd, LOCK_UN);
130
+ closeSync(fd);
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // Lock held by another process (or we are on Windows) — daemon is running
136
+ closeSync(fd);
137
+ const pid = readPid();
138
+
139
+ if (isWindows && pid !== null) {
140
+ try {
141
+ process.kill(pid, 0);
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // Container safety: if PID is 1 and we're inside a container,
148
+ // the lock file is stale from a previous container lifecycle
149
+ if (pid === 1 && isInsideContainer()) {
150
+ releaseLock();
151
+ return null;
152
+ }
153
+
154
+ return pid;
155
+ } catch {
156
+ try { closeSync(fd); } catch { /* already closed */ }
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Release the lock (close the FD) and remove the lock file.
163
+ */
164
+ export function releaseLock(): void {
165
+ if (lockFd !== null) {
166
+ try {
167
+ closeSync(lockFd);
168
+ } catch {
169
+ // Already closed
170
+ }
171
+ lockFd = null;
172
+ }
173
+ try {
174
+ if (existsSync(LOCK_PATH)) unlinkSync(LOCK_PATH);
175
+ } catch { /* ignore */ }
176
+ }
177
+
178
+ /**
179
+ * Read the PID from the lock file. Returns null if no file or invalid content.
180
+ */
181
+ export function readPid(): number | null {
182
+ if (!existsSync(LOCK_PATH)) return null;
183
+ try {
184
+ const content = readFileSync(LOCK_PATH, 'utf-8').trim();
185
+ const pid = parseInt(content, 10);
186
+ if (isNaN(pid) || pid <= 0) return null;
187
+ return pid;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get the lock file path (for display purposes).
195
+ */
196
+ export function getPidPath(): string {
197
+ return LOCK_PATH;
198
+ }
199
+
200
+ /**
201
+ * Get the log file path. Creates the log directory if needed.
202
+ */
203
+ export function getLogPath(): string {
204
+ mkdirSync(LOG_DIR, { recursive: true });
205
+ return LOG_PATH;
206
+ }
207
+
208
+ /**
209
+ * Get the log directory path.
210
+ */
211
+ export function getLogDir(): string {
212
+ return LOG_DIR;
213
+ }
214
+
215
+ // ── Helpers ──────────────────────────────────────────────────────────
216
+
217
+ function isInsideContainer(): boolean {
218
+ if (existsSync('/.dockerenv')) return true;
219
+ try {
220
+ return readFileSync('/proc/1/cgroup', 'utf-8').includes('docker');
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * ResearchQueue — Background Research Engine
3
+ *
4
+ * Manages a queue of topics for JARVIS to research during idle time.
5
+ * Topics come from conversations, explicit requests, or the agent itself.
6
+ * The heartbeat picks up the next topic when nothing urgent is happening.
7
+ */
8
+
9
+ export type ResearchPriority = 'high' | 'normal' | 'low';
10
+ export type ResearchStatus = 'queued' | 'in_progress' | 'completed' | 'failed';
11
+
12
+ export type ResearchTopic = {
13
+ id: string;
14
+ topic: string;
15
+ reason: string;
16
+ source: string; // 'user' | 'agent' | 'conversation'
17
+ priority: ResearchPriority;
18
+ status: ResearchStatus;
19
+ result?: string;
20
+ failReason?: string;
21
+ created_at: number;
22
+ completed_at?: number;
23
+ };
24
+
25
+ const MAX_QUEUE_SIZE = 50;
26
+
27
+ export class ResearchQueue {
28
+ private topics: Map<string, ResearchTopic> = new Map();
29
+
30
+ /**
31
+ * Add a topic to the research queue.
32
+ */
33
+ addTopic(
34
+ topic: string,
35
+ reason: string,
36
+ source: string = 'user',
37
+ priority: ResearchPriority = 'normal'
38
+ ): ResearchTopic {
39
+ // Enforce max queue size (drop oldest low-priority)
40
+ if (this.topics.size >= MAX_QUEUE_SIZE) {
41
+ this.evictOldest();
42
+ }
43
+
44
+ const entry: ResearchTopic = {
45
+ id: crypto.randomUUID(),
46
+ topic,
47
+ reason,
48
+ source,
49
+ priority,
50
+ status: 'queued',
51
+ created_at: Date.now(),
52
+ };
53
+
54
+ this.topics.set(entry.id, entry);
55
+ console.log(`[ResearchQueue] Added: "${topic}" (${priority}, from ${source})`);
56
+ return entry;
57
+ }
58
+
59
+ /**
60
+ * Get the next highest-priority queued topic.
61
+ */
62
+ getNext(): ResearchTopic | null {
63
+ const queued = Array.from(this.topics.values())
64
+ .filter((t) => t.status === 'queued')
65
+ .sort((a, b) => {
66
+ // Priority order: high > normal > low
67
+ const pOrder: Record<ResearchPriority, number> = { high: 0, normal: 1, low: 2 };
68
+ const pDiff = pOrder[a.priority] - pOrder[b.priority];
69
+ if (pDiff !== 0) return pDiff;
70
+ // Older topics first within same priority
71
+ return a.created_at - b.created_at;
72
+ });
73
+
74
+ return queued[0] ?? null;
75
+ }
76
+
77
+ /**
78
+ * Mark a topic as in-progress.
79
+ */
80
+ startResearch(id: string): boolean {
81
+ const topic = this.topics.get(id);
82
+ if (!topic || topic.status !== 'queued') return false;
83
+ topic.status = 'in_progress';
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * Mark a topic as completed with a result.
89
+ */
90
+ complete(id: string, result: string): boolean {
91
+ const topic = this.topics.get(id);
92
+ if (!topic) return false;
93
+ topic.status = 'completed';
94
+ topic.result = result;
95
+ topic.completed_at = Date.now();
96
+ console.log(`[ResearchQueue] Completed: "${topic.topic}"`);
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Mark a topic as failed.
102
+ */
103
+ fail(id: string, reason: string): boolean {
104
+ const topic = this.topics.get(id);
105
+ if (!topic) return false;
106
+ topic.status = 'failed';
107
+ topic.failReason = reason;
108
+ topic.completed_at = Date.now();
109
+ console.log(`[ResearchQueue] Failed: "${topic.topic}" — ${reason}`);
110
+ return true;
111
+ }
112
+
113
+ /**
114
+ * Remove a topic from the queue.
115
+ */
116
+ remove(id: string): boolean {
117
+ return this.topics.delete(id);
118
+ }
119
+
120
+ /**
121
+ * List all topics, optionally filtered by status.
122
+ */
123
+ list(status?: ResearchStatus): ResearchTopic[] {
124
+ const all = Array.from(this.topics.values());
125
+ if (status) return all.filter((t) => t.status === status);
126
+ return all;
127
+ }
128
+
129
+ /**
130
+ * Get count of queued topics.
131
+ */
132
+ queuedCount(): number {
133
+ return Array.from(this.topics.values()).filter((t) => t.status === 'queued').length;
134
+ }
135
+
136
+ private evictOldest(): void {
137
+ // Evict oldest completed, then oldest low-priority queued
138
+ const completed = Array.from(this.topics.values())
139
+ .filter((t) => t.status === 'completed' || t.status === 'failed')
140
+ .sort((a, b) => a.created_at - b.created_at);
141
+
142
+ if (completed.length > 0) {
143
+ this.topics.delete(completed[0]!.id);
144
+ return;
145
+ }
146
+
147
+ const low = Array.from(this.topics.values())
148
+ .filter((t) => t.priority === 'low' && t.status === 'queued')
149
+ .sort((a, b) => a.created_at - b.created_at);
150
+
151
+ if (low.length > 0) {
152
+ this.topics.delete(low[0]!.id);
153
+ }
154
+ }
155
+ }