@geravant/sinain 1.0.19 → 1.2.0
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/README.md +10 -1
- package/cli.js +176 -0
- package/index.ts +4 -2
- package/install.js +89 -14
- package/launcher.js +622 -0
- package/openclaw.plugin.json +4 -0
- package/pack-prepare.js +48 -0
- package/package.json +24 -5
- package/sense_client/README.md +82 -0
- package/sense_client/__init__.py +1 -0
- package/sense_client/__main__.py +462 -0
- package/sense_client/app_detector.py +54 -0
- package/sense_client/app_detector_win.py +83 -0
- package/sense_client/capture.py +215 -0
- package/sense_client/capture_win.py +88 -0
- package/sense_client/change_detector.py +86 -0
- package/sense_client/config.py +64 -0
- package/sense_client/gate.py +145 -0
- package/sense_client/ocr.py +347 -0
- package/sense_client/privacy.py +65 -0
- package/sense_client/requirements.txt +13 -0
- package/sense_client/roi_extractor.py +84 -0
- package/sense_client/sender.py +173 -0
- package/sense_client/tests/__init__.py +0 -0
- package/sense_client/tests/test_stream1_optimizations.py +234 -0
- package/setup-overlay.js +82 -0
- package/sinain-agent/.env.example +17 -0
- package/sinain-agent/CLAUDE.md +87 -0
- package/sinain-agent/mcp-config.json +12 -0
- package/sinain-agent/run.sh +248 -0
- package/sinain-core/.env.example +93 -0
- package/sinain-core/package-lock.json +552 -0
- package/sinain-core/package.json +21 -0
- package/sinain-core/src/agent/analyzer.ts +366 -0
- package/sinain-core/src/agent/context-window.ts +172 -0
- package/sinain-core/src/agent/loop.ts +404 -0
- package/sinain-core/src/agent/situation-writer.ts +187 -0
- package/sinain-core/src/agent/traits.ts +520 -0
- package/sinain-core/src/audio/capture-spawner-macos.ts +44 -0
- package/sinain-core/src/audio/capture-spawner-win.ts +37 -0
- package/sinain-core/src/audio/capture-spawner.ts +14 -0
- package/sinain-core/src/audio/pipeline.ts +335 -0
- package/sinain-core/src/audio/transcription-local.ts +141 -0
- package/sinain-core/src/audio/transcription.ts +278 -0
- package/sinain-core/src/buffers/feed-buffer.ts +71 -0
- package/sinain-core/src/buffers/sense-buffer.ts +425 -0
- package/sinain-core/src/config.ts +245 -0
- package/sinain-core/src/escalation/escalation-slot.ts +136 -0
- package/sinain-core/src/escalation/escalator.ts +828 -0
- package/sinain-core/src/escalation/message-builder.ts +370 -0
- package/sinain-core/src/escalation/openclaw-ws.ts +726 -0
- package/sinain-core/src/escalation/scorer.ts +166 -0
- package/sinain-core/src/index.ts +537 -0
- package/sinain-core/src/learning/feedback-store.ts +253 -0
- package/sinain-core/src/learning/signal-collector.ts +218 -0
- package/sinain-core/src/log.ts +24 -0
- package/sinain-core/src/overlay/commands.ts +126 -0
- package/sinain-core/src/overlay/ws-handler.ts +267 -0
- package/sinain-core/src/privacy/index.ts +18 -0
- package/sinain-core/src/privacy/presets.ts +40 -0
- package/sinain-core/src/privacy/redact.ts +92 -0
- package/sinain-core/src/profiler.ts +181 -0
- package/sinain-core/src/recorder.ts +186 -0
- package/sinain-core/src/server.ts +456 -0
- package/sinain-core/src/trace/trace-store.ts +73 -0
- package/sinain-core/src/trace/tracer.ts +94 -0
- package/sinain-core/src/types.ts +427 -0
- package/sinain-core/src/util/dedup.ts +48 -0
- package/sinain-core/src/util/task-store.ts +84 -0
- package/sinain-core/tsconfig.json +18 -0
- package/sinain-knowledge/curation/engine.ts +137 -24
- package/sinain-knowledge/data/git-store.ts +26 -0
- package/sinain-knowledge/data/store.ts +117 -0
- package/sinain-mcp-server/index.ts +417 -0
- package/sinain-mcp-server/package.json +19 -0
- package/sinain-mcp-server/tsconfig.json +15 -0
- package/sinain-memory/graph_query.py +185 -0
- package/sinain-memory/knowledge_integrator.py +450 -0
- package/sinain-memory/memory-config.json +3 -1
- package/sinain-memory/session_distiller.py +162 -0
package/launcher.js
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// sinain launcher — process orchestrator for `sinain start`
|
|
3
|
+
// Ports the logic from start.sh + sinain-agent/run.sh into a single Node.js process manager.
|
|
4
|
+
|
|
5
|
+
import { spawn, execSync } from "child_process";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import net from "net";
|
|
10
|
+
import readline from "readline";
|
|
11
|
+
|
|
12
|
+
// ── Colors ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const CYAN = "\x1b[36m";
|
|
15
|
+
const GREEN = "\x1b[32m";
|
|
16
|
+
const YELLOW = "\x1b[33m";
|
|
17
|
+
const MAGENTA = "\x1b[35m";
|
|
18
|
+
const RED = "\x1b[31m";
|
|
19
|
+
const BOLD = "\x1b[1m";
|
|
20
|
+
const DIM = "\x1b[2m";
|
|
21
|
+
const RESET = "\x1b[0m";
|
|
22
|
+
|
|
23
|
+
// ── Resolve paths ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
26
|
+
const HOME = os.homedir();
|
|
27
|
+
const SINAIN_DIR = path.join(HOME, ".sinain");
|
|
28
|
+
const PID_FILE = "/tmp/sinain-pids.txt";
|
|
29
|
+
|
|
30
|
+
// ── Parse flags ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const args = process.argv.slice(3); // skip node, cli.js, "start"
|
|
33
|
+
let skipSense = false;
|
|
34
|
+
let skipOverlay = false;
|
|
35
|
+
let skipAgent = false;
|
|
36
|
+
let agentName = null;
|
|
37
|
+
|
|
38
|
+
for (const arg of args) {
|
|
39
|
+
if (arg === "--no-sense") { skipSense = true; continue; }
|
|
40
|
+
if (arg === "--no-overlay") { skipOverlay = true; continue; }
|
|
41
|
+
if (arg === "--no-agent") { skipAgent = true; continue; }
|
|
42
|
+
if (arg.startsWith("--agent=")) { agentName = arg.split("=")[1]; continue; }
|
|
43
|
+
console.error(`Unknown flag: ${arg}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── State ───────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const children = []; // { name, proc, pid }
|
|
50
|
+
|
|
51
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
await main();
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
setupSignalHandlers();
|
|
57
|
+
|
|
58
|
+
log("Preflight checks...");
|
|
59
|
+
await preflight();
|
|
60
|
+
console.log();
|
|
61
|
+
|
|
62
|
+
// Load user config
|
|
63
|
+
loadUserEnv();
|
|
64
|
+
|
|
65
|
+
// Auto-detect transcription backend
|
|
66
|
+
detectTranscription();
|
|
67
|
+
|
|
68
|
+
// Kill stale processes
|
|
69
|
+
killStale();
|
|
70
|
+
|
|
71
|
+
// Install deps if needed
|
|
72
|
+
await installDeps();
|
|
73
|
+
|
|
74
|
+
// Start core
|
|
75
|
+
log("Starting sinain-core...");
|
|
76
|
+
const coreDir = path.join(PKG_DIR, "sinain-core");
|
|
77
|
+
const tsxBin = path.join(coreDir, "node_modules/.bin/tsx");
|
|
78
|
+
const coreEntry = path.join(coreDir, "src/index.ts");
|
|
79
|
+
|
|
80
|
+
// Pass .env vars to core (it also loads its own .env, but user config should override)
|
|
81
|
+
startProcess("core", tsxBin, ["watch", coreEntry], {
|
|
82
|
+
cwd: coreDir,
|
|
83
|
+
color: CYAN,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Health check
|
|
87
|
+
const healthy = await healthCheck("http://localhost:9500/health", 20);
|
|
88
|
+
if (!healthy) {
|
|
89
|
+
fail("sinain-core did not become healthy after 20s");
|
|
90
|
+
}
|
|
91
|
+
ok("sinain-core healthy on :9500");
|
|
92
|
+
|
|
93
|
+
// Start sense_client
|
|
94
|
+
let senseStatus = "skipped";
|
|
95
|
+
if (!skipSense) {
|
|
96
|
+
const hasPython = commandExists("python3");
|
|
97
|
+
if (hasPython) {
|
|
98
|
+
// Install sense deps if needed
|
|
99
|
+
const reqFile = path.join(PKG_DIR, "sense_client/requirements.txt");
|
|
100
|
+
if (fs.existsSync(reqFile)) {
|
|
101
|
+
const scDir = path.join(PKG_DIR, "sense_client");
|
|
102
|
+
// Check if key package is importable to skip pip
|
|
103
|
+
try {
|
|
104
|
+
execSync('python3 -c "import cv2; import skimage"', { stdio: "pipe" });
|
|
105
|
+
} catch {
|
|
106
|
+
log("Installing sense_client Python dependencies...");
|
|
107
|
+
try {
|
|
108
|
+
execSync(`pip3 install -r "${reqFile}" --quiet --break-system-packages`, { stdio: "inherit" });
|
|
109
|
+
} catch {
|
|
110
|
+
try {
|
|
111
|
+
execSync(`pip3 install -r "${reqFile}" --quiet`, { stdio: "inherit" });
|
|
112
|
+
} catch {
|
|
113
|
+
warn("pip3 install failed — sense_client may not work");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log("Starting sense_client...");
|
|
120
|
+
startProcess("sense", "python3", ["-m", "sense_client"], {
|
|
121
|
+
cwd: PKG_DIR,
|
|
122
|
+
color: YELLOW,
|
|
123
|
+
});
|
|
124
|
+
// Give it a moment to fail fast if misconfigured
|
|
125
|
+
await sleep(1000);
|
|
126
|
+
const senseChild = children.find(c => c.name === "sense");
|
|
127
|
+
if (senseChild && !senseChild.proc.killed && senseChild.proc.exitCode === null) {
|
|
128
|
+
ok(`sense_client running (pid:${senseChild.pid})`);
|
|
129
|
+
senseStatus = "running";
|
|
130
|
+
} else {
|
|
131
|
+
warn("sense_client exited early — check logs above");
|
|
132
|
+
senseStatus = "failed";
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
warn("python3 not found — sense_client skipped");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Start overlay
|
|
140
|
+
let overlayStatus = "skipped";
|
|
141
|
+
if (!skipOverlay) {
|
|
142
|
+
const overlayDir = findOverlayDir();
|
|
143
|
+
const hasFlutter = commandExists("flutter");
|
|
144
|
+
if (overlayDir && hasFlutter) {
|
|
145
|
+
log("Starting overlay...");
|
|
146
|
+
startProcess("overlay", "flutter", ["run", "-d", "macos"], {
|
|
147
|
+
cwd: overlayDir,
|
|
148
|
+
color: MAGENTA,
|
|
149
|
+
});
|
|
150
|
+
await sleep(2000);
|
|
151
|
+
const overlayChild = children.find(c => c.name === "overlay");
|
|
152
|
+
if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
|
|
153
|
+
ok(`overlay running (pid:${overlayChild.pid})`);
|
|
154
|
+
overlayStatus = "running";
|
|
155
|
+
} else {
|
|
156
|
+
warn("overlay exited early — check logs above");
|
|
157
|
+
overlayStatus = "failed";
|
|
158
|
+
}
|
|
159
|
+
} else if (!overlayDir) {
|
|
160
|
+
warn("overlay not found — run: sinain setup-overlay");
|
|
161
|
+
} else {
|
|
162
|
+
warn("flutter not found — overlay skipped");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Start agent
|
|
167
|
+
let agentStatus = "skipped";
|
|
168
|
+
if (!skipAgent) {
|
|
169
|
+
const runSh = path.join(PKG_DIR, "sinain-agent/run.sh");
|
|
170
|
+
if (fs.existsSync(runSh)) {
|
|
171
|
+
// Generate MCP config with absolute paths
|
|
172
|
+
const mcpConfigPath = generateMcpConfig();
|
|
173
|
+
|
|
174
|
+
// Resolve agent name
|
|
175
|
+
const agent = agentName || process.env.SINAIN_AGENT || "claude";
|
|
176
|
+
|
|
177
|
+
log(`Starting agent (${agent})...`);
|
|
178
|
+
startProcess("agent", "bash", [runSh], {
|
|
179
|
+
cwd: path.join(PKG_DIR, "sinain-agent"),
|
|
180
|
+
color: GREEN,
|
|
181
|
+
extraEnv: {
|
|
182
|
+
MCP_CONFIG: mcpConfigPath,
|
|
183
|
+
SINAIN_AGENT: agent,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
await sleep(2000);
|
|
187
|
+
const agentChild = children.find(c => c.name === "agent");
|
|
188
|
+
if (agentChild && !agentChild.proc.killed && agentChild.proc.exitCode === null) {
|
|
189
|
+
ok(`agent running (pid:${agentChild.pid})`);
|
|
190
|
+
agentStatus = "running";
|
|
191
|
+
} else {
|
|
192
|
+
warn("agent exited early — check logs above");
|
|
193
|
+
agentStatus = "failed";
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
warn("sinain-agent/run.sh not found — agent skipped");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Write PID file
|
|
201
|
+
writePidFile();
|
|
202
|
+
|
|
203
|
+
// Banner
|
|
204
|
+
printBanner({ senseStatus, overlayStatus, agentStatus });
|
|
205
|
+
|
|
206
|
+
// Wait forever (children keep us alive)
|
|
207
|
+
await new Promise(() => {});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Preflight ───────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function preflight() {
|
|
213
|
+
// Node version
|
|
214
|
+
const nodeVer = process.version;
|
|
215
|
+
const major = parseInt(nodeVer.slice(1));
|
|
216
|
+
if (major < 18) {
|
|
217
|
+
fail(`Node.js >= 18 required (found ${nodeVer})`);
|
|
218
|
+
}
|
|
219
|
+
ok(`node ${nodeVer}`);
|
|
220
|
+
|
|
221
|
+
// Python
|
|
222
|
+
if (commandExists("python3")) {
|
|
223
|
+
const pyVer = execSync("python3 --version 2>&1", { encoding: "utf-8" }).trim().split(" ")[1];
|
|
224
|
+
ok(`python3 ${pyVer}`);
|
|
225
|
+
} else {
|
|
226
|
+
warn("python3 not found — sense_client will be skipped");
|
|
227
|
+
skipSense = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Flutter (optional)
|
|
231
|
+
if (commandExists("flutter")) {
|
|
232
|
+
try {
|
|
233
|
+
const flutterVer = execSync("flutter --version 2>&1", { encoding: "utf-8" }).split("\n")[0].split(" ")[1];
|
|
234
|
+
ok(`flutter ${flutterVer}`);
|
|
235
|
+
} catch {
|
|
236
|
+
ok("flutter (version unknown)");
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
warn("flutter not found — overlay will be skipped");
|
|
240
|
+
skipOverlay = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Port 9500
|
|
244
|
+
const portFree = await isPortFree(9500);
|
|
245
|
+
if (!portFree) {
|
|
246
|
+
// Will be freed by killStale
|
|
247
|
+
warn("port 9500 in use — will attempt to free");
|
|
248
|
+
} else {
|
|
249
|
+
ok("port 9500 free");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── User environment ────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function loadUserEnv() {
|
|
256
|
+
const envPaths = [
|
|
257
|
+
path.join(SINAIN_DIR, ".env"),
|
|
258
|
+
path.join(PKG_DIR, "sinain-core/.env"),
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const envPath of envPaths) {
|
|
262
|
+
if (!fs.existsSync(envPath)) continue;
|
|
263
|
+
const lines = fs.readFileSync(envPath, "utf-8").split("\n");
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
const trimmed = line.trim();
|
|
266
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
267
|
+
const eq = trimmed.indexOf("=");
|
|
268
|
+
if (eq === -1) continue;
|
|
269
|
+
const key = trimmed.slice(0, eq).trim();
|
|
270
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
271
|
+
// Don't override existing env vars
|
|
272
|
+
if (!process.env[key]) {
|
|
273
|
+
process.env[key] = val;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Ensure ~/.sinain directory exists
|
|
279
|
+
fs.mkdirSync(SINAIN_DIR, { recursive: true });
|
|
280
|
+
fs.mkdirSync(path.join(HOME, ".sinain/capture"), { recursive: true });
|
|
281
|
+
|
|
282
|
+
// Check for API key
|
|
283
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
284
|
+
warn("OPENROUTER_API_KEY not set");
|
|
285
|
+
console.log(` Set it in ${path.join(SINAIN_DIR, ".env")}:`);
|
|
286
|
+
console.log(` OPENROUTER_API_KEY=sk-or-...`);
|
|
287
|
+
console.log();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Transcription auto-detect ───────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function detectTranscription() {
|
|
294
|
+
if (process.env.TRANSCRIPTION_BACKEND) return;
|
|
295
|
+
|
|
296
|
+
// Check for whisper-cli (local transcription)
|
|
297
|
+
if (commandExists("whisper-cli")) {
|
|
298
|
+
process.env.TRANSCRIPTION_BACKEND = "local";
|
|
299
|
+
ok("transcription: local (whisper-cli)");
|
|
300
|
+
|
|
301
|
+
// Try to find model path
|
|
302
|
+
if (!process.env.WHISPER_MODEL_PATH) {
|
|
303
|
+
const defaultModel = path.join(HOME, ".cache/whisper/ggml-base.en.bin");
|
|
304
|
+
if (fs.existsSync(defaultModel)) {
|
|
305
|
+
process.env.WHISPER_MODEL_PATH = defaultModel;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Fallback: OpenRouter API
|
|
312
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
313
|
+
process.env.TRANSCRIPTION_BACKEND = "openrouter";
|
|
314
|
+
ok("transcription: openrouter (API)");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
warn("No transcription backend detected");
|
|
319
|
+
console.log(" Option 1: Install whisper-cli for local transcription");
|
|
320
|
+
console.log(" Option 2: Set OPENROUTER_API_KEY for cloud transcription");
|
|
321
|
+
console.log();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Install dependencies ────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
async function installDeps() {
|
|
327
|
+
const coreDir = path.join(PKG_DIR, "sinain-core");
|
|
328
|
+
if (!fs.existsSync(path.join(coreDir, "node_modules"))) {
|
|
329
|
+
log("Installing sinain-core dependencies...");
|
|
330
|
+
execSync("npm install --production", { cwd: coreDir, stdio: "inherit" });
|
|
331
|
+
ok("sinain-core dependencies installed");
|
|
332
|
+
} else {
|
|
333
|
+
ok("sinain-core/node_modules present");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const mcpDir = path.join(PKG_DIR, "sinain-mcp-server");
|
|
337
|
+
if (fs.existsSync(mcpDir) && !fs.existsSync(path.join(mcpDir, "node_modules"))) {
|
|
338
|
+
log("Installing sinain-mcp-server dependencies...");
|
|
339
|
+
execSync("npm install --production", { cwd: mcpDir, stdio: "inherit" });
|
|
340
|
+
ok("sinain-mcp-server dependencies installed");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Kill stale processes ────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
function killStale() {
|
|
347
|
+
let killed = false;
|
|
348
|
+
const patterns = [
|
|
349
|
+
"sinain_hud.app/Contents/MacOS/sinain_hud",
|
|
350
|
+
"flutter run -d macos",
|
|
351
|
+
"python3 -m sense_client",
|
|
352
|
+
"Python -m sense_client",
|
|
353
|
+
"tsx.*src/index.ts",
|
|
354
|
+
"tsx watch src/index.ts",
|
|
355
|
+
"sinain-agent/run.sh",
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
for (const pat of patterns) {
|
|
359
|
+
try {
|
|
360
|
+
execSync(`pkill -f "${pat}"`, { stdio: "pipe" });
|
|
361
|
+
killed = true;
|
|
362
|
+
} catch { /* not running */ }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Free port 9500
|
|
366
|
+
try {
|
|
367
|
+
const pid = execSync("lsof -i :9500 -sTCP:LISTEN -t", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
368
|
+
if (pid) {
|
|
369
|
+
execSync(`kill ${pid}`, { stdio: "pipe" });
|
|
370
|
+
killed = true;
|
|
371
|
+
}
|
|
372
|
+
} catch { /* already free */ }
|
|
373
|
+
|
|
374
|
+
// Clean old PID file
|
|
375
|
+
if (fs.existsSync(PID_FILE)) {
|
|
376
|
+
try {
|
|
377
|
+
const lines = fs.readFileSync(PID_FILE, "utf-8").split("\n");
|
|
378
|
+
for (const line of lines) {
|
|
379
|
+
const pid = line.split("=")[1]?.trim();
|
|
380
|
+
if (pid) {
|
|
381
|
+
try { process.kill(parseInt(pid), "SIGTERM"); killed = true; } catch { /* gone */ }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch { /* ignore */ }
|
|
385
|
+
fs.unlinkSync(PID_FILE);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (killed) {
|
|
389
|
+
warn("killed stale processes from previous run");
|
|
390
|
+
// Brief pause for ports to free
|
|
391
|
+
execSync("sleep 1", { stdio: "pipe" });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Process management ──────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
function startProcess(name, command, args, { cwd, color, extraEnv = {} } = {}) {
|
|
398
|
+
const env = { ...process.env, ...extraEnv };
|
|
399
|
+
|
|
400
|
+
const proc = spawn(command, args, {
|
|
401
|
+
cwd,
|
|
402
|
+
env,
|
|
403
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const prefix = `${color}[${name}]${RESET}`.padEnd(22); // account for ANSI codes
|
|
407
|
+
|
|
408
|
+
// Pipe stdout with prefix
|
|
409
|
+
if (proc.stdout) {
|
|
410
|
+
const rl = readline.createInterface({ input: proc.stdout });
|
|
411
|
+
rl.on("line", (line) => {
|
|
412
|
+
process.stdout.write(`${prefix} ${line}\n`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Pipe stderr with prefix
|
|
417
|
+
if (proc.stderr) {
|
|
418
|
+
const rl = readline.createInterface({ input: proc.stderr });
|
|
419
|
+
rl.on("line", (line) => {
|
|
420
|
+
process.stderr.write(`${prefix} ${line}\n`);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
proc.on("exit", (code) => {
|
|
425
|
+
if (code !== null && code !== 0) {
|
|
426
|
+
console.log(`${prefix} exited with code ${code}`);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
children.push({ name, proc, pid: proc.pid });
|
|
431
|
+
return proc;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── MCP config generation ───────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
function generateMcpConfig() {
|
|
437
|
+
const coreDir = path.join(PKG_DIR, "sinain-core");
|
|
438
|
+
const tsxBin = path.join(coreDir, "node_modules/.bin/tsx");
|
|
439
|
+
const mcpEntry = path.join(PKG_DIR, "sinain-mcp-server/index.ts");
|
|
440
|
+
const workspace = process.env.SINAIN_WORKSPACE || path.join(HOME, ".openclaw/workspace");
|
|
441
|
+
|
|
442
|
+
const config = {
|
|
443
|
+
mcpServers: {
|
|
444
|
+
sinain: {
|
|
445
|
+
command: tsxBin,
|
|
446
|
+
args: [mcpEntry],
|
|
447
|
+
env: {
|
|
448
|
+
SINAIN_CORE_URL: process.env.SINAIN_CORE_URL || "http://localhost:9500",
|
|
449
|
+
SINAIN_WORKSPACE: workspace,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const tmpDir = path.join(SINAIN_DIR, "tmp");
|
|
456
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
457
|
+
const configPath = path.join(tmpDir, "mcp-config.json");
|
|
458
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
459
|
+
return configPath;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Overlay discovery ───────────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
function findOverlayDir() {
|
|
465
|
+
// 1. Sibling overlay/ (running from cloned repo)
|
|
466
|
+
const siblingOverlay = path.join(PKG_DIR, "..", "overlay");
|
|
467
|
+
if (fs.existsSync(path.join(siblingOverlay, "pubspec.yaml"))) {
|
|
468
|
+
return siblingOverlay;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 2. ~/.sinain/overlay/ (installed via setup-overlay)
|
|
472
|
+
const installedOverlay = path.join(SINAIN_DIR, "overlay");
|
|
473
|
+
if (fs.existsSync(path.join(installedOverlay, "pubspec.yaml"))) {
|
|
474
|
+
return installedOverlay;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Health check ────────────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
async function healthCheck(url, retries) {
|
|
483
|
+
for (let i = 0; i < retries; i++) {
|
|
484
|
+
try {
|
|
485
|
+
const res = await fetch(url);
|
|
486
|
+
if (res.ok) return true;
|
|
487
|
+
} catch { /* not ready yet */ }
|
|
488
|
+
await sleep(1000);
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Banner ──────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
function printBanner({ senseStatus, overlayStatus, agentStatus }) {
|
|
496
|
+
console.log();
|
|
497
|
+
console.log(`${BOLD}── SinainHUD ──────────────────────────${RESET}`);
|
|
498
|
+
|
|
499
|
+
// Core (always running if we got here)
|
|
500
|
+
console.log(` ${CYAN}core${RESET} :9500 ${GREEN}✓${RESET} (http+ws)`);
|
|
501
|
+
|
|
502
|
+
// Sense
|
|
503
|
+
printServiceLine("sense", YELLOW, senseStatus);
|
|
504
|
+
|
|
505
|
+
// Overlay
|
|
506
|
+
printServiceLine("overlay", MAGENTA, overlayStatus);
|
|
507
|
+
|
|
508
|
+
// Agent
|
|
509
|
+
printServiceLine("agent", GREEN, agentStatus);
|
|
510
|
+
|
|
511
|
+
console.log(`${BOLD}───────────────────────────────────────${RESET}`);
|
|
512
|
+
console.log(` Press ${BOLD}Ctrl+C${RESET} to stop all services`);
|
|
513
|
+
console.log(`${BOLD}───────────────────────────────────────${RESET}`);
|
|
514
|
+
console.log();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function printServiceLine(name, color, status) {
|
|
518
|
+
const padded = name.padEnd(8);
|
|
519
|
+
switch (status) {
|
|
520
|
+
case "running":
|
|
521
|
+
console.log(` ${color}${padded}${RESET} ${GREEN}✓${RESET} running`);
|
|
522
|
+
break;
|
|
523
|
+
case "failed":
|
|
524
|
+
console.log(` ${color}${padded}${RESET} ${RED}✗${RESET} failed`);
|
|
525
|
+
break;
|
|
526
|
+
case "skipped":
|
|
527
|
+
default:
|
|
528
|
+
console.log(` ${color}${padded}${RESET} ${DIM}— skipped${RESET}`);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── PID file ────────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
function writePidFile() {
|
|
536
|
+
const lines = children.map(c => `${c.name}=${c.pid}`).join("\n");
|
|
537
|
+
fs.writeFileSync(PID_FILE, lines + "\n");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Cleanup ─────────────────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
function setupSignalHandlers() {
|
|
543
|
+
let cleaning = false;
|
|
544
|
+
|
|
545
|
+
const cleanup = (signal) => {
|
|
546
|
+
if (cleaning) return;
|
|
547
|
+
cleaning = true;
|
|
548
|
+
console.log(`\n${BOLD}[start]${RESET} Shutting down services...`);
|
|
549
|
+
|
|
550
|
+
// SIGTERM all children
|
|
551
|
+
for (const { proc, name } of children) {
|
|
552
|
+
try {
|
|
553
|
+
if (!proc.killed) proc.kill("SIGTERM");
|
|
554
|
+
} catch { /* already gone */ }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Force kill after 2s
|
|
558
|
+
setTimeout(() => {
|
|
559
|
+
for (const { proc } of children) {
|
|
560
|
+
try {
|
|
561
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
562
|
+
} catch { /* already gone */ }
|
|
563
|
+
}
|
|
564
|
+
// Clean up port
|
|
565
|
+
try {
|
|
566
|
+
execSync("lsof -i :9500 -sTCP:LISTEN -t 2>/dev/null | xargs kill -9 2>/dev/null", { stdio: "pipe" });
|
|
567
|
+
} catch { /* ok */ }
|
|
568
|
+
|
|
569
|
+
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
|
570
|
+
console.log(`${BOLD}[start]${RESET} All services stopped.`);
|
|
571
|
+
process.exit(0);
|
|
572
|
+
}, 2000);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
process.on("SIGINT", cleanup);
|
|
576
|
+
process.on("SIGTERM", cleanup);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
function commandExists(cmd) {
|
|
582
|
+
try {
|
|
583
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function isPortFree(port) {
|
|
591
|
+
return new Promise((resolve) => {
|
|
592
|
+
const server = net.createServer();
|
|
593
|
+
server.once("error", () => resolve(false));
|
|
594
|
+
server.once("listening", () => { server.close(); resolve(true); });
|
|
595
|
+
server.listen(port, "127.0.0.1");
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function sleep(ms) {
|
|
600
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function log(msg) {
|
|
604
|
+
console.log(`${BOLD}[start]${RESET} ${msg}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function ok(msg) {
|
|
608
|
+
console.log(`${BOLD}[start]${RESET} ${GREEN}✓${RESET} ${msg}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function warn(msg) {
|
|
612
|
+
console.log(`${BOLD}[start]${RESET} ${YELLOW}⚠${RESET} ${msg}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function fail(msg) {
|
|
616
|
+
console.error(`${BOLD}[start]${RESET} ${RED}✗${RESET} ${msg}`);
|
|
617
|
+
// Kill any started children before exiting
|
|
618
|
+
for (const { proc } of children) {
|
|
619
|
+
try { if (!proc.killed) proc.kill("SIGKILL"); } catch { /* ok */ }
|
|
620
|
+
}
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"userTimezone": {
|
|
32
32
|
"type": "string",
|
|
33
33
|
"description": "IANA timezone for time-aware context injection (e.g. Europe/Berlin)"
|
|
34
|
+
},
|
|
35
|
+
"snapshotRepoPath": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Local path to git repo for periodic knowledge snapshots"
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
},
|
package/pack-prepare.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// prepack: replace symlinks with real copies so npm pack bundles the files.
|
|
3
|
+
// postpack: restore symlinks.
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
const LINKS = ["sinain-core", "sinain-mcp-server", "sinain-agent", "sense_client"];
|
|
9
|
+
const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
10
|
+
|
|
11
|
+
const action = process.argv[2]; // "pre" or "post"
|
|
12
|
+
|
|
13
|
+
if (action === "pre") {
|
|
14
|
+
for (const name of LINKS) {
|
|
15
|
+
const linkPath = path.join(PKG_DIR, name);
|
|
16
|
+
if (!fs.existsSync(linkPath)) continue;
|
|
17
|
+
const stat = fs.lstatSync(linkPath);
|
|
18
|
+
if (!stat.isSymbolicLink()) continue;
|
|
19
|
+
const target = fs.realpathSync(linkPath);
|
|
20
|
+
fs.unlinkSync(linkPath);
|
|
21
|
+
copyDir(target, linkPath);
|
|
22
|
+
}
|
|
23
|
+
console.log("prepack: symlinks → copies");
|
|
24
|
+
} else if (action === "post") {
|
|
25
|
+
for (const name of LINKS) {
|
|
26
|
+
const linkPath = path.join(PKG_DIR, name);
|
|
27
|
+
if (!fs.existsSync(linkPath)) continue;
|
|
28
|
+
const stat = fs.lstatSync(linkPath);
|
|
29
|
+
if (stat.isSymbolicLink()) continue; // already a symlink
|
|
30
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
31
|
+
fs.symlinkSync(`../${name}`, linkPath);
|
|
32
|
+
}
|
|
33
|
+
console.log("postpack: copies → symlinks");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyDir(src, dst) {
|
|
37
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
38
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
39
|
+
if (["node_modules", "__pycache__", ".pytest_cache", "dist", ".env"].includes(entry.name)) continue;
|
|
40
|
+
const s = path.join(src, entry.name);
|
|
41
|
+
const d = path.join(dst, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
copyDir(s, d);
|
|
44
|
+
} else {
|
|
45
|
+
fs.copyFileSync(s, d);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|