@geravant/sinain 1.8.0 → 1.10.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/.env.example +14 -13
- package/HEARTBEAT.md +1 -1
- package/README.md +4 -7
- package/cli.js +16 -2
- package/config-shared.js +469 -0
- package/config.js +152 -0
- package/index.ts +1 -3
- package/launcher.js +7 -1
- package/onboard.js +345 -0
- package/package.json +8 -2
- package/sense_client/__main__.py +8 -4
- package/sense_client/gate.py +1 -0
- package/sense_client/ocr.py +58 -25
- package/sense_client/sender.py +2 -0
- package/sense_client/vision.py +31 -11
- package/sinain-agent/CLAUDE.md +0 -1
- package/sinain-agent/run.sh +2 -1
- package/sinain-core/src/agent/analyzer.ts +56 -58
- package/sinain-core/src/agent/loop.ts +37 -11
- package/sinain-core/src/audio/transcription.ts +20 -5
- package/sinain-core/src/config.ts +20 -16
- package/sinain-core/src/cost/tracker.ts +64 -0
- package/sinain-core/src/escalation/escalator.ts +31 -59
- package/sinain-core/src/index.ts +41 -45
- package/sinain-core/src/overlay/commands.ts +12 -0
- package/sinain-core/src/overlay/ws-handler.ts +27 -0
- package/sinain-core/src/server.ts +41 -0
- package/sinain-core/src/types.ts +46 -11
- package/sinain-knowledge/curation/engine.ts +0 -17
- package/sinain-knowledge/protocol/heartbeat.md +1 -1
- package/sinain-mcp-server/index.ts +4 -20
- package/sinain-memory/git_backup.sh +0 -19
package/.env.example
CHANGED
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
# The launcher reads this file on every start. sinain-core and sinain-agent
|
|
4
4
|
# inherit all vars via the launcher's process environment.
|
|
5
5
|
|
|
6
|
-
# ──
|
|
6
|
+
# ── Context Analysis (HUD summarizer) ────────────────────────────────────────
|
|
7
|
+
# Provider: openrouter (cloud, needs API key) or ollama (local, free)
|
|
8
|
+
ANALYSIS_PROVIDER=openrouter
|
|
9
|
+
ANALYSIS_MODEL=google/gemini-2.5-flash-lite
|
|
10
|
+
# ANALYSIS_VISION_MODEL=google/gemini-2.5-flash # auto-upgrade for image ticks
|
|
11
|
+
# ANALYSIS_ENDPOINT= # default per provider; override for custom
|
|
12
|
+
# ANALYSIS_API_KEY= # uses OPENROUTER_API_KEY if not set
|
|
13
|
+
# ANALYSIS_FALLBACK_MODELS=google/gemini-2.5-flash,anthropic/claude-3.5-haiku
|
|
14
|
+
|
|
15
|
+
# ── API Keys ─────────────────────────────────────────────────────────────────
|
|
7
16
|
OPENROUTER_API_KEY= # get one free at https://openrouter.ai
|
|
17
|
+
# used by context analysis + transcription
|
|
8
18
|
|
|
9
19
|
# ── Privacy ──────────────────────────────────────────────────────────────────
|
|
10
20
|
PRIVACY_MODE=standard # off | standard | strict | paranoid
|
|
@@ -75,18 +85,6 @@ TRANSCRIPTION_LANGUAGE=en-US
|
|
|
75
85
|
# LOCAL_WHISPER_MODEL=~/models/ggml-large-v3-turbo.bin
|
|
76
86
|
# LOCAL_WHISPER_TIMEOUT_MS=15000
|
|
77
87
|
|
|
78
|
-
# ── Local Agent Loop ─────────────────────────────────────────────────────────
|
|
79
|
-
AGENT_ENABLED=true
|
|
80
|
-
AGENT_MODEL=google/gemini-2.5-flash-lite
|
|
81
|
-
# AGENT_FALLBACK_MODELS=google/gemini-2.5-flash,anthropic/claude-3.5-haiku
|
|
82
|
-
AGENT_MAX_TOKENS=300
|
|
83
|
-
AGENT_TEMPERATURE=0.3
|
|
84
|
-
AGENT_PUSH_TO_FEED=true
|
|
85
|
-
AGENT_DEBOUNCE_MS=3000
|
|
86
|
-
AGENT_MAX_INTERVAL_MS=30000
|
|
87
|
-
AGENT_COOLDOWN_MS=10000
|
|
88
|
-
AGENT_MAX_AGE_MS=120000 # context window lookback (2 min)
|
|
89
|
-
|
|
90
88
|
# ── OpenClaw / NemoClaw Gateway ──────────────────────────────────────────────
|
|
91
89
|
# Leave blank to run without a gateway (bare agent mode).
|
|
92
90
|
# The setup wizard fills these in if you have an OpenClaw gateway.
|
|
@@ -111,3 +109,6 @@ SITUATION_MD_PATH=~/.openclaw/workspace/SITUATION.md
|
|
|
111
109
|
# ── Tracing ──────────────────────────────────────────────────────────────────
|
|
112
110
|
TRACE_ENABLED=true
|
|
113
111
|
TRACE_DIR=~/.sinain-core/traces
|
|
112
|
+
|
|
113
|
+
# ── Cost Display ─────────────────────────────────────────────────────────────
|
|
114
|
+
COST_DISPLAY_ENABLED=false # show LLM cost counter in overlay (always logged to stdout)
|
package/HEARTBEAT.md
CHANGED
|
@@ -59,4 +59,4 @@ SINAIN_BACKUP_REPO=<git-url> npx sinain
|
|
|
59
59
|
- Token printed at end (or visible in Brev dashboard → Gateway Token)
|
|
60
60
|
- Mac side: `./setup-nemoclaw.sh` → 5 prompts → overlay starts
|
|
61
61
|
|
|
62
|
-
Memory is
|
|
62
|
+
Memory is backed up via knowledge snapshots to `~/.sinain/knowledge-snapshots/`. New instances restore instantly via `SINAIN_BACKUP_REPO`.
|
package/README.md
CHANGED
|
@@ -29,13 +29,12 @@ Five lifecycle hooks, one tool, four commands, and a background service:
|
|
|
29
29
|
|
|
30
30
|
| Tool | Purpose |
|
|
31
31
|
|---|---|
|
|
32
|
-
| `sinain_heartbeat_tick` | Executes all heartbeat mechanical work (
|
|
32
|
+
| `sinain_heartbeat_tick` | Executes all heartbeat mechanical work (signal analysis, insight synthesis, log writing). Returns structured JSON with results, recommended actions, and Telegram output. |
|
|
33
33
|
|
|
34
34
|
The heartbeat tool accepts `{ sessionSummary: string, idle: boolean }` and runs:
|
|
35
|
-
1. `
|
|
36
|
-
2. `uv run python3 sinain-memory/
|
|
37
|
-
3.
|
|
38
|
-
4. Writes log entry to `memory/playbook-logs/YYYY-MM-DD.jsonl`
|
|
35
|
+
1. `uv run python3 sinain-memory/signal_analyzer.py` (60s timeout)
|
|
36
|
+
2. `uv run python3 sinain-memory/insight_synthesizer.py` (60s timeout)
|
|
37
|
+
3. Writes log entry to `memory/playbook-logs/YYYY-MM-DD.jsonl`
|
|
39
38
|
|
|
40
39
|
### Commands
|
|
41
40
|
|
|
@@ -114,8 +113,6 @@ Also ensures these directories exist:
|
|
|
114
113
|
- `memory/`, `memory/playbook-archive/`, `memory/playbook-logs/`
|
|
115
114
|
- `memory/eval-logs/`, `memory/eval-reports/`
|
|
116
115
|
|
|
117
|
-
The `git_backup.sh` script is automatically made executable (chmod 755) after sync.
|
|
118
|
-
|
|
119
116
|
After syncing modules, the plugin generates `memory/sinain-playbook-effective.md` — a merged view of active module patterns (sorted by priority) plus the base playbook.
|
|
120
117
|
|
|
121
118
|
## Heartbeat Compliance Validation
|
package/cli.js
CHANGED
|
@@ -24,8 +24,19 @@ switch (cmd) {
|
|
|
24
24
|
await showStatus();
|
|
25
25
|
break;
|
|
26
26
|
|
|
27
|
+
case "onboard":
|
|
28
|
+
await import("./onboard.js");
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case "config":
|
|
32
|
+
await import("./config.js");
|
|
33
|
+
break;
|
|
34
|
+
|
|
27
35
|
case "setup":
|
|
28
|
-
|
|
36
|
+
// Legacy — redirect to onboard
|
|
37
|
+
console.log("\x1b[33m ⚠ `sinain setup` is deprecated. Use: sinain onboard\x1b[0m");
|
|
38
|
+
console.log("\x1b[2m Or: sinain onboard --advanced for full options\x1b[0m\n");
|
|
39
|
+
await import("./onboard.js");
|
|
29
40
|
break;
|
|
30
41
|
|
|
31
42
|
case "setup-overlay":
|
|
@@ -513,10 +524,13 @@ function printUsage() {
|
|
|
513
524
|
sinain — AI overlay system for macOS and Windows
|
|
514
525
|
|
|
515
526
|
Usage:
|
|
527
|
+
sinain onboard Interactive setup wizard (recommended)
|
|
528
|
+
sinain onboard --advanced Full setup with privacy, models, gateway options
|
|
529
|
+
sinain onboard --reset Reset config and start fresh
|
|
516
530
|
sinain start [options] Launch sinain services
|
|
517
531
|
sinain stop Stop all sinain services
|
|
518
532
|
sinain status Check what's running
|
|
519
|
-
sinain setup
|
|
533
|
+
sinain setup (deprecated — use onboard)
|
|
520
534
|
sinain setup-overlay Download pre-built overlay app
|
|
521
535
|
sinain setup-sck-capture Download sck-capture audio binary (macOS)
|
|
522
536
|
sinain export-knowledge Export knowledge for transfer to another machine
|
package/config-shared.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers and step functions for sinain onboard + sinain config.
|
|
3
|
+
* Both commands import from here to avoid duplication.
|
|
4
|
+
*/
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import net from "net";
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
|
|
12
|
+
export const HOME = os.homedir();
|
|
13
|
+
export const SINAIN_DIR = path.join(HOME, ".sinain");
|
|
14
|
+
export const ENV_PATH = path.join(SINAIN_DIR, ".env");
|
|
15
|
+
export const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
16
|
+
export const IS_WINDOWS = os.platform() === "win32";
|
|
17
|
+
export const IS_MAC = os.platform() === "darwin";
|
|
18
|
+
|
|
19
|
+
// ── Colors ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export const c = {
|
|
22
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
23
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
24
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
25
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
26
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export function guard(value) {
|
|
33
|
+
if (p.isCancel(value)) {
|
|
34
|
+
p.cancel("Cancelled.");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cmdExists(cmd) {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync(IS_WINDOWS ? "where" : "which", [cmd], { stdio: "pipe" });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isPortOpen(port) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const sock = new net.Socket();
|
|
52
|
+
sock.setTimeout(800);
|
|
53
|
+
sock.on("connect", () => { sock.destroy(); resolve(true); });
|
|
54
|
+
sock.on("error", () => resolve(false));
|
|
55
|
+
sock.on("timeout", () => { sock.destroy(); resolve(false); });
|
|
56
|
+
sock.connect(port, "127.0.0.1");
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function maskKey(key) {
|
|
61
|
+
if (!key || key.length < 12) return "****";
|
|
62
|
+
return key.slice(0, 8) + "..." + key.slice(-4);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Read existing .env into a key=value map */
|
|
66
|
+
export function readEnv(envPath = ENV_PATH) {
|
|
67
|
+
if (!fs.existsSync(envPath)) return {};
|
|
68
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
69
|
+
const vars = {};
|
|
70
|
+
for (const line of content.split("\n")) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
73
|
+
const eq = trimmed.indexOf("=");
|
|
74
|
+
if (eq === -1) continue;
|
|
75
|
+
const key = trimmed.slice(0, eq).trim();
|
|
76
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
77
|
+
if (val) vars[key] = val;
|
|
78
|
+
}
|
|
79
|
+
return vars;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Write vars to .env, preserving template structure */
|
|
83
|
+
export function writeEnv(vars) {
|
|
84
|
+
fs.mkdirSync(SINAIN_DIR, { recursive: true });
|
|
85
|
+
|
|
86
|
+
const examplePath = path.join(PKG_DIR, ".env.example");
|
|
87
|
+
let template = fs.existsSync(examplePath)
|
|
88
|
+
? fs.readFileSync(examplePath, "utf-8")
|
|
89
|
+
: "";
|
|
90
|
+
|
|
91
|
+
if (template) {
|
|
92
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
93
|
+
const regex = new RegExp(`^#?\\s*${k}=.*$`, "m");
|
|
94
|
+
if (regex.test(template)) {
|
|
95
|
+
template = template.replace(regex, `${k}=${v}`);
|
|
96
|
+
} else {
|
|
97
|
+
template += `\n${k}=${v}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
template = `# Generated by sinain — ${new Date().toISOString()}\n${template}`;
|
|
101
|
+
fs.writeFileSync(ENV_PATH, template);
|
|
102
|
+
} else {
|
|
103
|
+
const lines = [
|
|
104
|
+
"# sinain configuration",
|
|
105
|
+
`# Generated by sinain — ${new Date().toISOString()}`,
|
|
106
|
+
"",
|
|
107
|
+
];
|
|
108
|
+
for (const [k, v] of Object.entries(vars)) lines.push(`${k}=${v}`);
|
|
109
|
+
lines.push("");
|
|
110
|
+
fs.writeFileSync(ENV_PATH, lines.join("\n"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function fetchHealth(port = 9500) {
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000) });
|
|
117
|
+
return await res.json();
|
|
118
|
+
} catch { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Build a config summary for display */
|
|
122
|
+
export function summarizeConfig(existing) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
if (existing.OPENROUTER_API_KEY) lines.push(`API Key: ${maskKey(existing.OPENROUTER_API_KEY)}`);
|
|
125
|
+
if (existing.TRANSCRIPTION_BACKEND) lines.push(`Transcription: ${existing.TRANSCRIPTION_BACKEND}`);
|
|
126
|
+
if (existing.AGENT_MODEL) lines.push(`Model: ${existing.AGENT_MODEL}`);
|
|
127
|
+
if (existing.PRIVACY_MODE) lines.push(`Privacy: ${existing.PRIVACY_MODE}`);
|
|
128
|
+
if (existing.ESCALATION_MODE) lines.push(`Escalation: ${existing.ESCALATION_MODE}`);
|
|
129
|
+
if (existing.OPENCLAW_WS_URL) lines.push(`Gateway: ${existing.OPENCLAW_WS_URL}`);
|
|
130
|
+
if (existing.SINAIN_AGENT) lines.push(`Agent: ${existing.SINAIN_AGENT}`);
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Health check ────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
export async function runHealthCheck() {
|
|
137
|
+
const checks = [];
|
|
138
|
+
|
|
139
|
+
const coreUp = await isPortOpen(9500);
|
|
140
|
+
if (coreUp) {
|
|
141
|
+
checks.push(c.green(" sinain-core: reachable (port 9500)"));
|
|
142
|
+
const health = await fetchHealth();
|
|
143
|
+
if (health) {
|
|
144
|
+
const audioState = health.audio?.state || health.audioPipeline?.state;
|
|
145
|
+
if (audioState === "active" || audioState === "running") {
|
|
146
|
+
checks.push(c.green(" Audio pipeline: active"));
|
|
147
|
+
} else {
|
|
148
|
+
checks.push(c.yellow(" Audio pipeline: inactive — check audio device settings"));
|
|
149
|
+
}
|
|
150
|
+
const screenActive = health.screen?.state === "active" || health.senseActive;
|
|
151
|
+
if (screenActive) {
|
|
152
|
+
checks.push(c.green(" Screen capture: active"));
|
|
153
|
+
} else {
|
|
154
|
+
if (IS_MAC) {
|
|
155
|
+
checks.push(c.yellow(" Screen capture: inactive"));
|
|
156
|
+
checks.push(c.dim(" Grant Screen Recording in System Settings → Privacy & Security"));
|
|
157
|
+
} else {
|
|
158
|
+
checks.push(c.yellow(" Screen capture: inactive"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const gwConnected = health.escalation?.gatewayConnected;
|
|
162
|
+
if (gwConnected) {
|
|
163
|
+
checks.push(c.green(" OpenClaw gateway: connected"));
|
|
164
|
+
} else if (health.escalation?.mode === "off") {
|
|
165
|
+
checks.push(c.dim(" OpenClaw gateway: not configured (standalone mode)"));
|
|
166
|
+
} else {
|
|
167
|
+
checks.push(c.yellow(" OpenClaw gateway: not connected"));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
checks.push(c.dim(" sinain-core: not running"));
|
|
172
|
+
checks.push(c.dim(" Start with: sinain start (or ./start.sh)"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (checks.length > 0) {
|
|
176
|
+
p.note(checks.join("\n"), "System health");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Step functions ──────────────────────────────────────────────────────────
|
|
181
|
+
// label param: optional prefix like "[1/5]" for onboard, omitted for config
|
|
182
|
+
|
|
183
|
+
export async function stepApiKey(existing, label = "OpenRouter API key") {
|
|
184
|
+
const current = existing.OPENROUTER_API_KEY;
|
|
185
|
+
const message = current
|
|
186
|
+
? `${label} ${c.dim(`(current: ${maskKey(current)})`)}`
|
|
187
|
+
: label;
|
|
188
|
+
|
|
189
|
+
p.note(
|
|
190
|
+
`Key is stored in plain text at ${c.dim(ENV_PATH)}.\nDon't commit this file or share it.`,
|
|
191
|
+
"Security",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const key = guard(await p.text({
|
|
195
|
+
message,
|
|
196
|
+
placeholder: current ? "Press Enter to keep current" : "sk-or-v1-...",
|
|
197
|
+
defaultValue: current || "",
|
|
198
|
+
validate: (val) => {
|
|
199
|
+
if (!val && !current) return "API key is required. Get one free at https://openrouter.ai/keys";
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
return key || current;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function stepTranscription(existing, label = "Audio transcription") {
|
|
207
|
+
const current = existing.TRANSCRIPTION_BACKEND || "openrouter";
|
|
208
|
+
const hasWhisper = !IS_WINDOWS && cmdExists("whisper-cli");
|
|
209
|
+
|
|
210
|
+
const choice = guard(await p.select({
|
|
211
|
+
message: label,
|
|
212
|
+
options: [
|
|
213
|
+
{
|
|
214
|
+
value: "openrouter",
|
|
215
|
+
label: "Cloud (OpenRouter)",
|
|
216
|
+
hint: "Uses your OpenRouter key, no setup needed",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
value: "local",
|
|
220
|
+
label: "Local (Whisper)",
|
|
221
|
+
hint: hasWhisper ? "whisper-cli detected on this machine" : "~1.5 GB download, fully private",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
initialValue: current,
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
if (choice === "local" && !hasWhisper && IS_MAC) {
|
|
228
|
+
const install = guard(await p.confirm({
|
|
229
|
+
message: "whisper-cli not found. Install via Homebrew?",
|
|
230
|
+
initialValue: true,
|
|
231
|
+
}));
|
|
232
|
+
if (install) {
|
|
233
|
+
const s = p.spinner();
|
|
234
|
+
s.start("Installing whisper-cpp...");
|
|
235
|
+
try {
|
|
236
|
+
execFileSync("brew", ["install", "whisper-cpp"], { stdio: "pipe" });
|
|
237
|
+
s.stop(c.green("whisper-cpp installed."));
|
|
238
|
+
} catch {
|
|
239
|
+
s.stop(c.yellow("Install failed — falling back to cloud."));
|
|
240
|
+
return "openrouter";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return choice;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function stepGateway(existing, label = "OpenClaw gateway") {
|
|
249
|
+
p.note(
|
|
250
|
+
"The gateway gives sinain a persistent AI agent (Claude/Codex/Goose)\n" +
|
|
251
|
+
"that handles complex tasks, background research, and multi-step actions.\n" +
|
|
252
|
+
"Without it, sinain still works — HUD analysis runs locally via OpenRouter.",
|
|
253
|
+
"What is a gateway?",
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const choice = guard(await p.select({
|
|
257
|
+
message: label,
|
|
258
|
+
options: [
|
|
259
|
+
{
|
|
260
|
+
value: "skip",
|
|
261
|
+
label: "Skip / Disable",
|
|
262
|
+
hint: "HUD works fine without it — add later with sinain config",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
value: "local",
|
|
266
|
+
label: "Local gateway",
|
|
267
|
+
hint: "Install & start openclaw on this machine",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
value: "remote",
|
|
271
|
+
label: "Remote gateway",
|
|
272
|
+
hint: "Connect to existing gateway (URL + token)",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
initialValue: existing.OPENCLAW_WS_URL ? "remote" : "skip",
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
if (choice === "skip") {
|
|
279
|
+
return {
|
|
280
|
+
OPENCLAW_WS_URL: "",
|
|
281
|
+
OPENCLAW_HTTP_URL: "",
|
|
282
|
+
OPENCLAW_WS_TOKEN: "",
|
|
283
|
+
OPENCLAW_HTTP_TOKEN: "",
|
|
284
|
+
ESCALATION_MODE: "off",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (choice === "local") {
|
|
289
|
+
return await setupLocalGateway(existing);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// remote
|
|
293
|
+
const wsUrl = guard(await p.text({
|
|
294
|
+
message: "Gateway WebSocket URL",
|
|
295
|
+
placeholder: "Example: ws://192.168.1.100:18789",
|
|
296
|
+
defaultValue: existing.OPENCLAW_WS_URL || "",
|
|
297
|
+
validate: (val) => {
|
|
298
|
+
if (!val) return "URL is required";
|
|
299
|
+
if (!val.startsWith("ws://") && !val.startsWith("wss://")) return "Must start with ws:// or wss://";
|
|
300
|
+
},
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const token = guard(await p.text({
|
|
304
|
+
message: "Gateway auth token (48-char hex)",
|
|
305
|
+
placeholder: existing.OPENCLAW_WS_TOKEN ? "Press Enter to keep current" : "Paste token here",
|
|
306
|
+
defaultValue: existing.OPENCLAW_WS_TOKEN || "",
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const s = p.spinner();
|
|
310
|
+
s.start("Testing gateway connection...");
|
|
311
|
+
try {
|
|
312
|
+
const parsed = new URL(wsUrl);
|
|
313
|
+
const port = parseInt(parsed.port || "18789");
|
|
314
|
+
const reachable = await isPortOpen(port);
|
|
315
|
+
if (reachable) {
|
|
316
|
+
s.stop(c.green("Gateway reachable."));
|
|
317
|
+
} else {
|
|
318
|
+
s.stop(c.yellow("Gateway not reachable — config saved anyway."));
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
s.stop(c.yellow("Could not parse URL — config saved anyway."));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const httpUrl = wsUrl.replace(/^ws/, "http") + "/hooks/agent";
|
|
325
|
+
return {
|
|
326
|
+
OPENCLAW_WS_URL: wsUrl,
|
|
327
|
+
OPENCLAW_HTTP_URL: httpUrl,
|
|
328
|
+
OPENCLAW_WS_TOKEN: token,
|
|
329
|
+
OPENCLAW_HTTP_TOKEN: token,
|
|
330
|
+
OPENCLAW_SESSION_KEY: "agent:main:sinain",
|
|
331
|
+
ESCALATION_MODE: "rich",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function setupLocalGateway(existing) {
|
|
336
|
+
const hasOpenclaw = cmdExists("openclaw");
|
|
337
|
+
|
|
338
|
+
if (!hasOpenclaw) {
|
|
339
|
+
const install = guard(await p.confirm({
|
|
340
|
+
message: "openclaw CLI not found. Install globally via npm?",
|
|
341
|
+
initialValue: true,
|
|
342
|
+
}));
|
|
343
|
+
if (install) {
|
|
344
|
+
const s = p.spinner();
|
|
345
|
+
s.start("Installing openclaw...");
|
|
346
|
+
try {
|
|
347
|
+
execFileSync("npm", ["install", "-g", "openclaw@latest"], { stdio: "pipe", timeout: 120_000 });
|
|
348
|
+
s.stop(c.green("openclaw installed."));
|
|
349
|
+
} catch {
|
|
350
|
+
s.stop(c.red("Installation failed."));
|
|
351
|
+
p.note(
|
|
352
|
+
"Install manually: npm install -g openclaw\nThen re-run setup.",
|
|
353
|
+
"Manual install",
|
|
354
|
+
);
|
|
355
|
+
return { OPENCLAW_WS_URL: "", OPENCLAW_HTTP_URL: "", ESCALATION_MODE: "off" };
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
return { OPENCLAW_WS_URL: "", OPENCLAW_HTTP_URL: "", ESCALATION_MODE: "off" };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const s = p.spinner();
|
|
363
|
+
s.start("Checking gateway...");
|
|
364
|
+
const gatewayUp = await isPortOpen(18789);
|
|
365
|
+
|
|
366
|
+
const ocJsonPath = path.join(HOME, ".openclaw/openclaw.json");
|
|
367
|
+
let token = "";
|
|
368
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
369
|
+
try {
|
|
370
|
+
const ocConfig = JSON.parse(fs.readFileSync(ocJsonPath, "utf-8"));
|
|
371
|
+
token = ocConfig.gateway?.auth?.token || "";
|
|
372
|
+
} catch { /* ignore */ }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (gatewayUp) {
|
|
376
|
+
s.stop(c.green("Gateway already running (port 18789)."));
|
|
377
|
+
} else {
|
|
378
|
+
s.stop(c.yellow("Gateway not running."));
|
|
379
|
+
p.note("Start your gateway:\n openclaw gateway run\n or: openclaw onboard --install-daemon", "Next step");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
OPENCLAW_WS_URL: "ws://localhost:18789",
|
|
384
|
+
OPENCLAW_HTTP_URL: "http://localhost:18789/hooks/agent",
|
|
385
|
+
OPENCLAW_WS_TOKEN: token,
|
|
386
|
+
OPENCLAW_HTTP_TOKEN: token,
|
|
387
|
+
OPENCLAW_SESSION_KEY: "agent:main:sinain",
|
|
388
|
+
ESCALATION_MODE: "rich",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function stepPrivacy(existing, label = "Privacy mode") {
|
|
393
|
+
const current = existing.PRIVACY_MODE || "standard";
|
|
394
|
+
return guard(await p.select({
|
|
395
|
+
message: label,
|
|
396
|
+
options: [
|
|
397
|
+
{
|
|
398
|
+
value: "off",
|
|
399
|
+
label: "Off",
|
|
400
|
+
hint: "No filtering — screen text, credentials, everything sent to cloud",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
value: "standard",
|
|
404
|
+
label: "Standard",
|
|
405
|
+
hint: "Auto-redacts cards, API keys, tokens before sending to cloud",
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
value: "strict",
|
|
409
|
+
label: "Strict",
|
|
410
|
+
hint: "Only summaries leave your machine, no raw screen text or audio",
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
value: "paranoid",
|
|
414
|
+
label: "Paranoid",
|
|
415
|
+
hint: "Zero cloud calls — needs Whisper + Ollama installed or nothing works",
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
initialValue: current,
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function stepModel(existing, label = "AI model for HUD analysis") {
|
|
423
|
+
const current = existing.AGENT_MODEL || "google/gemini-2.5-flash-lite";
|
|
424
|
+
const knownModels = [
|
|
425
|
+
"google/gemini-2.5-flash-lite",
|
|
426
|
+
"google/gemini-2.5-flash",
|
|
427
|
+
"anthropic/claude-3.5-haiku",
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const choice = guard(await p.select({
|
|
431
|
+
message: label,
|
|
432
|
+
options: [
|
|
433
|
+
{ value: "google/gemini-2.5-flash-lite", label: "gemini-2.5-flash-lite", hint: "Fastest, cheapest — recommended" },
|
|
434
|
+
{ value: "google/gemini-2.5-flash", label: "gemini-2.5-flash", hint: "Better quality, slightly more expensive" },
|
|
435
|
+
{ value: "anthropic/claude-3.5-haiku", label: "claude-3.5-haiku", hint: "Anthropic, fast and affordable" },
|
|
436
|
+
{ value: "custom", label: "Custom", hint: "Enter any model ID from openrouter.ai/models" },
|
|
437
|
+
],
|
|
438
|
+
initialValue: knownModels.includes(current) ? current : "custom",
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
if (choice === "custom") {
|
|
442
|
+
return guard(await p.text({
|
|
443
|
+
message: "Model ID (from openrouter.ai/models)",
|
|
444
|
+
placeholder: "provider/model-name",
|
|
445
|
+
defaultValue: knownModels.includes(current) ? "" : current,
|
|
446
|
+
validate: (val) => {
|
|
447
|
+
if (!val) return "Model ID is required";
|
|
448
|
+
if (!val.includes("/")) return "Format: provider/model-name";
|
|
449
|
+
},
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return choice;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function stepAgent(existing, label = "Bare agent") {
|
|
457
|
+
const current = existing.SINAIN_AGENT || "claude";
|
|
458
|
+
return guard(await p.select({
|
|
459
|
+
message: label,
|
|
460
|
+
options: [
|
|
461
|
+
{ value: "claude", label: "Claude Code", hint: "Calls sinain tools directly — recommended" },
|
|
462
|
+
{ value: "codex", label: "Codex", hint: "Calls sinain tools directly" },
|
|
463
|
+
{ value: "goose", label: "Goose", hint: "Calls sinain tools directly" },
|
|
464
|
+
{ value: "junie", label: "Junie", hint: "JetBrains IDE agent" },
|
|
465
|
+
{ value: "aider", label: "Aider", hint: "Receives text only, no tool access" },
|
|
466
|
+
],
|
|
467
|
+
initialValue: current,
|
|
468
|
+
}));
|
|
469
|
+
}
|