@geravant/sinain 1.6.7 → 1.6.8

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 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
 
@@ -127,7 +134,10 @@ async function main() {
127
134
  const scDir = path.join(PKG_DIR, "sense_client");
128
135
  // Check if key package is importable to skip pip
129
136
  try {
130
- execSync('python3 -c "import PIL; import skimage"', { stdio: "pipe" });
137
+ const depCheck = IS_WINDOWS
138
+ ? 'python3 -c "import PIL; import skimage"'
139
+ : 'python3 -c "import PIL; import skimage; import Quartz; import Vision"';
140
+ execSync(depCheck, { stdio: "pipe" });
131
141
  } catch {
132
142
  log("Installing sense_client Python dependencies...");
133
143
  try {
@@ -213,7 +223,41 @@ async function main() {
213
223
  warn("flutter not found — overlay source found but can't build");
214
224
  }
215
225
  } else {
216
- warn("overlay not found — run: sinain setup-overlay");
226
+ // Auto-download overlay if not found
227
+ log("overlay not found — downloading from GitHub Releases...");
228
+ try {
229
+ const { downloadOverlay } = await import("./setup-overlay.js");
230
+ const success = await downloadOverlay({ silent: false });
231
+ if (success) {
232
+ // Re-find and launch the freshly downloaded overlay
233
+ const freshOverlay = findOverlay();
234
+ if (freshOverlay?.type === "prebuilt") {
235
+ if (!IS_WINDOWS) {
236
+ try {
237
+ execSync(`xattr -cr "${freshOverlay.path}"`, { stdio: "pipe" });
238
+ } catch { /* no quarantine */ }
239
+ }
240
+ log("Starting overlay (pre-built)...");
241
+ const binary = IS_WINDOWS
242
+ ? freshOverlay.path
243
+ : path.join(freshOverlay.path, "Contents/MacOS/sinain_hud");
244
+ startProcess("overlay", binary, [], { color: MAGENTA });
245
+ await sleep(2000);
246
+ const overlayChild = children.find(c => c.name === "overlay");
247
+ if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
248
+ ok(`overlay running (pid:${overlayChild.pid})`);
249
+ overlayStatus = "running";
250
+ } else {
251
+ warn("overlay exited early — check logs above");
252
+ overlayStatus = "failed";
253
+ }
254
+ }
255
+ } else {
256
+ warn("overlay auto-download failed — run: sinain setup-overlay");
257
+ }
258
+ } catch (e) {
259
+ warn(`overlay auto-download failed: ${e.message}`);
260
+ }
217
261
  }
218
262
  }
219
263
 
@@ -309,25 +353,38 @@ async function preflight() {
309
353
  ok("port 9500 free");
310
354
  }
311
355
 
312
- // Ollama (if local vision enabled)
313
- if (process.env.LOCAL_VISION_ENABLED === "true") {
314
- try {
315
- const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
316
- if (resp.ok) {
317
- ok("ollama server running");
318
- } else {
319
- warn("ollama server not responding — local vision will be unavailable");
320
- }
321
- } catch {
322
- // 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);
323
375
  try {
324
- const { spawn: spawnProc } = await import("child_process");
325
- spawnProc("ollama", ["serve"], { detached: true, stdio: "ignore" }).unref();
326
- ok("ollama server started in background");
327
- } catch {
328
- warn("ollama not running and could not auto-start — local vision disabled");
329
- }
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 */ }
330
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;
331
388
  }
332
389
  }
333
390
 
