@shykaruu/jarvis-brain 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -204,7 +204,7 @@ This means you can run the daemon on an always-on server and still interact with
204
204
  **Via bun:**
205
205
 
206
206
  ```bash
207
- bun install -g @usejarvis/sidecar
207
+ bun install -g @shykaruu/jarvis-sidecar
208
208
  ```
209
209
 
210
210
  **Or download the binary** from [GitHub Releases](https://github.com/vierisid/jarvis/releases) for your platform (macOS, Linux, Windows).
@@ -232,6 +232,8 @@ jarvis-sidecar
232
232
 
233
233
  Once connected, the sidecar appears as online in the Settings page where you can configure its capabilities (terminal, filesystem, desktop, browser, clipboard, screenshot, awareness).
234
234
 
235
+ If your daemon is hosted remotely behind DNS/TLS or a reverse proxy, set `daemon.brain_url` in `~/.jarvis/config.yaml` so sidecar enrollment tokens point at the correct WebSocket endpoint. If unset, JARVIS preserves the existing localhost fallback.
236
+
235
237
  ---
236
238
 
237
239
  ## 🧠 Core Capabilities
@@ -291,6 +293,8 @@ daemon:
291
293
  port: 3142
292
294
  data_dir: "~/.jarvis"
293
295
  db_path: "~/.jarvis/jarvis.db"
296
+ # Optional full sidecar WebSocket URL for remote deployments
297
+ # brain_url: "wss://your-domain.example/sidecar"
294
298
 
295
299
  llm:
296
300
  primary: "anthropic"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shykaruu/jarvis-brain",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "J.A.R.V.I.S. — Just A Rather Very Intelligent System. An always-on autonomous AI daemon.",
5
5
  "module": "src/daemon/index.ts",
6
6
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/vierisid/jarvis.git"
15
+ "url": "git+https://github.com/Shykaruu/jarvis.git"
16
16
  },
17
17
  "license": "SEE LICENSE IN LICENSE",
18
18
  "keywords": ["jarvis", "ai", "daemon", "assistant", "cli"],
@@ -363,6 +363,39 @@ test('WebSocketServer - WebSocket allowed with auth cookie', async () => {
363
363
  }
364
364
  });
365
365
 
