@geravant/sinain 1.23.2 → 1.23.4

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.23.2",
4
- "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
3
+ "version": "1.23.4",
4
+ "description": "Context OS captures what you see and hear, distills it into a private knowledge graph for AI-powered work",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sinain": "./cli.js",
@@ -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
+ }
@@ -53,10 +53,10 @@
53
53
  "agentMaxTurns": 8,
54
54
  "spawnMaxTurns": 25,
55
55
 
56
- "allowedTools": "mcp__sinain",
56
+ "allowedTools": "mcp__sinain ToolSearch",
57
57
  "escAllowedTools": "${allowedTools} Bash(git:*) Edit Write Read Glob Grep LS",
58
58
  "spawnAllowedTools": "${allowedTools} Bash(git:*) Edit Write Read Glob Grep LS",
59
- "autoApproveTools": "Read Glob Grep Ls Cat mcp__sinain*",
59
+ "autoApproveTools": "Read Glob Grep Ls Cat mcp__sinain* ToolSearch",
60
60
 
61
61
  "analyzer": {
62
62
  "debounceMs": 6000,
@@ -259,6 +259,12 @@ invoke_agent() {
259
259
  # still routes each call to the overlay for user Allow/Deny. Widen the
260
260
  # whitelist so the hook can do its job. Override via SINAIN_SPAWN_ALLOWED_TOOLS.
261
261
  local spawn_allowed="${SINAIN_SPAWN_ALLOWED_TOOLS:-${ALLOWED_TOOLS} Bash(git:*) Edit Write Read Glob Grep LS}"
262
+ # ToolSearch is a built-in Claude Code uses to load deferred MCP tool
263
+ # schemas. Without it pre-approved, every escalation that needs an
264
+ # un-cached sinain_* tool triggers a permission prompt — Test Mac
265
+ # hit this on overlay-v1.24.5 (~4 prompts per 7min). Always include
266
+ # regardless of agents.json content (defense-in-depth).
267
+ spawn_allowed="$spawn_allowed ToolSearch"
262
268
  if [ "$quiet" = "true" ]; then
263
269
  "$bin" \
264
270
  --mcp-config "$MCP_CONFIG" \
@@ -278,6 +284,9 @@ invoke_agent() {
278
284
  else
279
285
  # Escalation path. Override via SINAIN_ESC_ALLOWED_TOOLS.
280
286
  local esc_allowed="${SINAIN_ESC_ALLOWED_TOOLS:-${ALLOWED_TOOLS} Bash(git:*) Edit Write Read Glob Grep LS}"
287
+ # See spawn_allowed comment above — ToolSearch must be pre-approved
288
+ # or every escalation triggers a permission prompt.
289
+ esc_allowed="$esc_allowed ToolSearch"
281
290
  if [ "$quiet" = "true" ]; then
282
291
  "$bin" \
283
292
  --mcp-config "$MCP_CONFIG" \
@@ -1019,6 +1019,23 @@ async function main() {
1019
1019
  }
1020
1020
  }
1021
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
+
1022
1039
  // ── Create HTTP + WS server ──
1023
1040
  const server = createAppServer({
1024
1041
  config,