@geravant/sinain 1.15.5 → 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.
- package/cli.js +0 -171
- package/launcher.js +0 -298
- 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
|
|
|
@@ -417,303 +416,6 @@ async function ensureOllama() {
|
|
|
417
416
|
}
|
|
418
417
|
}
|
|
419
418
|
|
|
420
|
-
// ── Setup wizard ─────────────────────────────────────────────────────────────
|
|
421
|
-
|
|
422
|
-
async function setupWizard(envPath) {
|
|
423
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
424
|
-
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
425
|
-
|
|
426
|
-
// Load existing .env values as defaults (for re-configuration)
|
|
427
|
-
const existing = {};
|
|
428
|
-
if (fs.existsSync(envPath)) {
|
|
429
|
-
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
430
|
-
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
431
|
-
if (m) existing[m[1]] = m[2];
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
const hasExisting = Object.keys(existing).length > 0;
|
|
435
|
-
|
|
436
|
-
console.log();
|
|
437
|
-
console.log(`${BOLD}── ${hasExisting ? "Re-configure" : "First-time setup"} ────────────────────${RESET}`);
|
|
438
|
-
console.log(` Configuring ${DIM}~/.sinain/.env${RESET}`);
|
|
439
|
-
if (hasExisting) console.log(` ${DIM}Press Enter to keep current values shown in [brackets]${RESET}`);
|
|
440
|
-
console.log();
|
|
441
|
-
|
|
442
|
-
const vars = {};
|
|
443
|
-
|
|
444
|
-
// 1. Transcription backend — auto-detect whisper-cli
|
|
445
|
-
let transcriptionBackend = "openrouter";
|
|
446
|
-
const hasWhisper = !IS_WINDOWS && commandExists("whisper-cli");
|
|
447
|
-
|
|
448
|
-
if (IS_WINDOWS) {
|
|
449
|
-
console.log(` ${DIM}(Local whisper not available on Windows — using OpenRouter)${RESET}`);
|
|
450
|
-
} else if (hasWhisper) {
|
|
451
|
-
const choice = await ask(` Transcription backend? [${BOLD}local${RESET}/cloud] (local = whisper-cli, no API key): `);
|
|
452
|
-
if (choice.trim().toLowerCase() === "cloud") {
|
|
453
|
-
transcriptionBackend = "openrouter";
|
|
454
|
-
} else {
|
|
455
|
-
transcriptionBackend = "local";
|
|
456
|
-
}
|
|
457
|
-
} else {
|
|
458
|
-
const installWhisper = await ask(` whisper-cli not found. Install via Homebrew? [Y/n]: `);
|
|
459
|
-
if (!installWhisper.trim() || installWhisper.trim().toLowerCase() === "y") {
|
|
460
|
-
try {
|
|
461
|
-
console.log(` ${DIM}Installing whisper-cpp...${RESET}`);
|
|
462
|
-
execSync("brew install whisper-cpp", { stdio: "inherit" });
|
|
463
|
-
|
|
464
|
-
// Download model
|
|
465
|
-
const modelDir = path.join(HOME, "models");
|
|
466
|
-
const modelPath = path.join(modelDir, "ggml-large-v3-turbo.bin");
|
|
467
|
-
if (!fs.existsSync(modelPath)) {
|
|
468
|
-
console.log(` ${DIM}Downloading ggml-large-v3-turbo (~1.5 GB)...${RESET}`);
|
|
469
|
-
fs.mkdirSync(modelDir, { recursive: true });
|
|
470
|
-
execSync(
|
|
471
|
-
`curl -L --progress-bar -o "${modelPath}" "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin"`,
|
|
472
|
-
{ stdio: "inherit" }
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
transcriptionBackend = "local";
|
|
477
|
-
vars.LOCAL_WHISPER_MODEL = modelPath;
|
|
478
|
-
ok("whisper-cpp installed");
|
|
479
|
-
} catch {
|
|
480
|
-
warn("whisper-cpp install failed — falling back to OpenRouter");
|
|
481
|
-
transcriptionBackend = "openrouter";
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
transcriptionBackend = "openrouter";
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
vars.TRANSCRIPTION_BACKEND = transcriptionBackend;
|
|
488
|
-
|
|
489
|
-
// 2. OpenRouter API key (if cloud backend or for vision/OCR)
|
|
490
|
-
if (transcriptionBackend === "openrouter") {
|
|
491
|
-
const existingKey = existing.OPENROUTER_API_KEY;
|
|
492
|
-
const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
|
|
493
|
-
let key = "";
|
|
494
|
-
while (!key) {
|
|
495
|
-
key = await ask(` OpenRouter API key (sk-or-...)${keyHint}: `);
|
|
496
|
-
key = key.trim();
|
|
497
|
-
if (!key && existingKey) { key = existingKey; break; }
|
|
498
|
-
if (key && !key.startsWith("sk-or-")) {
|
|
499
|
-
console.log(` ${YELLOW}⚠${RESET} Key should start with sk-or-. Try again or press Enter to skip.`);
|
|
500
|
-
const retry = await ask(` Use this key anyway? [y/N]: `);
|
|
501
|
-
if (retry.trim().toLowerCase() !== "y") { key = ""; continue; }
|
|
502
|
-
}
|
|
503
|
-
if (!key) {
|
|
504
|
-
console.log(` ${DIM}You can set OPENROUTER_API_KEY later in ~/.sinain/.env${RESET}`);
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (key) vars.OPENROUTER_API_KEY = key;
|
|
509
|
-
} else {
|
|
510
|
-
// Still ask for OpenRouter key (needed for vision/OCR)
|
|
511
|
-
const existingKey = existing.OPENROUTER_API_KEY;
|
|
512
|
-
const keyHint = existingKey ? ` [${existingKey.slice(0, 8)}...${existingKey.slice(-4)}]` : "";
|
|
513
|
-
const key = await ask(` OpenRouter API key for vision/OCR (optional, Enter to skip)${keyHint}: `);
|
|
514
|
-
if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
|
|
515
|
-
else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// 3. Agent selection — written to agents.json `default` field. Existing
|
|
519
|
-
// value is read from agents.json first, falling back to .env for users
|
|
520
|
-
// mid-migration.
|
|
521
|
-
const existingAgentsCfg = readAgentsConfig();
|
|
522
|
-
const agentsPatch = {};
|
|
523
|
-
const defaultAgent = existingAgentsCfg?.default || existing.SINAIN_AGENT || "claude";
|
|
524
|
-
const agentChoice = await ask(` Default agent? [${BOLD}${defaultAgent}${RESET}/claude/openclaude/codex/goose/junie/aider]: `);
|
|
525
|
-
agentsPatch.default = agentChoice.trim().toLowerCase() || defaultAgent;
|
|
526
|
-
|
|
527
|
-
// 3b. Local vision (Ollama)
|
|
528
|
-
const IS_MACOS = os.platform() === "darwin";
|
|
529
|
-
const hasOllama = commandExists("ollama");
|
|
530
|
-
if (hasOllama) {
|
|
531
|
-
const useVision = await ask(` Enable local vision AI? [Y/n] (Ollama — screen understanding without cloud API): `);
|
|
532
|
-
if (!useVision.trim() || useVision.trim().toLowerCase() === "y") {
|
|
533
|
-
vars.LOCAL_VISION_ENABLED = "true";
|
|
534
|
-
// Ensure ollama serve is running before list/pull
|
|
535
|
-
const ollamaReady = await ensureOllama();
|
|
536
|
-
if (ollamaReady) {
|
|
537
|
-
try {
|
|
538
|
-
const models = execSync("ollama list 2>/dev/null", { encoding: "utf-8" });
|
|
539
|
-
if (!models.includes("llava")) {
|
|
540
|
-
const pull = await ask(` Pull llava vision model (~4GB)? [Y/n]: `);
|
|
541
|
-
if (!pull.trim() || pull.trim().toLowerCase() === "y") {
|
|
542
|
-
console.log(` ${DIM}Pulling llava...${RESET}`);
|
|
543
|
-
execSync("ollama pull llava", { stdio: "inherit" });
|
|
544
|
-
ok("llava model pulled");
|
|
545
|
-
}
|
|
546
|
-
} else {
|
|
547
|
-
ok("llava model already available");
|
|
548
|
-
}
|
|
549
|
-
} catch {
|
|
550
|
-
warn("Could not check Ollama models");
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
vars.LOCAL_VISION_MODEL = "llava";
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
const installOllama = await ask(` Install Ollama for local vision AI? [y/N]: `);
|
|
557
|
-
if (installOllama.trim().toLowerCase() === "y") {
|
|
558
|
-
try {
|
|
559
|
-
if (IS_MACOS) {
|
|
560
|
-
console.log(` ${DIM}Installing Ollama via Homebrew...${RESET}`);
|
|
561
|
-
execSync("brew install ollama", { stdio: "inherit" });
|
|
562
|
-
} else {
|
|
563
|
-
console.log(` ${DIM}Installing Ollama...${RESET}`);
|
|
564
|
-
execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
|
|
565
|
-
}
|
|
566
|
-
// Start ollama serve before pulling
|
|
567
|
-
await ensureOllama();
|
|
568
|
-
console.log(` ${DIM}Pulling llava vision model...${RESET}`);
|
|
569
|
-
execSync("ollama pull llava", { stdio: "inherit" });
|
|
570
|
-
vars.LOCAL_VISION_ENABLED = "true";
|
|
571
|
-
vars.LOCAL_VISION_MODEL = "llava";
|
|
572
|
-
ok("Ollama + llava installed");
|
|
573
|
-
} catch {
|
|
574
|
-
warn("Ollama installation failed — local vision disabled");
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// 4. Escalation mode — agents.json `escalation.mode`
|
|
580
|
-
console.log();
|
|
581
|
-
console.log(` ${DIM}Escalation modes:${RESET}`);
|
|
582
|
-
console.log(` off — no escalation`);
|
|
583
|
-
console.log(` selective — score-based (errors, questions trigger it)`);
|
|
584
|
-
console.log(` focus — always escalate every tick`);
|
|
585
|
-
console.log(` rich — always escalate with maximum context`);
|
|
586
|
-
const defaultEsc = existingAgentsCfg?.escalation?.mode || existing.ESCALATION_MODE || "selective";
|
|
587
|
-
const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
|
|
588
|
-
agentsPatch.escalationMode = escMode.trim().toLowerCase() || defaultEsc;
|
|
589
|
-
|
|
590
|
-
// 5. OpenClaw gateway — URLs/session → agents.json (openclaw profile),
|
|
591
|
-
// tokens → .env. No transport question; agent identity (openclaw vs
|
|
592
|
-
// local) determines dispatch path.
|
|
593
|
-
const existingOpenclaw = existingAgentsCfg?.profiles?.openclaw;
|
|
594
|
-
const existingWsUrl = existingOpenclaw?.wsUrl || existing.OPENCLAW_WS_URL || "";
|
|
595
|
-
const hadGateway = !!existingWsUrl;
|
|
596
|
-
const gatewayDefault = hadGateway ? "Y" : "N";
|
|
597
|
-
const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
|
|
598
|
-
const wantsGateway = hasGateway.trim()
|
|
599
|
-
? hasGateway.trim().toLowerCase() === "y"
|
|
600
|
-
: hadGateway;
|
|
601
|
-
if (wantsGateway) {
|
|
602
|
-
const defaultWs = existingWsUrl || "ws://localhost:18789";
|
|
603
|
-
const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
|
|
604
|
-
const finalWsUrl = wsUrl.trim() || defaultWs;
|
|
605
|
-
|
|
606
|
-
const existingToken = existing.OPENCLAW_WS_TOKEN;
|
|
607
|
-
const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
|
|
608
|
-
const wsToken = await ask(` Gateway auth token (48-char hex)${tokenHint}: `);
|
|
609
|
-
if (wsToken.trim()) {
|
|
610
|
-
vars.OPENCLAW_WS_TOKEN = wsToken.trim();
|
|
611
|
-
vars.OPENCLAW_HTTP_TOKEN = wsToken.trim();
|
|
612
|
-
} else if (existingToken) {
|
|
613
|
-
vars.OPENCLAW_WS_TOKEN = existingToken;
|
|
614
|
-
vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
agentsPatch.openclawProfile = {
|
|
618
|
-
wsUrl: finalWsUrl,
|
|
619
|
-
httpUrl: finalWsUrl.replace(/^ws/, "http") + "/hooks/agent",
|
|
620
|
-
wsToken: "${OPENCLAW_WS_TOKEN}",
|
|
621
|
-
httpToken: "${OPENCLAW_HTTP_TOKEN}",
|
|
622
|
-
sessionKey: existingOpenclaw?.sessionKey || "agent:main:sinain",
|
|
623
|
-
};
|
|
624
|
-
} else {
|
|
625
|
-
// No gateway — drop the openclaw profile entirely.
|
|
626
|
-
agentsPatch.openclawProfile = null;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// 6. Knowledge import (for standalone machines)
|
|
630
|
-
console.log();
|
|
631
|
-
const wantImport = await ask(` Import knowledge from another machine? [y/N]: `);
|
|
632
|
-
if (wantImport.trim().toLowerCase() === "y") {
|
|
633
|
-
const filePath = await ask(` Path to knowledge export (.tar.gz): `);
|
|
634
|
-
const resolved = filePath.trim().replace(/^~/, HOME);
|
|
635
|
-
if (resolved && fs.existsSync(resolved)) {
|
|
636
|
-
const targetWorkspace = path.join(HOME, ".sinain/workspace");
|
|
637
|
-
fs.mkdirSync(targetWorkspace, { recursive: true });
|
|
638
|
-
try {
|
|
639
|
-
execSync(`tar xzf "${resolved}" -C "${targetWorkspace}"`, { stdio: "inherit" });
|
|
640
|
-
// Symlink sinain-memory scripts from npm package
|
|
641
|
-
const srcMemory = path.join(PKG_DIR, "sinain-memory");
|
|
642
|
-
const dstMemory = path.join(targetWorkspace, "sinain-memory");
|
|
643
|
-
try { fs.rmSync(dstMemory, { recursive: true }); } catch {}
|
|
644
|
-
fs.symlinkSync(srcMemory, dstMemory);
|
|
645
|
-
vars.SINAIN_WORKSPACE = targetWorkspace;
|
|
646
|
-
vars.OPENCLAW_WORKSPACE_DIR = targetWorkspace;
|
|
647
|
-
ok(`Knowledge imported to ${targetWorkspace}`);
|
|
648
|
-
} catch (e) {
|
|
649
|
-
warn(`Import failed: ${e.message}`);
|
|
650
|
-
}
|
|
651
|
-
} else if (resolved) {
|
|
652
|
-
warn(`File not found: ${resolved}`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// 7. Agent-specific defaults
|
|
657
|
-
vars.SINAIN_POLL_INTERVAL = "5";
|
|
658
|
-
vars.SINAIN_HEARTBEAT_INTERVAL = "900";
|
|
659
|
-
vars.PRIVACY_MODE = "standard";
|
|
660
|
-
|
|
661
|
-
// Write .env — start from .env.example template, patch wizard values in
|
|
662
|
-
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
663
|
-
|
|
664
|
-
const examplePath = path.join(PKG_DIR, ".env.example");
|
|
665
|
-
let template = "";
|
|
666
|
-
if (fs.existsSync(examplePath)) {
|
|
667
|
-
template = fs.readFileSync(examplePath, "utf-8");
|
|
668
|
-
} else {
|
|
669
|
-
// Fallback: try sibling (running from cloned repo)
|
|
670
|
-
const siblingExample = path.join(PKG_DIR, "..", ".env.example");
|
|
671
|
-
if (fs.existsSync(siblingExample)) {
|
|
672
|
-
template = fs.readFileSync(siblingExample, "utf-8");
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (template) {
|
|
677
|
-
// Patch each wizard var into the template by replacing the KEY=... line
|
|
678
|
-
for (const [key, val] of Object.entries(vars)) {
|
|
679
|
-
// Match KEY=anything (possibly commented out with #)
|
|
680
|
-
const regex = new RegExp(`^#?\\s*${key}=.*$`, "m");
|
|
681
|
-
if (regex.test(template)) {
|
|
682
|
-
template = template.replace(regex, `${key}=${val}`);
|
|
683
|
-
} else {
|
|
684
|
-
// Key not in template — append it
|
|
685
|
-
template += `\n${key}=${val}`;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
// Add wizard timestamp header
|
|
689
|
-
template = `# Generated by sinain setup wizard — ${new Date().toISOString()}\n${template}`;
|
|
690
|
-
fs.writeFileSync(envPath, template);
|
|
691
|
-
} else {
|
|
692
|
-
// No template found — write bare vars (fallback)
|
|
693
|
-
const lines = [];
|
|
694
|
-
lines.push("# sinain configuration — generated by setup wizard");
|
|
695
|
-
lines.push(`# ${new Date().toISOString()}`);
|
|
696
|
-
lines.push("");
|
|
697
|
-
for (const [key, val] of Object.entries(vars)) {
|
|
698
|
-
lines.push(`${key}=${val}`);
|
|
699
|
-
}
|
|
700
|
-
lines.push("");
|
|
701
|
-
fs.writeFileSync(envPath, lines.join("\n"));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Flush agent + gateway answers to ~/.sinain/agents.json (separate file
|
|
705
|
-
// from .env after the profile-config refactor).
|
|
706
|
-
if (Object.keys(agentsPatch).length > 0) {
|
|
707
|
-
writeAgentsConfig(agentsPatch);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
rl.close();
|
|
711
|
-
|
|
712
|
-
console.log();
|
|
713
|
-
ok(`Config written to ${envPath} + ~/.sinain/agents.json`);
|
|
714
|
-
console.log();
|
|
715
|
-
}
|
|
716
|
-
|
|
717
419
|
// ── User environment ────────────────────────────────────────────────────────
|
|
718
420
|
|
|
719
421
|
function loadUserEnv() {
|