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