366
+ test('WebSocketServer - sidecar websocket accepts /sidecar path with bearer auth', async () => {
367
+ const authServer = new WebSocketServer(3156);
368
+ authServer.setSidecarManager({
369
+ async validateToken(token: string) {
370
+ if (token === 'sidecar-token') {
371
+ return { sid: 'sidecar-123' } as any;
372
+ }
373
+ return null;
374
+ },
375
+ handleSidecarConnect() {},
376
+ handleSidecarDisconnect() {},
377
+ } as any);
378
+ authServer.start();
379
+
380
+ try {
381
+ const ws = new WebSocket('ws://localhost:3156/sidecar', {
382
+ headers: { Authorization: 'Bearer sidecar-token' },
383
+ } as any);
384
+
385
+ const connected = await new Promise<boolean>((resolve) => {
386
+ ws.onopen = () => resolve(true);
387
+ ws.onerror = () => resolve(false);
388
+ setTimeout(() => resolve(false), 2000);
389
+ });
390
+
391
+ expect(connected).toBe(true);
392
+ ws.close();
393
+ await new Promise((resolve) => setTimeout(resolve, 100));
394
+ } finally {
395
+ authServer.stop();
396
+ }
397
+ });
398
+
366
399
  test('WebSocketServer - sendToClient unicasts JSON', async () => {
367
400
  let serverWsRef: any = null;
368
401
 
@@ -38,6 +38,7 @@ const TOKEN_STRIP_SCRIPT = `<script>(function(){var h=location.hash,i=h.indexOf(
38
38
  function isPublicRoute(pathname: string, method: string): boolean {
39
39
  return (
40
40
  pathname === '/health' ||
41
+ pathname === '/sidecar' ||
41
42
  pathname === '/sidecar/connect' ||
42
43
  pathname === '/api/sidecars/.well-known/jwks.json' ||
43
44
  pathname === '/api/auth/login' ||
@@ -157,7 +158,7 @@ export class WebSocketServer {
157
158
  const pathname = url.pathname;
158
159
 
159
160
  // 0. Sidecar WebSocket upgrade (has its own JWT auth)
160
- if (pathname === '/sidecar/connect' && self.sidecarManager) {
161
+ if ((pathname === '/sidecar' || pathname === '/sidecar/connect') && self.sidecarManager) {
161
162
  const authHeader = req.headers.get('Authorization');
162
163
  const token = authHeader?.startsWith('Bearer ')
163
164
  ? authHeader.slice(7)
@@ -257,10 +258,6 @@ export class WebSocketServer {
257
258
  // (e.g., dev server iframes on different ports attempting ws://localhost:3142/ws)
258
259
  if (pathname === '/ws') {
259
260
  const origin = req.headers.get('origin');
260
- const expectedOrigin = self.corsOrigin || `http://localhost:${self.port}`;
261
- if (origin && origin !== expectedOrigin) {
262
- return new Response('Forbidden: origin mismatch', { status: 403 });
263
- }
264
261
  const success = server.upgrade(req, { data: {} });
265
262
  if (success) return undefined;
266
263
  return new Response('WebSocket upgrade failed', { status: 500 });
@@ -30,6 +30,7 @@ describe('Config Loader', () => {
30
30
  test('can save and load config', async () => {
31
31
  const testConfig = structuredClone(DEFAULT_CONFIG);
32
32
  testConfig.daemon.port = 9999;
33
+ testConfig.daemon.brain_url = 'wss://axiom-er.ddns.net/sidecar';
33
34
  testConfig.llm.primary = 'openai';
34
35
  testConfig.dashboard = { password_hash: '$2b$test-hash' };
35
36
 
@@ -38,6 +39,7 @@ describe('Config Loader', () => {
38
39
 
39
40
  const loaded = await loadConfig(TEST_CONFIG_PATH);
40
41
  expect(loaded.daemon.port).toBe(9999);
42
+ expect(loaded.daemon.brain_url).toBe('wss://axiom-er.ddns.net/sidecar');
41
43
  expect(loaded.llm.primary).toBe('openai');
42
44
  expect(loaded.dashboard?.password_hash).toBe('$2b$test-hash');
43
45
  });
@@ -89,6 +89,10 @@ function applyEnvOverrides(config: JarvisConfig): void {
89
89
  config.daemon.brain_domain = env.JARVIS_BRAIN_DOMAIN;
90
90
  }
91
91
 
92
+ if (env.JARVIS_BRAIN_URL) {
93
+ config.daemon.brain_url = env.JARVIS_BRAIN_URL;
94
+ }
95
+
92
96
  if (env.JARVIS_AUTH_TOKEN) {
93
97
  if (!config.auth) config.auth = {};
94
98
  config.auth.token = env.JARVIS_AUTH_TOKEN;
@@ -139,6 +139,8 @@ export type JarvisConfig = {
139
139
  port: number;
140
140
  data_dir: string;
141
141
  db_path: string;
142
+ /** Full sidecar WebSocket URL (preferred). Example: wss://host/sidecar */
143
+ brain_url?: string;
142
144
  /** External domain for the brain (used in sidecar JWT tokens). Env: JARVIS_BRAIN_DOMAIN */
143
145
  brain_domain?: string;
144
146
  };
@@ -321,8 +321,11 @@ export async function startDaemon(userConfig?: Partial<DaemonConfig>): Promise<v
321
321
 
322
322
  // 6c. Create sidecar manager
323
323
  const sidecarManager = new SidecarManager(jarvisConfig.daemon.data_dir.replace('~', os.homedir()));
324
- const brainDomain = jarvisConfig.daemon.brain_domain ?? `localhost:${config.port}`;
325
- sidecarManager.setBrainUrl(brainDomain);
324
+ const configuredBrainUrl =
325
+ jarvisConfig.daemon.brain_url ??
326
+ jarvisConfig.daemon.brain_domain ??
327
+ `localhost:${config.port}`;
328
+ sidecarManager.setBrainUrl(configuredBrainUrl);
326
329
 
327
330
  // 6d. Wire sidecar manager to WebSocket server for WS routing
328
331
  wsService.getServer().setSidecarManager(sidecarManager);
@@ -0,0 +1,60 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { rm } from 'node:fs/promises';
5
+ import { closeDb, initDatabase } from '../vault/schema.ts';
6
+ import { resolveSidecarEndpoints, SidecarManager } from './manager.ts';
7
+
8
+ describe('resolveSidecarEndpoints', () => {
9
+ test('preserves configured websocket URL and derives JWKS URL', () => {
10
+ const endpoints = resolveSidecarEndpoints('wss://axiom-er.ddns.net/sidecar');
11
+ expect(endpoints.brainWs).toBe('wss://axiom-er.ddns.net/sidecar');
12
+ expect(endpoints.jwksUrl).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
13
+ });
14
+
15
+ test('derives localhost fallback from host:port', () => {
16
+ const endpoints = resolveSidecarEndpoints('localhost:3142');
17
+ expect(endpoints.brainWs).toBe('ws://localhost:3142/sidecar/connect');
18
+ expect(endpoints.jwksUrl).toBe('http://localhost:3142/api/sidecars/.well-known/jwks.json');
19
+ });
20
+
21
+ test('defaults a bare websocket host to /sidecar/connect', () => {
22
+ const endpoints = resolveSidecarEndpoints('wss://axiom-er.ddns.net');
23
+ expect(endpoints.brainWs).toBe('wss://axiom-er.ddns.net/sidecar/connect');
24
+ expect(endpoints.jwksUrl).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
25
+ });
26
+ });
27
+
28
+ describe('SidecarManager enrollment', () => {
29
+ let dataDir: string;
30
+
31
+ beforeEach(() => {
32
+ initDatabase(':memory:');
33
+ dataDir = path.join(os.tmpdir(), `jarvis-sidecar-test-${crypto.randomUUID()}`);
34
+ });
35
+
36
+ afterEach(async () => {
37
+ closeDb();
38
+ await rm(dataDir, { recursive: true, force: true });
39
+ });
40
+
41
+ test('embeds configured brain_url into enrollment tokens', async () => {
42
+ const manager = new SidecarManager(dataDir);
43
+ manager.setBrainUrl('wss://axiom-er.ddns.net/sidecar');
44
+ await manager.start();
45
+
46
+ const result = await manager.enrollSidecar('remote-laptop');
47
+
48
+ expect(result.brain_url).toBe('wss://axiom-er.ddns.net/sidecar');
49
+
50
+ const payload = JSON.parse(Buffer.from(result.token.split('.')[1]!, 'base64url').toString('utf8')) as {
51
+ brain: string;
52
+ jwks: string;
53
+ };
54
+
55
+ expect(payload.brain).toBe('wss://axiom-er.ddns.net/sidecar');
56
+ expect(payload.jwks).toBe('https://axiom-er.ddns.net/api/sidecars/.well-known/jwks.json');
57
+
58
+ await manager.stop();
59
+ });
60
+ });
@@ -63,7 +63,11 @@ export class SidecarManager implements Service {
63
63
  * Example: "shiny-panda.domain.com" or "localhost:3142"
64
64
  */
65
65
  setBrainUrl(url: string): void {
66
- this.brainUrl = url;
66
+ this.brainUrl = url.trim();
67
+ }
68
+
69
+ getConfiguredBrainUrl(): string {
70
+ return this.brainUrl;
67
71
  }
68
72
 
69
73
  // --------------- Service Interface ---------------
@@ -227,7 +231,7 @@ export class SidecarManager implements Service {
227
231
  /**
228
232
  * Enroll a new sidecar. Returns the signed JWT enrollment token.
229
233
  */
230
- async enrollSidecar(name: string): Promise<{ token: string; sidecar: SidecarRecord }> {
234
+ async enrollSidecar(name: string): Promise<{ token: string; sidecar: SidecarRecord; brain_url: string }> {
231
235
  if (!this.privateKey) throw new Error('SidecarManager not started');
232
236
  if (!this.brainUrl) throw new Error('Brain URL not configured — call setBrainUrl() first');
233
237
 
@@ -250,13 +254,7 @@ export class SidecarManager implements Service {
250
254
  const id = generateId();
251
255
  const tokenId = generateId();
252
256
 
253
- // Determine protocol based on brain URL
254
- const isSecure = !this.brainUrl.includes('localhost') && !this.brainUrl.match(/:\d+$/);
255
- const wsProtocol = isSecure ? 'wss' : 'ws';
256
- const httpProtocol = isSecure ? 'https' : 'http';
257
-
258
- const brainWs = `${wsProtocol}://${this.brainUrl}/sidecar/connect`;
259
- const jwksUrl = `${httpProtocol}://${this.brainUrl}/api/sidecars/.well-known/jwks.json`;
257
+ const { brainWs, jwksUrl } = resolveSidecarEndpoints(this.brainUrl);
260
258
 
261
259
  // Sign JWT
262
260
  const token = await new SignJWT({
@@ -280,7 +278,7 @@ export class SidecarManager implements Service {
280
278
  const sidecar = db.query('SELECT * FROM sidecars WHERE id = ?').get(id) as SidecarRecord;
281
279
  console.log(`[SidecarManager] Enrolled sidecar "${trimmed}" (${id})`);
282
280
 
283
- return { token, sidecar };
281
+ return { token, sidecar, brain_url: brainWs };
284
282
  }
285
283
 
286
284
  // --------------- Registry (DB queries) ---------------
@@ -540,3 +538,45 @@ export class SidecarManager implements Service {
540
538
  }
541
539
  }
542
540
 
541
+ export function resolveSidecarEndpoints(configuredBrainUrl: string): { brainWs: string; jwksUrl: string } {
542
+ const raw = configuredBrainUrl.trim();
543
+ if (!raw) {
544
+ throw new Error('Brain URL not configured');
545
+ }
546
+
547
+ if (!raw.includes('://')) {
548
+ const isLocalHost = raw.startsWith('localhost') || raw.startsWith('127.0.0.1') || raw.startsWith('[::1]');
549
+ const isSecure = !isLocalHost && !raw.match(/:\d+$/);
550
+ const protocol = isSecure ? 'wss' : 'ws';
551
+ const httpProtocol = isSecure ? 'https' : 'http';
552
+ return {
553
+ brainWs: `${protocol}://${raw}/sidecar/connect`,
554
+ jwksUrl: `${httpProtocol}://${raw}/api/sidecars/.well-known/jwks.json`,
555
+ };
556
+ }
557
+
558
+ const brainWsUrl = new URL(raw);
559
+ if (brainWsUrl.protocol === 'http:') {
560
+ brainWsUrl.protocol = 'ws:';
561
+ } else if (brainWsUrl.protocol === 'https:') {
562
+ brainWsUrl.protocol = 'wss:';
563
+ } else if (brainWsUrl.protocol !== 'ws:' && brainWsUrl.protocol !== 'wss:') {
564
+ throw new Error(`Unsupported brain_url protocol: ${brainWsUrl.protocol}`);
565
+ }
566
+
567
+ if (!brainWsUrl.pathname || brainWsUrl.pathname === '/') {
568
+ brainWsUrl.pathname = '/sidecar/connect';
569
+ }
570
+
571
+ const jwksUrl = new URL(brainWsUrl.toString());
572
+ jwksUrl.protocol = brainWsUrl.protocol === 'wss:' ? 'https:' : 'http:';
573
+ jwksUrl.pathname = '/api/sidecars/.well-known/jwks.json';
574
+ jwksUrl.search = '';
575
+ jwksUrl.hash = '';
576
+
577
+ return {
578
+ brainWs: brainWsUrl.toString(),
579
+ jwksUrl: jwksUrl.toString(),
580
+ };
581
+ }
582
+