@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Tracker — Two-Timeout State Machine
|
|
3
|
+
*
|
|
4
|
+
* Tracks outbound RPC requests with an initial timeout (fast ack)
|
|
5
|
+
* and a max timeout (detached execution). Resolves promises accordingly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RPCState, RPCTimeouts, RPCRequest } from './protocol.ts';
|
|
9
|
+
import { DEFAULT_RPC_TIMEOUTS } from './protocol.ts';
|
|
10
|
+
|
|
11
|
+
export interface PendingRPC {
|
|
12
|
+
id: string;
|
|
13
|
+
sidecarId: string;
|
|
14
|
+
method: string;
|
|
15
|
+
state: RPCState;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
/** Resolves the dispatch() promise. For pending: result value. For detached: "detached" string. */
|
|
18
|
+
resolve: (value: unknown) => void;
|
|
19
|
+
reject: (error: Error) => void;
|
|
20
|
+
initialTimer: Timer | null;
|
|
21
|
+
maxTimer: Timer | null;
|
|
22
|
+
/** Callback invoked when a detached RPC completes */
|
|
23
|
+
detachedCallback?: (rpcId: string, result: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DetachedCompleteCallback = (rpcId: string, result: unknown, error?: Error) => void;
|
|
27
|
+
|
|
28
|
+
export class RPCTracker {
|
|
29
|
+
private pending = new Map<string, PendingRPC>();
|
|
30
|
+
private detachedCallback: DetachedCompleteCallback | null = null;
|
|
31
|
+
|
|
32
|
+
/** Register a global callback for when detached RPCs complete */
|
|
33
|
+
onDetachedComplete(callback: DetachedCompleteCallback): void {
|
|
34
|
+
this.detachedCallback = callback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Dispatch an RPC: creates a tracked entry, sets initial timer.
|
|
39
|
+
* Returns the result if it arrives within initial_timeout, or "detached" if it transitions.
|
|
40
|
+
*/
|
|
41
|
+
dispatch(
|
|
42
|
+
rpcId: string,
|
|
43
|
+
sidecarId: string,
|
|
44
|
+
method: string,
|
|
45
|
+
timeouts: RPCTimeouts = DEFAULT_RPC_TIMEOUTS,
|
|
46
|
+
): Promise<unknown> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const entry: PendingRPC = {
|
|
49
|
+
id: rpcId,
|
|
50
|
+
sidecarId,
|
|
51
|
+
method,
|
|
52
|
+
state: 'pending',
|
|
53
|
+
createdAt: Date.now(),
|
|
54
|
+
resolve,
|
|
55
|
+
reject,
|
|
56
|
+
initialTimer: null,
|
|
57
|
+
maxTimer: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Initial timeout: transition to DETACHED
|
|
61
|
+
entry.initialTimer = setTimeout(() => {
|
|
62
|
+
if (entry.state !== 'pending') return;
|
|
63
|
+
|
|
64
|
+
entry.state = 'detached';
|
|
65
|
+
entry.initialTimer = null;
|
|
66
|
+
|
|
67
|
+
// Resolve the dispatch promise with "detached"
|
|
68
|
+
resolve('detached');
|
|
69
|
+
|
|
70
|
+
// Start max timeout
|
|
71
|
+
entry.maxTimer = setTimeout(() => {
|
|
72
|
+
if (entry.state !== 'detached') return;
|
|
73
|
+
entry.state = 'timed_out';
|
|
74
|
+
this.pending.delete(rpcId);
|
|
75
|
+
console.warn(`[RPCTracker] RPC ${rpcId} (${method}) timed out after max timeout`);
|
|
76
|
+
}, timeouts.max);
|
|
77
|
+
}, timeouts.initial);
|
|
78
|
+
|
|
79
|
+
this.pending.set(rpcId, entry);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Called when an rpc_result event arrives. Resolves/rejects the RPC. */
|
|
84
|
+
resolve(rpcId: string, result: unknown): void {
|
|
85
|
+
const entry = this.pending.get(rpcId);
|
|
86
|
+
if (!entry) return;
|
|
87
|
+
|
|
88
|
+
this.clearTimers(entry);
|
|
89
|
+
|
|
90
|
+
if (entry.state === 'pending') {
|
|
91
|
+
// Fast path: result arrived within initial timeout
|
|
92
|
+
entry.state = 'completed';
|
|
93
|
+
entry.resolve(result);
|
|
94
|
+
} else if (entry.state === 'detached') {
|
|
95
|
+
// Late arrival: dispatch promise already resolved with "detached"
|
|
96
|
+
entry.state = 'completed';
|
|
97
|
+
this.detachedCallback?.(rpcId, result);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.pending.delete(rpcId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Called when an rpc_result with error arrives. */
|
|
104
|
+
fail(rpcId: string, error: Error): void {
|
|
105
|
+
const entry = this.pending.get(rpcId);
|
|
106
|
+
if (!entry) return;
|
|
107
|
+
|
|
108
|
+
this.clearTimers(entry);
|
|
109
|
+
|
|
110
|
+
if (entry.state === 'pending') {
|
|
111
|
+
entry.state = 'failed';
|
|
112
|
+
entry.reject(error);
|
|
113
|
+
} else if (entry.state === 'detached') {
|
|
114
|
+
entry.state = 'failed';
|
|
115
|
+
this.detachedCallback?.(rpcId, undefined, error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.pending.delete(rpcId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Fail all pending RPCs for a disconnected sidecar */
|
|
122
|
+
failAll(sidecarId: string, reason: string): void {
|
|
123
|
+
for (const [rpcId, entry] of this.pending) {
|
|
124
|
+
if (entry.sidecarId !== sidecarId) continue;
|
|
125
|
+
|
|
126
|
+
this.clearTimers(entry);
|
|
127
|
+
|
|
128
|
+
const error = new Error(`Sidecar disconnected: ${reason}`);
|
|
129
|
+
if (entry.state === 'pending') {
|
|
130
|
+
entry.state = 'failed';
|
|
131
|
+
entry.reject(error);
|
|
132
|
+
} else if (entry.state === 'detached') {
|
|
133
|
+
entry.state = 'failed';
|
|
134
|
+
this.detachedCallback?.(rpcId, undefined, error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.pending.delete(rpcId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Get a pending RPC by ID */
|
|
142
|
+
get(rpcId: string): PendingRPC | undefined {
|
|
143
|
+
return this.pending.get(rpcId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Get count of pending RPCs */
|
|
147
|
+
get size(): number {
|
|
148
|
+
return this.pending.size;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private clearTimers(entry: PendingRPC): void {
|
|
152
|
+
if (entry.initialTimer) {
|
|
153
|
+
clearTimeout(entry.initialTimer);
|
|
154
|
+
entry.initialTimer = null;
|
|
155
|
+
}
|
|
156
|
+
if (entry.maxTimer) {
|
|
157
|
+
clearTimeout(entry.maxTimer);
|
|
158
|
+
entry.maxTimer = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Scheduler — Round-Robin Fairness
|
|
3
|
+
*
|
|
4
|
+
* Processes sidecar events with round-robin scheduling across sidecars
|
|
5
|
+
* to prevent any single sidecar from monopolizing event handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SidecarEvent, EventPriority } from './protocol.ts';
|
|
9
|
+
|
|
10
|
+
interface QueuedEvent {
|
|
11
|
+
sidecarId: string;
|
|
12
|
+
event: SidecarEvent;
|
|
13
|
+
priority: EventPriority;
|
|
14
|
+
enqueuedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type EventHandler = (sidecarId: string, event: SidecarEvent) => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export class EventScheduler {
|
|
20
|
+
private queues = new Map<string, QueuedEvent[]>();
|
|
21
|
+
private sidecarIds: string[] = [];
|
|
22
|
+
private roundRobinIndex = 0;
|
|
23
|
+
private handlers = new Map<string, EventHandler[]>();
|
|
24
|
+
private running = false;
|
|
25
|
+
private processing = false;
|
|
26
|
+
private drainTimer: Timer | null = null;
|
|
27
|
+
private readonly drainIntervalMs: number;
|
|
28
|
+
|
|
29
|
+
constructor(drainIntervalMs = 50) {
|
|
30
|
+
this.drainIntervalMs = drainIntervalMs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Register a handler for a specific event_type */
|
|
34
|
+
on(eventType: string, handler: EventHandler): void {
|
|
35
|
+
const existing = this.handlers.get(eventType) ?? [];
|
|
36
|
+
existing.push(handler);
|
|
37
|
+
this.handlers.set(eventType, existing);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Enqueue an event from a sidecar */
|
|
41
|
+
enqueue(sidecarId: string, event: SidecarEvent, priority?: EventPriority): void {
|
|
42
|
+
let queue = this.queues.get(sidecarId);
|
|
43
|
+
if (!queue) {
|
|
44
|
+
queue = [];
|
|
45
|
+
this.queues.set(sidecarId, queue);
|
|
46
|
+
this.sidecarIds.push(sidecarId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
queue.push({
|
|
50
|
+
sidecarId,
|
|
51
|
+
event,
|
|
52
|
+
priority: priority ?? event.priority ?? 'normal',
|
|
53
|
+
enqueuedAt: Date.now(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Sort by priority within each sidecar's queue
|
|
57
|
+
queue.sort((a, b) => priorityWeight(a.priority) - priorityWeight(b.priority));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Remove a sidecar's queue (on disconnect) */
|
|
61
|
+
removeSidecar(sidecarId: string): void {
|
|
62
|
+
this.queues.delete(sidecarId);
|
|
63
|
+
this.sidecarIds = this.sidecarIds.filter(id => id !== sidecarId);
|
|
64
|
+
if (this.roundRobinIndex >= this.sidecarIds.length) {
|
|
65
|
+
this.roundRobinIndex = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Start the processing loop */
|
|
70
|
+
start(): void {
|
|
71
|
+
if (this.running) return;
|
|
72
|
+
this.running = true;
|
|
73
|
+
this.drainTimer = setInterval(() => this.drain(), this.drainIntervalMs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Stop the processing loop */
|
|
77
|
+
stop(): void {
|
|
78
|
+
this.running = false;
|
|
79
|
+
if (this.drainTimer) {
|
|
80
|
+
clearInterval(this.drainTimer);
|
|
81
|
+
this.drainTimer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async drain(): Promise<void> {
|
|
86
|
+
if (this.processing || this.sidecarIds.length === 0) return;
|
|
87
|
+
this.processing = true;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// One round-robin pass: try each sidecar once
|
|
91
|
+
const count = this.sidecarIds.length;
|
|
92
|
+
for (let i = 0; i < count; i++) {
|
|
93
|
+
const idx = (this.roundRobinIndex + i) % count;
|
|
94
|
+
const sidecarId = this.sidecarIds[idx]!;
|
|
95
|
+
const queue = this.queues.get(sidecarId);
|
|
96
|
+
|
|
97
|
+
if (!queue || queue.length === 0) continue;
|
|
98
|
+
|
|
99
|
+
const item = queue.shift()!;
|
|
100
|
+
this.roundRobinIndex = (idx + 1) % count;
|
|
101
|
+
|
|
102
|
+
await this.dispatch(item);
|
|
103
|
+
|
|
104
|
+
// Process one event per drain tick to stay non-blocking
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('[EventScheduler] Drain error:', err);
|
|
109
|
+
} finally {
|
|
110
|
+
this.processing = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async dispatch(item: QueuedEvent): Promise<void> {
|
|
115
|
+
const handlers = this.handlers.get(item.event.event_type) ?? [];
|
|
116
|
+
const wildcardHandlers = this.handlers.get('*') ?? [];
|
|
117
|
+
const allHandlers = [...handlers, ...wildcardHandlers];
|
|
118
|
+
|
|
119
|
+
for (const handler of allHandlers) {
|
|
120
|
+
try {
|
|
121
|
+
await handler(item.sidecarId, item.event);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`[EventScheduler] Handler error for ${item.event.event_type}:`, err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function priorityWeight(p: EventPriority): number {
|
|
130
|
+
switch (p) {
|
|
131
|
+
case 'critical': return 0;
|
|
132
|
+
case 'high': return 1;
|
|
133
|
+
case 'normal': return 2;
|
|
134
|
+
case 'low': return 3;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar Types
|
|
3
|
+
*
|
|
4
|
+
* Types for the brain-side sidecar management system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Capabilities a sidecar can advertise */
|
|
8
|
+
export type SidecarCapability =
|
|
9
|
+
| 'terminal'
|
|
10
|
+
| 'filesystem'
|
|
11
|
+
| 'desktop'
|
|
12
|
+
| 'browser'
|
|
13
|
+
| 'clipboard'
|
|
14
|
+
| 'screenshot'
|
|
15
|
+
| 'system_info'
|
|
16
|
+
| 'awareness';
|
|
17
|
+
|
|
18
|
+
/** A capability that is enabled in config but unavailable on the system */
|
|
19
|
+
export interface UnavailableCapability {
|
|
20
|
+
name: SidecarCapability;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Sidecar status in the database */
|
|
25
|
+
export type SidecarStatus = 'enrolled' | 'revoked';
|
|
26
|
+
|
|
27
|
+
/** Sidecar record as stored in the database */
|
|
28
|
+
export interface SidecarRecord {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
token_id: string;
|
|
32
|
+
enrolled_at: string;
|
|
33
|
+
last_seen_at: string | null;
|
|
34
|
+
status: SidecarStatus;
|
|
35
|
+
/** Populated after first connection */
|
|
36
|
+
hostname: string | null;
|
|
37
|
+
os: string | null;
|
|
38
|
+
platform: string | null;
|
|
39
|
+
/** JSON-encoded SidecarCapability[] — populated after first connection */
|
|
40
|
+
capabilities: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** JWT claims for a sidecar enrollment token */
|
|
44
|
+
export interface SidecarTokenClaims {
|
|
45
|
+
/** Subject: "sidecar:<id>" */
|
|
46
|
+
sub: string;
|
|
47
|
+
/** Unique token ID (for revocation tracking) */
|
|
48
|
+
jti: string;
|
|
49
|
+
/** Sidecar UUID */
|
|
50
|
+
sid: string;
|
|
51
|
+
/** Human-readable sidecar name */
|
|
52
|
+
name: string;
|
|
53
|
+
/** WebSocket URL for the sidecar to connect to */
|
|
54
|
+
brain: string;
|
|
55
|
+
/** URL to fetch the brain's JWKS public key */
|
|
56
|
+
jwks: string;
|
|
57
|
+
/** Issued-at timestamp */
|
|
58
|
+
iat: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Registration message sent by sidecar on WebSocket connect */
|
|
62
|
+
export interface SidecarRegistration {
|
|
63
|
+
type: 'register';
|
|
64
|
+
hostname: string;
|
|
65
|
+
os: string;
|
|
66
|
+
platform: string;
|
|
67
|
+
capabilities: SidecarCapability[];
|
|
68
|
+
unavailable_capabilities?: UnavailableCapability[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Capabilities update message sent by sidecar when config changes */
|
|
72
|
+
export interface SidecarCapabilitiesUpdate {
|
|
73
|
+
type: 'capabilities_update';
|
|
74
|
+
capabilities: SidecarCapability[];
|
|
75
|
+
unavailable_capabilities?: UnavailableCapability[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A connected sidecar (runtime state, not persisted) */
|
|
79
|
+
export interface ConnectedSidecar {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
hostname: string;
|
|
83
|
+
os: string;
|
|
84
|
+
platform: string;
|
|
85
|
+
capabilities: SidecarCapability[];
|
|
86
|
+
unavailableCapabilities: UnavailableCapability[];
|
|
87
|
+
connectedAt: Date;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Sidecar config as returned by get_config RPC (token excluded) */
|
|
91
|
+
export interface SidecarConfig {
|
|
92
|
+
capabilities: SidecarCapability[];
|
|
93
|
+
terminal: { blocked_commands: string[]; default_shell: string; timeout_ms: number };
|
|
94
|
+
filesystem: { blocked_paths: string[]; max_file_size_kb: number };
|
|
95
|
+
browser: { cdp_port: number; profile_dir: string };
|
|
96
|
+
awareness: { screen_interval_ms: number; window_interval_ms: number; min_change_threshold: number; stuck_threshold_ms: number };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Sidecar info returned by API (DB record + connection state) */
|
|
100
|
+
export interface SidecarInfo {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
enrolled_at: string;
|
|
104
|
+
last_seen_at: string | null;
|
|
105
|
+
status: SidecarStatus;
|
|
106
|
+
connected: boolean;
|
|
107
|
+
hostname?: string;
|
|
108
|
+
os?: string;
|
|
109
|
+
platform?: string;
|
|
110
|
+
capabilities?: SidecarCapability[];
|
|
111
|
+
unavailable_capabilities?: UnavailableCapability[];
|
|
112
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar Validator — Schema Validation & Sanitization
|
|
3
|
+
*
|
|
4
|
+
* Validates inbound sidecar events and binary frames.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SidecarEvent, EventPriority } from './protocol.ts';
|
|
8
|
+
|
|
9
|
+
// Size limits
|
|
10
|
+
export const MAX_JSON_SIZE = 1 * 1024 * 1024; // 1 MB
|
|
11
|
+
export const MAX_BINARY_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
12
|
+
export const BINARY_INLINE_THRESHOLD = 256 * 1024; // 256 KB
|
|
13
|
+
export const BINARY_REF_ID_LENGTH = 36; // UUID length
|
|
14
|
+
|
|
15
|
+
export interface ValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
event?: SidecarEvent;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BinaryValidationResult {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
refId?: string;
|
|
24
|
+
payload?: Buffer;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VALID_EVENT_TYPES = new Set([
|
|
29
|
+
'rpc_result', 'rpc_progress', 'sidecar_event',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const VALID_PRIORITIES = new Set<EventPriority>([
|
|
33
|
+
'critical', 'high', 'normal', 'low',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Recursively strip dangerous keys from an object.
|
|
40
|
+
*/
|
|
41
|
+
function sanitize(obj: unknown): unknown {
|
|
42
|
+
if (obj === null || obj === undefined) return obj;
|
|
43
|
+
if (typeof obj !== 'object') return obj;
|
|
44
|
+
if (Array.isArray(obj)) return obj.map(sanitize);
|
|
45
|
+
|
|
46
|
+
const clean: Record<string, unknown> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
48
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
49
|
+
clean[key] = sanitize(value);
|
|
50
|
+
}
|
|
51
|
+
return clean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate and sanitize an inbound sidecar event (parsed JSON).
|
|
56
|
+
*/
|
|
57
|
+
export function validateEvent(raw: unknown): ValidationResult {
|
|
58
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
59
|
+
return { valid: false, error: 'Event must be a JSON object' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const obj = raw as Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
// type
|
|
65
|
+
if (typeof obj.type !== 'string' || !VALID_EVENT_TYPES.has(obj.type)) {
|
|
66
|
+
return { valid: false, error: `Invalid event type: ${String(obj.type)}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// event_type
|
|
70
|
+
if (typeof obj.event_type !== 'string' || obj.event_type.length === 0) {
|
|
71
|
+
return { valid: false, error: 'Missing or empty event_type' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// timestamp
|
|
75
|
+
if (typeof obj.timestamp !== 'number' || !Number.isFinite(obj.timestamp)) {
|
|
76
|
+
return { valid: false, error: 'Invalid timestamp' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// payload
|
|
80
|
+
if (typeof obj.payload !== 'object' || obj.payload === null || Array.isArray(obj.payload)) {
|
|
81
|
+
return { valid: false, error: 'Payload must be a JSON object' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate payload fields per type
|
|
85
|
+
const payload = obj.payload as Record<string, unknown>;
|
|
86
|
+
|
|
87
|
+
if (obj.type === 'rpc_result') {
|
|
88
|
+
if (typeof payload.rpc_id !== 'string') {
|
|
89
|
+
return { valid: false, error: 'rpc_result requires payload.rpc_id (string)' };
|
|
90
|
+
}
|
|
91
|
+
if (payload.result === undefined && payload.error === undefined) {
|
|
92
|
+
return { valid: false, error: 'rpc_result requires payload.result or payload.error' };
|
|
93
|
+
}
|
|
94
|
+
if (payload.error !== undefined) {
|
|
95
|
+
const err = payload.error as Record<string, unknown>;
|
|
96
|
+
if (typeof err !== 'object' || typeof err.code !== 'string' || typeof err.message !== 'string') {
|
|
97
|
+
return { valid: false, error: 'rpc_result error must have code and message strings' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (obj.type === 'rpc_progress') {
|
|
103
|
+
if (typeof payload.rpc_id !== 'string') {
|
|
104
|
+
return { valid: false, error: 'rpc_progress requires payload.rpc_id (string)' };
|
|
105
|
+
}
|
|
106
|
+
if (typeof payload.progress !== 'number') {
|
|
107
|
+
return { valid: false, error: 'rpc_progress requires payload.progress (number)' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// priority (optional)
|
|
112
|
+
if (obj.priority !== undefined && !VALID_PRIORITIES.has(obj.priority as EventPriority)) {
|
|
113
|
+
return { valid: false, error: `Invalid priority: ${String(obj.priority)}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Sanitize the entire event
|
|
117
|
+
const sanitized = sanitize(obj) as SidecarEvent;
|
|
118
|
+
|
|
119
|
+
return { valid: true, event: sanitized };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate a binary frame. First 36 bytes = ref_id (ASCII UUID), rest = payload.
|
|
124
|
+
*/
|
|
125
|
+
export function validateBinaryFrame(data: Buffer): BinaryValidationResult {
|
|
126
|
+
if (data.length < BINARY_REF_ID_LENGTH) {
|
|
127
|
+
return { valid: false, error: `Binary frame too small: ${data.length} bytes (min ${BINARY_REF_ID_LENGTH})` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (data.length > MAX_BINARY_SIZE) {
|
|
131
|
+
return { valid: false, error: `Binary frame too large: ${data.length} bytes (max ${MAX_BINARY_SIZE})` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const refId = data.subarray(0, BINARY_REF_ID_LENGTH).toString('ascii');
|
|
135
|
+
|
|
136
|
+
// Basic UUID format check
|
|
137
|
+
if (!/^[0-9a-f-]{36}$/i.test(refId)) {
|
|
138
|
+
return { valid: false, error: `Invalid ref_id format: ${refId}` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const payload = data.subarray(BINARY_REF_ID_LENGTH);
|
|
142
|
+
|
|
143
|
+
return { valid: true, refId, payload };
|
|
144
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Vault Module
|
|
2
|
+
|
|
3
|
+
Knowledge graph storage and extraction system for J.A.R.V.I.S.
|
|
4
|
+
|
|
5
|
+
## Modules
|
|
6
|
+
|
|
7
|
+
- **`schema.ts`**: Database schema and initialization
|
|
8
|
+
- **`entities.ts`**: Entity CRUD operations
|
|
9
|
+
- **`facts.ts`**: Fact storage and queries
|
|
10
|
+
- **`relationships.ts`**: Relationship management
|
|
11
|
+
- **`commitments.ts`**: Promise and task tracking
|
|
12
|
+
- **`observations.ts`**: Raw event storage
|
|
13
|
+
- **`vectors.ts`**: Embedding storage for semantic search
|
|
14
|
+
- **`extractor.ts`**: LLM-powered knowledge extraction
|
|
15
|
+
- **`index.ts`**: Public API exports
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Initialize Database
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { initDatabase } from '@/vault';
|
|
23
|
+
|
|
24
|
+
initDatabase('./data/jarvis.db');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Store Knowledge Manually
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createEntity, createFact, createRelationship } from '@/vault';
|
|
31
|
+
|
|
32
|
+
// Create entity
|
|
33
|
+
const anna = createEntity('person', 'Anna', {
|
|
34
|
+
role: 'software engineer',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Add fact
|
|
38
|
+
createFact(anna.id, 'birthday_is', 'March 15', {
|
|
39
|
+
confidence: 1.0,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create relationship
|
|
43
|
+
const google = createEntity('tool', 'Google');
|
|
44
|
+
createRelationship(anna.id, google.id, 'works_at');
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Extract Knowledge with LLM
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { extractAndStore } from '@/vault';
|
|
51
|
+
import { AnthropicProvider } from '@/llm/anthropic';
|
|
52
|
+
|
|
53
|
+
const llm = new AnthropicProvider(process.env.ANTHROPIC_API_KEY);
|
|
54
|
+
|
|
55
|
+
const result = await extractAndStore(
|
|
56
|
+
"Anna's birthday is March 15th",
|
|
57
|
+
"I'll remember that!",
|
|
58
|
+
llm
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Query Knowledge
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { findEntities, findFacts, findCommitments } from '@/vault';
|
|
66
|
+
|
|
67
|
+
// Find people
|
|
68
|
+
const people = findEntities({ type: 'person' });
|
|
69
|
+
|
|
70
|
+
// Find facts about Anna
|
|
71
|
+
const facts = findFacts({ subject_id: anna.id });
|
|
72
|
+
|
|
73
|
+
// Find pending commitments
|
|
74
|
+
const pending = findCommitments({ status: 'pending' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Documentation
|
|
78
|
+
|
|
79
|
+
- **Vault Extractor**: [docs/VAULT_EXTRACTOR.md](~/jarvis/docs/VAULT_EXTRACTOR.md)
|
|
80
|
+
- **Entity Types**: person, project, tool, place, concept, event
|
|
81
|
+
- **Commitment Statuses**: pending, active, completed, failed, escalated
|
|
82
|
+
|
|
83
|
+
## Testing
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Test extractor
|
|
87
|
+
bun test src/vault/extractor.test.ts
|
|
88
|
+
|
|
89
|
+
# Test entire vault (if you have other tests)
|
|
90
|
+
bun test src/vault/**/*.test.ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Demo
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bun run examples/extractor-demo.ts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Database Schema
|
|
100
|
+
|
|
101
|
+
SQLite database with the following tables:
|
|
102
|
+
- `entities`: People, projects, tools, places, concepts, events
|
|
103
|
+
- `facts`: Atomic knowledge with confidence scores
|
|
104
|
+
- `relationships`: Typed edges between entities
|
|
105
|
+
- `commitments`: Promises and tasks
|
|
106
|
+
- `observations`: Raw events
|
|
107
|
+
- `vectors`: Embeddings for semantic search
|
|
108
|
+
- `agent_messages`: Inter-agent communication
|
|
109
|
+
- `personality_state`: Personality model storage
|
|
110
|
+
- `conversations`: Conversation tracking
|