@geravant/sinain 1.6.4 → 1.6.6

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.
Files changed (2) hide show
  1. package/launcher.js +101 -45
  2. package/package.json +1 -1
package/launcher.js CHANGED
@@ -35,11 +35,13 @@ let skipSense = false;
35
35
  let skipOverlay = false;
36
36
  let skipAgent = false;
37
37
  let agentName = null;
38
+ let forceSetup = false;
38
39
 
39
40
  for (const arg of args) {
40
41
  if (arg === "--no-sense") { skipSense = true; continue; }
41
42
  if (arg === "--no-overlay") { skipOverlay = true; continue; }
42
43
  if (arg === "--no-agent") { skipAgent = true; continue; }
44
+ if (arg === "--setup") { forceSetup = true; continue; }
43
45
  if (arg.startsWith("--agent=")) { agentName = arg.split("=")[1]; continue; }
44
46
  console.error(`Unknown flag: ${arg}`);
45
47
  process.exit(1);
@@ -60,15 +62,20 @@ async function main() {
60
62
  await preflight();
61
63
  console.log();
62
64
 
63
- // Run setup wizard on first launch (no ~/.sinain/.env)
65
+ // Run setup wizard on first launch (no ~/.sinain/.env) or when --setup flag is passed
64
66
  const userEnvPath = path.join(SINAIN_DIR, ".env");
65
- if (!fs.existsSync(userEnvPath)) {
67
+ if (forceSetup || !fs.existsSync(userEnvPath)) {
66
68
  await setupWizard(userEnvPath);
67
69
  }
68
70
 
69
71
  // Load user config
70
72
  loadUserEnv();
71
73
 
74
+ // Ensure Ollama is running (if local vision enabled)
75
+ if (process.env.LOCAL_VISION_ENABLED === "true") {
76
+ await ensureOllama();
77
+ }
78
+
72
79
  // Auto-detect transcription backend
73
80
  detectTranscription();
74
81
 
@@ -346,25 +353,38 @@ async function preflight() {
346
353
  ok("port 9500 free");
347
354
  }
348
355
 
349
- // Ollama (if local vision enabled)
350
- if (process.env.LOCAL_VISION_ENABLED === "true") {
351
- try {
352
- const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
353
- if (resp.ok) {
354
- ok("ollama server running");
355
- } else {
356
- warn("ollama server not responding — local vision will be unavailable");
357
- }
358
- } catch {
359
- // Try to start Ollama in background
356
+ }
357
+
358
+ async function ensureOllama() {
359
+ try {
360
+ const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
361
+ if (resp.ok) {
362
+ ok("ollama server running");
363
+ return true;
364
+ }
365
+ } catch { /* not running */ }
366
+
367
+ // Try to start Ollama in background
368
+ log("Starting ollama server...");
369
+ try {
370
+ const { spawn: spawnProc } = await import("child_process");
371
+ spawnProc("ollama", ["serve"], { detached: true, stdio: "ignore" }).unref();
372
+ // Wait for it to become ready
373
+ for (let i = 0; i < 10; i++) {
374
+ await sleep(500);
360
375
  try {
361
- const { spawn: spawnProc } = await import("child_process");
362
- spawnProc("ollama", ["serve"], { detached: true, stdio: "ignore" }).unref();
363
- ok("ollama server started in background");
364
- } catch {
365
- warn("ollama not running and could not auto-start — local vision disabled");
366
- }
376
+ const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(1000) });
377
+ if (resp.ok) {
378
+ ok("ollama server started");
379
+ return true;
380
+ }
381
+ } catch { /* not ready yet */ }
367
382
  }
383
+ warn("ollama started but not responding — local vision may not work");
384
+ return false;
385
+ } catch {
386
+ warn("ollama not found — local vision disabled. Install: brew install ollama");
387
+ return false;
368
388
  }
369
389
  }
370
390
 
@@ -374,9 +394,20 @@ async function setupWizard(envPath) {
374
394
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
375
395
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
376
396
 
397
+ // Load existing .env values as defaults (for re-configuration)
398
+ const existing = {};
399
+ if (fs.existsSync(envPath)) {
400
+ for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
401
+ const m = line.match(/^([A-Z_]+)=(.*)$/);
402
+ if (m) existing[m[1]] = m[2];
403
+ }
404
+ }
405
+ const hasExisting = Object.keys(existing).length > 0;
406
+
377
407
  console.log();
378
- console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
408
+ console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
379
409
  console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
410
+ if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
380
411
  console.log();
381
412
 
382
413
  const vars = {};
@@ -428,10 +459,13 @@ async function setupWizard(envPath) {
428
459
 
429
460
  // 2. OpenRouter API key (if cloud backend or for vision/OCR)
430
461
  if (transcriptionBackend === "openrouter") {
462
+ const existingKey = existing.OPENROUTER_API_KEY;
463
+ const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
431
464
  let key = "";
432
465
  while (!key) {
433
- key = await ask(` OpenRouter API key (sk-or-...): `);
466
+ key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
434
467
  key = key.trim();
468
+ if (!key && existingKey) { key = existingKey; break; }
435
469
  if (key && !key.startsWith("sk-or-")) {
436
470
  console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
437
471
  const retry = await ask(` Use this key anyway? [y/N]: `);
@@ -445,13 +479,17 @@ async function setupWizard(envPath) {
445
479
  if (key) vars.OPENROUTER_API_KEY = key;
446
480
  } else {
447
481
  // Still ask for OpenRouter key (needed for vision/OCR)
448
- const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip): `);
482
+ const existingKey = existing.OPENROUTER_API_KEY;
483
+ const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
484
+ const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip)${keyHint}: `);
449
485
  if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
486
+ else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
450
487
  }
451
488
 
452
489
  // 3. Agent selection
453
- const agentChoice = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
454
- vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || "claude";
490
+ const defaultAgent = existing.SINAIN_AGENT || "claude";
491
+ const agentChoice = await ask(` Agent? [${BOLD}${defaultAgent}${RESET}/claude/codex/goose/junie/aider]: `);
492
+ vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || defaultAgent;
455
493
 
456
494
  // 3b. Local vision (Ollama)
457
495
  const IS_MACOS = os.platform() === "darwin";
@@ -460,20 +498,24 @@ async function setupWizard(envPath) {
460
498
  const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
461
499
  if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
462
500
  vars.LOCAL_VISION_ENABLED = "true";
463
- try {
464
- const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
465
- if (!models.includes("llava")) {
466
- const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
467
- if (!pull.trim() || pull.trim().toLowerCase() === "y") {
468
- console.log(` ${DIM}Pulling llava...${RESET}`);
469
- execSync("ollama pull llava", { stdio: "inherit" });
470
- ok("llava model pulled");
501
+ // Ensure ollama serve is running before list/pull
502
+ const ollamaReady = await ensureOllama();
503
+ if (ollamaReady) {
504
+ try {
505
+ const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
506
+ if (!models.includes("llava")) {
507
+ const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
508
+ if (!pull.trim() || pull.trim().toLowerCase() === "y") {
509
+ console.log(` ${DIM}Pulling llava...${RESET}`);
510
+ execSync("ollama pull llava", { stdio: "inherit" });
511
+ ok("llava model pulled");
512
+ }
513
+ } else {
514
+ ok("llava model already available");
471
515
  }
472
- } else {
473
- ok("llava model already available");
516
+ } catch {
517
+ warn("Could not check Ollama models");
474
518
  }
475
- } catch {
476
- warn("Could not check Ollama models");
477
519
  }
478
520
  vars.LOCAL_VISION_MODEL = "llava";
479
521
  }
@@ -488,6 +530,8 @@ async function setupWizard(envPath) {
488
530
  console.log(` ${DIM}Installing Ollama...${RESET}`);
489
531
  execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
490
532
  }
533
+ // Start ollama serve before pulling
534
+ await ensureOllama();
491
535
  console.log(` ${DIM}Pulling llava vision model...${RESET}`);
492
536
  execSync("ollama pull llava", { stdio: "inherit" });
493
537
  vars.LOCAL_VISION_ENABLED = "true";
@@ -506,25 +550,37 @@ async function setupWizard(envPath) {
506
550
  console.log(` selective — score-based (errors, questions trigger it)`);
507
551
  console.log(` focus — always escalate every tick`);
508
552
  console.log(` rich — always escalate with maximum context`);
509
- const escMode = await ask(` Escalation mode? [off/${BOLD}selective${RESET}/focus/rich]: `);
510
- vars.ESCALATION_MODE = escMode.trim().toLowerCase() || "selective";
553
+ const defaultEsc = existing.ESCALATION_MODE || "selective";
554
+ const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
555
+ vars.ESCALATION_MODE = escMode.trim().toLowerCase() || defaultEsc;
511
556
 
512
557
  // 5. OpenClaw gateway
513
- const hasGateway = await ask(` Do you have an OpenClaw gateway? [y/N]: `);
514
- if (hasGateway.trim().toLowerCase() === "y") {
515
- const wsUrl = await ask(` Gateway WebSocket URL [ws://localhost:18789]: `);
516
- vars.OPENCLAW_WS_URL = wsUrl.trim() || "ws://localhost:18789";
517
-
518
- const wsToken = await ask(` Gateway auth token (48-char hex): `);
558
+ const hadGateway = !!(existing.OPENCLAW_WS_URL);
559
+ const gatewayDefault = hadGateway ? "Y" : "N";
560
+ const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
561
+ const wantsGateway = hasGateway.trim()
562
+ ? hasGateway.trim().toLowerCase() === "y"
563
+ : hadGateway;
564
+ if (wantsGateway) {
565
+ const defaultWs = existing.OPENCLAW_WS_URL || "ws://localhost:18789";
566
+ const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
567
+ vars.OPENCLAW_WS_URL = wsUrl.trim() || defaultWs;
568
+
569
+ const existingToken = existing.OPENCLAW_WS_TOKEN;
570
+ const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
571
+ const wsToken = await ask(` Gateway auth token (48-char hex)${tokenHint}: `);
519
572
  if (wsToken.trim()) {
520
573
  vars.OPENCLAW_WS_TOKEN = wsToken.trim();
521
574
  vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
575
+ } else if (existingToken) {
576
+ vars.OPENCLAW_WS_TOKEN = existingToken;
577
+ vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
522
578
  }
523
579
 
524
580
  // Derive HTTP URL from WS URL
525
581
  const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
526
582
  vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
527
- vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
583
+ vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
528
584
  } else {
529
585
  // No gateway — disable WS connection attempts
530
586
  vars.OPENCLAW_WS_URL = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Ambient AI overlay invisible to screen capture — real-time insights from audio + screen context",
5
5
  "type": "module",
6
6
  "bin": {