@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.
- package/LICENSE +153 -0
- package/README.md +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +261 -0
- package/src/actions/browser/session.ts +506 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +363 -0
- package/src/actions/tools/builtin.ts +950 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +301 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/dashboard-auth.ts +75 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/dashboard-auth.test.ts +170 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +407 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +120 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +218 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +195 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- 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
|
+
}
|