@geravant/sinain 1.13.0 → 1.15.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.
Files changed (78) hide show
  1. package/.env.example +33 -27
  2. package/cli.js +30 -14
  3. package/config-shared.js +173 -30
  4. package/launcher.js +38 -21
  5. package/onboard.js +36 -20
  6. package/package.json +4 -1
  7. package/sinain-agent/run.sh +600 -127
  8. package/sinain-core/src/agents-loader.ts +254 -0
  9. package/sinain-core/src/buffers/feed-buffer.ts +6 -4
  10. package/sinain-core/src/config.ts +77 -15
  11. package/sinain-core/src/escalation/escalator.ts +178 -18
  12. package/sinain-core/src/index.ts +218 -31
  13. package/sinain-core/src/learning/local-curation.ts +81 -27
  14. package/sinain-core/src/overlay/commands.ts +25 -0
  15. package/sinain-core/src/overlay/ws-handler.ts +3 -0
  16. package/sinain-core/src/server.ts +101 -10
  17. package/sinain-core/src/types.ts +29 -3
  18. package/sinain-memory/graph_query.py +12 -3
  19. package/sinain-memory/knowledge_integrator.py +194 -10
  20. package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
  21. package/sinain-memory/__pycache__/embed_client.cpython-312.pyc +0 -0
  22. package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
  23. package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
  24. package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
  25. package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
  26. package/sinain-memory/eval/__init__.py +0 -0
  27. package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
  28. package/sinain-memory/eval/assertions.py +0 -267
  29. package/sinain-memory/eval/benchmarks/__init__.py +0 -0
  30. package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
  32. package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
  33. package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
  34. package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
  35. package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
  36. package/sinain-memory/eval/benchmarks/__pycache__/meeting_adapter.cpython-312.pyc +0 -0
  37. package/sinain-memory/eval/benchmarks/__pycache__/meeting_runner.cpython-312.pyc +0 -0
  38. package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
  39. package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
  40. package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
  41. package/sinain-memory/eval/benchmarks/base_adapter.py +0 -43
  42. package/sinain-memory/eval/benchmarks/config.py +0 -23
  43. package/sinain-memory/eval/benchmarks/evaluate.py +0 -146
  44. package/sinain-memory/eval/benchmarks/ingest.py +0 -152
  45. package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
  46. package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
  47. package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
  48. package/sinain-memory/eval/benchmarks/judges/qa_judge.py +0 -81
  49. package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +0 -177
  50. package/sinain-memory/eval/benchmarks/meeting_adapter.py +0 -81
  51. package/sinain-memory/eval/benchmarks/meeting_runner.py +0 -230
  52. package/sinain-memory/eval/benchmarks/query.py +0 -193
  53. package/sinain-memory/eval/benchmarks/report.py +0 -87
  54. package/sinain-memory/eval/benchmarks/run_meeting_bench.sh +0 -318
  55. package/sinain-memory/eval/benchmarks/runner.py +0 -283
  56. package/sinain-memory/eval/judges/__init__.py +0 -0
  57. package/sinain-memory/eval/judges/base_judge.py +0 -61
  58. package/sinain-memory/eval/judges/curation_judge.py +0 -46
  59. package/sinain-memory/eval/judges/insight_judge.py +0 -48
  60. package/sinain-memory/eval/judges/mining_judge.py +0 -42
  61. package/sinain-memory/eval/judges/signal_judge.py +0 -45
  62. package/sinain-memory/eval/retrieval_benchmark.jsonl +0 -12
  63. package/sinain-memory/eval/retrieval_evaluator.py +0 -186
  64. package/sinain-memory/eval/schemas.py +0 -247
  65. package/sinain-memory/tests/__init__.py +0 -0
  66. package/sinain-memory/tests/conftest.py +0 -189
  67. package/sinain-memory/tests/test_curator_helpers.py +0 -94
  68. package/sinain-memory/tests/test_embedder.py +0 -210
  69. package/sinain-memory/tests/test_extract_json.py +0 -124
  70. package/sinain-memory/tests/test_feedback_computation.py +0 -121
  71. package/sinain-memory/tests/test_miner_helpers.py +0 -71
  72. package/sinain-memory/tests/test_module_management.py +0 -458
  73. package/sinain-memory/tests/test_parsers.py +0 -96
  74. package/sinain-memory/tests/test_tick_evaluator.py +0 -430
  75. package/sinain-memory/tests/test_triple_extractor.py +0 -255
  76. package/sinain-memory/tests/test_triple_ingest.py +0 -191
  77. package/sinain-memory/tests/test_triple_migrate.py +0 -138
  78. package/sinain-memory/tests/test_triplestore.py +0 -248
