@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.
- package/launcher.js +101 -45
- 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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
|
454
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
}
|
|
473
|
-
|
|
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
|
|
510
|
-
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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 = "";
|