@geravant/sinain 1.2.1 → 1.4.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/cli.js CHANGED
@@ -6,6 +6,7 @@ import fs from "fs";
6
6
  import path from "path";
7
7
 
8
8
  const cmd = process.argv[2];
9
+ const IS_WINDOWS = os.platform() === "win32";
9
10
 
10
11
  switch (cmd) {
11
12
  case "start":
@@ -20,6 +21,10 @@ switch (cmd) {
20
21
  await showStatus();
21
22
  break;
22
23
 
24
+ case "setup":
25
+ await runSetupWizard();
26
+ break;
27
+
23
28
  case "setup-overlay":
24
29
  await import("./setup-overlay.js");
25
30
  break;
@@ -41,39 +46,185 @@ switch (cmd) {
41
46
  break;
42
47
  }
43
48
 
49
+ // ── Setup wizard (standalone) ─────────────────────────────────────────────────
50
+
51
+ async function runSetupWizard() {
52
+ // Force-run the wizard even if .env exists (re-configure)
53
+ const { setupWizard } = await import("./launcher.js?setup-only");
54
+ // The wizard is embedded in launcher.js; we import the module dynamically.
55
+ // Since launcher.js runs main() on import, we instead inline a lightweight version.
56
+
57
+ const readline = await import("readline");
58
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
60
+
61
+ const HOME = os.homedir();
62
+ const SINAIN_DIR = path.join(HOME, ".sinain");
63
+ const envPath = path.join(SINAIN_DIR, ".env");
64
+
65
+ const BOLD = "\x1b[1m";
66
+ const DIM = "\x1b[2m";
67
+ const GREEN = "\x1b[32m";
68
+ const YELLOW = "\x1b[33m";
69
+ const RESET = "\x1b[0m";
70
+ const IS_WIN = os.platform() === "win32";
71
+
72
+ const cmdExists = (cmd) => {
73
+ try { import("child_process").then(cp => cp.execSync(`which ${cmd}`, { stdio: "pipe" })); return true; }
74
+ catch { return false; }
75
+ };
76
+ // Synchronous version
77
+ const { execSync } = await import("child_process");
78
+ const cmdExistsSync = (cmd) => {
79
+ try { execSync(`which ${cmd}`, { stdio: "pipe" }); return true; }
80
+ catch { return false; }
81
+ };
82
+
83
+ if (fs.existsSync(envPath)) {
84
+ const overwrite = await ask(` ${envPath} already exists. Overwrite? [y/N]: `);
85
+ if (overwrite.trim().toLowerCase() !== "y") {
86
+ console.log(" Aborted.");
87
+ rl.close();
88
+ return;
89
+ }
90
+ }
91
+
92
+ console.log();
93
+ console.log(`${BOLD}── Sinain Setup Wizard ─────────────────${RESET}`);
94
+ console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
95
+ console.log();
96
+
97
+ const vars = {};
98
+
99
+ // Transcription backend
100
+ let transcriptionBackend = "openrouter";
101
+ const hasWhisper = !IS_WIN && cmdExistsSync("whisper-cli");
102
+
103
+ if (IS_WIN) {
104
+ console.log(` ${DIM}(Local whisper not available on Windows — using OpenRouter)${RESET}`);
105
+ } else if (hasWhisper) {
106
+ const choice = await ask(` Transcription backend? [${BOLD}local${RESET}/cloud]: `);
107
+ transcriptionBackend = choice.trim().toLowerCase() === "cloud" ? "openrouter" : "local";
108
+ } else {
109
+ const install = await ask(` whisper-cli not found. Install via Homebrew? [Y/n]: `);
110
+ if (!install.trim() || install.trim().toLowerCase() === "y") {
111
+ try {
112
+ execSync("brew install whisper-cpp", { stdio: "inherit" });
113
+ const modelDir = path.join(HOME, "models");
114
+ const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
115
+ if (!fs.existsSync(modelPath)) {
116
+ console.log(` ${DIM}Downloading model (~1.5 GB)...${RESET}`);
117
+ fs.mkdirSync(modelDir, { recursive: true });
118
+ execSync(`curl -L --progress-bar -o "${modelPath}" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin"`, { stdio: "inherit" });
119
+ }
120
+ transcriptionBackend = "local";
121
+ vars.LOCAL_WHISPER_MODEL = modelPath;
122
+ } catch {
123
+ console.log(` ${YELLOW}Install failed — falling back to OpenRouter${RESET}`);
124
+ }
125
+ }
126
+ }
127
+ vars.TRANSCRIPTION_BACKEND = transcriptionBackend;
128
+
129
+ // API key
130
+ if (transcriptionBackend === "openrouter") {
131
+ const key = await ask(` OpenRouter API key (sk-or-...): `);
132
+ if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
133
+ } else {
134
+ const key = await ask(` OpenRouter API key for vision/OCR (optional): `);
135
+ if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
136
+ }
137
+
138
+ // Agent
139
+ const agent = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
140
+ vars.SINAIN_AGENT = agent.trim().toLowerCase() || "claude";
141
+
142
+ // Escalation
143
+ console.log(`\n ${DIM}Escalation: off | selective | focus | rich${RESET}`);
144
+ const esc = await ask(` Escalation mode? [${BOLD}selective${RESET}]: `);
145
+ vars.ESCALATION_MODE = esc.trim().toLowerCase() || "selective";
146
+
147
+ // Gateway
148
+ const gw = await ask(` OpenClaw gateway? [y/N]: `);
149
+ if (gw.trim().toLowerCase() === "y") {
150
+ const url = await ask(` Gateway WS URL [ws://localhost:18789]: `);
151
+ vars.OPENCLAW_WS_URL = url.trim() || "ws://localhost:18789";
152
+ const token = await ask(` Auth token (48-char hex): `);
153
+ if (token.trim()) {
154
+ vars.OPENCLAW_WS_TOKEN = token.trim();
155
+ vars.OPENCLAW_HTTP_TOKEN = token.trim();
156
+ }
157
+ vars.OPENCLAW_HTTP_URL = vars.OPENCLAW_WS_URL.replace(/^ws/, "http") + "/hooks/agent";
158
+ vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
159
+ }
160
+
161
+ vars.SINAIN_POLL_INTERVAL = "5";
162
+ vars.SINAIN_HEARTBEAT_INTERVAL = "900";
163
+ vars.PRIVACY_MODE = "standard";
164
+
165
+ // Write
166
+ fs.mkdirSync(SINAIN_DIR, { recursive: true });
167
+ const lines = ["# sinain configuration — generated by setup wizard", `# ${new Date().toISOString()}`, ""];
168
+ for (const [k, v] of Object.entries(vars)) lines.push(`${k}=${v}`);
169
+ lines.push("");
170
+ fs.writeFileSync(envPath, lines.join("\n"));
171
+
172
+ rl.close();
173
+ console.log(`\n ${GREEN}✓${RESET} Config written to ${envPath}\n`);
174
+ }
175
+
44
176
  // ── Stop ──────────────────────────────────────────────────────────────────────
45
177
 
46
178
  async function stopServices() {
47
179
  let killed = false;
48
180
 
49
- const patterns = [
50
- "tsx.*src/index.ts",
51
- "tsx watch src/index.ts",
52
- "python3 -m sense_client",
53
- "Python -m sense_client",
54
- "flutter run -d macos",
55
- "sinain_hud.app/Contents/MacOS/sinain_hud",
56
- "sinain-agent/run.sh",
57
- ];
58
-
59
- for (const pat of patterns) {
60
- try {
61
- execSync(`pkill -f "${pat}"`, { stdio: "pipe" });
62
- killed = true;
63
- } catch { /* not running */ }
181
+ if (IS_WINDOWS) {
182
+ const exes = ["sinain_hud.exe", "tsx.cmd", "python3.exe", "python.exe"];
183
+ for (const exe of exes) {
184
+ try {
185
+ execSync(`taskkill /F /IM "${exe}" 2>NUL`, { stdio: "pipe" });
186
+ killed = true;
187
+ } catch { /* not running */ }
188
+ }
189
+ } else {
190
+ const patterns = [
191
+ "tsx.*src/index.ts",
192
+ "tsx watch src/index.ts",
193
+ "python3 -m sense_client",
194
+ "Python -m sense_client",
195
+ "flutter run -d macos",
196
+ "sinain_hud.app/Contents/MacOS/sinain_hud",
197
+ "sinain-agent/run.sh",
198
+ ];
199
+
200
+ for (const pat of patterns) {
201
+ try {
202
+ execSync(`pkill -f "${pat}"`, { stdio: "pipe" });
203
+ killed = true;
204
+ } catch { /* not running */ }
205
+ }
64
206
  }
65
207
 
66
208
  // Free port 9500
67
209
  try {
68
- const pid = execSync("lsof -i :9500 -sTCP:LISTEN -t", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
69
- if (pid) {
70
- execSync(`kill ${pid}`, { stdio: "pipe" });
71
- killed = true;
210
+ if (IS_WINDOWS) {
211
+ const out = execSync('netstat -ano | findstr ":9500" | findstr "LISTENING"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
212
+ const pid = out.split(/\s+/).pop();
213
+ if (pid && pid !== "0") {
214
+ execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
215
+ killed = true;
216
+ }
217
+ } else {
218
+ const pid = execSync("lsof -i :9500 -sTCP:LISTEN -t", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
219
+ if (pid) {
220
+ execSync(`kill ${pid}`, { stdio: "pipe" });
221
+ killed = true;
222
+ }
72
223
  }
73
224
  } catch { /* port already free */ }
74
225
 
75
226
  // Clean PID file
76
- const pidFile = "/tmp/sinain-pids.txt";
227
+ const pidFile = path.join(os.tmpdir(), "sinain-pids.txt");
77
228
  if (fs.existsSync(pidFile)) {
78
229
  fs.unlinkSync(pidFile);
79
230
  }
@@ -107,7 +258,7 @@ async function showStatus() {
107
258
  console.log(` ${CYAN}core${RESET} :9500 ${RED}✗${RESET} stopped`);
108
259
  }
109
260
 
110
- // Sense: check pgrep
261
+ // Sense: check process
111
262
  const senseUp = isProcessRunning("python3 -m sense_client") || isProcessRunning("Python -m sense_client");
112
263
  if (senseUp) {
113
264
  console.log(` ${YELLOW}sense${RESET} ${GREEN}✓${RESET} running`);
@@ -116,7 +267,7 @@ async function showStatus() {
116
267
  }
117
268
 
118
269
  // Overlay
119
- const overlayUp = isProcessRunning("sinain_hud.app") || isProcessRunning("flutter run -d macos");
270
+ const overlayUp = isProcessRunning("sinain_hud");
120
271
  if (overlayUp) {
121
272
  console.log(` ${MAGENTA}overlay${RESET} ${GREEN}✓${RESET} running`);
122
273
  } else {
@@ -124,7 +275,7 @@ async function showStatus() {
124
275
  }
125
276
 
126
277
  // Agent
127
- const agentUp = isProcessRunning("sinain-agent/run.sh");
278
+ const agentUp = isProcessRunning("sinain-agent");
128
279
  if (agentUp) {
129
280
  console.log(` ${GREEN}agent${RESET} ${GREEN}✓${RESET} running`);
130
281
  } else {
@@ -147,8 +298,13 @@ function isPortOpen(port) {
147
298
 
148
299
  function isProcessRunning(pattern) {
149
300
  try {
150
- execSync(`pgrep -f "${pattern}"`, { stdio: "pipe" });
151
- return true;
301
+ if (IS_WINDOWS) {
302
+ const out = execSync(`tasklist /FI "IMAGENAME eq ${pattern}.exe" 2>NUL`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
303
+ return out.includes(pattern);
304
+ } else {
305
+ execSync(`pgrep -f "${pattern}"`, { stdio: "pipe" });
306
+ return true;
307
+ }
152
308
  } catch {
153
309
  return false;
154
310
  }
@@ -158,19 +314,25 @@ function isProcessRunning(pattern) {
158
314
 
159
315
  function printUsage() {
160
316
  console.log(`
161
- sinain — AI overlay system for macOS
317
+ sinain — AI overlay system for macOS and Windows
162
318
 
163
319
  Usage:
164
320
  sinain start [options] Launch sinain services
165
321
  sinain stop Stop all sinain services
166
322
  sinain status Check what's running
167
- sinain setup-overlay Clone and build the overlay app
323
+ sinain setup Run interactive setup wizard (~/.sinain/.env)
324
+ sinain setup-overlay Download pre-built overlay app
168
325
  sinain install Install OpenClaw plugin (server-side)
169
326
 
170
327
  Start options:
171
328
  --no-sense Skip screen capture (sense_client)
172
- --no-overlay Skip Flutter overlay
329
+ --no-overlay Skip overlay
173
330
  --no-agent Skip agent poll loop
174
331
  --agent=<name> Agent to use: claude, codex, goose, aider (default: claude)
332
+
333
+ Setup-overlay options:
334
+ --from-source Build from Flutter source instead of downloading
335
+ --update Force re-download even if version matches
175
336
  `);
337
+
176
338
  }
package/launcher.js CHANGED
@@ -25,7 +25,8 @@ const RESET = "\x1b[0m";
25
25
  const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
26
26
  const HOME = os.homedir();
27
27
  const SINAIN_DIR = path.join(HOME, ".sinain");
28
- const PID_FILE = "/tmp/sinain-pids.txt";
28
+ const PID_FILE = path.join(os.tmpdir(), "sinain-pids.txt");
29
+ const IS_WINDOWS = os.platform() === "win32";
29
30
 
30
31
  // ── Parse flags ─────────────────────────────────────────────────────────────
31
32
 
@@ -59,6 +60,12 @@ async function main() {
59
60
  await preflight();
60
61
  console.log();
61
62
 
63
+ // Run setup wizard on first launch (no ~/.sinain/.env)
64
+ const userEnvPath = path.join(SINAIN_DIR, ".env");
65
+ if (!fs.existsSync(userEnvPath)) {
66
+ await setupWizard(userEnvPath);
67
+ }
68
+
62
69
  // Load user config
63
70
  loadUserEnv();
64
71
 
@@ -139,14 +146,23 @@ async function main() {
139
146
  // Start overlay
140
147
  let overlayStatus = "skipped";
141
148
  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
- });
149
+ const overlay = findOverlay();
150
+ if (overlay?.type === "prebuilt") {
151
+ // Remove macOS quarantine if present (ad-hoc signed app)
152
+ if (!IS_WINDOWS) {
153
+ try {
154
+ const xattrs = execSync(`xattr "${overlay.path}"`, { encoding: "utf-8" });
155
+ if (xattrs.includes("com.apple.quarantine")) {
156
+ execSync(`xattr -dr com.apple.quarantine "${overlay.path}"`, { stdio: "pipe" });
157
+ }
158
+ } catch { /* no quarantine or xattr failed — try launching anyway */ }
159
+ }
160
+
161
+ log("Starting overlay (pre-built)...");
162
+ const binary = IS_WINDOWS
163
+ ? overlay.path // sinain_hud.exe
164
+ : path.join(overlay.path, "Contents/MacOS/sinain_hud");
165
+ startProcess("overlay", binary, [], { color: MAGENTA });
150
166
  await sleep(2000);
151
167
  const overlayChild = children.find(c => c.name === "overlay");
152
168
  if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
@@ -156,10 +172,29 @@ async function main() {
156
172
  warn("overlay exited early — check logs above");
157
173
  overlayStatus = "failed";
158
174
  }
159
- } else if (!overlayDir) {
160
- warn("overlay not found — run: sinain setup-overlay");
175
+ } else if (overlay?.type === "source") {
176
+ const hasFlutter = commandExists("flutter");
177
+ if (hasFlutter) {
178
+ log("Starting overlay (flutter run)...");
179
+ const device = IS_WINDOWS ? "windows" : "macos";
180
+ startProcess("overlay", "flutter", ["run", "-d", device], {
181
+ cwd: overlay.path,
182
+ color: MAGENTA,
183
+ });
184
+ await sleep(2000);
185
+ const overlayChild = children.find(c => c.name === "overlay");
186
+ if (overlayChild && !overlayChild.proc.killed && overlayChild.proc.exitCode === null) {
187
+ ok(`overlay running (pid:${overlayChild.pid})`);
188
+ overlayStatus = "running";
189
+ } else {
190
+ warn("overlay exited early — check logs above");
191
+ overlayStatus = "failed";
192
+ }
193
+ } else {
194
+ warn("flutter not found — overlay source found but can't build");
195
+ }
161
196
  } else {
162
- warn("flutter not found — overlay skipped");
197
+ warn("overlay not found — run: sinain setup-overlay");
163
198
  }
164
199
  }
165
200
 
@@ -227,7 +262,7 @@ async function preflight() {
227
262
  skipSense = true;
228
263
  }
229
264
 
230
- // Flutter (optional)
265
+ // Flutter (optional — only needed if no pre-built overlay)
231
266
  if (commandExists("flutter")) {
232
267
  try {
233
268
  const flutterVer = execSync("flutter --version 2>&1", { encoding: "utf-8" }).split("\n")[0].split(" ")[1];
@@ -236,8 +271,14 @@ async function preflight() {
236
271
  ok("flutter (version unknown)");
237
272
  }
238
273
  } else {
239
- warn("flutter not found overlay will be skipped");
240
- skipOverlay = true;
274
+ const prebuiltName = IS_WINDOWS ? "sinain_hud.exe" : "sinain_hud.app";
275
+ const prebuiltApp = path.join(SINAIN_DIR, "overlay-app", prebuiltName);
276
+ if (fs.existsSync(prebuiltApp)) {
277
+ ok("overlay: pre-built app");
278
+ } else {
279
+ warn("no overlay available — run: sinain setup-overlay");
280
+ skipOverlay = true;
281
+ }
241
282
  }
242
283
 
243
284
  // Port 9500
@@ -250,12 +291,148 @@ async function preflight() {
250
291
  }
251
292
  }
252
293
 
294
+ // ── Setup wizard ─────────────────────────────────────────────────────────────
295
+
296
+ async function setupWizard(envPath) {
297
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
298
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
299
+
300
+ console.log();
301
+ console.log(`${BOLD}── First-time setup ────────────────────${RESET}`);
302
+ console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
303
+ console.log();
304
+
305
+ const vars = {};
306
+
307
+ // 1. Transcription backend — auto-detect whisper-cli
308
+ let transcriptionBackend = "openrouter";
309
+ const hasWhisper = !IS_WINDOWS && commandExists("whisper-cli");
310
+
311
+ if (IS_WINDOWS) {
312
+ console.log(` ${DIM}(Local whisper not available on Windows — using OpenRouter)${RESET}`);
313
+ } else if (hasWhisper) {
314
+ const choice = await ask(` Transcription backend? [${BOLD}local${RESET}/cloud] (local = whisper-cli, no API key): `);
315
+ if (choice.trim().toLowerCase() === "cloud") {
316
+ transcriptionBackend = "openrouter";
317
+ } else {
318
+ transcriptionBackend = "local";
319
+ }
320
+ } else {
321
+ const installWhisper = await ask(` whisper-cli not found. Install via Homebrew? [Y/n]: `);
322
+ if (!installWhisper.trim() || installWhisper.trim().toLowerCase() === "y") {
323
+ try {
324
+ console.log(` ${DIM}Installing whisper-cpp...${RESET}`);
325
+ execSync("brew install whisper-cpp", { stdio: "inherit" });
326
+
327
+ // Download model
328
+ const modelDir = path.join(HOME, "models");
329
+ const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
330
+ if (!fs.existsSync(modelPath)) {
331
+ console.log(` ${DIM}Downloading ggml-large-v3-turbo (~1.5 GB)...${RESET}`);
332
+ fs.mkdirSync(modelDir, { recursive: true });
333
+ execSync(
334
+ `curl -L --progress-bar -o "${modelPath}" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin"`,
335
+ { stdio: "inherit" }
336
+ );
337
+ }
338
+
339
+ transcriptionBackend = "local";
340
+ vars.LOCAL_WHISPER_MODEL = modelPath;
341
+ ok("whisper-cpp installed");
342
+ } catch {
343
+ warn("whisper-cpp install failed — falling back to OpenRouter");
344
+ transcriptionBackend = "openrouter";
345
+ }
346
+ } else {
347
+ transcriptionBackend = "openrouter";
348
+ }
349
+ }
350
+ vars.TRANSCRIPTION_BACKEND = transcriptionBackend;
351
+
352
+ // 2. OpenRouter API key (if cloud backend or for vision/OCR)
353
+ if (transcriptionBackend === "openrouter") {
354
+ let key = "";
355
+ while (!key) {
356
+ key = await ask(` OpenRouter API key (sk-or-...): `);
357
+ key = key.trim();
358
+ if (key && !key.startsWith("sk-or-")) {
359
+ console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
360
+ const retry = await ask(` Use this key anyway? [y/N]: `);
361
+ if (retry.trim().toLowerCase() !== "y") { key = ""; continue; }
362
+ }
363
+ if (!key) {
364
+ console.log(` ${DIM}You can set OPENROUTER_API_KEY later in ~/.sinain/.env${RESET}`);
365
+ break;
366
+ }
367
+ }
368
+ if (key) vars.OPENROUTER_API_KEY = key;
369
+ } else {
370
+ // Still ask for OpenRouter key (needed for vision/OCR)
371
+ const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip): `);
372
+ if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
373
+ }
374
+
375
+ // 3. Agent selection
376
+ const agentChoice = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
377
+ vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || "claude";
378
+
379
+ // 4. Escalation mode
380
+ console.log();
381
+ console.log(` ${DIM}Escalation modes:${RESET}`);
382
+ console.log(` off — no escalation to gateway`);
383
+ console.log(` selective — score-based (errors, questions trigger it)`);
384
+ console.log(` focus — always escalate every tick`);
385
+ console.log(` rich — always escalate with maximum context`);
386
+ const escMode = await ask(` Escalation mode? [off/${BOLD}selective${RESET}/focus/rich]: `);
387
+ vars.ESCALATION_MODE = escMode.trim().toLowerCase() || "selective";
388
+
389
+ // 5. OpenClaw gateway
390
+ const hasGateway = await ask(` Do you have an OpenClaw gateway? [y/N]: `);
391
+ if (hasGateway.trim().toLowerCase() === "y") {
392
+ const wsUrl = await ask(` Gateway WebSocket URL [ws://localhost:18789]: `);
393
+ vars.OPENCLAW_WS_URL = wsUrl.trim() || "ws://localhost:18789";
394
+
395
+ const wsToken = await ask(` Gateway auth token (48-char hex): `);
396
+ if (wsToken.trim()) {
397
+ vars.OPENCLAW_WS_TOKEN = wsToken.trim();
398
+ vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
399
+ }
400
+
401
+ // Derive HTTP URL from WS URL
402
+ const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
403
+ vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
404
+ vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
405
+ }
406
+
407
+ // 6. Agent-specific defaults
408
+ vars.SINAIN_POLL_INTERVAL = "5";
409
+ vars.SINAIN_HEARTBEAT_INTERVAL = "900";
410
+ vars.PRIVACY_MODE = "standard";
411
+
412
+ // Write .env
413
+ fs.mkdirSync(path.dirname(envPath), { recursive: true });
414
+ const lines = [];
415
+ lines.push("# sinain configuration — generated by setup wizard");
416
+ lines.push(`# ${new Date().toISOString()}`);
417
+ lines.push("");
418
+ for (const [key, val] of Object.entries(vars)) {
419
+ lines.push(`${key}=${val}`);
420
+ }
421
+ lines.push("");
422
+ fs.writeFileSync(envPath, lines.join("\n"));
423
+
424
+ rl.close();
425
+
426
+ console.log();
427
+ ok(`Config written to ${envPath}`);
428
+ console.log();
429
+ }
430
+
253
431
  // ── User environment ────────────────────────────────────────────────────────
254
432
 
255
433
  function loadUserEnv() {
256
434
  const envPaths = [
257
435
  path.join(SINAIN_DIR, ".env"),
258
- path.join(PKG_DIR, "sinain-core/.env"),
259
436
  ];
260
437
 
261
438
  for (const envPath of envPaths) {
@@ -345,31 +522,51 @@ async function installDeps() {
345
522
 
346
523
  function killStale() {
347
524
  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
525
 
358
- for (const pat of patterns) {
526
+ if (IS_WINDOWS) {
527
+ const exes = ["sinain_hud.exe", "tsx.cmd"];
528
+ for (const exe of exes) {
529
+ try {
530
+ execSync(`taskkill /F /IM "${exe}" 2>NUL`, { stdio: "pipe" });
531
+ killed = true;
532
+ } catch { /* not running */ }
533
+ }
534
+ // Free port 9500
359
535
  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;
536
+ const out = execSync('netstat -ano | findstr ":9500" | findstr "LISTENING"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
537
+ const pid = out.split(/\s+/).pop();
538
+ if (pid && pid !== "0") {
539
+ execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
540
+ killed = true;
541
+ }
542
+ } catch { /* already free */ }
543
+ } else {
544
+ const patterns = [
545
+ "sinain_hud.app/Contents/MacOS/sinain_hud",
546
+ "flutter run -d macos",
547
+ "python3 -m sense_client",
548
+ "Python -m sense_client",
549
+ "tsx.*src/index.ts",
550
+ "tsx watch src/index.ts",
551
+ "sinain-agent/run.sh",
552
+ ];
553
+
554
+ for (const pat of patterns) {
555
+ try {
556
+ execSync(`pkill -f "${pat}"`, { stdio: "pipe" });
557
+ killed = true;
558
+ } catch { /* not running */ }
371
559
  }
372
- } catch { /* already free */ }
560
+
561
+ // Free port 9500
562
+ try {
563
+ const pid = execSync("lsof -i :9500 -sTCP:LISTEN -t", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
564
+ if (pid) {
565
+ execSync(`kill ${pid}`, { stdio: "pipe" });
566
+ killed = true;
567
+ }
568
+ } catch { /* already free */ }
569
+ }
373
570
 
374
571
  // Clean old PID file
375
572
  if (fs.existsSync(PID_FILE)) {
@@ -461,17 +658,24 @@ function generateMcpConfig() {
461
658
 
462
659
  // ── Overlay discovery ───────────────────────────────────────────────────────
463
660
 
464
- function findOverlayDir() {
465
- // 1. Sibling overlay/ (running from cloned repo)
661
+ function findOverlay() {
662
+ // 1. Dev monorepo: sibling overlay/ with pubspec.yaml (Flutter source)
466
663
  const siblingOverlay = path.join(PKG_DIR, "..", "overlay");
467
664
  if (fs.existsSync(path.join(siblingOverlay, "pubspec.yaml"))) {
468
- return siblingOverlay;
665
+ return { type: "source", path: siblingOverlay };
666
+ }
667
+
668
+ // 2. Pre-built app (downloaded by setup-overlay)
669
+ const prebuiltName = IS_WINDOWS ? "sinain_hud.exe" : "sinain_hud.app";
670
+ const prebuiltApp = path.join(SINAIN_DIR, "overlay-app", prebuiltName);
671
+ if (fs.existsSync(prebuiltApp)) {
672
+ return { type: "prebuilt", path: prebuiltApp };
469
673
  }
470
674
 
471
- // 2. ~/.sinain/overlay/ (installed via setup-overlay)
675
+ // 3. Legacy: ~/.sinain/overlay/ source install (setup-overlay --from-source)
472
676
  const installedOverlay = path.join(SINAIN_DIR, "overlay");
473
677
  if (fs.existsSync(path.join(installedOverlay, "pubspec.yaml"))) {
474
- return installedOverlay;
678
+ return { type: "source", path: installedOverlay };
475
679
  }
476
680
 
477
681
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Ambient AI overlay invisible to screen capture — real-time insights from audio + screen context",
5
5
  "type": "module",
6
6
  "bin": {
package/setup-overlay.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sinain setup-overlay — clone and build the Flutter overlay app
2
+ // sinain setup-overlay — download pre-built overlay app (or build from source)
3
3
 
4
4
  import { execSync } from "child_process";
5
5
  import fs from "fs";
@@ -8,75 +8,245 @@ import os from "os";
8
8
 
9
9
  const HOME = os.homedir();
10
10
  const SINAIN_DIR = path.join(HOME, ".sinain");
11
+ const APP_DIR = path.join(SINAIN_DIR, "overlay-app");
12
+ const VERSION_FILE = path.join(APP_DIR, "version.json");
13
+ const IS_WINDOWS = os.platform() === "win32";
14
+
15
+ // Platform-specific asset and app path
16
+ const ASSET_NAME = IS_WINDOWS ? "sinain_hud_windows.zip" : "sinain_hud.app.zip";
17
+ const APP_PATH = IS_WINDOWS
18
+ ? path.join(APP_DIR, "sinain_hud.exe")
19
+ : path.join(APP_DIR, "sinain_hud.app");
20
+
21
+ const REPO = "anthillnet/sinain-hud";
22
+ const RELEASES_API = `https://api.github.com/repos/${REPO}/releases`;
23
+
24
+ // Legacy source-build paths
11
25
  const REPO_DIR = path.join(SINAIN_DIR, "overlay-repo");
12
26
  const OVERLAY_LINK = path.join(SINAIN_DIR, "overlay");
13
27
 
14
28
  const BOLD = "\x1b[1m";
15
29
  const GREEN = "\x1b[32m";
30
+ const YELLOW = "\x1b[33m";
16
31
  const RED = "\x1b[31m";
32
+ const DIM = "\x1b[2m";
17
33
  const RESET = "\x1b[0m";
18
34
 
19
- function log(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${msg}`); }
20
- function ok(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${GREEN}✓${RESET} ${msg}`); }
35
+ function log(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${msg}`); }
36
+ function ok(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${GREEN}✓${RESET} ${msg}`); }
37
+ function warn(msg) { console.log(`${BOLD}[setup-overlay]${RESET} ${YELLOW}⚠${RESET} ${msg}`); }
21
38
  function fail(msg) { console.error(`${BOLD}[setup-overlay]${RESET} ${RED}✗${RESET} ${msg}`); process.exit(1); }
22
39
 
23
- // Check flutter
24
- try {
25
- execSync("which flutter", { stdio: "pipe" });
26
- } catch {
27
- fail("flutter not found. Install it: https://docs.flutter.dev/get-started/install");
40
+ // ── Parse flags ──────────────────────────────────────────────────────────────
41
+
42
+ const args = process.argv.slice(2);
43
+ const fromSource = args.includes("--from-source");
44
+ const forceUpdate = args.includes("--update");
45
+
46
+ if (fromSource) {
47
+ await buildFromSource();
48
+ } else {
49
+ await downloadPrebuilt();
28
50
  }
29
51
 
30
- const flutterVer = execSync("flutter --version 2>&1", { encoding: "utf-8" }).split("\n")[0];
31
- ok(`flutter: ${flutterVer}`);
52
+ // ── Download pre-built .app ──────────────────────────────────────────────────
32
53
 
33
- fs.mkdirSync(SINAIN_DIR, { recursive: true });
54
+ async function downloadPrebuilt() {
55
+ fs.mkdirSync(APP_DIR, { recursive: true });
34
56
 
35
- // Clone or update
36
- if (fs.existsSync(path.join(REPO_DIR, ".git"))) {
37
- log("Updating existing overlay repo...");
38
- execSync("git pull --ff-only", { cwd: REPO_DIR, stdio: "inherit" });
39
- ok("Repository updated");
40
- } else {
41
- log("Cloning overlay (sparse checkout — only overlay/ directory)...");
42
- if (fs.existsSync(REPO_DIR)) {
43
- fs.rmSync(REPO_DIR, { recursive: true, force: true });
57
+ // Find latest overlay release
58
+ log("Checking for latest overlay release...");
59
+ let release;
60
+ try {
61
+ const res = await fetch(`${RELEASES_API}?per_page=20`, {
62
+ signal: AbortSignal.timeout(10000),
63
+ headers: { "Accept": "application/vnd.github+json" },
64
+ });
65
+ if (!res.ok) throw new Error(`GitHub API returned ${res.status}`);
66
+ const releases = await res.json();
67
+ release = releases.find(r => r.tag_name?.startsWith("overlay-v"));
68
+ if (!release) throw new Error("No overlay release found");
69
+ } catch (e) {
70
+ fail(`Failed to fetch releases: ${e.message}\n Try: sinain setup-overlay --from-source`);
44
71
  }
45
- execSync(
46
- `git clone --depth 1 --filter=blob:none --sparse https://github.com/anthillnet/sinain-hud.git "${REPO_DIR}"`,
47
- { stdio: "inherit" }
48
- );
49
- execSync("git sparse-checkout set overlay", { cwd: REPO_DIR, stdio: "inherit" });
50
- ok("Repository cloned");
51
- }
52
72
 
53
- // Build
54
- const overlayDir = path.join(REPO_DIR, "overlay");
55
- if (!fs.existsSync(path.join(overlayDir, "pubspec.yaml"))) {
56
- fail("overlay/pubspec.yaml not found — sparse checkout may have failed");
57
- }
73
+ const tag = release.tag_name;
74
+ const version = tag.replace("overlay-v", "");
58
75
 
59
- log("Installing Flutter dependencies...");
60
- execSync("flutter pub get", { cwd: overlayDir, stdio: "inherit" });
76
+ // Check if already up-to-date
77
+ if (!forceUpdate && fs.existsSync(VERSION_FILE) && fs.existsSync(APP_PATH)) {
78
+ try {
79
+ const local = JSON.parse(fs.readFileSync(VERSION_FILE, "utf-8"));
80
+ if (local.tag === tag) {
81
+ ok(`Overlay already up-to-date (${version})`);
82
+ return;
83
+ }
84
+ log(`Updating: ${local.tag} → ${tag}`);
85
+ } catch { /* corrupt version file — re-download */ }
86
+ }
61
87
 
62
- log("Building overlay (this may take a few minutes)...");
63
- execSync("flutter build macos", { cwd: overlayDir, stdio: "inherit" });
64
- ok("Overlay built successfully");
88
+ // Find the .zip asset for this platform
89
+ const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
90
+ if (!zipAsset) {
91
+ fail(`Release ${tag} has no ${ASSET_NAME} asset.\n Try: sinain setup-overlay --from-source`);
92
+ }
65
93
 
66
- // Symlink ~/.sinain/overlay → the overlay source dir
67
- try {
68
- if (fs.existsSync(OVERLAY_LINK)) {
69
- fs.unlinkSync(OVERLAY_LINK);
94
+ // Download with progress
95
+ log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
96
+ const zipPath = path.join(APP_DIR, ASSET_NAME);
97
+
98
+ try {
99
+ const res = await fetch(zipAsset.browser_download_url, {
100
+ signal: AbortSignal.timeout(120000),
101
+ redirect: "follow",
102
+ });
103
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
104
+
105
+ const total = parseInt(res.headers.get("content-length") || "0");
106
+ const chunks = [];
107
+ let downloaded = 0;
108
+
109
+ const reader = res.body.getReader();
110
+ while (true) {
111
+ const { done, value } = await reader.read();
112
+ if (done) break;
113
+ chunks.push(value);
114
+ downloaded += value.length;
115
+ if (total > 0) {
116
+ const pct = Math.round((downloaded / total) * 100);
117
+ process.stdout.write(`\r${BOLD}[setup-overlay]${RESET} ${DIM}${pct}% (${formatBytes(downloaded)} / ${formatBytes(total)})${RESET}`);
118
+ }
119
+ }
120
+ process.stdout.write("\n");
121
+
122
+ const buffer = Buffer.concat(chunks);
123
+ fs.writeFileSync(zipPath, buffer);
124
+ ok(`Downloaded ${formatBytes(buffer.length)}`);
125
+ } catch (e) {
126
+ fail(`Download failed: ${e.message}`);
127
+ }
128
+
129
+ // Remove old app if present
130
+ if (fs.existsSync(APP_PATH)) {
131
+ fs.rmSync(APP_PATH, { recursive: true, force: true });
70
132
  }
71
- fs.symlinkSync(overlayDir, OVERLAY_LINK);
72
- ok(`Symlinked: ${OVERLAY_LINK} → ${overlayDir}`);
73
- } catch (e) {
74
- // Symlink may fail on some systems — fall back to just noting the path
75
- log(`Overlay built at: ${overlayDir}`);
133
+
134
+ // Extract
135
+ log("Extracting...");
136
+ if (IS_WINDOWS) {
137
+ try {
138
+ execSync(
139
+ `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${APP_DIR}' -Force"`,
140
+ { stdio: "pipe" }
141
+ );
142
+ } catch (e) {
143
+ fail(`Extraction failed: ${e.message}`);
144
+ }
145
+ } else {
146
+ // ditto preserves macOS extended attributes (critical for code signing)
147
+ try {
148
+ execSync(`ditto -x -k "${zipPath}" "${APP_DIR}"`, { stdio: "pipe" });
149
+ } catch {
150
+ try {
151
+ execSync(`unzip -o -q "${zipPath}" -d "${APP_DIR}"`, { stdio: "pipe" });
152
+ } catch (e) {
153
+ fail(`Extraction failed: ${e.message}`);
154
+ }
155
+ }
156
+
157
+ // Remove quarantine attribute (ad-hoc signed app downloaded from internet)
158
+ try {
159
+ execSync(`xattr -cr "${APP_PATH}"`, { stdio: "pipe" });
160
+ } catch { /* xattr may not be needed */ }
161
+ }
162
+
163
+ // Write version marker
164
+ fs.writeFileSync(VERSION_FILE, JSON.stringify({
165
+ tag,
166
+ version,
167
+ installedAt: new Date().toISOString(),
168
+ }, null, 2));
169
+
170
+ // Clean up zip
171
+ fs.unlinkSync(zipPath);
172
+
173
+ ok(`Overlay ${version} installed`);
174
+ console.log(`
175
+ ${GREEN}✓${RESET} Overlay ready!
176
+ Location: ${APP_PATH}
177
+ The overlay will auto-start with: ${BOLD}sinain start${RESET}
178
+ `);
76
179
  }
77
180
 
78
- console.log(`
181
+ // ── Build from source (legacy) ───────────────────────────────────────────────
182
+
183
+ async function buildFromSource() {
184
+ // Check flutter
185
+ try {
186
+ execSync("which flutter", { stdio: "pipe" });
187
+ } catch {
188
+ fail("flutter not found. Install it: https://docs.flutter.dev/get-started/install");
189
+ }
190
+
191
+ const flutterVer = execSync("flutter --version 2>&1", { encoding: "utf-8" }).split("\n")[0];
192
+ ok(`flutter: ${flutterVer}`);
193
+
194
+ fs.mkdirSync(SINAIN_DIR, { recursive: true });
195
+
196
+ // Clone or update
197
+ if (fs.existsSync(path.join(REPO_DIR, ".git"))) {
198
+ log("Updating existing overlay repo...");
199
+ execSync("git pull --ff-only", { cwd: REPO_DIR, stdio: "inherit" });
200
+ ok("Repository updated");
201
+ } else {
202
+ log("Cloning overlay (sparse checkout — only overlay/ directory)...");
203
+ if (fs.existsSync(REPO_DIR)) {
204
+ fs.rmSync(REPO_DIR, { recursive: true, force: true });
205
+ }
206
+ execSync(
207
+ `git clone --depth 1 --filter=blob:none --sparse https://github.com/${REPO}.git "${REPO_DIR}"`,
208
+ { stdio: "inherit" }
209
+ );
210
+ execSync("git sparse-checkout set overlay", { cwd: REPO_DIR, stdio: "inherit" });
211
+ ok("Repository cloned");
212
+ }
213
+
214
+ // Build
215
+ const overlayDir = path.join(REPO_DIR, "overlay");
216
+ if (!fs.existsSync(path.join(overlayDir, "pubspec.yaml"))) {
217
+ fail("overlay/pubspec.yaml not found — sparse checkout may have failed");
218
+ }
219
+
220
+ log("Installing Flutter dependencies...");
221
+ execSync("flutter pub get", { cwd: overlayDir, stdio: "inherit" });
222
+
223
+ const buildTarget = IS_WINDOWS ? "windows" : "macos";
224
+ log(`Building overlay for ${buildTarget} (this may take a few minutes)...`);
225
+ execSync(`flutter build ${buildTarget}`, { cwd: overlayDir, stdio: "inherit" });
226
+ ok("Overlay built successfully");
227
+
228
+ // Symlink ~/.sinain/overlay → the overlay source dir
229
+ try {
230
+ if (fs.existsSync(OVERLAY_LINK)) {
231
+ fs.unlinkSync(OVERLAY_LINK);
232
+ }
233
+ fs.symlinkSync(overlayDir, OVERLAY_LINK);
234
+ ok(`Symlinked: ${OVERLAY_LINK} → ${overlayDir}`);
235
+ } catch (e) {
236
+ log(`Overlay built at: ${overlayDir}`);
237
+ }
238
+
239
+ console.log(`
79
240
  ${GREEN}✓${RESET} Overlay setup complete!
80
241
  The overlay will auto-start with: ${BOLD}sinain start${RESET}
81
- Or run manually: cd ${overlayDir} && flutter run -d macos
242
+ Or run manually: cd ${overlayDir} && flutter run -d ${IS_WINDOWS ? "windows" : "macos"}
82
243
  `);
244
+ }
245
+
246
+ // ── Helpers ──────────────────────────────────────────────────────────────────
247
+
248
+ function formatBytes(bytes) {
249
+ if (bytes < 1024) return `${bytes} B`;
250
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
251
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
252
+ }
@@ -82,6 +82,6 @@ Your working memory lives at `~/.openclaw/workspace/memory/`:
82
82
 
83
83
  ## Privacy
84
84
 
85
- The HUD overlay is invisible to screen capture. All content you receive has already been privacy-stripped by sinain-core. Your responses appear only on the ghost overlay — they are never captured in screenshots or recordings.
85
+ The HUD overlay is invisible to screen capture. All content you receive has already been privacy-stripped by sinain-core. Your responses appear only on the invisible overlay — they are never captured in screenshots or recordings.
86
86
 
87
87
  Never include `<private>` tagged content in your responses — it will be stripped automatically, but avoid echoing it.
@@ -3,12 +3,19 @@ set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
 
6
- # Load .env if present (does not override existing env vars)
6
+ # Load .env as fallback does NOT override vars already in the environment
7
+ # (e.g. vars set by the launcher from ~/.sinain/.env)
7
8
  if [ -f "$SCRIPT_DIR/.env" ]; then
8
- set -a
9
- # shellcheck source=/dev/null
10
- . "$SCRIPT_DIR/.env"
11
- set +a
9
+ while IFS='=' read -r key val; do
10
+ # Skip comments and blank lines
11
+ [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
12
+ key=$(echo "$key" | xargs) # trim whitespace
13
+ val=$(echo "$val" | xargs)
14
+ # Only set if not already in environment
15
+ if [ -z "${!key+x}" ]; then
16
+ export "$key=$val"
17
+ fi
18
+ done < "$SCRIPT_DIR/.env"
12
19
  fi
13
20
 
14
21
  MCP_CONFIG="${MCP_CONFIG:-$SCRIPT_DIR/mcp-config.json}"