package/.env.example CHANGED
@@ -22,30 +22,34 @@ PRIVACY_MODE=standard # off | standard | strict | paranoid
22
22
  # strict: only summaries leave your machine
23
23
  # paranoid: almost nothing leaves your machine
24
24
 
25
- # ── Agent ────────────────────────────────────────────────────────────────────
26
- SINAIN_AGENT=claude # claude | codex | junie | goose | aider | <custom command>
27
- # MCP agents (claude, codex, junie, goose) call sinain tools directly
28
- # Pipe agents (aider, custom) receive escalation text on stdin
25
+ # ── Agent + OpenClaw config → sinain-agent/agents.json ──────────────────────
26
+ # All bare-agent and OpenClaw gateway config (default agent, allowed-tools
27
+ # whitelists, max turns, poll interval, escalation mode/transport, gateway
28
+ # URLs/timeouts) lives in sinain-agent/agents.json. See agents.example.json
29
+ # in that directory for the full schema and per-profile overrides.
30
+ #
31
+ # .env still works as a fallback for individual values during migration —
32
+ # uncomment any of the lines below to override agents.json on this host:
33
+ # SINAIN_AGENT=openclaude # → agents.json `default`
34
+ # SINAIN_POLL_INTERVAL=5 # → agents.json `pollIntervalSec`
35
+ # SINAIN_AGENT_MAX_TURNS=8 # → agents.json `agentMaxTurns`
36
+ # SINAIN_SPAWN_MAX_TURNS=25 # → agents.json `spawnMaxTurns`
37
+ # SINAIN_ALLOWED_TOOLS=mcp__sinain # → agents.json `allowedTools`
38
+ # SINAIN_ESC_ALLOWED_TOOLS=... # → agents.json `escAllowedTools`
39
+ # SINAIN_SPAWN_ALLOWED_TOOLS=... # → agents.json `spawnAllowedTools`
40
+ # SINAIN_AUTO_APPROVE_TOOLS=... # → agents.json `autoApproveTools`
41
+ # AGENT_DEBOUNCE_MS=6000 # → agents.json `analyzer.debounceMs`
42
+ # AGENT_MAX_INTERVAL_MS=60000 # → agents.json `analyzer.maxIntervalMs`
43
+ # ESCALATION_MODE=rich # → agents.json `escalation.mode`
44
+ # ESCALATION_COOLDOWN_MS=30000 # → agents.json `escalation.cooldownMs`
45
+ # ESCALATION_TRANSPORT was REMOVED — agent identity (openclaw vs local) is
46
+ # the transport now. Pick "openclaw" in the overlay selector for WS dispatch.
47
+
29
48
  SINAIN_CORE_URL=http://localhost:9500
30
- SINAIN_POLL_INTERVAL=5 # seconds between escalation polls
31
- SINAIN_HEARTBEAT_INTERVAL=900 # seconds between heartbeat ticks (15 min)
32
49
  SINAIN_WORKSPACE=~/.openclaw/workspace # knowledge files, curation scripts, playbook
33
- # SINAIN_ALLOWED_TOOLS=mcp__sinain # MCP tools auto-approved for bare agent
34
- SINAIN_AGENT_MAX_TURNS=5 # max tool-use turns for escalation responses
35
- SINAIN_SPAWN_MAX_TURNS=25 # max tool-use turns for spawn tasks (Shift+Enter)
36
- # auto-derived from mcp-config.json if unset
37
- # format: mcp__<server> (all) | mcp__<server>__<tool> (specific)
38
-
39
- # ── Escalation ───────────────────────────────────────────────────────────────
40
- ESCALATION_MODE=rich # off | selective | focus | rich
41
- # off: no escalation
42
- # selective: score-based (errors, questions trigger it)
43
- # focus: always escalate every tick
44
- # rich: always escalate with maximum context
45
- # ESCALATION_COOLDOWN_MS=10000
46
- # ESCALATION_TRANSPORT=auto # ws | http | auto
47
- # auto = WS when gateway connected, HTTP fallback
48
- # http = bare agent only (no gateway)
50
+ # Heartbeat (signal/insight/memory/playbook) now runs natively in sinain-core's
51
+ # LocalCurationService every 30 min — no bare-agent heartbeat configuration needed.
52
+ SINAIN_HEARTBEAT_INTERVAL=900
49
53
 
50
54
  # ── Server ───────────────────────────────────────────────────────────────────
51
55
  PORT=9500
@@ -88,15 +92,17 @@ TRANSCRIPTION_LANGUAGE=en-US
88
92
  # ── OpenClaw / NemoClaw Gateway ──────────────────────────────────────────────
