@geravant/sinain 1.6.6 → 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/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
@@ -587,7 +587,34 @@ async function setupWizard(envPath) {
587
587
  vars.OPENCLAW_HTTP_URL = "";
588
588
  }
589
589
 
590
- // 6. Agent-specific defaults
590
+ // 6. Knowledge import (for standalone machines)
591
+ console.log();
592
+ const wantImport = await ask(` Import knowledge from another machine? [y/N]: `);
593
+ if (wantImport.trim().toLowerCase() === "y") {
594
+ const filePath = await ask(` Path to knowledge export (.tar.gz): `);
595
+ const resolved = filePath.trim().replace(/^~/, HOME);
596
+ if (resolved && fs.existsSync(resolved)) {
597
+ const targetWorkspace = path.join(HOME, ".sinain/workspace");
598
+ fs.mkdirSync(targetWorkspace, { recursive: true });
599
+ try {
600
+ execSync(`tar xzf "${resolved}" -C "${targetWorkspace}"`, { stdio: "inherit" });
601
+ // Symlink sinain-memory scripts from npm package
602
+ const srcMemory = path.join(PKG_DIR, "sinain-memory");
603
+ const dstMemory = path.join(targetWorkspace, "sinain-memory");
604
+ try { fs.rmSync(dstMemory, { recursive: true }); } catch {}
605
+ fs.symlinkSync(srcMemory, dstMemory);
606
+ vars.SINAIN_WORKSPACE = targetWorkspace;
607
+ vars.OPENCLAW_WORKSPACE_DIR = targetWorkspace;
608
+ ok(`Knowledge imported to ${targetWorkspace}`);
609
+ } catch (e) {
610
+ warn(`Import failed: ${e.message}`);
611
+ }
612
+ } else if (resolved) {
613
+ warn(`File not found: ${resolved}`);
614
+ }
615
+ }
616
+
617
+ // 7. Agent-specific defaults
591
618
  vars.SINAIN_POLL_INTERVAL = "5";
592
619
  vars.SINAIN_HEARTBEAT_INTERVAL = "900";
593
620
  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.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": {
@@ -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
@@ -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 {