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