89
93
  # Leave blank to run without a gateway (bare agent mode).
90
94
  # The setup wizard fills these in if you have an OpenClaw gateway.
91
- OPENCLAW_WS_URL=ws://localhost:18789
95
+ # Gateway URLs + session moved to agents.json (`profiles.openclaw` block).
96
+ # Tokens stay here as secrets and are referenced from agents.json via
97
+ # ${OPENCLAW_WS_TOKEN} / ${OPENCLAW_HTTP_TOKEN} indirection.
92
98
  OPENCLAW_WS_TOKEN= # 48-char hex — from gateway config
93
- OPENCLAW_HTTP_URL=http://localhost:18789/hooks/agent
94
99
  OPENCLAW_HTTP_TOKEN= # same token as WS_TOKEN
95
- OPENCLAW_SESSION_KEY=agent:main:sinain
100
+ # Override URLs/timeouts here too if you don't want to edit agents.json:
101
+ # OPENCLAW_WS_URL=ws://localhost:18789
102
+ # OPENCLAW_HTTP_URL=http://localhost:18789/hooks/agent
103
+ # OPENCLAW_SESSION_KEY=agent:main:sinain
96
104
  # OPENCLAW_PHASE1_TIMEOUT_MS=10000
97
105
  # OPENCLAW_PHASE2_TIMEOUT_MS=120000
98
- # OPENCLAW_QUEUE_TTL_MS=300000
99
- # OPENCLAW_QUEUE_MAX_SIZE=10
100
106
  # OPENCLAW_PING_INTERVAL_MS=30000
101
107
 
102
108
  # ── SITUATION.md ─────────────────────────────────────────────────────────────
package/cli.js CHANGED
@@ -4,6 +4,7 @@ import net from "net";
4
4
  import os from "os";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
+ import { writeAgentsConfig } from "./config-shared.js";
7
8
 
8
9
  const cmd = process.argv[2];
9
10
  const IS_WINDOWS = os.platform() === "win32";
@@ -168,34 +169,46 @@ async function runSetupWizard() {
168
169
  if (key.trim()) vars.OPENROUTER_API_KEY = key.trim();
169
170
  }
170
171
 
171
- // Agent
172
- const agent = await ask(` Agent? [${BOLD}claude${RESET}/codex/goose/junie/aider]: `);
173
- vars.SINAIN_AGENT = agent.trim().toLowerCase() || "claude";
172
+ // Agent + escalation + gateway → agents.json (not .env). Tokens stay in
173
+ // .env as secrets, referenced from agents.json via ${VAR} indirection.
174
+ const agentsPatch = {};
175
+
176
+ // Default agent — overlay's chip selector lets the user switch at runtime;
177
+ // this just sets the boot-time default.
178
+ const agent = await ask(` Default agent? [${BOLD}claude${RESET}/openclaude/codex/goose/junie/aider]: `);
179
+ agentsPatch.default = agent.trim().toLowerCase() || "claude";
174
180
 
175
- // Escalation
181
+ // Escalation mode
176
182
  console.log(`\n ${DIM}Escalation: off | selective | focus | rich${RESET}`);
177
183
  const esc = await ask(` Escalation mode? [${BOLD}selective${RESET}]: `);
178
- vars.ESCALATION_MODE = esc.trim().toLowerCase() || "selective";
184
+ agentsPatch.escalationMode = esc.trim().toLowerCase() || "selective";
179
185
 
180
- // Gateway
186
+ // Gateway — when enabled, writes the openclaw profile to agents.json
187
+ // (URLs + session) and tokens to .env. There's no transport choice
188
+ // anymore; picking openclaw vs a local agent in the overlay determines
189
+ // dispatch (WS vs HTTP).
181
190
  const gw = await ask(` OpenClaw gateway? [y/N]: `);
