@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,473 @@
|
|
|
1
|
+
import type { Server, ServerWebSocket } from 'bun';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { SidecarManager } from '../sidecar/manager.ts';
|
|
4
|
+
|
|
5
|
+
export type WSMessage = {
|
|
6
|
+
type: 'chat' | 'command' | 'status' | 'stream' | 'error' | 'notification'
|
|
7
|
+
| 'tts_start' | 'tts_end' | 'voice_start' | 'voice_end'
|
|
8
|
+
| 'workflow_event'
|
|
9
|
+
| 'goal_event';
|
|
10
|
+
payload: unknown;
|
|
11
|
+
id?: string;
|
|
12
|
+
priority?: 'urgent' | 'normal' | 'low';
|
|
13
|
+
timestamp: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WSClientHandler = {
|
|
17
|
+
onMessage: (msg: WSMessage, ws: ServerWebSocket<unknown>) => Promise<WSMessage | void>;
|
|
18
|
+
onBinaryMessage?: (data: Buffer, ws: ServerWebSocket<unknown>) => Promise<void>;
|
|
19
|
+
onConnect: (ws: ServerWebSocket<unknown>) => void;
|
|
20
|
+
onDisconnect: (ws: ServerWebSocket<unknown>) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RouteHandler = (req: Request) => Response | Promise<Response>;
|
|
24
|
+
type MethodRoutes = { [method: string]: RouteHandler };
|
|
25
|
+
|
|
26
|
+
/** 401 HTML page loaded from auth-error.html */
|
|
27
|
+
const AUTH_ERROR_HTML = await Bun.file(path.join(import.meta.dir, 'auth-error.html')).text();
|
|
28
|
+
|
|
29
|
+
/** Inline script injected into authed HTML pages — strips ?token= from the hash. */
|
|
30
|
+
const TOKEN_STRIP_SCRIPT = `<script>(function(){var h=location.hash,i=h.indexOf('?');if(i===-1)return;var p=new URLSearchParams(h.slice(i));if(!p.has('token'))return;p.delete('token');var c=h.slice(0,i),r=p.toString();if(r)c+='?'+r;location.replace(location.pathname+location.search+c)})()</script>`;
|
|
31
|
+
|
|
32
|
+
function getCookie(req: Request, name: string): string | null {
|
|
33
|
+
const cookies = req.headers.get('Cookie');
|
|
34
|
+
if (!cookies) return null;
|
|
35
|
+
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
36
|
+
return match ? decodeURIComponent(match[1]!) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isPublicRoute(pathname: string, method: string): boolean {
|
|
40
|
+
return (
|
|
41
|
+
pathname === '/health' ||
|
|
42
|
+
pathname === '/sidecar/connect' ||
|
|
43
|
+
pathname === '/api/sidecars/.well-known/jwks.json' ||
|
|
44
|
+
pathname.startsWith('/api/webhooks/') ||
|
|
45
|
+
method === 'OPTIONS'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class WebSocketServer {
|
|
50
|
+
private server: Server<any> | null = null;
|
|
51
|
+
private clients: Set<ServerWebSocket<unknown>> = new Set();
|
|
52
|
+
private handler: WSClientHandler | null = null;
|
|
53
|
+
private port: number;
|
|
54
|
+
private startTime: number = 0;
|
|
55
|
+
private apiRoutes: Map<string, MethodRoutes> = new Map();
|
|
56
|
+
private staticDir: string | null = null;
|
|
57
|
+
private publicDir: string | null = null;
|
|
58
|
+
private sidecarManager: SidecarManager | null = null;
|
|
59
|
+
private authToken: string | null = null;
|
|
60
|
+
|
|
61
|
+
constructor(port: number = 3142) {
|
|
62
|
+
this.port = port;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setAuthToken(token: string): void {
|
|
66
|
+
this.authToken = token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setHandler(handler: WSClientHandler): void {
|
|
70
|
+
this.handler = handler;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setSidecarManager(manager: SidecarManager): void {
|
|
74
|
+
this.sidecarManager = manager;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register API route handlers (method-based).
|
|
79
|
+
* Example: setApiRoutes({ '/api/health': { GET: handler } })
|
|
80
|
+
*/
|
|
81
|
+
setApiRoutes(routes: Record<string, MethodRoutes>): void {
|
|
82
|
+
for (const [path, methods] of Object.entries(routes)) {
|
|
83
|
+
this.apiRoutes.set(path, methods);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set directory for serving static files (pre-built dashboard).
|
|
89
|
+
*/
|
|
90
|
+
setStaticDir(dir: string): void {
|
|
91
|
+
this.staticDir = dir;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set directory for serving public assets (models, WASM, etc.).
|
|
96
|
+
* Falls through to this if file not found in staticDir.
|
|
97
|
+
*/
|
|
98
|
+
setPublicDir(dir: string): void {
|
|
99
|
+
this.publicDir = dir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
start(): void {
|
|
103
|
+
if (this.server) {
|
|
104
|
+
console.warn('[WebSocketServer] Server already running');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.startTime = Date.now();
|
|
109
|
+
const self = this;
|
|
110
|
+
|
|
111
|
+
this.server = Bun.serve<{ sidecar_id?: string }>({
|
|
112
|
+
port: this.port,
|
|
113
|
+
idleTimeout: 30, // seconds — prevent timeout during heavy processing (OCR, PowerShell)
|
|
114
|
+
|
|
115
|
+
async fetch(req, server) {
|
|
116
|
+
const url = new URL(req.url);
|
|
117
|
+
const pathname = url.pathname;
|
|
118
|
+
|
|
119
|
+
// 0. Sidecar WebSocket upgrade (has its own JWT auth)
|
|
120
|
+
if (pathname === '/sidecar/connect' && self.sidecarManager) {
|
|
121
|
+
const authHeader = req.headers.get('Authorization');
|
|
122
|
+
const token = authHeader?.startsWith('Bearer ')
|
|
123
|
+
? authHeader.slice(7)
|
|
124
|
+
: null;
|
|
125
|
+
if (!token) {
|
|
126
|
+
return new Response('Missing token', { status: 401 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const claims = await self.sidecarManager.validateToken(token);
|
|
130
|
+
if (!claims) {
|
|
131
|
+
return new Response('Invalid or revoked token', { status: 403 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const success = server.upgrade(req, { data: { sidecar_id: claims.sid } });
|
|
135
|
+
if (success) return undefined;
|
|
136
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 1. Auth check (if configured)
|
|
140
|
+
if (self.authToken && !isPublicRoute(pathname, req.method)) {
|
|
141
|
+
const cookieToken = getCookie(req, 'token');
|
|
142
|
+
if (cookieToken !== self.authToken) {
|
|
143
|
+
// Check ?token= query param — set cookie via Set-Cookie and redirect
|
|
144
|
+
const queryToken = url.searchParams.get('token');
|
|
145
|
+
if (queryToken === self.authToken) {
|
|
146
|
+
const cleanParams = new URLSearchParams(url.searchParams);
|
|
147
|
+
cleanParams.delete('token');
|
|
148
|
+
const qs = cleanParams.toString();
|
|
149
|
+
const redirectTo = pathname + (qs ? '?' + qs : '');
|
|
150
|
+
return new Response(null, {
|
|
151
|
+
status: 302,
|
|
152
|
+
headers: {
|
|
153
|
+
'Location': redirectTo || '/',
|
|
154
|
+
'Set-Cookie': `token=${queryToken}; Path=/; SameSite=Lax`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// No valid auth — API & WebSocket get JSON 401; browsers get the auth error page
|
|
159
|
+
if (pathname.startsWith('/api/') || pathname === '/ws') {
|
|
160
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
161
|
+
}
|
|
162
|
+
return new Response(AUTH_ERROR_HTML, {
|
|
163
|
+
status: 401,
|
|
164
|
+
headers: { 'Content-Type': 'text/html' },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. WebSocket upgrade
|
|
170
|
+
if (pathname === '/ws') {
|
|
171
|
+
const success = server.upgrade(req, { data: {} });
|
|
172
|
+
if (success) return undefined;
|
|
173
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3. Health check (always public)
|
|
177
|
+
if (pathname === '/health') {
|
|
178
|
+
return Response.json({
|
|
179
|
+
status: 'ok',
|
|
180
|
+
uptime: Date.now() - self.startTime,
|
|
181
|
+
clients: self.clients.size,
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. API routes
|
|
187
|
+
if (pathname.startsWith('/api/')) {
|
|
188
|
+
// Handle CORS preflight
|
|
189
|
+
if (req.method === 'OPTIONS') {
|
|
190
|
+
return new Response(null, {
|
|
191
|
+
status: 204,
|
|
192
|
+
headers: {
|
|
193
|
+
'Access-Control-Allow-Origin': '*',
|
|
194
|
+
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
|
|
195
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Try exact match first
|
|
201
|
+
const exactRoute = self.apiRoutes.get(pathname);
|
|
202
|
+
if (exactRoute) {
|
|
203
|
+
const handler = exactRoute[req.method];
|
|
204
|
+
if (handler) return handler(req);
|
|
205
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try parameterized routes (e.g., /api/vault/entities/:id)
|
|
209
|
+
for (const [pattern, methods] of self.apiRoutes) {
|
|
210
|
+
const params = matchRoute(pattern, pathname);
|
|
211
|
+
if (params) {
|
|
212
|
+
const handler = methods[req.method];
|
|
213
|
+
if (handler) {
|
|
214
|
+
// Attach params to request
|
|
215
|
+
(req as any).params = params;
|
|
216
|
+
return handler(req);
|
|
217
|
+
}
|
|
218
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 5a. Overlay widget (served from ui/ source, not dist/)
|
|
226
|
+
if (pathname === '/overlay' && self.staticDir) {
|
|
227
|
+
// overlay.html lives in the ui/ source directory (parent of dist/)
|
|
228
|
+
const overlayPath = path.join(self.staticDir, '..', 'overlay.html');
|
|
229
|
+
const overlayFile = Bun.file(overlayPath);
|
|
230
|
+
if (await overlayFile.exists()) {
|
|
231
|
+
if (self.authToken) {
|
|
232
|
+
const html = await overlayFile.text();
|
|
233
|
+
return new Response(injectTokenStrip(html), { headers: { 'Content-Type': 'text/html' } });
|
|
234
|
+
}
|
|
235
|
+
return new Response(overlayFile, { headers: { 'Content-Type': 'text/html' } });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 5b. Static files (dashboard)
|
|
240
|
+
if (self.staticDir) {
|
|
241
|
+
let filePath: string;
|
|
242
|
+
|
|
243
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
244
|
+
filePath = path.join(self.staticDir, 'index.html');
|
|
245
|
+
} else {
|
|
246
|
+
// Serve JS/CSS/assets
|
|
247
|
+
filePath = path.join(self.staticDir, pathname);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const file = Bun.file(filePath);
|
|
251
|
+
if (await file.exists()) {
|
|
252
|
+
if (self.authToken && filePath.endsWith('.html')) {
|
|
253
|
+
const html = await file.text();
|
|
254
|
+
return new Response(injectTokenStrip(html), { headers: { 'Content-Type': 'text/html' } });
|
|
255
|
+
}
|
|
256
|
+
return new Response(file);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 6. Public assets fallback (models, WASM, etc.)
|
|
261
|
+
if (self.publicDir) {
|
|
262
|
+
const publicPath = path.join(self.publicDir, pathname);
|
|
263
|
+
const publicFile = Bun.file(publicPath);
|
|
264
|
+
if (await publicFile.exists()) {
|
|
265
|
+
return new Response(publicFile);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return new Response('Not Found', { status: 404 });
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
websocket: {
|
|
273
|
+
open(ws) {
|
|
274
|
+
const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
|
|
275
|
+
if (sidecarId && self.sidecarManager) {
|
|
276
|
+
self.sidecarManager.handleSidecarConnect(ws, sidecarId);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
self.clients.add(ws);
|
|
281
|
+
console.log('[WebSocketServer] Client connected. Total clients:', self.clients.size);
|
|
282
|
+
self.handler?.onConnect(ws);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async message(ws, message) {
|
|
286
|
+
const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
|
|
287
|
+
if (sidecarId && self.sidecarManager) {
|
|
288
|
+
self.sidecarManager.handleSidecarMessage(ws, message);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Binary frame = audio data (mic audio from client)
|
|
293
|
+
if (message instanceof Buffer) {
|
|
294
|
+
if (self.handler?.onBinaryMessage) {
|
|
295
|
+
try {
|
|
296
|
+
await self.handler.onBinaryMessage(message, ws);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('[WebSocketServer] Error processing binary message:', error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Text frame = JSON message (existing protocol)
|
|
305
|
+
try {
|
|
306
|
+
const msg: WSMessage = JSON.parse(message.toString());
|
|
307
|
+
console.log('[WebSocketServer] Received:', msg.type, msg.id);
|
|
308
|
+
|
|
309
|
+
if (self.handler) {
|
|
310
|
+
const response = await self.handler.onMessage(msg, ws);
|
|
311
|
+
if (response) {
|
|
312
|
+
ws.send(JSON.stringify(response));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('[WebSocketServer] Error processing message:', error);
|
|
317
|
+
const errorMsg: WSMessage = {
|
|
318
|
+
type: 'error',
|
|
319
|
+
payload: {
|
|
320
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
321
|
+
},
|
|
322
|
+
timestamp: Date.now(),
|
|
323
|
+
};
|
|
324
|
+
ws.send(JSON.stringify(errorMsg));
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
pong(ws) {
|
|
329
|
+
const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
|
|
330
|
+
if (sidecarId && self.sidecarManager) {
|
|
331
|
+
self.sidecarManager.handleSidecarPong(sidecarId);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
close(ws) {
|
|
336
|
+
const sidecarId = (ws.data as any)?.sidecar_id as string | undefined;
|
|
337
|
+
if (sidecarId && self.sidecarManager) {
|
|
338
|
+
self.sidecarManager.handleSidecarDisconnect(sidecarId);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
self.clients.delete(ws);
|
|
343
|
+
console.log('[WebSocketServer] Client disconnected. Total clients:', self.clients.size);
|
|
344
|
+
self.handler?.onDisconnect(ws);
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
console.log(`[WebSocketServer] Started on ws://localhost:${this.port}/ws`);
|
|
350
|
+
console.log(`[WebSocketServer] Health endpoint: http://localhost:${this.port}/health`);
|
|
351
|
+
if (this.staticDir) {
|
|
352
|
+
console.log(`[WebSocketServer] Dashboard: http://localhost:${this.port}/`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
stop(): void {
|
|
357
|
+
if (this.server) {
|
|
358
|
+
this.server.stop();
|
|
359
|
+
this.server = null;
|
|
360
|
+
this.clients.clear();
|
|
361
|
+
console.log('[WebSocketServer] Stopped');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
broadcast(message: WSMessage): void {
|
|
366
|
+
const payload = JSON.stringify(message);
|
|
367
|
+
let sent = 0;
|
|
368
|
+
|
|
369
|
+
for (const client of this.clients) {
|
|
370
|
+
try {
|
|
371
|
+
client.send(payload);
|
|
372
|
+
sent++;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error('[WebSocketServer] Error broadcasting to client:', error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Only log errors or when no clients received the message
|
|
379
|
+
if (sent === 0 && this.clients.size > 0) {
|
|
380
|
+
console.warn(`[WebSocketServer] Broadcast failed: 0/${this.clients.size} clients received message`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
send(client: ServerWebSocket<unknown>, message: WSMessage): void {
|
|
385
|
+
try {
|
|
386
|
+
client.send(JSON.stringify(message));
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error('[WebSocketServer] Error sending to client:', error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Unicast a JSON message to a specific client (e.g. tts_start/tts_end signals).
|
|
394
|
+
*/
|
|
395
|
+
sendToClient(ws: ServerWebSocket<unknown>, message: WSMessage): void {
|
|
396
|
+
try {
|
|
397
|
+
ws.send(JSON.stringify(message));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('[WebSocketServer] Error unicasting to client:', error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Unicast binary data to a specific client (e.g. TTS audio chunks).
|
|
405
|
+
*/
|
|
406
|
+
sendBinary(ws: ServerWebSocket<unknown>, data: Buffer): void {
|
|
407
|
+
try {
|
|
408
|
+
ws.sendBinary(data);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('[WebSocketServer] Error sending binary to client:', error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
isRunning(): boolean {
|
|
415
|
+
return this.server !== null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getPort(): number {
|
|
419
|
+
return this.port;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getClientCount(): number {
|
|
423
|
+
return this.clients.size;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
getClients(): Set<ServerWebSocket<unknown>> {
|
|
427
|
+
return this.clients;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Inject the token-stripping script into an HTML page (right after <head>).
|
|
433
|
+
*/
|
|
434
|
+
function injectTokenStrip(html: string): string {
|
|
435
|
+
const headIdx = html.indexOf('<head>');
|
|
436
|
+
if (headIdx !== -1) {
|
|
437
|
+
return html.slice(0, headIdx + 6) + TOKEN_STRIP_SCRIPT + html.slice(headIdx + 6);
|
|
438
|
+
}
|
|
439
|
+
const htmlIdx = html.indexOf('<html');
|
|
440
|
+
if (htmlIdx !== -1) {
|
|
441
|
+
const closeTag = html.indexOf('>', htmlIdx);
|
|
442
|
+
if (closeTag !== -1) {
|
|
443
|
+
return html.slice(0, closeTag + 1) + TOKEN_STRIP_SCRIPT + html.slice(closeTag + 1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return TOKEN_STRIP_SCRIPT + html;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Match a route pattern like '/api/vault/entities/:id/facts' against a pathname.
|
|
451
|
+
* Returns params object if matched, null otherwise.
|
|
452
|
+
*/
|
|
453
|
+
function matchRoute(pattern: string, pathname: string): Record<string, string> | null {
|
|
454
|
+
// Skip wildcard patterns
|
|
455
|
+
if (pattern.includes('*')) return null;
|
|
456
|
+
|
|
457
|
+
const patternParts = pattern.split('/');
|
|
458
|
+
const pathParts = pathname.split('/');
|
|
459
|
+
|
|
460
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
461
|
+
|
|
462
|
+
const params: Record<string, string> = {};
|
|
463
|
+
|
|
464
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
465
|
+
if (patternParts[i]!.startsWith(':')) {
|
|
466
|
+
params[patternParts[i]!.slice(1)] = pathParts[i]!;
|
|
467
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
473
|
+
}
|