@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,38 @@
1
+ /**
2
+ * Key-value settings store backed by SQLite.
3
+ *
4
+ * Used for persistent configuration that can be edited from the dashboard
5
+ * (e.g., LLM provider/model preferences).
6
+ */
7
+
8
+ import { getDb } from './schema.ts';
9
+
10
+ export function getSetting(key: string): string | null {
11
+ const db = getDb();
12
+ const row = db.query('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | null;
13
+ return row?.value ?? null;
14
+ }
15
+
16
+ export function setSetting(key: string, value: string): void {
17
+ const db = getDb();
18
+ db.run(
19
+ `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, unixepoch())
20
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
21
+ [key, value],
22
+ );
23
+ }
24
+
25
+ export function deleteSetting(key: string): void {
26
+ const db = getDb();
27
+ db.run('DELETE FROM settings WHERE key = ?', [key]);
28
+ }
29
+
30
+ export function getSettingsByPrefix(prefix: string): Record<string, string> {
31
+ const db = getDb();
32
+ const rows = db.query('SELECT key, value FROM settings WHERE key LIKE ?').all(`${prefix}%`) as Array<{ key: string; value: string }>;
33
+ const result: Record<string, string> = {};
34
+ for (const row of rows) {
35
+ result[row.key] = row.value;
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,113 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { closeDb, initDatabase } from './schema.ts';
3
+ import { clearUserProfile, getUserProfile, saveUserProfile } from './user-profile.ts';
4
+ import { findEntities } from './entities.ts';
5
+ import { findFacts } from './facts.ts';
6
+ import { countAnsweredUserProfileQuestions, formatUserProfileForPrompt } from '../user/profile.ts';
7
+
8
+ describe('Vault — User Profile', () => {
9
+ afterEach(() => {
10
+ closeDb();
11
+ });
12
+
13
+ test('save + load persists normalized answers', () => {
14
+ initDatabase(':memory:');
15
+
16
+ const saved = saveUserProfile({
17
+ preferred_name: ' Alex ',
18
+ interests: 'AI, cars',
19
+ empty_field: '',
20
+ });
21
+
22
+ expect(saved.answers.preferred_name).toBe('Alex');
23
+ expect(saved.answers.interests).toBe('AI, cars');
24
+ expect(saved.completed_at).toBeNumber();
25
+
26
+ const loaded = getUserProfile();
27
+ expect(loaded?.answers.preferred_name).toBe('Alex');
28
+ expect(loaded?.answers.interests).toBe('AI, cars');
29
+ });
30
+
31
+ test('clear removes saved profile', () => {
32
+ initDatabase(':memory:');
33
+
34
+ saveUserProfile({ preferred_name: 'Alex' });
35
+ clearUserProfile();
36
+
37
+ expect(getUserProfile()).toBeNull();
38
+ expect(findEntities({ name: 'Alex' })).toHaveLength(0);
39
+ expect(findFacts({ predicate: 'preferred_name' })).toHaveLength(0);
40
+ });
41
+
42
+ test('save syncs profile answers into the vault knowledge base', () => {
43
+ initDatabase(':memory:');
44
+
45
+ saveUserProfile({
46
+ preferred_name: 'Alex',
47
+ interests: 'AI, cars',
48
+ location_timezone: 'Miami / America/New_York',
49
+ });
50
+
51
+ const entities = findEntities({ name: 'Alex' });
52
+ expect(entities).toHaveLength(1);
53
+ expect(entities[0]!.source).toBe('user_profile');
54
+
55
+ const facts = findFacts({ subject_id: entities[0]!.id });
56
+ const factMap = new Map(facts.map((fact) => [fact.predicate, fact.object]));
57
+ expect(factMap.get('preferred_name')).toBe('Alex');
58
+ expect(factMap.get('interests')).toBe('AI, cars');
59
+ expect(factMap.get('location_timezone')).toBe('Miami / America/New_York');
60
+ expect(factMap.get('name')).toBe('Alex');
61
+ });
62
+
63
+ test('save derives alias facts from free-form profile answers', () => {
64
+ initDatabase(':memory:');
65
+
66
+ saveUserProfile({
67
+ preferred_name: 'Sebastian',
68
+ important_people: 'im Sebastian but my common alias/username is Crayon.',
69
+ });
70
+
71
+ const entities = findEntities({ name: 'Sebastian' });
72
+ expect(entities).toHaveLength(1);
73
+
74
+ const facts = findFacts({ subject_id: entities[0]!.id });
75
+ const aliases = facts
76
+ .filter((fact) => fact.predicate === 'alias' || fact.predicate === 'username')
77
+ .map((fact) => fact.object);
78
+
79
+ expect(aliases).toContain('Crayon');
80
+ });
81
+
82
+ test('prompt formatter includes answered fields only', () => {
83
+ initDatabase(':memory:');
84
+
85
+ const profile = saveUserProfile({
86
+ preferred_name: 'Alex',
87
+ communication_preferences: 'Be direct and concise.',
88
+ });
89
+
90
+ expect(countAnsweredUserProfileQuestions(profile)).toBe(2);
91
+
92
+ const prompt = formatUserProfileForPrompt(profile);
93
+ expect(prompt).toContain('Preferred Name: |');
94
+ expect(prompt).toContain(' Alex');
95
+ expect(prompt).toContain('Communication Preferences: |');
96
+ expect(prompt).toContain(' Be direct and concise.');
97
+ expect(prompt).not.toContain('Pronouns');
98
+ });
99
+
100
+ test('prompt formatter indents multiline answers to keep them contained', () => {
101
+ initDatabase(':memory:');
102
+
103
+ const profile = saveUserProfile({
104
+ anything_else: 'Line one\n# not a heading\n- not a list item',
105
+ });
106
+
107
+ const prompt = formatUserProfileForPrompt(profile);
108
+ expect(prompt).toContain('Anything Else: |');
109
+ expect(prompt).toContain(' Line one');
110
+ expect(prompt).toContain(' # not a heading');
111
+ expect(prompt).toContain(' - not a list item');
112
+ });
113
+ });
@@ -0,0 +1,176 @@
1
+ import { deleteSetting, getSetting, setSetting } from './settings.ts';
2
+ import { createEntity, updateEntity } from './entities.ts';
3
+ import { createFact } from './facts.ts';
4
+ import { getDb } from './schema.ts';
5
+ import {
6
+ USER_PROFILE_QUESTIONS,
7
+ USER_PROFILE_SETTING_KEY,
8
+ createEmptyUserProfile,
9
+ countAnsweredUserProfileQuestions,
10
+ normalizeUserProfileAnswers,
11
+ type UserProfileRecord,
12
+ } from '../user/profile.ts';
13
+
14
+ export const USER_PROFILE_VAULT_SOURCE = 'user_profile';
15
+ const USER_PROFILE_FOLLOWUP_STATE_KEY = 'user.profile.followup.v1';
16
+
17
+ export function getUserProfile(): UserProfileRecord | null {
18
+ const raw = getSetting(USER_PROFILE_SETTING_KEY);
19
+ if (!raw) return null;
20
+
21
+ try {
22
+ const parsed = JSON.parse(raw) as Partial<UserProfileRecord>;
23
+ const base = createEmptyUserProfile();
24
+ return {
25
+ version: 1,
26
+ answers: normalizeUserProfileAnswers((parsed.answers ?? {}) as Record<string, unknown>),
27
+ created_at: typeof parsed.created_at === 'number' ? parsed.created_at : base.created_at,
28
+ updated_at: typeof parsed.updated_at === 'number' ? parsed.updated_at : base.updated_at,
29
+ completed_at: typeof parsed.completed_at === 'number' ? parsed.completed_at : null,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function saveUserProfile(input: Record<string, unknown>): UserProfileRecord {
37
+ const existing = getUserProfile();
38
+ const now = Date.now();
39
+ const answers = normalizeUserProfileAnswers(input);
40
+ const profile: UserProfileRecord = {
41
+ version: 1,
42
+ answers,
43
+ created_at: existing?.created_at ?? now,
44
+ updated_at: now,
45
+ completed_at: countAnsweredUserProfileQuestions({
46
+ version: 1,
47
+ answers,
48
+ created_at: existing?.created_at ?? now,
49
+ updated_at: now,
50
+ completed_at: null,
51
+ }) > 0 ? now : null,
52
+ };
53
+
54
+ setSetting(USER_PROFILE_SETTING_KEY, JSON.stringify(profile));
55
+ syncUserProfileKnowledge(profile);
56
+ return profile;
57
+ }
58
+
59
+ export function clearUserProfile(): void {
60
+ deleteSetting(USER_PROFILE_SETTING_KEY);
61
+ clearUserProfileKnowledge();
62
+ deleteSetting(USER_PROFILE_FOLLOWUP_STATE_KEY);
63
+ }
64
+
65
+ function syncUserProfileKnowledge(profile: UserProfileRecord): void {
66
+ if (countAnsweredUserProfileQuestions(profile) === 0) {
67
+ clearUserProfileKnowledge();
68
+ return;
69
+ }
70
+
71
+ const db = getDb();
72
+ const entityName = profile.answers.preferred_name?.trim() || 'User';
73
+ const entityProperties = {
74
+ is_current_user: true,
75
+ profile_version: profile.version,
76
+ profile_updated_at: profile.updated_at,
77
+ };
78
+
79
+ const entityRow = db.prepare(
80
+ 'SELECT id FROM entities WHERE source = ? ORDER BY updated_at DESC LIMIT 1'
81
+ ).get(USER_PROFILE_VAULT_SOURCE) as { id: string } | null;
82
+
83
+ const entity = entityRow
84
+ ? updateEntity(entityRow.id, { name: entityName, properties: entityProperties })
85
+ : createEntity('person', entityName, entityProperties, USER_PROFILE_VAULT_SOURCE);
86
+
87
+ if (!entity) {
88
+ throw new Error('Failed to sync user profile entity to vault');
89
+ }
90
+
91
+ db.prepare('DELETE FROM facts WHERE subject_id = ? AND source = ?').run(entity.id, USER_PROFILE_VAULT_SOURCE);
92
+
93
+ for (const question of USER_PROFILE_QUESTIONS) {
94
+ const answer = profile.answers[question.id]?.trim();
95
+ if (!answer) continue;
96
+ createFact(entity.id, question.id, answer, {
97
+ confidence: 1,
98
+ source: USER_PROFILE_VAULT_SOURCE,
99
+ });
100
+ }
101
+
102
+ for (const fact of getDerivedUserProfileFacts(profile)) {
103
+ createFact(entity.id, fact.predicate, fact.object, {
104
+ confidence: 1,
105
+ source: USER_PROFILE_VAULT_SOURCE,
106
+ });
107
+ }
108
+ }
109
+
110
+ function clearUserProfileKnowledge(): void {
111
+ const db = getDb();
112
+ const rows = db.prepare('SELECT id FROM entities WHERE source = ?').all(USER_PROFILE_VAULT_SOURCE) as Array<{ id: string }>;
113
+ db.prepare('DELETE FROM facts WHERE source = ?').run(USER_PROFILE_VAULT_SOURCE);
114
+ for (const row of rows) {
115
+ db.prepare('DELETE FROM entities WHERE id = ?').run(row.id);
116
+ }
117
+ }
118
+
119
+ function getDerivedUserProfileFacts(profile: UserProfileRecord): Array<{ predicate: string; object: string }> {
120
+ const facts: Array<{ predicate: string; object: string }> = [];
121
+ const seen = new Set<string>();
122
+
123
+ const preferredName = profile.answers.preferred_name?.trim();
124
+ if (preferredName) {
125
+ pushFact(facts, seen, 'name', preferredName);
126
+ }
127
+
128
+ const aliasSources = [
129
+ profile.answers.important_people,
130
+ profile.answers.anything_else,
131
+ profile.answers.work_role,
132
+ profile.answers.communication_preferences,
133
+ ].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
134
+
135
+ for (const source of aliasSources) {
136
+ for (const alias of extractAliases(source)) {
137
+ pushFact(facts, seen, 'alias', alias);
138
+ pushFact(facts, seen, 'username', alias);
139
+ }
140
+ }
141
+
142
+ return facts;
143
+ }
144
+
145
+ function pushFact(
146
+ facts: Array<{ predicate: string; object: string }>,
147
+ seen: Set<string>,
148
+ predicate: string,
149
+ object: string,
150
+ ): void {
151
+ const value = object.trim();
152
+ if (!value) return;
153
+ const key = `${predicate}\u0000${value.toLowerCase()}`;
154
+ if (seen.has(key)) return;
155
+ seen.add(key);
156
+ facts.push({ predicate, object: value });
157
+ }
158
+
159
+ function extractAliases(text: string): string[] {
160
+ const aliases = new Set<string>();
161
+ const patterns = [
162
+ /\b(?:alias|username|user\s*name|handle)\s*(?:is|=|:)?\s*["']?([A-Za-z0-9._-]{2,32})["']?/gi,
163
+ /\bgo by\s+["']?([A-Za-z0-9._-]{2,32})["']?/gi,
164
+ /\bcalled\s+["']?([A-Za-z0-9._-]{2,32})["']?/gi,
165
+ ];
166
+
167
+ for (const pattern of patterns) {
168
+ let match: RegExpExecArray | null;
169
+ while ((match = pattern.exec(text)) !== null) {
170
+ const alias = match[1]?.trim().replace(/[.,!?;:]+$/g, '');
171
+ if (alias) aliases.add(alias);
172
+ }
173
+ }
174
+
175
+ return [...aliases];
176
+ }
@@ -0,0 +1,92 @@
1
+ import { getDb, generateId } from './schema.ts';
2
+
3
+ export type VectorRecord = {
4
+ id: string;
5
+ ref_type: string;
6
+ ref_id: string;
7
+ embedding: Float32Array;
8
+ model: string;
9
+ created_at: number;
10
+ };
11
+
12
+ type VectorRow = {
13
+ id: string;
14
+ ref_type: string;
15
+ ref_id: string;
16
+ embedding: ArrayBuffer;
17
+ model: string;
18
+ created_at: number;
19
+ };
20
+
21
+ /**
22
+ * Parse vector row from database, converting BLOB to Float32Array
23
+ */
24
+ function parseVector(row: VectorRow): VectorRecord {
25
+ return {
26
+ ...row,
27
+ embedding: new Float32Array(row.embedding),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Store a vector embedding for a reference entity or fact
33
+ */
34
+ export function storeVector(
35
+ ref_type: string,
36
+ ref_id: string,
37
+ embedding: Float32Array,
38
+ model: string
39
+ ): VectorRecord {
40
+ const db = getDb();
41
+ const id = generateId();
42
+ const now = Date.now();
43
+
44
+ // Convert Float32Array to Buffer for SQLite BLOB storage
45
+ const buffer = Buffer.from(embedding.buffer);
46
+
47
+ const stmt = db.prepare(
48
+ 'INSERT INTO vectors (id, ref_type, ref_id, embedding, model, created_at) VALUES (?, ?, ?, ?, ?, ?)'
49
+ );
50
+
51
+ stmt.run(id, ref_type, ref_id, buffer, model, now);
52
+
53
+ return {
54
+ id,
55
+ ref_type,
56
+ ref_id,
57
+ embedding,
58
+ model,
59
+ created_at: now,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Find similar vectors using cosine similarity
65
+ *
66
+ * TODO: This is a stub implementation. For production use, integrate sqlite-vec extension
67
+ * which provides optimized vector similarity search with HNSW indexing.
68
+ *
69
+ * See: https://github.com/asg017/sqlite-vec
70
+ *
71
+ * Example with sqlite-vec:
72
+ * SELECT ref_type, ref_id, vec_distance_cosine(embedding, ?) as similarity
73
+ * FROM vectors
74
+ * ORDER BY similarity DESC
75
+ * LIMIT ?
76
+ */
77
+ export function findSimilar(
78
+ embedding: Float32Array,
79
+ limit: number = 10
80
+ ): Array<{ ref_type: string; ref_id: string; similarity: number }> {
81
+ // TODO: Implement vector similarity search with sqlite-vec extension
82
+ return [];
83
+ }
84
+
85
+ /**
86
+ * Delete all vectors for a given reference
87
+ */
88
+ export function deleteVectors(ref_type: string, ref_id: string): void {
89
+ const db = getDb();
90
+ const stmt = db.prepare('DELETE FROM vectors WHERE ref_type = ? AND ref_id = ?');
91
+ stmt.run(ref_type, ref_id);
92
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Webapp Template Seeds — Load templates from YAML files
3
+ *
4
+ * Scans two directories for .yaml/.yml files:
5
+ * 1. Built-in: webapp-templates/ in the package root (shipped with codebase)
6
+ * 2. User overrides: ~/.jarvis/webapp-templates/ (user-created or customized)
7
+ *
8
+ * User files override built-in files when they share the same app_name.
9
+ * Called once at startup — uses upsert so templates update without data loss.
10
+ */
11
+
12
+ import { join } from 'node:path';
13
+ import { readdirSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { parse as parseYAML } from 'yaml';
16
+ import { upsertWebappTemplate } from './webapp-templates.ts';
17
+
18
+ export type TemplateSeed = {
19
+ app_name: string;
20
+ domains: string[];
21
+ keywords?: string[];
22
+ description: string;
23
+ instructions: string;
24
+ version?: number;
25
+ };
26
+
27
+ /**
28
+ * Load all .yaml/.yml files from a directory into TemplateSeed objects.
29
+ * Returns a Map keyed by app_name (lowercase) for easy merging.
30
+ */
31
+ function loadTemplatesFromDir(dir: string): Map<string, TemplateSeed> {
32
+ const templates = new Map<string, TemplateSeed>();
33
+
34
+ if (!existsSync(dir)) return templates;
35
+
36
+ let files: string[];
37
+ try {
38
+ files = readdirSync(dir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
39
+ } catch {
40
+ return templates;
41
+ }
42
+
43
+ for (const file of files) {
44
+ try {
45
+ const content = readFileSync(join(dir, file), 'utf-8');
46
+ const parsed = parseYAML(content) as Record<string, unknown>;
47
+
48
+ if (!parsed.app_name || !parsed.domains || !parsed.instructions) {
49
+ console.warn(`[WebappTemplates] Skipping ${file}: missing required fields (app_name, domains, instructions)`);
50
+ continue;
51
+ }
52
+
53
+ const seed: TemplateSeed = {
54
+ app_name: parsed.app_name as string,
55
+ domains: parsed.domains as string[],
56
+ keywords: (parsed.keywords as string[]) || [],
57
+ description: (parsed.description as string) || '',
58
+ instructions: (parsed.instructions as string).trim(),
59
+ version: parsed.version as number | undefined,
60
+ };
61
+
62
+ templates.set(seed.app_name.toLowerCase(), seed);
63
+ } catch (err) {
64
+ console.warn(`[WebappTemplates] Failed to parse ${file}:`, err instanceof Error ? err.message : err);
65
+ }
66
+ }
67
+
68
+ return templates;
69
+ }
70
+
71
+ /**
72
+ * Seed webapp templates from YAML files into the database.
73
+ *
74
+ * Load order:
75
+ * 1. Built-in templates from webapp-templates/ (package root)
76
+ * 2. User overrides from ~/.jarvis/webapp-templates/
77
+ * User files override built-in files with the same app_name.
78
+ *
79
+ * Safe to call multiple times — uses upsert.
80
+ */
81
+ export function seedWebappTemplates(): void {
82
+ // Resolve built-in directory relative to this source file (works for npm + git)
83
+ const pkgRoot = join(import.meta.dir, '../..');
84
+ const builtinDir = join(pkgRoot, 'webapp-templates');
85
+
86
+ // User override directory
87
+ const userDir = join(homedir(), '.jarvis', 'webapp-templates');
88
+
89
+ // Ensure user directory exists
90
+ mkdirSync(userDir, { recursive: true });
91
+
92
+ // 1. Load built-in templates
93
+ const templates = loadTemplatesFromDir(builtinDir);
94
+ const builtinCount = templates.size;
95
+
96
+ // 2. Layer user overrides (same app_name replaces built-in)
97
+ const userTemplates = loadTemplatesFromDir(userDir);
98
+ for (const [key, seed] of userTemplates) {
99
+ templates.set(key, seed);
100
+ }
101
+
102
+ // 3. Upsert all into database
103
+ let count = 0;
104
+ for (const seed of templates.values()) {
105
+ try {
106
+ upsertWebappTemplate(seed);
107
+ count++;
108
+ } catch (err) {
109
+ console.error(`[WebappTemplates] Failed to seed ${seed.app_name}:`, err);
110
+ }
111
+ }
112
+
113
+ const userCount = userTemplates.size;
114
+ const overrideNote = userCount > 0 ? ` (${userCount} user override${userCount > 1 ? 's' : ''})` : '';
115
+ console.log(`[WebappTemplates] Seeded ${count} webapp templates${overrideNote}`);
116
+ }