@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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/agents/argus.md +19 -0
- package/dist/agents/atlas.md +17 -0
- package/dist/agents/compass.md +20 -0
- package/dist/agents/echo.md +19 -0
- package/dist/agents/forge.md +24 -0
- package/dist/agents/helios.md +20 -0
- package/dist/agents/lumen.md +23 -0
- package/dist/agents/manifest.json +140 -0
- package/dist/agents/mira.md +19 -0
- package/dist/agents/sentinel.md +26 -0
- package/dist/agents/vale.md +18 -0
- package/dist/hooks/notification.sh +16 -0
- package/dist/hooks/post-tool.sh +16 -0
- package/dist/hooks/pre-tool-bash.sh +16 -0
- package/dist/hooks/pre-tool-file.sh +16 -0
- package/dist/hooks/pre-tool-task.sh +16 -0
- package/dist/hooks/prompt-submit.sh +16 -0
- package/dist/hooks/session-start.sh +16 -0
- package/dist/hooks/stop.sh +16 -0
- package/dist/hooks/subagent-stop.sh +16 -0
- package/dist/index.js +3645 -0
- package/dist/web/assets/index-Bqgw2ZPc.css +1 -0
- package/dist/web/assets/index-Wwg3QOZU.js +4191 -0
- package/dist/web/index.html +13 -0
- package/dist/web/textures/.gitkeep +0 -0
- package/dist/web/textures/earth.jpg +0 -0
- package/dist/web/textures/earth_clouds.png +0 -0
- package/dist/web/textures/jupiter.jpg +0 -0
- package/dist/web/textures/mars.jpg +0 -0
- package/dist/web/textures/milky_way.jpg +0 -0
- package/dist/web/textures/moon.jpg +0 -0
- package/dist/web/textures/saturn.jpg +0 -0
- package/dist/web/textures/saturn_ring.png +0 -0
- package/dist/web/textures/sun.jpg +0 -0
- package/package.json +65 -0
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
|
+
});
|