@geravant/sinain 1.23.2 → 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 +11 -0
- package/launcher.js +11 -0
- package/onboard.js +21 -0
- package/package.json +2 -1
- package/setup-embedding.js +163 -0
- package/sinain-core/src/index.ts +17 -0
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.
|
|
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
|
+
}
|
package/sinain-core/src/index.ts
CHANGED
|
@@ -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,
|