@geravant/sinain 1.15.4 → 1.15.6

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.
Files changed (4) hide show
  1. package/cli.js +0 -171
  2. package/launcher.js +12 -299
  3. package/onboard.js +23 -0
  4. package/package.json +1 -1
package/cli.js CHANGED
@@ -4,7 +4,6 @@ import net from "net";
4
4
  import os from "os";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
- import { writeAgentsConfig } from "./config-shared.js";
8
7
  import { checkForUpdate } from "./self-update.js";
9
8
 
10
9
  const cmd = process.argv[2];
@@ -95,176 +94,6 @@ switch (cmd) {
95
94
  break;
96
95
  }
97
96
 
98
- // ── Setup wizard (standalone) ─────────────────────────────────────────────────
99
-
100
- async function runSetupWizard() {
101
- // Force-run the wizard even if .env exists (re-configure)
102
- const { setupWizard } = await import("./launcher.js?setup-only");
103
- // The wizard is embedded in launcher.js; we import the module dynamically.
104
- // Since launcher.js runs main() on import, we instead inline a lightweight version.
105
-
106
- const readline = await import("readline");
107
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
108
- const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
109
-
110
- const HOME = os.homedir();
111
- const SINAIN_DIR = path.join(HOME, ".sinain");
112
- const envPath = path.join(SINAIN_DIR, ".env");
113
-
114
- const BOLD = "\x1b[1m";
115
- const DIM = "\x1b[2m";
116
- const GREEN = "\x1b[32m";
117
- const YELLOW = "\x1b[33m";
118
- const RESET = "\x1b[0m";
119
- const IS_WIN = os.platform() === "win32";
120
-
121
- const cmdExists = (cmd) => {
122
- try { import("child_process").then(cp => cp.execSync(`which ${cmd}`, { stdio: "pipe" })); return true; }
123
- catch { return false; }
124
- };
125
- // Synchronous version
126
- const { execSync } = await import("child_process");
127
- const cmdExistsSync = (cmd) => {
128
- try { execSync(`which ${cmd}`, { stdio: "pipe" }); return true; }
129
- catch { return false; }
130
- };
131
-
132
- if (fs.existsSync(envPath)) {
133
- const overwrite = await ask(` ${envPath} already exists. Overwrite? [y/N]: `);
134
- if (overwrite.trim().toLowerCase() !== "y") {
135
- console.log(" Aborted.");
136
- rl.close();
137
- return;
138
- }
139
- }
140
-
141
- console.log();
142
- console.log(`${BOLD}── Sinain Setup Wizard ─────────────────${RESET}`);
143
- console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
144
- console.log();
145
-
146
- const vars = {};
147
-
148
- // Transcription backend
149
- let transcriptionBackend = "openrouter";
150
- const hasWhisper = !IS_WIN && cmdExistsSync("whisper-cli");
151
-
152
- if (IS_WIN) {
153
- console.log(` ${DIM}(Local whisper not available on Windows — using OpenRouter)${RESET}`);
154
- } else if (hasWhisper) {
155
- const choice = await ask(` Transcription backend? [${BOLD}local${RESET}/cloud]: `);
156
- transcriptionBackend = choice.trim().toLowerCase() === "cloud" ? "openrouter" : "local";
157
- } else {
158
- const install = await ask(` whisper-cli not found. Install via Homebrew? [Y/n]: `);
159
- if (!install.trim() || install.trim().toLowerCase() === "y") {
160
- try {
161
- execSync("brew install whisper-cpp", { stdio: "inherit" });
162
- const modelDir = path.join(HOME, "models");
163
- const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
164
- if (!fs.existsSync(modelPath)) {
165
- console.log(` ${DIM}Downloading model (~1.5 GB)...${RESET}`);
166
- fs.mkdirSync(modelDir, { recursive: true });
167
- execSync(`curl -L --progress-bar -o "${modelPath}" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin"`, { stdio: "inherit" });
168
- }
169
- transcriptionBackend = "local";
170
- vars.LOCAL_WHISPER_MODEL = modelPath;
171
- } catch {
172
- console.log(` ${YELLOW}Install failed — falling back to OpenRouter${RESET}`);
173
- }
174
- }
175
- }
176
- vars.TRANSCRIPTION_BACKEND = transcriptionBackend;
177
-
178
- // API key
179
- if (transcriptionBackend === "openrouter") {
180
- const key = await ask(` OpenRouter API key (sk-or-...): `);
181
- if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
182
- } else {
183
- const key = await ask(` OpenRouter API key for vision/OCR (optional): `);
184
- if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
185
- }
186
-
187
- // Agent + escalation + gateway → agents.json (not .env). Tokens stay in
188
- // .env as secrets, referenced from agents.json via ${VAR} indirection.
189
- const agentsPatch = {};
190
-
191
- // Default agent — overlay's chip selector lets the user switch at runtime;
192
- // this just sets the boot-time default.
193
- const agent = await ask(` Default agent? [${BOLD}claude${RESET}/openclaude/codex/goose/junie/aider]: `);
194
- agentsPatch.default = agent.trim().toLowerCase() || "claude";
195
-
196
- // Escalation mode
197
- console.log(`\n ${DIM}Escalation: off | selective | focus | rich${RESET}`);
198
- const esc = await ask(` Escalation mode? [${BOLD}selective${RESET}]: `);
199
- agentsPatch.escalationMode = esc.trim().toLowerCase() || "selective";
200
-
201
- // Gateway — when enabled, writes the openclaw profile to agents.json
202
- // (URLs + session) and tokens to .env. There's no transport choice
203
- // anymore; picking openclaw vs a local agent in the overlay determines
204
- // dispatch (WS vs HTTP).
205
- const gw = await ask(` OpenClaw gateway? [y/N]: `);
206
- if (gw.trim().toLowerCase() === "y") {
207
- const url = await ask(` Gateway WS URL [ws://localhost:18789]: `);
208
- const wsUrl = url.trim() || "ws://localhost:18789";
209
- const token = await ask(` Auth token (48-char hex): `);
210
- if (token.trim()) {
211
- vars.OPENCLAW_WS_TOKEN = token.trim();
212
- vars.OPENCLAW_HTTP_TOKEN = token.trim();
213
- }
214
- agentsPatch.openclawProfile = {
215
- wsUrl,
216
- httpUrl: wsUrl.replace(/^ws/, "http") + "/hooks/agent",
217
- wsToken: "${OPENCLAW_WS_TOKEN}",
218
- httpToken: "${OPENCLAW_HTTP_TOKEN}",
219
- sessionKey: "agent:main:sinain",
220
- };
221
- } else {
222
- // Skip / disable: drop the openclaw profile entirely.
223
- agentsPatch.openclawProfile = null;
224
- agentsPatch.escalationMode = "off";
225
- }
226
-
227
- vars.SINAIN_HEARTBEAT_INTERVAL = "900";
228
- vars.PRIVACY_MODE = "standard";
229
-
230
- // Write — start from .env.example template, patch wizard values in
231
- fs.mkdirSync(SINAIN_DIR, { recursive: true });
232
-
233
- const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
234
- const examplePath = path.join(PKG_DIR, ".env.example");
235
- const siblingExample = path.join(PKG_DIR, "..", ".env.example");
236
- let template = "";
237
- if (fs.existsSync(examplePath)) {
238
- template = fs.readFileSync(examplePath, "utf-8");
239
- } else if (fs.existsSync(siblingExample)) {
240
- template = fs.readFileSync(siblingExample, "utf-8");
241
- }
242
-
243
- if (template) {
244
- for (const [k, v] of Object.entries(vars)) {
245
- const regex = new RegExp(`^#?\\s*${k}=.*$`, "m");
246
- if (regex.test(template)) {
247
- template = template.replace(regex, `${k}=${v}`);
248
- } else {
249
- template += `\n${k}=${v}`;
250
- }
251
- }
252
- template = `# Generated by sinain setup wizard — ${new Date().toISOString()}\n${template}`;
253
- fs.writeFileSync(envPath, template);
254
- } else {
255
- const lines = ["# sinain configuration — generated by setup wizard", `# ${new Date().toISOString()}`, ""];
256
- for (const [k, v] of Object.entries(vars)) lines.push(`${k}=${v}`);
257
- lines.push("");
258
- fs.writeFileSync(envPath, lines.join("\n"));
259
- }
260
-
261
- // Patch ~/.sinain/agents.json with the wizard's agent + gateway choices.
262
- writeAgentsConfig(agentsPatch);
263
-
264
- rl.close();
265
- console.log(`\n ${GREEN}✓${RESET} Config written to ${envPath} + ~/.sinain/agents.json\n`);
266
- }
267
-
268
97
  // ── Stop ──────────────────────────────────────────────────────────────────────
