@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 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
- # ── Required ─────────────────────────────────────────────────────────────────
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 git-backed via `git_backup.sh` on every heartbeat tick. New instances restore instantly via `SINAIN_BACKUP_REPO`.
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 (git backup, signal analysis, insight synthesis, log writing). Returns structured JSON with results, recommended actions, and Telegram output. |
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. `bash sinain-memory/git_backup.sh` (30s timeout)
36
- 2. `uv run python3 sinain-memory/signal_analyzer.py` (60s timeout)
37
- 3. `uv run python3 sinain-memory/insight_synthesizer.py` (60s timeout)
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
- await runSetupWizard();
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 Run interactive setup wizard (~/.sinain/.env)
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
@@ -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
+ }