@shmulikdav/solix 1.5.0 → 1.9.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/dist/index.js CHANGED
@@ -56,195 +56,527 @@ var pinAdvisorCmd = (id) => postAdvisor(id, "pin");
56
56
  var unpinAdvisorCmd = (id) => postAdvisor(id, "unpin");
57
57
 
58
58
  // src/demo.ts
59
+ import { spawn } from "child_process";
60
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "fs";
59
61
  import { homedir } from "os";
60
62
  import { join } from "path";
61
- var PORT2 = process.env.SOLIX_PORT ?? "4242";
62
- var BASE2 = `http://127.0.0.1:${PORT2}`;
63
- async function postEvent(payload) {
64
- await fetch(`${BASE2}/events`, {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify(payload)
68
- });
63
+ import { fileURLToPath } from "url";
64
+ var SOLIX_HOME = process.env.SOLIX_HOME ?? join(homedir(), ".solix");
65
+ var DEMO_DB_PATH = join(SOLIX_HOME, "demo.db");
66
+ var DEMO_PID_PATH = join(SOLIX_HOME, "demo.pid");
67
+ var TOKEN_PATH = join(SOLIX_HOME, "token");
68
+ var demoToken = "";
69
+ try {
70
+ demoToken = readFileSync(TOKEN_PATH, "utf8").trim();
71
+ } catch {
72
+ }
73
+ var TICKER_COMET_MS = 2500;
74
+ var TICKER_PROMOTE_MS = 25e3;
75
+ var TICKER_MISSION_MS = 45e3;
76
+ var TICKER_PERMISSION_MS = 75e3;
77
+ var TICKER_ADVISOR_MS = 12e4;
78
+ function baseUrl(port) {
79
+ return `http://127.0.0.1:${port}`;
69
80
  }
70
81
  function ts() {
71
82
  return Date.now();
72
83
  }
73
- async function ensureReachable() {
84
+ async function sleep(ms) {
85
+ return new Promise((r) => setTimeout(r, ms));
86
+ }
87
+ async function isPortFree(port) {
74
88
  try {
75
- const res = await fetch(`${BASE2}/api/health`, {
76
- signal: AbortSignal.timeout(800)
89
+ await fetch(`${baseUrl(port)}/api/health`, {
90
+ signal: AbortSignal.timeout(300)
77
91
  });
78
- return res.ok;
79
- } catch {
80
92
  return false;
93
+ } catch {
94
+ return true;
81
95
  }
82
96
  }
83
- async function pin(advisorId) {
84
- await fetch(`${BASE2}/api/advisors/${encodeURIComponent(advisorId)}/pin`, {
85
- method: "POST"
86
- });
97
+ async function waitForServer(port, timeoutMs = 8e3) {
98
+ const start2 = Date.now();
99
+ while (Date.now() - start2 < timeoutMs) {
100
+ if (!await isPortFree(port)) return true;
101
+ await sleep(150);
102
+ }
103
+ return false;
87
104
  }
88
- async function sleep(ms) {
89
- return new Promise((r) => setTimeout(r, ms));
105
+ async function postEvent(base, payload) {
106
+ try {
107
+ await fetch(`${base}/events`, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ ...demoToken ? { "x-solix-token": demoToken } : {}
112
+ },
113
+ body: JSON.stringify(payload)
114
+ });
115
+ } catch {
116
+ }
90
117
  }
91
- async function demoCmd(opts = {}) {
92
- if (opts.port) process.env.SOLIX_PORT = String(opts.port);
93
- if (!await ensureReachable()) {
94
- console.error(
95
- "[solix] server not reachable \u2014 run `solix start` first, then `solix demo` in another terminal"
118
+ async function postJson(base, path, body) {
119
+ try {
120
+ const res = await fetch(`${base}${path}`, {
121
+ method: "POST",
122
+ headers: body ? { "Content-Type": "application/json" } : {},
123
+ body: body ? JSON.stringify(body) : void 0
124
+ });
125
+ if (!res.ok) return null;
126
+ if (res.headers.get("content-type")?.includes("json")) {
127
+ return await res.json();
128
+ }
129
+ return null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ async function getJson(base, path) {
135
+ try {
136
+ const res = await fetch(`${base}${path}`);
137
+ if (!res.ok) return null;
138
+ return await res.json();
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ function randomChoice(arr) {
144
+ return arr[Math.floor(Math.random() * arr.length)];
145
+ }
146
+ async function bootSandbox(preferredPort) {
147
+ let port = preferredPort;
148
+ if (!await isPortFree(port)) {
149
+ console.log(
150
+ `[solix demo] port ${port} is in use \u2014 falling back to ${port + 1} for the sandbox.`
96
151
  );
97
- process.exitCode = 1;
98
- return;
152
+ port += 1;
153
+ if (!await isPortFree(port)) {
154
+ console.error(
155
+ `[solix demo] both ${preferredPort} and ${port} are in use. Stop one of them or pass --port.`
156
+ );
157
+ return null;
158
+ }
99
159
  }
100
- const cwd = opts.cwd ?? join(homedir(), "demo-project");
101
- console.log(`[solix demo] seeding fake state for ${BASE2}`);
102
- console.log(`[solix demo] using fake cwd: ${cwd}`);
103
- const sessions = [
104
- {
105
- id: "demo-a",
106
- pid: 90001,
107
- cwd,
108
- payload: { session_id: "demo-a", model: "opus" },
109
- prompt: "Refactor the orbital math for stable layout",
110
- tools: [
111
- { tool: "Read", file: "packages/web/src/scene/orbits.ts" },
112
- { tool: "Edit", file: "packages/web/src/scene/orbits.ts" },
113
- { tool: "Bash", cmd: "pnpm --filter @solix/web typecheck" }
114
- ]
115
- },
116
- {
117
- id: "demo-b",
118
- pid: 90002,
119
- cwd,
120
- payload: { session_id: "demo-b", model: "sonnet" },
121
- prompt: "Wire up the asteroid belt to real skill data",
122
- tools: [
123
- { tool: "Read", file: "packages/server/src/state/skills.ts" },
124
- { tool: "Write", file: "packages/web/src/scene/AsteroidBelt.tsx" }
125
- ]
126
- },
160
+ if (existsSync(DEMO_PID_PATH)) {
161
+ try {
162
+ const pid = parseInt(readFileSync(DEMO_PID_PATH, "utf8").trim(), 10);
163
+ if (pid > 0) {
164
+ try {
165
+ process.kill(pid, 0);
166
+ try {
167
+ process.kill(pid);
168
+ } catch {
169
+ }
170
+ } catch {
171
+ }
172
+ }
173
+ } catch {
174
+ }
175
+ }
176
+ mkdirSync(SOLIX_HOME, { recursive: true });
177
+ const selfScript = fileURLToPath(import.meta.url);
178
+ const child = spawn(
179
+ process.execPath,
180
+ [selfScript, "start", "--port", String(port), "--no-open"],
127
181
  {
128
- id: "demo-c",
129
- pid: 90003,
130
- cwd,
131
- payload: { session_id: "demo-c", model: "haiku" },
132
- prompt: "Document the context envelope strategy",
133
- tools: []
182
+ env: {
183
+ ...process.env,
184
+ SOLIX_DB_PATH: DEMO_DB_PATH
185
+ },
186
+ stdio: ["ignore", "inherit", "inherit"],
187
+ detached: false
134
188
  }
135
- ];
136
- for (const s of sessions) {
137
- await postEvent({
189
+ );
190
+ if (child.pid) {
191
+ try {
192
+ (await import("fs")).writeFileSync(
193
+ DEMO_PID_PATH,
194
+ String(child.pid)
195
+ );
196
+ } catch {
197
+ }
198
+ }
199
+ child.on("exit", (code) => {
200
+ if (code != null && code !== 0) {
201
+ console.error(`[solix demo] sandbox server exited with code ${code}`);
202
+ }
203
+ });
204
+ if (!await waitForServer(port)) {
205
+ console.error(`[solix demo] sandbox server failed to start within 8s.`);
206
+ try {
207
+ child.kill();
208
+ } catch {
209
+ }
210
+ return null;
211
+ }
212
+ return { child, port, base: baseUrl(port) };
213
+ }
214
+ var PROJECTS = [
215
+ "web-app",
216
+ "infrastructure",
217
+ "data-pipeline",
218
+ "mobile-client",
219
+ "design-system",
220
+ "observability",
221
+ "ml-research",
222
+ "docs-site"
223
+ ];
224
+ var MODELS = ["opus", "sonnet", "haiku", "default"];
225
+ var PROMPTS = [
226
+ "Refactor the orbital math for stable layout",
227
+ "Wire up the asteroid belt to real skill data",
228
+ "Document the context envelope strategy",
229
+ "Audit the auth flow for token leak risk",
230
+ "Generate a migration plan for the new schema",
231
+ "Triage the failing Playwright spec",
232
+ "Build a flame graph from the last week of traces",
233
+ "Sweep deprecated APIs out of the SDK",
234
+ "Polish the README with three quickstart examples",
235
+ "Draft a runbook for the budget breach scenario"
236
+ ];
237
+ var TOOL_FILES = [
238
+ "packages/web/src/scene/Planet.tsx",
239
+ "packages/server/src/router.ts",
240
+ "packages/cli/src/install.ts",
241
+ "packages/shared/src/types.ts",
242
+ "packages/web/src/store/index.ts"
243
+ ];
244
+ var TOOL_COMMANDS = [
245
+ "pnpm -r typecheck",
246
+ "pnpm --filter @solix/web build",
247
+ "git status -sb",
248
+ "cargo test --workspace",
249
+ "curl -s http://127.0.0.1:4242/api/health"
250
+ ];
251
+ var STATUS_PLAN = [
252
+ ...Array(5).fill("active"),
253
+ ...Array(3).fill("awaiting_permission"),
254
+ ...Array(2).fill("awaiting_input"),
255
+ ...Array(1).fill("error"),
256
+ ...Array(1).fill("plan_review"),
257
+ ...Array(18).fill("idle")
258
+ ];
259
+ async function richSeed(base, demoRootCwd) {
260
+ console.log(`[solix demo] seeding ${STATUS_PLAN.length} sessions across ${PROJECTS.length} projects\u2026`);
261
+ const advisors2 = await getJson(base, "/api/advisors") ?? [];
262
+ for (const a of advisors2) {
263
+ await postJson(base, `/api/advisors/${encodeURIComponent(a.id)}/enable`);
264
+ }
265
+ console.log(`[solix demo] enabled ${advisors2.length} advisors`);
266
+ const sessions = [];
267
+ let pid = 9e4;
268
+ for (let i = 0; i < STATUS_PLAN.length; i++) {
269
+ const projectName = PROJECTS[i % PROJECTS.length];
270
+ const cwd = join(demoRootCwd, projectName);
271
+ const status = STATUS_PLAN[i];
272
+ const id = `demo-${String(i).padStart(2, "0")}-${projectName}`;
273
+ const model = MODELS[i % MODELS.length];
274
+ sessions.push({ id, pid, cwd, projectName, status });
275
+ await postEvent(base, {
138
276
  event: "session_start",
277
+ pid,
278
+ cwd,
279
+ ts: ts(),
280
+ payload: { session_id: id, model }
281
+ });
282
+ pid += 1;
283
+ }
284
+ await sleep(120);
285
+ for (const s of sessions) {
286
+ const prompt = randomChoice(PROMPTS);
287
+ if (s.status === "active" || s.status === "idle" || s.status === "awaiting_input") {
288
+ await postEvent(base, {
289
+ event: "user_prompt_submit",
290
+ pid: s.pid,
291
+ cwd: s.cwd,
292
+ ts: ts(),
293
+ payload: { session_id: s.id, prompt }
294
+ });
295
+ }
296
+ if (s.status === "idle") {
297
+ await postEvent(base, {
298
+ event: "stop",
299
+ pid: s.pid,
300
+ cwd: s.cwd,
301
+ ts: ts(),
302
+ payload: { session_id: s.id }
303
+ });
304
+ }
305
+ if (s.status === "awaiting_permission") {
306
+ await postEvent(base, {
307
+ event: "notification",
308
+ pid: s.pid,
309
+ cwd: s.cwd,
310
+ ts: ts(),
311
+ payload: {
312
+ session_id: s.id,
313
+ tool_name: "Bash",
314
+ tool_input: { command: "git push origin main" },
315
+ message: "Permission for git push"
316
+ }
317
+ });
318
+ }
319
+ }
320
+ const active = sessions.filter((s) => s.status === "active");
321
+ for (const s of active) {
322
+ await postEvent(base, {
323
+ event: "pre_tool_file",
139
324
  pid: s.pid,
140
325
  cwd: s.cwd,
141
326
  ts: ts(),
142
- payload: s.payload
327
+ payload: {
328
+ session_id: s.id,
329
+ tool_name: "Read",
330
+ tool_input: { file_path: randomChoice(TOOL_FILES) }
331
+ }
143
332
  });
333
+ await sleep(40);
144
334
  }
145
- await sleep(150);
146
- for (const s of sessions.slice(0, 2)) {
147
- await postEvent({
148
- event: "user_prompt_submit",
335
+ for (const s of active.slice(0, 2)) {
336
+ await postEvent(base, {
337
+ event: "pre_tool_task",
149
338
  pid: s.pid,
150
339
  cwd: s.cwd,
151
340
  ts: ts(),
152
- payload: { session_id: s.id, prompt: s.prompt }
341
+ payload: { session_id: s.id }
153
342
  });
154
343
  }
155
- await sleep(100);
156
- for (const t of sessions[0].tools) {
157
- if (t.tool === "Bash") {
158
- await postEvent({
159
- event: "pre_tool_bash",
160
- pid: sessions[0].pid,
161
- cwd: sessions[0].cwd,
344
+ if (active[0]) {
345
+ await postJson(base, `/api/sessions/${active[0].id}/context`, { pct: 62 });
346
+ }
347
+ if (active[1]) {
348
+ await postJson(base, `/api/sessions/${active[1].id}/context`, { pct: 89 });
349
+ }
350
+ console.log(`[solix demo] seed complete:`);
351
+ console.log(` \u2022 ${sessions.length} sessions across ${PROJECTS.length} projects`);
352
+ console.log(` \u2022 ${advisors2.length} advisors enabled`);
353
+ console.log(` \u2022 status mix: 5 active, 18 idle, 3 awaiting_permission, 2 awaiting_input, 1 error, 1 plan_review`);
354
+ console.log(` \u2022 2 subagent moons, 1 high-context flare`);
355
+ return { sessions, advisorIds: advisors2.map((a) => a.id) };
356
+ }
357
+ function startTicker(base, state) {
358
+ const intervals = [];
359
+ const activeIds = new Set(
360
+ state.sessions.filter((s) => s.status === "active").map((s) => s.id)
361
+ );
362
+ const idleIds = new Set(
363
+ state.sessions.filter((s) => s.status === "idle").map((s) => s.id)
364
+ );
365
+ const byId = new Map(state.sessions.map((s) => [s.id, s]));
366
+ intervals.push(
367
+ setInterval(() => {
368
+ const candidates = [...activeIds];
369
+ if (candidates.length === 0) return;
370
+ const id = randomChoice(candidates);
371
+ const s = byId.get(id);
372
+ if (!s) return;
373
+ const useBash = Math.random() < 0.4;
374
+ void postEvent(base, {
375
+ event: useBash ? "pre_tool_bash" : "pre_tool_file",
376
+ pid: s.pid,
377
+ cwd: s.cwd,
162
378
  ts: ts(),
163
- payload: {
164
- session_id: sessions[0].id,
165
- command: t.cmd
379
+ payload: useBash ? { session_id: s.id, command: randomChoice(TOOL_COMMANDS) } : {
380
+ session_id: s.id,
381
+ tool_name: Math.random() < 0.5 ? "Read" : "Edit",
382
+ tool_input: { file_path: randomChoice(TOOL_FILES) }
166
383
  }
167
384
  });
168
- } else {
169
- await postEvent({
170
- event: "pre_tool_file",
171
- pid: sessions[0].pid,
172
- cwd: sessions[0].cwd,
385
+ }, TICKER_COMET_MS)
386
+ );
387
+ intervals.push(
388
+ setInterval(() => {
389
+ if (Math.random() < 0.5 && idleIds.size > 0) {
390
+ const id = randomChoice([...idleIds]);
391
+ const s = byId.get(id);
392
+ if (!s) return;
393
+ idleIds.delete(id);
394
+ activeIds.add(id);
395
+ void postEvent(base, {
396
+ event: "user_prompt_submit",
397
+ pid: s.pid,
398
+ cwd: s.cwd,
399
+ ts: ts(),
400
+ payload: { session_id: id, prompt: randomChoice(PROMPTS) }
401
+ });
402
+ } else if (activeIds.size > 1) {
403
+ const id = randomChoice([...activeIds]);
404
+ const s = byId.get(id);
405
+ if (!s) return;
406
+ activeIds.delete(id);
407
+ idleIds.add(id);
408
+ void postEvent(base, {
409
+ event: "stop",
410
+ pid: s.pid,
411
+ cwd: s.cwd,
412
+ ts: ts(),
413
+ payload: { session_id: id }
414
+ });
415
+ }
416
+ }, TICKER_PROMOTE_MS)
417
+ );
418
+ intervals.push(
419
+ setInterval(() => {
420
+ const actives = [...activeIds];
421
+ if (actives.length < 2) return;
422
+ const finisher = byId.get(randomChoice(actives));
423
+ const starter = byId.get(
424
+ randomChoice(actives.filter((id) => id !== finisher.id))
425
+ );
426
+ void postEvent(base, {
427
+ event: "stop",
428
+ pid: finisher.pid,
429
+ cwd: finisher.cwd,
430
+ ts: ts(),
431
+ payload: { session_id: finisher.id }
432
+ });
433
+ void postEvent(base, {
434
+ event: "user_prompt_submit",
435
+ pid: starter.pid,
436
+ cwd: starter.cwd,
437
+ ts: ts(),
438
+ payload: { session_id: starter.id, prompt: randomChoice(PROMPTS) }
439
+ });
440
+ }, TICKER_MISSION_MS)
441
+ );
442
+ intervals.push(
443
+ setInterval(() => {
444
+ const actives = [...activeIds];
445
+ if (actives.length === 0) return;
446
+ const s = byId.get(randomChoice(actives));
447
+ if (!s) return;
448
+ void postEvent(base, {
449
+ event: "notification",
450
+ pid: s.pid,
451
+ cwd: s.cwd,
173
452
  ts: ts(),
174
453
  payload: {
175
- session_id: sessions[0].id,
176
- tool_name: t.tool,
177
- tool_input: { file_path: t.file }
454
+ session_id: s.id,
455
+ tool_name: "Bash",
456
+ tool_input: { command: "rm -rf node_modules" },
457
+ message: "Permission for destructive shell command"
178
458
  }
179
459
  });
460
+ }, TICKER_PERMISSION_MS)
461
+ );
462
+ intervals.push(
463
+ setInterval(() => {
464
+ if (state.advisorIds.length === 0 || activeIds.size === 0) return;
465
+ const advisorId = randomChoice(state.advisorIds);
466
+ const targetSessionId = randomChoice([...activeIds]);
467
+ void postJson(
468
+ base,
469
+ `/api/advisors/${encodeURIComponent(advisorId)}/invoke`,
470
+ {
471
+ targetSessionId,
472
+ prompt: "Spot-check this session before the next mission."
473
+ }
474
+ );
475
+ }, TICKER_ADVISOR_MS)
476
+ );
477
+ return () => {
478
+ for (const i of intervals) clearInterval(i);
479
+ };
480
+ }
481
+ function registerTeardown(opts) {
482
+ let torn = false;
483
+ const onSignal = (sig) => {
484
+ if (torn) return;
485
+ torn = true;
486
+ console.log(`
487
+ [solix demo] received ${sig} \u2014 tearing down\u2026`);
488
+ if (opts.stopTicker) opts.stopTicker();
489
+ if (opts.child) {
490
+ try {
491
+ opts.child.kill();
492
+ } catch {
493
+ }
494
+ }
495
+ if (!opts.keep) {
496
+ for (const p of [DEMO_DB_PATH, `${DEMO_DB_PATH}-shm`, `${DEMO_DB_PATH}-wal`, DEMO_PID_PATH]) {
497
+ try {
498
+ if (existsSync(p)) unlinkSync(p);
499
+ } catch {
500
+ }
501
+ }
502
+ console.log(`[solix demo] sandbox cleaned up.`);
503
+ } else {
504
+ console.log(`[solix demo] --keep set; ${DEMO_DB_PATH} preserved.`);
180
505
  }
181
- await sleep(80);
506
+ process.exit(0);
507
+ };
508
+ process.on("SIGINT", onSignal);
509
+ process.on("SIGTERM", onSignal);
510
+ }
511
+ async function tryOpenBrowser(url) {
512
+ const platform = process.platform;
513
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
514
+ try {
515
+ const child = spawn(cmd, [url], { stdio: "ignore", detached: true });
516
+ child.on("error", () => {
517
+ });
518
+ child.unref();
519
+ } catch {
182
520
  }
183
- await postEvent({
184
- event: "pre_tool_task",
185
- pid: sessions[1].pid,
186
- cwd: sessions[1].cwd,
187
- ts: ts(),
188
- payload: { session_id: sessions[1].id }
189
- });
190
- await postEvent({
191
- event: "notification",
192
- pid: sessions[2].pid,
193
- cwd: sessions[2].cwd,
194
- ts: ts(),
195
- payload: {
196
- session_id: sessions[2].id,
197
- tool_name: "Bash",
198
- tool_input: { command: "git push origin main" },
199
- message: "Permission for git push"
521
+ }
522
+ async function demoCmd(opts = {}) {
523
+ const preferredPort = opts.port ?? 4242;
524
+ const demoRootCwd = opts.cwd ?? join(homedir(), "demo-projects");
525
+ let boot = null;
526
+ if (opts.noServer) {
527
+ if (!await waitForServer(preferredPort, 1e3)) {
528
+ console.error(
529
+ `[solix demo] --no-server set but nothing is listening on ${preferredPort}. Start a server first.`
530
+ );
531
+ process.exitCode = 1;
532
+ return;
200
533
  }
201
- });
202
- await fetch(`${BASE2}/api/sessions/demo-a/context`, {
203
- method: "POST",
204
- headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ pct: 62 })
206
- });
207
- await fetch(`${BASE2}/api/sessions/demo-b/context`, {
208
- method: "POST",
209
- headers: { "Content-Type": "application/json" },
210
- body: JSON.stringify({ pct: 87 })
211
- });
212
- await pin("compass");
213
- console.log(`[solix demo] seeded:`);
214
- console.log(` \u2022 3 user planets (opus / sonnet / haiku)`);
215
- console.log(` \u2022 1 active mission with tool-call comets`);
216
- console.log(` \u2022 1 subagent moon`);
217
- console.log(` \u2022 1 planet awaiting permission (red flare)`);
218
- console.log(` \u2022 1 planet at 87% context (orange flare)`);
219
- console.log(` \u2022 Compass pinned (always-on)`);
220
- console.log(`[solix demo] open ${BASE2} to see it.`);
534
+ boot = { child: null, port: preferredPort, base: baseUrl(preferredPort) };
535
+ } else {
536
+ boot = await bootSandbox(preferredPort);
537
+ if (!boot) {
538
+ process.exitCode = 1;
539
+ return;
540
+ }
541
+ console.log(
542
+ `[solix demo] server up at ${boot.base} (sandbox DB at ${DEMO_DB_PATH})`
543
+ );
544
+ }
545
+ const seed = await richSeed(boot.base, demoRootCwd);
546
+ console.log(`[solix demo] open ${boot.base} to see the galaxy.`);
547
+ void tryOpenBrowser(boot.base);
548
+ if (opts.noTicker) {
549
+ console.log(
550
+ `[solix demo] --no-ticker set; static snapshot only. Exiting.`
551
+ );
552
+ return;
553
+ }
221
554
  console.log(
222
- `[solix demo] in ~3s: Compass will be invoked on demo-a (watch for the toast + Audit tab row).`
555
+ `[solix demo] live ticker running. Press Ctrl+C to stop and tear down.`
223
556
  );
224
- await sleep(3e3);
225
- await fetch(`${BASE2}/api/advisors/compass/invoke`, {
226
- method: "POST",
227
- headers: { "Content-Type": "application/json" },
228
- body: JSON.stringify({
229
- targetSessionId: "demo-a",
230
- prompt: "Review the orbital math refactor before merging"
231
- })
557
+ const stopTicker = startTicker(boot.base, seed);
558
+ registerTeardown({
559
+ child: boot.child,
560
+ stopTicker,
561
+ keep: opts.keep ?? false
562
+ });
563
+ await new Promise(() => {
232
564
  });
233
- console.log(`[solix demo] Compass invoked. Demo complete.`);
234
565
  }
235
566
 
236
567
  // src/doctor.ts
237
- import { existsSync as existsSync2, readdirSync, statSync } from "fs";
568
+ import { existsSync as existsSync3, readdirSync, statSync } from "fs";
238
569
  import { join as join3 } from "path";
239
570
 
240
571
  // src/paths.ts
241
572
  import { homedir as homedir2 } from "os";
242
- import { existsSync } from "fs";
243
- import { join as join2, dirname } from "path";
244
- import { fileURLToPath } from "url";
245
- var SOLIX_HOME = process.env.SOLIX_HOME ?? join2(homedir2(), ".solix");
246
- var HOOKS_DIR = join2(SOLIX_HOME, "hooks");
247
- var SOLIX_SKILLS_DIR = join2(SOLIX_HOME, "skills");
573
+ import { existsSync as existsSync2 } from "fs";
574
+ import { join as join2, dirname as dirname2 } from "path";
575
+ import { fileURLToPath as fileURLToPath2 } from "url";
576
+ var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join2(homedir2(), ".solix");
577
+ var HOOKS_DIR = join2(SOLIX_HOME2, "hooks");
578
+ var SOLIX_SKILLS_DIR = join2(SOLIX_HOME2, "skills");
579
+ var SOLIX_TOKEN_FILE = join2(SOLIX_HOME2, "token");
248
580
  var CLAUDE_DIR = join2(homedir2(), ".claude");
249
581
  var CLAUDE_SETTINGS = join2(CLAUDE_DIR, "settings.json");
250
582
  var CLAUDE_BACKUP = join2(CLAUDE_DIR, "settings.solix.backup.json");
@@ -262,38 +594,40 @@ var HOOK_NAMES = [
262
594
  "notification"
263
595
  ];
264
596
  function packagedHooksDir() {
265
- const here = dirname(fileURLToPath(import.meta.url));
597
+ const here = dirname2(fileURLToPath2(import.meta.url));
266
598
  const candidates = [
267
599
  join2(here, "hooks"),
268
600
  join2(here, "..", "hooks"),
269
601
  join2(here, "..", "..", "hooks")
270
602
  ];
271
603
  for (const p of candidates) {
272
- if (existsSync(join2(p, "session-start.sh"))) return p;
604
+ if (existsSync2(join2(p, "session-start.sh"))) return p;
273
605
  }
274
606
  return candidates[0];
275
607
  }
276
608
  function packagedAgentsDir() {
277
- const here = dirname(fileURLToPath(import.meta.url));
609
+ const here = dirname2(fileURLToPath2(import.meta.url));
278
610
  const candidates = [
611
+ join2(here, "agents"),
279
612
  join2(here, "..", "..", "agents"),
280
613
  join2(here, "..", "..", "..", "agents"),
281
614
  join2(here, "..", "..", "..", "..", "packages", "agents")
282
615
  ];
283
616
  for (const p of candidates) {
284
- if (existsSync(join2(p, "manifest.json"))) return p;
617
+ if (existsSync2(join2(p, "manifest.json"))) return p;
285
618
  }
286
619
  return candidates[0];
287
620
  }
288
621
  function packagedSkillsDir() {
289
- const here = dirname(fileURLToPath(import.meta.url));
622
+ const here = dirname2(fileURLToPath2(import.meta.url));
290
623
  const candidates = [
624
+ join2(here, "skills"),
291
625
  join2(here, "..", "..", "skills"),
292
626
  join2(here, "..", "..", "..", "skills"),
293
627
  join2(here, "..", "..", "..", "..", "packages", "skills")
294
628
  ];
295
629
  for (const p of candidates) {
296
- if (existsSync(p)) return p;
630
+ if (existsSync2(p)) return p;
297
631
  }
298
632
  return candidates[0];
299
633
  }
@@ -394,15 +728,15 @@ async function doctor() {
394
728
  detail: `v${nodeVersion}`
395
729
  });
396
730
  checks.push({
397
- ok: existsSync2(SOLIX_HOME),
731
+ ok: existsSync3(SOLIX_HOME2),
398
732
  label: "Solix home directory",
399
- detail: SOLIX_HOME
733
+ detail: SOLIX_HOME2
400
734
  });
401
735
  let allHooksPresent = true;
402
736
  const missing = [];
403
737
  for (const name of HOOK_NAMES) {
404
738
  const p = join3(HOOKS_DIR, `${name}.sh`);
405
- if (!existsSync2(p)) {
739
+ if (!existsSync3(p)) {
406
740
  allHooksPresent = false;
407
741
  missing.push(name);
408
742
  continue;
@@ -420,17 +754,17 @@ async function doctor() {
420
754
  detail: allHooksPresent ? `${HOOK_NAMES.length} scripts in ${HOOKS_DIR}` : `missing: ${missing.join(", ")}`
421
755
  });
422
756
  checks.push({
423
- ok: existsSync2(CLAUDE_SETTINGS),
757
+ ok: existsSync3(CLAUDE_SETTINGS),
424
758
  label: "Claude settings.json present",
425
759
  detail: CLAUDE_SETTINGS
426
760
  });
427
761
  checks.push({
428
- ok: existsSync2(CLAUDE_BACKUP),
762
+ ok: existsSync3(CLAUDE_BACKUP),
429
763
  label: "Backup of settings.json",
430
- detail: existsSync2(CLAUDE_BACKUP) ? CLAUDE_BACKUP : "not yet created"
764
+ detail: existsSync3(CLAUDE_BACKUP) ? CLAUDE_BACKUP : "not yet created"
431
765
  });
432
766
  let advisorCount = 0;
433
- if (existsSync2(CLAUDE_AGENTS_DIR)) {
767
+ if (existsSync3(CLAUDE_AGENTS_DIR)) {
434
768
  try {
435
769
  advisorCount = readdirSync(CLAUDE_AGENTS_DIR).filter(
436
770
  (f) => f.endsWith(".md")
@@ -445,7 +779,7 @@ async function doctor() {
445
779
  detail: advisorCount > 0 ? `${advisorCount} agents in ${CLAUDE_AGENTS_DIR}` : "none yet \u2014 run `solix install`"
446
780
  });
447
781
  let skillCount = 0;
448
- if (existsSync2(SOLIX_SKILLS_DIR)) {
782
+ if (existsSync3(SOLIX_SKILLS_DIR)) {
449
783
  try {
450
784
  skillCount = readdirSync(SOLIX_SKILLS_DIR).filter((entry) => {
451
785
  try {
@@ -486,11 +820,11 @@ async function doctor() {
486
820
  }
487
821
 
488
822
  // src/galaxy.ts
489
- import { readFileSync, writeFileSync } from "fs";
490
- var PORT3 = process.env.SOLIX_PORT ?? "4242";
491
- var BASE3 = `http://127.0.0.1:${PORT3}`;
823
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
824
+ var PORT2 = process.env.SOLIX_PORT ?? "4242";
825
+ var BASE2 = `http://127.0.0.1:${PORT2}`;
492
826
  async function api2(path, init) {
493
- const res = await fetch(`${BASE3}${path}`, init);
827
+ const res = await fetch(`${BASE2}${path}`, init);
494
828
  if (!res.ok) {
495
829
  const text = await res.text().catch(() => "");
496
830
  throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
@@ -517,7 +851,7 @@ async function exportGalaxyCmd(outFile, opts = {}) {
517
851
  }
518
852
  async function publishGalaxyCmd(slug, opts = {}) {
519
853
  try {
520
- const res = await fetch(`${BASE3}/api/galaxy/publish`, {
854
+ const res = await fetch(`${BASE2}/api/galaxy/publish`, {
521
855
  method: "POST",
522
856
  headers: { "Content-Type": "application/json" },
523
857
  body: JSON.stringify({ slug, ...opts })
@@ -542,7 +876,7 @@ async function publishGalaxyCmd(slug, opts = {}) {
542
876
  async function installFromRegistryCmd(slug) {
543
877
  try {
544
878
  const res = await fetch(
545
- `${BASE3}/api/galaxy/registry/${encodeURIComponent(slug)}/install`,
879
+ `${BASE2}/api/galaxy/registry/${encodeURIComponent(slug)}/install`,
546
880
  { method: "POST" }
547
881
  );
548
882
  const data = await res.json();
@@ -567,7 +901,7 @@ async function importGalaxyCmd(fileOrUrl) {
567
901
  if (fileOrUrl.startsWith("http://") || fileOrUrl.startsWith("https://")) {
568
902
  body = JSON.stringify({ url: fileOrUrl });
569
903
  } else {
570
- const text = readFileSync(fileOrUrl, "utf8");
904
+ const text = readFileSync2(fileOrUrl, "utf8");
571
905
  body = text;
572
906
  }
573
907
  const res = await api2(`/api/galaxy/import`, {
@@ -593,19 +927,20 @@ async function importGalaxyCmd(fileOrUrl) {
593
927
  import {
594
928
  copyFileSync,
595
929
  cpSync,
596
- existsSync as existsSync3,
597
- mkdirSync,
930
+ existsSync as existsSync4,
931
+ mkdirSync as mkdirSync2,
598
932
  readdirSync as readdirSync2,
599
- readFileSync as readFileSync2,
933
+ readFileSync as readFileSync3,
600
934
  statSync as statSync2,
601
935
  writeFileSync as writeFileSync2,
602
936
  chmodSync
603
937
  } from "fs";
938
+ import { randomBytes } from "crypto";
604
939
  import { join as join4 } from "path";
605
940
  function readSettings() {
606
- if (!existsSync3(CLAUDE_SETTINGS)) return {};
941
+ if (!existsSync4(CLAUDE_SETTINGS)) return {};
607
942
  try {
608
- const txt = readFileSync2(CLAUDE_SETTINGS, "utf8");
943
+ const txt = readFileSync3(CLAUDE_SETTINGS, "utf8");
609
944
  return JSON.parse(txt);
610
945
  } catch (err) {
611
946
  console.warn(`[solix] could not parse ${CLAUDE_SETTINGS}: ${String(err)}`);
@@ -654,10 +989,19 @@ function mergeHooks(existing, solix) {
654
989
  }
655
990
  return merged;
656
991
  }
992
+ function ensureToken() {
993
+ if (existsSync4(SOLIX_TOKEN_FILE)) return;
994
+ const token = randomBytes(24).toString("hex");
995
+ writeFileSync2(SOLIX_TOKEN_FILE, token, { mode: 384 });
996
+ try {
997
+ chmodSync(SOLIX_TOKEN_FILE, 384);
998
+ } catch {
999
+ }
1000
+ }
657
1001
  function installHookScripts() {
658
- mkdirSync(HOOKS_DIR, { recursive: true });
1002
+ mkdirSync2(HOOKS_DIR, { recursive: true });
659
1003
  const src = packagedHooksDir();
660
- if (!existsSync3(src)) {
1004
+ if (!existsSync4(src)) {
661
1005
  throw new Error(
662
1006
  `Solix hook scripts not found at ${src}. Did the package build correctly?`
663
1007
  );
@@ -671,19 +1015,19 @@ function installHookScripts() {
671
1015
  }
672
1016
  function installAdvisorAgents() {
673
1017
  const src = packagedAgentsDir();
674
- if (!existsSync3(src)) {
1018
+ if (!existsSync4(src)) {
675
1019
  console.warn(`[solix] no advisors/ directory at ${src}; skipping`);
676
1020
  return 0;
677
1021
  }
678
1022
  const manifestPath = join4(src, "manifest.json");
679
- if (!existsSync3(manifestPath)) return 0;
680
- const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
681
- mkdirSync(CLAUDE_AGENTS_DIR, { recursive: true });
1023
+ if (!existsSync4(manifestPath)) return 0;
1024
+ const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
1025
+ mkdirSync2(CLAUDE_AGENTS_DIR, { recursive: true });
682
1026
  let copied = 0;
683
1027
  for (const a of manifest.advisors) {
684
1028
  const from = join4(src, a.agentMd);
685
1029
  const to = join4(CLAUDE_AGENTS_DIR, a.agentMd);
686
- if (!existsSync3(from)) continue;
1030
+ if (!existsSync4(from)) continue;
687
1031
  copyFileSync(from, to);
688
1032
  copied += 1;
689
1033
  }
@@ -691,8 +1035,8 @@ function installAdvisorAgents() {
691
1035
  }
692
1036
  function installSolixSkills() {
693
1037
  const src = packagedSkillsDir();
694
- if (!existsSync3(src)) return 0;
695
- mkdirSync(SOLIX_SKILLS_DIR, { recursive: true });
1038
+ if (!existsSync4(src)) return 0;
1039
+ mkdirSync2(SOLIX_SKILLS_DIR, { recursive: true });
696
1040
  let copied = 0;
697
1041
  for (const entry of readdirSync2(src)) {
698
1042
  const fromDir = join4(src, entry);
@@ -710,15 +1054,16 @@ function installSolixSkills() {
710
1054
  return copied;
711
1055
  }
712
1056
  function install(opts = {}) {
713
- mkdirSync(SOLIX_HOME, { recursive: true });
714
- mkdirSync(CLAUDE_DIR, { recursive: true });
1057
+ mkdirSync2(SOLIX_HOME2, { recursive: true });
1058
+ mkdirSync2(CLAUDE_DIR, { recursive: true });
715
1059
  const existing = readSettings();
716
- if (existsSync3(CLAUDE_SETTINGS) && !existsSync3(CLAUDE_BACKUP)) {
1060
+ if (existsSync4(CLAUDE_SETTINGS) && !existsSync4(CLAUDE_BACKUP)) {
717
1061
  copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
718
1062
  console.log(`[solix] backed up settings.json -> ${CLAUDE_BACKUP}`);
719
- } else if (opts.force && existsSync3(CLAUDE_SETTINGS)) {
1063
+ } else if (opts.force && existsSync4(CLAUDE_SETTINGS)) {
720
1064
  copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
721
1065
  }
1066
+ ensureToken();
722
1067
  installHookScripts();
723
1068
  console.log(`[solix] installed hook scripts in ${HOOKS_DIR}`);
724
1069
  const advisorsCopied = installAdvisorAgents();
@@ -742,8 +1087,8 @@ function install(opts = {}) {
742
1087
  // src/install-shim.ts
743
1088
  import {
744
1089
  appendFileSync,
745
- existsSync as existsSync4,
746
- readFileSync as readFileSync3,
1090
+ existsSync as existsSync5,
1091
+ readFileSync as readFileSync4,
747
1092
  writeFileSync as writeFileSync3
748
1093
  } from "fs";
749
1094
  import { homedir as homedir3 } from "os";
@@ -753,19 +1098,19 @@ var BLOCK_END = "# <<< solix shim <<<";
753
1098
  function detectShellRcPath() {
754
1099
  const shell = process.env.SHELL ?? "";
755
1100
  const home = homedir3();
756
- if (shell.endsWith("zsh") || existsSync4(join5(home, ".zshrc"))) {
1101
+ if (shell.endsWith("zsh") || existsSync5(join5(home, ".zshrc"))) {
757
1102
  return join5(home, ".zshrc");
758
1103
  }
759
- if (shell.endsWith("bash") || existsSync4(join5(home, ".bashrc"))) {
1104
+ if (shell.endsWith("bash") || existsSync5(join5(home, ".bashrc"))) {
760
1105
  return join5(home, ".bashrc");
761
1106
  }
762
- if (existsSync4(join5(home, ".bash_profile"))) {
1107
+ if (existsSync5(join5(home, ".bash_profile"))) {
763
1108
  return join5(home, ".bash_profile");
764
1109
  }
765
1110
  return null;
766
1111
  }
767
1112
  function readRc(rcPath) {
768
- return existsSync4(rcPath) ? readFileSync3(rcPath, "utf8") : "";
1113
+ return existsSync5(rcPath) ? readFileSync4(rcPath, "utf8") : "";
769
1114
  }
770
1115
  function blockText() {
771
1116
  return [
@@ -816,15 +1161,15 @@ function uninstallShim() {
816
1161
 
817
1162
  // src/run.ts
818
1163
  import { createServer as createUnixServer } from "net";
819
- import { mkdirSync as mkdirSync2, unlinkSync } from "fs";
1164
+ import { mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
820
1165
  import { homedir as homedir4 } from "os";
821
1166
  import { join as join6 } from "path";
822
1167
  import { nanoid } from "nanoid";
823
- var PORT4 = process.env.SOLIX_PORT ?? "4242";
824
- var BASE4 = `http://127.0.0.1:${PORT4}`;
1168
+ var PORT3 = process.env.SOLIX_PORT ?? "4242";
1169
+ var BASE3 = `http://127.0.0.1:${PORT3}`;
825
1170
  async function registerWithServer(payload) {
826
1171
  try {
827
- const res = await fetch(`${BASE4}/api/wrappers/register`, {
1172
+ const res = await fetch(`${BASE3}/api/wrappers/register`, {
828
1173
  method: "POST",
829
1174
  headers: { "Content-Type": "application/json" },
830
1175
  body: JSON.stringify(payload),
@@ -838,7 +1183,7 @@ async function registerWithServer(payload) {
838
1183
  async function unregisterFromServer(wrapperId) {
839
1184
  try {
840
1185
  await fetch(
841
- `${BASE4}/api/wrappers/${encodeURIComponent(wrapperId)}/unregister`,
1186
+ `${BASE3}/api/wrappers/${encodeURIComponent(wrapperId)}/unregister`,
842
1187
  { method: "POST", signal: AbortSignal.timeout(800) }
843
1188
  );
844
1189
  } catch {
@@ -857,7 +1202,7 @@ async function runWrapped(args) {
857
1202
  }
858
1203
  const wrapperId = nanoid(10);
859
1204
  const sockDir = join6(homedir4(), ".solix", "wrappers");
860
- mkdirSync2(sockDir, { recursive: true });
1205
+ mkdirSync3(sockDir, { recursive: true });
861
1206
  const socketPath = join6(sockDir, `${wrapperId}.sock`);
862
1207
  const cwd = process.cwd();
863
1208
  const registered = await registerWithServer({ wrapperId, socketPath, cwd });
@@ -919,7 +1264,7 @@ async function runWrapped(args) {
919
1264
  );
920
1265
  } else {
921
1266
  process.stderr.write(
922
- `[solix run] note: Solix server not reachable at ${BASE4}; claude will run normally, but the UI composer won't be active.
1267
+ `[solix run] note: Solix server not reachable at ${BASE3}; claude will run normally, but the UI composer won't be active.
923
1268
  `
924
1269
  );
925
1270
  }
@@ -932,7 +1277,7 @@ async function runWrapped(args) {
932
1277
  } catch {
933
1278
  }
934
1279
  try {
935
- unlinkSync(socketPath);
1280
+ unlinkSync2(socketPath);
936
1281
  } catch {
937
1282
  }
938
1283
  if (registered) await unregisterFromServer(wrapperId);
@@ -960,10 +1305,10 @@ async function runWrapped(args) {
960
1305
  }
961
1306
 
962
1307
  // src/skills.ts
963
- var PORT5 = process.env.SOLIX_PORT ?? "4242";
964
- var BASE5 = `http://127.0.0.1:${PORT5}`;
1308
+ var PORT4 = process.env.SOLIX_PORT ?? "4242";
1309
+ var BASE4 = `http://127.0.0.1:${PORT4}`;
965
1310
  async function api3(path, init) {
966
- const res = await fetch(`${BASE5}${path}`, init);
1311
+ const res = await fetch(`${BASE4}${path}`, init);
967
1312
  if (!res.ok) {
968
1313
  const text = await res.text().catch(() => "");
969
1314
  throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
@@ -987,7 +1332,7 @@ async function listSkillsCmd() {
987
1332
  );
988
1333
  }
989
1334
  } catch (err) {
990
- console.error(`[solix] could not reach server at ${BASE5}: ${String(err)}`);
1335
+ console.error(`[solix] could not reach server at ${BASE4}: ${String(err)}`);
991
1336
  process.exitCode = 1;
992
1337
  }
993
1338
  }
@@ -1020,8 +1365,145 @@ async function installSkillCmd(id, projectId) {
1020
1365
  }
1021
1366
  }
1022
1367
 
1368
+ // src/schedule.ts
1369
+ var PORT5 = process.env.SOLIX_PORT ?? "4242";
1370
+ var BASE5 = `http://127.0.0.1:${PORT5}`;
1371
+ async function api4(path, init) {
1372
+ const res = await fetch(`${BASE5}${path}`, {
1373
+ ...init,
1374
+ headers: { "content-type": "application/json", ...init?.headers ?? {} }
1375
+ });
1376
+ if (!res.ok) {
1377
+ const text = await res.text().catch(() => "");
1378
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
1379
+ }
1380
+ return await res.json();
1381
+ }
1382
+ function unreachable(err) {
1383
+ console.error(`[solix] could not reach server at ${BASE5}: ${String(err)}`);
1384
+ console.error("[solix] is `solix start` running?");
1385
+ process.exitCode = 1;
1386
+ }
1387
+ async function listSchedulesCmd() {
1388
+ try {
1389
+ const list = await api4("/api/schedules");
1390
+ if (!list.length) {
1391
+ console.log("No schedules. Add one with `solix schedule add`.");
1392
+ return;
1393
+ }
1394
+ console.log("id every state next run prompt");
1395
+ for (const s of list) {
1396
+ const next = new Date(s.nextRunAt).toLocaleString();
1397
+ const state = s.enabled ? "on " : "off";
1398
+ console.log(
1399
+ ` ${s.id.padEnd(8)} ${s.cron.padEnd(5)} [${state}] ${next.padEnd(20)} ${s.prompt.slice(0, 40)}`
1400
+ );
1401
+ }
1402
+ } catch (err) {
1403
+ unreachable(err);
1404
+ }
1405
+ }
1406
+ async function addScheduleCmd(prompt, opts) {
1407
+ const cwd = opts.cwd ?? process.cwd();
1408
+ const cadence = opts.every ?? "1h";
1409
+ try {
1410
+ const s = await api4("/api/schedules", {
1411
+ method: "POST",
1412
+ body: JSON.stringify({ cwd, prompt, cadence, name: opts.name })
1413
+ });
1414
+ console.log(
1415
+ `[solix] scheduled ${s.id} \u2014 every ${s.cron} in ${cwd}
1416
+ next run: ${new Date(s.nextRunAt).toLocaleString()}`
1417
+ );
1418
+ } catch (err) {
1419
+ unreachable(err);
1420
+ }
1421
+ }
1422
+ async function toggle(id, enabled) {
1423
+ try {
1424
+ await api4(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
1425
+ method: "POST",
1426
+ body: JSON.stringify({ enabled })
1427
+ });
1428
+ console.log(`[solix] schedule ${id} \u2192 ${enabled ? "enabled" : "disabled"}`);
1429
+ } catch (err) {
1430
+ unreachable(err);
1431
+ }
1432
+ }
1433
+ var enableScheduleCmd = (id) => toggle(id, true);
1434
+ var disableScheduleCmd = (id) => toggle(id, false);
1435
+ async function removeScheduleCmd(id) {
1436
+ try {
1437
+ await api4(`/api/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
1438
+ console.log(`[solix] removed schedule ${id}`);
1439
+ } catch (err) {
1440
+ unreachable(err);
1441
+ }
1442
+ }
1443
+
1444
+ // src/goals.ts
1445
+ var PORT6 = process.env.SOLIX_PORT ?? "4242";
1446
+ var BASE6 = `http://127.0.0.1:${PORT6}`;
1447
+ async function api5(path, init) {
1448
+ const res = await fetch(`${BASE6}${path}`, {
1449
+ ...init,
1450
+ headers: { "content-type": "application/json", ...init?.headers ?? {} }
1451
+ });
1452
+ if (!res.ok) {
1453
+ const text = await res.text().catch(() => "");
1454
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
1455
+ }
1456
+ return await res.json();
1457
+ }
1458
+ function unreachable2(err) {
1459
+ console.error(`[solix] could not reach server at ${BASE6}: ${String(err)}`);
1460
+ console.error("[solix] is `solix start` running?");
1461
+ process.exitCode = 1;
1462
+ }
1463
+ async function listGoalsCmd() {
1464
+ try {
1465
+ const goals2 = await api5("/api/goals");
1466
+ if (!goals2.length) {
1467
+ console.log('No goals. Add one with `solix goal add "<name>"`.');
1468
+ return;
1469
+ }
1470
+ console.log("id color name");
1471
+ for (const g of goals2) {
1472
+ console.log(` ${g.id.padEnd(8)} ${g.color.padEnd(8)} ${g.name}`);
1473
+ }
1474
+ } catch (err) {
1475
+ unreachable2(err);
1476
+ }
1477
+ }
1478
+ async function addGoalCmd(name, opts) {
1479
+ try {
1480
+ const g = await api5("/api/goals", {
1481
+ method: "POST",
1482
+ body: JSON.stringify({
1483
+ name,
1484
+ description: opts.description,
1485
+ color: opts.color
1486
+ })
1487
+ });
1488
+ console.log(`[solix] created goal ${g.id} \u2014 "${g.name}" (${g.color})`);
1489
+ } catch (err) {
1490
+ unreachable2(err);
1491
+ }
1492
+ }
1493
+ async function removeGoalCmd(id) {
1494
+ try {
1495
+ await api5(`/api/goals/${encodeURIComponent(id)}`, { method: "DELETE" });
1496
+ console.log(`[solix] removed goal ${id}`);
1497
+ } catch (err) {
1498
+ unreachable2(err);
1499
+ }
1500
+ }
1501
+
1023
1502
  // ../server/src/create.ts
1024
1503
  import { serve } from "@hono/node-server";
1504
+ import { readFileSync as readFileSync9 } from "fs";
1505
+ import { homedir as homedir11 } from "os";
1506
+ import { join as join15 } from "path";
1025
1507
 
1026
1508
  // ../server/src/broadcaster.ts
1027
1509
  var Broadcaster = class {
@@ -1055,14 +1537,14 @@ import Database from "better-sqlite3";
1055
1537
  // ../server/src/paths.ts
1056
1538
  import { homedir as homedir5 } from "os";
1057
1539
  import { join as join7 } from "path";
1058
- import { mkdirSync as mkdirSync3 } from "fs";
1059
- var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join7(homedir5(), ".solix");
1060
- var DB_PATH = join7(SOLIX_HOME2, "solix.db");
1061
- var HOOKS_DIR2 = join7(SOLIX_HOME2, "hooks");
1062
- var LOG_PATH = join7(SOLIX_HOME2, "solix.log");
1540
+ import { mkdirSync as mkdirSync4 } from "fs";
1541
+ var SOLIX_HOME3 = process.env.SOLIX_HOME ?? join7(homedir5(), ".solix");
1542
+ var DB_PATH = process.env.SOLIX_DB_PATH ?? join7(SOLIX_HOME3, "solix.db");
1543
+ var HOOKS_DIR2 = join7(SOLIX_HOME3, "hooks");
1544
+ var LOG_PATH = join7(SOLIX_HOME3, "solix.log");
1063
1545
  function ensureSolixHome() {
1064
- mkdirSync3(SOLIX_HOME2, { recursive: true });
1065
- mkdirSync3(HOOKS_DIR2, { recursive: true });
1546
+ mkdirSync4(SOLIX_HOME3, { recursive: true });
1547
+ mkdirSync4(HOOKS_DIR2, { recursive: true });
1066
1548
  }
1067
1549
 
1068
1550
  // ../server/src/db.ts
@@ -1197,6 +1679,14 @@ CREATE TABLE IF NOT EXISTS scheduled_tasks (
1197
1679
  last_run_at INTEGER,
1198
1680
  next_run_at INTEGER NOT NULL
1199
1681
  );
1682
+
1683
+ CREATE TABLE IF NOT EXISTS goals (
1684
+ id TEXT PRIMARY KEY,
1685
+ name TEXT NOT NULL,
1686
+ description TEXT,
1687
+ color TEXT NOT NULL,
1688
+ created_at INTEGER NOT NULL
1689
+ );
1200
1690
  `;
1201
1691
  var _db = null;
1202
1692
  function ensureColumn(db, table, column, ddl) {
@@ -1222,14 +1712,20 @@ function getDb() {
1222
1712
  ensureColumn(db, "sessions", "pr_check_status", "pr_check_status TEXT");
1223
1713
  ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
1224
1714
  ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
1715
+ ensureColumn(db, "sessions", "cost_usd", "cost_usd REAL DEFAULT 0");
1716
+ ensureColumn(db, "sessions", "budget_usd", "budget_usd REAL");
1717
+ ensureColumn(db, "sessions", "current_goal_id", "current_goal_id TEXT");
1718
+ ensureColumn(db, "missions", "goal_id", "goal_id TEXT");
1719
+ ensureColumn(db, "scheduled_tasks", "cwd", "cwd TEXT");
1720
+ ensureColumn(db, "scheduled_tasks", "name", "name TEXT");
1225
1721
  _db = db;
1226
1722
  return db;
1227
1723
  }
1228
1724
 
1229
1725
  // ../server/src/http.ts
1230
- import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
1231
- import { dirname as dirname4, extname, join as join11, resolve as resolve3 } from "path";
1232
- import { fileURLToPath as fileURLToPath4 } from "url";
1726
+ import { existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
1727
+ import { dirname as dirname5, extname, join as join11, resolve as resolve3 } from "path";
1728
+ import { fileURLToPath as fileURLToPath5 } from "url";
1233
1729
  import { spawnSync } from "child_process";
1234
1730
  import { Hono } from "hono";
1235
1731
  import { cors } from "hono/cors";
@@ -1311,7 +1807,10 @@ function rowToSession(row) {
1311
1807
  agentViewId: row.agent_view_id ?? void 0,
1312
1808
  agentViewSummary: row.agent_view_summary ?? void 0,
1313
1809
  prUrl: row.pr_url ?? void 0,
1314
- prCheckStatus: row.pr_check_status ?? void 0
1810
+ prCheckStatus: row.pr_check_status ?? void 0,
1811
+ costUsd: row.cost_usd ?? 0,
1812
+ budgetUsd: row.budget_usd ?? void 0,
1813
+ currentGoalId: row.current_goal_id ?? void 0
1315
1814
  };
1316
1815
  }
1317
1816
  function nextOrbitSlot(db, projectId) {
@@ -1392,7 +1891,8 @@ function upsertSession(db, input) {
1392
1891
  agentViewId: input.agentViewId,
1393
1892
  agentViewSummary: input.agentViewSummary,
1394
1893
  prUrl: input.prUrl,
1395
- prCheckStatus: input.prCheckStatus
1894
+ prCheckStatus: input.prCheckStatus,
1895
+ costUsd: 0
1396
1896
  };
1397
1897
  }
1398
1898
  function setAgentViewFields(db, sessionId, fields) {
@@ -1456,6 +1956,27 @@ function setSessionContextUsage(db, sessionId, pct) {
1456
1956
  ).run(clamped, ts2, sessionId);
1457
1957
  return getSession(db, sessionId);
1458
1958
  }
1959
+ function setSessionCost(db, sessionId, costUsd) {
1960
+ const ts2 = now();
1961
+ db.prepare(
1962
+ `UPDATE sessions SET cost_usd = ?, updated_at = ? WHERE id = ?`
1963
+ ).run(Math.max(0, costUsd), ts2, sessionId);
1964
+ return getSession(db, sessionId);
1965
+ }
1966
+ function setSessionBudget(db, sessionId, budgetUsd) {
1967
+ const ts2 = now();
1968
+ db.prepare(
1969
+ `UPDATE sessions SET budget_usd = ?, updated_at = ? WHERE id = ?`
1970
+ ).run(budgetUsd, ts2, sessionId);
1971
+ return getSession(db, sessionId);
1972
+ }
1973
+ function setSessionGoal(db, sessionId, goalId) {
1974
+ const ts2 = now();
1975
+ db.prepare(
1976
+ `UPDATE sessions SET current_goal_id = ?, updated_at = ? WHERE id = ?`
1977
+ ).run(goalId, ts2, sessionId);
1978
+ return getSession(db, sessionId);
1979
+ }
1459
1980
  function getSession(db, sessionId) {
1460
1981
  const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1461
1982
  return row ? rowToSession(row) : null;
@@ -1504,7 +2025,8 @@ function rowToMission(row) {
1504
2025
  toolCallCount: row.tool_call_count
1505
2026
  },
1506
2027
  filesTouched,
1507
- errorSummary: row.error_summary ?? void 0
2028
+ errorSummary: row.error_summary ?? void 0,
2029
+ goalId: row.goal_id ?? void 0
1508
2030
  };
1509
2031
  }
1510
2032
  function setMissionError(db, missionId, errorSummary) {
@@ -1520,14 +2042,14 @@ function shortNameFromPrompt(prompt) {
1520
2042
  (w) => w.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/^./, (c) => c.toUpperCase())
1521
2043
  ).filter(Boolean).join(" ") || "New Mission";
1522
2044
  }
1523
- function startMission(db, sessionId, prompt) {
2045
+ function startMission(db, sessionId, prompt, goalId) {
1524
2046
  const id = nanoid2();
1525
2047
  const ts2 = now();
1526
2048
  const shortName = shortNameFromPrompt(prompt);
1527
2049
  db.prepare(
1528
- `INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json)
1529
- VALUES (?, ?, ?, ?, 'active', ?, '[]')`
1530
- ).run(id, sessionId, prompt, shortName, ts2);
2050
+ `INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json, goal_id)
2051
+ VALUES (?, ?, ?, ?, 'active', ?, '[]', ?)`
2052
+ ).run(id, sessionId, prompt, shortName, ts2, goalId ?? null);
1531
2053
  return {
1532
2054
  id,
1533
2055
  sessionId,
@@ -1536,9 +2058,16 @@ function startMission(db, sessionId, prompt) {
1536
2058
  shortName,
1537
2059
  status: "active",
1538
2060
  metrics: { subagentCount: 0, toolCallCount: 0 },
1539
- filesTouched: []
2061
+ filesTouched: [],
2062
+ goalId
1540
2063
  };
1541
2064
  }
2065
+ function addMissionTokens(db, missionId, tokens) {
2066
+ if (tokens <= 0) return;
2067
+ db.prepare(
2068
+ `UPDATE missions SET total_tokens = COALESCE(total_tokens, 0) + ? WHERE id = ?`
2069
+ ).run(Math.round(tokens), missionId);
2070
+ }
1542
2071
  function completeMission(db, missionId, status = "completed") {
1543
2072
  const ts2 = now();
1544
2073
  const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
@@ -1778,14 +2307,14 @@ function listAudit(db, opts = {}) {
1778
2307
  }
1779
2308
 
1780
2309
  // ../server/src/state/advisors.ts
1781
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1782
- import { dirname as dirname2, join as join8, resolve } from "path";
1783
- import { fileURLToPath as fileURLToPath2 } from "url";
2310
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2311
+ import { dirname as dirname3, join as join8, resolve } from "path";
2312
+ import { fileURLToPath as fileURLToPath3 } from "url";
1784
2313
  function findAgentsDir() {
1785
- if (process.env.SOLIX_AGENTS_DIR && existsSync5(process.env.SOLIX_AGENTS_DIR)) {
2314
+ if (process.env.SOLIX_AGENTS_DIR && existsSync6(process.env.SOLIX_AGENTS_DIR)) {
1786
2315
  return process.env.SOLIX_AGENTS_DIR;
1787
2316
  }
1788
- const here = dirname2(fileURLToPath2(import.meta.url));
2317
+ const here = dirname3(fileURLToPath3(import.meta.url));
1789
2318
  const candidates = [
1790
2319
  // Bundled npm package: agents/ ships next to the bundled JS file.
1791
2320
  resolve(here, "agents"),
@@ -1795,17 +2324,17 @@ function findAgentsDir() {
1795
2324
  resolve(process.cwd(), "packages", "agents")
1796
2325
  ];
1797
2326
  for (const c of candidates) {
1798
- if (existsSync5(join8(c, "manifest.json"))) return c;
2327
+ if (existsSync6(join8(c, "manifest.json"))) return c;
1799
2328
  }
1800
2329
  return candidates[0];
1801
2330
  }
1802
2331
  var AGENTS_DIR = findAgentsDir();
1803
2332
  function readManifest() {
1804
2333
  const path = join8(AGENTS_DIR, "manifest.json");
1805
- if (!existsSync5(path)) {
2334
+ if (!existsSync6(path)) {
1806
2335
  return { version: 1, advisors: [] };
1807
2336
  }
1808
- return JSON.parse(readFileSync4(path, "utf8"));
2337
+ return JSON.parse(readFileSync5(path, "utf8"));
1809
2338
  }
1810
2339
  function rowToAdvisor(row) {
1811
2340
  let requiredSkills = [];
@@ -1906,15 +2435,15 @@ function setAdvisorPinned(db, id, pinned, sessionId) {
1906
2435
  return getAdvisor(db, id);
1907
2436
  }
1908
2437
  function readAdvisorAgentMd(advisor) {
1909
- if (!existsSync5(advisor.agentMdPath)) {
2438
+ if (!existsSync6(advisor.agentMdPath)) {
1910
2439
  return "";
1911
2440
  }
1912
- return readFileSync4(advisor.agentMdPath, "utf8");
2441
+ return readFileSync5(advisor.agentMdPath, "utf8");
1913
2442
  }
1914
2443
 
1915
2444
  // ../server/src/state/wrappers.ts
1916
2445
  import { connect } from "net";
1917
- import { existsSync as existsSync6, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "fs";
2446
+ import { existsSync as existsSync7, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
1918
2447
  import { homedir as homedir6 } from "os";
1919
2448
  import { join as join9 } from "path";
1920
2449
  var wrappers = /* @__PURE__ */ new Map();
@@ -1937,12 +2466,12 @@ function listWrappers() {
1937
2466
  }
1938
2467
  function cleanupOrphanedSockets() {
1939
2468
  const dir = join9(homedir6(), ".solix", "wrappers");
1940
- if (!existsSync6(dir)) return 0;
2469
+ if (!existsSync7(dir)) return 0;
1941
2470
  let removed = 0;
1942
2471
  for (const f of readdirSync3(dir)) {
1943
2472
  if (!f.endsWith(".sock")) continue;
1944
2473
  try {
1945
- unlinkSync2(join9(dir, f));
2474
+ unlinkSync3(join9(dir, f));
1946
2475
  removed++;
1947
2476
  } catch {
1948
2477
  }
@@ -1963,7 +2492,7 @@ function claimWrapperForCwd(cwd) {
1963
2492
  return best;
1964
2493
  }
1965
2494
  function writeToWrapperSocket(socketPath, text) {
1966
- if (!existsSync6(socketPath)) return false;
2495
+ if (!existsSync7(socketPath)) return false;
1967
2496
  try {
1968
2497
  const client = connect(socketPath);
1969
2498
  client.on("error", () => {
@@ -1980,6 +2509,138 @@ function writeToWrapperSocket(socketPath, text) {
1980
2509
  }
1981
2510
  }
1982
2511
 
2512
+ // ../server/src/state/schedules.ts
2513
+ import { nanoid as nanoid4 } from "nanoid";
2514
+ function rowToSchedule(row) {
2515
+ return {
2516
+ id: row.id,
2517
+ projectId: row.project_id,
2518
+ cwd: row.cwd ?? "",
2519
+ name: row.name ?? void 0,
2520
+ prompt: row.prompt,
2521
+ cron: row.cron,
2522
+ enabled: row.enabled !== 0,
2523
+ lastRunAt: row.last_run_at ?? void 0,
2524
+ nextRunAt: row.next_run_at
2525
+ };
2526
+ }
2527
+ function cadenceToMs(cadence) {
2528
+ const m = cadence.trim().match(/^(\d+)\s*([mhd])$/i);
2529
+ if (!m) return null;
2530
+ const n = Number(m[1]);
2531
+ if (!Number.isFinite(n) || n <= 0) return null;
2532
+ const unit = m[2].toLowerCase();
2533
+ const mult = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2534
+ return n * mult;
2535
+ }
2536
+ function nextRunFrom(fromMs, cadence) {
2537
+ const ms = cadenceToMs(cadence);
2538
+ return fromMs + (ms ?? 36e5);
2539
+ }
2540
+ function createSchedule(db, input) {
2541
+ const project = ensureProject(db, input.cwd);
2542
+ const id = nanoid4(8);
2543
+ const ts2 = now();
2544
+ const nextRun = nextRunFrom(ts2, input.cadence);
2545
+ db.prepare(
2546
+ `INSERT INTO scheduled_tasks
2547
+ (id, project_id, cwd, name, prompt, cron, enabled, last_run_at, next_run_at)
2548
+ VALUES (?, ?, ?, ?, ?, ?, 1, NULL, ?)`
2549
+ ).run(id, project.id, input.cwd, input.name ?? null, input.prompt, input.cadence, nextRun);
2550
+ return getSchedule(db, id);
2551
+ }
2552
+ function getSchedule(db, id) {
2553
+ const row = db.prepare("SELECT * FROM scheduled_tasks WHERE id = ?").get(id);
2554
+ return row ? rowToSchedule(row) : null;
2555
+ }
2556
+ function listSchedules(db) {
2557
+ const rows = db.prepare("SELECT * FROM scheduled_tasks ORDER BY next_run_at ASC").all();
2558
+ return rows.map(rowToSchedule);
2559
+ }
2560
+ function listDueSchedules(db, asOf) {
2561
+ const rows = db.prepare(
2562
+ `SELECT * FROM scheduled_tasks WHERE enabled = 1 AND next_run_at <= ?`
2563
+ ).all(asOf);
2564
+ return rows.map(rowToSchedule);
2565
+ }
2566
+ function setScheduleEnabled(db, id, enabled) {
2567
+ db.prepare("UPDATE scheduled_tasks SET enabled = ? WHERE id = ?").run(
2568
+ enabled ? 1 : 0,
2569
+ id
2570
+ );
2571
+ return getSchedule(db, id);
2572
+ }
2573
+ function markScheduleRun(db, id) {
2574
+ const sched = getSchedule(db, id);
2575
+ if (!sched) return null;
2576
+ const ts2 = now();
2577
+ const next = nextRunFrom(ts2, sched.cron);
2578
+ db.prepare(
2579
+ "UPDATE scheduled_tasks SET last_run_at = ?, next_run_at = ? WHERE id = ?"
2580
+ ).run(ts2, next, id);
2581
+ return getSchedule(db, id);
2582
+ }
2583
+ function deleteSchedule(db, id) {
2584
+ const res = db.prepare("DELETE FROM scheduled_tasks WHERE id = ?").run(id);
2585
+ return res.changes > 0;
2586
+ }
2587
+
2588
+ // ../server/src/state/goals.ts
2589
+ import { nanoid as nanoid5 } from "nanoid";
2590
+ var PALETTE = [
2591
+ "#38bdf8",
2592
+ // sky
2593
+ "#a78bfa",
2594
+ // violet
2595
+ "#34d399",
2596
+ // emerald
2597
+ "#fbbf24",
2598
+ // amber
2599
+ "#f472b6",
2600
+ // pink
2601
+ "#f87171",
2602
+ // red
2603
+ "#22d3ee",
2604
+ // cyan
2605
+ "#c084fc"
2606
+ // purple
2607
+ ];
2608
+ function rowToGoal(row) {
2609
+ return {
2610
+ id: row.id,
2611
+ name: row.name,
2612
+ description: row.description ?? void 0,
2613
+ color: row.color,
2614
+ createdAt: row.created_at
2615
+ };
2616
+ }
2617
+ function nextColor(db) {
2618
+ const count = db.prepare("SELECT COUNT(*) AS n FROM goals").get().n;
2619
+ return PALETTE[count % PALETTE.length];
2620
+ }
2621
+ function createGoal(db, input) {
2622
+ const id = nanoid5(8);
2623
+ const ts2 = now();
2624
+ const color = input.color ?? nextColor(db);
2625
+ db.prepare(
2626
+ `INSERT INTO goals (id, name, description, color, created_at)
2627
+ VALUES (?, ?, ?, ?, ?)`
2628
+ ).run(id, input.name, input.description ?? null, color, ts2);
2629
+ return { id, name: input.name, description: input.description, color, createdAt: ts2 };
2630
+ }
2631
+ function listGoals(db) {
2632
+ const rows = db.prepare("SELECT * FROM goals ORDER BY created_at ASC").all();
2633
+ return rows.map(rowToGoal);
2634
+ }
2635
+ function deleteGoal(db, id) {
2636
+ db.prepare(
2637
+ `UPDATE sessions SET current_goal_id = NULL WHERE current_goal_id = ?`
2638
+ ).run(id);
2639
+ db.prepare(`UPDATE missions SET goal_id = NULL WHERE goal_id = ?`).run(id);
2640
+ const res = db.prepare("DELETE FROM goals WHERE id = ?").run(id);
2641
+ return res.changes > 0;
2642
+ }
2643
+
1983
2644
  // ../server/src/state/context.ts
1984
2645
  var MISSIONS_FOR_HANDOFF = 3;
1985
2646
  var DEFAULT_ASKS = {
@@ -2067,19 +2728,19 @@ function buildContextEnvelope(db, args) {
2067
2728
  }
2068
2729
 
2069
2730
  // ../server/src/state/skills.ts
2070
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
2071
- import { dirname as dirname3, join as join10, resolve as resolve2 } from "path";
2072
- import { fileURLToPath as fileURLToPath3 } from "url";
2731
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
2732
+ import { dirname as dirname4, join as join10, resolve as resolve2 } from "path";
2733
+ import { fileURLToPath as fileURLToPath4 } from "url";
2073
2734
  import { homedir as homedir7 } from "os";
2074
2735
  function findSolixSkillsDir() {
2075
- const here = dirname3(fileURLToPath3(import.meta.url));
2736
+ const here = dirname4(fileURLToPath4(import.meta.url));
2076
2737
  const candidates = [
2077
2738
  resolve2(here, "..", "..", "..", "skills"),
2078
2739
  resolve2(here, "..", "..", "skills"),
2079
2740
  resolve2(process.cwd(), "packages", "skills")
2080
2741
  ];
2081
2742
  for (const c of candidates) {
2082
- if (existsSync7(c)) return c;
2743
+ if (existsSync8(c)) return c;
2083
2744
  }
2084
2745
  return candidates[0];
2085
2746
  }
@@ -2087,7 +2748,7 @@ var SOLIX_SKILLS_DIR2 = findSolixSkillsDir();
2087
2748
  var ANTHROPIC_SKILLS_DIR = join10(homedir7(), ".claude", "skills");
2088
2749
  function parseSkillManifest(manifestPath, fallbackId) {
2089
2750
  try {
2090
- const txt = readFileSync5(manifestPath, "utf8");
2751
+ const txt = readFileSync6(manifestPath, "utf8");
2091
2752
  const match = txt.match(/^---\n([\s\S]*?)\n---/);
2092
2753
  let name = fallbackId;
2093
2754
  let description = "";
@@ -2136,7 +2797,7 @@ function discoverSkills(db) {
2136
2797
  { dir: SOLIX_SKILLS_DIR2, source: "solix" }
2137
2798
  ];
2138
2799
  for (const { dir, source } of sources) {
2139
- if (!existsSync7(dir)) continue;
2800
+ if (!existsSync8(dir)) continue;
2140
2801
  for (const entry of readdirSync4(dir)) {
2141
2802
  const full = join10(dir, entry);
2142
2803
  let isDir = false;
@@ -2147,7 +2808,7 @@ function discoverSkills(db) {
2147
2808
  }
2148
2809
  if (!isDir) continue;
2149
2810
  const manifestPath = join10(full, "SKILL.md");
2150
- if (!existsSync7(manifestPath)) continue;
2811
+ if (!existsSync8(manifestPath)) continue;
2151
2812
  const parsed = parseSkillManifest(manifestPath, entry);
2152
2813
  if (!parsed) continue;
2153
2814
  const id = `${source}:${parsed.id}`;
@@ -2172,8 +2833,8 @@ function getSkill(db, id) {
2172
2833
  return row ? rowToSkill(row) : null;
2173
2834
  }
2174
2835
  function readSkillManifest(skill) {
2175
- if (!existsSync7(skill.manifestPath)) return "";
2176
- return readFileSync5(skill.manifestPath, "utf8");
2836
+ if (!existsSync8(skill.manifestPath)) return "";
2837
+ return readFileSync6(skill.manifestPath, "utf8");
2177
2838
  }
2178
2839
  function recordSkillInstall(db, skillId, projectId) {
2179
2840
  const skill = getSkill(db, skillId);
@@ -2186,7 +2847,7 @@ function recordSkillInstall(db, skillId, projectId) {
2186
2847
  }
2187
2848
 
2188
2849
  // ../server/src/state/galaxy.ts
2189
- import { nanoid as nanoid4 } from "nanoid";
2850
+ import { nanoid as nanoid6 } from "nanoid";
2190
2851
  function exportManifest(db, opts = {}) {
2191
2852
  const advisors2 = listAdvisors(db);
2192
2853
  const skills2 = listSkills(db);
@@ -2234,7 +2895,7 @@ function importManifest(db, manifest, sourceUrl) {
2234
2895
  db.prepare(
2235
2896
  `INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
2236
2897
  VALUES (?, ?, ?, ?)`
2237
- ).run(nanoid4(), sourceUrl ?? null, JSON.stringify(manifest), now());
2898
+ ).run(nanoid6(), sourceUrl ?? null, JSON.stringify(manifest), now());
2238
2899
  return {
2239
2900
  advisorsEnabled: enabled,
2240
2901
  advisorsDisabled: disabled,
@@ -2295,7 +2956,7 @@ function snapshotExport(db, manifest) {
2295
2956
  return rowToVersion(existing);
2296
2957
  }
2297
2958
  }
2298
- const id = nanoid4();
2959
+ const id = nanoid6();
2299
2960
  const ts2 = now();
2300
2961
  const ordinal = (last?.ordinal ?? 0) + 1;
2301
2962
  db.prepare(
@@ -2373,10 +3034,33 @@ function diffManifests(a, b) {
2373
3034
  };
2374
3035
  }
2375
3036
 
3037
+ // ../shared/src/pricing.ts
3038
+ var MODEL_PRICING = {
3039
+ opus: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
3040
+ sonnet: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
3041
+ haiku: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }
3042
+ };
3043
+ function pricingFor(model) {
3044
+ const m = (model ?? "").toLowerCase();
3045
+ if (m.includes("opus")) return MODEL_PRICING.opus;
3046
+ if (m.includes("haiku")) return MODEL_PRICING.haiku;
3047
+ return MODEL_PRICING.sonnet;
3048
+ }
3049
+ function costForUsage(model, usage) {
3050
+ if (!usage) return 0;
3051
+ const p = pricingFor(model);
3052
+ const cost = ((usage.input_tokens ?? 0) * p.input + (usage.output_tokens ?? 0) * p.output + (usage.cache_read_input_tokens ?? 0) * p.cacheRead + (usage.cache_creation_input_tokens ?? 0) * p.cacheWrite) / 1e6;
3053
+ return cost;
3054
+ }
3055
+ function totalTokens(usage) {
3056
+ if (!usage) return 0;
3057
+ return (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
3058
+ }
3059
+
2376
3060
  // ../server/src/cloud.ts
2377
3061
  var RegistryClient = class {
2378
- constructor(baseUrl = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
2379
- this.baseUrl = baseUrl;
3062
+ constructor(baseUrl2 = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
3063
+ this.baseUrl = baseUrl2;
2380
3064
  this.apiKey = apiKey;
2381
3065
  }
2382
3066
  baseUrl;
@@ -2446,6 +3130,42 @@ var RegistryClient = class {
2446
3130
  }
2447
3131
  };
2448
3132
 
3133
+ // ../server/src/origins.ts
3134
+ function isAllowedOrigin(origin) {
3135
+ if (!origin) return true;
3136
+ let hostname;
3137
+ try {
3138
+ hostname = new URL(origin).hostname;
3139
+ } catch {
3140
+ return false;
3141
+ }
3142
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
3143
+ }
3144
+ function isSafeFetchUrl(raw) {
3145
+ let u;
3146
+ try {
3147
+ u = new URL(raw);
3148
+ } catch {
3149
+ return false;
3150
+ }
3151
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
3152
+ const h = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
3153
+ if (h === "localhost" || h === "::1" || h.endsWith(".localhost")) return false;
3154
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
3155
+ if (m) {
3156
+ const a = Number(m[1]);
3157
+ const b = Number(m[2]);
3158
+ if (a === 0 || a === 127 || a === 10) return false;
3159
+ if (a === 169 && b === 254) return false;
3160
+ if (a === 172 && b >= 16 && b <= 31) return false;
3161
+ if (a === 192 && b === 168) return false;
3162
+ }
3163
+ if (h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80")) {
3164
+ return false;
3165
+ }
3166
+ return true;
3167
+ }
3168
+
2449
3169
  // ../server/src/http.ts
2450
3170
  function isAgentViewVersion(version) {
2451
3171
  if (!version) return false;
@@ -2462,14 +3182,44 @@ function isAgentViewVersion(version) {
2462
3182
  }
2463
3183
  function createHttpApp(opts) {
2464
3184
  const app = new Hono();
2465
- app.use("*", cors());
3185
+ app.use(
3186
+ "*",
3187
+ cors({
3188
+ origin: [
3189
+ "http://127.0.0.1:4242",
3190
+ "http://localhost:4242",
3191
+ "http://127.0.0.1:4243",
3192
+ "http://localhost:4243"
3193
+ ]
3194
+ })
3195
+ );
3196
+ app.use("*", async (c, next) => {
3197
+ const method = c.req.method;
3198
+ const mutating = method !== "GET" && method !== "HEAD" && method !== "OPTIONS";
3199
+ if (mutating && !isAllowedOrigin(c.req.header("origin"))) {
3200
+ return c.json({ error: "cross-origin request refused" }, 403);
3201
+ }
3202
+ await next();
3203
+ });
3204
+ if (opts.token) {
3205
+ const expected = opts.token;
3206
+ const paths = ["/events", "/events/permission"];
3207
+ for (const p of paths) {
3208
+ app.use(p, async (c, next) => {
3209
+ if (c.req.header("x-solix-token") !== expected) {
3210
+ return c.json({ error: "unauthorized" }, 401);
3211
+ }
3212
+ await next();
3213
+ });
3214
+ }
3215
+ }
2466
3216
  const registry = new RegistryClient();
2467
3217
  app.get(
2468
3218
  "/api/health",
2469
3219
  (c) => c.json({
2470
3220
  ok: true,
2471
3221
  service: "solix",
2472
- version: "1.0.0",
3222
+ version: opts.version ?? "unknown",
2473
3223
  ts: Date.now()
2474
3224
  })
2475
3225
  );
@@ -2486,6 +3236,18 @@ function createHttpApp(opts) {
2486
3236
  }
2487
3237
  return c.json({ ok: true });
2488
3238
  });
3239
+ app.post("/events/permission", async (c) => {
3240
+ let body = null;
3241
+ try {
3242
+ body = await c.req.json();
3243
+ } catch {
3244
+ return c.json({ decision: "allow" });
3245
+ }
3246
+ if (!body || !body.event) return c.json({ decision: "allow" });
3247
+ const result = await opts.router.requestPermission(body);
3248
+ const decision = result.timedOut ? "timeout" : result.approved ? "allow" : "deny";
3249
+ return c.json({ decision });
3250
+ });
2489
3251
  app.get("/api/projects", (c) => c.json(listProjects(opts.db)));
2490
3252
  app.get("/api/projects/:id/sessions", (c) => {
2491
3253
  const id = c.req.param("id");
@@ -2566,11 +3328,11 @@ function createHttpApp(opts) {
2566
3328
  return c.json({ ...a, agentMd: readAdvisorAgentMd(a) });
2567
3329
  });
2568
3330
  app.post("/api/advisors/:id/enable", (c) => {
2569
- const a = setAdvisorEnabled(opts.db, c.req.param("id"), true);
3331
+ const a = opts.router.setAdvisorEnabled(c.req.param("id"), true);
2570
3332
  return c.json({ ok: Boolean(a), advisor: a });
2571
3333
  });
2572
3334
  app.post("/api/advisors/:id/disable", (c) => {
2573
- const a = setAdvisorEnabled(opts.db, c.req.param("id"), false);
3335
+ const a = opts.router.setAdvisorEnabled(c.req.param("id"), false);
2574
3336
  return c.json({ ok: Boolean(a), advisor: a });
2575
3337
  });
2576
3338
  app.post("/api/advisors/:id/pin", (c) => {
@@ -2673,6 +3435,12 @@ function createHttpApp(opts) {
2673
3435
  let sourceUrl;
2674
3436
  if ("url" in body && typeof body.url === "string") {
2675
3437
  sourceUrl = body.url;
3438
+ if (!isSafeFetchUrl(body.url)) {
3439
+ return c.json(
3440
+ { error: "import URL not allowed (must be a public http(s) address)" },
3441
+ 400
3442
+ );
3443
+ }
2676
3444
  try {
2677
3445
  const res = await fetch(body.url, {
2678
3446
  signal: AbortSignal.timeout(5e3)
@@ -2726,6 +3494,52 @@ function createHttpApp(opts) {
2726
3494
  return c.json({ ok: true });
2727
3495
  });
2728
3496
  app.get("/api/wrappers", (c) => c.json(listWrappers()));
3497
+ app.get("/api/schedules", (c) => c.json(listSchedules(opts.db)));
3498
+ app.post("/api/schedules", async (c) => {
3499
+ const body = await c.req.json().catch(() => ({}));
3500
+ if (!body.cwd || !body.prompt || !body.cadence) {
3501
+ return c.json({ error: "cwd, prompt, cadence required" }, 400);
3502
+ }
3503
+ const schedule = createSchedule(opts.db, {
3504
+ cwd: body.cwd,
3505
+ prompt: body.prompt,
3506
+ cadence: body.cadence,
3507
+ name: body.name
3508
+ });
3509
+ opts.router.broadcastScheduleUpsert(schedule);
3510
+ return c.json(schedule);
3511
+ });
3512
+ app.post("/api/schedules/:id/toggle", async (c) => {
3513
+ const body = await c.req.json().catch(() => ({}));
3514
+ const s = setScheduleEnabled(opts.db, c.req.param("id"), Boolean(body.enabled));
3515
+ if (!s) return c.json({ error: "not found" }, 404);
3516
+ opts.router.broadcastScheduleUpsert(s);
3517
+ return c.json(s);
3518
+ });
3519
+ app.delete("/api/schedules/:id", (c) => {
3520
+ const id = c.req.param("id");
3521
+ const ok = deleteSchedule(opts.db, id);
3522
+ if (ok) opts.router.broadcastScheduleRemove(id);
3523
+ return c.json({ ok });
3524
+ });
3525
+ app.get("/api/goals", (c) => c.json(listGoals(opts.db)));
3526
+ app.post("/api/goals", async (c) => {
3527
+ const body = await c.req.json().catch(() => ({}));
3528
+ if (!body.name) return c.json({ error: "name required" }, 400);
3529
+ const goal = createGoal(opts.db, {
3530
+ name: body.name,
3531
+ description: body.description,
3532
+ color: body.color
3533
+ });
3534
+ opts.router.broadcastGoalUpsert(goal);
3535
+ return c.json(goal);
3536
+ });
3537
+ app.delete("/api/goals/:id", (c) => {
3538
+ const id = c.req.param("id");
3539
+ const ok = deleteGoal(opts.db, id);
3540
+ if (ok) opts.router.broadcastGoalRemove(id);
3541
+ return c.json({ ok });
3542
+ });
2729
3543
  let preflightCache = null;
2730
3544
  app.get("/api/system/preflight", (c) => {
2731
3545
  if (preflightCache) return c.json(preflightCache);
@@ -2760,14 +3574,14 @@ function createHttpApp(opts) {
2760
3574
  const candidate = join11(webDist, safe === "/" ? "index.html" : safe);
2761
3575
  let filePath = candidate;
2762
3576
  try {
2763
- if (!existsSync8(filePath) || statSync4(filePath).isDirectory()) {
3577
+ if (!existsSync9(filePath) || statSync4(filePath).isDirectory()) {
2764
3578
  filePath = join11(webDist, "index.html");
2765
3579
  }
2766
3580
  } catch {
2767
3581
  filePath = join11(webDist, "index.html");
2768
3582
  }
2769
- if (!existsSync8(filePath)) return c.notFound();
2770
- const data = readFileSync6(filePath);
3583
+ if (!existsSync9(filePath)) return c.notFound();
3584
+ const data = readFileSync7(filePath);
2771
3585
  return new Response(data, {
2772
3586
  headers: { "Content-Type": mimeFor(filePath) }
2773
3587
  });
@@ -2825,9 +3639,9 @@ function createHttpApp(opts) {
2825
3639
  }
2826
3640
  function findWebDist() {
2827
3641
  if (process.env.SOLIX_WEB_DIST) {
2828
- return existsSync8(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
3642
+ return existsSync9(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
2829
3643
  }
2830
- const here = dirname4(fileURLToPath4(import.meta.url));
3644
+ const here = dirname5(fileURLToPath5(import.meta.url));
2831
3645
  const candidates = [
2832
3646
  // Bundled npm package: web/ ships next to the bundled JS file.
2833
3647
  resolve3(here, "web"),
@@ -2838,7 +3652,7 @@ function findWebDist() {
2838
3652
  resolve3(process.cwd(), "packages", "web", "dist")
2839
3653
  ];
2840
3654
  for (const c of candidates) {
2841
- if (existsSync8(join11(c, "index.html"))) return c;
3655
+ if (existsSync9(join11(c, "index.html"))) return c;
2842
3656
  }
2843
3657
  return null;
2844
3658
  }
@@ -2863,11 +3677,11 @@ function mimeFor(filePath) {
2863
3677
  }
2864
3678
 
2865
3679
  // ../server/src/launcher.ts
2866
- import { spawn, spawnSync as spawnSync2 } from "child_process";
2867
- import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
3680
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
3681
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
2868
3682
  import { homedir as homedir8 } from "os";
2869
3683
  import { basename as basename3, join as join12 } from "path";
2870
- import { nanoid as nanoid5 } from "nanoid";
3684
+ import { nanoid as nanoid7 } from "nanoid";
2871
3685
  function ensureWorktree(opts) {
2872
3686
  const repoRoot = (() => {
2873
3687
  const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
@@ -2890,7 +3704,7 @@ function ensureWorktree(opts) {
2890
3704
  if (list.status === 0 && (list.stdout ?? "").includes(`worktree ${path}`)) {
2891
3705
  return { path, created: false };
2892
3706
  }
2893
- mkdirSync4(worktreesDir, { recursive: true });
3707
+ mkdirSync5(worktreesDir, { recursive: true });
2894
3708
  const branchProbe = spawnSync2(
2895
3709
  "git",
2896
3710
  ["rev-parse", "--verify", "--quiet", `refs/heads/${opts.branch}`],
@@ -2909,6 +3723,53 @@ function ensureWorktree(opts) {
2909
3723
  return { path, created: true };
2910
3724
  }
2911
3725
  var FAKE_CLAUDE = process.env.SOLIX_FAKE_CLAUDE === "1";
3726
+ function buildSpawnEnv() {
3727
+ const scrub = process.env.SOLIX_ENV_SCRUB === "1" || (process.env.SOLIX_SANDBOX_CMD ?? "").trim() !== "";
3728
+ if (!scrub) return void 0;
3729
+ const allow = [
3730
+ "PATH",
3731
+ "HOME",
3732
+ "USER",
3733
+ "LOGNAME",
3734
+ "SHELL",
3735
+ "LANG",
3736
+ "LC_ALL",
3737
+ "TERM",
3738
+ "TMPDIR",
3739
+ "TZ",
3740
+ "HTTP_PROXY",
3741
+ "HTTPS_PROXY",
3742
+ "NO_PROXY",
3743
+ "http_proxy",
3744
+ "https_proxy",
3745
+ "no_proxy",
3746
+ "SOLIX_HOME",
3747
+ "SOLIX_HOST",
3748
+ "SOLIX_PORT",
3749
+ "SOLIX_GATE_ENABLED",
3750
+ "SOLIX_GATE_POLICY",
3751
+ "SOLIX_GATE_TIMEOUT"
3752
+ ];
3753
+ const extra = (process.env.SOLIX_ENV_PASSTHROUGH ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3754
+ const keep = /* @__PURE__ */ new Set([...allow, ...extra]);
3755
+ const env = {};
3756
+ for (const k of keep) {
3757
+ if (process.env[k] !== void 0) env[k] = process.env[k];
3758
+ }
3759
+ for (const k of Object.keys(process.env)) {
3760
+ if (k.startsWith("ANTHROPIC_") || k.startsWith("CLAUDE_")) {
3761
+ env[k] = process.env[k];
3762
+ }
3763
+ }
3764
+ return env;
3765
+ }
3766
+ function sandboxWrap(file, args) {
3767
+ const cmd = (process.env.SOLIX_SANDBOX_CMD ?? "").trim();
3768
+ if (!cmd) return { file, args };
3769
+ const parts = cmd.split(/\s+/);
3770
+ const bin = parts[0];
3771
+ return { file: bin, args: [...parts.slice(1), file, ...args] };
3772
+ }
2912
3773
  var Launcher = class {
2913
3774
  constructor(db, broadcaster) {
2914
3775
  this.db = db;
@@ -2938,15 +3799,17 @@ var Launcher = class {
2938
3799
  return this.pinSynthetic(advisor.id, advisor.codename, cwd);
2939
3800
  }
2940
3801
  try {
2941
- const child = spawn(
2942
- "claude",
2943
- ["--agent", advisor.id, "--no-tty"],
2944
- {
2945
- cwd,
2946
- stdio: ["pipe", "pipe", "pipe"],
2947
- detached: false
2948
- }
2949
- );
3802
+ const spawnSpec = sandboxWrap("claude", [
3803
+ "--agent",
3804
+ advisor.id,
3805
+ "--no-tty"
3806
+ ]);
3807
+ const child = spawn2(spawnSpec.file, spawnSpec.args, {
3808
+ cwd,
3809
+ stdio: ["pipe", "pipe", "pipe"],
3810
+ detached: false,
3811
+ env: buildSpawnEnv()
3812
+ });
2950
3813
  const pid = child.pid;
2951
3814
  if (!pid) {
2952
3815
  this.broadcaster.broadcast({
@@ -2985,7 +3848,7 @@ var Launcher = class {
2985
3848
  }
2986
3849
  pinSynthetic(advisorId, codename, cwd) {
2987
3850
  const project = ensureProject(this.db, cwd);
2988
- const sessionId = `advisor-${advisorId}-${nanoid5(6)}`;
3851
+ const sessionId = `advisor-${advisorId}-${nanoid7(6)}`;
2989
3852
  const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
2990
3853
  const session = upsertSession(this.db, {
2991
3854
  id: sessionId,
@@ -3099,10 +3962,12 @@ var Launcher = class {
3099
3962
  cwd: spawnCwd,
3100
3963
  model: opts.model,
3101
3964
  initialPrompt: opts.initialPrompt,
3102
- worktreePath
3965
+ worktreePath,
3966
+ budgetUsd: opts.budgetUsd,
3967
+ goalId: opts.goalId
3103
3968
  });
3104
3969
  }
3105
- if (!existsSync9(spawnCwd)) {
3970
+ if (!existsSync10(spawnCwd)) {
3106
3971
  this.broadcaster.broadcast({
3107
3972
  type: "toast",
3108
3973
  level: "error",
@@ -3113,13 +3978,15 @@ var Launcher = class {
3113
3978
  const args = ["--print"];
3114
3979
  if (opts.model) args.push("--model", String(opts.model));
3115
3980
  args.push(opts.initialPrompt);
3116
- const sessionId = `task-${nanoid5(8)}`;
3981
+ const sessionId = `task-${nanoid7(8)}`;
3117
3982
  return this.spawnPrint({
3118
3983
  sessionId,
3119
3984
  cwd: spawnCwd,
3120
3985
  args,
3121
3986
  isFollowUp: false,
3122
- worktreePath
3987
+ worktreePath,
3988
+ budgetUsd: opts.budgetUsd,
3989
+ goalId: opts.goalId
3123
3990
  });
3124
3991
  }
3125
3992
  /**
@@ -3138,7 +4005,7 @@ var Launcher = class {
3138
4005
  });
3139
4006
  return { ok: true };
3140
4007
  }
3141
- if (!existsSync9(opts.cwd)) {
4008
+ if (!existsSync10(opts.cwd)) {
3142
4009
  this.broadcaster.broadcast({
3143
4010
  type: "toast",
3144
4011
  level: "error",
@@ -3152,10 +4019,12 @@ var Launcher = class {
3152
4019
  args.push("--bg", opts.initialPrompt);
3153
4020
  let child;
3154
4021
  try {
3155
- child = spawn("claude", args, {
4022
+ const spawnSpec = sandboxWrap("claude", args);
4023
+ child = spawn2(spawnSpec.file, spawnSpec.args, {
3156
4024
  cwd: opts.cwd,
3157
4025
  stdio: ["ignore", "pipe", "pipe"],
3158
- detached: false
4026
+ detached: false,
4027
+ env: buildSpawnEnv()
3159
4028
  });
3160
4029
  } catch (err) {
3161
4030
  this.broadcaster.broadcast({
@@ -3199,6 +4068,15 @@ var Launcher = class {
3199
4068
  });
3200
4069
  return false;
3201
4070
  }
4071
+ const full = getSession(this.db, sessionId);
4072
+ if (full?.budgetUsd != null && full.costUsd >= full.budgetUsd) {
4073
+ this.broadcaster.broadcast({
4074
+ type: "toast",
4075
+ level: "warn",
4076
+ message: `Budget reached for ${full.name ?? sessionId.slice(0, 8)} ($${full.costUsd.toFixed(2)}/$${full.budgetUsd.toFixed(2)}). Raise the cap to continue.`
4077
+ });
4078
+ return false;
4079
+ }
3202
4080
  if (FAKE_CLAUDE) {
3203
4081
  this.broadcaster.broadcast({
3204
4082
  type: "chat_delta",
@@ -3230,13 +4108,29 @@ var Launcher = class {
3230
4108
  }
3231
4109
  return void 0;
3232
4110
  }
4111
+ /** Sprint M — budget cap recorded at launch for a cwd, if any. */
4112
+ budgetForInternalCwd(cwd) {
4113
+ for (const rec of this.internalTasks.values()) {
4114
+ if (rec.cwd === cwd && rec.budgetUsd != null) return rec.budgetUsd;
4115
+ }
4116
+ return void 0;
4117
+ }
4118
+ /** Sprint M — goal recorded at launch for a cwd, if any. */
4119
+ goalForInternalCwd(cwd) {
4120
+ for (const rec of this.internalTasks.values()) {
4121
+ if (rec.cwd === cwd && rec.goalId) return rec.goalId;
4122
+ }
4123
+ return void 0;
4124
+ }
3233
4125
  spawnPrint(opts) {
3234
4126
  let child;
3235
4127
  try {
3236
- child = spawn("claude", opts.args, {
4128
+ const spawnSpec = sandboxWrap("claude", opts.args);
4129
+ child = spawn2(spawnSpec.file, spawnSpec.args, {
3237
4130
  cwd: opts.cwd,
3238
4131
  stdio: ["ignore", "pipe", "pipe"],
3239
- detached: false
4132
+ detached: false,
4133
+ env: buildSpawnEnv()
3240
4134
  });
3241
4135
  } catch (err) {
3242
4136
  this.broadcaster.broadcast({
@@ -3251,7 +4145,9 @@ var Launcher = class {
3251
4145
  if (!opts.isFollowUp) {
3252
4146
  this.internalTasks.set(opts.sessionId, {
3253
4147
  cwd: opts.cwd,
3254
- worktreePath: opts.worktreePath
4148
+ worktreePath: opts.worktreePath,
4149
+ budgetUsd: opts.budgetUsd,
4150
+ goalId: opts.goalId
3255
4151
  });
3256
4152
  }
3257
4153
  let stdout = "";
@@ -3297,7 +4193,7 @@ var Launcher = class {
3297
4193
  }
3298
4194
  launchSynthetic(opts) {
3299
4195
  const project = ensureProject(this.db, opts.cwd);
3300
- const sessionId = `task-${nanoid5(8)}`;
4196
+ const sessionId = `task-${nanoid7(8)}`;
3301
4197
  const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
3302
4198
  upsertSession(this.db, {
3303
4199
  id: sessionId,
@@ -3308,6 +4204,8 @@ var Launcher = class {
3308
4204
  model: opts.model ?? "sonnet",
3309
4205
  worktreePath: opts.worktreePath
3310
4206
  });
4207
+ if (opts.budgetUsd != null) setSessionBudget(this.db, sessionId, opts.budgetUsd);
4208
+ if (opts.goalId) setSessionGoal(this.db, sessionId, opts.goalId);
3311
4209
  const active = setSessionStatus(this.db, sessionId, "active");
3312
4210
  if (active)
3313
4211
  this.broadcaster.broadcast({ type: "session_upsert", session: active });
@@ -3343,12 +4241,12 @@ var Launcher = class {
3343
4241
  };
3344
4242
 
3345
4243
  // ../server/src/router.ts
3346
- import { nanoid as nanoid7 } from "nanoid";
4244
+ import { nanoid as nanoid9 } from "nanoid";
3347
4245
 
3348
4246
  // ../server/src/state/toolcalls.ts
3349
- import { nanoid as nanoid6 } from "nanoid";
4247
+ import { nanoid as nanoid8 } from "nanoid";
3350
4248
  function recordToolCall(db, input) {
3351
- const id = nanoid6();
4249
+ const id = nanoid8();
3352
4250
  const ts2 = now();
3353
4251
  const status = input.status ?? "running";
3354
4252
  db.prepare(
@@ -3450,6 +4348,8 @@ var EventRouter = class {
3450
4348
  const sessionId = this.extractSessionId(event);
3451
4349
  const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
3452
4350
  const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
4351
+ const launchBudget = this.launcher?.budgetForInternalCwd(event.cwd);
4352
+ const launchGoal = this.launcher?.goalForInternalCwd(event.cwd);
3453
4353
  const wrapper = claimWrapperForCwd(event.cwd);
3454
4354
  const session = upsertSession(this.db, {
3455
4355
  id: sessionId,
@@ -3465,7 +4365,14 @@ var EventRouter = class {
3465
4365
  wrapperSocketPath: wrapper?.socketPath
3466
4366
  });
3467
4367
  if (wrapper) bindWrapperToSession(wrapper.wrapperId, session.id);
3468
- this.broadcaster.broadcast({ type: "session_upsert", session });
4368
+ let enriched = session;
4369
+ if (launchBudget != null) {
4370
+ enriched = setSessionBudget(this.db, session.id, launchBudget) ?? enriched;
4371
+ }
4372
+ if (launchGoal) {
4373
+ enriched = setSessionGoal(this.db, session.id, launchGoal) ?? enriched;
4374
+ }
4375
+ this.broadcaster.broadcast({ type: "session_upsert", session: enriched });
3469
4376
  if (!session.parentSessionId) {
3470
4377
  this.transcripts?.startWatching(sessionId, event.cwd);
3471
4378
  }
@@ -3486,7 +4393,7 @@ var EventRouter = class {
3486
4393
  model: this.extractModel(event)
3487
4394
  });
3488
4395
  }
3489
- const mission = startMission(this.db, sessionId, prompt);
4396
+ const mission = startMission(this.db, sessionId, prompt, session.currentGoalId);
3490
4397
  const updated = setSessionMission(this.db, sessionId, mission.id);
3491
4398
  const active = updated ? setSessionStatus(this.db, sessionId, "active") : null;
3492
4399
  this.broadcaster.broadcast({ type: "mission_upsert", mission });
@@ -3530,7 +4437,7 @@ var EventRouter = class {
3530
4437
  const parentSessionId = this.extractSessionId(event);
3531
4438
  const parent = getSession(this.db, parentSessionId);
3532
4439
  if (!parent) return;
3533
- const subId = nanoid7();
4440
+ const subId = nanoid9();
3534
4441
  const sub = upsertSession(this.db, {
3535
4442
  id: subId,
3536
4443
  pid: event.pid,
@@ -3607,7 +4514,7 @@ var EventRouter = class {
3607
4514
  const p = event.payload;
3608
4515
  const message = typeof p.message === "string" ? p.message : "Permission requested";
3609
4516
  const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
3610
- const requestId = nanoid7();
4517
+ const requestId = nanoid9();
3611
4518
  this.permissions.set(requestId, {
3612
4519
  requestId,
3613
4520
  sessionId,
@@ -3635,6 +4542,90 @@ var EventRouter = class {
3635
4542
  message: `Permission requested: ${message}`
3636
4543
  });
3637
4544
  }
4545
+ /**
4546
+ * Synchronous human-in-the-loop gate for the blocking PreToolUse path.
4547
+ * Records the tool call (so the comet/timeline visuals still fire — the gate
4548
+ * hook no longer POSTs to /events), broadcasts a permission_request, and
4549
+ * returns a promise that resolves when a human answers via `permission_response`
4550
+ * (reused unchanged) or when the server-side timeout fires.
4551
+ */
4552
+ requestPermission(event) {
4553
+ const sessionId = this.extractSessionId(event);
4554
+ const p = event.payload;
4555
+ const { tool, args } = this.describeGatedTool(event.event, p);
4556
+ const session = getSession(this.db, sessionId);
4557
+ if (session) {
4558
+ const toolCall = recordToolCall(this.db, {
4559
+ sessionId,
4560
+ missionId: session.currentMissionId,
4561
+ tool,
4562
+ args
4563
+ });
4564
+ if (event.event === "pre_tool_file" && session.currentMissionId && typeof args.file_path === "string" && args.file_path) {
4565
+ addTouchedFile(this.db, session.currentMissionId, args.file_path);
4566
+ }
4567
+ this.broadcaster.broadcast({ type: "tool_call", toolCall });
4568
+ }
4569
+ const requestId = nanoid9();
4570
+ const timeoutMs = Number(process.env.SOLIX_GATE_TIMEOUT_MS ?? 3e5);
4571
+ return new Promise((resolve4) => {
4572
+ const timer = setTimeout(() => {
4573
+ const pending = this.permissions.get(requestId);
4574
+ if (!pending) return;
4575
+ this.permissions.delete(requestId);
4576
+ const s = setSessionStatus(this.db, sessionId, "active");
4577
+ if (s) this.broadcaster.broadcast({ type: "session_upsert", session: s });
4578
+ resolve4({ approved: false, timedOut: true });
4579
+ }, timeoutMs);
4580
+ this.permissions.set(requestId, {
4581
+ requestId,
4582
+ sessionId,
4583
+ tool,
4584
+ args,
4585
+ createdAt: Date.now(),
4586
+ resolve: (approved) => resolve4({ approved, timedOut: false }),
4587
+ timer
4588
+ });
4589
+ const updated = setSessionStatus(
4590
+ this.db,
4591
+ sessionId,
4592
+ "awaiting_permission"
4593
+ );
4594
+ if (updated) {
4595
+ this.broadcaster.broadcast({ type: "session_upsert", session: updated });
4596
+ }
4597
+ this.broadcaster.broadcast({
4598
+ type: "permission_request",
4599
+ sessionId,
4600
+ tool,
4601
+ args,
4602
+ requestId
4603
+ });
4604
+ this.broadcaster.broadcast({
4605
+ type: "toast",
4606
+ level: "warn",
4607
+ message: `Approval requested: ${tool}`
4608
+ });
4609
+ });
4610
+ }
4611
+ describeGatedTool(eventName, p) {
4612
+ const toolInput = p.tool_input ?? {};
4613
+ if (eventName === "pre_tool_bash") {
4614
+ const command = typeof p.command === "string" ? p.command : typeof toolInput.command === "string" ? toolInput.command : "";
4615
+ return { tool: "Bash", args: { command } };
4616
+ }
4617
+ if (eventName === "pre_tool_file") {
4618
+ const tool2 = typeof p.tool_name === "string" ? p.tool_name : "File";
4619
+ const filePath = typeof p.file_path === "string" ? p.file_path : typeof toolInput.file_path === "string" ? toolInput.file_path : "";
4620
+ return { tool: tool2, args: { file_path: filePath } };
4621
+ }
4622
+ if (eventName === "pre_tool_task") {
4623
+ const tool2 = typeof p.tool_name === "string" ? p.tool_name : "Task";
4624
+ return { tool: tool2, args: toolInput };
4625
+ }
4626
+ const tool = typeof p.tool_name === "string" ? p.tool_name : "tool";
4627
+ return { tool, args: toolInput };
4628
+ }
3638
4629
  invokeAdvisor(advisorId, targetSessionId, prompt) {
3639
4630
  const advisor = getAdvisor(this.db, advisorId);
3640
4631
  if (!advisor) return { ok: false };
@@ -3687,6 +4678,20 @@ var EventRouter = class {
3687
4678
  }
3688
4679
  return ok;
3689
4680
  }
4681
+ /** Sprint N — enable/disable an advisor and broadcast the change so open
4682
+ * browsers (and the CLI path) update live. Returns the updated advisor. */
4683
+ setAdvisorEnabled(advisorId, enabled) {
4684
+ const advisor = setAdvisorEnabled(this.db, advisorId, enabled);
4685
+ if (advisor) {
4686
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
4687
+ this.broadcaster.broadcast({
4688
+ type: "toast",
4689
+ level: "info",
4690
+ message: `${advisor.codename} ${enabled ? "added to crew" : "disabled"}`
4691
+ });
4692
+ }
4693
+ return advisor;
4694
+ }
3690
4695
  unpinAdvisor(advisorId) {
3691
4696
  if (this.launcher) {
3692
4697
  this.launcher.unpin(advisorId);
@@ -3708,6 +4713,8 @@ var EventRouter = class {
3708
4713
  const pending = this.permissions.get(requestId);
3709
4714
  if (!pending) return false;
3710
4715
  this.permissions.delete(requestId);
4716
+ if (pending.timer) clearTimeout(pending.timer);
4717
+ if (pending.resolve) pending.resolve(approved);
3711
4718
  const status = approved ? "active" : "idle";
3712
4719
  const session = setSessionStatus(this.db, pending.sessionId, status);
3713
4720
  if (session)
@@ -3745,7 +4752,27 @@ var EventRouter = class {
3745
4752
  worktreeBranch: opts.worktreeBranch,
3746
4753
  worktreeBaseRef: opts.worktreeBaseRef,
3747
4754
  useAgentView: opts.useAgentView,
3748
- agentName: opts.agentName
4755
+ agentName: opts.agentName,
4756
+ budgetUsd: opts.budgetUsd,
4757
+ goalId: opts.goalId
4758
+ });
4759
+ }
4760
+ /** Sprint M — raise (or clear) a session's budget cap. Clears any standing
4761
+ * budget breach by re-broadcasting the current cost against the new cap. */
4762
+ raiseBudget(sessionId, budgetUsd) {
4763
+ const session = setSessionBudget(this.db, sessionId, budgetUsd);
4764
+ if (!session) return;
4765
+ this.broadcaster.broadcast({ type: "session_upsert", session });
4766
+ this.broadcaster.broadcast({
4767
+ type: "cost_update",
4768
+ sessionId,
4769
+ costUsd: session.costUsd,
4770
+ budgetUsd: session.budgetUsd
4771
+ });
4772
+ this.broadcaster.broadcast({
4773
+ type: "toast",
4774
+ level: "info",
4775
+ message: `Budget raised to $${budgetUsd.toFixed(2)} for ${session.name ?? sessionId.slice(0, 8)}`
3749
4776
  });
3750
4777
  }
3751
4778
  sendPromptToSession(sessionId, text) {
@@ -3790,6 +4817,19 @@ var EventRouter = class {
3790
4817
  broadcastSessionUpsert(session) {
3791
4818
  this.broadcaster.broadcast({ type: "session_upsert", session });
3792
4819
  }
4820
+ // Sprint M — broadcast helpers for schedule/goal CRUD driven by HTTP/CLI.
4821
+ broadcastScheduleUpsert(schedule) {
4822
+ this.broadcaster.broadcast({ type: "schedule_upsert", schedule });
4823
+ }
4824
+ broadcastScheduleRemove(scheduleId) {
4825
+ this.broadcaster.broadcast({ type: "schedule_remove", scheduleId });
4826
+ }
4827
+ broadcastGoalUpsert(goal) {
4828
+ this.broadcaster.broadcast({ type: "goal_upsert", goal });
4829
+ }
4830
+ broadcastGoalRemove(goalId) {
4831
+ this.broadcaster.broadcast({ type: "goal_remove", goalId });
4832
+ }
3793
4833
  broadcastGalaxyImported(manifest) {
3794
4834
  this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
3795
4835
  this.broadcaster.broadcast({
@@ -3832,6 +4872,11 @@ function attachWs(server, ctx) {
3832
4872
  socket.destroy();
3833
4873
  return;
3834
4874
  }
4875
+ if (!isAllowedOrigin(req.headers.origin)) {
4876
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
4877
+ socket.destroy();
4878
+ return;
4879
+ }
3835
4880
  wss.handleUpgrade(req, socket, head, (ws) => {
3836
4881
  wss.emit("connection", ws, req);
3837
4882
  });
@@ -3845,7 +4890,9 @@ function attachWs(server, ctx) {
3845
4890
  sessions: listActiveSessions(ctx.db),
3846
4891
  missions: listMissions(ctx.db, { limit: 100 }),
3847
4892
  advisors: listAdvisors(ctx.db),
3848
- skills: listSkills(ctx.db)
4893
+ skills: listSkills(ctx.db),
4894
+ schedules: listSchedules(ctx.db),
4895
+ goals: listGoals(ctx.db)
3849
4896
  };
3850
4897
  ctx.broadcaster.send(ws, snapshot);
3851
4898
  for (const p of ctx.router.pendingPermissions()) {
@@ -3894,9 +4941,16 @@ function handleClientMessage(ctx, _ws, msg) {
3894
4941
  worktreeBranch: msg.worktreeBranch,
3895
4942
  worktreeBaseRef: msg.worktreeBaseRef,
3896
4943
  useAgentView: msg.useAgentView,
3897
- agentName: msg.agentName
4944
+ agentName: msg.agentName,
4945
+ budgetUsd: msg.budgetUsd,
4946
+ goalId: msg.goalId
3898
4947
  });
3899
4948
  break;
4949
+ case "raise_budget":
4950
+ ctx.router.raiseBudget(msg.sessionId, msg.budgetUsd);
4951
+ break;
4952
+ case "dismiss_budget_alert":
4953
+ break;
3900
4954
  case "invoke_advisor":
3901
4955
  ctx.router.invokeAdvisor(
3902
4956
  msg.advisorId,
@@ -3910,6 +4964,9 @@ function handleClientMessage(ctx, _ws, msg) {
3910
4964
  case "unpin_advisor":
3911
4965
  ctx.router.unpinAdvisor(msg.advisorId);
3912
4966
  break;
4967
+ case "set_advisor_enabled":
4968
+ ctx.router.setAdvisorEnabled(msg.advisorId, msg.enabled);
4969
+ break;
3913
4970
  default:
3914
4971
  break;
3915
4972
  }
@@ -3918,7 +4975,7 @@ function handleClientMessage(ctx, _ws, msg) {
3918
4975
  // ../server/src/state/transcript.ts
3919
4976
  import {
3920
4977
  closeSync,
3921
- existsSync as existsSync10,
4978
+ existsSync as existsSync11,
3922
4979
  openSync,
3923
4980
  readSync,
3924
4981
  statSync as statSync5,
@@ -3945,12 +5002,15 @@ var TranscriptWatcherManager = class {
3945
5002
  constructor(db, broadcaster) {
3946
5003
  this.db = db;
3947
5004
  this.broadcaster = broadcaster;
3948
- void this.db;
3949
5005
  }
3950
5006
  db;
3951
5007
  broadcaster;
3952
5008
  records = /* @__PURE__ */ new Map();
3953
5009
  deferredRetry = /* @__PURE__ */ new Map();
5010
+ // Sprint M: sessions we've already raised a budget alert for, so we don't
5011
+ // re-fire on every subsequent assistant message. Cleared when spend drops
5012
+ // back under the cap (e.g. after the cap is raised).
5013
+ budgetAlerted = /* @__PURE__ */ new Set();
3954
5014
  /**
3955
5015
  * Begin tailing this session's transcript. Idempotent. If the file doesn't
3956
5016
  * exist yet, retries every second for up to 10 s (Claude Code creates the
@@ -3959,7 +5019,7 @@ var TranscriptWatcherManager = class {
3959
5019
  startWatching(sessionId, cwd) {
3960
5020
  if (this.records.has(sessionId)) return;
3961
5021
  const filePath = transcriptPathFor(cwd, sessionId);
3962
- if (!existsSync10(filePath)) {
5022
+ if (!existsSync11(filePath)) {
3963
5023
  this.scheduleRetry(sessionId, cwd, 0);
3964
5024
  return;
3965
5025
  }
@@ -3970,7 +5030,7 @@ var TranscriptWatcherManager = class {
3970
5030
  const t = setTimeout(() => {
3971
5031
  this.deferredRetry.delete(sessionId);
3972
5032
  const filePath = transcriptPathFor(cwd, sessionId);
3973
- if (existsSync10(filePath)) {
5033
+ if (existsSync11(filePath)) {
3974
5034
  this.attach(sessionId, filePath);
3975
5035
  } else {
3976
5036
  this.scheduleRetry(sessionId, cwd, attempt + 1);
@@ -4082,6 +5142,39 @@ var TranscriptWatcherManager = class {
4082
5142
  sessionId,
4083
5143
  usagePct: pct
4084
5144
  });
5145
+ const inc = costForUsage(message.model, message.usage);
5146
+ const session = getSession(this.db, sessionId);
5147
+ if (session) {
5148
+ const updated = setSessionCost(this.db, sessionId, session.costUsd + inc);
5149
+ const costUsd = updated?.costUsd ?? session.costUsd + inc;
5150
+ const cap = updated?.budgetUsd ?? session.budgetUsd;
5151
+ this.broadcaster.broadcast({
5152
+ type: "cost_update",
5153
+ sessionId,
5154
+ costUsd,
5155
+ budgetUsd: cap
5156
+ });
5157
+ if (session.currentMissionId) {
5158
+ addMissionTokens(
5159
+ this.db,
5160
+ session.currentMissionId,
5161
+ totalTokens(message.usage)
5162
+ );
5163
+ }
5164
+ if (cap != null && costUsd >= cap) {
5165
+ if (!this.budgetAlerted.has(sessionId)) {
5166
+ this.budgetAlerted.add(sessionId);
5167
+ this.broadcaster.broadcast({
5168
+ type: "budget_alert",
5169
+ sessionId,
5170
+ costUsd,
5171
+ budgetUsd: cap
5172
+ });
5173
+ }
5174
+ } else {
5175
+ this.budgetAlerted.delete(sessionId);
5176
+ }
5177
+ }
4085
5178
  }
4086
5179
  const content = this.flattenAssistantContent(message.content);
4087
5180
  if (!content) return;
@@ -4160,7 +5253,7 @@ ${text.slice(0, 600)}`);
4160
5253
  };
4161
5254
 
4162
5255
  // ../server/src/state/agentview.ts
4163
- import { existsSync as existsSync11, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6, watch as watch2 } from "fs";
5256
+ import { existsSync as existsSync12, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync6, watch as watch2 } from "fs";
4164
5257
  import { homedir as homedir10 } from "os";
4165
5258
  import { join as join14 } from "path";
4166
5259
  var ROSTER_PATH = join14(homedir10(), ".claude", "daemon", "roster.json");
@@ -4190,9 +5283,9 @@ function mapPrStatus(s) {
4190
5283
  return void 0;
4191
5284
  }
4192
5285
  function readRoster() {
4193
- if (!existsSync11(ROSTER_PATH)) return [];
5286
+ if (!existsSync12(ROSTER_PATH)) return [];
4194
5287
  try {
4195
- const raw = readFileSync7(ROSTER_PATH, "utf8");
5288
+ const raw = readFileSync8(ROSTER_PATH, "utf8");
4196
5289
  const parsed = JSON.parse(raw);
4197
5290
  if (Array.isArray(parsed)) return parsed;
4198
5291
  if (parsed && Array.isArray(parsed.sessions)) return parsed.sessions;
@@ -4202,7 +5295,7 @@ function readRoster() {
4202
5295
  }
4203
5296
  }
4204
5297
  function readJobIds() {
4205
- if (!existsSync11(JOBS_DIR)) return [];
5298
+ if (!existsSync12(JOBS_DIR)) return [];
4206
5299
  try {
4207
5300
  return readdirSync5(JOBS_DIR).filter((entry) => {
4208
5301
  try {
@@ -4217,9 +5310,9 @@ function readJobIds() {
4217
5310
  }
4218
5311
  function readJobState(jobId) {
4219
5312
  const p = join14(JOBS_DIR, jobId, "state.json");
4220
- if (!existsSync11(p)) return null;
5313
+ if (!existsSync12(p)) return null;
4221
5314
  try {
4222
- return JSON.parse(readFileSync7(p, "utf8"));
5315
+ return JSON.parse(readFileSync8(p, "utf8"));
4223
5316
  } catch {
4224
5317
  return null;
4225
5318
  }
@@ -4299,7 +5392,7 @@ function debounce(fn, ms) {
4299
5392
  }
4300
5393
  function startAgentViewBridge(opts) {
4301
5394
  const claudeRoot = join14(homedir10(), ".claude");
4302
- if (!existsSync11(claudeRoot)) return () => {
5395
+ if (!existsSync12(claudeRoot)) return () => {
4303
5396
  };
4304
5397
  const sync = () => {
4305
5398
  try {
@@ -4312,7 +5405,7 @@ function startAgentViewBridge(opts) {
4312
5405
  sync();
4313
5406
  const watchers = [];
4314
5407
  const daemonDir = join14(homedir10(), ".claude", "daemon");
4315
- if (existsSync11(daemonDir)) {
5408
+ if (existsSync12(daemonDir)) {
4316
5409
  try {
4317
5410
  watchers.push(watch2(daemonDir, { persistent: false }, debounced));
4318
5411
  } catch (err) {
@@ -4322,7 +5415,7 @@ function startAgentViewBridge(opts) {
4322
5415
  );
4323
5416
  }
4324
5417
  }
4325
- if (existsSync11(JOBS_DIR)) {
5418
+ if (existsSync12(JOBS_DIR)) {
4326
5419
  try {
4327
5420
  watchers.push(watch2(JOBS_DIR, { recursive: true, persistent: false }, debounced));
4328
5421
  } catch (err) {
@@ -4367,7 +5460,17 @@ async function createSolixServer(opts = {}) {
4367
5460
  const launcher = new Launcher(db, broadcaster);
4368
5461
  const transcripts = new TranscriptWatcherManager(db, broadcaster);
4369
5462
  const router = new EventRouter(db, broadcaster, launcher, transcripts);
4370
- const app = createHttpApp({ db, router });
5463
+ const tokenPath = join15(
5464
+ process.env.SOLIX_HOME ?? join15(homedir11(), ".solix"),
5465
+ "token"
5466
+ );
5467
+ let token = null;
5468
+ try {
5469
+ token = readFileSync9(tokenPath, "utf8").trim() || null;
5470
+ } catch {
5471
+ token = null;
5472
+ }
5473
+ const app = createHttpApp({ db, router, token, version: opts.version });
4371
5474
  const server = serve({
4372
5475
  fetch: app.fetch,
4373
5476
  port,
@@ -4379,10 +5482,31 @@ async function createSolixServer(opts = {}) {
4379
5482
  broadcaster
4380
5483
  });
4381
5484
  const stopAgentViewBridge = startAgentViewBridge({ db, broadcaster });
5485
+ const scheduleTimer = setInterval(() => {
5486
+ try {
5487
+ const due = listDueSchedules(db, now());
5488
+ for (const s of due) {
5489
+ if (!s.cwd) continue;
5490
+ launcher.launch({ cwd: s.cwd, initialPrompt: s.prompt });
5491
+ const updated = markScheduleRun(db, s.id);
5492
+ if (updated) {
5493
+ broadcaster.broadcast({ type: "schedule_upsert", schedule: updated });
5494
+ broadcaster.broadcast({
5495
+ type: "toast",
5496
+ level: "info",
5497
+ message: `Heartbeat fired: ${s.name ?? s.prompt.slice(0, 32)}`
5498
+ });
5499
+ }
5500
+ }
5501
+ } catch (err) {
5502
+ console.warn("[scheduler] tick failed:", err.message);
5503
+ }
5504
+ }, 3e4);
4382
5505
  return {
4383
5506
  port,
4384
5507
  hostname,
4385
5508
  close: () => new Promise((resolve4) => {
5509
+ clearInterval(scheduleTimer);
4386
5510
  stopAgentViewBridge();
4387
5511
  transcripts.shutdownAll();
4388
5512
  launcher.shutdownAll();
@@ -4405,7 +5529,7 @@ var BANNER = `
4405
5529
  async function start(opts = {}) {
4406
5530
  const port = opts.port ?? Number(process.env.SOLIX_PORT ?? 4242);
4407
5531
  console.log(BANNER);
4408
- const handle = await createSolixServer({ port });
5532
+ const handle = await createSolixServer({ port, version: "1.9.0" });
4409
5533
  const url = `http://${handle.hostname}:${handle.port}`;
4410
5534
  console.log(`[solix] server listening on ${url}`);
4411
5535
  console.log(`[solix] events -> POST ${url}/events`);
@@ -4431,20 +5555,20 @@ async function start(opts = {}) {
4431
5555
  }
4432
5556
 
4433
5557
  // src/uninstall.ts
4434
- import { copyFileSync as copyFileSync2, existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
5558
+ import { copyFileSync as copyFileSync2, existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
4435
5559
  function uninstall() {
4436
5560
  uninstallShim();
4437
- if (existsSync12(CLAUDE_BACKUP)) {
5561
+ if (existsSync13(CLAUDE_BACKUP)) {
4438
5562
  copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
4439
5563
  console.log(`[solix] restored settings.json from backup`);
4440
5564
  return;
4441
5565
  }
4442
- if (!existsSync12(CLAUDE_SETTINGS)) {
5566
+ if (!existsSync13(CLAUDE_SETTINGS)) {
4443
5567
  console.log("[solix] nothing to uninstall (no settings.json found)");
4444
5568
  return;
4445
5569
  }
4446
5570
  const cur = JSON.parse(
4447
- readFileSync8(CLAUDE_SETTINGS, "utf8")
5571
+ readFileSync10(CLAUDE_SETTINGS, "utf8")
4448
5572
  );
4449
5573
  if (cur.hooks) {
4450
5574
  for (const [evt, entries] of Object.entries(cur.hooks)) {
@@ -4460,7 +5584,7 @@ function uninstall() {
4460
5584
 
4461
5585
  // src/index.ts
4462
5586
  var program = new Command();
4463
- program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.5.0");
5587
+ program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.9.0");
4464
5588
  program.command("start", { isDefault: true }).description("Start the Solix server and open the browser").option("-p, --port <port>", "port to listen on", (v) => parseInt(v, 10), 4242).option("--no-open", "do not open browser automatically").action(async (opts) => {
4465
5589
  await start({ port: opts.port, noOpen: !opts.open });
4466
5590
  });
@@ -4485,49 +5609,84 @@ program.command("doctor").description("Run diagnostics").action(async () => {
4485
5609
  await doctor();
4486
5610
  });
4487
5611
  program.command("demo").description(
4488
- "Seed the running server with fake planets, missions, and a pinned advisor (great for first-run)"
4489
- ).option("-p, --port <port>", "server port", (v) => parseInt(v, 10), 4242).action(async (opts) => {
4490
- await demoCmd({ port: opts.port });
4491
- });
5612
+ "Boot a sandbox server, seed a rich galaxy (8 projects, ~30 sessions, all advisors), and keep firing activity until Ctrl+C. Showcase mode for live demos."
5613
+ ).option("-p, --port <port>", "server port (falls back to +1 on conflict)", (v) => parseInt(v, 10), 4242).option("--keep", "preserve ~/.solix/demo.db after teardown (default removes it)").option("--no-server", "skip spawning a sandbox server; seed against the server you already have running").option("--no-ticker", "seed once and exit (static snapshot for screenshots)").action(
5614
+ async (opts) => {
5615
+ await demoCmd({
5616
+ port: opts.port,
5617
+ keep: opts.keep,
5618
+ noServer: !opts.server,
5619
+ noTicker: !opts.ticker
5620
+ });
5621
+ }
5622
+ );
4492
5623
  var advisors = program.command("advisors").description("Manage built-in advisor agents (PM, Builder, UX, etc.)");
4493
5624
  advisors.command("list", { isDefault: true }).description("List all advisor agents and their state").action(async () => {
4494
5625
  await listAdvisorsCmd();
4495
5626
  });
4496
- advisors.command("enable <id>").description("Enable an advisor (renders in the inner crew ring)").action(async (id) => {
5627
+ advisors.command("enable <id>").description("Enable an advisor (add to the inner ring)").action(async (id) => {
4497
5628
  await enableAdvisorCmd(id);
4498
5629
  });
4499
- advisors.command("disable <id>").description("Disable an advisor").action(async (id) => {
5630
+ advisors.command("disable <id>").description("Disable an advisor (remove from the inner ring)").action(async (id) => {
4500
5631
  await disableAdvisorCmd(id);
4501
5632
  });
4502
- advisors.command("pin <id>").description("Pin an advisor (always-on planet)").action(async (id) => {
5633
+ advisors.command("pin <id>").description("Pin an advisor (spawn an always-on session)").action(async (id) => {
4503
5634
  await pinAdvisorCmd(id);
4504
5635
  });
4505
- advisors.command("unpin <id>").description("Unpin an advisor (back to on-demand)").action(async (id) => {
5636
+ advisors.command("unpin <id>").description("Unpin an advisor (kill the always-on session)").action(async (id) => {
4506
5637
  await unpinAdvisorCmd(id);
4507
5638
  });
4508
- var skills = program.command("skills").description("Manage discovered skills (asteroid belt)");
4509
- skills.command("list", { isDefault: true }).description("List all known skills (Anthropic + Solix pack)").action(async () => {
5639
+ var skills = program.command("skills").description("Browse + install skills from Anthropic, Solix, and your own");
5640
+ skills.command("list", { isDefault: true }).description("List skills detected in this project").action(async () => {
4510
5641
  await listSkillsCmd();
4511
5642
  });
4512
- skills.command("install <id>").description("Mark a skill as installed in a project").option("--project <projectId>", "project id (hash of cwd)").action(async (id, opts) => {
4513
- await installSkillCmd(id, opts.project);
5643
+ skills.command("install <id>").description("Install a skill into the current project").action(async (id) => {
5644
+ await installSkillCmd(id);
4514
5645
  });
4515
- var galaxy = program.command("galaxy").description("Export and import shareable galaxy configurations");
4516
- galaxy.command("export <out>").description("Export the current galaxy to a JSON manifest file").option("--name <name>", "galaxy name", "My Galaxy").option("--author <author>", "author name").option("--description <desc>", "short description").action(
4517
- async (out, opts) => {
4518
- await exportGalaxyCmd(out, opts);
5646
+ var galaxy = program.command("galaxy").description("Export, import, and publish galaxy presets");
5647
+ galaxy.command("export <name>").description("Snapshot the current crew + skills + projects into a manifest").option("--author <author>", "author tag for the manifest").option("--description <description>", "one-line manifest description").action(
5648
+ async (name, opts) => {
5649
+ await exportGalaxyCmd(name, opts);
4519
5650
  }
4520
5651
  );
4521
- galaxy.command("import <fileOrUrl>").description("Import a galaxy manifest from a local file or URL").action(async (fileOrUrl) => {
4522
- await importGalaxyCmd(fileOrUrl);
5652
+ galaxy.command("import <path>").description("Apply a galaxy manifest (enables advisors, seeds skills + projects)").action(async (path) => {
5653
+ await importGalaxyCmd(path);
5654
+ });
5655
+ galaxy.command("publish <path>").description("Publish a manifest to the configured registry (SOLIX_REGISTRY_URL)").action(async (path) => {
5656
+ await publishGalaxyCmd(path);
5657
+ });
5658
+ galaxy.command("install <id>").description("Install a galaxy preset from the registry by id").action(async (id) => {
5659
+ await installFromRegistryCmd(id);
5660
+ });
5661
+ var schedules = program.command("schedules").description("Manage recurring scheduled tasks");
5662
+ schedules.command("list", { isDefault: true }).description("List schedules across all projects").action(async () => {
5663
+ await listSchedulesCmd();
5664
+ });
5665
+ schedules.command("add <prompt>").description("Schedule a recurring task").option("--cwd <dir>", "working directory (default: current dir)").option("--every <cadence>", "cadence, e.g. 30m, 1h, 1d (default: 1h)").option("--name <name>", "short label for the schedule").action(
5666
+ async (prompt, opts) => {
5667
+ await addScheduleCmd(prompt, opts);
5668
+ }
5669
+ );
5670
+ schedules.command("remove <id>").description("Remove a schedule").action(async (id) => {
5671
+ await removeScheduleCmd(id);
5672
+ });
5673
+ schedules.command("enable <id>").description("Enable a schedule").action(async (id) => {
5674
+ await enableScheduleCmd(id);
5675
+ });
5676
+ schedules.command("disable <id>").description("Disable a schedule").action(async (id) => {
5677
+ await disableScheduleCmd(id);
5678
+ });
5679
+ var goals = program.command("goals").description("Manage cross-session goals (constellations)");
5680
+ goals.command("list", { isDefault: true }).description("List all goals").action(async () => {
5681
+ await listGoalsCmd();
4523
5682
  });
4524
- galaxy.command("publish <slug>").description("Publish the current galaxy to the configured registry").option("--name <name>", "galaxy name", "My Galaxy").option("--author <author>", "author name").option("--description <desc>", "short description").action(
4525
- async (slug, opts) => {
4526
- await publishGalaxyCmd(slug, opts);
5683
+ goals.command("add <name>").description("Create a goal").option("--description <desc>", "optional description").option("--color <hex>", "goal color (default sky blue)").action(
5684
+ async (name, opts) => {
5685
+ await addGoalCmd(name, opts);
4527
5686
  }
4528
5687
  );
4529
- galaxy.command("install <slug>").description("Pull and install a galaxy from the configured registry").action(async (slug) => {
4530
- await installFromRegistryCmd(slug);
5688
+ goals.command("remove <id>").description("Remove a goal").action(async (id) => {
5689
+ await removeGoalCmd(id);
4531
5690
  });
4532
5691
  program.parseAsync(process.argv).catch((err) => {
4533
5692
  console.error(err);