269
98
 
270
99
  async function stopServices() {
package/launcher.js CHANGED
@@ -8,7 +8,6 @@ import path from "path";
8
8
  import os from "os";
9
9
  import net from "net";
10
10
  import readline from "readline";
11
- import { writeAgentsConfig, readAgentsConfig } from "./config-shared.js";
12
11
 
13
12
  // ── Colors ──────────────────────────────────────────────────────────────────
14
13
 
@@ -64,11 +63,22 @@ async function main() {
64
63
  console.log();
65
64
 
66
65
  // Run setup wizard on first launch (no ~/.sinain/.env) or when --setup flag is passed
66
+ //
67
+ // Delegates to onboard.js's clack-based runOnboard (Mitch's wizard from
68
+ // PR #43). Previously, launcher.js had its own readline-based setupWizard
69
+ // that diverged from `npx sinain onboard`'s flow — same package, two
70
+ // different setup experiences depending on entry point. This collapses
71
+ // both paths to a single source of truth in config-shared.js.
72
+ //
73
+ // skipLaunchPrompt: true tells runOnboard not to ask "start sinain now?"
74
+ // at the end — we're already inside the launcher and will continue
75
+ // start-up automatically once the wizard returns.
67
76
  const userEnvPath = path.join(SINAIN_DIR, ".env");
68
77
  const envExists = fs.existsSync(userEnvPath);
69
78
  if (forceSetup || !envExists) {
70
79
  log(envExists ? "Re-running setup wizard (--setup flag)..." : "First-time setup — running wizard...");
71
- await setupWizard(userEnvPath);
80
+ const { runOnboard } = await import("./onboard.js");
81
+ await runOnboard({ skipLaunchPrompt: true });
72
82
  } else {
73
83
  log(`Existing config found at ${DIM}${userEnvPath}${RESET} — skipping wizard. (Use ${BOLD}--setup${RESET} to re-configure.)`);
74
84
  }
@@ -406,303 +416,6 @@ async function ensureOllama() {
406
416
  }
407
417
  }
408
418
 
409
- // ── Setup wizard ─────────────────────────────────────────────────────────────
410
-
411
- async function setupWizard(envPath) {
412
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
413
- const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
414
-
415
- // Load existing .env values as defaults (for re-configuration)
416
- const existing = {};
417
- if (fs.existsSync(envPath)) {
418
- for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
419
- const m = line.match(/^([A-Z_]+)=(.*)$/);
420
- if (m) existing[m[1]] = m[2];
421
- }
422
- }
423
- const hasExisting = Object.keys(existing).length > 0;
424
-
425
- console.log();
426
- console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
427
- console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
428
- if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
429
- console.log();
430
-
431
- const vars = {};
432
-
433
- // 1. Transcription backend — auto-detect whisper-cli
434
- let transcriptionBackend = "openrouter";
435
- const hasWhisper = !IS_WINDOWS && commandExists("whisper-cli");
436
-
437
- if (IS_WINDOWS) {
438
- console.log(` ${DIM}(Local whisper not available on Windows — using OpenRouter)${RESET}`);
439
- } else if (hasWhisper) {
440
- const choice = await ask(` Transcription backend? [${BOLD}local${RESET}/cloud] (local = whisper-cli, no API key): `);
441
- if (choice.trim().toLowerCase() === "cloud") {
442
- transcriptionBackend = "openrouter";
443
- } else {
444
- transcriptionBackend = "local";
445
- }
446
- } else {
447
- const installWhisper = await ask(` whisper-cli not found. Install via Homebrew? [Y/n]: `);
448
- if (!installWhisper.trim() || installWhisper.trim().toLowerCase() === "y") {
449
- try {
450
- console.log(` ${DIM}Installing whisper-cpp...${RESET}`);
451
- execSync("brew install whisper-cpp", { stdio: "inherit" });
452
-
453
- // Download model
454
- const modelDir = path.join(HOME, "models");
455
- const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
456
- if (!fs.existsSync(modelPath)) {
457
- console.log(` ${DIM}Downloading ggml-large-v3-turbo (~1.5 GB)...${RESET}`);
458
- fs.mkdirSync(modelDir, { recursive: true });
459
- execSync(
460
- `curl -L --progress-bar -o "${modelPath}" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin"`,
461
- { stdio: "inherit" }
462
- );
463
- }
464
-
465
- transcriptionBackend = "local";
466
- vars.LOCAL_WHISPER_MODEL = modelPath;
467
- ok("whisper-cpp installed");
468
- } catch {
469
- warn("whisper-cpp install failed — falling back to OpenRouter");
470
- transcriptionBackend = "openrouter";
471
- }
472
- } else {
473
- transcriptionBackend = "openrouter";
474
- }
475
- }
476
- vars.TRANSCRIPTION_BACKEND = transcriptionBackend;
477
-
478
- // 2. OpenRouter API key (if cloud backend or for vision/OCR)
479
- if (transcriptionBackend === "openrouter") {
480
- const existingKey = existing.OPENROUTER_API_KEY;
481
- const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
482
- let key = "";
483
- while (!key) {
484
- key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
485
- key = key.trim();
486
- if (!key && existingKey) { key = existingKey; break; }
487
- if (key && !key.startsWith("sk-or-")) {
488
- console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
489
- const retry = await ask(` Use this key anyway? [y/N]: `);
490
- if (retry.trim().toLowerCase() !== "y") { key = ""; continue; }
491
- }
492
- if (!key) {
493
- console.log(` ${DIM}You can set OPENROUTER_API_KEY later in ~/.sinain/.env${RESET}`);
494
- break;
495
- }
496
- }
497
- if (key) vars.OPENROUTER_API_KEY = key;
498
- } else {
499
- // Still ask for OpenRouter key (needed for vision/OCR)
500
- const existingKey = existing.OPENROUTER_API_KEY;
501
- const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
502
- const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip)${keyHint}: `);
503
- if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
504
- else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
505
- }
506
-
507
- // 3. Agent selection — written to agents.json `default` field. Existing
508
- // value is read from agents.json first, falling back to .env for users
509
- // mid-migration.
510
- const existingAgentsCfg = readAgentsConfig();
511
- const agentsPatch = {};
512
- const defaultAgent = existingAgentsCfg?.default || existing.SINAIN_AGENT || "claude";
513
- const agentChoice = await ask(` Default agent? [${BOLD}${defaultAgent}${RESET}/claude/openclaude/codex/goose/junie/aider]: `);
514
- agentsPatch.default = agentChoice.trim().toLowerCase() || defaultAgent;
515
-
516
- // 3b. Local vision (Ollama)
517
- const IS_MACOS = os.platform() === "darwin";
518
- const hasOllama = commandExists("ollama");
519
- if (hasOllama) {
520
- const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
521
- if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
522
- vars.LOCAL_VISION_ENABLED = "true";
523
- // Ensure ollama serve is running before list/pull
524
- const ollamaReady = await ensureOllama();
525
- if (ollamaReady) {
526
- try {
527
- const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
528
- if (!models.includes("llava")) {
529
- const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
530
- if (!pull.trim() || pull.trim().toLowerCase() === "y") {
531
- console.log(` ${DIM}Pulling llava...${RESET}`);
532
- execSync("ollama pull llava", { stdio: "inherit" });
533
- ok("llava model pulled");
534
- }
535
- } else {
536
- ok("llava model already available");
537
- }
538
- } catch {
539
- warn("Could not check Ollama models");
540
- }
541
- }
542
- vars.LOCAL_VISION_MODEL = "llava";
543
- }
544
- } else {
545
- const installOllama = await ask(` Install Ollama for local vision AI? [y/N]: `);
546
- if (installOllama.trim().toLowerCase() === "y") {
547
- try {
548
- if (IS_MACOS) {
549
- console.log(` ${DIM}Installing Ollama via Homebrew...${RESET}`);
550
- execSync("brew install ollama", { stdio: "inherit" });
551
- } else {
552
- console.log(` ${DIM}Installing Ollama...${RESET}`);
553
- execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
554
- }
555
- // Start ollama serve before pulling
556
- await ensureOllama();
557
- console.log(` ${DIM}Pulling llava vision model...${RESET}`);
558
- execSync("ollama pull llava", { stdio: "inherit" });
559
- vars.LOCAL_VISION_ENABLED = "true";
560
- vars.LOCAL_VISION_MODEL = "llava";
561
- ok("Ollama + llava installed");
562
- } catch {
563
- warn("Ollama installation failed — local vision disabled");
564
- }
565
- }
566
- }
567
-
568
- // 4. Escalation mode — agents.json `escalation.mode`
569
- console.log();
570
- console.log(` ${DIM}Escalation modes:${RESET}`);
571
- console.log(` off — no escalation`);
572
- console.log(` selective — score-based (errors, questions trigger it)`);
573
- console.log(` focus — always escalate every tick`);
574
- console.log(` rich — always escalate with maximum context`);
575
- const defaultEsc = existingAgentsCfg?.escalation?.mode || existing.ESCALATION_MODE || "selective";
576
- const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
577
- agentsPatch.escalationMode = escMode.trim().toLowerCase() || defaultEsc;
578
-
579
- // 5. OpenClaw gateway — URLs/session → agents.json (openclaw profile),
580
- // tokens → .env. No transport question; agent identity (openclaw vs
581
- // local) determines dispatch path.
582
- const existingOpenclaw = existingAgentsCfg?.profiles?.openclaw;
583
- const existingWsUrl = existingOpenclaw?.wsUrl || existing.OPENCLAW_WS_URL || "";
584
- const hadGateway = !!existingWsUrl;
585
- const gatewayDefault = hadGateway ? "Y" : "N";
586
- const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
587
- const wantsGateway = hasGateway.trim()
588
- ? hasGateway.trim().toLowerCase() === "y"
589
- : hadGateway;
590
- if (wantsGateway) {
591
- const defaultWs = existingWsUrl || "ws://localhost:18789";
592
- const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
593
- const finalWsUrl = wsUrl.trim() || defaultWs;
594
-
595
- const existingToken = existing.OPENCLAW_WS_TOKEN;
596
- const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
597
- const wsToken = await ask(` Gateway auth token (48-char hex)${tokenHint}: `);
598
- if (wsToken.trim()) {
599
- vars.OPENCLAW_WS_TOKEN = wsToken.trim();
600
- vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
601
- } else if (existingToken) {
602
- vars.OPENCLAW_WS_TOKEN = existingToken;
603
- vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
604
- }
605
-
606
- agentsPatch.openclawProfile = {
607
- wsUrl: finalWsUrl,
608
- httpUrl: finalWsUrl.replace(/^ws/, "http") + "/hooks/agent",
609
- wsToken: "${OPENCLAW_WS_TOKEN}",
610
- httpToken: "${OPENCLAW_HTTP_TOKEN}",
611
- sessionKey: existingOpenclaw?.sessionKey || "agent:main:sinain",
612
- };
613
- } else {
614
- // No gateway — drop the openclaw profile entirely.
615
- agentsPatch.openclawProfile = null;
616
- }
617
-
618
- // 6. Knowledge import (for standalone machines)
619
- console.log();
620
- const wantImport = await ask(` Import knowledge from another machine? [y/N]: `);
621
- if (wantImport.trim().toLowerCase() === "y") {
622
- const filePath = await ask(` Path to knowledge export (.tar.gz): `);
623
- const resolved = filePath.trim().replace(/^~/, HOME);
624
- if (resolved && fs.existsSync(resolved)) {
625
- const targetWorkspace = path.join(HOME, ".sinain/workspace");
626
- fs.mkdirSync(targetWorkspace, { recursive: true });
627
- try {
628
- execSync(`tar xzf "${resolved}" -C "${targetWorkspace}"`, { stdio: "inherit" });
629
- // Symlink sinain-memory scripts from npm package
630
- const srcMemory = path.join(PKG_DIR, "sinain-memory");
631
- const dstMemory = path.join(targetWorkspace, "sinain-memory");
632
- try { fs.rmSync(dstMemory, { recursive: true }); } catch {}
633
- fs.symlinkSync(srcMemory, dstMemory);
634
- vars.SINAIN_WORKSPACE = targetWorkspace;
635
- vars.OPENCLAW_WORKSPACE_DIR = targetWorkspace;
636
- ok(`Knowledge imported to ${targetWorkspace}`);
637
- } catch (e) {
638
- warn(`Import failed: ${e.message}`);
639
- }
640
- } else if (resolved) {
641
- warn(`File not found: ${resolved}`);
642
- }
643
- }
644
-
645
- // 7. Agent-specific defaults
646
- vars.SINAIN_POLL_INTERVAL = "5";
647
- vars.SINAIN_HEARTBEAT_INTERVAL = "900";
648
- vars.PRIVACY_MODE = "standard";
649
-
650
- // Write .env — start from .env.example template, patch wizard values in
651
- fs.mkdirSync(path.dirname(envPath), { recursive: true });
652
-
653
- const examplePath = path.join(PKG_DIR, ".env.example");
654
- let template = "";
655
- if (fs.existsSync(examplePath)) {
656
- template = fs.readFileSync(examplePath, "utf-8");
657
- } else {
658
- // Fallback: try sibling (running from cloned repo)
659
- const siblingExample = path.join(PKG_DIR, "..", ".env.example");
660
- if (fs.existsSync(siblingExample)) {
661
- template = fs.readFileSync(siblingExample, "utf-8");
662
- }
663
- }
664
-
665
- if (template) {
666
- // Patch each wizard var into the template by replacing the KEY=... line
667
- for (const [key, val] of Object.entries(vars)) {
668
- // Match KEY=anything (possibly commented out with #)
669
- const regex = new RegExp(`^#?\\s*${key}=.*$`, "m");
670
- if (regex.test(template)) {
671
- template = template.replace(regex, `${key}=${val}`);
672
- } else {
673
- // Key not in template — append it
674
- template += `\n${key}=${val}`;
675
- }
676
- }
677
- // Add wizard timestamp header
678
- template = `# Generated by sinain setup wizard — ${new Date().toISOString()}\n${template}`;
679
- fs.writeFileSync(envPath, template);
680
- } else {
681
- // No template found — write bare vars (fallback)
682
- const lines = [];
683
- lines.push("# sinain configuration — generated by setup wizard");
684
- lines.push(`# ${new Date().toISOString()}`);
685
- lines.push("");
686
- for (const [key, val] of Object.entries(vars)) {
687
- lines.push(`${key}=${val}`);
688
- }
689
- lines.push("");
690
- fs.writeFileSync(envPath, lines.join("\n"));
691
- }
692
-
693
- // Flush agent + gateway answers to ~/.sinain/agents.json (separate file
694
- // from .env after the profile-config refactor).
695
- if (Object.keys(agentsPatch).length > 0) {
696
- writeAgentsConfig(agentsPatch);
697
- }
698
-
699
- rl.close();
700
-
701
- console.log();
702
- ok(`Config written to ${envPath} + ~/.sinain/agents.json`);
703
- console.log();
704
- }
705
-
706
419
  // ── User environment ────────────────────────────────────────────────────────
