@usejarvis/brain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -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 +252 -0
- package/src/actions/browser/session.ts +437 -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 +321 -0
- package/src/actions/tools/builtin.ts +846 -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/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -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 +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -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 +297 -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 +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -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/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -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/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -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 +348 -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 +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -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 +119 -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 +194 -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 +190 -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/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 +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -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 +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -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-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault Module - Data Access Layer for J.A.R.V.I.S. Knowledge Graph
|
|
3
|
+
*
|
|
4
|
+
* This module provides CRUD operations for all core data types in the knowledge graph:
|
|
5
|
+
* - Entities: People, projects, tools, places, concepts, events
|
|
6
|
+
* - Facts: Atomic pieces of knowledge with confidence scores
|
|
7
|
+
* - Relationships: Typed edges between entities
|
|
8
|
+
* - Commitments: Promises and tasks the AI needs to fulfill
|
|
9
|
+
* - Observations: Raw events from the observation layer
|
|
10
|
+
* - Vectors: Embeddings for semantic search
|
|
11
|
+
*
|
|
12
|
+
* All modules use Bun's SQLite API and handle JSON serialization automatically.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Re-export schema utilities
|
|
16
|
+
export { initDatabase, getDb, closeDb, generateId } from './schema.ts';
|
|
17
|
+
|
|
18
|
+
// Re-export entities module
|
|
19
|
+
export type { Entity, EntityType } from './entities.ts';
|
|
20
|
+
export {
|
|
21
|
+
createEntity,
|
|
22
|
+
getEntity,
|
|
23
|
+
findEntities,
|
|
24
|
+
updateEntity,
|
|
25
|
+
deleteEntity,
|
|
26
|
+
searchEntitiesByName,
|
|
27
|
+
} from './entities.ts';
|
|
28
|
+
|
|
29
|
+
// Re-export facts module
|
|
30
|
+
export type { Fact } from './facts.ts';
|
|
31
|
+
export {
|
|
32
|
+
createFact,
|
|
33
|
+
getFact,
|
|
34
|
+
findFacts,
|
|
35
|
+
queryFact,
|
|
36
|
+
updateFact,
|
|
37
|
+
deleteFact,
|
|
38
|
+
verifyFact,
|
|
39
|
+
} from './facts.ts';
|
|
40
|
+
|
|
41
|
+
// Re-export relationships module
|
|
42
|
+
export type { Relationship } from './relationships.ts';
|
|
43
|
+
export {
|
|
44
|
+
createRelationship,
|
|
45
|
+
getRelationship,
|
|
46
|
+
findRelationships,
|
|
47
|
+
getEntityRelationships,
|
|
48
|
+
deleteRelationship,
|
|
49
|
+
} from './relationships.ts';
|
|
50
|
+
|
|
51
|
+
// Re-export commitments module
|
|
52
|
+
export type { Commitment, CommitmentPriority, CommitmentStatus, RetryPolicy } from './commitments.ts';
|
|
53
|
+
export {
|
|
54
|
+
createCommitment,
|
|
55
|
+
getCommitment,
|
|
56
|
+
findCommitments,
|
|
57
|
+
getUpcoming,
|
|
58
|
+
completeCommitment,
|
|
59
|
+
failCommitment,
|
|
60
|
+
escalateCommitment,
|
|
61
|
+
getDueCommitments,
|
|
62
|
+
} from './commitments.ts';
|
|
63
|
+
|
|
64
|
+
// Re-export observations module
|
|
65
|
+
export type { Observation, ObservationType } from './observations.ts';
|
|
66
|
+
export {
|
|
67
|
+
createObservation,
|
|
68
|
+
getUnprocessed,
|
|
69
|
+
markProcessed,
|
|
70
|
+
getRecentObservations,
|
|
71
|
+
} from './observations.ts';
|
|
72
|
+
|
|
73
|
+
// Re-export vectors module
|
|
74
|
+
export type { VectorRecord } from './vectors.ts';
|
|
75
|
+
export {
|
|
76
|
+
storeVector,
|
|
77
|
+
findSimilar,
|
|
78
|
+
deleteVectors,
|
|
79
|
+
} from './vectors.ts';
|
|
80
|
+
|
|
81
|
+
// Re-export extractor module
|
|
82
|
+
export type { ExtractionResult } from './extractor.ts';
|
|
83
|
+
export {
|
|
84
|
+
buildExtractionPrompt,
|
|
85
|
+
parseExtractionResponse,
|
|
86
|
+
extractAndStore,
|
|
87
|
+
} from './extractor.ts';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted secrets store for JARVIS.
|
|
3
|
+
*
|
|
4
|
+
* Stores secrets in an AES-256-GCM encrypted file (~/.jarvis/.secrets.enc)
|
|
5
|
+
* with a random key stored in ~/.jarvis/.secrets.key (chmod 600).
|
|
6
|
+
*
|
|
7
|
+
* This avoids depending on OS keychain daemons (which are unreliable on WSL2).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
const JARVIS_DIR = join(homedir(), '.jarvis');
|
|
16
|
+
const KEY_PATH = join(JARVIS_DIR, '.secrets.key');
|
|
17
|
+
const SECRETS_PATH = join(JARVIS_DIR, '.secrets.enc');
|
|
18
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
19
|
+
const IV_LENGTH = 12;
|
|
20
|
+
const TAG_LENGTH = 16;
|
|
21
|
+
|
|
22
|
+
function ensureDir(): void {
|
|
23
|
+
if (!existsSync(JARVIS_DIR)) {
|
|
24
|
+
mkdirSync(JARVIS_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getOrCreateKey(): Buffer {
|
|
29
|
+
ensureDir();
|
|
30
|
+
if (existsSync(KEY_PATH)) {
|
|
31
|
+
const hex = readFileSync(KEY_PATH, 'utf-8').trim();
|
|
32
|
+
return Buffer.from(hex, 'hex');
|
|
33
|
+
}
|
|
34
|
+
const key = randomBytes(32);
|
|
35
|
+
writeFileSync(KEY_PATH, key.toString('hex'), { mode: 0o600 });
|
|
36
|
+
try { chmodSync(KEY_PATH, 0o600); } catch {}
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encrypt(key: Buffer, plaintext: string): Buffer {
|
|
41
|
+
const iv = randomBytes(IV_LENGTH);
|
|
42
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
43
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
44
|
+
const tag = cipher.getAuthTag();
|
|
45
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decrypt(key: Buffer, data: Buffer): string {
|
|
49
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
50
|
+
const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
51
|
+
const encrypted = data.subarray(IV_LENGTH + TAG_LENGTH);
|
|
52
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
53
|
+
decipher.setAuthTag(tag);
|
|
54
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadSecrets(): Record<string, string> {
|
|
58
|
+
if (!existsSync(SECRETS_PATH)) return {};
|
|
59
|
+
try {
|
|
60
|
+
const key = getOrCreateKey();
|
|
61
|
+
const raw = readFileSync(SECRETS_PATH);
|
|
62
|
+
const json = decrypt(key, raw);
|
|
63
|
+
return JSON.parse(json);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn('[Keychain] Failed to decrypt secrets file, starting fresh:', err);
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function saveSecrets(secrets: Record<string, string>): void {
|
|
71
|
+
ensureDir();
|
|
72
|
+
const key = getOrCreateKey();
|
|
73
|
+
const json = JSON.stringify(secrets);
|
|
74
|
+
const encrypted = encrypt(key, json);
|
|
75
|
+
writeFileSync(SECRETS_PATH, encrypted, { mode: 0o600 });
|
|
76
|
+
try { chmodSync(SECRETS_PATH, 0o600); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getSecret(name: string): string | null {
|
|
80
|
+
const secrets = loadSecrets();
|
|
81
|
+
return secrets[name] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setSecret(name: string, value: string): void {
|
|
85
|
+
const secrets = loadSecrets();
|
|
86
|
+
secrets[name] = value;
|
|
87
|
+
saveSecrets(secrets);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function deleteSecret(name: string): void {
|
|
91
|
+
const secrets = loadSecrets();
|
|
92
|
+
delete secrets[name];
|
|
93
|
+
saveSecrets(secrets);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function hasSecret(name: string): boolean {
|
|
97
|
+
const secrets = loadSecrets();
|
|
98
|
+
return name in secrets;
|
|
99
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getDb, generateId } from './schema.ts';
|
|
2
|
+
|
|
3
|
+
export type ObservationType =
|
|
4
|
+
| 'file_change'
|
|
5
|
+
| 'notification'
|
|
6
|
+
| 'clipboard'
|
|
7
|
+
| 'app_activity'
|
|
8
|
+
| 'calendar'
|
|
9
|
+
| 'email'
|
|
10
|
+
| 'browser'
|
|
11
|
+
| 'process'
|
|
12
|
+
| 'screen_capture';
|
|
13
|
+
|
|
14
|
+
export type Observation = {
|
|
15
|
+
id: string;
|
|
16
|
+
type: ObservationType;
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
processed: boolean;
|
|
19
|
+
created_at: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ObservationRow = {
|
|
23
|
+
id: string;
|
|
24
|
+
type: ObservationType;
|
|
25
|
+
data: string;
|
|
26
|
+
processed: number;
|
|
27
|
+
created_at: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse observation row from database, deserializing JSON fields
|
|
32
|
+
*/
|
|
33
|
+
function parseObservation(row: ObservationRow): Observation {
|
|
34
|
+
return {
|
|
35
|
+
id: row.id,
|
|
36
|
+
type: row.type,
|
|
37
|
+
data: JSON.parse(row.data),
|
|
38
|
+
processed: row.processed === 1,
|
|
39
|
+
created_at: row.created_at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new observation
|
|
45
|
+
*/
|
|
46
|
+
export function createObservation(
|
|
47
|
+
type: ObservationType,
|
|
48
|
+
data: Record<string, unknown>
|
|
49
|
+
): Observation {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const id = generateId();
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
|
|
54
|
+
const stmt = db.prepare(
|
|
55
|
+
'INSERT INTO observations (id, type, data, processed, created_at) VALUES (?, ?, ?, ?, ?)'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
stmt.run(id, type, JSON.stringify(data), 0, now);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
type,
|
|
63
|
+
data,
|
|
64
|
+
processed: false,
|
|
65
|
+
created_at: now,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get unprocessed observations
|
|
71
|
+
*/
|
|
72
|
+
export function getUnprocessed(limit: number = 100): Observation[] {
|
|
73
|
+
const db = getDb();
|
|
74
|
+
const stmt = db.prepare(
|
|
75
|
+
'SELECT * FROM observations WHERE processed = 0 ORDER BY created_at ASC LIMIT ?'
|
|
76
|
+
);
|
|
77
|
+
const rows = stmt.all(limit) as ObservationRow[];
|
|
78
|
+
|
|
79
|
+
return rows.map(parseObservation);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mark an observation as processed
|
|
84
|
+
*/
|
|
85
|
+
export function markProcessed(id: string): void {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
const stmt = db.prepare('UPDATE observations SET processed = 1 WHERE id = ?');
|
|
88
|
+
stmt.run(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get recent observations, optionally filtered by type
|
|
93
|
+
*/
|
|
94
|
+
export function getRecentObservations(
|
|
95
|
+
type?: ObservationType,
|
|
96
|
+
limit: number = 50
|
|
97
|
+
): Observation[] {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
|
|
100
|
+
let query = 'SELECT * FROM observations';
|
|
101
|
+
const params: unknown[] = [];
|
|
102
|
+
|
|
103
|
+
if (type) {
|
|
104
|
+
query += ' WHERE type = ?';
|
|
105
|
+
params.push(type);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
109
|
+
params.push(limit);
|
|
110
|
+
|
|
111
|
+
const stmt = db.prepare(query);
|
|
112
|
+
const rows = stmt.all(...params as any[]) as ObservationRow[];
|
|
113
|
+
|
|
114
|
+
return rows.map(parseObservation);
|
|
115
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { getDb, generateId } from './schema.ts';
|
|
2
|
+
import type { Entity } from './entities.ts';
|
|
3
|
+
|
|
4
|
+
export type Relationship = {
|
|
5
|
+
id: string;
|
|
6
|
+
from_id: string;
|
|
7
|
+
to_id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
properties: Record<string, unknown> | null;
|
|
10
|
+
created_at: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type RelationshipRow = {
|
|
14
|
+
id: string;
|
|
15
|
+
from_id: string;
|
|
16
|
+
to_id: string;
|
|
17
|
+
type: string;
|
|
18
|
+
properties: string | null;
|
|
19
|
+
created_at: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse relationship row from database, deserializing JSON fields
|
|
24
|
+
*/
|
|
25
|
+
function parseRelationship(row: RelationshipRow): Relationship {
|
|
26
|
+
return {
|
|
27
|
+
...row,
|
|
28
|
+
properties: row.properties ? JSON.parse(row.properties) : null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new relationship between entities
|
|
34
|
+
*/
|
|
35
|
+
export function createRelationship(
|
|
36
|
+
from_id: string,
|
|
37
|
+
to_id: string,
|
|
38
|
+
type: string,
|
|
39
|
+
properties?: Record<string, unknown>
|
|
40
|
+
): Relationship {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const id = generateId();
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
|
|
45
|
+
const stmt = db.prepare(
|
|
46
|
+
'INSERT INTO relationships (id, from_id, to_id, type, properties, created_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
stmt.run(id, from_id, to_id, type, properties ? JSON.stringify(properties) : null, now);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
from_id,
|
|
54
|
+
to_id,
|
|
55
|
+
type,
|
|
56
|
+
properties: properties ?? null,
|
|
57
|
+
created_at: now,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a relationship by ID
|
|
63
|
+
*/
|
|
64
|
+
export function getRelationship(id: string): Relationship | null {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const stmt = db.prepare('SELECT * FROM relationships WHERE id = ?');
|
|
67
|
+
const row = stmt.get(id) as RelationshipRow | null;
|
|
68
|
+
|
|
69
|
+
if (!row) return null;
|
|
70
|
+
|
|
71
|
+
return parseRelationship(row);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find relationships matching query criteria
|
|
76
|
+
*/
|
|
77
|
+
export function findRelationships(query: {
|
|
78
|
+
from_id?: string;
|
|
79
|
+
to_id?: string;
|
|
80
|
+
type?: string;
|
|
81
|
+
}): Relationship[] {
|
|
82
|
+
const db = getDb();
|
|
83
|
+
const conditions: string[] = [];
|
|
84
|
+
const params: unknown[] = [];
|
|
85
|
+
|
|
86
|
+
if (query.from_id) {
|
|
87
|
+
conditions.push('from_id = ?');
|
|
88
|
+
params.push(query.from_id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (query.to_id) {
|
|
92
|
+
conditions.push('to_id = ?');
|
|
93
|
+
params.push(query.to_id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (query.type) {
|
|
97
|
+
conditions.push('type = ?');
|
|
98
|
+
params.push(query.type);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
102
|
+
const stmt = db.prepare(`SELECT * FROM relationships ${where} ORDER BY created_at DESC`);
|
|
103
|
+
const rows = stmt.all(...params as any[]) as RelationshipRow[];
|
|
104
|
+
|
|
105
|
+
return rows.map(parseRelationship);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all relationships for an entity (both incoming and outgoing) with full entity details
|
|
110
|
+
*/
|
|
111
|
+
export function getEntityRelationships(
|
|
112
|
+
entityId: string
|
|
113
|
+
): Array<Relationship & { from_entity: Entity; to_entity: Entity }> {
|
|
114
|
+
const db = getDb();
|
|
115
|
+
|
|
116
|
+
const stmt = db.prepare(`
|
|
117
|
+
SELECT
|
|
118
|
+
r.*,
|
|
119
|
+
e1.id as from_entity_id,
|
|
120
|
+
e1.type as from_entity_type,
|
|
121
|
+
e1.name as from_entity_name,
|
|
122
|
+
e1.properties as from_entity_properties,
|
|
123
|
+
e1.created_at as from_entity_created_at,
|
|
124
|
+
e1.updated_at as from_entity_updated_at,
|
|
125
|
+
e1.source as from_entity_source,
|
|
126
|
+
e2.id as to_entity_id,
|
|
127
|
+
e2.type as to_entity_type,
|
|
128
|
+
e2.name as to_entity_name,
|
|
129
|
+
e2.properties as to_entity_properties,
|
|
130
|
+
e2.created_at as to_entity_created_at,
|
|
131
|
+
e2.updated_at as to_entity_updated_at,
|
|
132
|
+
e2.source as to_entity_source
|
|
133
|
+
FROM relationships r
|
|
134
|
+
JOIN entities e1 ON r.from_id = e1.id
|
|
135
|
+
JOIN entities e2 ON r.to_id = e2.id
|
|
136
|
+
WHERE r.from_id = ? OR r.to_id = ?
|
|
137
|
+
ORDER BY r.created_at DESC
|
|
138
|
+
`);
|
|
139
|
+
|
|
140
|
+
const rows = stmt.all(entityId, entityId) as any[];
|
|
141
|
+
|
|
142
|
+
return rows.map((row) => ({
|
|
143
|
+
id: row.id,
|
|
144
|
+
from_id: row.from_id,
|
|
145
|
+
to_id: row.to_id,
|
|
146
|
+
type: row.type,
|
|
147
|
+
properties: row.properties ? JSON.parse(row.properties) : null,
|
|
148
|
+
created_at: row.created_at,
|
|
149
|
+
from_entity: {
|
|
150
|
+
id: row.from_entity_id,
|
|
151
|
+
type: row.from_entity_type,
|
|
152
|
+
name: row.from_entity_name,
|
|
153
|
+
properties: row.from_entity_properties ? JSON.parse(row.from_entity_properties) : null,
|
|
154
|
+
created_at: row.from_entity_created_at,
|
|
155
|
+
updated_at: row.from_entity_updated_at,
|
|
156
|
+
source: row.from_entity_source,
|
|
157
|
+
},
|
|
158
|
+
to_entity: {
|
|
159
|
+
id: row.to_entity_id,
|
|
160
|
+
type: row.to_entity_type,
|
|
161
|
+
name: row.to_entity_name,
|
|
162
|
+
properties: row.to_entity_properties ? JSON.parse(row.to_entity_properties) : null,
|
|
163
|
+
created_at: row.to_entity_created_at,
|
|
164
|
+
updated_at: row.to_entity_updated_at,
|
|
165
|
+
source: row.to_entity_source,
|
|
166
|
+
},
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Delete a relationship
|
|
172
|
+
*/
|
|
173
|
+
export function deleteRelationship(id: string): boolean {
|
|
174
|
+
const db = getDb();
|
|
175
|
+
const stmt = db.prepare('DELETE FROM relationships WHERE id = ?');
|
|
176
|
+
const result = stmt.run(id);
|
|
177
|
+
return result.changes > 0;
|
|
178
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { initDatabase } from './schema.ts';
|
|
3
|
+
import { createEntity } from './entities.ts';
|
|
4
|
+
import { createFact } from './facts.ts';
|
|
5
|
+
import { createRelationship } from './relationships.ts';
|
|
6
|
+
import {
|
|
7
|
+
extractSearchTerms,
|
|
8
|
+
retrieveForMessage,
|
|
9
|
+
formatKnowledgeContext,
|
|
10
|
+
getKnowledgeForMessage,
|
|
11
|
+
} from './retrieval.ts';
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
initDatabase(':memory:');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// --- extractSearchTerms ---
|
|
18
|
+
|
|
19
|
+
test('extractSearchTerms filters stopwords', () => {
|
|
20
|
+
const terms = extractSearchTerms('Where does John work at Google?');
|
|
21
|
+
expect(terms).toContain('john');
|
|
22
|
+
expect(terms).toContain('google');
|
|
23
|
+
expect(terms).not.toContain('where');
|
|
24
|
+
expect(terms).not.toContain('does');
|
|
25
|
+
expect(terms).not.toContain('at');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('extractSearchTerms deduplicates', () => {
|
|
29
|
+
const terms = extractSearchTerms('John and John went to Google Google');
|
|
30
|
+
const johnCount = terms.filter(t => t === 'john').length;
|
|
31
|
+
expect(johnCount).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('extractSearchTerms handles empty input', () => {
|
|
35
|
+
expect(extractSearchTerms('')).toEqual([]);
|
|
36
|
+
expect(extractSearchTerms('the is a')).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// --- retrieveForMessage ---
|
|
40
|
+
|
|
41
|
+
test('retrieveForMessage finds entities by name', () => {
|
|
42
|
+
createEntity('person', 'John', { role: 'engineer' });
|
|
43
|
+
createEntity('person', 'Anna');
|
|
44
|
+
|
|
45
|
+
const profiles = retrieveForMessage('Tell me about John');
|
|
46
|
+
expect(profiles.length).toBe(1);
|
|
47
|
+
expect(profiles[0]!.entity.name).toBe('John');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('retrieveForMessage finds entities via fact objects', () => {
|
|
51
|
+
const john = createEntity('person', 'John');
|
|
52
|
+
createFact(john.id, 'works_at', 'Google');
|
|
53
|
+
|
|
54
|
+
// Search for "Google" — should find John because he has a fact with object "Google"
|
|
55
|
+
const profiles = retrieveForMessage('What do you know about Google?');
|
|
56
|
+
expect(profiles.length).toBeGreaterThanOrEqual(1);
|
|
57
|
+
const names = profiles.map(p => p.entity.name);
|
|
58
|
+
expect(names).toContain('John');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('retrieveForMessage includes facts for matched entities', () => {
|
|
62
|
+
const john = createEntity('person', 'John');
|
|
63
|
+
createFact(john.id, 'works_at', 'Google');
|
|
64
|
+
createFact(john.id, 'birthday', 'March 15');
|
|
65
|
+
|
|
66
|
+
const profiles = retrieveForMessage('Tell me about John');
|
|
67
|
+
expect(profiles.length).toBe(1);
|
|
68
|
+
expect(profiles[0]!.facts.length).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('retrieveForMessage includes relationships', () => {
|
|
72
|
+
const john = createEntity('person', 'John');
|
|
73
|
+
const google = createEntity('concept', 'Google');
|
|
74
|
+
createRelationship(john.id, google.id, 'works_at');
|
|
75
|
+
|
|
76
|
+
const profiles = retrieveForMessage('What about John?');
|
|
77
|
+
expect(profiles.length).toBeGreaterThanOrEqual(1);
|
|
78
|
+
|
|
79
|
+
const johnProfile = profiles.find(p => p.entity.name === 'John');
|
|
80
|
+
expect(johnProfile).toBeDefined();
|
|
81
|
+
expect(johnProfile!.relationships.length).toBeGreaterThanOrEqual(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('retrieveForMessage returns empty for irrelevant query', () => {
|
|
85
|
+
createEntity('person', 'John');
|
|
86
|
+
const profiles = retrieveForMessage('the is a');
|
|
87
|
+
expect(profiles.length).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// --- formatKnowledgeContext ---
|
|
91
|
+
|
|
92
|
+
test('formatKnowledgeContext formats entity with facts', () => {
|
|
93
|
+
const john = createEntity('person', 'John');
|
|
94
|
+
createFact(john.id, 'works_at', 'Google');
|
|
95
|
+
|
|
96
|
+
const profiles = retrieveForMessage('John');
|
|
97
|
+
const context = formatKnowledgeContext(profiles);
|
|
98
|
+
|
|
99
|
+
expect(context).toContain('**John** (person)');
|
|
100
|
+
expect(context).toContain('works_at: Google');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('formatKnowledgeContext returns empty for no profiles', () => {
|
|
104
|
+
expect(formatKnowledgeContext([])).toBe('');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// --- getKnowledgeForMessage (integration) ---
|
|
108
|
+
|
|
109
|
+
test('getKnowledgeForMessage end-to-end', () => {
|
|
110
|
+
const john = createEntity('person', 'John');
|
|
111
|
+
createFact(john.id, 'works_at', 'Google');
|
|
112
|
+
createFact(john.id, 'location', 'San Francisco');
|
|
113
|
+
|
|
114
|
+
const anna = createEntity('person', 'Anna');
|
|
115
|
+
createFact(anna.id, 'sister_of', 'John');
|
|
116
|
+
|
|
117
|
+
const context = getKnowledgeForMessage('Where does John live?');
|
|
118
|
+
expect(context).toContain('John');
|
|
119
|
+
expect(context).toContain('works_at: Google');
|
|
120
|
+
expect(context).toContain('location: San Francisco');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('getKnowledgeForMessage handles no matches gracefully', () => {
|
|
124
|
+
const context = getKnowledgeForMessage('Tell me about quantum physics');
|
|
125
|
+
expect(context).toBe('');
|
|
126
|
+
});
|