182
191
  if (gw.trim().toLowerCase() === "y") {
183
192
  const url = await ask(` Gateway WS URL [ws://localhost:18789]: `);
184
- vars.OPENCLAW_WS_URL = url.trim() || "ws://localhost:18789";
193
+ const wsUrl = url.trim() || "ws://localhost:18789";
185
194
  const token = await ask(` Auth token (48-char hex): `);
186
195
  if (token.trim()) {
187
196
  vars.OPENCLAW_WS_TOKEN = token.trim();
188
197
  vars.OPENCLAW_HTTP_TOKEN = token.trim();
189
198
  }
190
- vars.OPENCLAW_HTTP_URL = vars.OPENCLAW_WS_URL.replace(/^ws/, "http") + "/hooks/agent";
191
- vars.OPENCLAW_SESSION_KEY = "agent:main:sinain";
199
+ agentsPatch.openclawProfile = {
200
+ wsUrl,
201
+ httpUrl: wsUrl.replace(/^ws/, "http") + "/hooks/agent",
202
+ wsToken: "${OPENCLAW_WS_TOKEN}",
203
+ httpToken: "${OPENCLAW_HTTP_TOKEN}",
204
+ sessionKey: "agent:main:sinain",
205
+ };
192
206
  } else {
193
- // No gateway disable WS connection attempts
194
- vars.OPENCLAW_WS_URL = "";
195
- vars.OPENCLAW_HTTP_URL = "";
207
+ // Skip / disable: drop the openclaw profile entirely.
208
+ agentsPatch.openclawProfile = null;
209
+ agentsPatch.escalationMode = "off";
196
210
  }
197
211
 
198
- vars.SINAIN_POLL_INTERVAL = "5";
199
212
  vars.SINAIN_HEARTBEAT_INTERVAL = "900";
200
213
  vars.PRIVACY_MODE = "standard";
201
214
 
@@ -230,8 +243,11 @@ async function runSetupWizard() {
230
243
  fs.writeFileSync(envPath, lines.join("\n"));
231
244
  }
232
245
 
246
+ // Patch ~/.sinain/agents.json with the wizard's agent + gateway choices.
247
+ writeAgentsConfig(agentsPatch);
248
+
233
249
  rl.close();
234
- console.log(`\n ${GREEN}✓${RESET} Config written to ${envPath}\n`);
250
+ console.log(`\n ${GREEN}✓${RESET} Config written to ${envPath} + ~/.sinain/agents.json\n`);
235
251
  }
236
252
 
237
253
  // ── Stop ──────────────────────────────────────────────────────────────────────
package/config-shared.js CHANGED
@@ -16,6 +16,16 @@ export const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
16
16
  export const IS_WINDOWS = os.platform() === "win32";
17
17
  export const IS_MAC = os.platform() === "darwin";
18
18
 
19
+ // Wizard write target for agents.json. Lives in user home so npm-installed
20
+ // users (whose package dir is often read-only) get a writable path.
21
+ // sinain-agent/run.sh and sinain-core/agents-loader.ts both check this
22
+ // path with the highest priority after AGENTS_CONFIG_PATH env override.
23
+ export const AGENTS_JSON_PATH = path.join(SINAIN_DIR, "agents.json");
24
+ // agents.example.json ships inside the package as the bootstrap template
25
+ // for first-run wizard writes (and as the fallback when no agents.json
26
+ // exists yet on this host).
27
+ export const AGENTS_EXAMPLE_PATH = path.join(PKG_DIR, "sinain-agent", "agents.example.json");
28
+
19
29
  // ── Colors ──────────────────────────────────────────────────────────────────
20
30
 
21
31
  export const c = {
@@ -111,6 +121,74 @@ export function writeEnv(vars) {
111
121
  }
112
122
  }
113
123
 