707
420
 
708
421
  function loadUserEnv() {
package/onboard.js CHANGED
@@ -272,6 +272,15 @@ export async function runOnboard(args = {}) {
272
272
  printOutro();
273
273
 
274
274
  // ── Start? ────────────────────────────────────────────────────────────
275
+ //
276
+ // When called from launcher.js (via `sinain start --setup`), the launcher
277
+ // is about to start sinain itself — asking the user again is redundant.
278
+ // Caller passes { skipLaunchPrompt: true } in that case.
279
+
280
+ if (args.skipLaunchPrompt) {
281
+ p.outro("Setup complete — starting sinain...");
282
+ return;
283
+ }
275
284
 
276
285
  const startNow = guard(await p.confirm({
277
286
  message: "Start sinain now?",
@@ -309,6 +318,18 @@ function printOutro() {
309
318
  }
310
319
 
311
320
  // ── CLI entry point ─────────────────────────────────────────────────────────
321
+ //
322
+ // Guarded so `import { runOnboard } from "./onboard.js"` from launcher.js
323
+ // (or anywhere else) doesn't trigger side effects. The CLI block runs only
324
+ // when this file is the program's entry point — i.e. `node onboard.js ...`
325
+ // or `npx @geravant/sinain onboard ...`.
326
+
327
+ const isMainModule = import.meta.url === new URL(`file://${process.argv[1]}`).href
328
+ || (process.argv[1] && import.meta.url.endsWith(path.basename(process.argv[1])));
329
+
330
+ if (!isMainModule) {
331
+ // Imported as a module — exports are available; do not parse argv.
332
+ } else {
312
333
 
313
334
  const cliArgs = process.argv.slice(2);
314
335
  const flags = {};
@@ -359,3 +380,5 @@ if (flags.nonInteractive) {
359
380
  process.exit(1);
360
381
  });
361
382
  }
383
+
384
+ } // end isMainModule guard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.15.4",
3
+ "version": "1.15.6",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {