@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 +183 -26
- package/launcher.js +206 -36
- package/package.json +1 -1
- package/setup-overlay.js +38 -20
- package/sinain-agent/CLAUDE.md +1 -1
- package/sinain-agent/run.sh +12 -5
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
|
-
|
|
50
|
-
"tsx
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
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 = "
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
|
500
|
-
const
|
|
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
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 ===
|
|
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
|
|
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,
|
|
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
|
|
134
|
+
// Extract
|
|
129
135
|
log("Extracting...");
|
|
130
|
-
|
|
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(
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
package/sinain-agent/CLAUDE.md
CHANGED
|
@@ -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
|
|
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.
|
package/sinain-agent/run.sh
CHANGED
|
@@ -3,12 +3,19 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
5
|
|
|
6
|
-
# Load .env
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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}"
|