124
+ /** Read agents.json (the wizard's authoritative agent + gateway config).
125
+ *
126
+ * Resolution order mirrors agents-loader.ts and run.sh:
127
+ * 1. ~/.sinain/agents.json (the wizard write target)
128
+ * 2. <pkg>/sinain-agent/agents.example.json (bootstrap template fallback)
129
+ *
130
+ * Returns an empty object if neither file exists or parsing fails — callers
131
+ * treat null/missing fields as "use schema defaults".
132
+ */
133
+ export function readAgentsConfig() {
134
+ for (const p of [AGENTS_JSON_PATH, AGENTS_EXAMPLE_PATH]) {
135
+ if (!fs.existsSync(p)) continue;
136
+ try {
137
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
138
+ } catch (err) {
139
+ // Bad JSON — surface, but don't crash the wizard
140
+ console.warn(`[config] failed to parse ${p}: ${err.message}`);
141
+ }
142
+ }
143
+ return {};
144
+ }
145
+
146
+ /** Patch ~/.sinain/agents.json with the given updates.
147
+ *
148
+ * `patch` shape (all optional):
149
+ * - default: string — top-level default agent name
150
+ * - escalationMode: string — sets escalation.mode
151
+ * - openclawProfile: object | null — sets profiles.openclaw
152
+ * (null deletes the openclaw profile entirely,
153
+ * which disables the gateway path)
154
+ *
155
+ * Reads the existing config first (or falls back to the shipped example),
156
+ * applies the patch, writes pretty-printed JSON to AGENTS_JSON_PATH.
157
+ * The example file is the "schema source of truth" — patches preserve any
158
+ * other fields the user has customized (custom profiles, allowedTools,
159
+ * analyzer pacing, etc.).
160
+ */
161
+ export function writeAgentsConfig(patch) {
162
+ fs.mkdirSync(SINAIN_DIR, { recursive: true });
163
+ // Start from existing user config if present, else the example template
164
+ // (so first-run writes get the full schema, not just the patched fields).
165
+ let cfg;
166
+ if (fs.existsSync(AGENTS_JSON_PATH)) {
167
+ cfg = JSON.parse(fs.readFileSync(AGENTS_JSON_PATH, "utf-8"));
168
+ } else if (fs.existsSync(AGENTS_EXAMPLE_PATH)) {
169
+ cfg = JSON.parse(fs.readFileSync(AGENTS_EXAMPLE_PATH, "utf-8"));
170
+ } else {
171
+ cfg = { profiles: {} };
172
+ }
173
+
174
+ if (patch.default !== undefined) {
175
+ cfg.default = patch.default;
176
+ }
177
+ if (patch.escalationMode !== undefined) {
178
+ cfg.escalation = cfg.escalation || {};
179
+ cfg.escalation.mode = patch.escalationMode;
180
+ }
181
+ if (patch.openclawProfile === null) {
182
+ // Explicit removal: user picked "skip / no gateway"
183
+ if (cfg.profiles?.openclaw) delete cfg.profiles.openclaw;
184
+ } else if (patch.openclawProfile) {
185
+ cfg.profiles = cfg.profiles || {};
186
+ cfg.profiles.openclaw = { type: "openclaw", ...patch.openclawProfile };
187
+ }
188
+
189
+ fs.writeFileSync(AGENTS_JSON_PATH, JSON.stringify(cfg, null, 2) + "\n");
190
+ }
191
+
114
192
  export async function fetchHealth(port = 9500) {
115
193
  try {
116
194
  const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000) });
@@ -118,16 +196,28 @@ export async function fetchHealth(port = 9500) {
118
196
  } catch { return null; }
119
197
  }
120
198
 
