@geravant/sinain 1.6.6 → 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,20 +60,15 @@ 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
 
71
69
  // Load user config
72
70
  loadUserEnv();
73
71
 
74
- // Ensure Ollama is running (if local vision enabled)
75
- if (process.env.LOCAL_VISION_ENABLED === "true") {
76
- await ensureOllama();
77
- }
78
-
79
72
  // Auto-detect transcription backend
80
73
  detectTranscription();
81
74
 
@@ -134,10 +127,7 @@ async function main() {
134
127
  const scDir = path.join(PKG_DIR, "sense_client");
135
128
  // Check if key package is importable to skip pip
136
129
  try {
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" });
130
+ execSync('python3 -c "import PIL; import skimage"', { stdio: "pipe" });
141
131
  } catch {
142
132
  log("Installing sense_client Python dependencies...");
143
133
  try {
@@ -223,41 +213,7 @@ async function main() {
223
213
  warn("flutter not found — overlay source found but can't build");
224
214
  }
225
215
  } else {
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
- }
216
+ warn("overlay not found run: sinain setup-overlay");
261
217
  }
262
218
  }
263
219
 
@@ -353,38 +309,25 @@ async function preflight() {
353
309
  ok("port 9500 free");
354
310
  }
355
311
 
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);
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
375
323
  try {
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 */ }
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
+ }
382
330
  }
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;
388
331
  }
389
332
  }
390
333
 
@@ -394,20 +337,9 @@ async function setupWizard(envPath) {
394
337
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
395
338
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
396
339
 
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
-
407
340
  console.log();
408
- console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
341
+ console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
409
342
  console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
410
- if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
411
343
  console.log();
412
344
 
413
345
  const vars = {};
@@ -459,13 +391,10 @@ async function setupWizard(envPath) {
459
391
 
460
392
  // 2. OpenRouter API key (if cloud backend or for vision/OCR)
461
393
  if (transcriptionBackend === "openrouter") {
462
- const existingKey = existing.OPENROUTER_API_KEY;
463
- const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
464
394
  let key = "";
465
395
  while (!key) {
466
- key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
396
+ key = await ask(` OpenRouter API key (sk-or-...): `);
467
397
  key = key.trim();
468
- if (!key && existingKey) { key = existingKey; break; }
469
398
  if (key && !key.startsWith("sk-or-")) {
470
399
  console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
471
400
  const retry = await ask(` Use this key anyway? [y/N]: `);
@@ -479,17 +408,13 @@ async function setupWizard(envPath) {
479
408
  if (key) vars.OPENROUTER_API_KEY = key;
480
409
  } else {
481
410
  // Still ask for OpenRouter key (needed for vision/OCR)
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}: `);
411
+ const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip): `);
485
412
  if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
486
- else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
487
413
  }
488
414
 
489
415
  // 3. Agent selection
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;
416
+ const agentChoice = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
417
+ vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || "claude";
493
418
 
494
419
  // 3b. Local vision (Ollama)
495
420
  const IS_MACOS = os.platform() === "darwin";
@@ -498,24 +423,20 @@ async function setupWizard(envPath) {
498
423
  const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
499
424
  if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
500
425
  vars.LOCAL_VISION_ENABLED = "true";
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");
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");
515
434
  }
516
- } catch {
517
- warn("Could not check Ollama models");
435
+ } else {
436
+ ok("llava model already available");
518
437
  }
438
+ } catch {
439
+ warn("Could not check Ollama models");
519
440
  }
520
441
  vars.LOCAL_VISION_MODEL = "llava";
521
442
  }
@@ -530,8 +451,6 @@ async function setupWizard(envPath) {
530
451
  console.log(` ${DIM}Installing Ollama...${RESET}`);
531
452
  execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
532
453
  }
533
- // Start ollama serve before pulling
534
- await ensureOllama();
535
454
  console.log(` ${DIM}Pulling llava vision model...${RESET}`);
536
455
  execSync("ollama pull llava", { stdio: "inherit" });
537
456
  vars.LOCAL_VISION_ENABLED = "true";
@@ -550,44 +469,59 @@ async function setupWizard(envPath) {
550
469
  console.log(` selective — score-based (errors, questions trigger it)`);
551
470
  console.log(` focus — always escalate every tick`);
552
471
  console.log(` rich — always escalate with maximum context`);
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;
472
+ const escMode = await ask(` Escalation mode? [off/${BOLD}selective${RESET}/focus/rich]: `);
473
+ vars.ESCALATION_MODE = escMode.trim().toLowerCase() || "selective";
556
474
 
557
475
  // 5. OpenClaw gateway
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}: `);
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): `);
572
482
  if (wsToken.trim()) {
573
483
  vars.OPENCLAW_WS_TOKEN = wsToken.trim();
574
484
  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;
578
485
  }
579
486
 
580
487
  // Derive HTTP URL from WS URL
581
488
  const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
582
489
  vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
583
- vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
490
+ vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
584
491
  } else {
585
492
  // No gateway — disable WS connection attempts
586
493
  vars.OPENCLAW_WS_URL = "";
587
494
  vars.OPENCLAW_HTTP_URL = "";
588
495
  }
589
496
 
590
- // 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
591
525
  vars.SINAIN_POLL_INTERVAL = "5";
592
526
  vars.SINAIN_HEARTBEAT_INTERVAL = "900";
593
527
  vars.PRIVACY_MODE = "standard";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.6.6",
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 {