@@ -337,9 +394,20 @@ async function setupWizard(envPath) {
337
394
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
338
395
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
339
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
+
340
407
  console.log();
341
- console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
408
+ console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
342
409
  console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
410
+ if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
343
411
  console.log();
344
412
 
345
413
  const vars = {};
@@ -391,10 +459,13 @@ async function setupWizard(envPath) {
391
459
 
392
460
  // 2. OpenRouter API key (if cloud backend or for vision/OCR)
393
461
  if (transcriptionBackend === "openrouter") {
462
+ const existingKey = existing.OPENROUTER_API_KEY;
463
+ const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
394
464
  let key = "";
395
465
  while (!key) {
396
- key = await ask(` OpenRouter API key (sk-or-...): `);
466
+ key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
397
467
  key = key.trim();
468
+ if (!key && existingKey) { key = existingKey; break; }
398
469
  if (key && !key.startsWith("sk-or-")) {
399
470
  console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
400
471
  const retry = await ask(` Use this key anyway? [y/N]: `);
@@ -408,13 +479,17 @@ async function setupWizard(envPath) {
408
479
  if (key) vars.OPENROUTER_API_KEY = key;
409
480
  } else {
410
481
  // Still ask for OpenRouter key (needed for vision/OCR)
411
- 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}: `);
412
485
  if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
486
+ else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
413
487
  }
414
488
 
415
489
  // 3. Agent selection
416
- const agentChoice = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
417
- 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;
418
493
 
419
494
  // 3b. Local vision (Ollama)
420
495
  const IS_MACOS = os.platform() === "darwin";
@@ -423,20 +498,24 @@ async function setupWizard(envPath) {
423
498
  const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
424
499
  if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
425
500
  vars.LOCAL_VISION_ENABLED = "true";
426
- try {
427
- const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
428
- if (!models.includes("llava")) {
429
- const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
430
- if (!pull.trim() || pull.trim().toLowerCase() === "y") {
431
- console.log(` ${DIM}Pulling llava...${RESET}`);
432
- execSync("ollama pull llava", { stdio: "inherit" });
433
- 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");
434
515
  }
435
- } else {
436
- ok("llava model already available");
516
+ } catch {
517
+ warn("Could not check Ollama models");
437
518
  }
438
- } catch {
439
- warn("Could not check Ollama models");
440
519
  }
441
520
  vars.LOCAL_VISION_MODEL = "llava";
442
521
  }
@@ -451,6 +530,8 @@ async function setupWizard(envPath) {
451
530
  console.log(` ${DIM}Installing Ollama...${RESET}`);
452
531
  execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
453
532
  }
533
+ // Start ollama serve before pulling
534
+ await ensureOllama();
454
535
  console.log(` ${DIM}Pulling llava vision model...${RESET}`);
455
536
  execSync("ollama pull llava", { stdio: "inherit" });
456
537
  vars.LOCAL_VISION_ENABLED = "true";
@@ -469,25 +550,37 @@ async function setupWizard(envPath) {
469
550
  console.log(` selective — score-based (errors, questions trigger it)`);
470
551
  console.log(` focus — always escalate every tick`);
471
552
  console.log(` rich — always escalate with maximum context`);
472
- const escMode = await ask(` Escalation mode? [off/${BOLD}selective${RESET}/focus/rich]: `);
473
- 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;
474
556
 
475
557
  // 5. OpenClaw gateway
476
- const hasGateway = await ask(` Do you have an OpenClaw gateway? [y/N]: `);
477
- if (hasGateway.trim().toLowerCase() === "y") {
478
- const wsUrl = await ask(` Gateway WebSocket URL [ws://localhost:18789]: `);
479
- vars.OPENCLAW_WS_URL = wsUrl.trim() || "ws://localhost:18789";
480
-
481
- 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}: `);
482
572
  if (wsToken.trim()) {
483
573
  vars.OPENCLAW_WS_TOKEN = wsToken.trim();
484
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;
485
578
  }
486
579
 
487
580
  // Derive HTTP URL from WS URL
488
581
  const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
489
582
  vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
490
- vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
583
+ vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
491
584
  } else {
492
585
  // No gateway — disable WS connection attempts
493
586
  vars.OPENCLAW_WS_URL = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.6.7",
3
+ "version": "1.6.8",
4
4
  "description": "Ambient AI overlay invisible to screen capture — real-time insights from audio + screen context",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@ numpy>=1.24
4
4
  pytesseract>=0.3
5
5
  requests>=2.31
6
6
  pyobjc-framework-Quartz>=10.0; sys_platform == "darwin"
7
+ pyobjc-framework-Vision>=10.0; sys_platform == "darwin"
7
8
  mss>=9.0; sys_platform == "win32"
8
9
  psutil>=5.9; sys_platform == "win32"
9
10
  winrt-Windows.Media.Ocr>=2.0; sys_platform == "win32"
package/setup-overlay.js CHANGED
@@ -37,25 +37,36 @@ function ok(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${GREEN}✓${RE
37
37
  function warn(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${YELLOW}⚠${RESET} ${msg}`); }
38
38
  function fail(msg) { console.error(`${BOLD}[setup-overlay]${RESET} ${RED}✗${RESET} ${msg}`); process.exit(1); }
39
39
 
40
- // ── Parse flags ──────────────────────────────────────────────────────────────
40
+ // ── Entry point (only when run directly, not when imported) ──────────────────
41
41
 
42
- const args = process.argv.slice(2);
43
- const fromSource = args.includes("--from-source");
44
- const forceUpdate = args.includes("--update");
42
+ const isMain = process.argv[1] && (
43
+ import.meta.url === `file://${process.argv[1]}` ||
44
+ import.meta.url === new URL(process.argv[1], "file://").href
45
+ );
45
46
 
46
- if (fromSource) {
47
- await buildFromSource();
48
- } else {
49
- await downloadPrebuilt();
47
+ if (isMain) {
48
+ const args = process.argv.slice(2);
49
+ const fromSource = args.includes("--from-source");
50
+ const forceUpdate = args.includes("--update");
51
+
52
+ if (fromSource) {
53
+ await buildFromSource();
54
+ } else {
55
+ await downloadOverlay({ forceUpdate });
56
+ }
50
57
  }
51
58
 
52
59
  // ── Download pre-built .app ──────────────────────────────────────────────────
53
60
 
54
- async function downloadPrebuilt() {
61
+ export async function downloadOverlay({ silent = false, forceUpdate = false } = {}) {
62
+ const _log = silent ? () => {} : log;
63
+ const _ok = silent ? () => {} : ok;
64
+ const _warn = silent ? () => {} : warn;
65
+
55
66
  fs.mkdirSync(APP_DIR, { recursive: true });
56
67
 
57
68
  // Find latest overlay release
58
- log("Checking for latest overlay release...");
69
+ _log("Checking for latest overlay release...");
59
70
  let release;
60
71
  try {
61
72
  const res = await fetch(`${RELEASES_API}?per_page=20`, {
@@ -67,6 +78,7 @@ async function downloadPrebuilt() {
67
78
  release = releases.find(r => r.tag_name?.startsWith("overlay-v"));
68
79
  if (!release) throw new Error("No overlay release found");
69
80
  } catch (e) {
81
+ if (silent) return false;
70
82
  fail(`Failed to fetch releases: ${e.message}\n Try: sinain setup-overlay --from-source`);
71
83
  }
72
84
 
@@ -78,21 +90,22 @@ async function downloadPrebuilt() {
78
90
  try {
79
91
  const local = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
80
92
  if (local.tag === tag) {
81
- ok(`Overlay already up-to-date (${version})`);
82
- return;
93
+ _ok(`Overlay already up-to-date (${version})`);
94
+ return true;
83
95
  }
84
- log(`Updating: ${local.tag} → ${tag}`);
96
+ _log(`Updating: ${local.tag} → ${tag}`);
85
97
  } catch { /* corrupt version file — re-download */ }
86
98
  }
87
99
 
88
100
  // Find the .zip asset for this platform
89
101
  const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
90
102
  if (!zipAsset) {
103
+ if (silent) return false;
91
104
  fail(`Release ${tag} has no ${ASSET_NAME} asset.\n Try: sinain setup-overlay --from-source`);
92
105
  }
93
106
 
94
107
  // Download with progress
95
- log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
108
+ _log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
96
109
  const zipPath = path.join(APP_DIR, ASSET_NAME);
97
110
 
98
111
  try {
@@ -112,17 +125,18 @@ async function downloadPrebuilt() {
112
125
  if (done) break;
113
126
  chunks.push(value);
114
127
  downloaded += value.length;
115
- if (total > 0) {
128
+ if (total > 0 && !silent) {
116
129
  const pct = Math.round((downloaded / total) * 100);
117
130
  process.stdout.write(`\r${BOLD}[setup-overlay]${RESET} ${DIM}${pct}% (${formatBytes(downloaded)} / ${formatBytes(total)})${RESET}`);
118
131
  }
119
132
  }
120
- process.stdout.write("\n");
133
+ if (!silent) process.stdout.write("\n");
121
134
 
122
135
  const buffer = Buffer.concat(chunks);
123
136
  fs.writeFileSync(zipPath, buffer);
124
- ok(`Downloaded ${formatBytes(buffer.length)}`);
137
+ _ok(`Downloaded ${formatBytes(buffer.length)}`);
125
138
  } catch (e) {
139
+ if (silent) return false;
126
140
  fail(`Download failed: ${e.message}`);
127
141
  }
128
142
 
@@ -132,7 +146,7 @@ async function downloadPrebuilt() {
132
146
  }
133
147
 
134
148
  // Extract
135
- log("Extracting...");
149
+ _log("Extracting...");
136
150
  if (IS_WINDOWS) {
137
151
  try {
138
152
  execSync(
@@ -140,6 +154,7 @@ async function downloadPrebuilt() {
140
154
  { stdio: "pipe" }
141
155
  );
142
156
  } catch (e) {
157
+ if (silent) return false;
143
158
  fail(`Extraction failed: ${e.message}`);
144
159
  }
145
160
  } else {
@@ -150,6 +165,7 @@ async function downloadPrebuilt() {
150
165
  try {
151
166
  execSync(`unzip -o -q "${zipPath}" -d "${APP_DIR}"`, { stdio: "pipe" });
152
167
  } catch (e) {
168
+ if (silent) return false;
153
169
  fail(`Extraction failed: ${e.message}`);
154
170
  }
155
171
  }
@@ -170,12 +186,15 @@ async function downloadPrebuilt() {
170
186
  // Clean up zip
171
187
  fs.unlinkSync(zipPath);
172
188
 
173
- ok(`Overlay ${version} installed`);
174
- console.log(`
189
+ _ok(`Overlay ${version} installed`);
190
+ if (!silent) {
191
+ console.log(`
175
192
  ${GREEN}✓${RESET} Overlay ready!
176
193
  Location: ${APP_PATH}
177
194
  The overlay will auto-start with: ${BOLD}sinain start${RESET}
178
195
  `);
196
+ }
197
+ return true;
179
198
  }
180
199
 
181
200
  // ── Build from source (legacy) ───────────────────────────────────────────────