@shmulikdav/solix 1.0.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 ADDED
@@ -0,0 +1,3645 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/advisors.ts
7
+ var PORT = process.env.SOLIX_PORT ?? "4242";
8
+ var BASE = `http://127.0.0.1:${PORT}`;
9
+ async function api(path, init) {
10
+ const res = await fetch(`${BASE}${path}`, init);
11
+ if (!res.ok) {
12
+ const text = await res.text().catch(() => "");
13
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
14
+ }
15
+ return await res.json();
16
+ }
17
+ async function listAdvisorsCmd() {
18
+ try {
19
+ const advisors2 = await api("/api/advisors");
20
+ if (!advisors2.length) {
21
+ console.log("No advisors found. Run `solix install` to seed the crew.");
22
+ return;
23
+ }
24
+ const lines = advisors2.map((a) => {
25
+ const flag = a.pinned ? "pinned" : a.enabled ? "on" : "off";
26
+ return ` ${a.id.padEnd(10)} ${a.codename.padEnd(10)} ${a.role.padEnd(10)} [${flag}]`;
27
+ });
28
+ console.log("id codename role state");
29
+ console.log(lines.join("\n"));
30
+ } catch (err) {
31
+ console.error(`[solix] could not reach server at ${BASE}: ${String(err)}`);
32
+ console.error("[solix] is `solix start` running?");
33
+ process.exitCode = 1;
34
+ }
35
+ }
36
+ async function postAdvisor(id, action) {
37
+ try {
38
+ const res = await api(
39
+ `/api/advisors/${encodeURIComponent(id)}/${action}`,
40
+ { method: "POST" }
41
+ );
42
+ if (res.ok) {
43
+ console.log(`[solix] ${id} \u2192 ${action}`);
44
+ } else {
45
+ console.error(`[solix] failed: advisor not found?`);
46
+ process.exitCode = 1;
47
+ }
48
+ } catch (err) {
49
+ console.error(`[solix] could not reach server: ${String(err)}`);
50
+ process.exitCode = 1;
51
+ }
52
+ }
53
+ var enableAdvisorCmd = (id) => postAdvisor(id, "enable");
54
+ var disableAdvisorCmd = (id) => postAdvisor(id, "disable");
55
+ var pinAdvisorCmd = (id) => postAdvisor(id, "pin");
56
+ var unpinAdvisorCmd = (id) => postAdvisor(id, "unpin");
57
+
58
+ // src/demo.ts
59
+ import { homedir } from "os";
60
+ 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
+ });
69
+ }
70
+ function ts() {
71
+ return Date.now();
72
+ }
73
+ async function ensureReachable() {
74
+ try {
75
+ const res = await fetch(`${BASE2}/api/health`, {
76
+ signal: AbortSignal.timeout(800)
77
+ });
78
+ return res.ok;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ async function pin(advisorId) {
84
+ await fetch(`${BASE2}/api/advisors/${encodeURIComponent(advisorId)}/pin`, {
85
+ method: "POST"
86
+ });
87
+ }
88
+ async function sleep(ms) {
89
+ return new Promise((r) => setTimeout(r, ms));
90
+ }
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"
96
+ );
97
+ process.exitCode = 1;
98
+ return;
99
+ }
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
+ },
127
+ {
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: []
134
+ }
135
+ ];
136
+ for (const s of sessions) {
137
+ await postEvent({
138
+ event: "session_start",
139
+ pid: s.pid,
140
+ cwd: s.cwd,
141
+ ts: ts(),
142
+ payload: s.payload
143
+ });
144
+ }
145
+ await sleep(150);
146
+ for (const s of sessions.slice(0, 2)) {
147
+ await postEvent({
148
+ event: "user_prompt_submit",
149
+ pid: s.pid,
150
+ cwd: s.cwd,
151
+ ts: ts(),
152
+ payload: { session_id: s.id, prompt: s.prompt }
153
+ });
154
+ }
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,
162
+ ts: ts(),
163
+ payload: {
164
+ session_id: sessions[0].id,
165
+ command: t.cmd
166
+ }
167
+ });
168
+ } else {
169
+ await postEvent({
170
+ event: "pre_tool_file",
171
+ pid: sessions[0].pid,
172
+ cwd: sessions[0].cwd,
173
+ ts: ts(),
174
+ payload: {
175
+ session_id: sessions[0].id,
176
+ tool_name: t.tool,
177
+ tool_input: { file_path: t.file }
178
+ }
179
+ });
180
+ }
181
+ await sleep(80);
182
+ }
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"
200
+ }
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.`);
221
+ }
222
+
223
+ // src/doctor.ts
224
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
225
+ import { join as join3 } from "path";
226
+
227
+ // src/paths.ts
228
+ import { homedir as homedir2 } from "os";
229
+ import { existsSync } from "fs";
230
+ import { join as join2, dirname } from "path";
231
+ import { fileURLToPath } from "url";
232
+ var SOLIX_HOME = process.env.SOLIX_HOME ?? join2(homedir2(), ".solix");
233
+ var HOOKS_DIR = join2(SOLIX_HOME, "hooks");
234
+ var SOLIX_SKILLS_DIR = join2(SOLIX_HOME, "skills");
235
+ var CLAUDE_DIR = join2(homedir2(), ".claude");
236
+ var CLAUDE_SETTINGS = join2(CLAUDE_DIR, "settings.json");
237
+ var CLAUDE_BACKUP = join2(CLAUDE_DIR, "settings.solix.backup.json");
238
+ var CLAUDE_AGENTS_DIR = join2(CLAUDE_DIR, "agents");
239
+ var CLAUDE_SKILLS_DIR = join2(CLAUDE_DIR, "skills");
240
+ var HOOK_NAMES = [
241
+ "session-start",
242
+ "prompt-submit",
243
+ "stop",
244
+ "subagent-stop",
245
+ "pre-tool-task",
246
+ "pre-tool-file",
247
+ "pre-tool-bash",
248
+ "post-tool",
249
+ "notification"
250
+ ];
251
+ function packagedHooksDir() {
252
+ const here = dirname(fileURLToPath(import.meta.url));
253
+ const candidates = [
254
+ join2(here, "hooks"),
255
+ join2(here, "..", "hooks"),
256
+ join2(here, "..", "..", "hooks")
257
+ ];
258
+ for (const p of candidates) {
259
+ if (existsSync(join2(p, "session-start.sh"))) return p;
260
+ }
261
+ return candidates[0];
262
+ }
263
+ function packagedAgentsDir() {
264
+ const here = dirname(fileURLToPath(import.meta.url));
265
+ const candidates = [
266
+ join2(here, "..", "..", "agents"),
267
+ join2(here, "..", "..", "..", "agents"),
268
+ join2(here, "..", "..", "..", "..", "packages", "agents")
269
+ ];
270
+ for (const p of candidates) {
271
+ if (existsSync(join2(p, "manifest.json"))) return p;
272
+ }
273
+ return candidates[0];
274
+ }
275
+ function packagedSkillsDir() {
276
+ const here = dirname(fileURLToPath(import.meta.url));
277
+ const candidates = [
278
+ join2(here, "..", "..", "skills"),
279
+ join2(here, "..", "..", "..", "skills"),
280
+ join2(here, "..", "..", "..", "..", "packages", "skills")
281
+ ];
282
+ for (const p of candidates) {
283
+ if (existsSync(p)) return p;
284
+ }
285
+ return candidates[0];
286
+ }
287
+
288
+ // src/doctor.ts
289
+ async function probeHealth(port) {
290
+ const url = `http://127.0.0.1:${port}/api/health`;
291
+ try {
292
+ const res = await fetch(url, {
293
+ signal: AbortSignal.timeout(800)
294
+ });
295
+ if (!res.ok) {
296
+ return { ok: false, label: "Server reachable", detail: `HTTP ${res.status}` };
297
+ }
298
+ return { ok: true, label: "Server reachable", detail: url };
299
+ } catch (err) {
300
+ return {
301
+ ok: false,
302
+ label: "Server reachable",
303
+ detail: `not running on ${url}`
304
+ };
305
+ }
306
+ }
307
+ async function doctor() {
308
+ const port = Number(process.env.SOLIX_PORT ?? 4242);
309
+ const checks = [];
310
+ const nodeVersion = process.versions.node;
311
+ const major = Number(nodeVersion.split(".")[0]);
312
+ checks.push({
313
+ ok: major >= 20,
314
+ label: "Node.js >= 20",
315
+ detail: `v${nodeVersion}`
316
+ });
317
+ checks.push({
318
+ ok: existsSync2(SOLIX_HOME),
319
+ label: "Solix home directory",
320
+ detail: SOLIX_HOME
321
+ });
322
+ let allHooksPresent = true;
323
+ const missing = [];
324
+ for (const name of HOOK_NAMES) {
325
+ const p = join3(HOOKS_DIR, `${name}.sh`);
326
+ if (!existsSync2(p)) {
327
+ allHooksPresent = false;
328
+ missing.push(name);
329
+ continue;
330
+ }
331
+ try {
332
+ statSync(p);
333
+ } catch {
334
+ allHooksPresent = false;
335
+ missing.push(name);
336
+ }
337
+ }
338
+ checks.push({
339
+ ok: allHooksPresent,
340
+ label: "Hook scripts installed",
341
+ detail: allHooksPresent ? `${HOOK_NAMES.length} scripts in ${HOOKS_DIR}` : `missing: ${missing.join(", ")}`
342
+ });
343
+ checks.push({
344
+ ok: existsSync2(CLAUDE_SETTINGS),
345
+ label: "Claude settings.json present",
346
+ detail: CLAUDE_SETTINGS
347
+ });
348
+ checks.push({
349
+ ok: existsSync2(CLAUDE_BACKUP),
350
+ label: "Backup of settings.json",
351
+ detail: existsSync2(CLAUDE_BACKUP) ? CLAUDE_BACKUP : "not yet created"
352
+ });
353
+ let advisorCount = 0;
354
+ if (existsSync2(CLAUDE_AGENTS_DIR)) {
355
+ try {
356
+ advisorCount = readdirSync(CLAUDE_AGENTS_DIR).filter(
357
+ (f) => f.endsWith(".md")
358
+ ).length;
359
+ } catch {
360
+ advisorCount = 0;
361
+ }
362
+ }
363
+ checks.push({
364
+ ok: advisorCount > 0,
365
+ label: "Advisor agents installed",
366
+ detail: advisorCount > 0 ? `${advisorCount} agents in ${CLAUDE_AGENTS_DIR}` : "none yet \u2014 run `solix install`"
367
+ });
368
+ let skillCount = 0;
369
+ if (existsSync2(SOLIX_SKILLS_DIR)) {
370
+ try {
371
+ skillCount = readdirSync(SOLIX_SKILLS_DIR).filter((entry) => {
372
+ try {
373
+ return statSync(join3(SOLIX_SKILLS_DIR, entry)).isDirectory();
374
+ } catch {
375
+ return false;
376
+ }
377
+ }).length;
378
+ } catch {
379
+ skillCount = 0;
380
+ }
381
+ }
382
+ checks.push({
383
+ ok: skillCount >= 0,
384
+ label: "Solix skill pack",
385
+ detail: skillCount > 0 ? `${skillCount} skills in ${SOLIX_SKILLS_DIR}` : "none"
386
+ });
387
+ checks.push(await probeHealth(port));
388
+ console.log("\nSolix Diagnostics\n");
389
+ let allOk = true;
390
+ for (const c of checks) {
391
+ const icon = c.ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
392
+ const detail = c.detail ? ` \x1B[2m${c.detail}\x1B[0m` : "";
393
+ console.log(`${icon} ${c.label}${detail}`);
394
+ if (!c.ok) allOk = false;
395
+ }
396
+ console.log("");
397
+ if (allOk) {
398
+ console.log("All checks passed. Solix is healthy.\n");
399
+ } else {
400
+ console.log(
401
+ "Some checks failed. Run `solix install` and `solix start` to fix common issues.\n"
402
+ );
403
+ process.exitCode = 1;
404
+ }
405
+ }
406
+
407
+ // src/galaxy.ts
408
+ import { readFileSync, writeFileSync } from "fs";
409
+ var PORT3 = process.env.SOLIX_PORT ?? "4242";
410
+ var BASE3 = `http://127.0.0.1:${PORT3}`;
411
+ async function api2(path, init) {
412
+ const res = await fetch(`${BASE3}${path}`, init);
413
+ if (!res.ok) {
414
+ const text = await res.text().catch(() => "");
415
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
416
+ }
417
+ return await res.json();
418
+ }
419
+ async function exportGalaxyCmd(outFile, opts = {}) {
420
+ try {
421
+ const params = new URLSearchParams();
422
+ if (opts.name) params.set("name", opts.name);
423
+ if (opts.author) params.set("author", opts.author);
424
+ if (opts.description) params.set("description", opts.description);
425
+ const qs = params.toString();
426
+ const manifest = await api2(
427
+ `/api/galaxy/export${qs ? `?${qs}` : ""}`
428
+ );
429
+ const text = JSON.stringify(manifest, null, 2) + "\n";
430
+ writeFileSync(outFile, text);
431
+ console.log(`[solix] exported galaxy to ${outFile}`);
432
+ } catch (err) {
433
+ console.error(`[solix] export failed: ${String(err)}`);
434
+ process.exitCode = 1;
435
+ }
436
+ }
437
+ async function publishGalaxyCmd(slug, opts = {}) {
438
+ try {
439
+ const res = await fetch(`${BASE3}/api/galaxy/publish`, {
440
+ method: "POST",
441
+ headers: { "Content-Type": "application/json" },
442
+ body: JSON.stringify({ slug, ...opts })
443
+ });
444
+ const data = await res.json();
445
+ if (!res.ok || !data.ok) {
446
+ console.error(
447
+ `[solix] publish failed: ${data.error ?? `HTTP ${res.status}`}`
448
+ );
449
+ console.error(
450
+ "[solix] hint: set SOLIX_REGISTRY_URL and SOLIX_REGISTRY_KEY before starting the server."
451
+ );
452
+ process.exitCode = 1;
453
+ return;
454
+ }
455
+ console.log(`[solix] published as ${data.slug ?? slug}`);
456
+ } catch (err) {
457
+ console.error(`[solix] publish failed: ${String(err)}`);
458
+ process.exitCode = 1;
459
+ }
460
+ }
461
+ async function installFromRegistryCmd(slug) {
462
+ try {
463
+ const res = await fetch(
464
+ `${BASE3}/api/galaxy/registry/${encodeURIComponent(slug)}/install`,
465
+ { method: "POST" }
466
+ );
467
+ const data = await res.json();
468
+ if (!res.ok || !data.ok) {
469
+ console.error(
470
+ `[solix] install failed: ${data.error ?? `HTTP ${res.status}`}`
471
+ );
472
+ process.exitCode = 1;
473
+ return;
474
+ }
475
+ console.log(
476
+ `[solix] installed ${slug}: ${data.advisorsEnabled} advisor(s) enabled, ${data.advisorsDisabled} disabled, ${data.projectsHinted} project(s) hinted`
477
+ );
478
+ } catch (err) {
479
+ console.error(`[solix] install failed: ${String(err)}`);
480
+ process.exitCode = 1;
481
+ }
482
+ }
483
+ async function importGalaxyCmd(fileOrUrl) {
484
+ try {
485
+ let body;
486
+ if (fileOrUrl.startsWith("http://") || fileOrUrl.startsWith("https://")) {
487
+ body = JSON.stringify({ url: fileOrUrl });
488
+ } else {
489
+ const text = readFileSync(fileOrUrl, "utf8");
490
+ body = text;
491
+ }
492
+ const res = await api2(`/api/galaxy/import`, {
493
+ method: "POST",
494
+ headers: { "Content-Type": "application/json" },
495
+ body
496
+ });
497
+ if (res.ok) {
498
+ console.log(
499
+ `[solix] imported: ${res.advisorsEnabled} advisor(s) enabled, ${res.advisorsDisabled} disabled, ${res.projectsHinted} project(s) hinted`
500
+ );
501
+ } else {
502
+ console.error(`[solix] import failed: ${res.error ?? "unknown"}`);
503
+ process.exitCode = 1;
504
+ }
505
+ } catch (err) {
506
+ console.error(`[solix] import failed: ${String(err)}`);
507
+ process.exitCode = 1;
508
+ }
509
+ }
510
+
511
+ // src/install.ts
512
+ import {
513
+ copyFileSync,
514
+ cpSync,
515
+ existsSync as existsSync3,
516
+ mkdirSync,
517
+ readdirSync as readdirSync2,
518
+ readFileSync as readFileSync2,
519
+ statSync as statSync2,
520
+ writeFileSync as writeFileSync2,
521
+ chmodSync
522
+ } from "fs";
523
+ import { join as join4 } from "path";
524
+ function readSettings() {
525
+ if (!existsSync3(CLAUDE_SETTINGS)) return {};
526
+ try {
527
+ const txt = readFileSync2(CLAUDE_SETTINGS, "utf8");
528
+ return JSON.parse(txt);
529
+ } catch (err) {
530
+ console.warn(`[solix] could not parse ${CLAUDE_SETTINGS}: ${String(err)}`);
531
+ return {};
532
+ }
533
+ }
534
+ function hookCommand(name) {
535
+ return `${HOOKS_DIR}/${name}.sh`;
536
+ }
537
+ function buildSolixHooks() {
538
+ return {
539
+ SessionStart: [
540
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand("session-start") }] }
541
+ ],
542
+ UserPromptSubmit: [
543
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand("prompt-submit") }] }
544
+ ],
545
+ Stop: [{ matcher: "*", hooks: [{ type: "command", command: hookCommand("stop") }] }],
546
+ SubagentStop: [
547
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand("subagent-stop") }] }
548
+ ],
549
+ PreToolUse: [
550
+ { matcher: "Task", hooks: [{ type: "command", command: hookCommand("pre-tool-task") }] },
551
+ {
552
+ matcher: "Read|Write|Edit|MultiEdit",
553
+ hooks: [{ type: "command", command: hookCommand("pre-tool-file") }]
554
+ },
555
+ { matcher: "Bash", hooks: [{ type: "command", command: hookCommand("pre-tool-bash") }] }
556
+ ],
557
+ PostToolUse: [
558
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand("post-tool") }] }
559
+ ],
560
+ Notification: [
561
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand("notification") }] }
562
+ ]
563
+ };
564
+ }
565
+ function isSolixHook(entry) {
566
+ return entry.hooks.some((h) => h.command.includes(`${HOOKS_DIR}/`));
567
+ }
568
+ function mergeHooks(existing, solix) {
569
+ const merged = { ...existing ?? {} };
570
+ for (const [evt, solixEntries] of Object.entries(solix)) {
571
+ const userEntries = (merged[evt] ?? []).filter((e) => !isSolixHook(e));
572
+ merged[evt] = [...userEntries, ...solixEntries];
573
+ }
574
+ return merged;
575
+ }
576
+ function installHookScripts() {
577
+ mkdirSync(HOOKS_DIR, { recursive: true });
578
+ const src = packagedHooksDir();
579
+ if (!existsSync3(src)) {
580
+ throw new Error(
581
+ `Solix hook scripts not found at ${src}. Did the package build correctly?`
582
+ );
583
+ }
584
+ for (const name of HOOK_NAMES) {
585
+ const from = join4(src, `${name}.sh`);
586
+ const to = join4(HOOKS_DIR, `${name}.sh`);
587
+ copyFileSync(from, to);
588
+ chmodSync(to, 493);
589
+ }
590
+ }
591
+ function installAdvisorAgents() {
592
+ const src = packagedAgentsDir();
593
+ if (!existsSync3(src)) {
594
+ console.warn(`[solix] no advisors/ directory at ${src}; skipping`);
595
+ return 0;
596
+ }
597
+ const manifestPath = join4(src, "manifest.json");
598
+ if (!existsSync3(manifestPath)) return 0;
599
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
600
+ mkdirSync(CLAUDE_AGENTS_DIR, { recursive: true });
601
+ let copied = 0;
602
+ for (const a of manifest.advisors) {
603
+ const from = join4(src, a.agentMd);
604
+ const to = join4(CLAUDE_AGENTS_DIR, a.agentMd);
605
+ if (!existsSync3(from)) continue;
606
+ copyFileSync(from, to);
607
+ copied += 1;
608
+ }
609
+ return copied;
610
+ }
611
+ function installSolixSkills() {
612
+ const src = packagedSkillsDir();
613
+ if (!existsSync3(src)) return 0;
614
+ mkdirSync(SOLIX_SKILLS_DIR, { recursive: true });
615
+ let copied = 0;
616
+ for (const entry of readdirSync2(src)) {
617
+ const fromDir = join4(src, entry);
618
+ let isDir = false;
619
+ try {
620
+ isDir = statSync2(fromDir).isDirectory();
621
+ } catch {
622
+ continue;
623
+ }
624
+ if (!isDir) continue;
625
+ const toDir = join4(SOLIX_SKILLS_DIR, entry);
626
+ cpSync(fromDir, toDir, { recursive: true });
627
+ copied += 1;
628
+ }
629
+ return copied;
630
+ }
631
+ function install(opts = {}) {
632
+ mkdirSync(SOLIX_HOME, { recursive: true });
633
+ mkdirSync(CLAUDE_DIR, { recursive: true });
634
+ const existing = readSettings();
635
+ if (existsSync3(CLAUDE_SETTINGS) && !existsSync3(CLAUDE_BACKUP)) {
636
+ copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
637
+ console.log(`[solix] backed up settings.json -> ${CLAUDE_BACKUP}`);
638
+ } else if (opts.force && existsSync3(CLAUDE_SETTINGS)) {
639
+ copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
640
+ }
641
+ installHookScripts();
642
+ console.log(`[solix] installed hook scripts in ${HOOKS_DIR}`);
643
+ const advisorsCopied = installAdvisorAgents();
644
+ if (advisorsCopied > 0) {
645
+ console.log(
646
+ `[solix] installed ${advisorsCopied} advisor agents in ${CLAUDE_AGENTS_DIR}`
647
+ );
648
+ }
649
+ const skillsCopied = installSolixSkills();
650
+ if (skillsCopied > 0) {
651
+ console.log(
652
+ `[solix] installed ${skillsCopied} Solix skills in ${SOLIX_SKILLS_DIR}`
653
+ );
654
+ }
655
+ const merged = mergeHooks(existing.hooks, buildSolixHooks());
656
+ const next = { ...existing, hooks: merged };
657
+ writeFileSync2(CLAUDE_SETTINGS, JSON.stringify(next, null, 2) + "\n");
658
+ console.log(`[solix] merged hooks into ${CLAUDE_SETTINGS}`);
659
+ }
660
+
661
+ // src/skills.ts
662
+ var PORT4 = process.env.SOLIX_PORT ?? "4242";
663
+ var BASE4 = `http://127.0.0.1:${PORT4}`;
664
+ async function api3(path, init) {
665
+ const res = await fetch(`${BASE4}${path}`, init);
666
+ if (!res.ok) {
667
+ const text = await res.text().catch(() => "");
668
+ throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
669
+ }
670
+ return await res.json();
671
+ }
672
+ async function listSkillsCmd() {
673
+ try {
674
+ const skills2 = await api3("/api/skills");
675
+ if (!skills2.length) {
676
+ console.log(
677
+ "No skills found. Drop SKILL.md files into ~/.claude/skills/ or ~/.solix/skills/."
678
+ );
679
+ return;
680
+ }
681
+ console.log("source id installed name");
682
+ for (const s of skills2) {
683
+ const installed = String(s.installedInProjects.length).padStart(2, " ");
684
+ console.log(
685
+ `${s.source.padEnd(10)} ${s.id.padEnd(32)} ${installed} ${s.name}`
686
+ );
687
+ }
688
+ } catch (err) {
689
+ console.error(`[solix] could not reach server at ${BASE4}: ${String(err)}`);
690
+ process.exitCode = 1;
691
+ }
692
+ }
693
+ async function installSkillCmd(id, projectId) {
694
+ if (!projectId) {
695
+ console.error(
696
+ "[solix] --project <projectId> is required (use `solix doctor` or `/api/projects` to find it)"
697
+ );
698
+ process.exitCode = 1;
699
+ return;
700
+ }
701
+ try {
702
+ const res = await api3(
703
+ `/api/skills/${encodeURIComponent(id)}/install`,
704
+ {
705
+ method: "POST",
706
+ headers: { "Content-Type": "application/json" },
707
+ body: JSON.stringify({ projectId })
708
+ }
709
+ );
710
+ if (res.ok) {
711
+ console.log(`[solix] ${id} installed in project ${projectId}`);
712
+ } else {
713
+ console.error(`[solix] failed: skill not found?`);
714
+ process.exitCode = 1;
715
+ }
716
+ } catch (err) {
717
+ console.error(`[solix] could not reach server: ${String(err)}`);
718
+ process.exitCode = 1;
719
+ }
720
+ }
721
+
722
+ // ../server/src/create.ts
723
+ import { serve } from "@hono/node-server";
724
+
725
+ // ../server/src/broadcaster.ts
726
+ var Broadcaster = class {
727
+ clients = /* @__PURE__ */ new Set();
728
+ add(ws) {
729
+ this.clients.add(ws);
730
+ }
731
+ remove(ws) {
732
+ this.clients.delete(ws);
733
+ }
734
+ send(ws, msg) {
735
+ if (ws.readyState !== ws.OPEN) return;
736
+ ws.send(JSON.stringify(msg));
737
+ }
738
+ broadcast(msg) {
739
+ const payload = JSON.stringify(msg);
740
+ for (const ws of this.clients) {
741
+ if (ws.readyState === ws.OPEN) {
742
+ ws.send(payload);
743
+ }
744
+ }
745
+ }
746
+ size() {
747
+ return this.clients.size;
748
+ }
749
+ };
750
+
751
+ // ../server/src/db.ts
752
+ import Database from "better-sqlite3";
753
+
754
+ // ../server/src/paths.ts
755
+ import { homedir as homedir3 } from "os";
756
+ import { join as join5 } from "path";
757
+ import { mkdirSync as mkdirSync2 } from "fs";
758
+ var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join5(homedir3(), ".solix");
759
+ var DB_PATH = join5(SOLIX_HOME2, "solix.db");
760
+ var HOOKS_DIR2 = join5(SOLIX_HOME2, "hooks");
761
+ var LOG_PATH = join5(SOLIX_HOME2, "solix.log");
762
+ function ensureSolixHome() {
763
+ mkdirSync2(SOLIX_HOME2, { recursive: true });
764
+ mkdirSync2(HOOKS_DIR2, { recursive: true });
765
+ }
766
+
767
+ // ../server/src/db.ts
768
+ var SCHEMA = `
769
+ CREATE TABLE IF NOT EXISTS projects (
770
+ id TEXT PRIMARY KEY,
771
+ cwd TEXT NOT NULL UNIQUE,
772
+ name TEXT NOT NULL,
773
+ first_seen_at INTEGER NOT NULL,
774
+ last_active_at INTEGER NOT NULL
775
+ );
776
+
777
+ CREATE TABLE IF NOT EXISTS sessions (
778
+ id TEXT PRIMARY KEY,
779
+ pid INTEGER,
780
+ project_id TEXT NOT NULL REFERENCES projects(id),
781
+ parent_session_id TEXT REFERENCES sessions(id),
782
+ origin TEXT NOT NULL CHECK (origin IN ('external','internal')),
783
+ model TEXT,
784
+ status TEXT NOT NULL,
785
+ context_usage_pct REAL DEFAULT 0,
786
+ orbit_slot INTEGER NOT NULL,
787
+ cwd TEXT NOT NULL,
788
+ name TEXT,
789
+ kind TEXT NOT NULL DEFAULT 'user',
790
+ advisor_role TEXT,
791
+ current_mission_id TEXT,
792
+ last_completed_mission_id TEXT,
793
+ created_at INTEGER NOT NULL,
794
+ updated_at INTEGER NOT NULL,
795
+ terminated_at INTEGER
796
+ );
797
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
798
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
799
+ CREATE INDEX IF NOT EXISTS idx_sessions_kind ON sessions(kind);
800
+
801
+ CREATE TABLE IF NOT EXISTS advisors (
802
+ id TEXT PRIMARY KEY,
803
+ role TEXT NOT NULL,
804
+ codename TEXT NOT NULL,
805
+ name TEXT NOT NULL,
806
+ description TEXT NOT NULL,
807
+ glyph TEXT,
808
+ color TEXT,
809
+ default_model TEXT,
810
+ agent_md_path TEXT NOT NULL,
811
+ required_skills_json TEXT DEFAULT '[]',
812
+ enabled INTEGER NOT NULL DEFAULT 0,
813
+ pinned INTEGER NOT NULL DEFAULT 0,
814
+ pinned_session_id TEXT,
815
+ updated_at INTEGER NOT NULL
816
+ );
817
+
818
+ CREATE TABLE IF NOT EXISTS skills (
819
+ id TEXT PRIMARY KEY,
820
+ name TEXT NOT NULL,
821
+ description TEXT,
822
+ source TEXT NOT NULL CHECK (source IN ('anthropic','solix','user')),
823
+ manifest_path TEXT NOT NULL,
824
+ installed_in_projects_json TEXT DEFAULT '[]',
825
+ updated_at INTEGER NOT NULL
826
+ );
827
+
828
+ CREATE TABLE IF NOT EXISTS galaxy_imports (
829
+ id TEXT PRIMARY KEY,
830
+ source_url TEXT,
831
+ manifest_json TEXT NOT NULL,
832
+ imported_at INTEGER NOT NULL
833
+ );
834
+
835
+ CREATE TABLE IF NOT EXISTS audit_events (
836
+ id TEXT PRIMARY KEY,
837
+ ts INTEGER NOT NULL,
838
+ kind TEXT NOT NULL,
839
+ session_id TEXT,
840
+ advisor_id TEXT,
841
+ project_id TEXT,
842
+ summary TEXT NOT NULL,
843
+ payload_json TEXT
844
+ );
845
+ CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_events(ts);
846
+ CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_events(session_id);
847
+
848
+ CREATE TABLE IF NOT EXISTS galaxy_versions (
849
+ id TEXT PRIMARY KEY,
850
+ ts INTEGER NOT NULL,
851
+ ordinal INTEGER NOT NULL,
852
+ name TEXT NOT NULL,
853
+ author TEXT,
854
+ description TEXT,
855
+ manifest_json TEXT NOT NULL
856
+ );
857
+ CREATE INDEX IF NOT EXISTS idx_galaxy_versions_ts ON galaxy_versions(ts);
858
+
859
+ CREATE TABLE IF NOT EXISTS missions (
860
+ id TEXT PRIMARY KEY,
861
+ session_id TEXT NOT NULL REFERENCES sessions(id),
862
+ prompt TEXT NOT NULL,
863
+ short_name TEXT,
864
+ long_summary TEXT,
865
+ status TEXT NOT NULL,
866
+ started_at INTEGER NOT NULL,
867
+ completed_at INTEGER,
868
+ duration_ms INTEGER,
869
+ total_tokens INTEGER,
870
+ lines_added INTEGER DEFAULT 0,
871
+ lines_removed INTEGER DEFAULT 0,
872
+ subagent_count INTEGER DEFAULT 0,
873
+ tool_call_count INTEGER DEFAULT 0,
874
+ files_touched_json TEXT DEFAULT '[]'
875
+ );
876
+ CREATE INDEX IF NOT EXISTS idx_missions_session ON missions(session_id);
877
+ CREATE INDEX IF NOT EXISTS idx_missions_completed_at ON missions(completed_at);
878
+
879
+ CREATE TABLE IF NOT EXISTS tool_calls (
880
+ id TEXT PRIMARY KEY,
881
+ session_id TEXT NOT NULL,
882
+ mission_id TEXT,
883
+ tool TEXT NOT NULL,
884
+ args_json TEXT,
885
+ status TEXT NOT NULL,
886
+ started_at INTEGER NOT NULL,
887
+ completed_at INTEGER
888
+ );
889
+
890
+ CREATE TABLE IF NOT EXISTS scheduled_tasks (
891
+ id TEXT PRIMARY KEY,
892
+ project_id TEXT NOT NULL REFERENCES projects(id),
893
+ prompt TEXT NOT NULL,
894
+ cron TEXT NOT NULL,
895
+ enabled INTEGER DEFAULT 1,
896
+ last_run_at INTEGER,
897
+ next_run_at INTEGER NOT NULL
898
+ );
899
+ `;
900
+ var _db = null;
901
+ function ensureColumn(db, table, column, ddl) {
902
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
903
+ if (!cols.some((c) => c.name === column)) {
904
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
905
+ }
906
+ }
907
+ function getDb() {
908
+ if (_db) return _db;
909
+ ensureSolixHome();
910
+ const db = new Database(DB_PATH);
911
+ db.pragma("journal_mode = WAL");
912
+ db.pragma("foreign_keys = ON");
913
+ db.exec(SCHEMA);
914
+ ensureColumn(db, "sessions", "kind", "kind TEXT NOT NULL DEFAULT 'user'");
915
+ ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
916
+ ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
917
+ _db = db;
918
+ return db;
919
+ }
920
+
921
+ // ../server/src/http.ts
922
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
923
+ import { dirname as dirname4, extname, join as join8, resolve as resolve3 } from "path";
924
+ import { fileURLToPath as fileURLToPath4 } from "url";
925
+ import { spawnSync } from "child_process";
926
+ import { Hono } from "hono";
927
+ import { cors } from "hono/cors";
928
+
929
+ // ../server/src/util.ts
930
+ import { createHash } from "crypto";
931
+ import { basename } from "path";
932
+ function hashCwd(cwd) {
933
+ return createHash("sha1").update(cwd).digest("hex").slice(0, 12);
934
+ }
935
+ function projectNameFromCwd(cwd) {
936
+ return basename(cwd) || cwd;
937
+ }
938
+ function now() {
939
+ return Date.now();
940
+ }
941
+
942
+ // ../server/src/state/projects.ts
943
+ function rowToProject(row) {
944
+ return {
945
+ id: row.id,
946
+ cwd: row.cwd,
947
+ name: row.name,
948
+ firstSeenAt: row.first_seen_at,
949
+ lastActiveAt: row.last_active_at
950
+ };
951
+ }
952
+ function ensureProject(db, cwd) {
953
+ const id = hashCwd(cwd);
954
+ const ts2 = now();
955
+ const existing = db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
956
+ if (existing) {
957
+ db.prepare("UPDATE projects SET last_active_at = ? WHERE id = ?").run(
958
+ ts2,
959
+ id
960
+ );
961
+ return rowToProject({ ...existing, last_active_at: ts2 });
962
+ }
963
+ const name = projectNameFromCwd(cwd);
964
+ db.prepare(
965
+ `INSERT INTO projects (id, cwd, name, first_seen_at, last_active_at)
966
+ VALUES (?, ?, ?, ?, ?)`
967
+ ).run(id, cwd, name, ts2, ts2);
968
+ return {
969
+ id,
970
+ cwd,
971
+ name,
972
+ firstSeenAt: ts2,
973
+ lastActiveAt: ts2
974
+ };
975
+ }
976
+ function listProjects(db) {
977
+ const rows = db.prepare("SELECT * FROM projects ORDER BY last_active_at DESC").all();
978
+ return rows.map(rowToProject);
979
+ }
980
+
981
+ // ../server/src/state/sessions.ts
982
+ function rowToSession(row) {
983
+ return {
984
+ id: row.id,
985
+ pid: row.pid ?? 0,
986
+ cwd: row.cwd,
987
+ projectId: row.project_id,
988
+ createdAt: row.created_at,
989
+ updatedAt: row.updated_at,
990
+ status: row.status,
991
+ model: row.model ?? "default",
992
+ origin: row.origin,
993
+ kind: row.kind ?? "user",
994
+ advisorRole: row.advisor_role ?? void 0,
995
+ parentSessionId: row.parent_session_id ?? void 0,
996
+ contextUsagePct: row.context_usage_pct,
997
+ currentMissionId: row.current_mission_id ?? void 0,
998
+ lastCompletedMissionId: row.last_completed_mission_id ?? void 0,
999
+ orbitSlot: row.orbit_slot,
1000
+ name: row.name ?? void 0
1001
+ };
1002
+ }
1003
+ function nextOrbitSlot(db, projectId) {
1004
+ const row = db.prepare(
1005
+ `SELECT COALESCE(MAX(orbit_slot), -1) AS max_slot
1006
+ FROM sessions
1007
+ WHERE project_id = ? AND parent_session_id IS NULL
1008
+ AND status NOT IN ('terminated')`
1009
+ ).get(projectId);
1010
+ return (row.max_slot ?? -1) + 1;
1011
+ }
1012
+ function upsertSession(db, input) {
1013
+ const ts2 = now();
1014
+ const existing = db.prepare("SELECT * FROM sessions WHERE id = ?").get(input.id);
1015
+ if (existing) {
1016
+ db.prepare(
1017
+ `UPDATE sessions
1018
+ SET pid = ?, status = CASE WHEN status = 'terminated' THEN 'idle' ELSE status END,
1019
+ updated_at = ?
1020
+ WHERE id = ?`
1021
+ ).run(input.pid, ts2, input.id);
1022
+ return rowToSession({
1023
+ ...existing,
1024
+ pid: input.pid,
1025
+ updated_at: ts2
1026
+ });
1027
+ }
1028
+ const orbitSlot = input.parentSessionId ? 0 : nextOrbitSlot(db, input.projectId);
1029
+ const status = "idle";
1030
+ const kind = input.kind ?? "user";
1031
+ db.prepare(
1032
+ `INSERT INTO sessions (
1033
+ id, pid, project_id, parent_session_id, origin, model, status,
1034
+ context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
1035
+ created_at, updated_at
1036
+ )
1037
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?)`
1038
+ ).run(
1039
+ input.id,
1040
+ input.pid,
1041
+ input.projectId,
1042
+ input.parentSessionId ?? null,
1043
+ input.origin,
1044
+ input.model ?? "default",
1045
+ status,
1046
+ orbitSlot,
1047
+ input.cwd,
1048
+ kind,
1049
+ input.advisorRole ?? null,
1050
+ ts2,
1051
+ ts2
1052
+ );
1053
+ return {
1054
+ id: input.id,
1055
+ pid: input.pid,
1056
+ cwd: input.cwd,
1057
+ projectId: input.projectId,
1058
+ createdAt: ts2,
1059
+ updatedAt: ts2,
1060
+ status,
1061
+ model: input.model ?? "default",
1062
+ origin: input.origin,
1063
+ kind,
1064
+ advisorRole: input.advisorRole,
1065
+ parentSessionId: input.parentSessionId,
1066
+ contextUsagePct: 0,
1067
+ orbitSlot
1068
+ };
1069
+ }
1070
+ function setSessionStatus(db, sessionId, status) {
1071
+ const ts2 = now();
1072
+ if (status === "terminated") {
1073
+ db.prepare(
1074
+ `UPDATE sessions SET status = ?, updated_at = ?, terminated_at = ? WHERE id = ?`
1075
+ ).run(status, ts2, ts2, sessionId);
1076
+ } else {
1077
+ db.prepare(
1078
+ `UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?`
1079
+ ).run(status, ts2, sessionId);
1080
+ }
1081
+ return getSession(db, sessionId);
1082
+ }
1083
+ function setSessionMission(db, sessionId, missionId) {
1084
+ const ts2 = now();
1085
+ db.prepare(
1086
+ `UPDATE sessions SET current_mission_id = ?, updated_at = ? WHERE id = ?`
1087
+ ).run(missionId, ts2, sessionId);
1088
+ return getSession(db, sessionId);
1089
+ }
1090
+ function setSessionContextUsage(db, sessionId, pct) {
1091
+ const clamped = Math.max(0, Math.min(100, pct));
1092
+ const ts2 = now();
1093
+ db.prepare(
1094
+ `UPDATE sessions SET context_usage_pct = ?, updated_at = ? WHERE id = ?`
1095
+ ).run(clamped, ts2, sessionId);
1096
+ return getSession(db, sessionId);
1097
+ }
1098
+ function getSession(db, sessionId) {
1099
+ const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
1100
+ return row ? rowToSession(row) : null;
1101
+ }
1102
+ function listActiveSessions(db) {
1103
+ const rows = db.prepare(
1104
+ `SELECT * FROM sessions
1105
+ WHERE status != 'terminated'
1106
+ ORDER BY created_at ASC`
1107
+ ).all();
1108
+ return rows.map(rowToSession);
1109
+ }
1110
+ function listSessionsForProject(db, projectId) {
1111
+ const rows = db.prepare(
1112
+ `SELECT * FROM sessions
1113
+ WHERE project_id = ? AND status != 'terminated'
1114
+ ORDER BY created_at ASC`
1115
+ ).all(projectId);
1116
+ return rows.map(rowToSession);
1117
+ }
1118
+
1119
+ // ../server/src/state/missions.ts
1120
+ import { nanoid } from "nanoid";
1121
+ function rowToMission(row) {
1122
+ let filesTouched = [];
1123
+ try {
1124
+ filesTouched = JSON.parse(row.files_touched_json);
1125
+ } catch {
1126
+ filesTouched = [];
1127
+ }
1128
+ return {
1129
+ id: row.id,
1130
+ sessionId: row.session_id,
1131
+ startedAt: row.started_at,
1132
+ completedAt: row.completed_at ?? void 0,
1133
+ prompt: row.prompt,
1134
+ shortName: row.short_name ?? row.prompt.slice(0, 32),
1135
+ longSummary: row.long_summary ?? void 0,
1136
+ status: row.status,
1137
+ metrics: {
1138
+ durationMs: row.duration_ms ?? void 0,
1139
+ totalTokens: row.total_tokens ?? void 0,
1140
+ linesAdded: row.lines_added,
1141
+ linesRemoved: row.lines_removed,
1142
+ subagentCount: row.subagent_count,
1143
+ toolCallCount: row.tool_call_count
1144
+ },
1145
+ filesTouched
1146
+ };
1147
+ }
1148
+ function shortNameFromPrompt(prompt) {
1149
+ const words = prompt.trim().split(/\s+/).slice(0, 3);
1150
+ if (!words.length) return "New Mission";
1151
+ return words.map(
1152
+ (w) => w.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/^./, (c) => c.toUpperCase())
1153
+ ).filter(Boolean).join(" ") || "New Mission";
1154
+ }
1155
+ function startMission(db, sessionId, prompt) {
1156
+ const id = nanoid();
1157
+ const ts2 = now();
1158
+ const shortName = shortNameFromPrompt(prompt);
1159
+ db.prepare(
1160
+ `INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json)
1161
+ VALUES (?, ?, ?, ?, 'active', ?, '[]')`
1162
+ ).run(id, sessionId, prompt, shortName, ts2);
1163
+ return {
1164
+ id,
1165
+ sessionId,
1166
+ startedAt: ts2,
1167
+ prompt,
1168
+ shortName,
1169
+ status: "active",
1170
+ metrics: { subagentCount: 0, toolCallCount: 0 },
1171
+ filesTouched: []
1172
+ };
1173
+ }
1174
+ function completeMission(db, missionId, status = "completed") {
1175
+ const ts2 = now();
1176
+ const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
1177
+ if (!row) return null;
1178
+ const durationMs = ts2 - row.started_at;
1179
+ db.prepare(
1180
+ `UPDATE missions
1181
+ SET status = ?, completed_at = ?, duration_ms = ?
1182
+ WHERE id = ?`
1183
+ ).run(status, ts2, durationMs, missionId);
1184
+ return getMission(db, missionId);
1185
+ }
1186
+ function bumpToolCallCount(db, missionId) {
1187
+ db.prepare(
1188
+ `UPDATE missions SET tool_call_count = tool_call_count + 1 WHERE id = ?`
1189
+ ).run(missionId);
1190
+ }
1191
+ function bumpSubagentCount(db, missionId) {
1192
+ db.prepare(
1193
+ `UPDATE missions SET subagent_count = subagent_count + 1 WHERE id = ?`
1194
+ ).run(missionId);
1195
+ }
1196
+ function addTouchedFile(db, missionId, filePath) {
1197
+ const row = db.prepare("SELECT files_touched_json FROM missions WHERE id = ?").get(missionId);
1198
+ if (!row) return;
1199
+ let files = [];
1200
+ try {
1201
+ files = JSON.parse(row.files_touched_json);
1202
+ } catch {
1203
+ files = [];
1204
+ }
1205
+ if (!files.includes(filePath)) {
1206
+ files.push(filePath);
1207
+ db.prepare("UPDATE missions SET files_touched_json = ? WHERE id = ?").run(
1208
+ JSON.stringify(files),
1209
+ missionId
1210
+ );
1211
+ }
1212
+ }
1213
+ function getMission(db, missionId) {
1214
+ const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
1215
+ return row ? rowToMission(row) : null;
1216
+ }
1217
+ function listMissions(db, opts = {}) {
1218
+ const limit = Math.min(opts.limit ?? 200, 1e3);
1219
+ if (opts.sessionId) {
1220
+ const rows2 = db.prepare(
1221
+ `SELECT * FROM missions WHERE session_id = ? ORDER BY started_at DESC LIMIT ?`
1222
+ ).all(opts.sessionId, limit);
1223
+ return rows2.map(rowToMission);
1224
+ }
1225
+ if (opts.projectId) {
1226
+ const rows2 = db.prepare(
1227
+ `SELECT m.* FROM missions m
1228
+ JOIN sessions s ON s.id = m.session_id
1229
+ WHERE s.project_id = ?
1230
+ ORDER BY m.started_at DESC LIMIT ?`
1231
+ ).all(opts.projectId, limit);
1232
+ return rows2.map(rowToMission);
1233
+ }
1234
+ const rows = db.prepare(`SELECT * FROM missions ORDER BY started_at DESC LIMIT ?`).all(limit);
1235
+ return rows.map(rowToMission);
1236
+ }
1237
+
1238
+ // ../server/src/state/timeline.ts
1239
+ function loadTimeline(db, opts = {}) {
1240
+ const sinceMs = opts.sinceMs ?? 0;
1241
+ const untilMs = opts.untilMs ?? Date.now();
1242
+ const limit = Math.min(opts.limit ?? 5e3, 2e4);
1243
+ const sessions = db.prepare(
1244
+ `SELECT id, project_id, cwd, status, created_at, terminated_at
1245
+ FROM sessions
1246
+ WHERE created_at <= ?
1247
+ AND (terminated_at IS NULL OR terminated_at >= ?)`
1248
+ ).all(untilMs, sinceMs);
1249
+ const missions = db.prepare(
1250
+ `SELECT id, session_id, short_name, prompt, status, started_at, completed_at
1251
+ FROM missions
1252
+ WHERE started_at <= ?
1253
+ AND (completed_at IS NULL OR completed_at >= ?)
1254
+ ORDER BY started_at ASC
1255
+ LIMIT ?`
1256
+ ).all(untilMs, sinceMs, limit);
1257
+ const toolCalls = db.prepare(
1258
+ `SELECT session_id, tool, started_at
1259
+ FROM tool_calls
1260
+ WHERE started_at BETWEEN ? AND ?
1261
+ ORDER BY started_at ASC
1262
+ LIMIT ?`
1263
+ ).all(sinceMs, untilMs, limit);
1264
+ const sessionMeta = /* @__PURE__ */ new Map();
1265
+ for (const s of sessions) {
1266
+ sessionMeta.set(s.id, { projectId: s.project_id, cwd: s.cwd });
1267
+ }
1268
+ const events = [];
1269
+ for (const s of sessions) {
1270
+ if (s.created_at >= sinceMs && s.created_at <= untilMs) {
1271
+ events.push({
1272
+ ts: s.created_at,
1273
+ type: "session_started",
1274
+ sessionId: s.id,
1275
+ projectId: s.project_id,
1276
+ cwd: s.cwd
1277
+ });
1278
+ }
1279
+ if (s.terminated_at && s.terminated_at >= sinceMs && s.terminated_at <= untilMs) {
1280
+ events.push({
1281
+ ts: s.terminated_at,
1282
+ type: "session_terminated",
1283
+ sessionId: s.id,
1284
+ projectId: s.project_id,
1285
+ cwd: s.cwd
1286
+ });
1287
+ }
1288
+ }
1289
+ for (const m of missions) {
1290
+ if (m.started_at >= sinceMs && m.started_at <= untilMs) {
1291
+ const meta = sessionMeta.get(m.session_id);
1292
+ events.push({
1293
+ ts: m.started_at,
1294
+ type: "mission_started",
1295
+ sessionId: m.session_id,
1296
+ projectId: meta?.projectId,
1297
+ cwd: meta?.cwd,
1298
+ missionId: m.id,
1299
+ missionShortName: m.short_name ?? void 0,
1300
+ missionPrompt: m.prompt
1301
+ });
1302
+ }
1303
+ if (m.completed_at && m.completed_at >= sinceMs && m.completed_at <= untilMs) {
1304
+ const meta = sessionMeta.get(m.session_id);
1305
+ events.push({
1306
+ ts: m.completed_at,
1307
+ type: "mission_completed",
1308
+ sessionId: m.session_id,
1309
+ projectId: meta?.projectId,
1310
+ cwd: meta?.cwd,
1311
+ missionId: m.id,
1312
+ missionShortName: m.short_name ?? void 0
1313
+ });
1314
+ }
1315
+ }
1316
+ for (const t of toolCalls) {
1317
+ const meta = sessionMeta.get(t.session_id);
1318
+ events.push({
1319
+ ts: t.started_at,
1320
+ type: "tool_call",
1321
+ sessionId: t.session_id,
1322
+ projectId: meta?.projectId,
1323
+ cwd: meta?.cwd,
1324
+ toolName: t.tool
1325
+ });
1326
+ }
1327
+ events.sort((a, b) => a.ts - b.ts);
1328
+ const capped = events.length > limit ? events.slice(events.length - limit) : events;
1329
+ const earliest = capped.length ? capped[0].ts : sinceMs;
1330
+ const latest = capped.length ? capped[capped.length - 1].ts : untilMs;
1331
+ return { earliest, latest, events: capped };
1332
+ }
1333
+
1334
+ // ../server/src/state/audit.ts
1335
+ import { nanoid as nanoid2 } from "nanoid";
1336
+ function rowToAuditEvent(row) {
1337
+ let payload;
1338
+ if (row.payload_json) {
1339
+ try {
1340
+ payload = JSON.parse(row.payload_json);
1341
+ } catch {
1342
+ payload = void 0;
1343
+ }
1344
+ }
1345
+ return {
1346
+ id: row.id,
1347
+ ts: row.ts,
1348
+ kind: row.kind,
1349
+ sessionId: row.session_id ?? void 0,
1350
+ advisorId: row.advisor_id ?? void 0,
1351
+ projectId: row.project_id ?? void 0,
1352
+ summary: row.summary,
1353
+ payload
1354
+ };
1355
+ }
1356
+ function recordAudit(db, input) {
1357
+ const event = {
1358
+ id: nanoid2(),
1359
+ ts: Date.now(),
1360
+ kind: input.kind,
1361
+ sessionId: input.sessionId,
1362
+ advisorId: input.advisorId,
1363
+ projectId: input.projectId,
1364
+ summary: input.summary,
1365
+ payload: input.payload
1366
+ };
1367
+ db.prepare(
1368
+ `INSERT INTO audit_events
1369
+ (id, ts, kind, session_id, advisor_id, project_id, summary, payload_json)
1370
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1371
+ ).run(
1372
+ event.id,
1373
+ event.ts,
1374
+ event.kind,
1375
+ event.sessionId ?? null,
1376
+ event.advisorId ?? null,
1377
+ event.projectId ?? null,
1378
+ event.summary,
1379
+ event.payload ? JSON.stringify(event.payload) : null
1380
+ );
1381
+ return event;
1382
+ }
1383
+ function listAudit(db, opts = {}) {
1384
+ const limit = Math.min(opts.limit ?? 200, 1e3);
1385
+ const where = [];
1386
+ const params = [];
1387
+ if (opts.sessionId) {
1388
+ where.push("session_id = ?");
1389
+ params.push(opts.sessionId);
1390
+ }
1391
+ if (opts.kind) {
1392
+ where.push("kind = ?");
1393
+ params.push(opts.kind);
1394
+ }
1395
+ if (typeof opts.since === "number") {
1396
+ where.push("ts >= ?");
1397
+ params.push(opts.since);
1398
+ }
1399
+ if (typeof opts.until === "number") {
1400
+ where.push("ts <= ?");
1401
+ params.push(opts.until);
1402
+ }
1403
+ const whereClause = where.length ? `WHERE ${where.join(" AND ")}` : "";
1404
+ const rows = db.prepare(
1405
+ `SELECT * FROM audit_events ${whereClause}
1406
+ ORDER BY ts DESC
1407
+ LIMIT ?`
1408
+ ).all(...params, limit);
1409
+ return rows.map(rowToAuditEvent);
1410
+ }
1411
+
1412
+ // ../server/src/state/advisors.ts
1413
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1414
+ import { dirname as dirname2, join as join6, resolve } from "path";
1415
+ import { fileURLToPath as fileURLToPath2 } from "url";
1416
+ function findAgentsDir() {
1417
+ if (process.env.SOLIX_AGENTS_DIR && existsSync4(process.env.SOLIX_AGENTS_DIR)) {
1418
+ return process.env.SOLIX_AGENTS_DIR;
1419
+ }
1420
+ const here = dirname2(fileURLToPath2(import.meta.url));
1421
+ const candidates = [
1422
+ // Bundled npm package: agents/ ships next to the bundled JS file.
1423
+ resolve(here, "agents"),
1424
+ resolve(here, "..", "..", "..", "agents"),
1425
+ resolve(here, "..", "..", "agents"),
1426
+ resolve(here, "..", "agents"),
1427
+ resolve(process.cwd(), "packages", "agents")
1428
+ ];
1429
+ for (const c of candidates) {
1430
+ if (existsSync4(join6(c, "manifest.json"))) return c;
1431
+ }
1432
+ return candidates[0];
1433
+ }
1434
+ var AGENTS_DIR = findAgentsDir();
1435
+ function readManifest() {
1436
+ const path = join6(AGENTS_DIR, "manifest.json");
1437
+ if (!existsSync4(path)) {
1438
+ return { version: 1, advisors: [] };
1439
+ }
1440
+ return JSON.parse(readFileSync3(path, "utf8"));
1441
+ }
1442
+ function rowToAdvisor(row) {
1443
+ let requiredSkills = [];
1444
+ try {
1445
+ requiredSkills = JSON.parse(row.required_skills_json);
1446
+ } catch {
1447
+ requiredSkills = [];
1448
+ }
1449
+ return {
1450
+ id: row.id,
1451
+ role: row.role,
1452
+ codename: row.codename,
1453
+ name: row.name,
1454
+ description: row.description,
1455
+ glyph: row.glyph ?? "",
1456
+ color: row.color ?? "#94a3b8",
1457
+ defaultModel: row.default_model ?? "default",
1458
+ agentMdPath: row.agent_md_path,
1459
+ enabled: row.enabled === 1,
1460
+ pinned: row.pinned === 1,
1461
+ pinnedSessionId: row.pinned_session_id ?? void 0,
1462
+ requiredSkills,
1463
+ texturePack: row.texture_pack ?? void 0
1464
+ };
1465
+ }
1466
+ function seedAdvisors(db) {
1467
+ const manifest = readManifest();
1468
+ const ts2 = now();
1469
+ const insert = db.prepare(
1470
+ `INSERT OR IGNORE INTO advisors
1471
+ (id, role, codename, name, description, glyph, color, default_model,
1472
+ agent_md_path, required_skills_json, enabled, pinned, texture_pack, updated_at)
1473
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`
1474
+ );
1475
+ const refresh = db.prepare(
1476
+ `UPDATE advisors
1477
+ SET role = ?, codename = ?, name = ?, description = ?, glyph = ?,
1478
+ color = ?, default_model = ?, agent_md_path = ?,
1479
+ required_skills_json = ?, texture_pack = ?, updated_at = ?
1480
+ WHERE id = ?`
1481
+ );
1482
+ for (const a of manifest.advisors) {
1483
+ const md = join6(AGENTS_DIR, a.agentMd);
1484
+ insert.run(
1485
+ a.id,
1486
+ a.role,
1487
+ a.codename,
1488
+ a.name,
1489
+ a.description,
1490
+ a.glyph,
1491
+ a.color,
1492
+ a.defaultModel,
1493
+ md,
1494
+ JSON.stringify(a.requiredSkills),
1495
+ a.enabledByDefault ? 1 : 0,
1496
+ a.texturePack ?? null,
1497
+ ts2
1498
+ );
1499
+ refresh.run(
1500
+ a.role,
1501
+ a.codename,
1502
+ a.name,
1503
+ a.description,
1504
+ a.glyph,
1505
+ a.color,
1506
+ a.defaultModel,
1507
+ md,
1508
+ JSON.stringify(a.requiredSkills),
1509
+ a.texturePack ?? null,
1510
+ ts2,
1511
+ a.id
1512
+ );
1513
+ }
1514
+ return listAdvisors(db);
1515
+ }
1516
+ function listAdvisors(db) {
1517
+ const rows = db.prepare(
1518
+ "SELECT * FROM advisors ORDER BY enabled DESC, pinned DESC, codename ASC"
1519
+ ).all();
1520
+ return rows.map(rowToAdvisor);
1521
+ }
1522
+ function getAdvisor(db, id) {
1523
+ const row = db.prepare("SELECT * FROM advisors WHERE id = ?").get(id);
1524
+ return row ? rowToAdvisor(row) : null;
1525
+ }
1526
+ function setAdvisorEnabled(db, id, enabled) {
1527
+ const ts2 = now();
1528
+ db.prepare(
1529
+ "UPDATE advisors SET enabled = ?, updated_at = ? WHERE id = ?"
1530
+ ).run(enabled ? 1 : 0, ts2, id);
1531
+ return getAdvisor(db, id);
1532
+ }
1533
+ function setAdvisorPinned(db, id, pinned, sessionId) {
1534
+ const ts2 = now();
1535
+ db.prepare(
1536
+ "UPDATE advisors SET pinned = ?, pinned_session_id = ?, updated_at = ? WHERE id = ?"
1537
+ ).run(pinned ? 1 : 0, sessionId ?? null, ts2, id);
1538
+ return getAdvisor(db, id);
1539
+ }
1540
+ function readAdvisorAgentMd(advisor) {
1541
+ if (!existsSync4(advisor.agentMdPath)) {
1542
+ return "";
1543
+ }
1544
+ return readFileSync3(advisor.agentMdPath, "utf8");
1545
+ }
1546
+
1547
+ // ../server/src/state/context.ts
1548
+ var MISSIONS_FOR_HANDOFF = 3;
1549
+ var DEFAULT_ASKS = {
1550
+ pm: "Review the recent missions and propose 1\u20133 next features in priority order, with a one-line rationale each.",
1551
+ builder: "Identify the smallest unfinished item from the recent missions and propose an implementation plan.",
1552
+ ux: "Audit the most recent UI-affecting mission for visual polish opportunities.",
1553
+ reviewer: "Review the diff of the most recently completed mission. Output: numbered findings with severity and proposed fix.",
1554
+ security: "Audit the changes from the last 3 missions for security regressions, especially around hooks, settings.json, and child-process spawns.",
1555
+ qa: "Identify the most recent untested change and propose the smallest test that would catch a regression in it.",
1556
+ devrel: "Identify the most recent user-facing change and update the README accordingly.",
1557
+ perf: "Profile the most recent change for hot-path allocations or bundle-size regressions.",
1558
+ release: "Decide whether the recent missions justify a patch / minor / major bump and draft a one-paragraph changelog entry.",
1559
+ curator: "Review skills installed in this project against missions performed; recommend additions or retirements."
1560
+ };
1561
+ function summarizeMission(m) {
1562
+ const head = `- ${m.shortName}: ${m.longSummary ?? m.prompt.slice(0, 120)}`;
1563
+ const meta = ` (status: ${m.status}, ${m.metrics.toolCallCount} tool calls, ${m.metrics.subagentCount} subagents)`;
1564
+ const files = m.filesTouched.length > 0 ? ` files: ${m.filesTouched.slice(0, 5).join(", ")}${m.filesTouched.length > 5 ? ` (+${m.filesTouched.length - 5})` : ""}` : "";
1565
+ return [head, meta, files].filter(Boolean).join("\n");
1566
+ }
1567
+ function contextBudgetNote(target) {
1568
+ if (!target) return "";
1569
+ const pct = target.contextUsagePct;
1570
+ if (pct >= 90) {
1571
+ return `
1572
+
1573
+ \u26A0 CONTEXT NEAR LIMIT: target session is at ${pct.toFixed(0)}%. Suggest the user run /compact before deeper work.`;
1574
+ }
1575
+ if (pct >= 80) {
1576
+ return `
1577
+
1578
+ \u26A0 Context budget warning: target session is at ${pct.toFixed(0)}%. Keep your output tight.`;
1579
+ }
1580
+ return "";
1581
+ }
1582
+ function buildContextEnvelope(db, args) {
1583
+ const advisor = getAdvisor(db, args.advisorId);
1584
+ if (!advisor) return null;
1585
+ const target = args.targetSessionId ? getSession(db, args.targetSessionId) : null;
1586
+ const recent = target ? listMissions(db, {
1587
+ sessionId: target.id,
1588
+ limit: MISSIONS_FOR_HANDOFF
1589
+ }) : [];
1590
+ const role = advisor.role;
1591
+ const defaultAsk = DEFAULT_ASKS[role] ?? `Act in your role as ${advisor.codename} (${advisor.name}).`;
1592
+ const userAsk = args.userPrompt?.trim();
1593
+ const lines = [];
1594
+ lines.push(`You are ${advisor.codename} (${advisor.name}).`);
1595
+ lines.push("");
1596
+ lines.push(advisor.description);
1597
+ if (target) {
1598
+ lines.push("");
1599
+ lines.push(`Active project: ${target.cwd}`);
1600
+ lines.push(
1601
+ `Focused planet: ${target.name ?? target.id.slice(0, 8)} (${target.model}, status=${target.status}, context=${target.contextUsagePct.toFixed(0)}%)`
1602
+ );
1603
+ }
1604
+ if (recent.length > 0) {
1605
+ lines.push("");
1606
+ lines.push(`Recent missions on this planet (latest first):`);
1607
+ for (const m of recent) {
1608
+ lines.push(summarizeMission(m));
1609
+ }
1610
+ } else if (target) {
1611
+ lines.push("");
1612
+ lines.push("No prior missions on this planet \u2014 you are starting clean.");
1613
+ } else {
1614
+ lines.push("");
1615
+ lines.push(
1616
+ "No focused planet \u2014 operate at the project level using your role guidance."
1617
+ );
1618
+ }
1619
+ lines.push("");
1620
+ lines.push("Specific ask:");
1621
+ lines.push(userAsk && userAsk.length > 0 ? userAsk : defaultAsk);
1622
+ const tail = contextBudgetNote(target);
1623
+ if (tail) lines.push(tail);
1624
+ return {
1625
+ advisorId: advisor.id,
1626
+ advisorRole: advisor.role,
1627
+ prompt: lines.join("\n"),
1628
+ recentMissions: recent,
1629
+ targetSession: target
1630
+ };
1631
+ }
1632
+
1633
+ // ../server/src/state/skills.ts
1634
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
1635
+ import { dirname as dirname3, join as join7, resolve as resolve2 } from "path";
1636
+ import { fileURLToPath as fileURLToPath3 } from "url";
1637
+ import { homedir as homedir4 } from "os";
1638
+ function findSolixSkillsDir() {
1639
+ const here = dirname3(fileURLToPath3(import.meta.url));
1640
+ const candidates = [
1641
+ resolve2(here, "..", "..", "..", "skills"),
1642
+ resolve2(here, "..", "..", "skills"),
1643
+ resolve2(process.cwd(), "packages", "skills")
1644
+ ];
1645
+ for (const c of candidates) {
1646
+ if (existsSync5(c)) return c;
1647
+ }
1648
+ return candidates[0];
1649
+ }
1650
+ var SOLIX_SKILLS_DIR2 = findSolixSkillsDir();
1651
+ var ANTHROPIC_SKILLS_DIR = join7(homedir4(), ".claude", "skills");
1652
+ function parseSkillManifest(manifestPath, fallbackId) {
1653
+ try {
1654
+ const txt = readFileSync4(manifestPath, "utf8");
1655
+ const match = txt.match(/^---\n([\s\S]*?)\n---/);
1656
+ let name = fallbackId;
1657
+ let description = "";
1658
+ if (match) {
1659
+ const fm = match[1] ?? "";
1660
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
1661
+ const descMatch = fm.match(/^description:\s*(.+)$/m);
1662
+ if (nameMatch) name = nameMatch[1].trim();
1663
+ if (descMatch) description = descMatch[1].trim();
1664
+ }
1665
+ return { id: fallbackId, name, description };
1666
+ } catch {
1667
+ return null;
1668
+ }
1669
+ }
1670
+ function rowToSkill(row) {
1671
+ let installed = [];
1672
+ try {
1673
+ installed = JSON.parse(row.installed_in_projects_json);
1674
+ } catch {
1675
+ installed = [];
1676
+ }
1677
+ return {
1678
+ id: row.id,
1679
+ name: row.name,
1680
+ description: row.description ?? "",
1681
+ source: row.source,
1682
+ manifestPath: row.manifest_path,
1683
+ installedInProjects: installed
1684
+ };
1685
+ }
1686
+ function discoverSkills(db) {
1687
+ const ts2 = now();
1688
+ const upsert = db.prepare(
1689
+ `INSERT INTO skills (id, name, description, source, manifest_path, installed_in_projects_json, updated_at)
1690
+ VALUES (?, ?, ?, ?, ?, '[]', ?)
1691
+ ON CONFLICT(id) DO UPDATE SET
1692
+ name = excluded.name,
1693
+ description = excluded.description,
1694
+ source = excluded.source,
1695
+ manifest_path = excluded.manifest_path,
1696
+ updated_at = excluded.updated_at`
1697
+ );
1698
+ const sources = [
1699
+ { dir: ANTHROPIC_SKILLS_DIR, source: "anthropic" },
1700
+ { dir: SOLIX_SKILLS_DIR2, source: "solix" }
1701
+ ];
1702
+ for (const { dir, source } of sources) {
1703
+ if (!existsSync5(dir)) continue;
1704
+ for (const entry of readdirSync3(dir)) {
1705
+ const full = join7(dir, entry);
1706
+ let isDir = false;
1707
+ try {
1708
+ isDir = statSync3(full).isDirectory();
1709
+ } catch {
1710
+ continue;
1711
+ }
1712
+ if (!isDir) continue;
1713
+ const manifestPath = join7(full, "SKILL.md");
1714
+ if (!existsSync5(manifestPath)) continue;
1715
+ const parsed = parseSkillManifest(manifestPath, entry);
1716
+ if (!parsed) continue;
1717
+ const id = `${source}:${parsed.id}`;
1718
+ upsert.run(
1719
+ id,
1720
+ parsed.name,
1721
+ parsed.description,
1722
+ source,
1723
+ manifestPath,
1724
+ ts2
1725
+ );
1726
+ }
1727
+ }
1728
+ return listSkills(db);
1729
+ }
1730
+ function listSkills(db) {
1731
+ const rows = db.prepare("SELECT * FROM skills ORDER BY source ASC, name ASC").all();
1732
+ return rows.map(rowToSkill);
1733
+ }
1734
+ function getSkill(db, id) {
1735
+ const row = db.prepare("SELECT * FROM skills WHERE id = ?").get(id);
1736
+ return row ? rowToSkill(row) : null;
1737
+ }
1738
+ function readSkillManifest(skill) {
1739
+ if (!existsSync5(skill.manifestPath)) return "";
1740
+ return readFileSync4(skill.manifestPath, "utf8");
1741
+ }
1742
+ function recordSkillInstall(db, skillId, projectId) {
1743
+ const skill = getSkill(db, skillId);
1744
+ if (!skill) return null;
1745
+ const projects = /* @__PURE__ */ new Set([...skill.installedInProjects, projectId]);
1746
+ db.prepare(
1747
+ "UPDATE skills SET installed_in_projects_json = ?, updated_at = ? WHERE id = ?"
1748
+ ).run(JSON.stringify([...projects]), now(), skillId);
1749
+ return getSkill(db, skillId);
1750
+ }
1751
+
1752
+ // ../server/src/state/galaxy.ts
1753
+ import { nanoid as nanoid3 } from "nanoid";
1754
+ function exportManifest(db, opts = {}) {
1755
+ const advisors2 = listAdvisors(db);
1756
+ const skills2 = listSkills(db);
1757
+ const projects = listProjects(db);
1758
+ const manifestAdvisors = advisors2.filter((a) => a.enabled).map((a) => ({
1759
+ role: a.id,
1760
+ pinned: a.pinned,
1761
+ model: a.defaultModel
1762
+ })).sort((a, b) => a.role.localeCompare(b.role));
1763
+ const manifestSkills = skills2.map((s) => ({ id: s.id, source: s.source })).sort((a, b) => a.id.localeCompare(b.id));
1764
+ const manifestProjects = projects.map((p) => ({ name: p.name, cwd: p.cwd })).sort((a, b) => a.name.localeCompare(b.name));
1765
+ return {
1766
+ version: 1,
1767
+ name: opts.name ?? "My Galaxy",
1768
+ author: opts.author,
1769
+ description: opts.description,
1770
+ advisors: manifestAdvisors,
1771
+ skills: manifestSkills,
1772
+ projects: manifestProjects
1773
+ };
1774
+ }
1775
+ function importManifest(db, manifest, sourceUrl) {
1776
+ if (manifest.version !== 1) {
1777
+ throw new Error(`Unsupported galaxy manifest version: ${manifest.version}`);
1778
+ }
1779
+ const enabledRoles = new Set(manifest.advisors.map((a) => a.role));
1780
+ const allAdvisors = listAdvisors(db);
1781
+ let enabled = 0;
1782
+ let disabled = 0;
1783
+ for (const a of allAdvisors) {
1784
+ const shouldEnable = enabledRoles.has(a.id);
1785
+ if (shouldEnable && !a.enabled) {
1786
+ setAdvisorEnabled(db, a.id, true);
1787
+ enabled++;
1788
+ } else if (!shouldEnable && a.enabled) {
1789
+ const isCore = ["compass", "forge", "lumen", "argus", "sentinel"].includes(
1790
+ a.id
1791
+ );
1792
+ if (!isCore) {
1793
+ setAdvisorEnabled(db, a.id, false);
1794
+ disabled++;
1795
+ }
1796
+ }
1797
+ }
1798
+ db.prepare(
1799
+ `INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
1800
+ VALUES (?, ?, ?, ?)`
1801
+ ).run(nanoid3(), sourceUrl ?? null, JSON.stringify(manifest), now());
1802
+ return {
1803
+ advisorsEnabled: enabled,
1804
+ advisorsDisabled: disabled,
1805
+ projectsHinted: manifest.projects.length
1806
+ };
1807
+ }
1808
+ function listImportHistory(db) {
1809
+ const rows = db.prepare(
1810
+ "SELECT id, source_url, manifest_json, imported_at FROM galaxy_imports ORDER BY imported_at DESC LIMIT 20"
1811
+ ).all();
1812
+ return rows.map((r) => {
1813
+ let name = "(unknown)";
1814
+ try {
1815
+ const m = JSON.parse(r.manifest_json);
1816
+ name = m.name;
1817
+ } catch {
1818
+ }
1819
+ return {
1820
+ id: r.id,
1821
+ sourceUrl: r.source_url ?? void 0,
1822
+ importedAt: r.imported_at,
1823
+ manifestName: name
1824
+ };
1825
+ });
1826
+ }
1827
+ function rowToVersion(row) {
1828
+ let manifest;
1829
+ try {
1830
+ manifest = JSON.parse(row.manifest_json);
1831
+ } catch {
1832
+ manifest = {
1833
+ version: 1,
1834
+ name: row.name,
1835
+ advisors: [],
1836
+ skills: [],
1837
+ projects: []
1838
+ };
1839
+ }
1840
+ return {
1841
+ id: row.id,
1842
+ ts: row.ts,
1843
+ ordinal: row.ordinal,
1844
+ name: row.name,
1845
+ author: row.author ?? void 0,
1846
+ description: row.description ?? void 0,
1847
+ manifest
1848
+ };
1849
+ }
1850
+ function snapshotExport(db, manifest) {
1851
+ const last = db.prepare(
1852
+ "SELECT manifest_json, ordinal FROM galaxy_versions ORDER BY ordinal DESC LIMIT 1"
1853
+ ).get();
1854
+ if (last) {
1855
+ const lastJson = last.manifest_json;
1856
+ const newJson = JSON.stringify(manifest);
1857
+ if (lastJson === newJson) {
1858
+ const existing = db.prepare("SELECT * FROM galaxy_versions WHERE ordinal = ? LIMIT 1").get(last.ordinal);
1859
+ return rowToVersion(existing);
1860
+ }
1861
+ }
1862
+ const id = nanoid3();
1863
+ const ts2 = now();
1864
+ const ordinal = (last?.ordinal ?? 0) + 1;
1865
+ db.prepare(
1866
+ `INSERT INTO galaxy_versions (id, ts, ordinal, name, author, description, manifest_json)
1867
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
1868
+ ).run(
1869
+ id,
1870
+ ts2,
1871
+ ordinal,
1872
+ manifest.name,
1873
+ manifest.author ?? null,
1874
+ manifest.description ?? null,
1875
+ JSON.stringify(manifest)
1876
+ );
1877
+ return {
1878
+ id,
1879
+ ts: ts2,
1880
+ ordinal,
1881
+ name: manifest.name,
1882
+ author: manifest.author,
1883
+ description: manifest.description,
1884
+ manifest
1885
+ };
1886
+ }
1887
+ function listVersions(db, limit = 50) {
1888
+ const rows = db.prepare(
1889
+ "SELECT * FROM galaxy_versions ORDER BY ordinal DESC LIMIT ?"
1890
+ ).all(Math.min(limit, 500));
1891
+ return rows.map(rowToVersion);
1892
+ }
1893
+ function getVersion(db, id) {
1894
+ const row = db.prepare("SELECT * FROM galaxy_versions WHERE id = ? LIMIT 1").get(id);
1895
+ return row ? rowToVersion(row) : null;
1896
+ }
1897
+
1898
+ // ../shared/src/galaxyDiff.ts
1899
+ function diffManifests(a, b) {
1900
+ const advisorsA = new Map(a.advisors.map((x) => [x.role, x]));
1901
+ const advisorsB = new Map(b.advisors.map((x) => [x.role, x]));
1902
+ const advisorAdded = [...advisorsB.keys()].filter(
1903
+ (k) => !advisorsA.has(k)
1904
+ );
1905
+ const advisorRemoved = [...advisorsA.keys()].filter(
1906
+ (k) => !advisorsB.has(k)
1907
+ );
1908
+ const advisorPinChanged = [...advisorsB.keys()].filter((k) => advisorsA.has(k)).map((k) => ({
1909
+ role: k,
1910
+ from: advisorsA.get(k).pinned,
1911
+ to: advisorsB.get(k).pinned
1912
+ })).filter((c) => c.from !== c.to);
1913
+ const skillsA = new Set(a.skills.map((s) => s.id));
1914
+ const skillsB = new Set(b.skills.map((s) => s.id));
1915
+ const skillAdded = [...skillsB].filter((id) => !skillsA.has(id));
1916
+ const skillRemoved = [...skillsA].filter((id) => !skillsB.has(id));
1917
+ const projectsA = new Set(a.projects.map((p) => p.name));
1918
+ const projectsB = new Set(b.projects.map((p) => p.name));
1919
+ const projectAdded = [...projectsB].filter((n) => !projectsA.has(n));
1920
+ const projectRemoved = [...projectsA].filter((n) => !projectsB.has(n));
1921
+ return {
1922
+ advisors: {
1923
+ added: advisorAdded.sort(),
1924
+ removed: advisorRemoved.sort(),
1925
+ pinChanged: advisorPinChanged.sort(
1926
+ (x, y) => x.role.localeCompare(y.role)
1927
+ )
1928
+ },
1929
+ skills: {
1930
+ added: skillAdded.sort(),
1931
+ removed: skillRemoved.sort()
1932
+ },
1933
+ projects: {
1934
+ added: projectAdded.sort(),
1935
+ removed: projectRemoved.sort()
1936
+ }
1937
+ };
1938
+ }
1939
+
1940
+ // ../server/src/cloud.ts
1941
+ var RegistryClient = class {
1942
+ constructor(baseUrl = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
1943
+ this.baseUrl = baseUrl;
1944
+ this.apiKey = apiKey;
1945
+ }
1946
+ baseUrl;
1947
+ apiKey;
1948
+ isConfigured() {
1949
+ return Boolean(this.baseUrl);
1950
+ }
1951
+ async publish(slug, manifest) {
1952
+ if (!this.isConfigured()) {
1953
+ throw new Error(
1954
+ "Registry URL not configured. Set SOLIX_REGISTRY_URL."
1955
+ );
1956
+ }
1957
+ if (!this.apiKey) {
1958
+ throw new Error(
1959
+ "Registry API key required to publish. Set SOLIX_REGISTRY_KEY."
1960
+ );
1961
+ }
1962
+ const url = `${this.baseUrl.replace(/\/$/, "")}/v1/galaxies/${encodeURIComponent(slug)}`;
1963
+ const res = await fetch(url, {
1964
+ method: "PUT",
1965
+ headers: {
1966
+ "Content-Type": "application/json",
1967
+ "X-API-Key": this.apiKey
1968
+ },
1969
+ body: JSON.stringify(manifest),
1970
+ signal: AbortSignal.timeout(8e3)
1971
+ });
1972
+ if (!res.ok) {
1973
+ const text = await res.text().catch(() => "");
1974
+ throw new Error(`Publish failed: HTTP ${res.status} ${text}`);
1975
+ }
1976
+ return await res.json();
1977
+ }
1978
+ async pull(slug) {
1979
+ if (!this.isConfigured()) {
1980
+ throw new Error(
1981
+ "Registry URL not configured. Set SOLIX_REGISTRY_URL."
1982
+ );
1983
+ }
1984
+ const url = `${this.baseUrl.replace(/\/$/, "")}/v1/galaxies/${encodeURIComponent(slug)}`;
1985
+ const res = await fetch(url, {
1986
+ signal: AbortSignal.timeout(5e3)
1987
+ });
1988
+ if (!res.ok) {
1989
+ const text = await res.text().catch(() => "");
1990
+ throw new Error(`Pull failed: HTTP ${res.status} ${text}`);
1991
+ }
1992
+ const data = await res.json();
1993
+ if ("manifest" in data && data.manifest) {
1994
+ return data.manifest;
1995
+ }
1996
+ return data;
1997
+ }
1998
+ async listSlugs() {
1999
+ if (!this.isConfigured()) return [];
2000
+ const url = `${this.baseUrl.replace(/\/$/, "")}/v1/galaxies`;
2001
+ try {
2002
+ const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
2003
+ if (!res.ok) return [];
2004
+ const data = await res.json();
2005
+ if (Array.isArray(data)) return data;
2006
+ return data.slugs ?? [];
2007
+ } catch {
2008
+ return [];
2009
+ }
2010
+ }
2011
+ };
2012
+
2013
+ // ../server/src/http.ts
2014
+ function createHttpApp(opts) {
2015
+ const app = new Hono();
2016
+ app.use("*", cors());
2017
+ const registry = new RegistryClient();
2018
+ app.get(
2019
+ "/api/health",
2020
+ (c) => c.json({
2021
+ ok: true,
2022
+ service: "solix",
2023
+ version: "1.0.0",
2024
+ ts: Date.now()
2025
+ })
2026
+ );
2027
+ app.post("/events", async (c) => {
2028
+ let body = null;
2029
+ try {
2030
+ body = await c.req.json();
2031
+ } catch (err) {
2032
+ console.warn("[events] bad JSON", err);
2033
+ return c.json({ ok: true });
2034
+ }
2035
+ if (body && body.event) {
2036
+ opts.router.handleHookEvent(body);
2037
+ }
2038
+ return c.json({ ok: true });
2039
+ });
2040
+ app.get("/api/projects", (c) => c.json(listProjects(opts.db)));
2041
+ app.get("/api/projects/:id/sessions", (c) => {
2042
+ const id = c.req.param("id");
2043
+ return c.json(listSessionsForProject(opts.db, id));
2044
+ });
2045
+ app.get("/api/sessions/:id", (c) => {
2046
+ const id = c.req.param("id");
2047
+ const s = getSession(opts.db, id);
2048
+ if (!s) return c.json({ error: "not found" }, 404);
2049
+ return c.json(s);
2050
+ });
2051
+ app.post("/api/sessions/:id/permission", async (c) => {
2052
+ const body = await c.req.json().catch(() => ({}));
2053
+ if (!body.requestId)
2054
+ return c.json({ error: "requestId required" }, 400);
2055
+ const ok = opts.router.resolvePermission(
2056
+ body.requestId,
2057
+ Boolean(body.approved)
2058
+ );
2059
+ return c.json({ ok });
2060
+ });
2061
+ app.post("/api/sessions/:id/terminate", (c) => {
2062
+ const id = c.req.param("id");
2063
+ const s = setSessionStatus(opts.db, id, "terminated");
2064
+ return c.json({ ok: Boolean(s) });
2065
+ });
2066
+ app.post("/api/sessions/:id/context", async (c) => {
2067
+ const id = c.req.param("id");
2068
+ const body = await c.req.json().catch(() => ({}));
2069
+ if (typeof body.pct !== "number") {
2070
+ return c.json({ error: "pct (number) required" }, 400);
2071
+ }
2072
+ opts.router.setContextUsage(id, body.pct);
2073
+ return c.json({ ok: true });
2074
+ });
2075
+ app.get("/api/missions", (c) => {
2076
+ const sessionId = c.req.query("sessionId");
2077
+ const projectId = c.req.query("projectId");
2078
+ const limitStr = c.req.query("limit");
2079
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
2080
+ return c.json(
2081
+ listMissions(opts.db, {
2082
+ sessionId,
2083
+ projectId,
2084
+ limit
2085
+ })
2086
+ );
2087
+ });
2088
+ app.get("/api/timeline", (c) => {
2089
+ const sinceMsStr = c.req.query("sinceMs");
2090
+ const untilMsStr = c.req.query("untilMs");
2091
+ const limitStr = c.req.query("limit");
2092
+ const sinceMs = sinceMsStr ? parseInt(sinceMsStr, 10) : Date.now() - 30 * 60 * 1e3;
2093
+ const untilMs = untilMsStr ? parseInt(untilMsStr, 10) : Date.now();
2094
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
2095
+ return c.json(loadTimeline(opts.db, { sinceMs, untilMs, limit }));
2096
+ });
2097
+ app.get("/api/audit", (c) => {
2098
+ const sessionId = c.req.query("sessionId") ?? void 0;
2099
+ const kindStr = c.req.query("kind");
2100
+ const sinceStr = c.req.query("since");
2101
+ const untilStr = c.req.query("until");
2102
+ const limitStr = c.req.query("limit");
2103
+ return c.json(
2104
+ listAudit(opts.db, {
2105
+ sessionId,
2106
+ kind: kindStr ? kindStr : void 0,
2107
+ since: sinceStr ? parseInt(sinceStr, 10) : void 0,
2108
+ until: untilStr ? parseInt(untilStr, 10) : void 0,
2109
+ limit: limitStr ? parseInt(limitStr, 10) : void 0
2110
+ })
2111
+ );
2112
+ });
2113
+ app.get("/api/advisors", (c) => c.json(listAdvisors(opts.db)));
2114
+ app.get("/api/advisors/:id", (c) => {
2115
+ const a = getAdvisor(opts.db, c.req.param("id"));
2116
+ if (!a) return c.json({ error: "not found" }, 404);
2117
+ return c.json({ ...a, agentMd: readAdvisorAgentMd(a) });
2118
+ });
2119
+ app.post("/api/advisors/:id/enable", (c) => {
2120
+ const a = setAdvisorEnabled(opts.db, c.req.param("id"), true);
2121
+ return c.json({ ok: Boolean(a), advisor: a });
2122
+ });
2123
+ app.post("/api/advisors/:id/disable", (c) => {
2124
+ const a = setAdvisorEnabled(opts.db, c.req.param("id"), false);
2125
+ return c.json({ ok: Boolean(a), advisor: a });
2126
+ });
2127
+ app.post("/api/advisors/:id/pin", (c) => {
2128
+ const ok = opts.router.pinAdvisor(c.req.param("id"));
2129
+ return c.json({ ok });
2130
+ });
2131
+ app.post("/api/advisors/:id/unpin", (c) => {
2132
+ const ok = opts.router.unpinAdvisor(c.req.param("id"));
2133
+ return c.json({ ok });
2134
+ });
2135
+ app.get("/api/advisors/:id/preview", (c) => {
2136
+ const id = c.req.param("id");
2137
+ const targetSessionId = c.req.query("targetSessionId") ?? void 0;
2138
+ const prompt = c.req.query("prompt") ?? void 0;
2139
+ const env = buildContextEnvelope(opts.db, {
2140
+ advisorId: id,
2141
+ targetSessionId,
2142
+ userPrompt: prompt
2143
+ });
2144
+ if (!env) return c.json({ error: "advisor not found" }, 404);
2145
+ return c.json({
2146
+ advisorId: env.advisorId,
2147
+ role: env.advisorRole,
2148
+ prompt: env.prompt,
2149
+ recentMissionsCount: env.recentMissions.length,
2150
+ targetSessionId: env.targetSession?.id ?? null,
2151
+ contextUsagePct: env.targetSession?.contextUsagePct ?? null
2152
+ });
2153
+ });
2154
+ app.post("/api/advisors/:id/invoke", async (c) => {
2155
+ const body = await c.req.json().catch(() => ({}));
2156
+ const result = opts.router.invokeAdvisor(
2157
+ c.req.param("id"),
2158
+ body.targetSessionId,
2159
+ body.prompt
2160
+ );
2161
+ return c.json(result);
2162
+ });
2163
+ app.get("/api/skills", (c) => c.json(listSkills(opts.db)));
2164
+ app.get("/api/skills/:id", (c) => {
2165
+ const id = decodeURIComponent(c.req.param("id"));
2166
+ const s = getSkill(opts.db, id);
2167
+ if (!s) return c.json({ error: "not found" }, 404);
2168
+ return c.json({ ...s, manifest: readSkillManifest(s) });
2169
+ });
2170
+ app.post("/api/skills/:id/install", async (c) => {
2171
+ const id = decodeURIComponent(c.req.param("id"));
2172
+ const body = await c.req.json().catch(() => ({}));
2173
+ if (!body.projectId)
2174
+ return c.json({ error: "projectId required" }, 400);
2175
+ const s = recordSkillInstall(opts.db, id, body.projectId);
2176
+ return c.json({ ok: Boolean(s), skill: s });
2177
+ });
2178
+ app.get("/api/galaxy/export", (c) => {
2179
+ const name = c.req.query("name") ?? void 0;
2180
+ const author = c.req.query("author") ?? void 0;
2181
+ const description = c.req.query("description") ?? void 0;
2182
+ const preview = c.req.query("preview") === "1";
2183
+ const manifest = exportManifest(opts.db, {
2184
+ name,
2185
+ author,
2186
+ description
2187
+ });
2188
+ if (!preview) snapshotExport(opts.db, manifest);
2189
+ return c.json(manifest);
2190
+ });
2191
+ app.get("/api/galaxy/versions", (c) => {
2192
+ const limitStr = c.req.query("limit");
2193
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
2194
+ return c.json(listVersions(opts.db, limit));
2195
+ });
2196
+ app.get("/api/galaxy/versions/:id", (c) => {
2197
+ const v = getVersion(opts.db, c.req.param("id"));
2198
+ if (!v) return c.json({ error: "not found" }, 404);
2199
+ return c.json(v);
2200
+ });
2201
+ app.get("/api/galaxy/diff", (c) => {
2202
+ const fromId = c.req.query("from");
2203
+ const toId = c.req.query("to");
2204
+ if (!fromId || !toId) {
2205
+ return c.json({ error: "from and to query params required" }, 400);
2206
+ }
2207
+ const from = getVersion(opts.db, fromId);
2208
+ const to = getVersion(opts.db, toId);
2209
+ if (!from || !to) return c.json({ error: "version not found" }, 404);
2210
+ return c.json({
2211
+ from: { id: from.id, ordinal: from.ordinal, ts: from.ts },
2212
+ to: { id: to.id, ordinal: to.ordinal, ts: to.ts },
2213
+ diff: diffManifests(from.manifest, to.manifest)
2214
+ });
2215
+ });
2216
+ app.post("/api/galaxy/import", async (c) => {
2217
+ let body;
2218
+ try {
2219
+ body = await c.req.json();
2220
+ } catch {
2221
+ return c.json({ error: "invalid JSON" }, 400);
2222
+ }
2223
+ let manifest;
2224
+ let sourceUrl;
2225
+ if ("url" in body && typeof body.url === "string") {
2226
+ sourceUrl = body.url;
2227
+ try {
2228
+ const res = await fetch(body.url, {
2229
+ signal: AbortSignal.timeout(5e3)
2230
+ });
2231
+ if (!res.ok) {
2232
+ return c.json(
2233
+ { error: `fetch failed: HTTP ${res.status}` },
2234
+ 502
2235
+ );
2236
+ }
2237
+ manifest = await res.json();
2238
+ } catch (err) {
2239
+ return c.json({ error: `fetch failed: ${String(err)}` }, 502);
2240
+ }
2241
+ } else {
2242
+ manifest = body;
2243
+ }
2244
+ if (typeof manifest.version !== "number") {
2245
+ return c.json({ error: "manifest missing version" }, 400);
2246
+ }
2247
+ try {
2248
+ const result = importManifest(opts.db, manifest, sourceUrl);
2249
+ opts.router.broadcastGalaxyImported(manifest);
2250
+ return c.json({ ok: true, ...result });
2251
+ } catch (err) {
2252
+ return c.json({ error: String(err) }, 400);
2253
+ }
2254
+ });
2255
+ app.get("/api/galaxy/imports", (c) => c.json(listImportHistory(opts.db)));
2256
+ let preflightCache = null;
2257
+ app.get("/api/system/preflight", (c) => {
2258
+ if (preflightCache) return c.json(preflightCache);
2259
+ try {
2260
+ const res = spawnSync("claude", ["--version"], {
2261
+ timeout: 2e3,
2262
+ encoding: "utf8"
2263
+ });
2264
+ if (res.status === 0) {
2265
+ preflightCache = {
2266
+ claudeAvailable: true,
2267
+ version: (res.stdout ?? "").trim() || void 0
2268
+ };
2269
+ } else {
2270
+ preflightCache = { claudeAvailable: false };
2271
+ }
2272
+ } catch {
2273
+ preflightCache = { claudeAvailable: false };
2274
+ }
2275
+ return c.json(preflightCache);
2276
+ });
2277
+ const webDist = findWebDist();
2278
+ if (webDist) {
2279
+ app.get("*", (c) => {
2280
+ const url = new URL(c.req.url);
2281
+ if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/events") || url.pathname.startsWith("/ws")) {
2282
+ return c.notFound();
2283
+ }
2284
+ const safe = url.pathname.replace(/\.\.+/g, ".");
2285
+ const candidate = join8(webDist, safe === "/" ? "index.html" : safe);
2286
+ let filePath = candidate;
2287
+ try {
2288
+ if (!existsSync6(filePath) || statSync4(filePath).isDirectory()) {
2289
+ filePath = join8(webDist, "index.html");
2290
+ }
2291
+ } catch {
2292
+ filePath = join8(webDist, "index.html");
2293
+ }
2294
+ if (!existsSync6(filePath)) return c.notFound();
2295
+ const data = readFileSync5(filePath);
2296
+ return new Response(data, {
2297
+ headers: { "Content-Type": mimeFor(filePath) }
2298
+ });
2299
+ });
2300
+ }
2301
+ app.get(
2302
+ "/api/galaxy/registry/status",
2303
+ (c) => c.json({
2304
+ configured: registry.isConfigured(),
2305
+ url: process.env.SOLIX_REGISTRY_URL ?? null
2306
+ })
2307
+ );
2308
+ app.get("/api/galaxy/registry", async (c) => {
2309
+ const slugs = await registry.listSlugs();
2310
+ return c.json({ slugs });
2311
+ });
2312
+ app.get("/api/galaxy/registry/:slug", async (c) => {
2313
+ const slug = c.req.param("slug");
2314
+ try {
2315
+ const manifest = await registry.pull(slug);
2316
+ return c.json(manifest);
2317
+ } catch (err) {
2318
+ return c.json({ error: String(err) }, 502);
2319
+ }
2320
+ });
2321
+ app.post("/api/galaxy/registry/:slug/install", async (c) => {
2322
+ const slug = c.req.param("slug");
2323
+ try {
2324
+ const manifest = await registry.pull(slug);
2325
+ const result = importManifest(opts.db, manifest, `registry:${slug}`);
2326
+ opts.router.broadcastGalaxyImported(manifest);
2327
+ return c.json({ ok: true, ...result });
2328
+ } catch (err) {
2329
+ return c.json({ error: String(err) }, 502);
2330
+ }
2331
+ });
2332
+ app.post("/api/galaxy/publish", async (c) => {
2333
+ const body = await c.req.json().catch(() => ({}));
2334
+ if (!body.slug) {
2335
+ return c.json({ error: "slug required" }, 400);
2336
+ }
2337
+ const manifest = exportManifest(opts.db, {
2338
+ name: body.name,
2339
+ author: body.author,
2340
+ description: body.description
2341
+ });
2342
+ try {
2343
+ const published = await registry.publish(body.slug, manifest);
2344
+ return c.json({ ok: true, ...published });
2345
+ } catch (err) {
2346
+ return c.json({ error: String(err) }, 502);
2347
+ }
2348
+ });
2349
+ return app;
2350
+ }
2351
+ function findWebDist() {
2352
+ if (process.env.SOLIX_WEB_DIST) {
2353
+ return existsSync6(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
2354
+ }
2355
+ const here = dirname4(fileURLToPath4(import.meta.url));
2356
+ const candidates = [
2357
+ // Bundled npm package: web/ ships next to the bundled JS file.
2358
+ resolve3(here, "web"),
2359
+ // Monorepo: server's compiled output is at packages/server/dist.
2360
+ resolve3(here, "..", "..", "web", "dist"),
2361
+ resolve3(here, "..", "..", "..", "web", "dist"),
2362
+ resolve3(here, "..", "..", "..", "packages", "web", "dist"),
2363
+ resolve3(process.cwd(), "packages", "web", "dist")
2364
+ ];
2365
+ for (const c of candidates) {
2366
+ if (existsSync6(join8(c, "index.html"))) return c;
2367
+ }
2368
+ return null;
2369
+ }
2370
+ var MIME = {
2371
+ ".html": "text/html; charset=utf-8",
2372
+ ".js": "text/javascript; charset=utf-8",
2373
+ ".mjs": "text/javascript; charset=utf-8",
2374
+ ".css": "text/css; charset=utf-8",
2375
+ ".json": "application/json; charset=utf-8",
2376
+ ".svg": "image/svg+xml",
2377
+ ".png": "image/png",
2378
+ ".jpg": "image/jpeg",
2379
+ ".jpeg": "image/jpeg",
2380
+ ".gif": "image/gif",
2381
+ ".ico": "image/x-icon",
2382
+ ".woff": "font/woff",
2383
+ ".woff2": "font/woff2",
2384
+ ".map": "application/json"
2385
+ };
2386
+ function mimeFor(filePath) {
2387
+ return MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream";
2388
+ }
2389
+
2390
+ // ../server/src/launcher.ts
2391
+ import { spawn } from "child_process";
2392
+ import { existsSync as existsSync7 } from "fs";
2393
+ import { nanoid as nanoid4 } from "nanoid";
2394
+ var FAKE_CLAUDE = process.env.SOLIX_FAKE_CLAUDE === "1";
2395
+ var Launcher = class {
2396
+ constructor(db, broadcaster) {
2397
+ this.db = db;
2398
+ this.broadcaster = broadcaster;
2399
+ }
2400
+ db;
2401
+ broadcaster;
2402
+ byPid = /* @__PURE__ */ new Map();
2403
+ byAdvisor = /* @__PURE__ */ new Map();
2404
+ // Sessions that were spawned by the UI's "+ Task" button. Keyed by the
2405
+ // sessionId we synthesize at launch (not Claude's session_id, which arrives
2406
+ // via the SessionStart hook later).
2407
+ internalTasks = /* @__PURE__ */ new Map();
2408
+ /** Returns the advisor role bound to a given pid, if any. */
2409
+ advisorRoleForPid(pid) {
2410
+ return this.byPid.get(pid)?.advisorId;
2411
+ }
2412
+ /**
2413
+ * Pin (spawn) an advisor as an always-on Claude Code session.
2414
+ * Returns true if the spawn succeeded (or a synthetic session was created).
2415
+ */
2416
+ pin(advisorId, cwd) {
2417
+ if (this.byAdvisor.has(advisorId)) return true;
2418
+ const advisor = getAdvisor(this.db, advisorId);
2419
+ if (!advisor) return false;
2420
+ if (FAKE_CLAUDE) {
2421
+ return this.pinSynthetic(advisor.id, advisor.codename, cwd);
2422
+ }
2423
+ try {
2424
+ const child = spawn(
2425
+ "claude",
2426
+ ["--agent", advisor.id, "--no-tty"],
2427
+ {
2428
+ cwd,
2429
+ stdio: ["pipe", "pipe", "pipe"],
2430
+ detached: false
2431
+ }
2432
+ );
2433
+ const pid = child.pid;
2434
+ if (!pid) {
2435
+ this.broadcaster.broadcast({
2436
+ type: "toast",
2437
+ level: "error",
2438
+ message: `Could not spawn claude for ${advisor.codename}`
2439
+ });
2440
+ return false;
2441
+ }
2442
+ const record = { advisorId, pid, child };
2443
+ this.byPid.set(pid, record);
2444
+ this.byAdvisor.set(advisorId, record);
2445
+ setAdvisorPinned(this.db, advisorId, true);
2446
+ child.on("exit", () => {
2447
+ this.cleanup(advisorId, pid);
2448
+ });
2449
+ child.on("error", (err) => {
2450
+ console.warn(`[launcher] ${advisorId} error:`, err.message);
2451
+ this.broadcaster.broadcast({
2452
+ type: "toast",
2453
+ level: "warn",
2454
+ message: `${advisor.codename} exited (${err.message})`
2455
+ });
2456
+ this.cleanup(advisorId, pid);
2457
+ });
2458
+ return true;
2459
+ } catch (err) {
2460
+ console.warn(`[launcher] spawn failed for ${advisorId}:`, err);
2461
+ this.broadcaster.broadcast({
2462
+ type: "toast",
2463
+ level: "warn",
2464
+ message: `claude binary not found \u2014 pinned ${advisor.codename} as synthetic`
2465
+ });
2466
+ return this.pinSynthetic(advisor.id, advisor.codename, cwd);
2467
+ }
2468
+ }
2469
+ pinSynthetic(advisorId, codename, cwd) {
2470
+ const project = ensureProject(this.db, cwd);
2471
+ const sessionId = `advisor-${advisorId}-${nanoid4(6)}`;
2472
+ const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
2473
+ const session = upsertSession(this.db, {
2474
+ id: sessionId,
2475
+ pid: fakePid,
2476
+ projectId: project.id,
2477
+ cwd,
2478
+ origin: "internal",
2479
+ kind: "advisor",
2480
+ advisorRole: advisorId
2481
+ });
2482
+ setSessionStatus(this.db, sessionId, "idle");
2483
+ setAdvisorPinned(this.db, advisorId, true, sessionId);
2484
+ const record = {
2485
+ advisorId,
2486
+ pid: fakePid,
2487
+ syntheticSessionId: sessionId
2488
+ };
2489
+ this.byPid.set(fakePid, record);
2490
+ this.byAdvisor.set(advisorId, record);
2491
+ this.broadcaster.broadcast({ type: "session_upsert", session });
2492
+ this.broadcaster.broadcast({
2493
+ type: "toast",
2494
+ level: "info",
2495
+ message: `${codename} pinned (always-on)`
2496
+ });
2497
+ return true;
2498
+ }
2499
+ unpin(advisorId) {
2500
+ const record = this.byAdvisor.get(advisorId);
2501
+ if (!record) {
2502
+ setAdvisorPinned(this.db, advisorId, false);
2503
+ return true;
2504
+ }
2505
+ if (record.child) {
2506
+ try {
2507
+ record.child.kill("SIGTERM");
2508
+ } catch (err) {
2509
+ console.warn(`[launcher] kill failed for ${advisorId}:`, err);
2510
+ }
2511
+ }
2512
+ this.cleanup(advisorId, record.pid);
2513
+ return true;
2514
+ }
2515
+ cleanup(advisorId, pid) {
2516
+ const record = this.byAdvisor.get(advisorId);
2517
+ this.byAdvisor.delete(advisorId);
2518
+ this.byPid.delete(pid);
2519
+ setAdvisorPinned(this.db, advisorId, false);
2520
+ if (record?.syntheticSessionId) {
2521
+ setSessionStatus(this.db, record.syntheticSessionId, "terminated");
2522
+ this.broadcaster.broadcast({
2523
+ type: "session_remove",
2524
+ sessionId: record.syntheticSessionId
2525
+ });
2526
+ }
2527
+ const advisor = getAdvisor(this.db, advisorId);
2528
+ if (advisor) {
2529
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
2530
+ }
2531
+ }
2532
+ shutdownAll() {
2533
+ for (const advisorId of [...this.byAdvisor.keys()]) {
2534
+ this.unpin(advisorId);
2535
+ }
2536
+ }
2537
+ /**
2538
+ * Spawn a fresh `claude --print` task in the given cwd. Hooks fire as
2539
+ * usual so the planet appears and animates; when the process exits, the
2540
+ * captured stdout becomes a final assistant message in the chat.
2541
+ *
2542
+ * In FAKE_CLAUDE dev mode the task is synthesized so the visuals work
2543
+ * without a real claude binary on PATH.
2544
+ */
2545
+ launch(opts) {
2546
+ if (!opts.initialPrompt.trim()) return { ok: false };
2547
+ if (FAKE_CLAUDE) {
2548
+ return this.launchSynthetic(opts);
2549
+ }
2550
+ if (!existsSync7(opts.cwd)) {
2551
+ this.broadcaster.broadcast({
2552
+ type: "toast",
2553
+ level: "error",
2554
+ message: `Launch failed: cwd does not exist (${opts.cwd})`
2555
+ });
2556
+ return { ok: false };
2557
+ }
2558
+ const args = ["--print"];
2559
+ if (opts.model) args.push("--model", String(opts.model));
2560
+ args.push(opts.initialPrompt);
2561
+ const sessionId = `task-${nanoid4(8)}`;
2562
+ return this.spawnPrint({
2563
+ sessionId,
2564
+ cwd: opts.cwd,
2565
+ args,
2566
+ isFollowUp: false
2567
+ });
2568
+ }
2569
+ sendPromptToInternal(sessionId, text) {
2570
+ if (!text.trim()) return false;
2571
+ const session = this.db.prepare(
2572
+ "SELECT cwd, origin FROM sessions WHERE id = ? LIMIT 1"
2573
+ ).get(sessionId);
2574
+ if (!session?.cwd) {
2575
+ this.broadcaster.broadcast({
2576
+ type: "toast",
2577
+ level: "warn",
2578
+ message: `Cannot send prompt: session not found`
2579
+ });
2580
+ return false;
2581
+ }
2582
+ if (session.origin !== "internal") {
2583
+ this.broadcaster.broadcast({
2584
+ type: "toast",
2585
+ level: "warn",
2586
+ message: `Cannot send prompt: external session \u2014 type in your terminal`
2587
+ });
2588
+ return false;
2589
+ }
2590
+ if (FAKE_CLAUDE) {
2591
+ this.broadcaster.broadcast({
2592
+ type: "chat_delta",
2593
+ sessionId,
2594
+ delta: {
2595
+ messageId: `fake-a-${Date.now()}`,
2596
+ role: "assistant",
2597
+ content: `(SOLIX_FAKE_CLAUDE=1) Pretending to run: ${text.slice(0, 200)}`,
2598
+ ts: Date.now(),
2599
+ done: true
2600
+ }
2601
+ });
2602
+ return true;
2603
+ }
2604
+ const args = ["--print", "--continue", text];
2605
+ return this.spawnPrint({
2606
+ sessionId,
2607
+ cwd: session.cwd,
2608
+ args,
2609
+ isFollowUp: true
2610
+ }).ok;
2611
+ }
2612
+ spawnPrint(opts) {
2613
+ let child;
2614
+ try {
2615
+ child = spawn("claude", opts.args, {
2616
+ cwd: opts.cwd,
2617
+ stdio: ["ignore", "pipe", "pipe"],
2618
+ detached: false
2619
+ });
2620
+ } catch (err) {
2621
+ this.broadcaster.broadcast({
2622
+ type: "toast",
2623
+ level: "error",
2624
+ message: `claude binary not found \u2014 is Claude Code installed?`
2625
+ });
2626
+ console.warn("[launcher] spawn failed", err);
2627
+ return { ok: false };
2628
+ }
2629
+ const pid = child.pid ?? 0;
2630
+ if (!opts.isFollowUp) {
2631
+ this.internalTasks.set(opts.sessionId, { cwd: opts.cwd });
2632
+ }
2633
+ let stdout = "";
2634
+ let stderr = "";
2635
+ child.stdout?.setEncoding("utf8").on("data", (c) => stdout += c);
2636
+ child.stderr?.setEncoding("utf8").on("data", (c) => stderr += c);
2637
+ this.broadcaster.broadcast({
2638
+ type: "toast",
2639
+ level: "info",
2640
+ message: `Launched task in ${opts.cwd} (pid ${pid})`
2641
+ });
2642
+ child.on("exit", (code) => {
2643
+ const text = stdout.trim();
2644
+ if (text) {
2645
+ this.broadcaster.broadcast({
2646
+ type: "chat_delta",
2647
+ sessionId: opts.sessionId,
2648
+ delta: {
2649
+ messageId: `task-${opts.sessionId}-${Date.now()}`,
2650
+ role: "assistant",
2651
+ content: text,
2652
+ ts: Date.now(),
2653
+ done: true
2654
+ }
2655
+ });
2656
+ }
2657
+ if (code !== 0) {
2658
+ this.broadcaster.broadcast({
2659
+ type: "toast",
2660
+ level: "warn",
2661
+ message: `Task exited ${code}${stderr ? `: ${stderr.slice(0, 120)}` : ""}`
2662
+ });
2663
+ }
2664
+ });
2665
+ child.on("error", (err) => {
2666
+ this.broadcaster.broadcast({
2667
+ type: "toast",
2668
+ level: "error",
2669
+ message: `Task error: ${err.message}`
2670
+ });
2671
+ });
2672
+ return { ok: true, sessionId: opts.sessionId };
2673
+ }
2674
+ launchSynthetic(opts) {
2675
+ const project = ensureProject(this.db, opts.cwd);
2676
+ const sessionId = `task-${nanoid4(8)}`;
2677
+ const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
2678
+ upsertSession(this.db, {
2679
+ id: sessionId,
2680
+ pid: fakePid,
2681
+ projectId: project.id,
2682
+ cwd: opts.cwd,
2683
+ origin: "internal",
2684
+ model: opts.model ?? "sonnet"
2685
+ });
2686
+ const active = setSessionStatus(this.db, sessionId, "active");
2687
+ if (active)
2688
+ this.broadcaster.broadcast({ type: "session_upsert", session: active });
2689
+ this.broadcaster.broadcast({
2690
+ type: "chat_delta",
2691
+ sessionId,
2692
+ delta: {
2693
+ messageId: `u-${sessionId}`,
2694
+ role: "user",
2695
+ content: opts.initialPrompt,
2696
+ ts: Date.now(),
2697
+ done: true
2698
+ }
2699
+ });
2700
+ setTimeout(() => {
2701
+ this.broadcaster.broadcast({
2702
+ type: "chat_delta",
2703
+ sessionId,
2704
+ delta: {
2705
+ messageId: `a-${sessionId}`,
2706
+ role: "assistant",
2707
+ content: `(SOLIX_FAKE_CLAUDE=1) Synthetic task complete. In real mode, Solix would have spawned \`claude --print\` at ${opts.cwd}.`,
2708
+ ts: Date.now(),
2709
+ done: true
2710
+ }
2711
+ });
2712
+ const idle = setSessionStatus(this.db, sessionId, "idle");
2713
+ if (idle)
2714
+ this.broadcaster.broadcast({ type: "session_upsert", session: idle });
2715
+ }, 600);
2716
+ return { ok: true, sessionId };
2717
+ }
2718
+ };
2719
+
2720
+ // ../server/src/router.ts
2721
+ import { nanoid as nanoid6 } from "nanoid";
2722
+
2723
+ // ../server/src/state/toolcalls.ts
2724
+ import { nanoid as nanoid5 } from "nanoid";
2725
+ function recordToolCall(db, input) {
2726
+ const id = nanoid5();
2727
+ const ts2 = now();
2728
+ const status = input.status ?? "running";
2729
+ db.prepare(
2730
+ `INSERT INTO tool_calls (id, session_id, mission_id, tool, args_json, status, started_at)
2731
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
2732
+ ).run(
2733
+ id,
2734
+ input.sessionId,
2735
+ input.missionId ?? null,
2736
+ input.tool,
2737
+ JSON.stringify(input.args ?? {}),
2738
+ status,
2739
+ ts2
2740
+ );
2741
+ return {
2742
+ id,
2743
+ sessionId: input.sessionId,
2744
+ missionId: input.missionId,
2745
+ tool: input.tool,
2746
+ args: input.args ?? {},
2747
+ startedAt: ts2,
2748
+ status
2749
+ };
2750
+ }
2751
+
2752
+ // ../server/src/router.ts
2753
+ var EventRouter = class {
2754
+ constructor(db, broadcaster, launcher, transcripts) {
2755
+ this.db = db;
2756
+ this.broadcaster = broadcaster;
2757
+ this.launcher = launcher;
2758
+ this.transcripts = transcripts;
2759
+ }
2760
+ db;
2761
+ broadcaster;
2762
+ launcher;
2763
+ transcripts;
2764
+ permissions = /* @__PURE__ */ new Map();
2765
+ setLauncher(launcher) {
2766
+ this.launcher = launcher;
2767
+ }
2768
+ handleHookEvent(event) {
2769
+ try {
2770
+ switch (event.event) {
2771
+ case "session_start":
2772
+ this.onSessionStart(event);
2773
+ break;
2774
+ case "user_prompt_submit":
2775
+ this.onUserPromptSubmit(event);
2776
+ break;
2777
+ case "stop":
2778
+ this.onStop(event);
2779
+ break;
2780
+ case "subagent_stop":
2781
+ this.onSubagentStop(event);
2782
+ break;
2783
+ case "pre_tool_task":
2784
+ this.onPreToolTask(event);
2785
+ break;
2786
+ case "pre_tool_file":
2787
+ this.onPreToolFile(event);
2788
+ break;
2789
+ case "pre_tool_bash":
2790
+ this.onPreToolBash(event);
2791
+ break;
2792
+ case "post_tool":
2793
+ this.onPostTool(event);
2794
+ break;
2795
+ case "notification":
2796
+ this.onNotification(event);
2797
+ break;
2798
+ default:
2799
+ console.warn("[router] unknown event", event);
2800
+ }
2801
+ } catch (err) {
2802
+ console.error("[router] error handling event", event.event, err);
2803
+ }
2804
+ }
2805
+ extractSessionId(event) {
2806
+ const p = event.payload;
2807
+ if (typeof p.session_id === "string") return p.session_id;
2808
+ if (typeof p.sessionId === "string") return p.sessionId;
2809
+ return `pid-${event.pid}`;
2810
+ }
2811
+ extractParentSessionId(event) {
2812
+ const p = event.payload;
2813
+ if (typeof p.parent_session_id === "string") return p.parent_session_id;
2814
+ if (typeof p.parentSessionId === "string") return p.parentSessionId;
2815
+ return void 0;
2816
+ }
2817
+ extractModel(event) {
2818
+ const p = event.payload;
2819
+ const m = p.model;
2820
+ if (typeof m === "string") return m;
2821
+ return "default";
2822
+ }
2823
+ onSessionStart(event) {
2824
+ const project = ensureProject(this.db, event.cwd);
2825
+ const sessionId = this.extractSessionId(event);
2826
+ const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
2827
+ const session = upsertSession(this.db, {
2828
+ id: sessionId,
2829
+ pid: event.pid,
2830
+ projectId: project.id,
2831
+ cwd: event.cwd,
2832
+ origin: event.payload.origin === "internal" ? "internal" : advisorRole ? "internal" : "external",
2833
+ model: this.extractModel(event),
2834
+ parentSessionId: this.extractParentSessionId(event),
2835
+ kind: advisorRole ? "advisor" : "user",
2836
+ advisorRole
2837
+ });
2838
+ this.broadcaster.broadcast({ type: "session_upsert", session });
2839
+ if (!session.parentSessionId) {
2840
+ this.transcripts?.startWatching(sessionId, event.cwd);
2841
+ }
2842
+ }
2843
+ onUserPromptSubmit(event) {
2844
+ const sessionId = this.extractSessionId(event);
2845
+ const p = event.payload;
2846
+ const prompt = typeof p.prompt === "string" ? p.prompt : "untitled prompt";
2847
+ let session = getSession(this.db, sessionId);
2848
+ if (!session) {
2849
+ const project = ensureProject(this.db, event.cwd);
2850
+ session = upsertSession(this.db, {
2851
+ id: sessionId,
2852
+ pid: event.pid,
2853
+ projectId: project.id,
2854
+ cwd: event.cwd,
2855
+ origin: "external",
2856
+ model: this.extractModel(event)
2857
+ });
2858
+ }
2859
+ const mission = startMission(this.db, sessionId, prompt);
2860
+ const updated = setSessionMission(this.db, sessionId, mission.id);
2861
+ const active = updated ? setSessionStatus(this.db, sessionId, "active") : null;
2862
+ this.broadcaster.broadcast({ type: "mission_upsert", mission });
2863
+ if (active) {
2864
+ this.broadcaster.broadcast({ type: "session_upsert", session: active });
2865
+ }
2866
+ }
2867
+ onStop(event) {
2868
+ const sessionId = this.extractSessionId(event);
2869
+ const session = getSession(this.db, sessionId);
2870
+ if (!session) return;
2871
+ if (session.currentMissionId) {
2872
+ const mission = completeMission(
2873
+ this.db,
2874
+ session.currentMissionId,
2875
+ "completed"
2876
+ );
2877
+ if (mission) {
2878
+ this.broadcaster.broadcast({ type: "mission_upsert", mission });
2879
+ }
2880
+ }
2881
+ const updated = setSessionMission(this.db, sessionId, null);
2882
+ const idle = updated ? setSessionStatus(this.db, sessionId, "idle") : null;
2883
+ if (idle) {
2884
+ this.broadcaster.broadcast({ type: "session_upsert", session: idle });
2885
+ }
2886
+ }
2887
+ onSubagentStop(event) {
2888
+ const subSessionId = this.extractSessionId(event);
2889
+ const session = getSession(this.db, subSessionId);
2890
+ if (!session) return;
2891
+ const terminated = setSessionStatus(this.db, subSessionId, "terminated");
2892
+ if (terminated) {
2893
+ this.broadcaster.broadcast({
2894
+ type: "session_remove",
2895
+ sessionId: subSessionId
2896
+ });
2897
+ }
2898
+ }
2899
+ onPreToolTask(event) {
2900
+ const parentSessionId = this.extractSessionId(event);
2901
+ const parent = getSession(this.db, parentSessionId);
2902
+ if (!parent) return;
2903
+ const subId = nanoid6();
2904
+ const sub = upsertSession(this.db, {
2905
+ id: subId,
2906
+ pid: event.pid,
2907
+ projectId: parent.projectId,
2908
+ cwd: event.cwd,
2909
+ origin: parent.origin,
2910
+ model: parent.model,
2911
+ parentSessionId
2912
+ });
2913
+ const active = setSessionStatus(this.db, subId, "active");
2914
+ if (parent.currentMissionId) {
2915
+ bumpSubagentCount(this.db, parent.currentMissionId);
2916
+ const mission = getMission(this.db, parent.currentMissionId);
2917
+ if (mission)
2918
+ this.broadcaster.broadcast({ type: "mission_upsert", mission });
2919
+ }
2920
+ this.broadcaster.broadcast({
2921
+ type: "session_upsert",
2922
+ session: active ?? sub
2923
+ });
2924
+ }
2925
+ onPreToolFile(event) {
2926
+ const sessionId = this.extractSessionId(event);
2927
+ const session = getSession(this.db, sessionId);
2928
+ if (!session) return;
2929
+ const p = event.payload;
2930
+ const tool = typeof p.tool_name === "string" ? p.tool_name : "File";
2931
+ const filePath = typeof p.file_path === "string" ? p.file_path : typeof p.tool_input?.file_path === "string" ? p.tool_input.file_path : "";
2932
+ const toolCall = recordToolCall(this.db, {
2933
+ sessionId,
2934
+ missionId: session.currentMissionId,
2935
+ tool,
2936
+ args: { file_path: filePath }
2937
+ });
2938
+ if (session.currentMissionId && filePath) {
2939
+ addTouchedFile(this.db, session.currentMissionId, filePath);
2940
+ }
2941
+ this.broadcaster.broadcast({ type: "tool_call", toolCall });
2942
+ }
2943
+ onPreToolBash(event) {
2944
+ const sessionId = this.extractSessionId(event);
2945
+ const session = getSession(this.db, sessionId);
2946
+ if (!session) return;
2947
+ const p = event.payload;
2948
+ const command = typeof p.command === "string" ? p.command : typeof p.tool_input?.command === "string" ? p.tool_input.command : "";
2949
+ const toolCall = recordToolCall(this.db, {
2950
+ sessionId,
2951
+ missionId: session.currentMissionId,
2952
+ tool: "Bash",
2953
+ args: { command }
2954
+ });
2955
+ this.broadcaster.broadcast({ type: "tool_call", toolCall });
2956
+ }
2957
+ onPostTool(event) {
2958
+ const sessionId = this.extractSessionId(event);
2959
+ const session = getSession(this.db, sessionId);
2960
+ if (!session?.currentMissionId) return;
2961
+ bumpToolCallCount(this.db, session.currentMissionId);
2962
+ const mission = getMission(this.db, session.currentMissionId);
2963
+ if (mission)
2964
+ this.broadcaster.broadcast({ type: "mission_upsert", mission });
2965
+ }
2966
+ onNotification(event) {
2967
+ const sessionId = this.extractSessionId(event);
2968
+ const p = event.payload;
2969
+ const message = typeof p.message === "string" ? p.message : "Permission requested";
2970
+ const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
2971
+ const requestId = nanoid6();
2972
+ this.permissions.set(requestId, {
2973
+ requestId,
2974
+ sessionId,
2975
+ tool,
2976
+ args: p.tool_input ?? {},
2977
+ createdAt: Date.now()
2978
+ });
2979
+ const updated = setSessionStatus(this.db, sessionId, "awaiting_permission");
2980
+ if (updated) {
2981
+ this.broadcaster.broadcast({
2982
+ type: "session_upsert",
2983
+ session: updated
2984
+ });
2985
+ }
2986
+ this.broadcaster.broadcast({
2987
+ type: "permission_request",
2988
+ sessionId,
2989
+ tool,
2990
+ args: p.tool_input ?? {},
2991
+ requestId
2992
+ });
2993
+ this.broadcaster.broadcast({
2994
+ type: "toast",
2995
+ level: "warn",
2996
+ message: `Permission requested: ${message}`
2997
+ });
2998
+ }
2999
+ invokeAdvisor(advisorId, targetSessionId, prompt) {
3000
+ const advisor = getAdvisor(this.db, advisorId);
3001
+ if (!advisor) return { ok: false };
3002
+ const envelope = buildContextEnvelope(this.db, {
3003
+ advisorId,
3004
+ targetSessionId,
3005
+ userPrompt: prompt
3006
+ });
3007
+ if (!envelope) return { ok: false };
3008
+ const target = envelope.targetSession;
3009
+ const targetLabel = target ? `${target.name ?? target.id.slice(0, 8)}` : "project level";
3010
+ this.broadcaster.broadcast({
3011
+ type: "toast",
3012
+ level: "info",
3013
+ message: `Invoke ${advisor.codename} \u2192 ${targetLabel} \xB7 ${envelope.recentMissions.length} mission(s) in envelope`
3014
+ });
3015
+ recordAudit(this.db, {
3016
+ kind: "advisor_invoked",
3017
+ advisorId,
3018
+ sessionId: target?.id,
3019
+ projectId: target?.projectId,
3020
+ summary: `Invoked ${advisor.codename} \u2192 ${targetLabel}`,
3021
+ payload: { prompt, missionsInEnvelope: envelope.recentMissions.length }
3022
+ });
3023
+ return { ok: true, envelope: envelope.prompt };
3024
+ }
3025
+ pinAdvisor(advisorId, cwd = process.cwd()) {
3026
+ if (!this.launcher) {
3027
+ const advisor2 = setAdvisorPinned(this.db, advisorId, true);
3028
+ if (!advisor2) return false;
3029
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor: advisor2 });
3030
+ recordAudit(this.db, {
3031
+ kind: "advisor_pinned",
3032
+ advisorId,
3033
+ summary: `Pinned ${advisor2.codename}`
3034
+ });
3035
+ return true;
3036
+ }
3037
+ const ok = this.launcher.pin(advisorId, cwd);
3038
+ const advisor = getAdvisor(this.db, advisorId);
3039
+ if (advisor) {
3040
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
3041
+ if (ok) {
3042
+ recordAudit(this.db, {
3043
+ kind: "advisor_pinned",
3044
+ advisorId,
3045
+ summary: `Pinned ${advisor.codename} in ${cwd}`
3046
+ });
3047
+ }
3048
+ }
3049
+ return ok;
3050
+ }
3051
+ unpinAdvisor(advisorId) {
3052
+ if (this.launcher) {
3053
+ this.launcher.unpin(advisorId);
3054
+ } else {
3055
+ setAdvisorPinned(this.db, advisorId, false);
3056
+ }
3057
+ const advisor = getAdvisor(this.db, advisorId);
3058
+ if (advisor) {
3059
+ this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
3060
+ recordAudit(this.db, {
3061
+ kind: "advisor_unpinned",
3062
+ advisorId,
3063
+ summary: `Unpinned ${advisor.codename}`
3064
+ });
3065
+ }
3066
+ return true;
3067
+ }
3068
+ resolvePermission(requestId, approved) {
3069
+ const pending = this.permissions.get(requestId);
3070
+ if (!pending) return false;
3071
+ this.permissions.delete(requestId);
3072
+ const status = approved ? "active" : "idle";
3073
+ const session = setSessionStatus(this.db, pending.sessionId, status);
3074
+ if (session)
3075
+ this.broadcaster.broadcast({ type: "session_upsert", session });
3076
+ const argSummary = (() => {
3077
+ const k = Object.keys(pending.args)[0];
3078
+ if (!k) return "";
3079
+ const v = pending.args[k];
3080
+ const s = typeof v === "string" ? v : JSON.stringify(v);
3081
+ return `: ${s.length > 80 ? s.slice(0, 80) + "\u2026" : s}`;
3082
+ })();
3083
+ recordAudit(this.db, {
3084
+ kind: approved ? "permission_approved" : "permission_denied",
3085
+ sessionId: pending.sessionId,
3086
+ projectId: session?.projectId,
3087
+ summary: `${approved ? "Approved" : "Denied"} ${pending.tool} for ${session?.name ?? pending.sessionId.slice(0, 8)}${argSummary}`,
3088
+ payload: { tool: pending.tool, args: pending.args }
3089
+ });
3090
+ return true;
3091
+ }
3092
+ launchInternalSession(opts) {
3093
+ if (!this.launcher) return { ok: false };
3094
+ if (!opts.initialPrompt?.trim()) {
3095
+ this.broadcaster.broadcast({
3096
+ type: "toast",
3097
+ level: "warn",
3098
+ message: "Launch needs an initial prompt"
3099
+ });
3100
+ return { ok: false };
3101
+ }
3102
+ return this.launcher.launch({
3103
+ cwd: opts.cwd,
3104
+ model: opts.model,
3105
+ initialPrompt: opts.initialPrompt
3106
+ });
3107
+ }
3108
+ sendPromptToSession(sessionId, text) {
3109
+ if (!this.launcher) return false;
3110
+ return this.launcher.sendPromptToInternal(sessionId, text);
3111
+ }
3112
+ /** Pending permission requests, used by the WS snapshot so a fresh
3113
+ * client connection sees what's already waiting on the server. */
3114
+ pendingPermissions() {
3115
+ return [...this.permissions.values()];
3116
+ }
3117
+ broadcastGalaxyImported(manifest) {
3118
+ this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
3119
+ this.broadcaster.broadcast({
3120
+ type: "toast",
3121
+ level: "info",
3122
+ message: `Galaxy "${manifest.name}" imported`
3123
+ });
3124
+ recordAudit(this.db, {
3125
+ kind: "galaxy_imported",
3126
+ summary: `Imported galaxy "${manifest.name}"`,
3127
+ payload: {
3128
+ author: manifest.author,
3129
+ advisorCount: manifest.advisors.length,
3130
+ skillCount: manifest.skills.length,
3131
+ projectCount: manifest.projects.length
3132
+ }
3133
+ });
3134
+ }
3135
+ setContextUsage(sessionId, pct) {
3136
+ const session = setSessionContextUsage(this.db, sessionId, pct);
3137
+ if (session) {
3138
+ this.broadcaster.broadcast({
3139
+ type: "context_update",
3140
+ sessionId,
3141
+ usagePct: session.contextUsagePct
3142
+ });
3143
+ }
3144
+ }
3145
+ };
3146
+
3147
+ // ../server/src/ws.ts
3148
+ import { WebSocketServer } from "ws";
3149
+ function attachWs(server, ctx) {
3150
+ const wss = new WebSocketServer({ noServer: true });
3151
+ server.on(
3152
+ "upgrade",
3153
+ (req, socket, head) => {
3154
+ const url = req.url ?? "";
3155
+ if (!url.startsWith("/ws")) {
3156
+ socket.destroy();
3157
+ return;
3158
+ }
3159
+ wss.handleUpgrade(req, socket, head, (ws) => {
3160
+ wss.emit("connection", ws, req);
3161
+ });
3162
+ }
3163
+ );
3164
+ wss.on("connection", (ws) => {
3165
+ ctx.broadcaster.add(ws);
3166
+ const snapshot = {
3167
+ type: "snapshot",
3168
+ projects: listProjects(ctx.db),
3169
+ sessions: listActiveSessions(ctx.db),
3170
+ missions: listMissions(ctx.db, { limit: 100 }),
3171
+ advisors: listAdvisors(ctx.db),
3172
+ skills: listSkills(ctx.db)
3173
+ };
3174
+ ctx.broadcaster.send(ws, snapshot);
3175
+ for (const p of ctx.router.pendingPermissions()) {
3176
+ ctx.broadcaster.send(ws, {
3177
+ type: "permission_request",
3178
+ sessionId: p.sessionId,
3179
+ tool: p.tool,
3180
+ args: p.args,
3181
+ requestId: p.requestId
3182
+ });
3183
+ }
3184
+ ws.on("message", (raw) => {
3185
+ let msg = null;
3186
+ try {
3187
+ msg = JSON.parse(String(raw));
3188
+ } catch {
3189
+ return;
3190
+ }
3191
+ if (!msg) return;
3192
+ handleClientMessage(ctx, ws, msg);
3193
+ });
3194
+ ws.on("close", () => {
3195
+ ctx.broadcaster.remove(ws);
3196
+ });
3197
+ ws.on("error", () => {
3198
+ ctx.broadcaster.remove(ws);
3199
+ });
3200
+ });
3201
+ return wss;
3202
+ }
3203
+ function handleClientMessage(ctx, _ws, msg) {
3204
+ switch (msg.type) {
3205
+ case "permission_response":
3206
+ ctx.router.resolvePermission(msg.requestId, msg.approved);
3207
+ break;
3208
+ case "terminate_session":
3209
+ break;
3210
+ case "send_prompt":
3211
+ ctx.router.sendPromptToSession(msg.sessionId, msg.text);
3212
+ break;
3213
+ case "launch_session":
3214
+ ctx.router.launchInternalSession({
3215
+ cwd: msg.cwd,
3216
+ model: msg.model,
3217
+ initialPrompt: msg.initialPrompt
3218
+ });
3219
+ break;
3220
+ case "invoke_advisor":
3221
+ ctx.router.invokeAdvisor(
3222
+ msg.advisorId,
3223
+ msg.targetSessionId,
3224
+ msg.prompt
3225
+ );
3226
+ break;
3227
+ case "pin_advisor":
3228
+ ctx.router.pinAdvisor(msg.advisorId);
3229
+ break;
3230
+ case "unpin_advisor":
3231
+ ctx.router.unpinAdvisor(msg.advisorId);
3232
+ break;
3233
+ default:
3234
+ break;
3235
+ }
3236
+ }
3237
+
3238
+ // ../server/src/state/transcript.ts
3239
+ import {
3240
+ closeSync,
3241
+ existsSync as existsSync8,
3242
+ openSync,
3243
+ readSync,
3244
+ statSync as statSync5,
3245
+ watch
3246
+ } from "fs";
3247
+ import { homedir as homedir5 } from "os";
3248
+ import { join as join9 } from "path";
3249
+ var TRANSCRIPT_BASE = join9(homedir5(), ".claude", "projects");
3250
+ var CONTEXT_BUDGETS_BY_MODEL = {
3251
+ "claude-opus-4-7": 2e5,
3252
+ "claude-opus-4-6": 2e5,
3253
+ "claude-sonnet-4-6": 2e5,
3254
+ "claude-haiku-4-5": 2e5,
3255
+ default: 2e5
3256
+ };
3257
+ var REPLAY_TAIL_BYTES = 64 * 1024;
3258
+ function encodeProjectPath(cwd) {
3259
+ return cwd.replace(/[/\\]/g, "-");
3260
+ }
3261
+ function transcriptPathFor(cwd, sessionId) {
3262
+ return join9(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
3263
+ }
3264
+ var TranscriptWatcherManager = class {
3265
+ constructor(db, broadcaster) {
3266
+ this.db = db;
3267
+ this.broadcaster = broadcaster;
3268
+ void this.db;
3269
+ }
3270
+ db;
3271
+ broadcaster;
3272
+ records = /* @__PURE__ */ new Map();
3273
+ deferredRetry = /* @__PURE__ */ new Map();
3274
+ /**
3275
+ * Begin tailing this session's transcript. Idempotent. If the file doesn't
3276
+ * exist yet, retries every second for up to 10 s (Claude Code creates the
3277
+ * file slightly after the SessionStart hook fires).
3278
+ */
3279
+ startWatching(sessionId, cwd) {
3280
+ if (this.records.has(sessionId)) return;
3281
+ const filePath = transcriptPathFor(cwd, sessionId);
3282
+ if (!existsSync8(filePath)) {
3283
+ this.scheduleRetry(sessionId, cwd, 0);
3284
+ return;
3285
+ }
3286
+ this.attach(sessionId, filePath);
3287
+ }
3288
+ scheduleRetry(sessionId, cwd, attempt) {
3289
+ if (attempt >= 10) return;
3290
+ const t = setTimeout(() => {
3291
+ this.deferredRetry.delete(sessionId);
3292
+ const filePath = transcriptPathFor(cwd, sessionId);
3293
+ if (existsSync8(filePath)) {
3294
+ this.attach(sessionId, filePath);
3295
+ } else {
3296
+ this.scheduleRetry(sessionId, cwd, attempt + 1);
3297
+ }
3298
+ }, 1e3);
3299
+ this.deferredRetry.set(sessionId, t);
3300
+ }
3301
+ attach(sessionId, filePath) {
3302
+ let size = 0;
3303
+ try {
3304
+ size = statSync5(filePath).size;
3305
+ } catch {
3306
+ return;
3307
+ }
3308
+ const startPos = Math.max(0, size - REPLAY_TAIL_BYTES);
3309
+ const record = {
3310
+ sessionId,
3311
+ filePath,
3312
+ position: startPos,
3313
+ initialRead: startPos > 0
3314
+ };
3315
+ this.records.set(sessionId, record);
3316
+ this.readNewLines(record);
3317
+ try {
3318
+ record.fsWatcher = watch(filePath, () => {
3319
+ this.readNewLines(record);
3320
+ });
3321
+ } catch (err) {
3322
+ console.warn(
3323
+ `[transcript] could not watch ${filePath}:`,
3324
+ err.message
3325
+ );
3326
+ }
3327
+ }
3328
+ readNewLines(record) {
3329
+ let stat;
3330
+ try {
3331
+ stat = statSync5(record.filePath);
3332
+ } catch {
3333
+ return;
3334
+ }
3335
+ if (stat.size <= record.position) return;
3336
+ let buf;
3337
+ try {
3338
+ const fd = openSync(record.filePath, "r");
3339
+ buf = Buffer.alloc(stat.size - record.position);
3340
+ readSync(fd, buf, 0, buf.length, record.position);
3341
+ closeSync(fd);
3342
+ } catch (err) {
3343
+ console.warn(
3344
+ `[transcript] read failed for ${record.filePath}:`,
3345
+ err.message
3346
+ );
3347
+ return;
3348
+ }
3349
+ record.position = stat.size;
3350
+ let text = buf.toString("utf8");
3351
+ if (record.initialRead) {
3352
+ const nl = text.indexOf("\n");
3353
+ text = nl >= 0 ? text.slice(nl + 1) : "";
3354
+ record.initialRead = false;
3355
+ }
3356
+ const lines = text.split("\n");
3357
+ for (const line of lines) {
3358
+ if (!line.trim()) continue;
3359
+ this.processLine(record.sessionId, line);
3360
+ }
3361
+ }
3362
+ processLine(sessionId, line) {
3363
+ let obj;
3364
+ try {
3365
+ obj = JSON.parse(line);
3366
+ } catch {
3367
+ return;
3368
+ }
3369
+ const message = obj.message;
3370
+ if (!message) return;
3371
+ const role = message.role;
3372
+ if (role === "user") {
3373
+ this.emitUser(sessionId, obj);
3374
+ } else if (role === "assistant") {
3375
+ this.emitAssistant(sessionId, obj);
3376
+ }
3377
+ }
3378
+ emitUser(sessionId, obj) {
3379
+ const content = this.flattenUserContent(obj.message?.content);
3380
+ if (!content) return;
3381
+ this.broadcaster.broadcast({
3382
+ type: "chat_delta",
3383
+ sessionId,
3384
+ delta: {
3385
+ messageId: obj.uuid ?? obj.promptId ?? `u-${Date.now()}-${Math.random()}`,
3386
+ role: "user",
3387
+ content,
3388
+ ts: this.parseTs(obj.timestamp),
3389
+ done: true
3390
+ }
3391
+ });
3392
+ }
3393
+ emitAssistant(sessionId, obj) {
3394
+ const message = obj.message;
3395
+ if (!message) return;
3396
+ if (message.usage) {
3397
+ const total = (message.usage.input_tokens ?? 0) + (message.usage.cache_read_input_tokens ?? 0) + (message.usage.cache_creation_input_tokens ?? 0);
3398
+ const budget = CONTEXT_BUDGETS_BY_MODEL[message.model ?? "default"] ?? CONTEXT_BUDGETS_BY_MODEL.default;
3399
+ const pct = Math.min(100, total / budget * 100);
3400
+ this.broadcaster.broadcast({
3401
+ type: "context_update",
3402
+ sessionId,
3403
+ usagePct: pct
3404
+ });
3405
+ }
3406
+ const content = this.flattenAssistantContent(message.content);
3407
+ if (!content) return;
3408
+ this.broadcaster.broadcast({
3409
+ type: "chat_delta",
3410
+ sessionId,
3411
+ delta: {
3412
+ messageId: message.id ?? `a-${Date.now()}-${Math.random()}`,
3413
+ role: "assistant",
3414
+ content,
3415
+ ts: this.parseTs(obj.timestamp),
3416
+ done: true
3417
+ }
3418
+ });
3419
+ }
3420
+ flattenUserContent(content) {
3421
+ if (!content) return "";
3422
+ if (typeof content === "string") return content;
3423
+ const parts = [];
3424
+ for (const b of content) {
3425
+ if (!b || typeof b !== "object") continue;
3426
+ if (b.type === "text" && b.text) parts.push(b.text);
3427
+ else if (b.type === "tool_result") {
3428
+ const inner = b.content;
3429
+ const text = typeof inner === "string" ? inner : Array.isArray(inner) ? inner.map(
3430
+ (x) => typeof x === "object" && x !== null && "text" in x ? String(x.text ?? "") : ""
3431
+ ).join("\n") : "";
3432
+ if (text) parts.push(`[tool result]
3433
+ ${text.slice(0, 600)}`);
3434
+ }
3435
+ }
3436
+ return parts.join("\n").trim();
3437
+ }
3438
+ flattenAssistantContent(content) {
3439
+ if (!content) return "";
3440
+ if (typeof content === "string") return content;
3441
+ const parts = [];
3442
+ for (const b of content) {
3443
+ if (!b || typeof b !== "object") continue;
3444
+ if (b.type === "text" && b.text) {
3445
+ parts.push(b.text);
3446
+ } else if (b.type === "tool_use" && b.name) {
3447
+ const inputSummary = JSON.stringify(b.input ?? {}).slice(0, 120);
3448
+ parts.push(`\u25B8 ${b.name} ${inputSummary}`);
3449
+ } else if (b.type === "thinking" && b.thinking) {
3450
+ const t = b.thinking.replace(/\s+/g, " ").slice(0, 100);
3451
+ if (t) parts.push(`\u{1F4AD} ${t}\u2026`);
3452
+ }
3453
+ }
3454
+ return parts.join("\n\n").trim();
3455
+ }
3456
+ parseTs(value) {
3457
+ if (!value) return Date.now();
3458
+ const t = Date.parse(value);
3459
+ return Number.isFinite(t) ? t : Date.now();
3460
+ }
3461
+ stopWatching(sessionId) {
3462
+ const r = this.records.get(sessionId);
3463
+ if (r?.fsWatcher) {
3464
+ try {
3465
+ r.fsWatcher.close();
3466
+ } catch {
3467
+ }
3468
+ }
3469
+ this.records.delete(sessionId);
3470
+ const t = this.deferredRetry.get(sessionId);
3471
+ if (t) {
3472
+ clearTimeout(t);
3473
+ this.deferredRetry.delete(sessionId);
3474
+ }
3475
+ }
3476
+ shutdownAll() {
3477
+ for (const id of [...this.records.keys()]) this.stopWatching(id);
3478
+ for (const id of [...this.deferredRetry.keys()]) this.stopWatching(id);
3479
+ }
3480
+ };
3481
+
3482
+ // ../server/src/create.ts
3483
+ async function createSolixServer(opts = {}) {
3484
+ const port = opts.port ?? 4242;
3485
+ const hostname = opts.hostname ?? "127.0.0.1";
3486
+ const db = getDb();
3487
+ seedAdvisors(db);
3488
+ discoverSkills(db);
3489
+ const broadcaster = new Broadcaster();
3490
+ const launcher = new Launcher(db, broadcaster);
3491
+ const transcripts = new TranscriptWatcherManager(db, broadcaster);
3492
+ const router = new EventRouter(db, broadcaster, launcher, transcripts);
3493
+ const app = createHttpApp({ db, router });
3494
+ const server = serve({
3495
+ fetch: app.fetch,
3496
+ port,
3497
+ hostname
3498
+ });
3499
+ attachWs(server, {
3500
+ db,
3501
+ router,
3502
+ broadcaster
3503
+ });
3504
+ return {
3505
+ port,
3506
+ hostname,
3507
+ close: () => new Promise((resolve4) => {
3508
+ transcripts.shutdownAll();
3509
+ launcher.shutdownAll();
3510
+ server.close(() => resolve4());
3511
+ })
3512
+ };
3513
+ }
3514
+
3515
+ // src/start.ts
3516
+ import open from "open";
3517
+ var BANNER = `
3518
+ ____ ___ _ ___ __ __
3519
+ / ___| / _ \\| | |_ _| \\/ |
3520
+ \\___ \\| | | | | | || |\\/| |
3521
+ ___) | |_| | |___ | || | | |
3522
+ |____/ \\___/|_____|___|_| |_|
3523
+
3524
+ a solar-system command center for Claude Code
3525
+ `;
3526
+ async function start(opts = {}) {
3527
+ const port = opts.port ?? Number(process.env.SOLIX_PORT ?? 4242);
3528
+ console.log(BANNER);
3529
+ const handle = await createSolixServer({ port });
3530
+ const url = `http://${handle.hostname}:${handle.port}`;
3531
+ console.log(`[solix] server listening on ${url}`);
3532
+ console.log(`[solix] events -> POST ${url}/events`);
3533
+ console.log(`[solix] ws -> ws://${handle.hostname}:${handle.port}/ws`);
3534
+ if (!opts.noOpen) {
3535
+ try {
3536
+ await open(url);
3537
+ } catch {
3538
+ console.log(`[solix] open ${url} in your browser to view`);
3539
+ }
3540
+ }
3541
+ console.log(
3542
+ "[solix] start any `claude` session to see your first planet appear"
3543
+ );
3544
+ const shutdown = async (sig) => {
3545
+ console.log(`
3546
+ [solix] ${sig} \u2014 shutting down`);
3547
+ await handle.close();
3548
+ process.exit(0);
3549
+ };
3550
+ process.on("SIGINT", () => void shutdown("SIGINT"));
3551
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
3552
+ }
3553
+
3554
+ // src/uninstall.ts
3555
+ import { copyFileSync as copyFileSync2, existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
3556
+ function uninstall() {
3557
+ if (existsSync9(CLAUDE_BACKUP)) {
3558
+ copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
3559
+ console.log(`[solix] restored settings.json from backup`);
3560
+ return;
3561
+ }
3562
+ if (!existsSync9(CLAUDE_SETTINGS)) {
3563
+ console.log("[solix] nothing to uninstall (no settings.json found)");
3564
+ return;
3565
+ }
3566
+ const cur = JSON.parse(
3567
+ readFileSync6(CLAUDE_SETTINGS, "utf8")
3568
+ );
3569
+ if (cur.hooks) {
3570
+ for (const [evt, entries] of Object.entries(cur.hooks)) {
3571
+ cur.hooks[evt] = entries.filter(
3572
+ (e) => !e.hooks.some((h) => h.command.includes(HOOKS_DIR))
3573
+ );
3574
+ if (cur.hooks[evt].length === 0) delete cur.hooks[evt];
3575
+ }
3576
+ }
3577
+ writeFileSync3(CLAUDE_SETTINGS, JSON.stringify(cur, null, 2) + "\n");
3578
+ console.log(`[solix] removed Solix hooks from ${CLAUDE_SETTINGS}`);
3579
+ }
3580
+
3581
+ // src/index.ts
3582
+ var program = new Command();
3583
+ program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.0.0");
3584
+ 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) => {
3585
+ await start({ port: opts.port, noOpen: !opts.open });
3586
+ });
3587
+ program.command("install").description("Install Solix hooks into ~/.claude/settings.json").option("--force", "overwrite even if hooks already present").action((opts) => {
3588
+ install({ force: opts.force });
3589
+ console.log("\n[solix] install complete. Run `solix start` next.");
3590
+ });
3591
+ program.command("uninstall").description("Restore ~/.claude/settings.json from backup").action(() => {
3592
+ uninstall();
3593
+ });
3594
+ program.command("doctor").description("Run diagnostics").action(async () => {
3595
+ await doctor();
3596
+ });
3597
+ program.command("demo").description(
3598
+ "Seed the running server with fake planets, missions, and a pinned advisor (great for first-run)"
3599
+ ).option("-p, --port <port>", "server port", (v) => parseInt(v, 10), 4242).action(async (opts) => {
3600
+ await demoCmd({ port: opts.port });
3601
+ });
3602
+ var advisors = program.command("advisors").description("Manage built-in advisor agents (PM, Builder, UX, etc.)");
3603
+ advisors.command("list", { isDefault: true }).description("List all advisor agents and their state").action(async () => {
3604
+ await listAdvisorsCmd();
3605
+ });
3606
+ advisors.command("enable <id>").description("Enable an advisor (renders in the inner crew ring)").action(async (id) => {
3607
+ await enableAdvisorCmd(id);
3608
+ });
3609
+ advisors.command("disable <id>").description("Disable an advisor").action(async (id) => {
3610
+ await disableAdvisorCmd(id);
3611
+ });
3612
+ advisors.command("pin <id>").description("Pin an advisor (always-on planet)").action(async (id) => {
3613
+ await pinAdvisorCmd(id);
3614
+ });
3615
+ advisors.command("unpin <id>").description("Unpin an advisor (back to on-demand)").action(async (id) => {
3616
+ await unpinAdvisorCmd(id);
3617
+ });
3618
+ var skills = program.command("skills").description("Manage discovered skills (asteroid belt)");
3619
+ skills.command("list", { isDefault: true }).description("List all known skills (Anthropic + Solix pack)").action(async () => {
3620
+ await listSkillsCmd();
3621
+ });
3622
+ 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) => {
3623
+ await installSkillCmd(id, opts.project);
3624
+ });
3625
+ var galaxy = program.command("galaxy").description("Export and import shareable galaxy configurations");
3626
+ 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(
3627
+ async (out, opts) => {
3628
+ await exportGalaxyCmd(out, opts);
3629
+ }
3630
+ );
3631
+ galaxy.command("import <fileOrUrl>").description("Import a galaxy manifest from a local file or URL").action(async (fileOrUrl) => {
3632
+ await importGalaxyCmd(fileOrUrl);
3633
+ });
3634
+ 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(
3635
+ async (slug, opts) => {
3636
+ await publishGalaxyCmd(slug, opts);
3637
+ }
3638
+ );
3639
+ galaxy.command("install <slug>").description("Pull and install a galaxy from the configured registry").action(async (slug) => {
3640
+ await installFromRegistryCmd(slug);
3641
+ });
3642
+ program.parseAsync(process.argv).catch((err) => {
3643
+ console.error(err);
3644
+ process.exit(1);
3645
+ });