@shmulikdav/solix 1.5.0 → 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 +815 -49
- package/dist/web/assets/CrewPanel-BZW5L0xR.js +1 -0
- package/dist/web/assets/{GalaxyPanel-CsENB_VZ.js → GalaxyPanel-CCsddWgL.js} +1 -1
- package/dist/web/assets/Scene-D9_dLZUf.js +700 -0
- package/dist/web/assets/{TimelineDrawer-DsGkgssp.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-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
|
@@ -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");
|
|
@@ -601,6 +602,7 @@ import {
|
|
|
601
602
|
writeFileSync as writeFileSync2,
|
|
602
603
|
chmodSync
|
|
603
604
|
} from "fs";
|
|
605
|
+
import { randomBytes } from "crypto";
|
|
604
606
|
import { join as join4 } from "path";
|
|
605
607
|
function readSettings() {
|
|
606
608
|
if (!existsSync3(CLAUDE_SETTINGS)) return {};
|
|
@@ -654,6 +656,15 @@ function mergeHooks(existing, solix) {
|
|
|
654
656
|
}
|
|
655
657
|
return merged;
|
|
656
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
|
+
}
|
|
657
668
|
function installHookScripts() {
|
|
658
669
|
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
659
670
|
const src = packagedHooksDir();
|
|
@@ -719,6 +730,7 @@ function install(opts = {}) {
|
|
|
719
730
|
} else if (opts.force && existsSync3(CLAUDE_SETTINGS)) {
|
|
720
731
|
copyFileSync(CLAUDE_SETTINGS, CLAUDE_BACKUP);
|
|
721
732
|
}
|
|
733
|
+
ensureToken();
|
|
722
734
|
installHookScripts();
|
|
723
735
|
console.log(`[solix] installed hook scripts in ${HOOKS_DIR}`);
|
|
724
736
|
const advisorsCopied = installAdvisorAgents();
|
|
@@ -1020,8 +1032,145 @@ async function installSkillCmd(id, projectId) {
|
|
|
1020
1032
|
}
|
|
1021
1033
|
}
|
|
1022
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
|
+
|
|
1023
1169
|
// ../server/src/create.ts
|
|
1024
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";
|
|
1025
1174
|
|
|
1026
1175
|
// ../server/src/broadcaster.ts
|
|
1027
1176
|
var Broadcaster = class {
|
|
@@ -1197,6 +1346,14 @@ CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
|
|
1197
1346
|
last_run_at INTEGER,
|
|
1198
1347
|
next_run_at INTEGER NOT NULL
|
|
1199
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
|
+
);
|
|
1200
1357
|
`;
|
|
1201
1358
|
var _db = null;
|
|
1202
1359
|
function ensureColumn(db, table, column, ddl) {
|
|
@@ -1222,6 +1379,12 @@ function getDb() {
|
|
|
1222
1379
|
ensureColumn(db, "sessions", "pr_check_status", "pr_check_status TEXT");
|
|
1223
1380
|
ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
|
|
1224
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");
|
|
1225
1388
|
_db = db;
|
|
1226
1389
|
return db;
|
|
1227
1390
|
}
|
|
@@ -1311,7 +1474,10 @@ function rowToSession(row) {
|
|
|
1311
1474
|
agentViewId: row.agent_view_id ?? void 0,
|
|
1312
1475
|
agentViewSummary: row.agent_view_summary ?? void 0,
|
|
1313
1476
|
prUrl: row.pr_url ?? void 0,
|
|
1314
|
-
prCheckStatus: row.pr_check_status ?? 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
|
|
1315
1481
|
};
|
|
1316
1482
|
}
|
|
1317
1483
|
function nextOrbitSlot(db, projectId) {
|
|
@@ -1392,7 +1558,8 @@ function upsertSession(db, input) {
|
|
|
1392
1558
|
agentViewId: input.agentViewId,
|
|
1393
1559
|
agentViewSummary: input.agentViewSummary,
|
|
1394
1560
|
prUrl: input.prUrl,
|
|
1395
|
-
prCheckStatus: input.prCheckStatus
|
|
1561
|
+
prCheckStatus: input.prCheckStatus,
|
|
1562
|
+
costUsd: 0
|
|
1396
1563
|
};
|
|
1397
1564
|
}
|
|
1398
1565
|
function setAgentViewFields(db, sessionId, fields) {
|
|
@@ -1456,6 +1623,27 @@ function setSessionContextUsage(db, sessionId, pct) {
|
|
|
1456
1623
|
).run(clamped, ts2, sessionId);
|
|
1457
1624
|
return getSession(db, sessionId);
|
|
1458
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
|
+
}
|
|
1459
1647
|
function getSession(db, sessionId) {
|
|
1460
1648
|
const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1461
1649
|
return row ? rowToSession(row) : null;
|
|
@@ -1504,7 +1692,8 @@ function rowToMission(row) {
|
|
|
1504
1692
|
toolCallCount: row.tool_call_count
|
|
1505
1693
|
},
|
|
1506
1694
|
filesTouched,
|
|
1507
|
-
errorSummary: row.error_summary ?? void 0
|
|
1695
|
+
errorSummary: row.error_summary ?? void 0,
|
|
1696
|
+
goalId: row.goal_id ?? void 0
|
|
1508
1697
|
};
|
|
1509
1698
|
}
|
|
1510
1699
|
function setMissionError(db, missionId, errorSummary) {
|
|
@@ -1520,14 +1709,14 @@ function shortNameFromPrompt(prompt) {
|
|
|
1520
1709
|
(w) => w.replace(/[^a-zA-Z0-9-]/g, "").toLowerCase().replace(/^./, (c) => c.toUpperCase())
|
|
1521
1710
|
).filter(Boolean).join(" ") || "New Mission";
|
|
1522
1711
|
}
|
|
1523
|
-
function startMission(db, sessionId, prompt) {
|
|
1712
|
+
function startMission(db, sessionId, prompt, goalId) {
|
|
1524
1713
|
const id = nanoid2();
|
|
1525
1714
|
const ts2 = now();
|
|
1526
1715
|
const shortName = shortNameFromPrompt(prompt);
|
|
1527
1716
|
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);
|
|
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);
|
|
1531
1720
|
return {
|
|
1532
1721
|
id,
|
|
1533
1722
|
sessionId,
|
|
@@ -1536,9 +1725,16 @@ function startMission(db, sessionId, prompt) {
|
|
|
1536
1725
|
shortName,
|
|
1537
1726
|
status: "active",
|
|
1538
1727
|
metrics: { subagentCount: 0, toolCallCount: 0 },
|
|
1539
|
-
filesTouched: []
|
|
1728
|
+
filesTouched: [],
|
|
1729
|
+
goalId
|
|
1540
1730
|
};
|
|
1541
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
|
+
}
|
|
1542
1738
|
function completeMission(db, missionId, status = "completed") {
|
|
1543
1739
|
const ts2 = now();
|
|
1544
1740
|
const row = db.prepare("SELECT * FROM missions WHERE id = ?").get(missionId);
|
|
@@ -1980,6 +2176,138 @@ function writeToWrapperSocket(socketPath, text) {
|
|
|
1980
2176
|
}
|
|
1981
2177
|
}
|
|
1982
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
|
+
|
|
1983
2311
|
// ../server/src/state/context.ts
|
|
1984
2312
|
var MISSIONS_FOR_HANDOFF = 3;
|
|
1985
2313
|
var DEFAULT_ASKS = {
|
|
@@ -2186,7 +2514,7 @@ function recordSkillInstall(db, skillId, projectId) {
|
|
|
2186
2514
|
}
|
|
2187
2515
|
|
|
2188
2516
|
// ../server/src/state/galaxy.ts
|
|
2189
|
-
import { nanoid as
|
|
2517
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2190
2518
|
function exportManifest(db, opts = {}) {
|
|
2191
2519
|
const advisors2 = listAdvisors(db);
|
|
2192
2520
|
const skills2 = listSkills(db);
|
|
@@ -2234,7 +2562,7 @@ function importManifest(db, manifest, sourceUrl) {
|
|
|
2234
2562
|
db.prepare(
|
|
2235
2563
|
`INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
|
|
2236
2564
|
VALUES (?, ?, ?, ?)`
|
|
2237
|
-
).run(
|
|
2565
|
+
).run(nanoid6(), sourceUrl ?? null, JSON.stringify(manifest), now());
|
|
2238
2566
|
return {
|
|
2239
2567
|
advisorsEnabled: enabled,
|
|
2240
2568
|
advisorsDisabled: disabled,
|
|
@@ -2295,7 +2623,7 @@ function snapshotExport(db, manifest) {
|
|
|
2295
2623
|
return rowToVersion(existing);
|
|
2296
2624
|
}
|
|
2297
2625
|
}
|
|
2298
|
-
const id =
|
|
2626
|
+
const id = nanoid6();
|
|
2299
2627
|
const ts2 = now();
|
|
2300
2628
|
const ordinal = (last?.ordinal ?? 0) + 1;
|
|
2301
2629
|
db.prepare(
|
|
@@ -2373,6 +2701,29 @@ function diffManifests(a, b) {
|
|
|
2373
2701
|
};
|
|
2374
2702
|
}
|
|
2375
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
|
+
|
|
2376
2727
|
// ../server/src/cloud.ts
|
|
2377
2728
|
var RegistryClient = class {
|
|
2378
2729
|
constructor(baseUrl = process.env.SOLIX_REGISTRY_URL ?? "", apiKey = process.env.SOLIX_REGISTRY_KEY) {
|
|
@@ -2462,7 +2813,29 @@ function isAgentViewVersion(version) {
|
|
|
2462
2813
|
}
|
|
2463
2814
|
function createHttpApp(opts) {
|
|
2464
2815
|
const app = new Hono();
|
|
2465
|
-
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
|
+
}
|
|
2466
2839
|
const registry = new RegistryClient();
|
|
2467
2840
|
app.get(
|
|
2468
2841
|
"/api/health",
|
|
@@ -2486,6 +2859,18 @@ function createHttpApp(opts) {
|
|
|
2486
2859
|
}
|
|
2487
2860
|
return c.json({ ok: true });
|
|
2488
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
|
+
});
|
|
2489
2874
|
app.get("/api/projects", (c) => c.json(listProjects(opts.db)));
|
|
2490
2875
|
app.get("/api/projects/:id/sessions", (c) => {
|
|
2491
2876
|
const id = c.req.param("id");
|
|
@@ -2566,11 +2951,11 @@ function createHttpApp(opts) {
|
|
|
2566
2951
|
return c.json({ ...a, agentMd: readAdvisorAgentMd(a) });
|
|
2567
2952
|
});
|
|
2568
2953
|
app.post("/api/advisors/:id/enable", (c) => {
|
|
2569
|
-
const a = setAdvisorEnabled(
|
|
2954
|
+
const a = opts.router.setAdvisorEnabled(c.req.param("id"), true);
|
|
2570
2955
|
return c.json({ ok: Boolean(a), advisor: a });
|
|
2571
2956
|
});
|
|
2572
2957
|
app.post("/api/advisors/:id/disable", (c) => {
|
|
2573
|
-
const a = setAdvisorEnabled(
|
|
2958
|
+
const a = opts.router.setAdvisorEnabled(c.req.param("id"), false);
|
|
2574
2959
|
return c.json({ ok: Boolean(a), advisor: a });
|
|
2575
2960
|
});
|
|
2576
2961
|
app.post("/api/advisors/:id/pin", (c) => {
|
|
@@ -2726,6 +3111,52 @@ function createHttpApp(opts) {
|
|
|
2726
3111
|
return c.json({ ok: true });
|
|
2727
3112
|
});
|
|
2728
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
|
+
});
|
|
2729
3160
|
let preflightCache = null;
|
|
2730
3161
|
app.get("/api/system/preflight", (c) => {
|
|
2731
3162
|
if (preflightCache) return c.json(preflightCache);
|
|
@@ -2867,7 +3298,7 @@ import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
|
2867
3298
|
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
|
|
2868
3299
|
import { homedir as homedir8 } from "os";
|
|
2869
3300
|
import { basename as basename3, join as join12 } from "path";
|
|
2870
|
-
import { nanoid as
|
|
3301
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
2871
3302
|
function ensureWorktree(opts) {
|
|
2872
3303
|
const repoRoot = (() => {
|
|
2873
3304
|
const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -2909,6 +3340,53 @@ function ensureWorktree(opts) {
|
|
|
2909
3340
|
return { path, created: true };
|
|
2910
3341
|
}
|
|
2911
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
|
+
}
|
|
2912
3390
|
var Launcher = class {
|
|
2913
3391
|
constructor(db, broadcaster) {
|
|
2914
3392
|
this.db = db;
|
|
@@ -2938,15 +3416,17 @@ var Launcher = class {
|
|
|
2938
3416
|
return this.pinSynthetic(advisor.id, advisor.codename, cwd);
|
|
2939
3417
|
}
|
|
2940
3418
|
try {
|
|
2941
|
-
const
|
|
2942
|
-
"
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
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
|
+
});
|
|
2950
3430
|
const pid = child.pid;
|
|
2951
3431
|
if (!pid) {
|
|
2952
3432
|
this.broadcaster.broadcast({
|
|
@@ -2985,7 +3465,7 @@ var Launcher = class {
|
|
|
2985
3465
|
}
|
|
2986
3466
|
pinSynthetic(advisorId, codename, cwd) {
|
|
2987
3467
|
const project = ensureProject(this.db, cwd);
|
|
2988
|
-
const sessionId = `advisor-${advisorId}-${
|
|
3468
|
+
const sessionId = `advisor-${advisorId}-${nanoid7(6)}`;
|
|
2989
3469
|
const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
|
|
2990
3470
|
const session = upsertSession(this.db, {
|
|
2991
3471
|
id: sessionId,
|
|
@@ -3099,7 +3579,9 @@ var Launcher = class {
|
|
|
3099
3579
|
cwd: spawnCwd,
|
|
3100
3580
|
model: opts.model,
|
|
3101
3581
|
initialPrompt: opts.initialPrompt,
|
|
3102
|
-
worktreePath
|
|
3582
|
+
worktreePath,
|
|
3583
|
+
budgetUsd: opts.budgetUsd,
|
|
3584
|
+
goalId: opts.goalId
|
|
3103
3585
|
});
|
|
3104
3586
|
}
|
|
3105
3587
|
if (!existsSync9(spawnCwd)) {
|
|
@@ -3113,13 +3595,15 @@ var Launcher = class {
|
|
|
3113
3595
|
const args = ["--print"];
|
|
3114
3596
|
if (opts.model) args.push("--model", String(opts.model));
|
|
3115
3597
|
args.push(opts.initialPrompt);
|
|
3116
|
-
const sessionId = `task-${
|
|
3598
|
+
const sessionId = `task-${nanoid7(8)}`;
|
|
3117
3599
|
return this.spawnPrint({
|
|
3118
3600
|
sessionId,
|
|
3119
3601
|
cwd: spawnCwd,
|
|
3120
3602
|
args,
|
|
3121
3603
|
isFollowUp: false,
|
|
3122
|
-
worktreePath
|
|
3604
|
+
worktreePath,
|
|
3605
|
+
budgetUsd: opts.budgetUsd,
|
|
3606
|
+
goalId: opts.goalId
|
|
3123
3607
|
});
|
|
3124
3608
|
}
|
|
3125
3609
|
/**
|
|
@@ -3152,10 +3636,12 @@ var Launcher = class {
|
|
|
3152
3636
|
args.push("--bg", opts.initialPrompt);
|
|
3153
3637
|
let child;
|
|
3154
3638
|
try {
|
|
3155
|
-
|
|
3639
|
+
const spawnSpec = sandboxWrap("claude", args);
|
|
3640
|
+
child = spawn(spawnSpec.file, spawnSpec.args, {
|
|
3156
3641
|
cwd: opts.cwd,
|
|
3157
3642
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3158
|
-
detached: false
|
|
3643
|
+
detached: false,
|
|
3644
|
+
env: buildSpawnEnv()
|
|
3159
3645
|
});
|
|
3160
3646
|
} catch (err) {
|
|
3161
3647
|
this.broadcaster.broadcast({
|
|
@@ -3199,6 +3685,15 @@ var Launcher = class {
|
|
|
3199
3685
|
});
|
|
3200
3686
|
return false;
|
|
3201
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
|
+
}
|
|
3202
3697
|
if (FAKE_CLAUDE) {
|
|
3203
3698
|
this.broadcaster.broadcast({
|
|
3204
3699
|
type: "chat_delta",
|
|
@@ -3230,13 +3725,29 @@ var Launcher = class {
|
|
|
3230
3725
|
}
|
|
3231
3726
|
return void 0;
|
|
3232
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
|
+
}
|
|
3233
3742
|
spawnPrint(opts) {
|
|
3234
3743
|
let child;
|
|
3235
3744
|
try {
|
|
3236
|
-
|
|
3745
|
+
const spawnSpec = sandboxWrap("claude", opts.args);
|
|
3746
|
+
child = spawn(spawnSpec.file, spawnSpec.args, {
|
|
3237
3747
|
cwd: opts.cwd,
|
|
3238
3748
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3239
|
-
detached: false
|
|
3749
|
+
detached: false,
|
|
3750
|
+
env: buildSpawnEnv()
|
|
3240
3751
|
});
|
|
3241
3752
|
} catch (err) {
|
|
3242
3753
|
this.broadcaster.broadcast({
|
|
@@ -3251,7 +3762,9 @@ var Launcher = class {
|
|
|
3251
3762
|
if (!opts.isFollowUp) {
|
|
3252
3763
|
this.internalTasks.set(opts.sessionId, {
|
|
3253
3764
|
cwd: opts.cwd,
|
|
3254
|
-
worktreePath: opts.worktreePath
|
|
3765
|
+
worktreePath: opts.worktreePath,
|
|
3766
|
+
budgetUsd: opts.budgetUsd,
|
|
3767
|
+
goalId: opts.goalId
|
|
3255
3768
|
});
|
|
3256
3769
|
}
|
|
3257
3770
|
let stdout = "";
|
|
@@ -3297,7 +3810,7 @@ var Launcher = class {
|
|
|
3297
3810
|
}
|
|
3298
3811
|
launchSynthetic(opts) {
|
|
3299
3812
|
const project = ensureProject(this.db, opts.cwd);
|
|
3300
|
-
const sessionId = `task-${
|
|
3813
|
+
const sessionId = `task-${nanoid7(8)}`;
|
|
3301
3814
|
const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
|
|
3302
3815
|
upsertSession(this.db, {
|
|
3303
3816
|
id: sessionId,
|
|
@@ -3308,6 +3821,8 @@ var Launcher = class {
|
|
|
3308
3821
|
model: opts.model ?? "sonnet",
|
|
3309
3822
|
worktreePath: opts.worktreePath
|
|
3310
3823
|
});
|
|
3824
|
+
if (opts.budgetUsd != null) setSessionBudget(this.db, sessionId, opts.budgetUsd);
|
|
3825
|
+
if (opts.goalId) setSessionGoal(this.db, sessionId, opts.goalId);
|
|
3311
3826
|
const active = setSessionStatus(this.db, sessionId, "active");
|
|
3312
3827
|
if (active)
|
|
3313
3828
|
this.broadcaster.broadcast({ type: "session_upsert", session: active });
|
|
@@ -3343,12 +3858,12 @@ var Launcher = class {
|
|
|
3343
3858
|
};
|
|
3344
3859
|
|
|
3345
3860
|
// ../server/src/router.ts
|
|
3346
|
-
import { nanoid as
|
|
3861
|
+
import { nanoid as nanoid9 } from "nanoid";
|
|
3347
3862
|
|
|
3348
3863
|
// ../server/src/state/toolcalls.ts
|
|
3349
|
-
import { nanoid as
|
|
3864
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
3350
3865
|
function recordToolCall(db, input) {
|
|
3351
|
-
const id =
|
|
3866
|
+
const id = nanoid8();
|
|
3352
3867
|
const ts2 = now();
|
|
3353
3868
|
const status = input.status ?? "running";
|
|
3354
3869
|
db.prepare(
|
|
@@ -3450,6 +3965,8 @@ var EventRouter = class {
|
|
|
3450
3965
|
const sessionId = this.extractSessionId(event);
|
|
3451
3966
|
const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
|
|
3452
3967
|
const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
|
|
3968
|
+
const launchBudget = this.launcher?.budgetForInternalCwd(event.cwd);
|
|
3969
|
+
const launchGoal = this.launcher?.goalForInternalCwd(event.cwd);
|
|
3453
3970
|
const wrapper = claimWrapperForCwd(event.cwd);
|
|
3454
3971
|
const session = upsertSession(this.db, {
|
|
3455
3972
|
id: sessionId,
|
|
@@ -3465,7 +3982,14 @@ var EventRouter = class {
|
|
|
3465
3982
|
wrapperSocketPath: wrapper?.socketPath
|
|
3466
3983
|
});
|
|
3467
3984
|
if (wrapper) bindWrapperToSession(wrapper.wrapperId, session.id);
|
|
3468
|
-
|
|
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 });
|
|
3469
3993
|
if (!session.parentSessionId) {
|
|
3470
3994
|
this.transcripts?.startWatching(sessionId, event.cwd);
|
|
3471
3995
|
}
|
|
@@ -3486,7 +4010,7 @@ var EventRouter = class {
|
|
|
3486
4010
|
model: this.extractModel(event)
|
|
3487
4011
|
});
|
|
3488
4012
|
}
|
|
3489
|
-
const mission = startMission(this.db, sessionId, prompt);
|
|
4013
|
+
const mission = startMission(this.db, sessionId, prompt, session.currentGoalId);
|
|
3490
4014
|
const updated = setSessionMission(this.db, sessionId, mission.id);
|
|
3491
4015
|
const active = updated ? setSessionStatus(this.db, sessionId, "active") : null;
|
|
3492
4016
|
this.broadcaster.broadcast({ type: "mission_upsert", mission });
|
|
@@ -3530,7 +4054,7 @@ var EventRouter = class {
|
|
|
3530
4054
|
const parentSessionId = this.extractSessionId(event);
|
|
3531
4055
|
const parent = getSession(this.db, parentSessionId);
|
|
3532
4056
|
if (!parent) return;
|
|
3533
|
-
const subId =
|
|
4057
|
+
const subId = nanoid9();
|
|
3534
4058
|
const sub = upsertSession(this.db, {
|
|
3535
4059
|
id: subId,
|
|
3536
4060
|
pid: event.pid,
|
|
@@ -3607,7 +4131,7 @@ var EventRouter = class {
|
|
|
3607
4131
|
const p = event.payload;
|
|
3608
4132
|
const message = typeof p.message === "string" ? p.message : "Permission requested";
|
|
3609
4133
|
const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
|
|
3610
|
-
const requestId =
|
|
4134
|
+
const requestId = nanoid9();
|
|
3611
4135
|
this.permissions.set(requestId, {
|
|
3612
4136
|
requestId,
|
|
3613
4137
|
sessionId,
|
|
@@ -3635,6 +4159,90 @@ var EventRouter = class {
|
|
|
3635
4159
|
message: `Permission requested: ${message}`
|
|
3636
4160
|
});
|
|
3637
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
|
+
}
|
|
3638
4246
|
invokeAdvisor(advisorId, targetSessionId, prompt) {
|
|
3639
4247
|
const advisor = getAdvisor(this.db, advisorId);
|
|
3640
4248
|
if (!advisor) return { ok: false };
|
|
@@ -3687,6 +4295,20 @@ var EventRouter = class {
|
|
|
3687
4295
|
}
|
|
3688
4296
|
return ok;
|
|
3689
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
|
+
}
|
|
3690
4312
|
unpinAdvisor(advisorId) {
|
|
3691
4313
|
if (this.launcher) {
|
|
3692
4314
|
this.launcher.unpin(advisorId);
|
|
@@ -3708,6 +4330,8 @@ var EventRouter = class {
|
|
|
3708
4330
|
const pending = this.permissions.get(requestId);
|
|
3709
4331
|
if (!pending) return false;
|
|
3710
4332
|
this.permissions.delete(requestId);
|
|
4333
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
4334
|
+
if (pending.resolve) pending.resolve(approved);
|
|
3711
4335
|
const status = approved ? "active" : "idle";
|
|
3712
4336
|
const session = setSessionStatus(this.db, pending.sessionId, status);
|
|
3713
4337
|
if (session)
|
|
@@ -3745,7 +4369,27 @@ var EventRouter = class {
|
|
|
3745
4369
|
worktreeBranch: opts.worktreeBranch,
|
|
3746
4370
|
worktreeBaseRef: opts.worktreeBaseRef,
|
|
3747
4371
|
useAgentView: opts.useAgentView,
|
|
3748
|
-
agentName: opts.agentName
|
|
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)}`
|
|
3749
4393
|
});
|
|
3750
4394
|
}
|
|
3751
4395
|
sendPromptToSession(sessionId, text) {
|
|
@@ -3790,6 +4434,19 @@ var EventRouter = class {
|
|
|
3790
4434
|
broadcastSessionUpsert(session) {
|
|
3791
4435
|
this.broadcaster.broadcast({ type: "session_upsert", session });
|
|
3792
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
|
+
}
|
|
3793
4450
|
broadcastGalaxyImported(manifest) {
|
|
3794
4451
|
this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
|
|
3795
4452
|
this.broadcaster.broadcast({
|
|
@@ -3845,7 +4502,9 @@ function attachWs(server, ctx) {
|
|
|
3845
4502
|
sessions: listActiveSessions(ctx.db),
|
|
3846
4503
|
missions: listMissions(ctx.db, { limit: 100 }),
|
|
3847
4504
|
advisors: listAdvisors(ctx.db),
|
|
3848
|
-
skills: listSkills(ctx.db)
|
|
4505
|
+
skills: listSkills(ctx.db),
|
|
4506
|
+
schedules: listSchedules(ctx.db),
|
|
4507
|
+
goals: listGoals(ctx.db)
|
|
3849
4508
|
};
|
|
3850
4509
|
ctx.broadcaster.send(ws, snapshot);
|
|
3851
4510
|
for (const p of ctx.router.pendingPermissions()) {
|
|
@@ -3894,9 +4553,16 @@ function handleClientMessage(ctx, _ws, msg) {
|
|
|
3894
4553
|
worktreeBranch: msg.worktreeBranch,
|
|
3895
4554
|
worktreeBaseRef: msg.worktreeBaseRef,
|
|
3896
4555
|
useAgentView: msg.useAgentView,
|
|
3897
|
-
agentName: msg.agentName
|
|
4556
|
+
agentName: msg.agentName,
|
|
4557
|
+
budgetUsd: msg.budgetUsd,
|
|
4558
|
+
goalId: msg.goalId
|
|
3898
4559
|
});
|
|
3899
4560
|
break;
|
|
4561
|
+
case "raise_budget":
|
|
4562
|
+
ctx.router.raiseBudget(msg.sessionId, msg.budgetUsd);
|
|
4563
|
+
break;
|
|
4564
|
+
case "dismiss_budget_alert":
|
|
4565
|
+
break;
|
|
3900
4566
|
case "invoke_advisor":
|
|
3901
4567
|
ctx.router.invokeAdvisor(
|
|
3902
4568
|
msg.advisorId,
|
|
@@ -3910,6 +4576,9 @@ function handleClientMessage(ctx, _ws, msg) {
|
|
|
3910
4576
|
case "unpin_advisor":
|
|
3911
4577
|
ctx.router.unpinAdvisor(msg.advisorId);
|
|
3912
4578
|
break;
|
|
4579
|
+
case "set_advisor_enabled":
|
|
4580
|
+
ctx.router.setAdvisorEnabled(msg.advisorId, msg.enabled);
|
|
4581
|
+
break;
|
|
3913
4582
|
default:
|
|
3914
4583
|
break;
|
|
3915
4584
|
}
|
|
@@ -3945,12 +4614,15 @@ var TranscriptWatcherManager = class {
|
|
|
3945
4614
|
constructor(db, broadcaster) {
|
|
3946
4615
|
this.db = db;
|
|
3947
4616
|
this.broadcaster = broadcaster;
|
|
3948
|
-
void this.db;
|
|
3949
4617
|
}
|
|
3950
4618
|
db;
|
|
3951
4619
|
broadcaster;
|
|
3952
4620
|
records = /* @__PURE__ */ new Map();
|
|
3953
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();
|
|
3954
4626
|
/**
|
|
3955
4627
|
* Begin tailing this session's transcript. Idempotent. If the file doesn't
|
|
3956
4628
|
* exist yet, retries every second for up to 10 s (Claude Code creates the
|
|
@@ -4082,6 +4754,39 @@ var TranscriptWatcherManager = class {
|
|
|
4082
4754
|
sessionId,
|
|
4083
4755
|
usagePct: pct
|
|
4084
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
|
+
}
|
|
4085
4790
|
}
|
|
4086
4791
|
const content = this.flattenAssistantContent(message.content);
|
|
4087
4792
|
if (!content) return;
|
|
@@ -4367,7 +5072,17 @@ async function createSolixServer(opts = {}) {
|
|
|
4367
5072
|
const launcher = new Launcher(db, broadcaster);
|
|
4368
5073
|
const transcripts = new TranscriptWatcherManager(db, broadcaster);
|
|
4369
5074
|
const router = new EventRouter(db, broadcaster, launcher, transcripts);
|
|
4370
|
-
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 });
|
|
4371
5086
|
const server = serve({
|
|
4372
5087
|
fetch: app.fetch,
|
|
4373
5088
|
port,
|
|
@@ -4379,10 +5094,31 @@ async function createSolixServer(opts = {}) {
|
|
|
4379
5094
|
broadcaster
|
|
4380
5095
|
});
|
|
4381
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);
|
|
4382
5117
|
return {
|
|
4383
5118
|
port,
|
|
4384
5119
|
hostname,
|
|
4385
5120
|
close: () => new Promise((resolve4) => {
|
|
5121
|
+
clearInterval(scheduleTimer);
|
|
4386
5122
|
stopAgentViewBridge();
|
|
4387
5123
|
transcripts.shutdownAll();
|
|
4388
5124
|
launcher.shutdownAll();
|
|
@@ -4431,7 +5167,7 @@ async function start(opts = {}) {
|
|
|
4431
5167
|
}
|
|
4432
5168
|
|
|
4433
5169
|
// src/uninstall.ts
|
|
4434
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync12, readFileSync as
|
|
5170
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
4435
5171
|
function uninstall() {
|
|
4436
5172
|
uninstallShim();
|
|
4437
5173
|
if (existsSync12(CLAUDE_BACKUP)) {
|
|
@@ -4444,7 +5180,7 @@ function uninstall() {
|
|
|
4444
5180
|
return;
|
|
4445
5181
|
}
|
|
4446
5182
|
const cur = JSON.parse(
|
|
4447
|
-
|
|
5183
|
+
readFileSync9(CLAUDE_SETTINGS, "utf8")
|
|
4448
5184
|
);
|
|
4449
5185
|
if (cur.hooks) {
|
|
4450
5186
|
for (const [evt, entries] of Object.entries(cur.hooks)) {
|
|
@@ -4460,7 +5196,7 @@ function uninstall() {
|
|
|
4460
5196
|
|
|
4461
5197
|
// src/index.ts
|
|
4462
5198
|
var program = new Command();
|
|
4463
|
-
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");
|
|
4464
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) => {
|
|
4465
5201
|
await start({ port: opts.port, noOpen: !opts.open });
|
|
4466
5202
|
});
|
|
@@ -4529,6 +5265,36 @@ galaxy.command("publish <slug>").description("Publish the current galaxy to the
|
|
|
4529
5265
|
galaxy.command("install <slug>").description("Pull and install a galaxy from the configured registry").action(async (slug) => {
|
|
4530
5266
|
await installFromRegistryCmd(slug);
|
|
4531
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
|
+
});
|
|
4532
5298
|
program.parseAsync(process.argv).catch((err) => {
|
|
4533
5299
|
console.error(err);
|
|
4534
5300
|
process.exit(1);
|