@geravant/sinain 1.6.5 → 1.6.7

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/cli.js CHANGED
@@ -7,6 +7,9 @@ import path from "path";
7
7
 
8
8
  const cmd = process.argv[2];
9
9
  const IS_WINDOWS = os.platform() === "win32";
10
+ const HOME = os.homedir();
11
+ const SINAIN_DIR = path.join(HOME, ".sinain");
12
+ const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
10
13
 
11
14
  switch (cmd) {
12
15
  case "start":
@@ -52,6 +55,14 @@ switch (cmd) {
52
55
  await import("./install.js");
53
56
  break;
54
57
 
58
+ case "export-knowledge":
59
+ await exportKnowledge();
60
+ break;
61
+
62
+ case "import-knowledge":
63
+ await importKnowledge();
64
+ break;
65
+
55
66
  default:
56
67
  printUsage();
57
68
  break;
@@ -349,6 +360,152 @@ function isProcessRunning(pattern) {
349
360
  }
350
361
  }
351
362
 
363
+ // ── Knowledge export/import ──────────────────────────────────────────────────
364
+
365
+ function findWorkspace() {
366
+ const candidates = [
367
+ process.env.SINAIN_WORKSPACE,
368
+ path.join(HOME, ".openclaw/workspace"),
369
+ path.join(HOME, ".sinain/workspace"),
370
+ ].filter(Boolean);
371
+ for (const dir of candidates) {
372
+ const resolved = dir.replace(/^~/, HOME);
373
+ if (fs.existsSync(resolved)) return resolved;
374
+ }
375
+ return null;
376
+ }
377
+
378
+ async function exportKnowledge() {
379
+ const BOLD = "\x1b[1m", GREEN = "\x1b[32m", RED = "\x1b[31m", DIM = "\x1b[2m", RESET = "\x1b[0m";
380
+
381
+ const workspace = findWorkspace();
382
+ if (!workspace) {
383
+ console.error(`${RED}✗${RESET} No knowledge workspace found.`);
384
+ console.error(` Checked: SINAIN_WORKSPACE env, ~/.openclaw/workspace, ~/.sinain/workspace`);
385
+ process.exit(1);
386
+ }
387
+
388
+ const outputIdx = process.argv.indexOf("--output");
389
+ const outputPath = outputIdx !== -1 && process.argv[outputIdx + 1]
390
+ ? path.resolve(process.argv[outputIdx + 1])
391
+ : path.join(HOME, "sinain-knowledge-export.tar.gz");
392
+
393
+ // Collect files that exist
394
+ const includes = [];
395
+ const check = (rel) => {
396
+ const full = path.join(workspace, rel);
397
+ if (fs.existsSync(full)) { includes.push(rel); return true; }
398
+ return false;
399
+ };
400
+
401
+ check("modules");
402
+ check("memory/sinain-playbook.md");
403
+ check("memory/knowledge-graph.db");
404
+ check("memory/playbook-base.md");
405
+ check("memory/playbook.md");
406
+ check("memory/sinain-knowledge.md");
407
+
408
+ if (includes.length === 0) {
409
+ console.error(`${RED}✗${RESET} No knowledge files found in ${workspace}`);
410
+ process.exit(1);
411
+ }
412
+
413
+ console.log(`${BOLD}[export]${RESET} Exporting from ${DIM}${workspace}${RESET}`);
414
+ for (const inc of includes) {
415
+ console.log(` ${GREEN}+${RESET} ${inc}`);
416
+ }
417
+
418
+ try {
419
+ execSync(
420
+ `tar czf "${outputPath}" --exclude="memory/triplestore.db" ${includes.map(i => `"${i}"`).join(" ")}`,
421
+ { cwd: workspace, stdio: "pipe" }
422
+ );
423
+ } catch (e) {
424
+ console.error(`${RED}✗${RESET} tar failed: ${e.message}`);
425
+ process.exit(1);
426
+ }
427
+
428
+ const size = fs.statSync(outputPath).size;
429
+ const sizeStr = size < 1024 * 1024
430
+ ? `${(size / 1024).toFixed(1)} KB`
431
+ : `${(size / (1024 * 1024)).toFixed(1)} MB`;
432
+
433
+ console.log(`\n${GREEN}✓${RESET} Exported to ${BOLD}${outputPath}${RESET} (${sizeStr})`);
434
+ console.log(` Transfer to another machine and run: ${BOLD}sinain import-knowledge ${path.basename(outputPath)}${RESET}`);
435
+ }
436
+
437
+ async function importKnowledge() {
438
+ const BOLD = "\x1b[1m", GREEN = "\x1b[32m", RED = "\x1b[31m", YELLOW = "\x1b[33m", DIM = "\x1b[2m", RESET = "\x1b[0m";
439
+
440
+ const filePath = process.argv[3];
441
+ if (!filePath) {
442
+ console.error(`${RED}✗${RESET} Usage: sinain import-knowledge <file.tar.gz>`);
443
+ process.exit(1);
444
+ }
445
+
446
+ const resolved = path.resolve(filePath.replace(/^~/, HOME));
447
+ if (!fs.existsSync(resolved)) {
448
+ console.error(`${RED}✗${RESET} File not found: ${resolved}`);
449
+ process.exit(1);
450
+ }
451
+
452
+ const targetWorkspace = path.join(HOME, ".sinain/workspace");
453
+ fs.mkdirSync(targetWorkspace, { recursive: true });
454
+
455
+ console.log(`${BOLD}[import]${RESET} Importing to ${DIM}${targetWorkspace}${RESET}`);
456
+
457
+ // Extract
458
+ try {
459
+ execSync(`tar xzf "${resolved}" -C "${targetWorkspace}"`, { stdio: "inherit" });
460
+ } catch (e) {
461
+ console.error(`${RED}✗${RESET} Extraction failed: ${e.message}`);
462
+ process.exit(1);
463
+ }
464
+
465
+ // Symlink sinain-memory scripts from npm package
466
+ const srcMemory = path.join(PKG_DIR, "sinain-memory");
467
+ const dstMemory = path.join(targetWorkspace, "sinain-memory");
468
+ if (fs.existsSync(srcMemory)) {
469
+ try { fs.rmSync(dstMemory, { recursive: true, force: true }); } catch {}
470
+ fs.symlinkSync(srcMemory, dstMemory, IS_WINDOWS ? "junction" : undefined);
471
+ console.log(` ${GREEN}✓${RESET} sinain-memory scripts linked`);
472
+ }
473
+
474
+ // Update ~/.sinain/.env
475
+ const envPath = path.join(SINAIN_DIR, ".env");
476
+ const envVars = {
477
+ SINAIN_WORKSPACE: targetWorkspace,
478
+ OPENCLAW_WORKSPACE_DIR: targetWorkspace,
479
+ };
480
+
481
+ if (fs.existsSync(envPath)) {
482
+ let content = fs.readFileSync(envPath, "utf-8");
483
+ for (const [key, val] of Object.entries(envVars)) {
484
+ const regex = new RegExp(`^#?\\s*${key}=.*$`, "m");
485
+ if (regex.test(content)) {
486
+ content = content.replace(regex, `${key}=${val}`);
487
+ } else {
488
+ content += `\n${key}=${val}`;
489
+ }
490
+ }
491
+ fs.writeFileSync(envPath, content);
492
+ } else {
493
+ fs.mkdirSync(SINAIN_DIR, { recursive: true });
494
+ const lines = Object.entries(envVars).map(([k, v]) => `${k}=${v}`);
495
+ fs.writeFileSync(envPath, lines.join("\n") + "\n");
496
+ }
497
+ console.log(` ${GREEN}✓${RESET} SINAIN_WORKSPACE set in ${DIM}~/.sinain/.env${RESET}`);
498
+
499
+ // Summary
500
+ const items = [];
501
+ if (fs.existsSync(path.join(targetWorkspace, "modules"))) items.push("modules");
502
+ if (fs.existsSync(path.join(targetWorkspace, "memory/sinain-playbook.md"))) items.push("playbook");
503
+ if (fs.existsSync(path.join(targetWorkspace, "memory/knowledge-graph.db"))) items.push("knowledge graph");
504
+
505
+ console.log(`\n${GREEN}✓${RESET} Knowledge imported: ${items.join(", ")}`);
506
+ console.log(` Workspace: ${BOLD}${targetWorkspace}${RESET}`);
507
+ }
508
+
352
509
  // ── Usage ─────────────────────────────────────────────────────────────────────
353
510
 
354
511
  function printUsage() {
@@ -362,6 +519,8 @@ Usage:
362
519
  sinain setup Run interactive setup wizard (~/.sinain/.env)
363
520
  sinain setup-overlay Download pre-built overlay app
364
521
  sinain setup-sck-capture Download sck-capture audio binary (macOS)
522
+ sinain export-knowledge Export knowledge for transfer to another machine
523
+ sinain import-knowledge <file> Import knowledge from export file
365
524
  sinain install Install OpenClaw plugin (server-side)
366
525
 
367
526
  Start options:
package/launcher.js CHANGED
@@ -35,13 +35,11 @@ let skipSense = false;
35
35
  let skipOverlay = false;
36
36
  let skipAgent = false;
37
37
  let agentName = null;
38
- let forceSetup = false;
39
38
 
40
39
  for (const arg of args) {
41
40
  if (arg === "--no-sense") { skipSense = true; continue; }
42
41
  if (arg === "--no-overlay") { skipOverlay = true; continue; }
43
42
  if (arg === "--no-agent") { skipAgent = true; continue; }
44
- if (arg === "--setup") { forceSetup = true; continue; }
45
43
  if (arg.startsWith("--agent=")) { agentName = arg.split("=")[1]; continue; }
46
44
  console.error(`Unknown flag: ${arg}`);
47
45
  process.exit(1);
@@ -62,9 +60,9 @@ async function main() {
62
60
  await preflight();
63
61
  console.log();
64
62
 
65
- // Run setup wizard on first launch (no ~/.sinain/.env) or when --setup flag is passed
63
+ // Run setup wizard on first launch (no ~/.sinain/.env)
66
64
  const userEnvPath = path.join(SINAIN_DIR, ".env");
67
- if (forceSetup || !fs.existsSync(userEnvPath)) {
65
+ if (!fs.existsSync(userEnvPath)) {
68
66
  await setupWizard(userEnvPath);
69
67
  }
70
68
 
@@ -129,10 +127,7 @@ async function main() {
129
127
  const scDir = path.join(PKG_DIR, "sense_client");
130
128
  // Check if key package is importable to skip pip
131
129
  try {
132
- const depCheck = IS_WINDOWS
133
- ? 'python3 -c "import PIL; import skimage"'
134
- : 'python3 -c "import PIL; import skimage; import Quartz; import Vision"';
135
- execSync(depCheck, { stdio: "pipe" });
130
+ execSync('python3 -c "import PIL; import skimage"', { stdio: "pipe" });
136
131
  } catch {
137
132
  log("Installing sense_client Python dependencies...");
138
133
  try {
@@ -218,41 +213,7 @@ async function main() {
218
213
  warn("flutter not found — overlay source found but can't build");
219
214
  }
220
215
  } else {
221
- // Auto-download overlay if not found
222
- log("overlay not found — downloading from GitHub Releases...");
223
- try {
224
- const { downloadOverlay } = await import("./setup-overlay.js");
225
- const success = await downloadOverlay({ silent: false });
226
- if (success) {
227
- // Re-find and launch the freshly downloaded overlay
228
- const freshOverlay = findOverlay();
229
- if (freshOverlay?.type === "prebuilt") {
230
- if (!IS_WINDOWS) {
231
- try {
232
- execSync(`xattr -cr "${freshOverlay.path}"`, { stdio: "pipe" });
233
- } catch { /* no quarantine */ }
234
- }
235
- log("Starting overlay (pre-built)...");
236
- const binary = IS_WINDOWS
237
- ? freshOverlay.path
238
- : path.join(freshOverlay.path, "Contents/MacOS/sinain_hud");
239
- startProcess("overlay", binary, [], { color: MAGENTA });
240
- await sleep(2000);
241
- const overlayChild = children.find(c => c.name === "overlay");
242
- if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
243
- ok(`overlay running (pid:${overlayChild.pid})`);
244
- overlayStatus = "running";
245
- } else {
246
- warn("overlay exited early — check logs above");
247
- overlayStatus = "failed";
248
- }
249
- }
250
- } else {
251
- warn("overlay auto-download failed — run: sinain setup-overlay");
252
- }
253
- } catch (e) {
254
- warn(`overlay auto-download failed: ${e.message}`);
255
- }
216
+ warn("overlay not found run: sinain setup-overlay");
256
217
  }
257
218
  }
258
219
 
@@ -376,20 +337,9 @@ async function setupWizard(envPath) {
376
337
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
377
338
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
378
339
 
379
- // Load existing .env values as defaults (for re-configuration)
380
- const existing = {};
381
- if (fs.existsSync(envPath)) {
382
- for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
383
- const m = line.match(/^([A-Z_]+)=(.*)$/);
384
- if (m) existing[m[1]] = m[2];
385
- }
386
- }
387
- const hasExisting = Object.keys(existing).length > 0;
388
-
389
340
  console.log();
390
- console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
341
+ console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
391
342
  console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
392
- if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
393
343
  console.log();
394
344
 
395
345
  const vars = {};
@@ -441,13 +391,10 @@ async function setupWizard(envPath) {
441
391
 
442
392
  // 2. OpenRouter API key (if cloud backend or for vision/OCR)
443
393
  if (transcriptionBackend === "openrouter") {
444
- const existingKey = existing.OPENROUTER_API_KEY;
445
- const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
446
394
  let key = "";
447
395
  while (!key) {
448
- key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
396
+ key = await ask(` OpenRouter API key (sk-or-...): `);
449
397
  key = key.trim();
450
- if (!key && existingKey) { key = existingKey; break; }
451
398
  if (key && !key.startsWith("sk-or-")) {
452
399
  console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
453
400
  const retry = await ask(` Use this key anyway? [y/N]: `);
@@ -461,17 +408,13 @@ async function setupWizard(envPath) {
461
408
  if (key) vars.OPENROUTER_API_KEY = key;
462
409
  } else {
463
410
  // Still ask for OpenRouter key (needed for vision/OCR)
464
- const existingKey = existing.OPENROUTER_API_KEY;
465
- const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
466
- const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip)${keyHint}: `);
411
+ const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip): `);
467
412
  if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
468
- else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
469
413
  }
470
414
 
471
415
  // 3. Agent selection
472
- const defaultAgent = existing.SINAIN_AGENT || "claude";
473
- const agentChoice = await ask(` Agent? [${BOLD}${defaultAgent}${RESET}/claude/codex/goose/junie/aider]: `);
474
- vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || defaultAgent;
416
+ const agentChoice = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
417
+ vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || "claude";
475
418
 
476
419
  // 3b. Local vision (Ollama)
477
420
  const IS_MACOS = os.platform() === "darwin";
@@ -526,44 +469,59 @@ async function setupWizard(envPath) {
526
469
  console.log(` selective — score-based (errors, questions trigger it)`);
527
470
  console.log(` focus — always escalate every tick`);
528
471
  console.log(` rich — always escalate with maximum context`);
529
- const defaultEsc = existing.ESCALATION_MODE || "selective";
530
- const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
531
- vars.ESCALATION_MODE = escMode.trim().toLowerCase() || defaultEsc;
472
+ const escMode = await ask(` Escalation mode? [off/${BOLD}selective${RESET}/focus/rich]: `);
473
+ vars.ESCALATION_MODE = escMode.trim().toLowerCase() || "selective";
532
474
 
533
475
  // 5. OpenClaw gateway
534
- const hadGateway = !!(existing.OPENCLAW_WS_URL);
535
- const gatewayDefault = hadGateway ? "Y" : "N";
536
- const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
537
- const wantsGateway = hasGateway.trim()
538
- ? hasGateway.trim().toLowerCase() === "y"
539
- : hadGateway;
540
- if (wantsGateway) {
541
- const defaultWs = existing.OPENCLAW_WS_URL || "ws://localhost:18789";
542
- const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
543
- vars.OPENCLAW_WS_URL = wsUrl.trim() || defaultWs;
544
-
545
- const existingToken = existing.OPENCLAW_WS_TOKEN;
546
- const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
547
- const wsToken = await ask(` Gateway auth token (48-char hex)${tokenHint}: `);
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): `);
548
482
  if (wsToken.trim()) {
549
483
  vars.OPENCLAW_WS_TOKEN = wsToken.trim();
550
484
  vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
551
- } else if (existingToken) {
552
- vars.OPENCLAW_WS_TOKEN = existingToken;
553
- vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
554
485
  }
555
486
 
556
487
  // Derive HTTP URL from WS URL
557
488
  const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
558
489
  vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
559
- vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
490
+ vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
560
491
  } else {
561
492
  // No gateway — disable WS connection attempts
562
493
  vars.OPENCLAW_WS_URL = "";
563
494
  vars.OPENCLAW_HTTP_URL = "";
564
495
  }
565
496
 
566
- // 6. Agent-specific defaults
497
+ // 6. Knowledge import (for standalone machines)
498
+ console.log();
499
+ const wantImport = await ask(` Import knowledge from another machine? [y/N]: `);
500
+ if (wantImport.trim().toLowerCase() === "y") {
501
+ const filePath = await ask(` Path to knowledge export (.tar.gz): `);
502
+ const resolved = filePath.trim().replace(/^~/, HOME);
503
+ if (resolved && fs.existsSync(resolved)) {
504
+ const targetWorkspace = path.join(HOME, ".sinain/workspace");
505
+ fs.mkdirSync(targetWorkspace, { recursive: true });
506
+ try {
507
+ execSync(`tar xzf "${resolved}" -C "${targetWorkspace}"`, { stdio: "inherit" });
508
+ // Symlink sinain-memory scripts from npm package
509
+ const srcMemory = path.join(PKG_DIR, "sinain-memory");
510
+ const dstMemory = path.join(targetWorkspace, "sinain-memory");
511
+ try { fs.rmSync(dstMemory, { recursive: true }); } catch {}
512
+ fs.symlinkSync(srcMemory, dstMemory);
513
+ vars.SINAIN_WORKSPACE = targetWorkspace;
514
+ vars.OPENCLAW_WORKSPACE_DIR = targetWorkspace;
515
+ ok(`Knowledge imported to ${targetWorkspace}`);
516
+ } catch (e) {
517
+ warn(`Import failed: ${e.message}`);
518
+ }
519
+ } else if (resolved) {
520
+ warn(`File not found: ${resolved}`);
521
+ }
522
+ }
523
+
524
+ // 7. Agent-specific defaults
567
525
  vars.SINAIN_POLL_INTERVAL = "5";
568
526
  vars.SINAIN_HEARTBEAT_INTERVAL = "900";
569
527
  vars.PRIVACY_MODE = "standard";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Ambient AI overlay invisible to screen capture — real-time insights from audio + screen context",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,6 @@ if sys.platform == "win32":
12
12
 
13
13
  import argparse
14
14
  import concurrent.futures
15
- import copy
16
15
  import json
17
16
  import os
18
17
  import time
@@ -29,7 +28,7 @@ from .capture import ScreenCapture, create_capture
29
28
  from .change_detector import ChangeDetector
30
29
  from .roi_extractor import ROIExtractor
31
30
  from .ocr import OCRResult, create_ocr
32
- from .gate import DecisionGate, SenseEvent, SenseObservation, SenseMeta
31
+ from .gate import DecisionGate, SenseObservation
33
32
  from .sender import SenseSender, package_full_frame, package_roi
34
33
  from .app_detector import AppDetector
35
34
  from .config import load_config
@@ -129,7 +128,6 @@ def main():
129
128
  )
130
129
  app_detector = AppDetector()
131
130
  ocr_pool = concurrent.futures.ThreadPoolExecutor(max_workers=4)
132
- vision_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix="vision")
133
131
 
134
132
  # Vision provider — routes to Ollama (local) or OpenRouter (cloud) based on config/privacy
135
133
  vision_cfg = config.get("vision", {})
@@ -357,28 +355,18 @@ def main():
357
355
  title=title, subtitle=subtitle, facts=facts,
358
356
  )
359
357
 
360
- # Vision scene analysis async: send text event immediately, vision follows
358
+ # Vision scene analysis (throttled, non-blocking on failure)
361
359
  if vision_provider and time.time() - last_vision_time >= vision_throttle_s:
362
- last_vision_time = time.time() # claim slot immediately to prevent concurrent calls
363
- _v_frame = use_frame.copy() if isinstance(use_frame, np.ndarray) else use_frame.copy()
364
- _v_meta = copy.copy(event.meta)
365
- _v_ts = event.ts
366
- _v_prompt = vision_prompt
367
- def _do_vision(frame, meta, ts, prompt):
368
- try:
369
- from PIL import Image as PILImage
370
- pil = PILImage.fromarray(frame) if isinstance(frame, np.ndarray) else frame
371
- scene = vision_provider.describe(pil, prompt=prompt or None)
372
- if scene:
373
- log(f"vision: {scene[:80]}...")
374
- ctx_ev = SenseEvent(type="context", ts=ts)
375
- ctx_ev.observation = SenseObservation(scene=scene)
376
- ctx_ev.meta = meta
377
- ctx_ev.roi = package_full_frame(frame)
378
- sender.send(ctx_ev)
379
- except Exception as e:
380
- log(f"vision error: {e}")
381
- vision_pool.submit(_do_vision, _v_frame, _v_meta, _v_ts, _v_prompt)
360
+ try:
361
+ from PIL import Image as PILImage
362
+ pil_frame = PILImage.fromarray(use_frame) if isinstance(use_frame, np.ndarray) else use_frame
363
+ scene = vision_provider.describe(pil_frame, prompt=vision_prompt or None)
364
+ if scene:
365
+ event.observation.scene = scene
366
+ last_vision_time = time.time()
367
+ log(f"vision: {scene[:80]}...")
368
+ except Exception as e:
369
+ log(f"vision error: {e}")
382
370
 
383
371
  # Send small thumbnail for ALL event types (agent uses vision)
384
372
  # Privacy matrix: gate image sending based on PRIVACY_IMAGES_OPENROUTER
@@ -4,7 +4,6 @@ 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"
8
7
  mss>=9.0; sys_platform == "win32"
9
8
  psutil>=5.9; sys_platform == "win32"
10
9
  winrt-Windows.Media.Ocr>=2.0; sys_platform == "win32"
package/setup-overlay.js CHANGED
@@ -37,36 +37,25 @@ 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
- // ── Entry point (only when run directly, not when imported) ──────────────────
40
+ // ── Parse flags ──────────────────────────────────────────────────────────────
41
41
 
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
- );
42
+ const args = process.argv.slice(2);
43
+ const fromSource = args.includes("--from-source");
44
+ const forceUpdate = args.includes("--update");
46
45
 
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
- }
46
+ if (fromSource) {
47
+ await buildFromSource();
48
+ } else {
49
+ await downloadPrebuilt();
57
50
  }
58
51
 
59
52
  // ── Download pre-built .app ──────────────────────────────────────────────────
60
53
 
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
-
54
+ async function downloadPrebuilt() {
66
55
  fs.mkdirSync(APP_DIR, { recursive: true });
67
56
 
68
57
  // Find latest overlay release
69
- _log("Checking for latest overlay release...");
58
+ log("Checking for latest overlay release...");
70
59
  let release;
71
60
  try {
72
61
  const res = await fetch(`${RELEASES_API}?per_page=20`, {
@@ -78,7 +67,6 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
78
67
  release = releases.find(r => r.tag_name?.startsWith("overlay-v"));
79
68
  if (!release) throw new Error("No overlay release found");
80
69
  } catch (e) {
81
- if (silent) return false;
82
70
  fail(`Failed to fetch releases: ${e.message}\n Try: sinain setup-overlay --from-source`);
83
71
  }
84
72
 
@@ -90,22 +78,21 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
90
78
  try {
91
79
  const local = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
92
80
  if (local.tag === tag) {
93
- _ok(`Overlay already up-to-date (${version})`);
94
- return true;
81
+ ok(`Overlay already up-to-date (${version})`);
82
+ return;
95
83
  }
96
- _log(`Updating: ${local.tag} → ${tag}`);
84
+ log(`Updating: ${local.tag} → ${tag}`);
97
85
  } catch { /* corrupt version file — re-download */ }
98
86
  }
99
87
 
100
88
  // Find the .zip asset for this platform
101
89
  const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
102
90
  if (!zipAsset) {
103
- if (silent) return false;
104
91
  fail(`Release ${tag} has no ${ASSET_NAME} asset.\n Try: sinain setup-overlay --from-source`);
105
92
  }
106
93
 
107
94
  // Download with progress
108
- _log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
95
+ log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
109
96
  const zipPath = path.join(APP_DIR, ASSET_NAME);
110
97
 
111
98
  try {
@@ -125,18 +112,17 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
125
112
  if (done) break;
126
113
  chunks.push(value);
127
114
  downloaded += value.length;
128
- if (total > 0 && !silent) {
115
+ if (total > 0) {
129
116
  const pct = Math.round((downloaded / total) * 100);
130
117
  process.stdout.write(`\r${BOLD}[setup-overlay]${RESET} ${DIM}${pct}% (${formatBytes(downloaded)} / ${formatBytes(total)})${RESET}`);
131
118
  }
132
119
  }
133
- if (!silent) process.stdout.write("\n");
120
+ process.stdout.write("\n");
134
121
 
135
122
  const buffer = Buffer.concat(chunks);
136
123
  fs.writeFileSync(zipPath, buffer);
137
- _ok(`Downloaded ${formatBytes(buffer.length)}`);
124
+ ok(`Downloaded ${formatBytes(buffer.length)}`);
138
125
  } catch (e) {
139
- if (silent) return false;
140
126
  fail(`Download failed: ${e.message}`);
141
127
  }
142
128
 
@@ -146,7 +132,7 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
146
132
  }
147
133
 
148
134
  // Extract
149
- _log("Extracting...");
135
+ log("Extracting...");
150
136
  if (IS_WINDOWS) {
151
137
  try {
152
138
  execSync(
@@ -154,7 +140,6 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
154
140
  { stdio: "pipe" }
155
141
  );
156
142
  } catch (e) {
157
- if (silent) return false;
158
143
  fail(`Extraction failed: ${e.message}`);
159
144
  }
160
145
  } else {
@@ -165,7 +150,6 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
165
150
  try {
166
151
  execSync(`unzip -o -q "${zipPath}" -d "${APP_DIR}"`, { stdio: "pipe" });
167
152
  } catch (e) {
168
- if (silent) return false;
169
153
  fail(`Extraction failed: ${e.message}`);
170
154
  }
171
155
  }
@@ -186,15 +170,12 @@ export async function downloadOverlay({ silent = false, forceUpdate = false } =
186
170
  // Clean up zip
187
171
  fs.unlinkSync(zipPath);
188
172
 
189
- _ok(`Overlay ${version} installed`);
190
- if (!silent) {
191
- console.log(`
173
+ ok(`Overlay ${version} installed`);
174
+ console.log(`
192
175
  ${GREEN}✓${RESET} Overlay ready!
193
176
  Location: ${APP_PATH}
194
177
  The overlay will auto-start with: ${BOLD}sinain start${RESET}
195
178
  `);
196
- }
197
- return true;
198
179
  }
199
180
 
200
181
  // ── Build from source (legacy) ───────────────────────────────────────────────
@@ -1,10 +1,9 @@
1
1
  import { EventEmitter } from "node:events";
2
- import fs from "node:fs";
3
2
  import type { FeedBuffer } from "../buffers/feed-buffer.js";
4
3
  import type { SenseBuffer } from "../buffers/sense-buffer.js";
5
- import type { AgentConfig, AgentEntry, ContextWindow, EscalationMode, ContextRichness, RecorderStatus, SenseEvent, FeedbackRecord } from "../types.js";
4
+ import type { AgentConfig, AgentEntry, ContextWindow, EscalationMode, ContextRichness, RecorderStatus } from "../types.js";
6
5
  import type { Profiler } from "../profiler.js";
7
- import { buildContextWindow, RICHNESS_PRESETS } from "./context-window.js";
6
+ import { buildContextWindow } from "./context-window.js";
8
7
  import { analyzeContext } from "./analyzer.js";
9
8
  import { writeSituationMd } from "./situation-writer.js";
10
9
  import { calculateEscalationScore } from "../escalation/scorer.js";
@@ -36,10 +35,6 @@ export interface AgentLoopDeps {
36
35
  traitEngine?: TraitEngine;
37
36
  /** Directory to write per-day trait log JSONL files. */
38
37
  traitLogDir?: string;
39
- /** Optional: path to sinain-knowledge.md for startup recap. */
40
- getKnowledgeDocPath?: () => string | null;
41
- /** Optional: feedback store for startup recap context. */
42
- feedbackStore?: { queryRecent(n: number): FeedbackRecord[] };
43
38
  }
44
39
 
45
40
  export interface TraceContext {
@@ -74,7 +69,6 @@ export class AgentLoop extends EventEmitter {
74
69
  private lastRunTs = 0;
75
70
  private running = false;
76
71
  private started = false;
77
- private firstTick = true;
78
72
 
79
73
  private lastPushedHud = "";
80
74
  private agentNextId = 1;
@@ -118,9 +112,6 @@ export class AgentLoop extends EventEmitter {
118
112
  }, this.deps.agentConfig.maxIntervalMs);
119
113
 
120
114
  log(TAG, `loop started (debounce=${this.deps.agentConfig.debounceMs}ms, max=${this.deps.agentConfig.maxIntervalMs}ms, cooldown=${this.deps.agentConfig.cooldownMs}ms, model=${this.deps.agentConfig.model})`);
121
-
122
- // Fire recap tick: immediate HUD from persistent knowledge (no sense data needed)
123
- this.fireRecapTick().catch(e => debug(TAG, "recap skipped:", String(e)));
124
115
  }
125
116
 
126
117
  /** Stop the agent loop. */
@@ -140,13 +131,12 @@ export class AgentLoop extends EventEmitter {
140
131
  onNewContext(): void {
141
132
  if (!this.started) return;
142
133
 
143
- // Fast first tick: 500ms debounce on startup, normal debounce after
144
- const delay = this.firstTick ? 500 : this.deps.agentConfig.debounceMs;
134
+ // Debounce: wait N ms after last event before running
145
135
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
146
136
  this.debounceTimer = setTimeout(() => {
147
137
  this.debounceTimer = null;
148
138
  this.run().catch(err => error(TAG, "debounce tick error:", err.message));
149
- }, delay);
139
+ }, this.deps.agentConfig.debounceMs);
150
140
  }
151
141
 
152
142
  /** Get agent results history (newest first). */
@@ -408,80 +398,7 @@ export class AgentLoop extends EventEmitter {
408
398
  traceCtx?.finish({ totalLatencyMs: Date.now() - Date.now(), llmLatencyMs: 0, llmInputTokens: 0, llmOutputTokens: 0, llmCost: 0, escalated: false, escalationScore: 0, contextScreenEvents: 0, contextAudioEntries: 0, contextRichness: richness, digestLength: 0, hudChanged: false });
409
399
  } finally {
410
400
  this.running = false;
411
- this.firstTick = false;
412
401
  this.lastRunTs = Date.now();
413
402
  }
414
403
  }
415
-
416
- // ── Private: startup recap tick from persistent knowledge ──
417
-
418
- private async fireRecapTick(): Promise<void> {
419
- if (this.running) return;
420
- this.running = true;
421
-
422
- try {
423
- const sections: string[] = [];
424
- const startTs = Date.now();
425
-
426
- // 1. sinain-knowledge.md (established patterns, user preferences)
427
- const knowledgePath = this.deps.getKnowledgeDocPath?.();
428
- if (knowledgePath) {
429
- const content = await fs.promises.readFile(knowledgePath, "utf-8").catch(() => "");
430
- if (content.length > 50) sections.push(content.slice(0, 2000));
431
- }
432
-
433
- // 2. SITUATION.md digest (if fresh — less than 5 minutes old)
434
- try {
435
- const stat = await fs.promises.stat(this.deps.situationMdPath);
436
- if (Date.now() - stat.mtimeMs < 5 * 60_000) {
437
- const sit = await fs.promises.readFile(this.deps.situationMdPath, "utf-8");
438
- const digestMatch = sit.match(/## Digest\n([\s\S]*?)(?=\n##|$)/);
439
- if (digestMatch?.[1]?.trim()) {
440
- sections.push(`Last session digest:\n${digestMatch[1].trim()}`);
441
- }
442
- }
443
- } catch { /* SITUATION.md missing — fine */ }
444
-
445
- // 3. Recent feedback records (last 5 escalation summaries)
446
- const records = this.deps.feedbackStore?.queryRecent(5) ?? [];
447
- if (records.length > 0) {
448
- const recaps = records.slice(0, 5).map(r => `- ${r.currentApp}: ${r.hud}`).join("\n");
449
- sections.push(`Recent activity:\n${recaps}`);
450
- }
451
-
452
- if (sections.length === 0) { return; }
453
-
454
- const recapContext = sections.join("\n\n");
455
-
456
- // Build synthetic ContextWindow with knowledge as screen entry
457
- const recapWindow: ContextWindow = {
458
- audio: [],
459
- screen: [{
460
- ts: Date.now(),
461
- ocr: recapContext,
462
- meta: { app: "sinain-recap", windowTitle: "startup" },
463
- type: "context",
464
- } as unknown as SenseEvent],
465
- images: [],
466
- currentApp: "sinain-recap",
467
- appHistory: [],
468
- audioCount: 0,
469
- screenCount: 1,
470
- windowMs: 0,
471
- newestEventTs: Date.now(),
472
- preset: RICHNESS_PRESETS.lean,
473
- };
474
-
475
- const result = await analyzeContext(recapWindow, this.deps.agentConfig, null);
476
- if (result?.hud && result.hud !== "—" && result.hud !== "Idle") {
477
- this.deps.onHudUpdate(result.hud);
478
- log(TAG, `recap tick (${Date.now() - startTs}ms, ${result.tokensIn}in+${result.tokensOut}out tok) hud="${result.hud}"`);
479
- }
480
- } catch (err: any) {
481
- debug(TAG, "recap tick error:", err.message || err);
482
- } finally {
483
- this.running = false;
484
- // Do NOT update lastRunTs — normal cooldown should not be affected by recap
485
- }
486
- }
487
404
  }
@@ -25,12 +25,6 @@ import { initPrivacy, levelFor, applyLevel } from "./privacy/index.js";
25
25
 
26
26
  const TAG = "core";
27
27
 
28
- /** Resolve workspace path, expanding leading ~ to HOME. */
29
- function resolveWorkspace(): string {
30
- const raw = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
31
- return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
32
- }
33
-
34
28
  async function main() {
35
29
  log(TAG, "sinain-core starting...");
36
30
 
@@ -86,7 +80,7 @@ async function main() {
86
80
  profiler,
87
81
  feedbackStore: feedbackStore ?? undefined,
88
82
  queryKnowledgeFacts: async (entities: string[], maxFacts: number) => {
89
- const workspace = resolveWorkspace();
83
+ const workspace = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
90
84
  const dbPath = `${workspace}/memory/knowledge-graph.db`;
91
85
  const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
92
86
  try {
@@ -162,13 +156,6 @@ async function main() {
162
156
  } : undefined,
163
157
  traitEngine,
164
158
  traitLogDir: config.traitConfig.logDir,
165
- getKnowledgeDocPath: () => {
166
- const workspace = resolveWorkspace();
167
- const p = `${workspace}/memory/sinain-knowledge.md`;
168
- try { if (existsSync(p)) return p; } catch {}
169
- return null;
170
- },
171
- feedbackStore: feedbackStore ?? undefined,
172
159
  });
173
160
 
174
161
  // ── Wire learning signal collector (needs agentLoop) ──
@@ -413,13 +400,13 @@ async function main() {
413
400
 
414
401
  // Knowledge graph integration
415
402
  getKnowledgeDocPath: () => {
416
- const workspace = resolveWorkspace();
403
+ const workspace = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
417
404
  const p = `${workspace}/memory/sinain-knowledge.md`;
418
405
  try { if (existsSync(p)) return p; } catch {}
419
406
  return null;
420
407
  },
421
408
  queryKnowledgeFacts: async (entities: string[], maxFacts: number) => {
422
- const workspace = resolveWorkspace();
409
+ const workspace = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
423
410
  const dbPath = `${workspace}/memory/knowledge-graph.db`;
424
411
  const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
425
412
  try {