@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,231 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { AnthropicProvider } from './anthropic.ts';
|
|
3
|
+
import { OpenAIProvider } from './openai.ts';
|
|
4
|
+
import { OllamaProvider } from './ollama.ts';
|
|
5
|
+
import { LLMManager } from './manager.ts';
|
|
6
|
+
import { guardImageSize, type LLMMessage, type ContentBlock } from './provider.ts';
|
|
7
|
+
import { isToolResult, type ToolResult } from '../actions/tools/registry.ts';
|
|
8
|
+
|
|
9
|
+
describe('LLM Provider Types', () => {
|
|
10
|
+
test('AnthropicProvider can be instantiated', () => {
|
|
11
|
+
const provider = new AnthropicProvider('test-key', 'test-model');
|
|
12
|
+
expect(provider.name).toBe('anthropic');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('OpenAIProvider can be instantiated', () => {
|
|
16
|
+
const provider = new OpenAIProvider('test-key', 'test-model');
|
|
17
|
+
expect(provider.name).toBe('openai');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('OllamaProvider can be instantiated', () => {
|
|
21
|
+
const provider = new OllamaProvider('http://localhost:11434', 'llama3');
|
|
22
|
+
expect(provider.name).toBe('ollama');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('LLMManager', () => {
|
|
27
|
+
test('can register providers', () => {
|
|
28
|
+
const manager = new LLMManager();
|
|
29
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
30
|
+
|
|
31
|
+
manager.registerProvider(anthropic);
|
|
32
|
+
expect(manager.getProvider('anthropic')).toBe(anthropic);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('sets first registered provider as primary', () => {
|
|
36
|
+
const manager = new LLMManager();
|
|
37
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
38
|
+
|
|
39
|
+
manager.registerProvider(anthropic);
|
|
40
|
+
// Primary is set automatically
|
|
41
|
+
expect(manager.getProvider('anthropic')).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('can change primary provider', () => {
|
|
45
|
+
const manager = new LLMManager();
|
|
46
|
+
const anthropic = new AnthropicProvider('test-key-1');
|
|
47
|
+
const openai = new OpenAIProvider('test-key-2');
|
|
48
|
+
|
|
49
|
+
manager.registerProvider(anthropic);
|
|
50
|
+
manager.registerProvider(openai);
|
|
51
|
+
manager.setPrimary('openai');
|
|
52
|
+
|
|
53
|
+
// Should not throw
|
|
54
|
+
expect(manager.getProvider('openai')).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('throws when setting non-existent provider as primary', () => {
|
|
58
|
+
const manager = new LLMManager();
|
|
59
|
+
expect(() => manager.setPrimary('nonexistent')).toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('can set fallback chain', () => {
|
|
63
|
+
const manager = new LLMManager();
|
|
64
|
+
const anthropic = new AnthropicProvider('test-key-1');
|
|
65
|
+
const openai = new OpenAIProvider('test-key-2');
|
|
66
|
+
|
|
67
|
+
manager.registerProvider(anthropic);
|
|
68
|
+
manager.registerProvider(openai);
|
|
69
|
+
manager.setPrimary('anthropic');
|
|
70
|
+
manager.setFallbackChain(['openai']);
|
|
71
|
+
|
|
72
|
+
// Should not throw
|
|
73
|
+
expect(manager.getProvider('anthropic')).toBeDefined();
|
|
74
|
+
expect(manager.getProvider('openai')).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('throws when setting non-existent fallback provider', () => {
|
|
78
|
+
const manager = new LLMManager();
|
|
79
|
+
const anthropic = new AnthropicProvider('test-key');
|
|
80
|
+
|
|
81
|
+
manager.registerProvider(anthropic);
|
|
82
|
+
expect(() => manager.setFallbackChain(['nonexistent'])).toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Message Types', () => {
|
|
87
|
+
test('LLMMessage has correct structure', () => {
|
|
88
|
+
const message: LLMMessage = {
|
|
89
|
+
role: 'user',
|
|
90
|
+
content: 'Hello',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
expect(message.role).toBe('user');
|
|
94
|
+
expect(message.content).toBe('Hello');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('supports all message roles', () => {
|
|
98
|
+
const messages: LLMMessage[] = [
|
|
99
|
+
{ role: 'system', content: 'You are helpful' },
|
|
100
|
+
{ role: 'user', content: 'Hello' },
|
|
101
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
expect(messages).toHaveLength(3);
|
|
105
|
+
expect(messages[0]!.role).toBe('system');
|
|
106
|
+
expect(messages[1]!.role).toBe('user');
|
|
107
|
+
expect(messages[2]!.role).toBe('assistant');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Provider URLs', () => {
|
|
112
|
+
test('AnthropicProvider uses correct API URL', () => {
|
|
113
|
+
const provider = new AnthropicProvider('test-key') as any;
|
|
114
|
+
expect(provider.apiUrl).toBe('https://api.anthropic.com/v1/messages');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('OpenAIProvider uses correct API URL', () => {
|
|
118
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
119
|
+
expect(provider.apiUrl).toBe('https://api.openai.com/v1/chat/completions');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('OllamaProvider uses correct base URL', () => {
|
|
123
|
+
const provider = new OllamaProvider() as any;
|
|
124
|
+
expect(provider.baseUrl).toBe('http://localhost:11434');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('OllamaProvider removes trailing slash from base URL', () => {
|
|
128
|
+
const provider = new OllamaProvider('http://localhost:11434/') as any;
|
|
129
|
+
expect(provider.baseUrl).toBe('http://localhost:11434');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Default Models', () => {
|
|
134
|
+
test('AnthropicProvider has correct default model', () => {
|
|
135
|
+
const provider = new AnthropicProvider('test-key') as any;
|
|
136
|
+
expect(provider.defaultModel).toBe('claude-sonnet-4-5-20250929');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('OpenAIProvider has correct default model', () => {
|
|
140
|
+
const provider = new OpenAIProvider('test-key') as any;
|
|
141
|
+
expect(provider.defaultModel).toBe('gpt-4o');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('OllamaProvider has correct default model', () => {
|
|
145
|
+
const provider = new OllamaProvider() as any;
|
|
146
|
+
expect(provider.defaultModel).toBe('llama3');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('can override default models', () => {
|
|
150
|
+
const anthropic = new AnthropicProvider('key', 'custom-model') as any;
|
|
151
|
+
const openai = new OpenAIProvider('key', 'custom-model') as any;
|
|
152
|
+
const ollama = new OllamaProvider('http://localhost:11434', 'custom-model') as any;
|
|
153
|
+
|
|
154
|
+
expect(anthropic.defaultModel).toBe('custom-model');
|
|
155
|
+
expect(openai.defaultModel).toBe('custom-model');
|
|
156
|
+
expect(ollama.defaultModel).toBe('custom-model');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Vision Support', () => {
|
|
161
|
+
describe('guardImageSize', () => {
|
|
162
|
+
test('passes text blocks through unchanged', () => {
|
|
163
|
+
const block: ContentBlock = { type: 'text', text: 'hello' };
|
|
164
|
+
expect(guardImageSize(block)).toBe(block);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('passes small images through unchanged', () => {
|
|
168
|
+
const block: ContentBlock = {
|
|
169
|
+
type: 'image',
|
|
170
|
+
source: { type: 'base64', media_type: 'image/png', data: 'abc123' },
|
|
171
|
+
};
|
|
172
|
+
expect(guardImageSize(block)).toBe(block);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('replaces oversized images with text warning', () => {
|
|
176
|
+
const bigData = 'x'.repeat(6 * 1024 * 1024); // 6 MB
|
|
177
|
+
const block: ContentBlock = {
|
|
178
|
+
type: 'image',
|
|
179
|
+
source: { type: 'base64', media_type: 'image/png', data: bigData },
|
|
180
|
+
};
|
|
181
|
+
const result = guardImageSize(block);
|
|
182
|
+
expect(result.type).toBe('text');
|
|
183
|
+
expect((result as { type: 'text'; text: string }).text).toContain('too large');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('isToolResult', () => {
|
|
188
|
+
test('returns true for valid ToolResult', () => {
|
|
189
|
+
const tr: ToolResult = {
|
|
190
|
+
content: [{ type: 'text', text: 'hello' }],
|
|
191
|
+
};
|
|
192
|
+
expect(isToolResult(tr)).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('returns false for plain string', () => {
|
|
196
|
+
expect(isToolResult('hello')).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('returns false for null', () => {
|
|
200
|
+
expect(isToolResult(null)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('returns false for object without content array', () => {
|
|
204
|
+
expect(isToolResult({ content: 'not an array' })).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('returns false for object with no content field', () => {
|
|
208
|
+
expect(isToolResult({ data: 'something' })).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('ContentBlock in LLMMessage', () => {
|
|
213
|
+
test('LLMMessage accepts string content', () => {
|
|
214
|
+
const msg: LLMMessage = { role: 'user', content: 'Hello' };
|
|
215
|
+
expect(typeof msg.content).toBe('string');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('LLMMessage accepts ContentBlock[] content', () => {
|
|
219
|
+
const msg: LLMMessage = {
|
|
220
|
+
role: 'tool',
|
|
221
|
+
content: [
|
|
222
|
+
{ type: 'text', text: 'Screenshot captured' },
|
|
223
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } },
|
|
224
|
+
],
|
|
225
|
+
tool_call_id: 'test-id',
|
|
226
|
+
};
|
|
227
|
+
expect(Array.isArray(msg.content)).toBe(true);
|
|
228
|
+
expect((msg.content as ContentBlock[]).length).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type ContentBlock =
|
|
2
|
+
| { type: 'text'; text: string }
|
|
3
|
+
| { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
|
|
4
|
+
|
|
5
|
+
export type LLMMessage = {
|
|
6
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
7
|
+
content: string | ContentBlock[];
|
|
8
|
+
tool_calls?: LLMToolCall[]; // present on assistant messages with tool use
|
|
9
|
+
tool_call_id?: string; // present on tool result messages
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LLMTool = {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: Record<string, unknown>; // JSON Schema
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LLMToolCall = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
arguments: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LLMResponse = {
|
|
25
|
+
content: string;
|
|
26
|
+
tool_calls: LLMToolCall[];
|
|
27
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
28
|
+
model: string;
|
|
29
|
+
finish_reason: 'stop' | 'tool_use' | 'length' | 'error';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type LLMStreamEvent =
|
|
33
|
+
| { type: 'text'; text: string }
|
|
34
|
+
| { type: 'tool_call'; tool_call: LLMToolCall }
|
|
35
|
+
| { type: 'done'; response: LLMResponse }
|
|
36
|
+
| { type: 'error'; error: string };
|
|
37
|
+
|
|
38
|
+
export type LLMOptions = {
|
|
39
|
+
model?: string;
|
|
40
|
+
temperature?: number;
|
|
41
|
+
max_tokens?: number;
|
|
42
|
+
tools?: LLMTool[];
|
|
43
|
+
stream?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface LLMProvider {
|
|
47
|
+
name: string;
|
|
48
|
+
chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse>;
|
|
49
|
+
stream(messages: LLMMessage[], options?: LLMOptions): AsyncIterable<LLMStreamEvent>;
|
|
50
|
+
listModels(): Promise<string[]>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB base64 limit
|
|
54
|
+
|
|
55
|
+
export function guardImageSize(block: ContentBlock): ContentBlock {
|
|
56
|
+
if (block.type === 'image' && block.source.data.length > MAX_IMAGE_BYTES) {
|
|
57
|
+
return { type: 'text', text: '[Image too large to send — saved to disk instead]' };
|
|
58
|
+
}
|
|
59
|
+
return block;
|
|
60
|
+
}
|
package/src/llm/test.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual test file for LLM providers
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun run src/llm/test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LLMManager, AnthropicProvider, OpenAIProvider, OllamaProvider } from './index.ts';
|
|
8
|
+
import { loadConfig } from '../config/index.ts';
|
|
9
|
+
|
|
10
|
+
async function testProviders() {
|
|
11
|
+
console.log('Loading config...');
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
|
|
14
|
+
const manager = new LLMManager();
|
|
15
|
+
|
|
16
|
+
// Register providers based on config
|
|
17
|
+
if (config.llm.anthropic?.api_key) {
|
|
18
|
+
const anthropic = new AnthropicProvider(
|
|
19
|
+
config.llm.anthropic.api_key,
|
|
20
|
+
config.llm.anthropic.model
|
|
21
|
+
);
|
|
22
|
+
manager.registerProvider(anthropic);
|
|
23
|
+
console.log('Registered Anthropic provider');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (config.llm.openai?.api_key) {
|
|
27
|
+
const openai = new OpenAIProvider(
|
|
28
|
+
config.llm.openai.api_key,
|
|
29
|
+
config.llm.openai.model
|
|
30
|
+
);
|
|
31
|
+
manager.registerProvider(openai);
|
|
32
|
+
console.log('Registered OpenAI provider');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.llm.ollama) {
|
|
36
|
+
const ollama = new OllamaProvider(
|
|
37
|
+
config.llm.ollama.base_url,
|
|
38
|
+
config.llm.ollama.model
|
|
39
|
+
);
|
|
40
|
+
manager.registerProvider(ollama);
|
|
41
|
+
console.log('Registered Ollama provider');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Set primary and fallbacks
|
|
45
|
+
manager.setPrimary(config.llm.primary);
|
|
46
|
+
manager.setFallbackChain(config.llm.fallback);
|
|
47
|
+
|
|
48
|
+
console.log(`\nPrimary provider: ${config.llm.primary}`);
|
|
49
|
+
console.log(`Fallback chain: ${config.llm.fallback.join(', ')}\n`);
|
|
50
|
+
|
|
51
|
+
// Test basic chat
|
|
52
|
+
console.log('Testing chat...');
|
|
53
|
+
const messages = [
|
|
54
|
+
{ role: 'system' as const, content: 'You are a helpful assistant.' },
|
|
55
|
+
{ role: 'user' as const, content: 'Say hello in exactly 5 words.' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await manager.chat(messages);
|
|
60
|
+
console.log('Response:', response.content);
|
|
61
|
+
console.log('Model:', response.model);
|
|
62
|
+
console.log('Usage:', response.usage);
|
|
63
|
+
console.log('Finish reason:', response.finish_reason);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Chat failed:', err);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Test streaming
|
|
69
|
+
console.log('\n\nTesting streaming...');
|
|
70
|
+
try {
|
|
71
|
+
for await (const event of manager.stream(messages)) {
|
|
72
|
+
if (event.type === 'text') {
|
|
73
|
+
process.stdout.write(event.text);
|
|
74
|
+
} else if (event.type === 'done') {
|
|
75
|
+
console.log('\n\nStream completed!');
|
|
76
|
+
console.log('Model:', event.response.model);
|
|
77
|
+
console.log('Usage:', event.response.usage);
|
|
78
|
+
} else if (event.type === 'error') {
|
|
79
|
+
console.error('Stream error:', event.error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Stream failed:', err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
testProviders().catch(console.error);
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Observer Layer
|
|
2
|
+
|
|
3
|
+
The Observer Layer monitors system events and emits standardized observations to the Vault. All observers implement a common interface and can be managed centrally through the `ObserverManager`.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
### Common Interface
|
|
8
|
+
|
|
9
|
+
All observers implement the `Observer` interface:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
interface Observer {
|
|
13
|
+
name: string;
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
isRunning(): boolean;
|
|
17
|
+
onEvent(handler: ObserverEventHandler): void;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Events are emitted in a standardized format:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
type ObserverEvent = {
|
|
25
|
+
type: string;
|
|
26
|
+
data: Record<string, unknown>;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Observers
|
|
32
|
+
|
|
33
|
+
### FileWatcher (Fully Implemented)
|
|
34
|
+
|
|
35
|
+
Monitors file system changes in specified directories.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { FileWatcher } from './file-watcher';
|
|
39
|
+
|
|
40
|
+
const watcher = new FileWatcher([
|
|
41
|
+
'/home/user/projects',
|
|
42
|
+
'/home/user/documents'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
watcher.onEvent((event) => {
|
|
46
|
+
// event.type: 'file_change'
|
|
47
|
+
// event.data: { path, eventType: 'rename'|'change', filename, basePath }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await watcher.start();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Features:
|
|
54
|
+
- Recursive directory watching using `node:fs` watch API
|
|
55
|
+
- Debouncing (100ms) to avoid duplicate rapid-fire events
|
|
56
|
+
- Automatic cleanup of old debounce entries to prevent memory leaks
|
|
57
|
+
|
|
58
|
+
### ClipboardMonitor (Fully Implemented)
|
|
59
|
+
|
|
60
|
+
Polls the system clipboard and detects content changes.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { ClipboardMonitor } from './clipboard';
|
|
64
|
+
|
|
65
|
+
const clipboard = new ClipboardMonitor(1000); // Poll every 1 second
|
|
66
|
+
|
|
67
|
+
clipboard.onEvent((event) => {
|
|
68
|
+
// event.type: 'clipboard'
|
|
69
|
+
// event.data: { content, length }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await clipboard.start();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Features:
|
|
76
|
+
- Cross-platform clipboard reading (Linux/macOS/Windows/WSL)
|
|
77
|
+
- Automatic platform detection and command selection
|
|
78
|
+
- Silent failure on read errors to avoid spam
|
|
79
|
+
|
|
80
|
+
Platform commands used:
|
|
81
|
+
- Linux: `xclip` or `xsel`
|
|
82
|
+
- macOS: `pbpaste`
|
|
83
|
+
- Windows/WSL: `powershell.exe Get-Clipboard`
|
|
84
|
+
|
|
85
|
+
### ProcessMonitor (Fully Implemented)
|
|
86
|
+
|
|
87
|
+
Monitors running processes and detects lifecycle changes.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { ProcessMonitor } from './processes';
|
|
91
|
+
|
|
92
|
+
const processes = new ProcessMonitor(5000); // Poll every 5 seconds
|
|
93
|
+
|
|
94
|
+
processes.onEvent((event) => {
|
|
95
|
+
// event.type: 'process_started' or 'process_stopped'
|
|
96
|
+
// event.data: { pid, name, cpu?, memory? }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await processes.start();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Features:
|
|
103
|
+
- Cross-platform process listing (Linux/macOS/Windows)
|
|
104
|
+
- Detects new and terminated processes
|
|
105
|
+
- Provides CPU and memory usage data
|
|
106
|
+
- Automatic cleanup of terminated process records
|
|
107
|
+
|
|
108
|
+
### NotificationListener (Stub)
|
|
109
|
+
|
|
110
|
+
Placeholder for system notification monitoring.
|
|
111
|
+
|
|
112
|
+
**TODO:**
|
|
113
|
+
- Linux: Monitor D-Bus `org.freedesktop.Notifications`
|
|
114
|
+
- Windows: Use PowerShell to read notification center
|
|
115
|
+
- macOS: Use notification center API
|
|
116
|
+
|
|
117
|
+
### CalendarSync (Stub)
|
|
118
|
+
|
|
119
|
+
Placeholder for calendar integration.
|
|
120
|
+
|
|
121
|
+
**TODO:**
|
|
122
|
+
- Google Calendar API integration (OAuth2)
|
|
123
|
+
- Microsoft Graph API (Outlook/Office 365)
|
|
124
|
+
- CalDAV support for generic providers
|
|
125
|
+
|
|
126
|
+
### EmailSync (Stub)
|
|
127
|
+
|
|
128
|
+
Placeholder for email integration.
|
|
129
|
+
|
|
130
|
+
**TODO:**
|
|
131
|
+
- Gmail API integration (OAuth2)
|
|
132
|
+
- Microsoft Graph API (Outlook/Office 365)
|
|
133
|
+
- IMAP support for generic providers
|
|
134
|
+
|
|
135
|
+
## ObserverManager
|
|
136
|
+
|
|
137
|
+
The `ObserverManager` provides centralized control over all observers.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { ObserverManager, FileWatcher, ClipboardMonitor } from './observers';
|
|
141
|
+
|
|
142
|
+
const manager = new ObserverManager();
|
|
143
|
+
|
|
144
|
+
// Register observers
|
|
145
|
+
manager.register(new FileWatcher(['/path/to/watch']));
|
|
146
|
+
manager.register(new ClipboardMonitor(1000));
|
|
147
|
+
|
|
148
|
+
// Set event handler (typically the Vault's ingestion function)
|
|
149
|
+
manager.setEventHandler((event) => {
|
|
150
|
+
console.log(`[${event.type}]`, event.data);
|
|
151
|
+
// Forward to Vault for storage/analysis
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Start all observers
|
|
155
|
+
await manager.startAll();
|
|
156
|
+
|
|
157
|
+
// Get status
|
|
158
|
+
console.log(manager.getStatus());
|
|
159
|
+
// => { 'file-watcher': true, 'clipboard': true }
|
|
160
|
+
|
|
161
|
+
// Stop specific observer
|
|
162
|
+
await manager.stopObserver('clipboard');
|
|
163
|
+
|
|
164
|
+
// Stop all observers
|
|
165
|
+
await manager.stopAll();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Manager API
|
|
169
|
+
|
|
170
|
+
- `register(observer: Observer)` - Register a new observer
|
|
171
|
+
- `setEventHandler(handler: ObserverEventHandler)` - Set global event handler
|
|
172
|
+
- `startAll()` - Start all registered observers
|
|
173
|
+
- `stopAll()` - Stop all registered observers
|
|
174
|
+
- `startObserver(name: string)` - Start specific observer
|
|
175
|
+
- `stopObserver(name: string)` - Stop specific observer
|
|
176
|
+
- `getStatus()` - Get running status of all observers
|
|
177
|
+
- `listObservers()` - Get list of registered observer names
|
|
178
|
+
|
|
179
|
+
## Usage Example
|
|
180
|
+
|
|
181
|
+
See `example.ts` for a complete working example:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bun run src/observers/example.ts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This will run all observers for 30 seconds and log events to the console.
|
|
188
|
+
|
|
189
|
+
## Integration with Vault
|
|
190
|
+
|
|
191
|
+
The Observer Layer is designed to integrate with the Vault for persistent storage:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { ObserverManager } from './observers';
|
|
195
|
+
import { Vault } from '../vault';
|
|
196
|
+
|
|
197
|
+
const vault = new Vault();
|
|
198
|
+
const manager = new ObserverManager();
|
|
199
|
+
|
|
200
|
+
// Register all observers...
|
|
201
|
+
manager.register(new FileWatcher(['/home/user/projects']));
|
|
202
|
+
// ... etc
|
|
203
|
+
|
|
204
|
+
// Connect to Vault
|
|
205
|
+
manager.setEventHandler((event) => {
|
|
206
|
+
vault.storeObservation(event);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await manager.startAll();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Event Types
|
|
213
|
+
|
|
214
|
+
Current event types emitted by observers:
|
|
215
|
+
|
|
216
|
+
- `file_change` - File system change detected
|
|
217
|
+
- `clipboard` - Clipboard content changed
|
|
218
|
+
- `process_started` - New process started
|
|
219
|
+
- `process_stopped` - Process terminated
|
|
220
|
+
|
|
221
|
+
Future event types (when stubs are implemented):
|
|
222
|
+
|
|
223
|
+
- `notification` - System notification received
|
|
224
|
+
- `calendar_event` - Calendar event upcoming/created/updated
|
|
225
|
+
- `new_email` - New email received
|
|
226
|
+
- `email_read` - Email marked as read
|
|
227
|
+
- `email_sent` - Email sent
|
|
228
|
+
|
|
229
|
+
## Performance Considerations
|
|
230
|
+
|
|
231
|
+
- **FileWatcher**: Uses native `node:fs` watch API with minimal overhead
|
|
232
|
+
- **ClipboardMonitor**: Polling-based, configurable interval (default 1s)
|
|
233
|
+
- **ProcessMonitor**: Polling-based, configurable interval (default 5s)
|
|
234
|
+
- **Debouncing**: FileWatcher implements 100ms debouncing for rapid changes
|
|
235
|
+
- **Memory**: Old debounce entries are automatically cleaned up
|
|
236
|
+
|
|
237
|
+
## Error Handling
|
|
238
|
+
|
|
239
|
+
All observers implement graceful error handling:
|
|
240
|
+
|
|
241
|
+
- Silent failures for transient errors (e.g., clipboard read failures)
|
|
242
|
+
- Console logging for persistent errors
|
|
243
|
+
- Proper cleanup in `stop()` methods
|
|
244
|
+
- Error isolation (one observer's failure doesn't affect others)
|
|
245
|
+
|
|
246
|
+
## Platform Support
|
|
247
|
+
|
|
248
|
+
- **Linux**: Full support for all implemented observers
|
|
249
|
+
- **macOS**: Full support for all implemented observers
|
|
250
|
+
- **Windows**: Full support for all implemented observers
|
|
251
|
+
- **WSL**: Full support (uses Windows clipboard via PowerShell)
|
|
252
|
+
|
|
253
|
+
## Testing
|
|
254
|
+
|
|
255
|
+
Run the example to test all observers:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
bun run src/observers/example.ts
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Or create custom tests:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { test, expect } from 'bun:test';
|
|
265
|
+
import { FileWatcher } from './file-watcher';
|
|
266
|
+
|
|
267
|
+
test('FileWatcher starts and stops', async () => {
|
|
268
|
+
const watcher = new FileWatcher(['/tmp']);
|
|
269
|
+
|
|
270
|
+
expect(watcher.isRunning()).toBe(false);
|
|
271
|
+
|
|
272
|
+
await watcher.start();
|
|
273
|
+
expect(watcher.isRunning()).toBe(true);
|
|
274
|
+
|
|
275
|
+
await watcher.stop();
|
|
276
|
+
expect(watcher.isRunning()).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
```
|