@geravant/sinain 1.23.1 → 1.23.3

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
@@ -69,6 +69,13 @@ switch (cmd) {
69
69
  break;
70
70
  }
71
71
 
72
+ case "setup-embedding": {
73
+ const { cacheEmbeddingModel } = await import("./setup-embedding.js");
74
+ const forceUpdate = process.argv.includes("--update");
75
+ await cacheEmbeddingModel({ forceUpdate });
76
+ break;
77
+ }
78
+
72
79
  case "install":
73
80
  // --if-openclaw: only run if OpenClaw is installed (for postinstall)
74
81
  if (process.argv.includes("--if-openclaw")) {
@@ -400,6 +407,7 @@ Usage:
400
407
  sinain setup (deprecated — use onboard)
401
408
  sinain setup-overlay Download pre-built overlay app
402
409
  sinain setup-sck-capture Download sck-capture audio binary (macOS)
410
+ sinain setup-embedding Pre-cache sentence-transformer model (~90MB)
403
411
  sinain export-knowledge Export knowledge for transfer to another machine
404
412
  sinain import-knowledge <file> Import knowledge from export file
405
413
  sinain install Install OpenClaw plugin (server-side)
@@ -416,6 +424,9 @@ Start options:
416
424
  Setup-overlay options:
417
425
  --from-source Build from Flutter source instead of downloading
418
426
  --update Force re-download even if version matches
427
+
428
+ Setup-embedding options:
429
+ --update Force re-download even if model is already cached
419
430
  `);
420
431
 
421
432
  }
package/launcher.js CHANGED
@@ -139,6 +139,17 @@ async function main() {
139
139
  }
140
140
  }
141
141
 
142
+ // Pre-cache embedding model if not already cached (prevents 10s huggingface.co
143
+ // download at sinain-core first-startup; skipped silently if SINAIN_SKIP_EMBEDDING_SETUP=1)
144
+ if (process.env.SINAIN_SKIP_EMBEDDING_SETUP !== "1") {
145
+ try {
146
+ const { cacheEmbeddingModel } = await import("./setup-embedding.js");
147
+ await cacheEmbeddingModel({ silent: true });
148
+ } catch (e) {
149
+ warn(`embedding model pre-cache skipped: ${e.message}`);
150
+ }
151
+ }
152
+
142
153
  // Start core
143
154
  log("Starting sinain-core...");
144
155
  const coreDir = path.join(PKG_DIR, "sinain-core");
package/onboard.js CHANGED
@@ -346,6 +346,27 @@ export async function runOnboard(args = {}) {
346
346
 
347
347
  await stepOverlay(base);
348
348
 
349
+ // ── Embedding model ───────────────────────────────────────────────────
350
+ // Pre-cache Xenova/all-MiniLM-L6-v2 (~90MB) so sinain-core startup is
351
+ // a cache-hit with no network activity. Helps all users, not just paranoid
352
+ // mode — runtime load goes from ~10s download to <1s cache read.
353
+
354
+ {
355
+ const s = p.spinner();
356
+ s.start("Pre-caching sentence-transformer model (~90MB)...");
357
+ try {
358
+ const { cacheEmbeddingModel } = await import("./setup-embedding.js");
359
+ await cacheEmbeddingModel({ silent: true });
360
+ s.stop(c.green("Embedding model cached."));
361
+ } catch (err) {
362
+ s.stop(c.yellow(`Embedding model pre-cache skipped: ${err.message}`));
363
+ p.note(
364
+ "Run manually: sinain setup-embedding\nOr set SINAIN_SKIP_EMBEDDING_SETUP=1 to skip.",
365
+ "Embedding",
366
+ );
367
+ }
368
+ }
369
+
349
370
  // ── Health check ──────────────────────────────────────────────────────
350
371
 
351
372
  await runHealthCheck();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.23.1",
3
+ "version": "1.23.3",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "mcp-register.js",
23
23
  "setup-overlay.js",
24
24
  "setup-sck-capture.js",
25
+ "setup-embedding.js",
25
26
  "pack-prepare.js",
26
27
  "install.js",
27
28
  "index.ts",
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ // sinain setup-embedding — pre-cache sentence-transformer model at setup time
3
+ //
4
+ // Moves the ~90MB Xenova/all-MiniLM-L6-v2 download from sinain-core's
5
+ // first-startup to install time, keeping runtime fully offline in paranoid mode.
6
+ // Mirrors the style of setup-overlay.js / setup-sck-capture.js.
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import { createRequire } from "module";
12
+
13
+ const HOME = os.homedir();
14
+ const MODEL_ID = "Xenova/all-MiniLM-L6-v2";
15
+
16
+ // sinain-core's node_modules contains @huggingface/transformers — load from there
17
+ // so we use the SAME package instance (and thus the same cache key) as runtime.
18
+ const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
19
+ const CORE_DIR = path.join(PKG_DIR, "sinain-core");
20
+
21
+ const BOLD = "\x1b[1m";
22
+ const GREEN = "\x1b[32m";
23
+ const YELLOW = "\x1b[33m";
24
+ const RED = "\x1b[31m";
25
+ const DIM = "\x1b[2m";
26
+ const RESET = "\x1b[0m";
27
+
28
+ function log(msg) { console.log(`${BOLD}[setup-embedding]${RESET} ${msg}`); }
29
+ function ok(msg) { console.log(`${BOLD}[setup-embedding]${RESET} ${GREEN}✓${RESET} ${msg}`); }
30
+ function warn(msg) { console.log(`${BOLD}[setup-embedding]${RESET} ${YELLOW}⚠${RESET} ${msg}`); }
31
+ function fail(msg) { console.error(`${BOLD}[setup-embedding]${RESET} ${RED}✗${RESET} ${msg}`); process.exit(1); }
32
+
33
+ // ── Entry point (only when run directly, not when imported) ──────────────────
34
+
35
+ const isMain = process.argv[1] && (
36
+ import.meta.url === `file://${process.argv[1]}` ||
37
+ import.meta.url === new URL(process.argv[1], "file://").href
38
+ );
39
+
40
+ if (isMain) {
41
+ const args = process.argv.slice(2);
42
+ const forceUpdate = args.includes("--update");
43
+ await cacheEmbeddingModel({ forceUpdate });
44
+ }
45
+
46
+ // ── Resolve cache directory (matches @huggingface/transformers default) ──────
47
+ //
48
+ // @huggingface/transformers stores models under:
49
+ // $TRANSFORMERS_CACHE OR
50
+ // $HF_HOME/hub OR
51
+ // ~/.cache/huggingface/hub/
52
+ //
53
+ // We use the same resolution order so setup and runtime share the same cache.
54
+
55
+ function resolveHfCacheDir() {
56
+ if (process.env.TRANSFORMERS_CACHE) return process.env.TRANSFORMERS_CACHE;
57
+ if (process.env.HF_HOME) return path.join(process.env.HF_HOME, "hub");
58
+ return path.join(HOME, ".cache", "huggingface", "hub");
59
+ }
60
+
61
+ // Model files land at: <cacheDir>/models--<org>--<name>/snapshots/**
62
+ // A snapshot directory means the model was cached successfully.
63
+
64
+ function isModelCached() {
65
+ const cacheDir = resolveHfCacheDir();
66
+ // Convert "Xenova/all-MiniLM-L6-v2" → "models--Xenova--all-MiniLM-L6-v2"
67
+ const modelFolder = `models--${MODEL_ID.replace("/", "--")}`;
68
+ const snapshotsDir = path.join(cacheDir, modelFolder, "snapshots");
69
+ if (!fs.existsSync(snapshotsDir)) return false;
70
+ // At least one snapshot sub-directory must exist and not be empty
71
+ try {
72
+ const snapshots = fs.readdirSync(snapshotsDir);
73
+ return snapshots.length > 0;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // ── Download / cache the model ───────────────────────────────────────────────
80
+
81
+ export async function cacheEmbeddingModel({ silent = false, forceUpdate = false } = {}) {
82
+ const _log = silent ? () => {} : log;
83
+ const _ok = silent ? () => {} : ok;
84
+ const _warn = silent ? () => {} : warn;
85
+
86
+ // Skip if already cached and not forcing update
87
+ if (!forceUpdate && isModelCached()) {
88
+ _ok(`Embedding model already cached (${MODEL_ID})`);
89
+ return true;
90
+ }
91
+
92
+ if (forceUpdate && isModelCached()) {
93
+ _log("Force update requested — re-downloading model...");
94
+ }
95
+
96
+ // Verify sinain-core's node_modules are present (installDeps runs first in launcher)
97
+ const transformersPath = path.join(CORE_DIR, "node_modules", "@huggingface", "transformers");
98
+ if (!fs.existsSync(transformersPath)) {
99
+ const msg = `sinain-core/node_modules not found at ${CORE_DIR}.\n` +
100
+ ` Run 'npm install' in sinain-core/ first, or let 'sinain start' handle it.`;
101
+ if (silent) { _warn(msg); return false; }
102
+ fail(msg);
103
+ }
104
+
105
+ _log(`Downloading sentence-transformer model (~90MB): ${MODEL_ID}`);
106
+ _log("This may take 30-60 seconds on a slow connection...");
107
+
108
+ const cacheDir = resolveHfCacheDir();
109
+ _log(`${DIM}Cache location: ${cacheDir}${RESET}`);
110
+
111
+ try {
112
+ // Load @huggingface/transformers from sinain-core's node_modules
113
+ // to guarantee the same module instance (and cache key) as the runtime.
114
+ const require = createRequire(path.join(CORE_DIR, "package.json"));
115
+ // Use dynamic import with the resolved path — createRequire gives us the path
116
+ const transformersEntry = require.resolve("@huggingface/transformers");
117
+ const { pipeline } = await import(transformersEntry);
118
+
119
+ const start = Date.now();
120
+
121
+ // Trigger the download by initialising the pipeline — identical call to
122
+ // EmbeddingService.loadAsync() in sinain-core/src/embedding/service.ts.
123
+ // When this resolves, the model is cached and subsequent calls (including
124
+ // sinain-core startup) are cache-hits with no network activity.
125
+ await pipeline("feature-extraction", MODEL_ID);
126
+
127
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
128
+ _ok(`Embedding model cached in ${elapsed}s`);
129
+
130
+ if (!silent) {
131
+ const cacheDir = resolveHfCacheDir();
132
+ console.log(`
133
+ ${GREEN}✓${RESET} Embedding model ready!
134
+ Model: ${MODEL_ID}
135
+ Cache: ${cacheDir}
136
+ sinain-core will load it from cache at startup (no network needed)
137
+ `);
138
+ }
139
+ return true;
140
+ } catch (e) {
141
+ const isNetworkError =
142
+ e.message?.includes("fetch") ||
143
+ e.message?.includes("network") ||
144
+ e.message?.includes("ENOTFOUND") ||
145
+ e.message?.includes("ECONNREFUSED") ||
146
+ e.message?.includes("huggingface") ||
147
+ e.code === "ENOTFOUND" ||
148
+ e.code === "ECONNREFUSED";
149
+
150
+ if (isNetworkError) {
151
+ const networkMsg =
152
+ `Failed to download embedding model from huggingface.co.\n` +
153
+ ` Check network access. To skip and let runtime download (not recommended\n` +
154
+ ` for paranoid mode), set SINAIN_SKIP_EMBEDDING_SETUP=1.`;
155
+ if (silent) { _warn(networkMsg); return false; }
156
+ fail(networkMsg);
157
+ }
158
+
159
+ const errorMsg = `Embedding model download failed: ${e.message?.slice(0, 200)}`;
160
+ if (silent) { _warn(errorMsg); return false; }
161
+ fail(errorMsg);
162
+ }
163
+ }
@@ -212,6 +212,8 @@ export class AudioPipeline extends EventEmitter {
212
212
 
213
213
  let headerSkipped = name !== "sox";
214
214
  let headerBuf = Buffer.alloc(0);
215
+ let stderrAccum = "";
216
+ const spawnTime = Date.now();
215
217
 
216
218
  proc.stdout?.on("data", (data: Buffer) => {
217
219
  if (!this.running) return;
@@ -237,6 +239,12 @@ export class AudioPipeline extends EventEmitter {
237
239
  if (msg && !/^In:.*Out:/.test(msg)) {
238
240
  log(TAG, `${name} stderr: ${msg.slice(0, 200)}`);
239
241
  }
242
+ // Accumulate stderr for TCC detection on exit
243
+ stderrAccum += data.toString();
244
+ // Cap accumulation to avoid unbounded growth (4KB is enough for any TCC message)
245
+ if (stderrAccum.length > 4096) {
246
+ stderrAccum = stderrAccum.slice(-4096);
247
+ }
240
248
  });
241
249
 
242
250
  proc.on("error", (err) => {
@@ -252,6 +260,45 @@ export class AudioPipeline extends EventEmitter {
252
260
  if (this.running && code !== 0) {
253
261
  this.errorCount++;
254
262
  this.profiler?.gauge("audio.errors", this.errorCount);
263
+
264
+ // Detect TCC (macOS Screen Recording / Microphone) permission denial.
265
+ // sck-capture logs "declined TCCs" to stderr when the entitlement is
266
+ // missing. The chicken-and-egg: clicking "Allow" on the prompt doesn't
267
+ // apply to a running process — the user must restart Terminal and
268
+ // re-run. We print a prominent banner and request graceful shutdown
269
+ // so users aren't left wondering why the agent never escalates.
270
+ const elapsedMs = Date.now() - spawnTime;
271
+ const isTccDenial = stderrAccum.includes("declined TCCs");
272
+ if (isTccDenial && elapsedMs < 5000) {
273
+ process.stdout.write([
274
+ "",
275
+ "=======================================================================",
276
+ " WARNING: Screen Recording permission needed",
277
+ "=======================================================================",
278
+ "",
279
+ " sck-capture cannot access screen capture and audio without",
280
+ " TCC (Screen Recording) permission from macOS.",
281
+ "",
282
+ " If you just clicked Allow -- that is normal! macOS does not apply",
283
+ " the permission to processes that are already running. To fix:",
284
+ "",
285
+ " 1. Press Ctrl+C to stop sinain",
286
+ " 2. Quit and restart your Terminal app (Cmd+Q, then reopen)",
287
+ " 3. Run again: npx @geravant/sinain@latest start",
288
+ "",
289
+ " Already declined? Re-grant permission:",
290
+ " System Settings > Privacy & Security > Screen Recording",
291
+ " > enable Terminal (or your terminal app)",
292
+ "",
293
+ "=======================================================================",
294
+ "",
295
+ ].join("\n"));
296
+
297
+ // Emit TCC-specific error so index.ts can initiate graceful shutdown
298
+ this.emit("tcc-denied");
299
+ return;
300
+ }
301
+
255
302
  warn(TAG, `${name} exited unexpectedly, stopping pipeline`);
256
303
  this.stop();
257
304
  }
@@ -543,7 +543,14 @@ async function importKnowledgeToLocal(data: string): Promise<string> {
543
543
  const dbPath = `${localDir}/knowledge-graph.db`;
544
544
 
545
545
  const __dir = dirname(fileURLToPath(import.meta.url));
546
- const scriptsDir = resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory");
546
+ // Two package layouts are supported:
547
+ // dev/monorepo: <repo>/sinain-core/src/ → ../../sinain-hud-plugin/sinain-memory
548
+ // npm-published flat: <pkg>/sinain-core/src/ → ../../sinain-memory
549
+ const scriptsDir = [
550
+ resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory"), // dev/monorepo layout
551
+ resolve(__dir, "..", "..", "sinain-memory"), // npm-published flat layout
552
+ resolve(__dir, "..", "sinain-memory"), // legacy alt
553
+ ].find(p => existsSync(`${p}/triplestore.py`)) || resolve(__dir, "..", "..", "sinain-memory");
547
554
 
548
555
  // Convert facts to graph ops for knowledge_integrator
549
556
  const graphOps = facts.map((f: any) => ({
@@ -834,6 +841,12 @@ async function main() {
834
841
  wsHandler.updateState({ audio: "muted" });
835
842
  });
836
843
 
844
+ systemAudioPipeline.on("tcc-denied", () => {
845
+ // Banner already printed by pipeline.ts. Initiate graceful shutdown so
846
+ // sinain exits cleanly rather than continuing with audio dead.
847
+ shutdown("TCC-DENIED").catch(() => process.exit(1));
848
+ });
849
+
837
850
  systemAudioPipeline.on("muted", () => {
838
851
  log(TAG, "system audio muted (capture process still running)");
839
852
  wsHandler.updateState({ audio: "muted" });
@@ -1006,6 +1019,23 @@ async function main() {
1006
1019
  }
1007
1020
  }
1008
1021
 
1022
+ // Pre-populate the roster from agents.json profiles so launchers that
1023
+ // don't run the bare-agent process (e.g. start.sh / start-local.sh) still
1024
+ // surface the user's configured profiles in the overlay's agent picker.
1025
+ // When the bare-agent IS running (npm install + cli.js start), its first
1026
+ // /bareagent/register POST narrows the list to PATH-installed binaries —
1027
+ // same final state as before, just with a usable initial state for the
1028
+ // dev-loop launcher.
1029
+ if (escalatorAgentsCfg?.profiles) {
1030
+ const profileNames = Object.keys(escalatorAgentsCfg.profiles)
1031
+ .filter((n) => AGENT_NAME_RE.test(n));
1032
+ if (profileNames.length > 0) {
1033
+ const defaultAgent = escalatorAgentsCfg.default ?? profileNames[0];
1034
+ registerBareAgent(profileNames, defaultAgent);
1035
+ log(TAG, `roster pre-populated from agents.json: ${profileNames.join(",")}`);
1036
+ }
1037
+ }
1038
+
1009
1039
  // ── Create HTTP + WS server ──
1010
1040
  const server = createAppServer({
1011
1041
  config,
@@ -55,10 +55,14 @@ export class EntityCache {
55
55
  return;
56
56
  }
57
57
 
58
- // Query entity names directly via SQLite
58
+ // Query entity names directly via SQLite.
59
+ // Two package layouts are supported:
60
+ // dev/monorepo: <repo>/sinain-core/src/learning/ → ../../../sinain-hud-plugin/sinain-memory
61
+ // npm-published flat: <pkg>/sinain-core/src/learning/ → ../../../sinain-memory
59
62
  const scriptCandidates = [
60
- resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
61
- resolve(__dir, "..", "sinain-memory", "graph_query.py"),
63
+ resolve(__dir, "..", "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"), // dev/monorepo layout
64
+ resolve(__dir, "..", "..", "..", "sinain-memory", "graph_query.py"), // npm-published flat layout
65
+ resolve(__dir, "..", "..", "sinain-memory", "graph_query.py"), // legacy alt
62
66
  ];
63
67
  const scriptPath = scriptCandidates.find(p => existsSync(p));
64
68
  if (!scriptPath) return;
@@ -23,18 +23,26 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
 
24
24
  /** Resolve the sinain-memory Python scripts directory. */
25
25
  function resolveScriptsDir(): string {
26
- // Look for sinain-memory scripts in known locations
26
+ // Look for sinain-memory scripts in known locations.
27
+ // Two package layouts are supported:
28
+ // dev/monorepo: <repo>/sinain-core/src/learning/ → ../../../sinain-hud-plugin/sinain-memory
29
+ // npm-published flat: <pkg>/sinain-core/src/learning/ → ../../../sinain-memory
27
30
  const candidates = [
28
- resolve(__dirname, "..", "..", "..", "sinain-hud-plugin", "sinain-memory"),
29
- resolve(__dirname, "..", "..", "sinain-memory"),
30
- resolve(process.env.HOME || "", ".sinain", "sinain-memory"),
31
+ resolve(__dirname, "..", "..", "..", "sinain-hud-plugin", "sinain-memory"), // dev/monorepo layout
32
+ resolve(__dirname, "..", "..", "..", "sinain-memory"), // npm-published flat layout
33
+ resolve(__dirname, "..", "..", "sinain-memory"), // legacy alt
34
+ resolve(process.env.HOME || "", ".sinain", "sinain-memory"), // user-local fallback
31
35
  ];
32
36
  for (const dir of candidates) {
33
37
  if (existsSync(resolve(dir, "session_distiller.py"))) {
34
38
  return dir;
35
39
  }
36
40
  }
37
- return candidates[0]; // Fallback
41
+ error(TAG, `sinain-memory scripts not found. Searched ${candidates.length} locations:`);
42
+ for (const dir of candidates) {
43
+ error(TAG, ` - ${dir}`);
44
+ }
45
+ return candidates[candidates.length - 1]; // Return user-local path as sentinel
38
46
  }
39
47
 
40
48
  /** Resolve the local memory directory. */