121
- /** Build a config summary for display */
199
+ /** Build a config summary for display.
200
+ * Reads BOTH .env (existing arg) AND ~/.sinain/agents.json — agent + gateway
201
+ * config moved out of .env into agents.json after the migration, so we
202
+ * have to consult both to render an accurate snapshot. */
122
203
  export function summarizeConfig(existing) {
123
204
  const lines = [];
205
+ const agentsCfg = readAgentsConfig();
206
+ const openclaw = agentsCfg?.profiles?.openclaw;
207
+
124
208
  if (existing.OPENROUTER_API_KEY) lines.push(`API Key: ${maskKey(existing.OPENROUTER_API_KEY)}`);
125
209
  if (existing.TRANSCRIPTION_BACKEND) lines.push(`Transcription: ${existing.TRANSCRIPTION_BACKEND}`);
126
210
  if (existing.AGENT_MODEL) lines.push(`Model: ${existing.AGENT_MODEL}`);
127
211
  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}`);
212
+
213
+ // Escalation mode + agent default + gateway URL all live in agents.json
214
+ // now (with .env as a fallback override during the migration window).
215
+ const escMode = agentsCfg?.escalation?.mode || existing.ESCALATION_MODE;
216
+ if (escMode) lines.push(`Escalation: ${escMode}`);
217
+ const gatewayUrl = openclaw?.wsUrl || existing.OPENCLAW_WS_URL;
218
+ if (gatewayUrl) lines.push(`Gateway: ${gatewayUrl}`);
219
+ const defaultAgent = agentsCfg?.default || existing.SINAIN_AGENT;
220
+ if (defaultAgent) lines.push(`Agent: ${defaultAgent}`);
131
221
  return lines;
132
222
  }
133
223
 
@@ -245,12 +335,40 @@ export async function stepTranscription(existing, label = "Audio transcription")
245
335
  return choice;
246
336
  }
247
337
 
338
+ /**
339
+ * Gateway setup step.
340
+ *
341
+ * Returns `{ envVars, agentsPatch }`:
342
+ * - envVars: tokens (OPENCLAW_WS_TOKEN, OPENCLAW_HTTP_TOKEN) — stay in
343
+ * .env as secrets, referenced from agents.json via ${VAR} indirection
344
+ * - agentsPatch: feed to writeAgentsConfig(); writes the openclaw profile
345
+ * (urls + session) and escalation.mode into ~/.sinain/agents.json.
346
+ *
347
+ * The user picks whether to wire up the gateway-style profile at all.
348
+ * The dispatch decision (which lane uses WS vs HTTP) is made later via
349
+ * the overlay's per-lane agent selector — there's no transport question
350
+ * here anymore; agent identity IS the transport.
351
+ *
352
+ * Pre-existing gateway config is read from agents.json first, then .env
353
+ * as a backwards-compat fallback for users migrating from before the
354
+ * profile refactor.
355
+ */
248
356
  export async function stepGateway(existing, label = "OpenClaw gateway") {
357
+ // Read existing gateway config from BOTH places — wizard might be
358
+ // running mid-migration where some users still have OPENCLAW_WS_URL in
359
+ // .env but agents.json's openclaw profile is absent.
360
+ const agentsCfg = readAgentsConfig();
361
+ const oc = agentsCfg?.profiles?.openclaw;
362
+ const existingWsUrl = oc?.wsUrl || existing.OPENCLAW_WS_URL || "";
363
+
249
364
  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?",
365
+ "The gateway gives sinain a persistent agent backend (OpenClaw/NemoClaw)\n" +
366
+ "that handles escalations and background tasks via WS RPC. Without it,\n" +
367
+ "the local bare agent (claude, openclaude, etc.) handles everything via\n" +
368
+ "the HTTP path.\n\n" +
369
+ "You can pick which agent handles each lane (escalation/spawn) at runtime\n" +
370
+ "via the overlay's flash-icon selector.",
371
+ "About the gateway",
254
372
  );
255
373
 
256
374
  const choice = guard(await p.select({
@@ -259,7 +377,7 @@ export async function stepGateway(existing, label = "OpenClaw gateway") {
259
377
  {
260
378
  value: "skip",
261
379
  label: "Skip / Disable",
262
- hint: "HUD works fine without it — add later with sinain config",
380
+ hint: "Local agents only — add a gateway later with `sinain config`",
263
381
  },
264
382
  {
265
383
  value: "local",
@@ -272,16 +390,15 @@ export async function stepGateway(existing, label = "OpenClaw gateway") {
272
390
  hint: "Connect to existing gateway (URL + token)",
273
391
  },
274
392
  ],
275
- initialValue: existing.OPENCLAW_WS_URL ? "remote" : "skip",
393
+ initialValue: existingWsUrl ? "remote" : "skip",
276
394
  }));
277
395
 
278
396
  if (choice === "skip") {
397
+ // Drop the openclaw profile from agents.json entirely. Tokens cleared
398
+ // in .env so they don't linger as misleading secrets.
279
399
  return {
280
- OPENCLAW_WS_URL: "",
281
- OPENCLAW_HTTP_URL: "",
282
- OPENCLAW_WS_TOKEN: "",
283
- OPENCLAW_HTTP_TOKEN: "",
284
- ESCALATION_MODE: "off",
400
+ envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
401
+ agentsPatch: { escalationMode: "off", openclawProfile: null },
285
402
  };
286
403
  }
287
404
 
@@ -293,7 +410,7 @@ export async function stepGateway(existing, label = "OpenClaw gateway") {
293
410
  const wsUrl = guard(await p.text({
294
411
  message: "Gateway WebSocket URL",
295
412
  placeholder: "Example: ws://192.168.1.100:18789",
296
- defaultValue: existing.OPENCLAW_WS_URL || "",
413
+ defaultValue: existingWsUrl,
297
414
  validate: (val) => {
298
415
  if (!val) return "URL is required";
299
416
  if (!val.startsWith("ws://") && !val.startsWith("wss://")) return "Must start with ws:// or wss://";
@@ -322,13 +439,24 @@ export async function stepGateway(existing, label = "OpenClaw gateway") {
322
439
  }
323
440
 
324
441
  const httpUrl = wsUrl.replace(/^ws/, "http") + "/hooks/agent";
442
+ // Tokens → .env; URLs + session → agents.json. The agents.json profile
443
+ // references the env tokens via ${OPENCLAW_WS_TOKEN}/${OPENCLAW_HTTP_TOKEN}
444
+ // indirection (resolved by sinain-core's agents-loader at startup).
325
445
  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",
446
+ envVars: {
447
+ OPENCLAW_WS_TOKEN: token,
448
+ OPENCLAW_HTTP_TOKEN: token,
449
+ },
450
+ agentsPatch: {
451
+ escalationMode: "rich",
452
+ openclawProfile: {
453
+ wsUrl,
454
+ httpUrl,
455
+ wsToken: "${OPENCLAW_WS_TOKEN}",
456
+ httpToken: "${OPENCLAW_HTTP_TOKEN}",
457
+ sessionKey: "agent:main:sinain",
458
+ },
459
+ },
332
460
  };
333
461
  }
334
462
 
@@ -352,10 +480,16 @@ async function setupLocalGateway(existing) {
352
480
  "Install manually: npm install -g openclaw\nThen re-run setup.",
353
481
  "Manual install",
354
482
  );
355
- return { OPENCLAW_WS_URL: "", OPENCLAW_HTTP_URL: "", ESCALATION_MODE: "off" };
483
+ return {
484
+ envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
485
+ agentsPatch: { escalationMode: "off", openclawProfile: null },
486
+ };
356
487
  }
357
488
  } else {
358
- return { OPENCLAW_WS_URL: "", OPENCLAW_HTTP_URL: "", ESCALATION_MODE: "off" };
489
+ return {
490
+ envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
491
+ agentsPatch: { escalationMode: "off", openclawProfile: null },
492
+ };
359
493
  }
360
494
  }
361
495
 
@@ -380,12 +514,20 @@ async function setupLocalGateway(existing) {
380
514
  }
381
515
 
382
516
  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",
517
+ envVars: {
518
+ OPENCLAW_WS_TOKEN: token,
519
+ OPENCLAW_HTTP_TOKEN: token,
520
+ },
521
+ agentsPatch: {
522
+ escalationMode: "rich",
523
+ openclawProfile: {
524
+ wsUrl: "ws://localhost:18789",
525
+ httpUrl: "http://localhost:18789/hooks/agent",
526
+ wsToken: "${OPENCLAW_WS_TOKEN}",
527
+ httpToken: "${OPENCLAW_HTTP_TOKEN}",
528
+ sessionKey: "agent:main:sinain",
529
+ },
530
+ },
389
531
  };
390
532
  }
391
533
 
@@ -459,6 +601,7 @@ export async function stepAgent(existing, label = "Bare agent") {
459
601
  message: label,
460
602
  options: [
461
603
  { value: "claude", label: "Claude Code", hint: "Calls sinain tools directly — recommended" },
604
+ { value: "openclaude", label: "OpenClaude", hint: "Claude Code clone, local-first (Ollama/OpenAI-compat)" },
462
605
  { value: "codex", label: "Codex", hint: "Calls sinain tools directly" },
463
606
  { value: "goose", label: "Goose", hint: "Calls sinain tools directly" },
464
607
  { value: "junie", label: "Junie", hint: "JetBrains IDE agent" },
package/launcher.js CHANGED
@@ -8,6 +8,7 @@ import path from "path";
8
8
  import os from "os";
9
9
  import net from "net";
10
10
  import readline from "readline";
11
+ import { writeAgentsConfig, readAgentsConfig } from "./config-shared.js";
11
12
 
12
13
  // ── Colors ──────────────────────────────────────────────────────────────────
13
14
 
@@ -486,10 +487,14 @@ async function setupWizard(envPath) {
486
487
  else if (existingKey) vars.OPENROUTER_API_KEY = existingKey;
487
488
  }
488
489
 
489
- // 3. Agent selection
490
- const defaultAgent = existing.SINAIN_AGENT || "claude";
491
- const agentChoice = await ask(` Agent? [${BOLD}${defaultAgent}${RESET}/claude/codex/goose/junie/aider]: `);
492
- vars.SINAIN_AGENT = agentChoice.trim().toLowerCase() || defaultAgent;
490
+ // 3. Agent selection — written to agents.json `default` field. Existing
491
+ // value is read from agents.json first, falling back to .env for users
492
+ // mid-migration.
493
+ const existingAgentsCfg = readAgentsConfig();
494
+ const agentsPatch = {};
495
+ const defaultAgent = existingAgentsCfg?.default || existing.SINAIN_AGENT || "claude";
496
+ const agentChoice = await ask(` Default agent? [${BOLD}${defaultAgent}${RESET}/claude/openclaude/codex/goose/junie/aider]: `);
497
+ agentsPatch.default = agentChoice.trim().toLowerCase() || defaultAgent;
493
498
 
494
499
  // 3b. Local vision (Ollama)
495
500
  const IS_MACOS = os.platform() === "darwin";
@@ -543,28 +548,32 @@ async function setupWizard(envPath) {
543
548
  }
544
549
  }
545
550
 
546
- // 4. Escalation mode
551
+ // 4. Escalation mode — agents.json `escalation.mode`
547
552
  console.log();
548
553
  console.log(` ${DIM}Escalation modes:${RESET}`);
549
- console.log(` off — no escalation to gateway`);
554
+ console.log(` off — no escalation`);
550
555
  console.log(` selective — score-based (errors, questions trigger it)`);
551
556
  console.log(` focus — always escalate every tick`);
552
557
  console.log(` rich — always escalate with maximum context`);
553
- const defaultEsc = existing.ESCALATION_MODE || "selective";
558
+ const defaultEsc = existingAgentsCfg?.escalation?.mode || existing.ESCALATION_MODE || "selective";
554
559
  const escMode = await ask(` Escalation mode? [off/${BOLD}${defaultEsc}${RESET}/selective/focus/rich]: `);
555
- vars.ESCALATION_MODE = escMode.trim().toLowerCase() || defaultEsc;
556
-
557
- // 5. OpenClaw gateway
558
- const hadGateway = !!(existing.OPENCLAW_WS_URL);
560
+ agentsPatch.escalationMode = escMode.trim().toLowerCase() || defaultEsc;
561
+
562
+ // 5. OpenClaw gateway — URLs/session → agents.json (openclaw profile),
563
+ // tokens .env. No transport question; agent identity (openclaw vs
564
+ // local) determines dispatch path.
565
+ const existingOpenclaw = existingAgentsCfg?.profiles?.openclaw;
566
+ const existingWsUrl = existingOpenclaw?.wsUrl || existing.OPENCLAW_WS_URL || "";
567
+ const hadGateway = !!existingWsUrl;
559
568
  const gatewayDefault = hadGateway ? "Y" : "N";
560
569
  const hasGateway = await ask(` Do you have an OpenClaw gateway? [${gatewayDefault === "Y" ? "Y/n" : "y/N"}]: `);
561
570
  const wantsGateway = hasGateway.trim()
562
571
  ? hasGateway.trim().toLowerCase() === "y"
563
572
  : hadGateway;
564
573
  if (wantsGateway) {
565
- const defaultWs = existing.OPENCLAW_WS_URL || "ws://localhost:18789";
574
+ const defaultWs = existingWsUrl || "ws://localhost:18789";
566
575
  const wsUrl = await ask(` Gateway WebSocket URL [${defaultWs}]: `);
567
- vars.OPENCLAW_WS_URL = wsUrl.trim() || defaultWs;
576
+ const finalWsUrl = wsUrl.trim() || defaultWs;
568
577
 
569
578
  const existingToken = existing.OPENCLAW_WS_TOKEN;
570
579
  const tokenHint = existingToken ? ` [${existingToken.slice(0, 6)}...${existingToken.slice(-4)}]` : "";
@@ -577,14 +586,16 @@ async function setupWizard(envPath) {
577
586
  vars.OPENCLAW_HTTP_TOKEN = existing.OPENCLAW_HTTP_TOKEN || existingToken;
578
587
  }
579
588
 
580
- // Derive HTTP URL from WS URL
581
- const httpBase = vars.OPENCLAW_WS_URL.replace(/^ws/, "http");
582
- vars.OPENCLAW_HTTP_URL = `${httpBase}/hooks/agent`;
583
- vars.OPENCLAW_SESSION_KEY = existing.OPENCLAW_SESSION_KEY || "agent:main:sinain";
589
+ agentsPatch.openclawProfile = {
590
+ wsUrl: finalWsUrl,
591
+ httpUrl: finalWsUrl.replace(/^ws/, "http") + "/hooks/agent",
592
+ wsToken: "${OPENCLAW_WS_TOKEN}",
593
+ httpToken: "${OPENCLAW_HTTP_TOKEN}",
594
+ sessionKey: existingOpenclaw?.sessionKey || "agent:main:sinain",
595
+ };
584
596
  } else {
585
- // No gateway — disable WS connection attempts
586
- vars.OPENCLAW_WS_URL = "";
587
- vars.OPENCLAW_HTTP_URL = "";
597
+ // No gateway — drop the openclaw profile entirely.
598
+ agentsPatch.openclawProfile = null;
588
599
  }
589
600
 
590
601
  // 6. Knowledge import (for standalone machines)
@@ -662,10 +673,16 @@ async function setupWizard(envPath) {
662
673
  fs.writeFileSync(envPath, lines.join("\n"));
663
674
  }
664
675
 
676
+ // Flush agent + gateway answers to ~/.sinain/agents.json (separate file
677
+ // from .env after the profile-config refactor).
678
+ if (Object.keys(agentsPatch).length > 0) {
679
+ writeAgentsConfig(agentsPatch);
680
+ }
681
+
665
682
  rl.close();
666
683
 
667
684
  console.log();
668
- ok(`Config written to ${envPath}`);
685
+ ok(`Config written to ${envPath} + ~/.sinain/agents.json`);
669
686
  console.log();
670
687
  }
671
688