@geravant/sinain 1.3.0 → 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,12 +314,13 @@ 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
323
+ sinain setup Run interactive setup wizard (~/.sinain/.env)
167
324
  sinain setup-overlay Download pre-built overlay app
168
325
  sinain install Install OpenClaw plugin (server-side)
169
326
 
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
 
@@ -141,16 +148,20 @@ async function main() {
141
148
  if (!skipOverlay) {
142
149
  const overlay = findOverlay();
143
150
  if (overlay?.type === "prebuilt") {
144
- // Remove quarantine if present (ad-hoc signed app)
145
- try {
146
- const xattrs = execSync(`xattr "${overlay.path}"`, { encoding: "utf-8" });
147
- if (xattrs.includes("com.apple.quarantine")) {
148
- execSync(`xattr -dr com.apple.quarantine "${overlay.path}"`, { stdio: "pipe" });
149
- }
150
- } catch { /* no quarantine or xattr failed — try launching anyway */ }
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
+ }
151
160
 
152
161
  log("Starting overlay (pre-built)...");
153
- const binary = path.join(overlay.path, "Contents/MacOS/sinain_hud");
162
+ const binary = IS_WINDOWS
163
+ ? overlay.path // sinain_hud.exe
164
+ : path.join(overlay.path, "Contents/MacOS/sinain_hud");
154
165
  startProcess("overlay", binary, [], { color: MAGENTA });
155
166
  await sleep(2000);
156
167
  const overlayChild = children.find(c => c.name === "overlay");
@@ -165,7 +176,8 @@ async function main() {
165
176
  const hasFlutter = commandExists("flutter");
166
177
  if (hasFlutter) {
167
178
  log("Starting overlay (flutter run)...");
168
- startProcess("overlay", "flutter", ["run", "-d", "macos"], {
179
+ const device = IS_WINDOWS ? "windows" : "macos";
180
+ startProcess("overlay", "flutter", ["run", "-d", device], {
169
181
  cwd: overlay.path,
170
182
  color: MAGENTA,
171
183
  });
@@ -259,7 +271,8 @@ async function preflight() {
259
271
  ok("flutter (version unknown)");
260
272
  }
261
273
  } else {
262
- const prebuiltApp = path.join(SINAIN_DIR, "overlay-app", "sinain_hud.app");
274
+ const prebuiltName = IS_WINDOWS ? "sinain_hud.exe" : "sinain_hud.app";
275
+ const prebuiltApp = path.join(SINAIN_DIR, "overlay-app", prebuiltName);
263
276
  if (fs.existsSync(prebuiltApp)) {
264
277
  ok("overlay: pre-built app");
265
278
  } else {
@@ -278,12 +291,148 @@ async function preflight() {
278
291
  }
279
292
  }
280
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
+
281
431
  // ── User environment ────────────────────────────────────────────────────────
282
432
 
283
433
  function loadUserEnv() {
284
434
  const envPaths = [
285
435
  path.join(SINAIN_DIR, ".env"),
286
- path.join(PKG_DIR, "sinain-core/.env"),
287
436
  ];
288
437
 
289
438
  for (const envPath of envPaths) {
@@ -373,31 +522,51 @@ async function installDeps() {
373
522
 
374
523
  function killStale() {
375
524
  let killed = false;
376
- const patterns = [
377
- "sinain_hud.app/Contents/MacOS/sinain_hud",
378
- "flutter run -d macos",
379
- "python3 -m sense_client",
380
- "Python -m sense_client",
381
- "tsx.*src/index.ts",
382
- "tsx watch src/index.ts",
383
- "sinain-agent/run.sh",
384
- ];
385
525
 
386
- 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
387
535
  try {
388
- execSync(`pkill -f "${pat}"`, { stdio: "pipe" });
389
- killed = true;
390
- } catch { /* not running */ }
391
- }
392
-
393
- // Free port 9500
394
- try {
395
- const pid = execSync("lsof -i :9500 -sTCP:LISTEN -t", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
396
- if (pid) {
397
- execSync(`kill ${pid}`, { stdio: "pipe" });
398
- 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 */ }
399
559
  }
400
- } 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
+ }
401
570
 
402
571
  // Clean old PID file
403
572
  if (fs.existsSync(PID_FILE)) {
@@ -496,8 +665,9 @@ function findOverlay() {
496
665
  return { type: "source", path: siblingOverlay };
497
666
  }
498
667
 
499
- // 2. Pre-built .app bundle (downloaded by setup-overlay)
500
- const prebuiltApp = path.join(SINAIN_DIR, "overlay-app", "sinain_hud.app");
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);
501
671
  if (fs.existsSync(prebuiltApp)) {
502
672
  return { type: "prebuilt", path: prebuiltApp };
503
673
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.3.0",
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
@@ -9,8 +9,14 @@ import os from "os";
9
9
  const HOME = os.homedir();
10
10
  const SINAIN_DIR = path.join(HOME, ".sinain");
11
11
  const APP_DIR = path.join(SINAIN_DIR, "overlay-app");
12
- const APP_PATH = path.join(APP_DIR, "sinain_hud.app");
13
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");
14
20
 
15
21
  const REPO = "anthillnet/sinain-hud";
16
22
  const RELEASES_API = `https://api.github.com/repos/${REPO}/releases`;
@@ -79,15 +85,15 @@ async function downloadPrebuilt() {
79
85
  } catch { /* corrupt version file — re-download */ }
80
86
  }
81
87
 
82
- // Find the .zip asset
83
- const zipAsset = release.assets?.find(a => a.name === "sinain_hud.app.zip");
88
+ // Find the .zip asset for this platform
89
+ const zipAsset = release.assets?.find(a => a.name === ASSET_NAME);
84
90
  if (!zipAsset) {
85
- fail(`Release ${tag} has no sinain_hud.app.zip asset.\n Try: sinain setup-overlay --from-source`);
91
+ fail(`Release ${tag} has no ${ASSET_NAME} asset.\n Try: sinain setup-overlay --from-source`);
86
92
  }
87
93
 
88
94
  // Download with progress
89
- log(`Downloading overlay ${version} (${formatBytes(zipAsset.size)})...`);
90
- const zipPath = path.join(APP_DIR, "sinain_hud.app.zip");
95
+ log(`Downloading overlay ${version} for ${IS_WINDOWS ? "Windows" : "macOS"} (${formatBytes(zipAsset.size)})...`);
96
+ const zipPath = path.join(APP_DIR, ASSET_NAME);
91
97
 
92
98
  try {
93
99
  const res = await fetch(zipAsset.browser_download_url, {
@@ -125,23 +131,34 @@ async function downloadPrebuilt() {
125
131
  fs.rmSync(APP_PATH, { recursive: true, force: true });
126
132
  }
127
133
 
128
- // Extract — ditto preserves macOS extended attributes (critical for code signing)
134
+ // Extract
129
135
  log("Extracting...");
130
- try {
131
- execSync(`ditto -x -k "${zipPath}" "${APP_DIR}"`, { stdio: "pipe" });
132
- } catch {
133
- // Fallback to unzip
136
+ if (IS_WINDOWS) {
134
137
  try {
135
- execSync(`unzip -o -q "${zipPath}" -d "${APP_DIR}"`, { stdio: "pipe" });
138
+ execSync(
139
+ `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${APP_DIR}' -Force"`,
140
+ { stdio: "pipe" }
141
+ );
136
142
  } catch (e) {
137
143
  fail(`Extraction failed: ${e.message}`);
138
144
  }
139
- }
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
+ }
140
156
 
141
- // Remove quarantine attribute (ad-hoc signed app downloaded from internet)
142
- try {
143
- execSync(`xattr -cr "${APP_PATH}"`, { stdio: "pipe" });
144
- } catch { /* xattr may not be needed */ }
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
+ }
145
162
 
146
163
  // Write version marker
147
164
  fs.writeFileSync(VERSION_FILE, JSON.stringify({
@@ -203,8 +220,9 @@ async function buildFromSource() {
203
220
  log("Installing Flutter dependencies...");
204
221
  execSync("flutter pub get", { cwd: overlayDir, stdio: "inherit" });
205
222
 
206
- log("Building overlay (this may take a few minutes)...");
207
- execSync("flutter build macos", { cwd: overlayDir, stdio: "inherit" });
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" });
208
226
  ok("Overlay built successfully");
209
227
 
210
228
  // Symlink ~/.sinain/overlay → the overlay source dir
@@ -221,7 +239,7 @@ async function buildFromSource() {
221
239
  console.log(`
222
240
  ${GREEN}✓${RESET} Overlay setup complete!
223
241
  The overlay will auto-start with: ${BOLD}sinain start${RESET}
224
- Or run manually: cd ${overlayDir} && flutter run -d macos
242
+ Or run manually: cd ${overlayDir} && flutter run -d ${IS_WINDOWS ? "windows" : "macos"}
225
243
  `);
226
244
  }
227
245
 
@@ -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}"