@shmulikdav/solix 1.4.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +1180 -54
- package/dist/web/assets/CrewPanel-BZW5L0xR.js +1 -0
- package/dist/web/assets/{GalaxyPanel-BOv3eUQ6.js → GalaxyPanel-CCsddWgL.js} +1 -1
- package/dist/web/assets/Scene-D9_dLZUf.js +700 -0
- package/dist/web/assets/{TimelineDrawer-B4MB3hvB.js → TimelineDrawer-xRdQJcuf.js} +1 -1
- package/dist/web/assets/index-Df150VgL.js +3900 -0
- package/dist/web/assets/index-V4rr9htx.css +1 -0
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/dist/web/assets/Scene-CUyQyFqH.js +0 -292
- package/dist/web/assets/index-BUrmJuUb.js +0 -3900
- package/dist/web/assets/index-CzhFbCNn.css +0 -1
package/dist/index.js
CHANGED
|
@@ -245,6 +245,7 @@ import { fileURLToPath } from "url";
|
|
|
245
245
|
var SOLIX_HOME = process.env.SOLIX_HOME ?? join2(homedir2(), ".solix");
|
|
246
246
|
var HOOKS_DIR = join2(SOLIX_HOME, "hooks");
|
|
247
247
|
var SOLIX_SKILLS_DIR = join2(SOLIX_HOME, "skills");
|
|
248
|
+
var SOLIX_TOKEN_FILE = join2(SOLIX_HOME, "token");
|
|
248
249
|
var CLAUDE_DIR = join2(homedir2(), ".claude");
|
|
249
250
|
var CLAUDE_SETTINGS = join2(CLAUDE_DIR, "settings.json");
|
|
250
251
|
var CLAUDE_BACKUP = join2(CLAUDE_DIR, "settings.solix.backup.json");
|
|
@@ -343,6 +344,46 @@ async function probeWrappers(port) {
|
|
|
343
344
|
};
|
|
344
345
|
}
|
|
345
346
|
}
|
|
347
|
+
async function probeAgentView(port) {
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/preflight`, {
|
|
350
|
+
signal: AbortSignal.timeout(1500)
|
|
351
|
+
});
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
label: "Agent View available",
|
|
356
|
+
detail: "server too old to report"
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const data = await res.json();
|
|
360
|
+
if (!data.claudeAvailable) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
label: "Agent View available",
|
|
364
|
+
detail: "claude not on PATH"
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (data.agentViewAvailable) {
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
label: "Agent View available",
|
|
371
|
+
detail: `yes (${data.version ?? "unknown version"})`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
label: "Agent View available",
|
|
377
|
+
detail: `no \u2014 need Claude Code 2.1.139+ (have ${data.version ?? "?"})`
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
label: "Agent View available",
|
|
383
|
+
detail: "unknown \u2014 server unreachable"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
346
387
|
async function doctor() {
|
|
347
388
|
const port = Number(process.env.SOLIX_PORT ?? 4242);
|
|
348
389
|
const checks = [];
|
|
@@ -425,6 +466,7 @@ async function doctor() {
|
|
|
425
466
|
});
|
|
426
467
|
checks.push(await probeHealth(port));
|
|
427
468
|
checks.push(await probeWrappers(port));
|
|
469
|
+
checks.push(await probeAgentView(port));
|
|
428
470
|
console.log("\nSolix Diagnostics\n");
|
|
429
471
|
let allOk = true;
|
|
430
472
|
for (const c of checks) {
|
|
@@ -560,6 +602,7 @@ import {
|
|
|
560
602
|
writeFileSync as writeFileSync2,
|
|
561
603
|
chmodSync
|
|
562
604
|
} from "fs";
|
|
605
|
+
import { randomBytes } from "crypto";
|
|
563
606
|
import { join as join4 } from "path";
|
|
564
607
|
function readSettings() {
|
|
565
608
|
if (!existsSync3(CLAUDE_SETTINGS)) return {};
|
|
@@ -613,6 +656,15 @@ function mergeHooks(existing, solix) {
|
|
|
613
656
|
}
|
|
614
657
|
return merged;
|
|
615
658
|
}
|
|
659
|
+
function ensureToken() {
|
|
660
|
+
if (existsSync3(SOLIX_TOKEN_FILE)) return;
|
|
661
|
+
const token = randomBytes(24).toString("hex");
|
|
662
|
+
writeFileSync2(SOLIX_TOKEN_FILE, token, { mode: 384 });
|
|
663
|
+
try {
|
|
664
|
+
chmodSync(SOLIX_TOKEN_FILE, 384);
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
616
668
|
function installHookScripts() {
|
|
617
669
|
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
618
670
|
const src = packagedHooksDir();
|
|
@@ -678,6 +730,7 @@ function install(opts = {}) {
|
|
|
678
730
|
} else if (opts.force && existsSync3(CLAUDE_SETTINGS)) {
|
|
679
731
|
copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
|
|
680
732
|
}
|
|
733
|
+
ensureToken();
|
|
681
734
|
installHookScripts();
|
|
682
735
|
console.log(`[solix] installed hook scripts in ${HOOKS_DIR}`);
|
|
683
736
|
const advisorsCopied = installAdvisorAgents();
|
|
@@ -979,8 +1032,145 @@ async function installSkillCmd(id, projectId) {
|
|
|
979
1032
|
}
|
|
980
1033
|
}
|
|
981
1034
|
|
|
1035
|
+
// src/schedule.ts
|
|
1036
|
+
var PORT6 = process.env.SOLIX_PORT ?? "4242";
|
|
1037
|
+
var BASE6 = `http://127.0.0.1:${PORT6}`;
|
|
1038
|
+
async function api4(path, init) {
|
|
1039
|
+
const res = await fetch(`${BASE6}${path}`, {
|
|
1040
|
+
...init,
|
|
1041
|
+
headers: { "content-type": "application/json", ...init?.headers ?? {} }
|
|
1042
|
+
});
|
|
1043
|
+
if (!res.ok) {
|
|
1044
|
+
const text = await res.text().catch(() => "");
|
|
1045
|
+
throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
|
|
1046
|
+
}
|
|
1047
|
+
return await res.json();
|
|
1048
|
+
}
|
|
1049
|
+
function unreachable(err) {
|
|
1050
|
+
console.error(`[solix] could not reach server at ${BASE6}: ${String(err)}`);
|
|
1051
|
+
console.error("[solix] is `solix start` running?");
|
|
1052
|
+
process.exitCode = 1;
|
|
1053
|
+
}
|
|
1054
|
+
async function listSchedulesCmd() {
|
|
1055
|
+
try {
|
|
1056
|
+
const list = await api4("/api/schedules");
|
|
1057
|
+
if (!list.length) {
|
|
1058
|
+
console.log("No schedules. Add one with `solix schedule add`.");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
console.log("id every state next run prompt");
|
|
1062
|
+
for (const s of list) {
|
|
1063
|
+
const next = new Date(s.nextRunAt).toLocaleString();
|
|
1064
|
+
const state = s.enabled ? "on " : "off";
|
|
1065
|
+
console.log(
|
|
1066
|
+
` ${s.id.padEnd(8)} ${s.cron.padEnd(5)} [${state}] ${next.padEnd(20)} ${s.prompt.slice(0, 40)}`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
unreachable(err);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async function addScheduleCmd(prompt, opts) {
|
|
1074
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1075
|
+
const cadence = opts.every ?? "1h";
|
|
1076
|
+
try {
|
|
1077
|
+
const s = await api4("/api/schedules", {
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
body: JSON.stringify({ cwd, prompt, cadence, name: opts.name })
|
|
1080
|
+
});
|
|
1081
|
+
console.log(
|
|
1082
|
+
`[solix] scheduled ${s.id} \u2014 every ${s.cron} in ${cwd}
|
|
1083
|
+
next run: ${new Date(s.nextRunAt).toLocaleString()}`
|
|
1084
|
+
);
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
unreachable(err);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async function toggle(id, enabled) {
|
|
1090
|
+
try {
|
|
1091
|
+
await api4(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
body: JSON.stringify({ enabled })
|
|
1094
|
+
});
|
|
1095
|
+
console.log(`[solix] schedule ${id} \u2192 ${enabled ? "enabled" : "disabled"}`);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
unreachable(err);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
var enableScheduleCmd = (id) => toggle(id, true);
|
|
1101
|
+
var disableScheduleCmd = (id) => toggle(id, false);
|
|
1102
|
+
async function removeScheduleCmd(id) {
|
|
1103
|
+
try {
|
|
1104
|
+
await api4(`/api/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1105
|
+
console.log(`[solix] removed schedule ${id}`);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
unreachable(err);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/goals.ts
|
|
1112
|
+
var PORT7 = process.env.SOLIX_PORT ?? "4242";
|
|
1113
|
+
var BASE7 = `http://127.0.0.1:${PORT7}`;
|
|
1114
|
+
async function api5(path, init) {
|
|
1115
|
+
const res = await fetch(`${BASE7}${path}`, {
|
|
1116
|
+
...init,
|
|
1117
|
+
headers: { "content-type": "application/json", ...init?.headers ?? {} }
|
|
1118
|
+
});
|
|
1119
|
+
if (!res.ok) {
|
|
1120
|
+
const text = await res.text().catch(() => "");
|
|
1121
|
+
throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
|
|
1122
|
+
}
|
|
1123
|
+
return await res.json();
|
|
1124
|
+
}
|
|
1125
|
+
function unreachable2(err) {
|
|
1126
|
+
console.error(`[solix] could not reach server at ${BASE7}: ${String(err)}`);
|
|
1127
|
+
console.error("[solix] is `solix start` running?");
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
}
|
|
1130
|
+
async function listGoalsCmd() {
|
|
1131
|
+
try {
|
|
1132
|
+
const goals = await api5("/api/goals");
|
|
1133
|
+
if (!goals.length) {
|
|
1134
|
+
console.log('No goals. Add one with `solix goal add "<name>"`.');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
console.log("id color name");
|
|
1138
|
+
for (const g of goals) {
|
|
1139
|
+
console.log(` ${g.id.padEnd(8)} ${g.color.padEnd(8)} ${g.name}`);
|
|
1140
|
+
}
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
unreachable2(err);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function addGoalCmd(name, opts) {
|
|
1146
|
+
try {
|
|
1147
|
+
const g = await api5("/api/goals", {
|
|
1148
|
+
method: "POST",
|
|
1149
|
+
body: JSON.stringify({
|
|
1150
|
+
name,
|
|
1151
|
+
description: opts.description,
|
|
1152
|
+
color: opts.color
|
|
1153
|
+
})
|
|
1154
|
+
});
|
|
1155
|
+
console.log(`[solix] created goal ${g.id} \u2014 "${g.name}" (${g.color})`);
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
unreachable2(err);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async function removeGoalCmd(id) {
|
|
1161
|
+
try {
|
|
1162
|
+
await api5(`/api/goals/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1163
|
+
console.log(`[solix] removed goal ${id}`);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
unreachable2(err);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
982
1169
|
// ../server/src/create.ts
|
|
983
1170
|
import { serve } from "@hono/node-server";
|
|
1171
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1172
|
+
import { homedir as homedir11 } from "os";
|
|
1173
|
+
import { join as join15 } from "path";
|
|
984
1174
|
|
|
985
1175
|
// ../server/src/broadcaster.ts
|
|
986
1176
|
var Broadcaster = class {
|
|
@@ -1156,6 +1346,14 @@ CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
|
1156
1346
|
last_run_at INTEGER,
|
|
1157
1347
|
next_run_at INTEGER NOT NULL
|
|
1158
1348
|
);
|
|
1349
|
+
|
|
1350
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
1351
|
+
id TEXT PRIMARY KEY,
|
|
1352
|
+
name TEXT NOT NULL,
|
|
1353
|
+
description TEXT,
|
|
1354
|
+
color TEXT NOT NULL,
|
|
1355
|
+
created_at INTEGER NOT NULL
|
|
1356
|
+
);
|
|
1159
1357
|
`;
|
|
1160
1358
|
var _db = null;
|
|
1161
1359
|
function ensureColumn(db, table, column, ddl) {
|
|
@@ -1175,8 +1373,18 @@ function getDb() {
|
|
|
1175
1373
|
ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
|
|
1176
1374
|
ensureColumn(db, "sessions", "worktree_path", "worktree_path TEXT");
|
|
1177
1375
|
ensureColumn(db, "sessions", "wrapper_socket_path", "wrapper_socket_path TEXT");
|
|
1376
|
+
ensureColumn(db, "sessions", "agent_view_id", "agent_view_id TEXT");
|
|
1377
|
+
ensureColumn(db, "sessions", "agent_view_summary", "agent_view_summary TEXT");
|
|
1378
|
+
ensureColumn(db, "sessions", "pr_url", "pr_url TEXT");
|
|
1379
|
+
ensureColumn(db, "sessions", "pr_check_status", "pr_check_status TEXT");
|
|
1178
1380
|
ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
|
|
1179
1381
|
ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
|
|
1382
|
+
ensureColumn(db, "sessions", "cost_usd", "cost_usd REAL DEFAULT 0");
|
|
1383
|
+
ensureColumn(db, "sessions", "budget_usd", "budget_usd REAL");
|
|
1384
|
+
ensureColumn(db, "sessions", "current_goal_id", "current_goal_id TEXT");
|
|
1385
|
+
ensureColumn(db, "missions", "goal_id", "goal_id TEXT");
|
|
1386
|
+
ensureColumn(db, "scheduled_tasks", "cwd", "cwd TEXT");
|
|
1387
|
+
ensureColumn(db, "scheduled_tasks", "name", "name TEXT");
|
|
1180
1388
|
_db = db;
|
|
1181
1389
|
return db;
|
|
1182
1390
|
}
|
|
@@ -1262,7 +1470,14 @@ function rowToSession(row) {
|
|
|
1262
1470
|
orbitSlot: row.orbit_slot,
|
|
1263
1471
|
name: row.name ?? void 0,
|
|
1264
1472
|
worktreePath: row.worktree_path ?? void 0,
|
|
1265
|
-
wrapperSocketPath: row.wrapper_socket_path ?? void 0
|
|
1473
|
+
wrapperSocketPath: row.wrapper_socket_path ?? void 0,
|
|
1474
|
+
agentViewId: row.agent_view_id ?? void 0,
|
|
1475
|
+
agentViewSummary: row.agent_view_summary ?? void 0,
|
|
1476
|
+
prUrl: row.pr_url ?? void 0,
|
|
1477
|
+
prCheckStatus: row.pr_check_status ?? void 0,
|
|
1478
|
+
costUsd: row.cost_usd ?? 0,
|
|
1479
|
+
budgetUsd: row.budget_usd ?? void 0,
|
|
1480
|
+
currentGoalId: row.current_goal_id ?? void 0
|
|
1266
1481
|
};
|
|
1267
1482
|
}
|
|
1268
1483
|
function nextOrbitSlot(db, projectId) {
|
|
@@ -1297,9 +1512,11 @@ function upsertSession(db, input) {
|
|
|
1297
1512
|
`INSERT INTO sessions (
|
|
1298
1513
|
id, pid, project_id, parent_session_id, origin, model, status,
|
|
1299
1514
|
context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
|
|
1300
|
-
worktree_path, wrapper_socket_path,
|
|
1515
|
+
worktree_path, wrapper_socket_path,
|
|
1516
|
+
agent_view_id, agent_view_summary, pr_url, pr_check_status,
|
|
1517
|
+
created_at, updated_at
|
|
1301
1518
|
)
|
|
1302
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?)`
|
|
1519
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1303
1520
|
).run(
|
|
1304
1521
|
input.id,
|
|
1305
1522
|
input.pid,
|
|
@@ -1314,6 +1531,10 @@ function upsertSession(db, input) {
|
|
|
1314
1531
|
input.advisorRole ?? null,
|
|
1315
1532
|
input.worktreePath ?? null,
|
|
1316
1533
|
input.wrapperSocketPath ?? null,
|
|
1534
|
+
input.agentViewId ?? null,
|
|
1535
|
+
input.agentViewSummary ?? null,
|
|
1536
|
+
input.prUrl ?? null,
|
|
1537
|
+
input.prCheckStatus ?? null,
|
|
1317
1538
|
ts2,
|
|
1318
1539
|
ts2
|
|
1319
1540
|
);
|
|
@@ -1333,9 +1554,40 @@ function upsertSession(db, input) {
|
|
|
1333
1554
|
contextUsagePct: 0,
|
|
1334
1555
|
orbitSlot,
|
|
1335
1556
|
worktreePath: input.worktreePath,
|
|
1336
|
-
wrapperSocketPath: input.wrapperSocketPath
|
|
1557
|
+
wrapperSocketPath: input.wrapperSocketPath,
|
|
1558
|
+
agentViewId: input.agentViewId,
|
|
1559
|
+
agentViewSummary: input.agentViewSummary,
|
|
1560
|
+
prUrl: input.prUrl,
|
|
1561
|
+
prCheckStatus: input.prCheckStatus,
|
|
1562
|
+
costUsd: 0
|
|
1337
1563
|
};
|
|
1338
1564
|
}
|
|
1565
|
+
function setAgentViewFields(db, sessionId, fields) {
|
|
1566
|
+
const ts2 = now();
|
|
1567
|
+
const updates = ["updated_at = ?"];
|
|
1568
|
+
const values = [ts2];
|
|
1569
|
+
if (fields.status !== void 0) {
|
|
1570
|
+
updates.push("status = ?");
|
|
1571
|
+
values.push(fields.status);
|
|
1572
|
+
}
|
|
1573
|
+
if (fields.agentViewSummary !== void 0) {
|
|
1574
|
+
updates.push("agent_view_summary = ?");
|
|
1575
|
+
values.push(fields.agentViewSummary);
|
|
1576
|
+
}
|
|
1577
|
+
if (fields.prUrl !== void 0) {
|
|
1578
|
+
updates.push("pr_url = ?");
|
|
1579
|
+
values.push(fields.prUrl);
|
|
1580
|
+
}
|
|
1581
|
+
if (fields.prCheckStatus !== void 0) {
|
|
1582
|
+
updates.push("pr_check_status = ?");
|
|
1583
|
+
values.push(fields.prCheckStatus);
|
|
1584
|
+
}
|
|
1585
|
+
values.push(sessionId);
|
|
1586
|
+
db.prepare(
|
|
1587
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`
|
|
1588
|
+
).run(...values);
|
|
1589
|
+
return getSession(db, sessionId);
|
|
1590
|
+
}
|
|
1339
1591
|
function setSessionStatus(db, sessionId, status) {
|
|
1340
1592
|
const ts2 = now();
|
|
1341
1593
|
if (status === "terminated") {
|
|
@@ -1371,6 +1623,27 @@ function setSessionContextUsage(db, sessionId, pct) {
|
|
|
1371
1623
|
).run(clamped, ts2, sessionId);
|
|
1372
1624
|
return getSession(db, sessionId);
|
|
1373
1625
|
}
|
|
1626
|
+
function setSessionCost(db, sessionId, costUsd) {
|
|
1627
|
+
const ts2 = now();
|
|
1628
|
+
db.prepare(
|
|
1629
|
+
`UPDATE sessions SET cost_usd = ?, updated_at = ? WHERE id = ?`
|
|
1630
|
+
).run(Math.max(0, costUsd), ts2, sessionId);
|
|
1631
|
+
return getSession(db, sessionId);
|
|
1632
|
+
}
|
|
1633
|
+
function setSessionBudget(db, sessionId, budgetUsd) {
|
|
1634
|
+
const ts2 = now();
|
|
1635
|
+
db.prepare(
|
|
1636
|
+
`UPDATE sessions SET budget_usd = ?, updated_at = ? WHERE id = ?`
|
|
1637
|
+
).run(budgetUsd, ts2, sessionId);
|
|
1638
|
+
return getSession(db, sessionId);
|
|
1639
|
+
}
|
|
1640
|
+
function setSessionGoal(db, sessionId, goalId) {
|
|
1641
|
+
const ts2 = now();
|
|
1642
|
+
db.prepare(
|
|
1643
|
+
`UPDATE sessions SET current_goal_id = ?, updated_at = ? WHERE id = ?`
|
|
1644
|
+
).run(goalId, ts2, sessionId);
|
|
1645
|
+
return getSession(db, sessionId);
|
|
1646
|
+
}
|
|
1374
1647
|
function getSession(db, sessionId) {
|
|
1375
1648
|
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1376
1649
|
return row ? rowToSession(row) : null;
|
|
@@ -1419,7 +1692,8 @@ function rowToMission(row) {
|
|
|
1419
1692
|
toolCallCount: row.tool_call_count
|
|
1420
1693
|
},
|
|
1421
1694
|
filesTouched,
|
|
1422
|
-
errorSummary: row.error_summary ?? void 0
|
|
1695
|
+
errorSummary: row.error_summary ?? void 0,
|
|
1696
|
+
goalId: row.goal_id ?? void 0
|
|
1423
1697
|
};
|
|
1424
1698
|
}
|
|
1425
1699
|
function setMissionError(db, missionId, errorSummary) {
|
|
@@ -1435,14 +1709,14 @@ function shortNameFromPrompt(prompt) {
|
|
|
1435
1709
|
(w) => w.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/^./, (c) => c.toUpperCase())
|
|
1436
1710
|
).filter(Boolean).join(" ") || "New Mission";
|
|
1437
1711
|
}
|
|
1438
|
-
function startMission(db, sessionId, prompt) {
|
|
1712
|
+
function startMission(db, sessionId, prompt, goalId) {
|
|
1439
1713
|
const id = nanoid2();
|
|
1440
1714
|
const ts2 = now();
|
|
1441
1715
|
const shortName = shortNameFromPrompt(prompt);
|
|
1442
1716
|
db.prepare(
|
|
1443
|
-
`INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json)
|
|
1444
|
-
VALUES (?, ?, ?, ?, 'active', ?, '[]')`
|
|
1445
|
-
).run(id, sessionId, prompt, shortName, ts2);
|
|
1717
|
+
`INSERT INTO missions (id, session_id, prompt, short_name, status, started_at, files_touched_json, goal_id)
|
|
1718
|
+
VALUES (?, ?, ?, ?, 'active', ?, '[]', ?)`
|
|
1719
|
+
).run(id, sessionId, prompt, shortName, ts2, goalId ?? null);
|
|
1446
1720
|
return {
|
|
1447
1721
|
id,
|
|
1448
1722
|
sessionId,
|
|
@@ -1451,9 +1725,16 @@ function startMission(db, sessionId, prompt) {
|
|
|
1451
1725
|
shortName,
|
|
1452
1726
|
status: "active",
|
|
1453
1727
|
metrics: { subagentCount: 0, toolCallCount: 0 },
|
|
1454
|
-
filesTouched: []
|
|
1728
|
+
filesTouched: [],
|
|
1729
|
+
goalId
|
|
1455
1730
|
};
|
|
1456
1731
|
}
|
|
1732
|
+
function addMissionTokens(db, missionId, tokens) {
|
|
1733
|
+
if (tokens <= 0) return;
|
|
1734
|
+
db.prepare(
|
|
1735
|
+
`UPDATE missions SET total_tokens = COALESCE(total_tokens, 0) + ? WHERE id = ?`
|
|
1736
|
+
).run(Math.round(tokens), missionId);
|
|
1737
|
+
}
|
|
1457
1738
|
function completeMission(db, missionId, status = "completed") {
|
|
1458
1739
|
const ts2 = now();
|
|
1459
1740
|
const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
|
|
@@ -1895,6 +2176,138 @@ function writeToWrapperSocket(socketPath, text) {
|
|
|
1895
2176
|
}
|
|
1896
2177
|
}
|
|
1897
2178
|
|
|
2179
|
+
// ../server/src/state/schedules.ts
|
|
2180
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2181
|
+
function rowToSchedule(row) {
|
|
2182
|
+
return {
|
|
2183
|
+
id: row.id,
|
|
2184
|
+
projectId: row.project_id,
|
|
2185
|
+
cwd: row.cwd ?? "",
|
|
2186
|
+
name: row.name ?? void 0,
|
|
2187
|
+
prompt: row.prompt,
|
|
2188
|
+
cron: row.cron,
|
|
2189
|
+
enabled: row.enabled !== 0,
|
|
2190
|
+
lastRunAt: row.last_run_at ?? void 0,
|
|
2191
|
+
nextRunAt: row.next_run_at
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
function cadenceToMs(cadence) {
|
|
2195
|
+
const m = cadence.trim().match(/^(\d+)\s*([mhd])$/i);
|
|
2196
|
+
if (!m) return null;
|
|
2197
|
+
const n = Number(m[1]);
|
|
2198
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
2199
|
+
const unit = m[2].toLowerCase();
|
|
2200
|
+
const mult = unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
2201
|
+
return n * mult;
|
|
2202
|
+
}
|
|
2203
|
+
function nextRunFrom(fromMs, cadence) {
|
|
2204
|
+
const ms = cadenceToMs(cadence);
|
|
2205
|
+
return fromMs + (ms ?? 36e5);
|
|
2206
|
+
}
|
|
2207
|
+
function createSchedule(db, input) {
|
|
2208
|
+
const project = ensureProject(db, input.cwd);
|
|
2209
|
+
const id = nanoid4(8);
|
|
2210
|
+
const ts2 = now();
|
|
2211
|
+
const nextRun = nextRunFrom(ts2, input.cadence);
|
|
2212
|
+
db.prepare(
|
|
2213
|
+
`INSERT INTO scheduled_tasks
|
|
2214
|
+
(id, project_id, cwd, name, prompt, cron, enabled, last_run_at, next_run_at)
|
|
2215
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, NULL, ?)`
|
|
2216
|
+
).run(id, project.id, input.cwd, input.name ?? null, input.prompt, input.cadence, nextRun);
|
|
2217
|
+
return getSchedule(db, id);
|
|
2218
|
+
}
|
|
2219
|
+
function getSchedule(db, id) {
|
|
2220
|
+
const row = db.prepare("SELECT * FROM scheduled_tasks WHERE id = ?").get(id);
|
|
2221
|
+
return row ? rowToSchedule(row) : null;
|
|
2222
|
+
}
|
|
2223
|
+
function listSchedules(db) {
|
|
2224
|
+
const rows = db.prepare("SELECT * FROM scheduled_tasks ORDER BY next_run_at ASC").all();
|
|
2225
|
+
return rows.map(rowToSchedule);
|
|
2226
|
+
}
|
|
2227
|
+
function listDueSchedules(db, asOf) {
|
|
2228
|
+
const rows = db.prepare(
|
|
2229
|
+
`SELECT * FROM scheduled_tasks WHERE enabled = 1 AND next_run_at <= ?`
|
|
2230
|
+
).all(asOf);
|
|
2231
|
+
return rows.map(rowToSchedule);
|
|
2232
|
+
}
|
|
2233
|
+
function setScheduleEnabled(db, id, enabled) {
|
|
2234
|
+
db.prepare("UPDATE scheduled_tasks SET enabled = ? WHERE id = ?").run(
|
|
2235
|
+
enabled ? 1 : 0,
|
|
2236
|
+
id
|
|
2237
|
+
);
|
|
2238
|
+
return getSchedule(db, id);
|
|
2239
|
+
}
|
|
2240
|
+
function markScheduleRun(db, id) {
|
|
2241
|
+
const sched = getSchedule(db, id);
|
|
2242
|
+
if (!sched) return null;
|
|
2243
|
+
const ts2 = now();
|
|
2244
|
+
const next = nextRunFrom(ts2, sched.cron);
|
|
2245
|
+
db.prepare(
|
|
2246
|
+
"UPDATE scheduled_tasks SET last_run_at = ?, next_run_at = ? WHERE id = ?"
|
|
2247
|
+
).run(ts2, next, id);
|
|
2248
|
+
return getSchedule(db, id);
|
|
2249
|
+
}
|
|
2250
|
+
function deleteSchedule(db, id) {
|
|
2251
|
+
const res = db.prepare("DELETE FROM scheduled_tasks WHERE id = ?").run(id);
|
|
2252
|
+
return res.changes > 0;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// ../server/src/state/goals.ts
|
|
2256
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2257
|
+
var PALETTE = [
|
|
2258
|
+
"#38bdf8",
|
|
2259
|
+
// sky
|
|
2260
|
+
"#a78bfa",
|
|
2261
|
+
// violet
|
|
2262
|
+
"#34d399",
|
|
2263
|
+
// emerald
|
|
2264
|
+
"#fbbf24",
|
|
2265
|
+
// amber
|
|
2266
|
+
"#f472b6",
|
|
2267
|
+
// pink
|
|
2268
|
+
"#f87171",
|
|
2269
|
+
// red
|
|
2270
|
+
"#22d3ee",
|
|
2271
|
+
// cyan
|
|
2272
|
+
"#c084fc"
|
|
2273
|
+
// purple
|
|
2274
|
+
];
|
|
2275
|
+
function rowToGoal(row) {
|
|
2276
|
+
return {
|
|
2277
|
+
id: row.id,
|
|
2278
|
+
name: row.name,
|
|
2279
|
+
description: row.description ?? void 0,
|
|
2280
|
+
color: row.color,
|
|
2281
|
+
createdAt: row.created_at
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function nextColor(db) {
|
|
2285
|
+
const count = db.prepare("SELECT COUNT(*) AS n FROM goals").get().n;
|
|
2286
|
+
return PALETTE[count % PALETTE.length];
|
|
2287
|
+
}
|
|
2288
|
+
function createGoal(db, input) {
|
|
2289
|
+
const id = nanoid5(8);
|
|
2290
|
+
const ts2 = now();
|
|
2291
|
+
const color = input.color ?? nextColor(db);
|
|
2292
|
+
db.prepare(
|
|
2293
|
+
`INSERT INTO goals (id, name, description, color, created_at)
|
|
2294
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
2295
|
+
).run(id, input.name, input.description ?? null, color, ts2);
|
|
2296
|
+
return { id, name: input.name, description: input.description, color, createdAt: ts2 };
|
|
2297
|
+
}
|
|
2298
|
+
function listGoals(db) {
|
|
2299
|
+
const rows = db.prepare("SELECT * FROM goals ORDER BY created_at ASC").all();
|
|
2300
|
+
return rows.map(rowToGoal);
|
|
2301
|
+
}
|
|
2302
|
+
function deleteGoal(db, id) {
|
|
2303
|
+
db.prepare(
|
|
2304
|
+
`UPDATE sessions SET current_goal_id = NULL WHERE current_goal_id = ?`
|
|
2305
|
+
).run(id);
|
|
2306
|
+
db.prepare(`UPDATE missions SET goal_id = NULL WHERE goal_id = ?`).run(id);
|
|
2307
|
+
const res = db.prepare("DELETE FROM goals WHERE id = ?").run(id);
|
|
2308
|
+
return res.changes > 0;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
1898
2311
|
// ../server/src/state/context.ts
|
|
1899
2312
|
var MISSIONS_FOR_HANDOFF = 3;
|
|
1900
2313
|
var DEFAULT_ASKS = {
|
|
@@ -2101,7 +2514,7 @@ function recordSkillInstall(db, skillId, projectId) {
|
|
|
2101
2514
|
}
|
|
2102
2515
|
|
|
2103
2516
|
// ../server/src/state/galaxy.ts
|
|
2104
|
-
import { nanoid as
|
|
2517
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2105
2518
|
function exportManifest(db, opts = {}) {
|
|
2106
2519
|
const advisors2 = listAdvisors(db);
|
|
2107
2520
|
const skills2 = listSkills(db);
|
|
@@ -2149,7 +2562,7 @@ function importManifest(db, manifest, sourceUrl) {
|
|
|
2149
2562
|
db.prepare(
|
|
2150
2563
|
`INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
|
|
2151
2564
|
VALUES (?, ?, ?, ?)`
|
|
2152
|
-
).run(
|
|
2565
|
+
).run(nanoid6(), sourceUrl ?? null, JSON.stringify(manifest), now());
|
|
2153
2566
|
return {
|
|
2154
2567
|
advisorsEnabled: enabled,
|
|
2155
2568
|
advisorsDisabled: disabled,
|
|
@@ -2210,7 +2623,7 @@ function snapshotExport(db, manifest) {
|
|
|
2210
2623
|
return rowToVersion(existing);
|
|
2211
2624
|
}
|
|
2212
2625
|
}
|
|
2213
|
-
const id =
|
|
2626
|
+
const id = nanoid6();
|
|
2214
2627
|
const ts2 = now();
|
|
2215
2628
|
const ordinal = (last?.ordinal ?? 0) + 1;
|
|
2216
2629
|
db.prepare(
|
|
@@ -2288,6 +2701,29 @@ function diffManifests(a, b) {
|
|
|
2288
2701
|
};
|
|
2289
2702
|
}
|
|
2290
2703
|
|
|
2704
|
+
// ../shared/src/pricing.ts
|
|
2705
|
+
var MODEL_PRICING = {
|
|
2706
|
+
opus: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
2707
|
+
sonnet: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
2708
|
+
haiku: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }
|
|
2709
|
+
};
|
|
2710
|
+
function pricingFor(model) {
|
|
2711
|
+
const m = (model ?? "").toLowerCase();
|
|
2712
|
+
if (m.includes("opus")) return MODEL_PRICING.opus;
|
|
2713
|
+
if (m.includes("haiku")) return MODEL_PRICING.haiku;
|
|
2714
|
+
return MODEL_PRICING.sonnet;
|
|
2715
|
+
}
|
|
2716
|
+
function costForUsage(model, usage) {
|
|
2717
|
+
if (!usage) return 0;
|
|
2718
|
+
const p = pricingFor(model);
|
|
2719
|
+
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;
|
|
2720
|
+
return cost;
|
|
2721
|
+
}
|
|
2722
|
+
function totalTokens(usage) {
|
|
2723
|
+
if (!usage) return 0;
|
|
2724
|
+
return (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2291
2727
|
// ../server/src/cloud.ts
|
|
2292
2728
|
var RegistryClient = class {
|
|
2293
2729
|
constructor(baseUrl = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
|
|
@@ -2362,9 +2798,44 @@ var RegistryClient = class {
|
|
|
2362
2798
|
};
|
|
2363
2799
|
|
|
2364
2800
|
// ../server/src/http.ts
|
|
2801
|
+
function isAgentViewVersion(version) {
|
|
2802
|
+
if (!version) return false;
|
|
2803
|
+
const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2804
|
+
if (!match) return false;
|
|
2805
|
+
const major = Number(match[1]);
|
|
2806
|
+
const minor = Number(match[2]);
|
|
2807
|
+
const patch = Number(match[3]);
|
|
2808
|
+
if (major > 2) return true;
|
|
2809
|
+
if (major < 2) return false;
|
|
2810
|
+
if (minor > 1) return true;
|
|
2811
|
+
if (minor < 1) return false;
|
|
2812
|
+
return patch >= 139;
|
|
2813
|
+
}
|
|
2365
2814
|
function createHttpApp(opts) {
|
|
2366
2815
|
const app = new Hono();
|
|
2367
|
-
app.use(
|
|
2816
|
+
app.use(
|
|
2817
|
+
"*",
|
|
2818
|
+
cors({
|
|
2819
|
+
origin: [
|
|
2820
|
+
"http://127.0.0.1:4242",
|
|
2821
|
+
"http://localhost:4242",
|
|
2822
|
+
"http://127.0.0.1:4243",
|
|
2823
|
+
"http://localhost:4243"
|
|
2824
|
+
]
|
|
2825
|
+
})
|
|
2826
|
+
);
|
|
2827
|
+
if (opts.token) {
|
|
2828
|
+
const expected = opts.token;
|
|
2829
|
+
const paths = ["/events", "/events/permission"];
|
|
2830
|
+
for (const p of paths) {
|
|
2831
|
+
app.use(p, async (c, next) => {
|
|
2832
|
+
if (c.req.header("x-solix-token") !== expected) {
|
|
2833
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
2834
|
+
}
|
|
2835
|
+
await next();
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2368
2839
|
const registry = new RegistryClient();
|
|
2369
2840
|
app.get(
|
|
2370
2841
|
"/api/health",
|
|
@@ -2388,6 +2859,18 @@ function createHttpApp(opts) {
|
|
|
2388
2859
|
}
|
|
2389
2860
|
return c.json({ ok: true });
|
|
2390
2861
|
});
|
|
2862
|
+
app.post("/events/permission", async (c) => {
|
|
2863
|
+
let body = null;
|
|
2864
|
+
try {
|
|
2865
|
+
body = await c.req.json();
|
|
2866
|
+
} catch {
|
|
2867
|
+
return c.json({ decision: "allow" });
|
|
2868
|
+
}
|
|
2869
|
+
if (!body || !body.event) return c.json({ decision: "allow" });
|
|
2870
|
+
const result = await opts.router.requestPermission(body);
|
|
2871
|
+
const decision = result.timedOut ? "timeout" : result.approved ? "allow" : "deny";
|
|
2872
|
+
return c.json({ decision });
|
|
2873
|
+
});
|
|
2391
2874
|
app.get("/api/projects", (c) => c.json(listProjects(opts.db)));
|
|
2392
2875
|
app.get("/api/projects/:id/sessions", (c) => {
|
|
2393
2876
|
const id = c.req.param("id");
|
|
@@ -2468,11 +2951,11 @@ function createHttpApp(opts) {
|
|
|
2468
2951
|
return c.json({ ...a, agentMd: readAdvisorAgentMd(a) });
|
|
2469
2952
|
});
|
|
2470
2953
|
app.post("/api/advisors/:id/enable", (c) => {
|
|
2471
|
-
const a = setAdvisorEnabled(
|
|
2954
|
+
const a = opts.router.setAdvisorEnabled(c.req.param("id"), true);
|
|
2472
2955
|
return c.json({ ok: Boolean(a), advisor: a });
|
|
2473
2956
|
});
|
|
2474
2957
|
app.post("/api/advisors/:id/disable", (c) => {
|
|
2475
|
-
const a = setAdvisorEnabled(
|
|
2958
|
+
const a = opts.router.setAdvisorEnabled(c.req.param("id"), false);
|
|
2476
2959
|
return c.json({ ok: Boolean(a), advisor: a });
|
|
2477
2960
|
});
|
|
2478
2961
|
app.post("/api/advisors/:id/pin", (c) => {
|
|
@@ -2628,6 +3111,52 @@ function createHttpApp(opts) {
|
|
|
2628
3111
|
return c.json({ ok: true });
|
|
2629
3112
|
});
|
|
2630
3113
|
app.get("/api/wrappers", (c) => c.json(listWrappers()));
|
|
3114
|
+
app.get("/api/schedules", (c) => c.json(listSchedules(opts.db)));
|
|
3115
|
+
app.post("/api/schedules", async (c) => {
|
|
3116
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3117
|
+
if (!body.cwd || !body.prompt || !body.cadence) {
|
|
3118
|
+
return c.json({ error: "cwd, prompt, cadence required" }, 400);
|
|
3119
|
+
}
|
|
3120
|
+
const schedule2 = createSchedule(opts.db, {
|
|
3121
|
+
cwd: body.cwd,
|
|
3122
|
+
prompt: body.prompt,
|
|
3123
|
+
cadence: body.cadence,
|
|
3124
|
+
name: body.name
|
|
3125
|
+
});
|
|
3126
|
+
opts.router.broadcastScheduleUpsert(schedule2);
|
|
3127
|
+
return c.json(schedule2);
|
|
3128
|
+
});
|
|
3129
|
+
app.post("/api/schedules/:id/toggle", async (c) => {
|
|
3130
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3131
|
+
const s = setScheduleEnabled(opts.db, c.req.param("id"), Boolean(body.enabled));
|
|
3132
|
+
if (!s) return c.json({ error: "not found" }, 404);
|
|
3133
|
+
opts.router.broadcastScheduleUpsert(s);
|
|
3134
|
+
return c.json(s);
|
|
3135
|
+
});
|
|
3136
|
+
app.delete("/api/schedules/:id", (c) => {
|
|
3137
|
+
const id = c.req.param("id");
|
|
3138
|
+
const ok = deleteSchedule(opts.db, id);
|
|
3139
|
+
if (ok) opts.router.broadcastScheduleRemove(id);
|
|
3140
|
+
return c.json({ ok });
|
|
3141
|
+
});
|
|
3142
|
+
app.get("/api/goals", (c) => c.json(listGoals(opts.db)));
|
|
3143
|
+
app.post("/api/goals", async (c) => {
|
|
3144
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3145
|
+
if (!body.name) return c.json({ error: "name required" }, 400);
|
|
3146
|
+
const goal2 = createGoal(opts.db, {
|
|
3147
|
+
name: body.name,
|
|
3148
|
+
description: body.description,
|
|
3149
|
+
color: body.color
|
|
3150
|
+
});
|
|
3151
|
+
opts.router.broadcastGoalUpsert(goal2);
|
|
3152
|
+
return c.json(goal2);
|
|
3153
|
+
});
|
|
3154
|
+
app.delete("/api/goals/:id", (c) => {
|
|
3155
|
+
const id = c.req.param("id");
|
|
3156
|
+
const ok = deleteGoal(opts.db, id);
|
|
3157
|
+
if (ok) opts.router.broadcastGoalRemove(id);
|
|
3158
|
+
return c.json({ ok });
|
|
3159
|
+
});
|
|
2631
3160
|
let preflightCache = null;
|
|
2632
3161
|
app.get("/api/system/preflight", (c) => {
|
|
2633
3162
|
if (preflightCache) return c.json(preflightCache);
|
|
@@ -2637,15 +3166,17 @@ function createHttpApp(opts) {
|
|
|
2637
3166
|
encoding: "utf8"
|
|
2638
3167
|
});
|
|
2639
3168
|
if (res.status === 0) {
|
|
3169
|
+
const version = (res.stdout ?? "").trim() || void 0;
|
|
2640
3170
|
preflightCache = {
|
|
2641
3171
|
claudeAvailable: true,
|
|
2642
|
-
version
|
|
3172
|
+
version,
|
|
3173
|
+
agentViewAvailable: isAgentViewVersion(version)
|
|
2643
3174
|
};
|
|
2644
3175
|
} else {
|
|
2645
|
-
preflightCache = { claudeAvailable: false };
|
|
3176
|
+
preflightCache = { claudeAvailable: false, agentViewAvailable: false };
|
|
2646
3177
|
}
|
|
2647
3178
|
} catch {
|
|
2648
|
-
preflightCache = { claudeAvailable: false };
|
|
3179
|
+
preflightCache = { claudeAvailable: false, agentViewAvailable: false };
|
|
2649
3180
|
}
|
|
2650
3181
|
return c.json(preflightCache);
|
|
2651
3182
|
});
|
|
@@ -2767,7 +3298,7 @@ import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
|
2767
3298
|
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
|
|
2768
3299
|
import { homedir as homedir8 } from "os";
|
|
2769
3300
|
import { basename as basename3, join as join12 } from "path";
|
|
2770
|
-
import { nanoid as
|
|
3301
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
2771
3302
|
function ensureWorktree(opts) {
|
|
2772
3303
|
const repoRoot = (() => {
|
|
2773
3304
|
const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -2809,6 +3340,53 @@ function ensureWorktree(opts) {
|
|
|
2809
3340
|
return { path, created: true };
|
|
2810
3341
|
}
|
|
2811
3342
|
var FAKE_CLAUDE = process.env.SOLIX_FAKE_CLAUDE === "1";
|
|
3343
|
+
function buildSpawnEnv() {
|
|
3344
|
+
const scrub = process.env.SOLIX_ENV_SCRUB === "1" || (process.env.SOLIX_SANDBOX_CMD ?? "").trim() !== "";
|
|
3345
|
+
if (!scrub) return void 0;
|
|
3346
|
+
const allow = [
|
|
3347
|
+
"PATH",
|
|
3348
|
+
"HOME",
|
|
3349
|
+
"USER",
|
|
3350
|
+
"LOGNAME",
|
|
3351
|
+
"SHELL",
|
|
3352
|
+
"LANG",
|
|
3353
|
+
"LC_ALL",
|
|
3354
|
+
"TERM",
|
|
3355
|
+
"TMPDIR",
|
|
3356
|
+
"TZ",
|
|
3357
|
+
"HTTP_PROXY",
|
|
3358
|
+
"HTTPS_PROXY",
|
|
3359
|
+
"NO_PROXY",
|
|
3360
|
+
"http_proxy",
|
|
3361
|
+
"https_proxy",
|
|
3362
|
+
"no_proxy",
|
|
3363
|
+
"SOLIX_HOME",
|
|
3364
|
+
"SOLIX_HOST",
|
|
3365
|
+
"SOLIX_PORT",
|
|
3366
|
+
"SOLIX_GATE_ENABLED",
|
|
3367
|
+
"SOLIX_GATE_POLICY",
|
|
3368
|
+
"SOLIX_GATE_TIMEOUT"
|
|
3369
|
+
];
|
|
3370
|
+
const extra = (process.env.SOLIX_ENV_PASSTHROUGH ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3371
|
+
const keep = /* @__PURE__ */ new Set([...allow, ...extra]);
|
|
3372
|
+
const env = {};
|
|
3373
|
+
for (const k of keep) {
|
|
3374
|
+
if (process.env[k] !== void 0) env[k] = process.env[k];
|
|
3375
|
+
}
|
|
3376
|
+
for (const k of Object.keys(process.env)) {
|
|
3377
|
+
if (k.startsWith("ANTHROPIC_") || k.startsWith("CLAUDE_")) {
|
|
3378
|
+
env[k] = process.env[k];
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
return env;
|
|
3382
|
+
}
|
|
3383
|
+
function sandboxWrap(file, args) {
|
|
3384
|
+
const cmd = (process.env.SOLIX_SANDBOX_CMD ?? "").trim();
|
|
3385
|
+
if (!cmd) return { file, args };
|
|
3386
|
+
const parts = cmd.split(/\s+/);
|
|
3387
|
+
const bin = parts[0];
|
|
3388
|
+
return { file: bin, args: [...parts.slice(1), file, ...args] };
|
|
3389
|
+
}
|
|
2812
3390
|
var Launcher = class {
|
|
2813
3391
|
constructor(db, broadcaster) {
|
|
2814
3392
|
this.db = db;
|
|
@@ -2838,15 +3416,17 @@ var Launcher = class {
|
|
|
2838
3416
|
return this.pinSynthetic(advisor.id, advisor.codename, cwd);
|
|
2839
3417
|
}
|
|
2840
3418
|
try {
|
|
2841
|
-
const
|
|
2842
|
-
"
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3419
|
+
const spawnSpec = sandboxWrap("claude", [
|
|
3420
|
+
"--agent",
|
|
3421
|
+
advisor.id,
|
|
3422
|
+
"--no-tty"
|
|
3423
|
+
]);
|
|
3424
|
+
const child = spawn(spawnSpec.file, spawnSpec.args, {
|
|
3425
|
+
cwd,
|
|
3426
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3427
|
+
detached: false,
|
|
3428
|
+
env: buildSpawnEnv()
|
|
3429
|
+
});
|
|
2850
3430
|
const pid = child.pid;
|
|
2851
3431
|
if (!pid) {
|
|
2852
3432
|
this.broadcaster.broadcast({
|
|
@@ -2885,7 +3465,7 @@ var Launcher = class {
|
|
|
2885
3465
|
}
|
|
2886
3466
|
pinSynthetic(advisorId, codename, cwd) {
|
|
2887
3467
|
const project = ensureProject(this.db, cwd);
|
|
2888
|
-
const sessionId = `advisor-${advisorId}-${
|
|
3468
|
+
const sessionId = `advisor-${advisorId}-${nanoid7(6)}`;
|
|
2889
3469
|
const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
|
|
2890
3470
|
const session = upsertSession(this.db, {
|
|
2891
3471
|
id: sessionId,
|
|
@@ -2961,6 +3541,14 @@ var Launcher = class {
|
|
|
2961
3541
|
*/
|
|
2962
3542
|
launch(opts) {
|
|
2963
3543
|
if (!opts.initialPrompt.trim()) return { ok: false };
|
|
3544
|
+
if (opts.useAgentView) {
|
|
3545
|
+
return this.dispatchAgentView({
|
|
3546
|
+
cwd: opts.cwd,
|
|
3547
|
+
model: opts.model,
|
|
3548
|
+
initialPrompt: opts.initialPrompt,
|
|
3549
|
+
agentName: opts.agentName
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
2964
3552
|
let spawnCwd = opts.cwd;
|
|
2965
3553
|
let worktreePath;
|
|
2966
3554
|
if (opts.worktreeBranch?.trim()) {
|
|
@@ -2991,7 +3579,9 @@ var Launcher = class {
|
|
|
2991
3579
|
cwd: spawnCwd,
|
|
2992
3580
|
model: opts.model,
|
|
2993
3581
|
initialPrompt: opts.initialPrompt,
|
|
2994
|
-
worktreePath
|
|
3582
|
+
worktreePath,
|
|
3583
|
+
budgetUsd: opts.budgetUsd,
|
|
3584
|
+
goalId: opts.goalId
|
|
2995
3585
|
});
|
|
2996
3586
|
}
|
|
2997
3587
|
if (!existsSync9(spawnCwd)) {
|
|
@@ -3005,14 +3595,74 @@ var Launcher = class {
|
|
|
3005
3595
|
const args = ["--print"];
|
|
3006
3596
|
if (opts.model) args.push("--model", String(opts.model));
|
|
3007
3597
|
args.push(opts.initialPrompt);
|
|
3008
|
-
const sessionId = `task-${
|
|
3598
|
+
const sessionId = `task-${nanoid7(8)}`;
|
|
3009
3599
|
return this.spawnPrint({
|
|
3010
3600
|
sessionId,
|
|
3011
3601
|
cwd: spawnCwd,
|
|
3012
3602
|
args,
|
|
3013
3603
|
isFollowUp: false,
|
|
3014
|
-
worktreePath
|
|
3604
|
+
worktreePath,
|
|
3605
|
+
budgetUsd: opts.budgetUsd,
|
|
3606
|
+
goalId: opts.goalId
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Sprint L: dispatch via Anthropic's Agent View daemon. Runs
|
|
3611
|
+
* `claude --bg "<prompt>"` so the session is hosted by the
|
|
3612
|
+
* supervisor and picked up by Solix's filesystem watcher within ~1s.
|
|
3613
|
+
* Returns the short id parsed from claude's output line:
|
|
3614
|
+
* backgrounded · 7c5dcf5d
|
|
3615
|
+
*/
|
|
3616
|
+
dispatchAgentView(opts) {
|
|
3617
|
+
if (FAKE_CLAUDE) {
|
|
3618
|
+
this.broadcaster.broadcast({
|
|
3619
|
+
type: "toast",
|
|
3620
|
+
level: "info",
|
|
3621
|
+
message: "(SOLIX_FAKE_CLAUDE=1) Agent View dispatch skipped"
|
|
3622
|
+
});
|
|
3623
|
+
return { ok: true };
|
|
3624
|
+
}
|
|
3625
|
+
if (!existsSync9(opts.cwd)) {
|
|
3626
|
+
this.broadcaster.broadcast({
|
|
3627
|
+
type: "toast",
|
|
3628
|
+
level: "error",
|
|
3629
|
+
message: `Agent View dispatch failed: cwd does not exist (${opts.cwd})`
|
|
3630
|
+
});
|
|
3631
|
+
return { ok: false };
|
|
3632
|
+
}
|
|
3633
|
+
const args = [];
|
|
3634
|
+
if (opts.agentName) args.push("--agent", opts.agentName);
|
|
3635
|
+
if (opts.model) args.push("--model", String(opts.model));
|
|
3636
|
+
args.push("--bg", opts.initialPrompt);
|
|
3637
|
+
let child;
|
|
3638
|
+
try {
|
|
3639
|
+
const spawnSpec = sandboxWrap("claude", args);
|
|
3640
|
+
child = spawn(spawnSpec.file, spawnSpec.args, {
|
|
3641
|
+
cwd: opts.cwd,
|
|
3642
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3643
|
+
detached: false,
|
|
3644
|
+
env: buildSpawnEnv()
|
|
3645
|
+
});
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
this.broadcaster.broadcast({
|
|
3648
|
+
type: "toast",
|
|
3649
|
+
level: "error",
|
|
3650
|
+
message: `claude --bg spawn failed: ${err.message}`
|
|
3651
|
+
});
|
|
3652
|
+
return { ok: false };
|
|
3653
|
+
}
|
|
3654
|
+
let stdout = "";
|
|
3655
|
+
child.stdout?.setEncoding("utf8").on("data", (c) => stdout += c);
|
|
3656
|
+
child.on("exit", () => {
|
|
3657
|
+
const match = stdout.match(/backgrounded[^a-z0-9]+([a-f0-9]{6,16})/i);
|
|
3658
|
+
const shortId = match?.[1];
|
|
3659
|
+
this.broadcaster.broadcast({
|
|
3660
|
+
type: "toast",
|
|
3661
|
+
level: "info",
|
|
3662
|
+
message: shortId ? `Dispatched to Agent View \xB7 ${shortId}` : "Dispatched to Agent View"
|
|
3663
|
+
});
|
|
3015
3664
|
});
|
|
3665
|
+
return { ok: true };
|
|
3016
3666
|
}
|
|
3017
3667
|
sendPromptToInternal(sessionId, text) {
|
|
3018
3668
|
if (!text.trim()) return false;
|
|
@@ -3035,6 +3685,15 @@ var Launcher = class {
|
|
|
3035
3685
|
});
|
|
3036
3686
|
return false;
|
|
3037
3687
|
}
|
|
3688
|
+
const full = getSession(this.db, sessionId);
|
|
3689
|
+
if (full?.budgetUsd != null && full.costUsd >= full.budgetUsd) {
|
|
3690
|
+
this.broadcaster.broadcast({
|
|
3691
|
+
type: "toast",
|
|
3692
|
+
level: "warn",
|
|
3693
|
+
message: `Budget reached for ${full.name ?? sessionId.slice(0, 8)} ($${full.costUsd.toFixed(2)}/$${full.budgetUsd.toFixed(2)}). Raise the cap to continue.`
|
|
3694
|
+
});
|
|
3695
|
+
return false;
|
|
3696
|
+
}
|
|
3038
3697
|
if (FAKE_CLAUDE) {
|
|
3039
3698
|
this.broadcaster.broadcast({
|
|
3040
3699
|
type: "chat_delta",
|
|
@@ -3066,13 +3725,29 @@ var Launcher = class {
|
|
|
3066
3725
|
}
|
|
3067
3726
|
return void 0;
|
|
3068
3727
|
}
|
|
3728
|
+
/** Sprint M — budget cap recorded at launch for a cwd, if any. */
|
|
3729
|
+
budgetForInternalCwd(cwd) {
|
|
3730
|
+
for (const rec of this.internalTasks.values()) {
|
|
3731
|
+
if (rec.cwd === cwd && rec.budgetUsd != null) return rec.budgetUsd;
|
|
3732
|
+
}
|
|
3733
|
+
return void 0;
|
|
3734
|
+
}
|
|
3735
|
+
/** Sprint M — goal recorded at launch for a cwd, if any. */
|
|
3736
|
+
goalForInternalCwd(cwd) {
|
|
3737
|
+
for (const rec of this.internalTasks.values()) {
|
|
3738
|
+
if (rec.cwd === cwd && rec.goalId) return rec.goalId;
|
|
3739
|
+
}
|
|
3740
|
+
return void 0;
|
|
3741
|
+
}
|
|
3069
3742
|
spawnPrint(opts) {
|
|
3070
3743
|
let child;
|
|
3071
3744
|
try {
|
|
3072
|
-
|
|
3745
|
+
const spawnSpec = sandboxWrap("claude", opts.args);
|
|
3746
|
+
child = spawn(spawnSpec.file, spawnSpec.args, {
|
|
3073
3747
|
cwd: opts.cwd,
|
|
3074
3748
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3075
|
-
detached: false
|
|
3749
|
+
detached: false,
|
|
3750
|
+
env: buildSpawnEnv()
|
|
3076
3751
|
});
|
|
3077
3752
|
} catch (err) {
|
|
3078
3753
|
this.broadcaster.broadcast({
|
|
@@ -3087,7 +3762,9 @@ var Launcher = class {
|
|
|
3087
3762
|
if (!opts.isFollowUp) {
|
|
3088
3763
|
this.internalTasks.set(opts.sessionId, {
|
|
3089
3764
|
cwd: opts.cwd,
|
|
3090
|
-
worktreePath: opts.worktreePath
|
|
3765
|
+
worktreePath: opts.worktreePath,
|
|
3766
|
+
budgetUsd: opts.budgetUsd,
|
|
3767
|
+
goalId: opts.goalId
|
|
3091
3768
|
});
|
|
3092
3769
|
}
|
|
3093
3770
|
let stdout = "";
|
|
@@ -3133,7 +3810,7 @@ var Launcher = class {
|
|
|
3133
3810
|
}
|
|
3134
3811
|
launchSynthetic(opts) {
|
|
3135
3812
|
const project = ensureProject(this.db, opts.cwd);
|
|
3136
|
-
const sessionId = `task-${
|
|
3813
|
+
const sessionId = `task-${nanoid7(8)}`;
|
|
3137
3814
|
const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
|
|
3138
3815
|
upsertSession(this.db, {
|
|
3139
3816
|
id: sessionId,
|
|
@@ -3144,6 +3821,8 @@ var Launcher = class {
|
|
|
3144
3821
|
model: opts.model ?? "sonnet",
|
|
3145
3822
|
worktreePath: opts.worktreePath
|
|
3146
3823
|
});
|
|
3824
|
+
if (opts.budgetUsd != null) setSessionBudget(this.db, sessionId, opts.budgetUsd);
|
|
3825
|
+
if (opts.goalId) setSessionGoal(this.db, sessionId, opts.goalId);
|
|
3147
3826
|
const active = setSessionStatus(this.db, sessionId, "active");
|
|
3148
3827
|
if (active)
|
|
3149
3828
|
this.broadcaster.broadcast({ type: "session_upsert", session: active });
|
|
@@ -3179,12 +3858,12 @@ var Launcher = class {
|
|
|
3179
3858
|
};
|
|
3180
3859
|
|
|
3181
3860
|
// ../server/src/router.ts
|
|
3182
|
-
import { nanoid as
|
|
3861
|
+
import { nanoid as nanoid9 } from "nanoid";
|
|
3183
3862
|
|
|
3184
3863
|
// ../server/src/state/toolcalls.ts
|
|
3185
|
-
import { nanoid as
|
|
3864
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
3186
3865
|
function recordToolCall(db, input) {
|
|
3187
|
-
const id =
|
|
3866
|
+
const id = nanoid8();
|
|
3188
3867
|
const ts2 = now();
|
|
3189
3868
|
const status = input.status ?? "running";
|
|
3190
3869
|
db.prepare(
|
|
@@ -3286,6 +3965,8 @@ var EventRouter = class {
|
|
|
3286
3965
|
const sessionId = this.extractSessionId(event);
|
|
3287
3966
|
const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
|
|
3288
3967
|
const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
|
|
3968
|
+
const launchBudget = this.launcher?.budgetForInternalCwd(event.cwd);
|
|
3969
|
+
const launchGoal = this.launcher?.goalForInternalCwd(event.cwd);
|
|
3289
3970
|
const wrapper = claimWrapperForCwd(event.cwd);
|
|
3290
3971
|
const session = upsertSession(this.db, {
|
|
3291
3972
|
id: sessionId,
|
|
@@ -3301,7 +3982,14 @@ var EventRouter = class {
|
|
|
3301
3982
|
wrapperSocketPath: wrapper?.socketPath
|
|
3302
3983
|
});
|
|
3303
3984
|
if (wrapper) bindWrapperToSession(wrapper.wrapperId, session.id);
|
|
3304
|
-
|
|
3985
|
+
let enriched = session;
|
|
3986
|
+
if (launchBudget != null) {
|
|
3987
|
+
enriched = setSessionBudget(this.db, session.id, launchBudget) ?? enriched;
|
|
3988
|
+
}
|
|
3989
|
+
if (launchGoal) {
|
|
3990
|
+
enriched = setSessionGoal(this.db, session.id, launchGoal) ?? enriched;
|
|
3991
|
+
}
|
|
3992
|
+
this.broadcaster.broadcast({ type: "session_upsert", session: enriched });
|
|
3305
3993
|
if (!session.parentSessionId) {
|
|
3306
3994
|
this.transcripts?.startWatching(sessionId, event.cwd);
|
|
3307
3995
|
}
|
|
@@ -3322,7 +4010,7 @@ var EventRouter = class {
|
|
|
3322
4010
|
model: this.extractModel(event)
|
|
3323
4011
|
});
|
|
3324
4012
|
}
|
|
3325
|
-
const mission = startMission(this.db, sessionId, prompt);
|
|
4013
|
+
const mission = startMission(this.db, sessionId, prompt, session.currentGoalId);
|
|
3326
4014
|
const updated = setSessionMission(this.db, sessionId, mission.id);
|
|
3327
4015
|
const active = updated ? setSessionStatus(this.db, sessionId, "active") : null;
|
|
3328
4016
|
this.broadcaster.broadcast({ type: "mission_upsert", mission });
|
|
@@ -3366,7 +4054,7 @@ var EventRouter = class {
|
|
|
3366
4054
|
const parentSessionId = this.extractSessionId(event);
|
|
3367
4055
|
const parent = getSession(this.db, parentSessionId);
|
|
3368
4056
|
if (!parent) return;
|
|
3369
|
-
const subId =
|
|
4057
|
+
const subId = nanoid9();
|
|
3370
4058
|
const sub = upsertSession(this.db, {
|
|
3371
4059
|
id: subId,
|
|
3372
4060
|
pid: event.pid,
|
|
@@ -3443,7 +4131,7 @@ var EventRouter = class {
|
|
|
3443
4131
|
const p = event.payload;
|
|
3444
4132
|
const message = typeof p.message === "string" ? p.message : "Permission requested";
|
|
3445
4133
|
const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
|
|
3446
|
-
const requestId =
|
|
4134
|
+
const requestId = nanoid9();
|
|
3447
4135
|
this.permissions.set(requestId, {
|
|
3448
4136
|
requestId,
|
|
3449
4137
|
sessionId,
|
|
@@ -3471,6 +4159,90 @@ var EventRouter = class {
|
|
|
3471
4159
|
message: `Permission requested: ${message}`
|
|
3472
4160
|
});
|
|
3473
4161
|
}
|
|
4162
|
+
/**
|
|
4163
|
+
* Synchronous human-in-the-loop gate for the blocking PreToolUse path.
|
|
4164
|
+
* Records the tool call (so the comet/timeline visuals still fire — the gate
|
|
4165
|
+
* hook no longer POSTs to /events), broadcasts a permission_request, and
|
|
4166
|
+
* returns a promise that resolves when a human answers via `permission_response`
|
|
4167
|
+
* (reused unchanged) or when the server-side timeout fires.
|
|
4168
|
+
*/
|
|
4169
|
+
requestPermission(event) {
|
|
4170
|
+
const sessionId = this.extractSessionId(event);
|
|
4171
|
+
const p = event.payload;
|
|
4172
|
+
const { tool, args } = this.describeGatedTool(event.event, p);
|
|
4173
|
+
const session = getSession(this.db, sessionId);
|
|
4174
|
+
if (session) {
|
|
4175
|
+
const toolCall = recordToolCall(this.db, {
|
|
4176
|
+
sessionId,
|
|
4177
|
+
missionId: session.currentMissionId,
|
|
4178
|
+
tool,
|
|
4179
|
+
args
|
|
4180
|
+
});
|
|
4181
|
+
if (event.event === "pre_tool_file" && session.currentMissionId && typeof args.file_path === "string" && args.file_path) {
|
|
4182
|
+
addTouchedFile(this.db, session.currentMissionId, args.file_path);
|
|
4183
|
+
}
|
|
4184
|
+
this.broadcaster.broadcast({ type: "tool_call", toolCall });
|
|
4185
|
+
}
|
|
4186
|
+
const requestId = nanoid9();
|
|
4187
|
+
const timeoutMs = Number(process.env.SOLIX_GATE_TIMEOUT_MS ?? 3e5);
|
|
4188
|
+
return new Promise((resolve4) => {
|
|
4189
|
+
const timer = setTimeout(() => {
|
|
4190
|
+
const pending = this.permissions.get(requestId);
|
|
4191
|
+
if (!pending) return;
|
|
4192
|
+
this.permissions.delete(requestId);
|
|
4193
|
+
const s = setSessionStatus(this.db, sessionId, "active");
|
|
4194
|
+
if (s) this.broadcaster.broadcast({ type: "session_upsert", session: s });
|
|
4195
|
+
resolve4({ approved: false, timedOut: true });
|
|
4196
|
+
}, timeoutMs);
|
|
4197
|
+
this.permissions.set(requestId, {
|
|
4198
|
+
requestId,
|
|
4199
|
+
sessionId,
|
|
4200
|
+
tool,
|
|
4201
|
+
args,
|
|
4202
|
+
createdAt: Date.now(),
|
|
4203
|
+
resolve: (approved) => resolve4({ approved, timedOut: false }),
|
|
4204
|
+
timer
|
|
4205
|
+
});
|
|
4206
|
+
const updated = setSessionStatus(
|
|
4207
|
+
this.db,
|
|
4208
|
+
sessionId,
|
|
4209
|
+
"awaiting_permission"
|
|
4210
|
+
);
|
|
4211
|
+
if (updated) {
|
|
4212
|
+
this.broadcaster.broadcast({ type: "session_upsert", session: updated });
|
|
4213
|
+
}
|
|
4214
|
+
this.broadcaster.broadcast({
|
|
4215
|
+
type: "permission_request",
|
|
4216
|
+
sessionId,
|
|
4217
|
+
tool,
|
|
4218
|
+
args,
|
|
4219
|
+
requestId
|
|
4220
|
+
});
|
|
4221
|
+
this.broadcaster.broadcast({
|
|
4222
|
+
type: "toast",
|
|
4223
|
+
level: "warn",
|
|
4224
|
+
message: `Approval requested: ${tool}`
|
|
4225
|
+
});
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
describeGatedTool(eventName, p) {
|
|
4229
|
+
const toolInput = p.tool_input ?? {};
|
|
4230
|
+
if (eventName === "pre_tool_bash") {
|
|
4231
|
+
const command = typeof p.command === "string" ? p.command : typeof toolInput.command === "string" ? toolInput.command : "";
|
|
4232
|
+
return { tool: "Bash", args: { command } };
|
|
4233
|
+
}
|
|
4234
|
+
if (eventName === "pre_tool_file") {
|
|
4235
|
+
const tool2 = typeof p.tool_name === "string" ? p.tool_name : "File";
|
|
4236
|
+
const filePath = typeof p.file_path === "string" ? p.file_path : typeof toolInput.file_path === "string" ? toolInput.file_path : "";
|
|
4237
|
+
return { tool: tool2, args: { file_path: filePath } };
|
|
4238
|
+
}
|
|
4239
|
+
if (eventName === "pre_tool_task") {
|
|
4240
|
+
const tool2 = typeof p.tool_name === "string" ? p.tool_name : "Task";
|
|
4241
|
+
return { tool: tool2, args: toolInput };
|
|
4242
|
+
}
|
|
4243
|
+
const tool = typeof p.tool_name === "string" ? p.tool_name : "tool";
|
|
4244
|
+
return { tool, args: toolInput };
|
|
4245
|
+
}
|
|
3474
4246
|
invokeAdvisor(advisorId, targetSessionId, prompt) {
|
|
3475
4247
|
const advisor = getAdvisor(this.db, advisorId);
|
|
3476
4248
|
if (!advisor) return { ok: false };
|
|
@@ -3523,6 +4295,20 @@ var EventRouter = class {
|
|
|
3523
4295
|
}
|
|
3524
4296
|
return ok;
|
|
3525
4297
|
}
|
|
4298
|
+
/** Sprint N — enable/disable an advisor and broadcast the change so open
|
|
4299
|
+
* browsers (and the CLI path) update live. Returns the updated advisor. */
|
|
4300
|
+
setAdvisorEnabled(advisorId, enabled) {
|
|
4301
|
+
const advisor = setAdvisorEnabled(this.db, advisorId, enabled);
|
|
4302
|
+
if (advisor) {
|
|
4303
|
+
this.broadcaster.broadcast({ type: "advisor_upsert", advisor });
|
|
4304
|
+
this.broadcaster.broadcast({
|
|
4305
|
+
type: "toast",
|
|
4306
|
+
level: "info",
|
|
4307
|
+
message: `${advisor.codename} ${enabled ? "added to crew" : "disabled"}`
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
return advisor;
|
|
4311
|
+
}
|
|
3526
4312
|
unpinAdvisor(advisorId) {
|
|
3527
4313
|
if (this.launcher) {
|
|
3528
4314
|
this.launcher.unpin(advisorId);
|
|
@@ -3544,6 +4330,8 @@ var EventRouter = class {
|
|
|
3544
4330
|
const pending = this.permissions.get(requestId);
|
|
3545
4331
|
if (!pending) return false;
|
|
3546
4332
|
this.permissions.delete(requestId);
|
|
4333
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
4334
|
+
if (pending.resolve) pending.resolve(approved);
|
|
3547
4335
|
const status = approved ? "active" : "idle";
|
|
3548
4336
|
const session = setSessionStatus(this.db, pending.sessionId, status);
|
|
3549
4337
|
if (session)
|
|
@@ -3579,7 +4367,29 @@ var EventRouter = class {
|
|
|
3579
4367
|
model: opts.model,
|
|
3580
4368
|
initialPrompt: opts.initialPrompt,
|
|
3581
4369
|
worktreeBranch: opts.worktreeBranch,
|
|
3582
|
-
worktreeBaseRef: opts.worktreeBaseRef
|
|
4370
|
+
worktreeBaseRef: opts.worktreeBaseRef,
|
|
4371
|
+
useAgentView: opts.useAgentView,
|
|
4372
|
+
agentName: opts.agentName,
|
|
4373
|
+
budgetUsd: opts.budgetUsd,
|
|
4374
|
+
goalId: opts.goalId
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
/** Sprint M — raise (or clear) a session's budget cap. Clears any standing
|
|
4378
|
+
* budget breach by re-broadcasting the current cost against the new cap. */
|
|
4379
|
+
raiseBudget(sessionId, budgetUsd) {
|
|
4380
|
+
const session = setSessionBudget(this.db, sessionId, budgetUsd);
|
|
4381
|
+
if (!session) return;
|
|
4382
|
+
this.broadcaster.broadcast({ type: "session_upsert", session });
|
|
4383
|
+
this.broadcaster.broadcast({
|
|
4384
|
+
type: "cost_update",
|
|
4385
|
+
sessionId,
|
|
4386
|
+
costUsd: session.costUsd,
|
|
4387
|
+
budgetUsd: session.budgetUsd
|
|
4388
|
+
});
|
|
4389
|
+
this.broadcaster.broadcast({
|
|
4390
|
+
type: "toast",
|
|
4391
|
+
level: "info",
|
|
4392
|
+
message: `Budget raised to $${budgetUsd.toFixed(2)} for ${session.name ?? sessionId.slice(0, 8)}`
|
|
3583
4393
|
});
|
|
3584
4394
|
}
|
|
3585
4395
|
sendPromptToSession(sessionId, text) {
|
|
@@ -3624,6 +4434,19 @@ var EventRouter = class {
|
|
|
3624
4434
|
broadcastSessionUpsert(session) {
|
|
3625
4435
|
this.broadcaster.broadcast({ type: "session_upsert", session });
|
|
3626
4436
|
}
|
|
4437
|
+
// Sprint M — broadcast helpers for schedule/goal CRUD driven by HTTP/CLI.
|
|
4438
|
+
broadcastScheduleUpsert(schedule2) {
|
|
4439
|
+
this.broadcaster.broadcast({ type: "schedule_upsert", schedule: schedule2 });
|
|
4440
|
+
}
|
|
4441
|
+
broadcastScheduleRemove(scheduleId) {
|
|
4442
|
+
this.broadcaster.broadcast({ type: "schedule_remove", scheduleId });
|
|
4443
|
+
}
|
|
4444
|
+
broadcastGoalUpsert(goal2) {
|
|
4445
|
+
this.broadcaster.broadcast({ type: "goal_upsert", goal: goal2 });
|
|
4446
|
+
}
|
|
4447
|
+
broadcastGoalRemove(goalId) {
|
|
4448
|
+
this.broadcaster.broadcast({ type: "goal_remove", goalId });
|
|
4449
|
+
}
|
|
3627
4450
|
broadcastGalaxyImported(manifest) {
|
|
3628
4451
|
this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
|
|
3629
4452
|
this.broadcaster.broadcast({
|
|
@@ -3679,7 +4502,9 @@ function attachWs(server, ctx) {
|
|
|
3679
4502
|
sessions: listActiveSessions(ctx.db),
|
|
3680
4503
|
missions: listMissions(ctx.db, { limit: 100 }),
|
|
3681
4504
|
advisors: listAdvisors(ctx.db),
|
|
3682
|
-
skills: listSkills(ctx.db)
|
|
4505
|
+
skills: listSkills(ctx.db),
|
|
4506
|
+
schedules: listSchedules(ctx.db),
|
|
4507
|
+
goals: listGoals(ctx.db)
|
|
3683
4508
|
};
|
|
3684
4509
|
ctx.broadcaster.send(ws, snapshot);
|
|
3685
4510
|
for (const p of ctx.router.pendingPermissions()) {
|
|
@@ -3726,9 +4551,18 @@ function handleClientMessage(ctx, _ws, msg) {
|
|
|
3726
4551
|
model: msg.model,
|
|
3727
4552
|
initialPrompt: msg.initialPrompt,
|
|
3728
4553
|
worktreeBranch: msg.worktreeBranch,
|
|
3729
|
-
worktreeBaseRef: msg.worktreeBaseRef
|
|
4554
|
+
worktreeBaseRef: msg.worktreeBaseRef,
|
|
4555
|
+
useAgentView: msg.useAgentView,
|
|
4556
|
+
agentName: msg.agentName,
|
|
4557
|
+
budgetUsd: msg.budgetUsd,
|
|
4558
|
+
goalId: msg.goalId
|
|
3730
4559
|
});
|
|
3731
4560
|
break;
|
|
4561
|
+
case "raise_budget":
|
|
4562
|
+
ctx.router.raiseBudget(msg.sessionId, msg.budgetUsd);
|
|
4563
|
+
break;
|
|
4564
|
+
case "dismiss_budget_alert":
|
|
4565
|
+
break;
|
|
3732
4566
|
case "invoke_advisor":
|
|
3733
4567
|
ctx.router.invokeAdvisor(
|
|
3734
4568
|
msg.advisorId,
|
|
@@ -3742,6 +4576,9 @@ function handleClientMessage(ctx, _ws, msg) {
|
|
|
3742
4576
|
case "unpin_advisor":
|
|
3743
4577
|
ctx.router.unpinAdvisor(msg.advisorId);
|
|
3744
4578
|
break;
|
|
4579
|
+
case "set_advisor_enabled":
|
|
4580
|
+
ctx.router.setAdvisorEnabled(msg.advisorId, msg.enabled);
|
|
4581
|
+
break;
|
|
3745
4582
|
default:
|
|
3746
4583
|
break;
|
|
3747
4584
|
}
|
|
@@ -3777,12 +4614,15 @@ var TranscriptWatcherManager = class {
|
|
|
3777
4614
|
constructor(db, broadcaster) {
|
|
3778
4615
|
this.db = db;
|
|
3779
4616
|
this.broadcaster = broadcaster;
|
|
3780
|
-
void this.db;
|
|
3781
4617
|
}
|
|
3782
4618
|
db;
|
|
3783
4619
|
broadcaster;
|
|
3784
4620
|
records = /* @__PURE__ */ new Map();
|
|
3785
4621
|
deferredRetry = /* @__PURE__ */ new Map();
|
|
4622
|
+
// Sprint M: sessions we've already raised a budget alert for, so we don't
|
|
4623
|
+
// re-fire on every subsequent assistant message. Cleared when spend drops
|
|
4624
|
+
// back under the cap (e.g. after the cap is raised).
|
|
4625
|
+
budgetAlerted = /* @__PURE__ */ new Set();
|
|
3786
4626
|
/**
|
|
3787
4627
|
* Begin tailing this session's transcript. Idempotent. If the file doesn't
|
|
3788
4628
|
* exist yet, retries every second for up to 10 s (Claude Code creates the
|
|
@@ -3914,6 +4754,39 @@ var TranscriptWatcherManager = class {
|
|
|
3914
4754
|
sessionId,
|
|
3915
4755
|
usagePct: pct
|
|
3916
4756
|
});
|
|
4757
|
+
const inc = costForUsage(message.model, message.usage);
|
|
4758
|
+
const session = getSession(this.db, sessionId);
|
|
4759
|
+
if (session) {
|
|
4760
|
+
const updated = setSessionCost(this.db, sessionId, session.costUsd + inc);
|
|
4761
|
+
const costUsd = updated?.costUsd ?? session.costUsd + inc;
|
|
4762
|
+
const cap = updated?.budgetUsd ?? session.budgetUsd;
|
|
4763
|
+
this.broadcaster.broadcast({
|
|
4764
|
+
type: "cost_update",
|
|
4765
|
+
sessionId,
|
|
4766
|
+
costUsd,
|
|
4767
|
+
budgetUsd: cap
|
|
4768
|
+
});
|
|
4769
|
+
if (session.currentMissionId) {
|
|
4770
|
+
addMissionTokens(
|
|
4771
|
+
this.db,
|
|
4772
|
+
session.currentMissionId,
|
|
4773
|
+
totalTokens(message.usage)
|
|
4774
|
+
);
|
|
4775
|
+
}
|
|
4776
|
+
if (cap != null && costUsd >= cap) {
|
|
4777
|
+
if (!this.budgetAlerted.has(sessionId)) {
|
|
4778
|
+
this.budgetAlerted.add(sessionId);
|
|
4779
|
+
this.broadcaster.broadcast({
|
|
4780
|
+
type: "budget_alert",
|
|
4781
|
+
sessionId,
|
|
4782
|
+
costUsd,
|
|
4783
|
+
budgetUsd: cap
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
} else {
|
|
4787
|
+
this.budgetAlerted.delete(sessionId);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
3917
4790
|
}
|
|
3918
4791
|
const content = this.flattenAssistantContent(message.content);
|
|
3919
4792
|
if (!content) return;
|
|
@@ -3991,6 +4864,196 @@ ${text.slice(0, 600)}`);
|
|
|
3991
4864
|
}
|
|
3992
4865
|
};
|
|
3993
4866
|
|
|
4867
|
+
// ../server/src/state/agentview.ts
|
|
4868
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync6, watch as watch2 } from "fs";
|
|
4869
|
+
import { homedir as homedir10 } from "os";
|
|
4870
|
+
import { join as join14 } from "path";
|
|
4871
|
+
var ROSTER_PATH = join14(homedir10(), ".claude", "daemon", "roster.json");
|
|
4872
|
+
var JOBS_DIR = join14(homedir10(), ".claude", "jobs");
|
|
4873
|
+
function mapStatus(state) {
|
|
4874
|
+
switch (state) {
|
|
4875
|
+
case "working":
|
|
4876
|
+
return "active";
|
|
4877
|
+
case "needs_input":
|
|
4878
|
+
return "awaiting_input";
|
|
4879
|
+
case "idle":
|
|
4880
|
+
return "idle";
|
|
4881
|
+
case "completed":
|
|
4882
|
+
return "terminated";
|
|
4883
|
+
case "failed":
|
|
4884
|
+
return "error";
|
|
4885
|
+
case "stopped":
|
|
4886
|
+
return "terminated";
|
|
4887
|
+
default:
|
|
4888
|
+
return "idle";
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
function mapPrStatus(s) {
|
|
4892
|
+
if (!s) return void 0;
|
|
4893
|
+
if (s === "pending" || s === "success" || s === "failure" || s === "neutral")
|
|
4894
|
+
return s;
|
|
4895
|
+
return void 0;
|
|
4896
|
+
}
|
|
4897
|
+
function readRoster() {
|
|
4898
|
+
if (!existsSync11(ROSTER_PATH)) return [];
|
|
4899
|
+
try {
|
|
4900
|
+
const raw = readFileSync7(ROSTER_PATH, "utf8");
|
|
4901
|
+
const parsed = JSON.parse(raw);
|
|
4902
|
+
if (Array.isArray(parsed)) return parsed;
|
|
4903
|
+
if (parsed && Array.isArray(parsed.sessions)) return parsed.sessions;
|
|
4904
|
+
return [];
|
|
4905
|
+
} catch {
|
|
4906
|
+
return [];
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
function readJobIds() {
|
|
4910
|
+
if (!existsSync11(JOBS_DIR)) return [];
|
|
4911
|
+
try {
|
|
4912
|
+
return readdirSync5(JOBS_DIR).filter((entry) => {
|
|
4913
|
+
try {
|
|
4914
|
+
return statSync6(join14(JOBS_DIR, entry)).isDirectory();
|
|
4915
|
+
} catch {
|
|
4916
|
+
return false;
|
|
4917
|
+
}
|
|
4918
|
+
});
|
|
4919
|
+
} catch {
|
|
4920
|
+
return [];
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
function readJobState(jobId) {
|
|
4924
|
+
const p = join14(JOBS_DIR, jobId, "state.json");
|
|
4925
|
+
if (!existsSync11(p)) return null;
|
|
4926
|
+
try {
|
|
4927
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
4928
|
+
} catch {
|
|
4929
|
+
return null;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
function syncFromDisk({ db, broadcaster }) {
|
|
4933
|
+
const roster = readRoster();
|
|
4934
|
+
const jobIds = readJobIds();
|
|
4935
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
4936
|
+
for (const e of roster) if (e.id) liveIds.add(e.id);
|
|
4937
|
+
for (const id of jobIds) liveIds.add(id);
|
|
4938
|
+
for (const agentViewId of liveIds) {
|
|
4939
|
+
const state = readJobState(agentViewId);
|
|
4940
|
+
if (!state) continue;
|
|
4941
|
+
const cwd = state.cwd ?? "";
|
|
4942
|
+
if (!cwd) continue;
|
|
4943
|
+
const solixId = `av-${agentViewId}`;
|
|
4944
|
+
const existing = getSession(db, solixId);
|
|
4945
|
+
const status = mapStatus(state.state);
|
|
4946
|
+
const summary = state.summary ?? null;
|
|
4947
|
+
const prUrl = state.pr_url ?? null;
|
|
4948
|
+
const prCheckStatus = mapPrStatus(state.pr_check_status) ?? null;
|
|
4949
|
+
if (!existing) {
|
|
4950
|
+
const project = ensureProject(db, cwd);
|
|
4951
|
+
const session = upsertSession(db, {
|
|
4952
|
+
id: solixId,
|
|
4953
|
+
pid: 0,
|
|
4954
|
+
// we don't know the pid; supervisor owns it
|
|
4955
|
+
projectId: project.id,
|
|
4956
|
+
cwd,
|
|
4957
|
+
origin: "agentview",
|
|
4958
|
+
model: state.model ?? "default",
|
|
4959
|
+
kind: "user",
|
|
4960
|
+
worktreePath: state.worktree_path ?? void 0,
|
|
4961
|
+
agentViewId,
|
|
4962
|
+
agentViewSummary: summary ?? void 0,
|
|
4963
|
+
prUrl: prUrl ?? void 0,
|
|
4964
|
+
prCheckStatus: prCheckStatus ?? void 0
|
|
4965
|
+
});
|
|
4966
|
+
const updated2 = setAgentViewFields(db, solixId, { status });
|
|
4967
|
+
broadcaster.broadcast({
|
|
4968
|
+
type: "session_upsert",
|
|
4969
|
+
session: updated2 ?? session
|
|
4970
|
+
});
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
const changed = existing.status !== status || (existing.agentViewSummary ?? null) !== summary || (existing.prUrl ?? null) !== prUrl || (existing.prCheckStatus ?? null) !== prCheckStatus;
|
|
4974
|
+
if (!changed) continue;
|
|
4975
|
+
const updated = setAgentViewFields(db, solixId, {
|
|
4976
|
+
status,
|
|
4977
|
+
agentViewSummary: summary,
|
|
4978
|
+
prUrl,
|
|
4979
|
+
prCheckStatus
|
|
4980
|
+
});
|
|
4981
|
+
if (updated) broadcaster.broadcast({ type: "session_upsert", session: updated });
|
|
4982
|
+
}
|
|
4983
|
+
const rows = db.prepare(
|
|
4984
|
+
`SELECT id, agent_view_id FROM sessions
|
|
4985
|
+
WHERE origin = 'agentview' AND status != 'terminated'`
|
|
4986
|
+
).all();
|
|
4987
|
+
for (const r of rows) {
|
|
4988
|
+
if (r.agent_view_id && !liveIds.has(r.agent_view_id)) {
|
|
4989
|
+
const updated = setAgentViewFields(db, r.id, { status: "terminated" });
|
|
4990
|
+
if (updated)
|
|
4991
|
+
broadcaster.broadcast({ type: "session_upsert", session: updated });
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
function debounce(fn, ms) {
|
|
4996
|
+
let h = null;
|
|
4997
|
+
return () => {
|
|
4998
|
+
if (h) clearTimeout(h);
|
|
4999
|
+
h = setTimeout(() => {
|
|
5000
|
+
h = null;
|
|
5001
|
+
fn();
|
|
5002
|
+
}, ms);
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
function startAgentViewBridge(opts) {
|
|
5006
|
+
const claudeRoot = join14(homedir10(), ".claude");
|
|
5007
|
+
if (!existsSync11(claudeRoot)) return () => {
|
|
5008
|
+
};
|
|
5009
|
+
const sync = () => {
|
|
5010
|
+
try {
|
|
5011
|
+
syncFromDisk(opts);
|
|
5012
|
+
} catch (err) {
|
|
5013
|
+
console.warn("[agentview] sync failed:", err.message);
|
|
5014
|
+
}
|
|
5015
|
+
};
|
|
5016
|
+
const debounced = debounce(sync, 50);
|
|
5017
|
+
sync();
|
|
5018
|
+
const watchers = [];
|
|
5019
|
+
const daemonDir = join14(homedir10(), ".claude", "daemon");
|
|
5020
|
+
if (existsSync11(daemonDir)) {
|
|
5021
|
+
try {
|
|
5022
|
+
watchers.push(watch2(daemonDir, { persistent: false }, debounced));
|
|
5023
|
+
} catch (err) {
|
|
5024
|
+
console.warn(
|
|
5025
|
+
"[agentview] could not watch daemon dir:",
|
|
5026
|
+
err.message
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
if (existsSync11(JOBS_DIR)) {
|
|
5031
|
+
try {
|
|
5032
|
+
watchers.push(watch2(JOBS_DIR, { recursive: true, persistent: false }, debounced));
|
|
5033
|
+
} catch (err) {
|
|
5034
|
+
console.warn("[agentview] recursive watch unsupported; falling back to poll");
|
|
5035
|
+
const poll = setInterval(sync, 3e3);
|
|
5036
|
+
return () => {
|
|
5037
|
+
clearInterval(poll);
|
|
5038
|
+
for (const w of watchers) {
|
|
5039
|
+
try {
|
|
5040
|
+
w.close();
|
|
5041
|
+
} catch {
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
};
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
return () => {
|
|
5048
|
+
for (const w of watchers) {
|
|
5049
|
+
try {
|
|
5050
|
+
w.close();
|
|
5051
|
+
} catch {
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
5056
|
+
|
|
3994
5057
|
// ../server/src/create.ts
|
|
3995
5058
|
async function createSolixServer(opts = {}) {
|
|
3996
5059
|
const port = opts.port ?? 4242;
|
|
@@ -4009,7 +5072,17 @@ async function createSolixServer(opts = {}) {
|
|
|
4009
5072
|
const launcher = new Launcher(db, broadcaster);
|
|
4010
5073
|
const transcripts = new TranscriptWatcherManager(db, broadcaster);
|
|
4011
5074
|
const router = new EventRouter(db, broadcaster, launcher, transcripts);
|
|
4012
|
-
const
|
|
5075
|
+
const tokenPath = join15(
|
|
5076
|
+
process.env.SOLIX_HOME ?? join15(homedir11(), ".solix"),
|
|
5077
|
+
"token"
|
|
5078
|
+
);
|
|
5079
|
+
let token = null;
|
|
5080
|
+
try {
|
|
5081
|
+
token = readFileSync8(tokenPath, "utf8").trim() || null;
|
|
5082
|
+
} catch {
|
|
5083
|
+
token = null;
|
|
5084
|
+
}
|
|
5085
|
+
const app = createHttpApp({ db, router, token });
|
|
4013
5086
|
const server = serve({
|
|
4014
5087
|
fetch: app.fetch,
|
|
4015
5088
|
port,
|
|
@@ -4020,10 +5093,33 @@ async function createSolixServer(opts = {}) {
|
|
|
4020
5093
|
router,
|
|
4021
5094
|
broadcaster
|
|
4022
5095
|
});
|
|
5096
|
+
const stopAgentViewBridge = startAgentViewBridge({ db, broadcaster });
|
|
5097
|
+
const scheduleTimer = setInterval(() => {
|
|
5098
|
+
try {
|
|
5099
|
+
const due = listDueSchedules(db, now());
|
|
5100
|
+
for (const s of due) {
|
|
5101
|
+
if (!s.cwd) continue;
|
|
5102
|
+
launcher.launch({ cwd: s.cwd, initialPrompt: s.prompt });
|
|
5103
|
+
const updated = markScheduleRun(db, s.id);
|
|
5104
|
+
if (updated) {
|
|
5105
|
+
broadcaster.broadcast({ type: "schedule_upsert", schedule: updated });
|
|
5106
|
+
broadcaster.broadcast({
|
|
5107
|
+
type: "toast",
|
|
5108
|
+
level: "info",
|
|
5109
|
+
message: `Heartbeat fired: ${s.name ?? s.prompt.slice(0, 32)}`
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
} catch (err) {
|
|
5114
|
+
console.warn("[scheduler] tick failed:", err.message);
|
|
5115
|
+
}
|
|
5116
|
+
}, 3e4);
|
|
4023
5117
|
return {
|
|
4024
5118
|
port,
|
|
4025
5119
|
hostname,
|
|
4026
5120
|
close: () => new Promise((resolve4) => {
|
|
5121
|
+
clearInterval(scheduleTimer);
|
|
5122
|
+
stopAgentViewBridge();
|
|
4027
5123
|
transcripts.shutdownAll();
|
|
4028
5124
|
launcher.shutdownAll();
|
|
4029
5125
|
server.close(() => resolve4());
|
|
@@ -4071,20 +5167,20 @@ async function start(opts = {}) {
|
|
|
4071
5167
|
}
|
|
4072
5168
|
|
|
4073
5169
|
// src/uninstall.ts
|
|
4074
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
5170
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
4075
5171
|
function uninstall() {
|
|
4076
5172
|
uninstallShim();
|
|
4077
|
-
if (
|
|
5173
|
+
if (existsSync12(CLAUDE_BACKUP)) {
|
|
4078
5174
|
copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
|
|
4079
5175
|
console.log(`[solix] restored settings.json from backup`);
|
|
4080
5176
|
return;
|
|
4081
5177
|
}
|
|
4082
|
-
if (!
|
|
5178
|
+
if (!existsSync12(CLAUDE_SETTINGS)) {
|
|
4083
5179
|
console.log("[solix] nothing to uninstall (no settings.json found)");
|
|
4084
5180
|
return;
|
|
4085
5181
|
}
|
|
4086
5182
|
const cur = JSON.parse(
|
|
4087
|
-
|
|
5183
|
+
readFileSync9(CLAUDE_SETTINGS, "utf8")
|
|
4088
5184
|
);
|
|
4089
5185
|
if (cur.hooks) {
|
|
4090
5186
|
for (const [evt, entries] of Object.entries(cur.hooks)) {
|
|
@@ -4100,7 +5196,7 @@ function uninstall() {
|
|
|
4100
5196
|
|
|
4101
5197
|
// src/index.ts
|
|
4102
5198
|
var program = new Command();
|
|
4103
|
-
program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.
|
|
5199
|
+
program.name("solix").description("Solix \u2014 a solar-system command center for Claude Code agents").version("1.8.0");
|
|
4104
5200
|
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) => {
|
|
4105
5201
|
await start({ port: opts.port, noOpen: !opts.open });
|
|
4106
5202
|
});
|
|
@@ -4169,6 +5265,36 @@ galaxy.command("publish <slug>").description("Publish the current galaxy to the
|
|
|
4169
5265
|
galaxy.command("install <slug>").description("Pull and install a galaxy from the configured registry").action(async (slug) => {
|
|
4170
5266
|
await installFromRegistryCmd(slug);
|
|
4171
5267
|
});
|
|
5268
|
+
var schedule = program.command("schedule").description('Manage recurring "heartbeat" tasks (Sprint M)');
|
|
5269
|
+
schedule.command("list", { isDefault: true }).description("List all scheduled tasks").action(async () => {
|
|
5270
|
+
await listSchedulesCmd();
|
|
5271
|
+
});
|
|
5272
|
+
schedule.command("add <prompt>").description("Schedule a recurring task").option("--cwd <dir>", "working directory (default: current dir)").option("--every <cadence>", "cadence: 30m, 2h, 1d", "1h").option("--name <name>", "short label for the galaxy node").action(
|
|
5273
|
+
async (prompt, opts) => {
|
|
5274
|
+
await addScheduleCmd(prompt, opts);
|
|
5275
|
+
}
|
|
5276
|
+
);
|
|
5277
|
+
schedule.command("enable <id>").description("Enable a schedule").action(async (id) => {
|
|
5278
|
+
await enableScheduleCmd(id);
|
|
5279
|
+
});
|
|
5280
|
+
schedule.command("disable <id>").description("Disable a schedule (keeps it, stops firing)").action(async (id) => {
|
|
5281
|
+
await disableScheduleCmd(id);
|
|
5282
|
+
});
|
|
5283
|
+
schedule.command("remove <id>").description("Delete a schedule").action(async (id) => {
|
|
5284
|
+
await removeScheduleCmd(id);
|
|
5285
|
+
});
|
|
5286
|
+
var goal = program.command("goal").description("Manage goals that missions roll up to (Sprint M)");
|
|
5287
|
+
goal.command("list", { isDefault: true }).description("List all goals").action(async () => {
|
|
5288
|
+
await listGoalsCmd();
|
|
5289
|
+
});
|
|
5290
|
+
goal.command("add <name>").description("Create a goal").option("--description <desc>", "optional description").option("--color <hex>", "optional hex color for the constellation").action(
|
|
5291
|
+
async (name, opts) => {
|
|
5292
|
+
await addGoalCmd(name, opts);
|
|
5293
|
+
}
|
|
5294
|
+
);
|
|
5295
|
+
goal.command("remove <id>").description("Delete a goal (detaches it from sessions/missions)").action(async (id) => {
|
|
5296
|
+
await removeGoalCmd(id);
|
|
5297
|
+
});
|
|
4172
5298
|
program.parseAsync(process.argv).catch((err) => {
|
|
4173
5299
|
console.error(err);
|
|
4174
5300
|
process.exit(1);
|