@shmulikdav/solix 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -317,6 +317,32 @@ async function probeHealth(port) {
|
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
+
async function probeWrappers(port) {
|
|
321
|
+
try {
|
|
322
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/wrappers`, {
|
|
323
|
+
signal: AbortSignal.timeout(800)
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
label: "Active solix run wrappers",
|
|
329
|
+
detail: "server too old to report (pre-1.2.1)"
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const records = await res.json();
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
label: "Active solix run wrappers",
|
|
336
|
+
detail: records.length === 0 ? "none registered" : `${records.length} active`
|
|
337
|
+
};
|
|
338
|
+
} catch {
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
label: "Active solix run wrappers",
|
|
342
|
+
detail: "unknown \u2014 server unreachable"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
320
346
|
async function doctor() {
|
|
321
347
|
const port = Number(process.env.SOLIX_PORT ?? 4242);
|
|
322
348
|
const checks = [];
|
|
@@ -398,6 +424,7 @@ async function doctor() {
|
|
|
398
424
|
detail: skillCount > 0 ? `${skillCount} skills in ${SOLIX_SKILLS_DIR}` : "none"
|
|
399
425
|
});
|
|
400
426
|
checks.push(await probeHealth(port));
|
|
427
|
+
checks.push(await probeWrappers(port));
|
|
401
428
|
console.log("\nSolix Diagnostics\n");
|
|
402
429
|
let allOk = true;
|
|
403
430
|
for (const c of checks) {
|
|
@@ -671,11 +698,231 @@ function install(opts = {}) {
|
|
|
671
698
|
console.log(`[solix] merged hooks into ${CLAUDE_SETTINGS}`);
|
|
672
699
|
}
|
|
673
700
|
|
|
674
|
-
// src/
|
|
701
|
+
// src/install-shim.ts
|
|
702
|
+
import {
|
|
703
|
+
appendFileSync,
|
|
704
|
+
existsSync as existsSync4,
|
|
705
|
+
readFileSync as readFileSync3,
|
|
706
|
+
writeFileSync as writeFileSync3
|
|
707
|
+
} from "fs";
|
|
708
|
+
import { homedir as homedir3 } from "os";
|
|
709
|
+
import { basename, join as join5 } from "path";
|
|
710
|
+
var BLOCK_START = "# >>> solix shim >>>";
|
|
711
|
+
var BLOCK_END = "# <<< solix shim <<<";
|
|
712
|
+
function detectShellRcPath() {
|
|
713
|
+
const shell = process.env.SHELL ?? "";
|
|
714
|
+
const home = homedir3();
|
|
715
|
+
if (shell.endsWith("zsh") || existsSync4(join5(home, ".zshrc"))) {
|
|
716
|
+
return join5(home, ".zshrc");
|
|
717
|
+
}
|
|
718
|
+
if (shell.endsWith("bash") || existsSync4(join5(home, ".bashrc"))) {
|
|
719
|
+
return join5(home, ".bashrc");
|
|
720
|
+
}
|
|
721
|
+
if (existsSync4(join5(home, ".bash_profile"))) {
|
|
722
|
+
return join5(home, ".bash_profile");
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
function readRc(rcPath) {
|
|
727
|
+
return existsSync4(rcPath) ? readFileSync3(rcPath, "utf8") : "";
|
|
728
|
+
}
|
|
729
|
+
function blockText() {
|
|
730
|
+
return [
|
|
731
|
+
BLOCK_START,
|
|
732
|
+
"# Aliases `claude` to `solix run` so every claude session is wrapped",
|
|
733
|
+
"# by Solix. The Solix UI composer becomes write-enabled for these.",
|
|
734
|
+
"# Remove with `solix uninstall` (or delete this block manually).",
|
|
735
|
+
"alias claude='solix run'",
|
|
736
|
+
BLOCK_END,
|
|
737
|
+
""
|
|
738
|
+
].join("\n");
|
|
739
|
+
}
|
|
740
|
+
function installShim() {
|
|
741
|
+
const rcPath = detectShellRcPath();
|
|
742
|
+
if (!rcPath) {
|
|
743
|
+
console.error(
|
|
744
|
+
"[solix] couldn't find a shell rc file (.zshrc / .bashrc). Add `alias claude='solix run'` manually to your shell config."
|
|
745
|
+
);
|
|
746
|
+
process.exitCode = 1;
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const current = readRc(rcPath);
|
|
750
|
+
if (current.includes(BLOCK_START)) {
|
|
751
|
+
console.log(`[solix] shim already installed in ${rcPath}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
|
|
755
|
+
appendFileSync(rcPath, prefix + "\n" + blockText());
|
|
756
|
+
console.log(`[solix] shim added to ${basename(rcPath)}.`);
|
|
757
|
+
console.log(
|
|
758
|
+
`[solix] run \`exec $SHELL\` (or open a new terminal) to activate, then \`claude\` will route through \`solix run\`.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
function uninstallShim() {
|
|
762
|
+
const rcPath = detectShellRcPath();
|
|
763
|
+
if (!rcPath) return false;
|
|
764
|
+
const current = readRc(rcPath);
|
|
765
|
+
if (!current.includes(BLOCK_START)) return false;
|
|
766
|
+
const startIdx = current.indexOf(BLOCK_START);
|
|
767
|
+
const endIdx = current.indexOf(BLOCK_END);
|
|
768
|
+
if (endIdx < 0) return false;
|
|
769
|
+
const before = current.slice(0, startIdx).replace(/\n+$/, "\n");
|
|
770
|
+
const after = current.slice(endIdx + BLOCK_END.length).replace(/^\n+/, "");
|
|
771
|
+
writeFileSync3(rcPath, before + after);
|
|
772
|
+
console.log(`[solix] shim removed from ${basename(rcPath)}.`);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/run.ts
|
|
777
|
+
import { createServer as createUnixServer } from "net";
|
|
778
|
+
import { mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
779
|
+
import { homedir as homedir4 } from "os";
|
|
780
|
+
import { join as join6 } from "path";
|
|
781
|
+
import { nanoid } from "nanoid";
|
|
675
782
|
var PORT4 = process.env.SOLIX_PORT ?? "4242";
|
|
676
783
|
var BASE4 = `http://127.0.0.1:${PORT4}`;
|
|
784
|
+
async function registerWithServer(payload) {
|
|
785
|
+
try {
|
|
786
|
+
const res = await fetch(`${BASE4}/api/wrappers/register`, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: { "Content-Type": "application/json" },
|
|
789
|
+
body: JSON.stringify(payload),
|
|
790
|
+
signal: AbortSignal.timeout(800)
|
|
791
|
+
});
|
|
792
|
+
return res.ok;
|
|
793
|
+
} catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function unregisterFromServer(wrapperId) {
|
|
798
|
+
try {
|
|
799
|
+
await fetch(
|
|
800
|
+
`${BASE4}/api/wrappers/${encodeURIComponent(wrapperId)}/unregister`,
|
|
801
|
+
{ method: "POST", signal: AbortSignal.timeout(800) }
|
|
802
|
+
);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async function runWrapped(args) {
|
|
807
|
+
let pty;
|
|
808
|
+
try {
|
|
809
|
+
pty = await import("node-pty");
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.error(
|
|
812
|
+
"[solix run] node-pty failed to load. Bidirectional chat needs a working PTY; on most platforms a clean `pnpm install` (or `npm rebuild`) fixes it."
|
|
813
|
+
);
|
|
814
|
+
console.error(`[solix run] underlying error: ${err.message}`);
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
const wrapperId = nanoid(10);
|
|
818
|
+
const sockDir = join6(homedir4(), ".solix", "wrappers");
|
|
819
|
+
mkdirSync2(sockDir, { recursive: true });
|
|
820
|
+
const socketPath = join6(sockDir, `${wrapperId}.sock`);
|
|
821
|
+
const cwd = process.cwd();
|
|
822
|
+
const registered = await registerWithServer({ wrapperId, socketPath, cwd });
|
|
823
|
+
const cols = process.stdout.columns ?? 80;
|
|
824
|
+
const rows = process.stdout.rows ?? 24;
|
|
825
|
+
const term = pty.spawn("claude", args, {
|
|
826
|
+
name: process.env.TERM ?? "xterm-256color",
|
|
827
|
+
cols,
|
|
828
|
+
rows,
|
|
829
|
+
cwd,
|
|
830
|
+
env: { ...process.env, SOLIX_WRAPPER_ID: wrapperId }
|
|
831
|
+
});
|
|
832
|
+
const wasRaw = Boolean(process.stdin.isTTY);
|
|
833
|
+
if (wasRaw) process.stdin.setRawMode?.(true);
|
|
834
|
+
process.stdin.resume();
|
|
835
|
+
const onStdin = (chunk) => {
|
|
836
|
+
term.write(chunk.toString("utf8"));
|
|
837
|
+
};
|
|
838
|
+
process.stdin.on("data", onStdin);
|
|
839
|
+
term.onData((d) => {
|
|
840
|
+
process.stdout.write(d);
|
|
841
|
+
});
|
|
842
|
+
const onResize = () => {
|
|
843
|
+
term.resize(process.stdout.columns ?? 80, process.stdout.rows ?? 24);
|
|
844
|
+
};
|
|
845
|
+
process.stdout.on("resize", onResize);
|
|
846
|
+
const sockServer = createUnixServer((conn) => {
|
|
847
|
+
let buf = "";
|
|
848
|
+
conn.setEncoding("utf8");
|
|
849
|
+
conn.on("data", (chunk) => {
|
|
850
|
+
buf += chunk;
|
|
851
|
+
let i = buf.indexOf("\n");
|
|
852
|
+
while (i >= 0) {
|
|
853
|
+
const line = buf.slice(0, i).trim();
|
|
854
|
+
buf = buf.slice(i + 1);
|
|
855
|
+
if (line) {
|
|
856
|
+
try {
|
|
857
|
+
const msg = JSON.parse(line);
|
|
858
|
+
if (msg.type === "send_prompt" && typeof msg.text === "string") {
|
|
859
|
+
term.write(msg.text + "\r");
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
i = buf.indexOf("\n");
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
conn.on("error", () => {
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
sockServer.on("error", (err) => {
|
|
871
|
+
console.error(`[solix run] socket error: ${err.message}`);
|
|
872
|
+
});
|
|
873
|
+
sockServer.listen(socketPath);
|
|
874
|
+
if (registered) {
|
|
875
|
+
process.stderr.write(
|
|
876
|
+
`[solix run] wrapped \u2014 UI prompts to this session will land here. Don't type a prompt while the UI is sending one.
|
|
877
|
+
`
|
|
878
|
+
);
|
|
879
|
+
} else {
|
|
880
|
+
process.stderr.write(
|
|
881
|
+
`[solix run] note: Solix server not reachable at ${BASE4}; claude will run normally, but the UI composer won't be active.
|
|
882
|
+
`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
let cleaned = false;
|
|
886
|
+
const cleanup = async (exitCode = 0) => {
|
|
887
|
+
if (cleaned) return;
|
|
888
|
+
cleaned = true;
|
|
889
|
+
try {
|
|
890
|
+
sockServer.close();
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
unlinkSync(socketPath);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
if (registered) await unregisterFromServer(wrapperId);
|
|
898
|
+
process.stdin.removeListener("data", onStdin);
|
|
899
|
+
process.stdout.removeListener("resize", onResize);
|
|
900
|
+
if (wasRaw) {
|
|
901
|
+
try {
|
|
902
|
+
process.stdin.setRawMode?.(false);
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
process.exit(exitCode);
|
|
907
|
+
};
|
|
908
|
+
term.onExit(({ exitCode }) => {
|
|
909
|
+
void cleanup(exitCode ?? 0);
|
|
910
|
+
});
|
|
911
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
912
|
+
process.on(sig, () => {
|
|
913
|
+
try {
|
|
914
|
+
term.kill();
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/skills.ts
|
|
922
|
+
var PORT5 = process.env.SOLIX_PORT ?? "4242";
|
|
923
|
+
var BASE5 = `http://127.0.0.1:${PORT5}`;
|
|
677
924
|
async function api3(path, init) {
|
|
678
|
-
const res = await fetch(`${
|
|
925
|
+
const res = await fetch(`${BASE5}${path}`, init);
|
|
679
926
|
if (!res.ok) {
|
|
680
927
|
const text = await res.text().catch(() => "");
|
|
681
928
|
throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
|
|
@@ -699,7 +946,7 @@ async function listSkillsCmd() {
|
|
|
699
946
|
);
|
|
700
947
|
}
|
|
701
948
|
} catch (err) {
|
|
702
|
-
console.error(`[solix] could not reach server at ${
|
|
949
|
+
console.error(`[solix] could not reach server at ${BASE5}: ${String(err)}`);
|
|
703
950
|
process.exitCode = 1;
|
|
704
951
|
}
|
|
705
952
|
}
|
|
@@ -765,16 +1012,16 @@ var Broadcaster = class {
|
|
|
765
1012
|
import Database from "better-sqlite3";
|
|
766
1013
|
|
|
767
1014
|
// ../server/src/paths.ts
|
|
768
|
-
import { homedir as
|
|
769
|
-
import { join as
|
|
770
|
-
import { mkdirSync as
|
|
771
|
-
var SOLIX_HOME2 = process.env.SOLIX_HOME ??
|
|
772
|
-
var DB_PATH =
|
|
773
|
-
var HOOKS_DIR2 =
|
|
774
|
-
var LOG_PATH =
|
|
1015
|
+
import { homedir as homedir5 } from "os";
|
|
1016
|
+
import { join as join7 } from "path";
|
|
1017
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
1018
|
+
var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join7(homedir5(), ".solix");
|
|
1019
|
+
var DB_PATH = join7(SOLIX_HOME2, "solix.db");
|
|
1020
|
+
var HOOKS_DIR2 = join7(SOLIX_HOME2, "hooks");
|
|
1021
|
+
var LOG_PATH = join7(SOLIX_HOME2, "solix.log");
|
|
775
1022
|
function ensureSolixHome() {
|
|
776
|
-
|
|
777
|
-
|
|
1023
|
+
mkdirSync3(SOLIX_HOME2, { recursive: true });
|
|
1024
|
+
mkdirSync3(HOOKS_DIR2, { recursive: true });
|
|
778
1025
|
}
|
|
779
1026
|
|
|
780
1027
|
// ../server/src/db.ts
|
|
@@ -927,6 +1174,7 @@ function getDb() {
|
|
|
927
1174
|
ensureColumn(db, "sessions", "kind", "kind TEXT NOT NULL DEFAULT 'user'");
|
|
928
1175
|
ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
|
|
929
1176
|
ensureColumn(db, "sessions", "worktree_path", "worktree_path TEXT");
|
|
1177
|
+
ensureColumn(db, "sessions", "wrapper_socket_path", "wrapper_socket_path TEXT");
|
|
930
1178
|
ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
|
|
931
1179
|
ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
|
|
932
1180
|
_db = db;
|
|
@@ -934,8 +1182,8 @@ function getDb() {
|
|
|
934
1182
|
}
|
|
935
1183
|
|
|
936
1184
|
// ../server/src/http.ts
|
|
937
|
-
import { existsSync as
|
|
938
|
-
import { dirname as dirname4, extname, join as
|
|
1185
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
1186
|
+
import { dirname as dirname4, extname, join as join11, resolve as resolve3 } from "path";
|
|
939
1187
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
940
1188
|
import { spawnSync } from "child_process";
|
|
941
1189
|
import { Hono } from "hono";
|
|
@@ -943,12 +1191,12 @@ import { cors } from "hono/cors";
|
|
|
943
1191
|
|
|
944
1192
|
// ../server/src/util.ts
|
|
945
1193
|
import { createHash } from "crypto";
|
|
946
|
-
import { basename } from "path";
|
|
1194
|
+
import { basename as basename2 } from "path";
|
|
947
1195
|
function hashCwd(cwd) {
|
|
948
1196
|
return createHash("sha1").update(cwd).digest("hex").slice(0, 12);
|
|
949
1197
|
}
|
|
950
1198
|
function projectNameFromCwd(cwd) {
|
|
951
|
-
return
|
|
1199
|
+
return basename2(cwd) || cwd;
|
|
952
1200
|
}
|
|
953
1201
|
function now() {
|
|
954
1202
|
return Date.now();
|
|
@@ -1013,7 +1261,8 @@ function rowToSession(row) {
|
|
|
1013
1261
|
lastCompletedMissionId: row.last_completed_mission_id ?? void 0,
|
|
1014
1262
|
orbitSlot: row.orbit_slot,
|
|
1015
1263
|
name: row.name ?? void 0,
|
|
1016
|
-
worktreePath: row.worktree_path ?? void 0
|
|
1264
|
+
worktreePath: row.worktree_path ?? void 0,
|
|
1265
|
+
wrapperSocketPath: row.wrapper_socket_path ?? void 0
|
|
1017
1266
|
};
|
|
1018
1267
|
}
|
|
1019
1268
|
function nextOrbitSlot(db, projectId) {
|
|
@@ -1048,9 +1297,9 @@ function upsertSession(db, input) {
|
|
|
1048
1297
|
`INSERT INTO sessions (
|
|
1049
1298
|
id, pid, project_id, parent_session_id, origin, model, status,
|
|
1050
1299
|
context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
|
|
1051
|
-
worktree_path, created_at, updated_at
|
|
1300
|
+
worktree_path, wrapper_socket_path, created_at, updated_at
|
|
1052
1301
|
)
|
|
1053
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?)`
|
|
1302
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?)`
|
|
1054
1303
|
).run(
|
|
1055
1304
|
input.id,
|
|
1056
1305
|
input.pid,
|
|
@@ -1064,6 +1313,7 @@ function upsertSession(db, input) {
|
|
|
1064
1313
|
kind,
|
|
1065
1314
|
input.advisorRole ?? null,
|
|
1066
1315
|
input.worktreePath ?? null,
|
|
1316
|
+
input.wrapperSocketPath ?? null,
|
|
1067
1317
|
ts2,
|
|
1068
1318
|
ts2
|
|
1069
1319
|
);
|
|
@@ -1082,7 +1332,8 @@ function upsertSession(db, input) {
|
|
|
1082
1332
|
parentSessionId: input.parentSessionId,
|
|
1083
1333
|
contextUsagePct: 0,
|
|
1084
1334
|
orbitSlot,
|
|
1085
|
-
worktreePath: input.worktreePath
|
|
1335
|
+
worktreePath: input.worktreePath,
|
|
1336
|
+
wrapperSocketPath: input.wrapperSocketPath
|
|
1086
1337
|
};
|
|
1087
1338
|
}
|
|
1088
1339
|
function setSessionStatus(db, sessionId, status) {
|
|
@@ -1105,6 +1356,13 @@ function setSessionMission(db, sessionId, missionId) {
|
|
|
1105
1356
|
).run(missionId, ts2, sessionId);
|
|
1106
1357
|
return getSession(db, sessionId);
|
|
1107
1358
|
}
|
|
1359
|
+
function clearSessionWrapper(db, sessionId) {
|
|
1360
|
+
const ts2 = now();
|
|
1361
|
+
db.prepare(
|
|
1362
|
+
`UPDATE sessions SET wrapper_socket_path = NULL, updated_at = ? WHERE id = ?`
|
|
1363
|
+
).run(ts2, sessionId);
|
|
1364
|
+
return getSession(db, sessionId);
|
|
1365
|
+
}
|
|
1108
1366
|
function setSessionContextUsage(db, sessionId, pct) {
|
|
1109
1367
|
const clamped = Math.max(0, Math.min(100, pct));
|
|
1110
1368
|
const ts2 = now();
|
|
@@ -1135,7 +1393,7 @@ function listSessionsForProject(db, projectId) {
|
|
|
1135
1393
|
}
|
|
1136
1394
|
|
|
1137
1395
|
// ../server/src/state/missions.ts
|
|
1138
|
-
import { nanoid } from "nanoid";
|
|
1396
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1139
1397
|
function rowToMission(row) {
|
|
1140
1398
|
let filesTouched = [];
|
|
1141
1399
|
try {
|
|
@@ -1178,7 +1436,7 @@ function shortNameFromPrompt(prompt) {
|
|
|
1178
1436
|
).filter(Boolean).join(" ") || "New Mission";
|
|
1179
1437
|
}
|
|
1180
1438
|
function startMission(db, sessionId, prompt) {
|
|
1181
|
-
const id =
|
|
1439
|
+
const id = nanoid2();
|
|
1182
1440
|
const ts2 = now();
|
|
1183
1441
|
const shortName = shortNameFromPrompt(prompt);
|
|
1184
1442
|
db.prepare(
|
|
@@ -1357,7 +1615,7 @@ function loadTimeline(db, opts = {}) {
|
|
|
1357
1615
|
}
|
|
1358
1616
|
|
|
1359
1617
|
// ../server/src/state/audit.ts
|
|
1360
|
-
import { nanoid as
|
|
1618
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
1361
1619
|
function rowToAuditEvent(row) {
|
|
1362
1620
|
let payload;
|
|
1363
1621
|
if (row.payload_json) {
|
|
@@ -1380,7 +1638,7 @@ function rowToAuditEvent(row) {
|
|
|
1380
1638
|
}
|
|
1381
1639
|
function recordAudit(db, input) {
|
|
1382
1640
|
const event = {
|
|
1383
|
-
id:
|
|
1641
|
+
id: nanoid3(),
|
|
1384
1642
|
ts: Date.now(),
|
|
1385
1643
|
kind: input.kind,
|
|
1386
1644
|
sessionId: input.sessionId,
|
|
@@ -1435,11 +1693,11 @@ function listAudit(db, opts = {}) {
|
|
|
1435
1693
|
}
|
|
1436
1694
|
|
|
1437
1695
|
// ../server/src/state/advisors.ts
|
|
1438
|
-
import { existsSync as
|
|
1439
|
-
import { dirname as dirname2, join as
|
|
1696
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1697
|
+
import { dirname as dirname2, join as join8, resolve } from "path";
|
|
1440
1698
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1441
1699
|
function findAgentsDir() {
|
|
1442
|
-
if (process.env.SOLIX_AGENTS_DIR &&
|
|
1700
|
+
if (process.env.SOLIX_AGENTS_DIR && existsSync5(process.env.SOLIX_AGENTS_DIR)) {
|
|
1443
1701
|
return process.env.SOLIX_AGENTS_DIR;
|
|
1444
1702
|
}
|
|
1445
1703
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
@@ -1452,17 +1710,17 @@ function findAgentsDir() {
|
|
|
1452
1710
|
resolve(process.cwd(), "packages", "agents")
|
|
1453
1711
|
];
|
|
1454
1712
|
for (const c of candidates) {
|
|
1455
|
-
if (
|
|
1713
|
+
if (existsSync5(join8(c, "manifest.json"))) return c;
|
|
1456
1714
|
}
|
|
1457
1715
|
return candidates[0];
|
|
1458
1716
|
}
|
|
1459
1717
|
var AGENTS_DIR = findAgentsDir();
|
|
1460
1718
|
function readManifest() {
|
|
1461
|
-
const path =
|
|
1462
|
-
if (!
|
|
1719
|
+
const path = join8(AGENTS_DIR, "manifest.json");
|
|
1720
|
+
if (!existsSync5(path)) {
|
|
1463
1721
|
return { version: 1, advisors: [] };
|
|
1464
1722
|
}
|
|
1465
|
-
return JSON.parse(
|
|
1723
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1466
1724
|
}
|
|
1467
1725
|
function rowToAdvisor(row) {
|
|
1468
1726
|
let requiredSkills = [];
|
|
@@ -1505,7 +1763,7 @@ function seedAdvisors(db) {
|
|
|
1505
1763
|
WHERE id = ?`
|
|
1506
1764
|
);
|
|
1507
1765
|
for (const a of manifest.advisors) {
|
|
1508
|
-
const md =
|
|
1766
|
+
const md = join8(AGENTS_DIR, a.agentMd);
|
|
1509
1767
|
insert.run(
|
|
1510
1768
|
a.id,
|
|
1511
1769
|
a.role,
|
|
@@ -1563,10 +1821,78 @@ function setAdvisorPinned(db, id, pinned, sessionId) {
|
|
|
1563
1821
|
return getAdvisor(db, id);
|
|
1564
1822
|
}
|
|
1565
1823
|
function readAdvisorAgentMd(advisor) {
|
|
1566
|
-
if (!
|
|
1824
|
+
if (!existsSync5(advisor.agentMdPath)) {
|
|
1567
1825
|
return "";
|
|
1568
1826
|
}
|
|
1569
|
-
return
|
|
1827
|
+
return readFileSync4(advisor.agentMdPath, "utf8");
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// ../server/src/state/wrappers.ts
|
|
1831
|
+
import { connect } from "net";
|
|
1832
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
1833
|
+
import { homedir as homedir6 } from "os";
|
|
1834
|
+
import { join as join9 } from "path";
|
|
1835
|
+
var wrappers = /* @__PURE__ */ new Map();
|
|
1836
|
+
var wrapperToSession = /* @__PURE__ */ new Map();
|
|
1837
|
+
var FRESHNESS_WINDOW_MS = 6e4;
|
|
1838
|
+
function registerWrapper(rec) {
|
|
1839
|
+
wrappers.set(rec.wrapperId, rec);
|
|
1840
|
+
}
|
|
1841
|
+
function unregisterWrapper(wrapperId) {
|
|
1842
|
+
wrappers.delete(wrapperId);
|
|
1843
|
+
const sessionId = wrapperToSession.get(wrapperId);
|
|
1844
|
+
wrapperToSession.delete(wrapperId);
|
|
1845
|
+
return sessionId;
|
|
1846
|
+
}
|
|
1847
|
+
function bindWrapperToSession(wrapperId, sessionId) {
|
|
1848
|
+
wrapperToSession.set(wrapperId, sessionId);
|
|
1849
|
+
}
|
|
1850
|
+
function listWrappers() {
|
|
1851
|
+
return [...wrappers.values()];
|
|
1852
|
+
}
|
|
1853
|
+
function cleanupOrphanedSockets() {
|
|
1854
|
+
const dir = join9(homedir6(), ".solix", "wrappers");
|
|
1855
|
+
if (!existsSync6(dir)) return 0;
|
|
1856
|
+
let removed = 0;
|
|
1857
|
+
for (const f of readdirSync3(dir)) {
|
|
1858
|
+
if (!f.endsWith(".sock")) continue;
|
|
1859
|
+
try {
|
|
1860
|
+
unlinkSync2(join9(dir, f));
|
|
1861
|
+
removed++;
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return removed;
|
|
1866
|
+
}
|
|
1867
|
+
function claimWrapperForCwd(cwd) {
|
|
1868
|
+
const now2 = Date.now();
|
|
1869
|
+
let best;
|
|
1870
|
+
for (const rec of wrappers.values()) {
|
|
1871
|
+
if (rec.cwd !== cwd) continue;
|
|
1872
|
+
if (now2 - rec.registeredAt > FRESHNESS_WINDOW_MS) continue;
|
|
1873
|
+
if (!best || rec.registeredAt > best.registeredAt) best = rec;
|
|
1874
|
+
}
|
|
1875
|
+
if (best) {
|
|
1876
|
+
wrappers.delete(best.wrapperId);
|
|
1877
|
+
}
|
|
1878
|
+
return best;
|
|
1879
|
+
}
|
|
1880
|
+
function writeToWrapperSocket(socketPath, text) {
|
|
1881
|
+
if (!existsSync6(socketPath)) return false;
|
|
1882
|
+
try {
|
|
1883
|
+
const client = connect(socketPath);
|
|
1884
|
+
client.on("error", () => {
|
|
1885
|
+
try {
|
|
1886
|
+
client.destroy();
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
client.write(JSON.stringify({ type: "send_prompt", text }) + "\n");
|
|
1891
|
+
client.end();
|
|
1892
|
+
return true;
|
|
1893
|
+
} catch {
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1570
1896
|
}
|
|
1571
1897
|
|
|
1572
1898
|
// ../server/src/state/context.ts
|
|
@@ -1656,10 +1982,10 @@ function buildContextEnvelope(db, args) {
|
|
|
1656
1982
|
}
|
|
1657
1983
|
|
|
1658
1984
|
// ../server/src/state/skills.ts
|
|
1659
|
-
import { existsSync as
|
|
1660
|
-
import { dirname as dirname3, join as
|
|
1985
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1986
|
+
import { dirname as dirname3, join as join10, resolve as resolve2 } from "path";
|
|
1661
1987
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1662
|
-
import { homedir as
|
|
1988
|
+
import { homedir as homedir7 } from "os";
|
|
1663
1989
|
function findSolixSkillsDir() {
|
|
1664
1990
|
const here = dirname3(fileURLToPath3(import.meta.url));
|
|
1665
1991
|
const candidates = [
|
|
@@ -1668,15 +1994,15 @@ function findSolixSkillsDir() {
|
|
|
1668
1994
|
resolve2(process.cwd(), "packages", "skills")
|
|
1669
1995
|
];
|
|
1670
1996
|
for (const c of candidates) {
|
|
1671
|
-
if (
|
|
1997
|
+
if (existsSync7(c)) return c;
|
|
1672
1998
|
}
|
|
1673
1999
|
return candidates[0];
|
|
1674
2000
|
}
|
|
1675
2001
|
var SOLIX_SKILLS_DIR2 = findSolixSkillsDir();
|
|
1676
|
-
var ANTHROPIC_SKILLS_DIR =
|
|
2002
|
+
var ANTHROPIC_SKILLS_DIR = join10(homedir7(), ".claude", "skills");
|
|
1677
2003
|
function parseSkillManifest(manifestPath, fallbackId) {
|
|
1678
2004
|
try {
|
|
1679
|
-
const txt =
|
|
2005
|
+
const txt = readFileSync5(manifestPath, "utf8");
|
|
1680
2006
|
const match = txt.match(/^---\n([\s\S]*?)\n---/);
|
|
1681
2007
|
let name = fallbackId;
|
|
1682
2008
|
let description = "";
|
|
@@ -1725,9 +2051,9 @@ function discoverSkills(db) {
|
|
|
1725
2051
|
{ dir: SOLIX_SKILLS_DIR2, source: "solix" }
|
|
1726
2052
|
];
|
|
1727
2053
|
for (const { dir, source } of sources) {
|
|
1728
|
-
if (!
|
|
1729
|
-
for (const entry of
|
|
1730
|
-
const full =
|
|
2054
|
+
if (!existsSync7(dir)) continue;
|
|
2055
|
+
for (const entry of readdirSync4(dir)) {
|
|
2056
|
+
const full = join10(dir, entry);
|
|
1731
2057
|
let isDir = false;
|
|
1732
2058
|
try {
|
|
1733
2059
|
isDir = statSync3(full).isDirectory();
|
|
@@ -1735,8 +2061,8 @@ function discoverSkills(db) {
|
|
|
1735
2061
|
continue;
|
|
1736
2062
|
}
|
|
1737
2063
|
if (!isDir) continue;
|
|
1738
|
-
const manifestPath =
|
|
1739
|
-
if (!
|
|
2064
|
+
const manifestPath = join10(full, "SKILL.md");
|
|
2065
|
+
if (!existsSync7(manifestPath)) continue;
|
|
1740
2066
|
const parsed = parseSkillManifest(manifestPath, entry);
|
|
1741
2067
|
if (!parsed) continue;
|
|
1742
2068
|
const id = `${source}:${parsed.id}`;
|
|
@@ -1761,8 +2087,8 @@ function getSkill(db, id) {
|
|
|
1761
2087
|
return row ? rowToSkill(row) : null;
|
|
1762
2088
|
}
|
|
1763
2089
|
function readSkillManifest(skill) {
|
|
1764
|
-
if (!
|
|
1765
|
-
return
|
|
2090
|
+
if (!existsSync7(skill.manifestPath)) return "";
|
|
2091
|
+
return readFileSync5(skill.manifestPath, "utf8");
|
|
1766
2092
|
}
|
|
1767
2093
|
function recordSkillInstall(db, skillId, projectId) {
|
|
1768
2094
|
const skill = getSkill(db, skillId);
|
|
@@ -1775,7 +2101,7 @@ function recordSkillInstall(db, skillId, projectId) {
|
|
|
1775
2101
|
}
|
|
1776
2102
|
|
|
1777
2103
|
// ../server/src/state/galaxy.ts
|
|
1778
|
-
import { nanoid as
|
|
2104
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
1779
2105
|
function exportManifest(db, opts = {}) {
|
|
1780
2106
|
const advisors2 = listAdvisors(db);
|
|
1781
2107
|
const skills2 = listSkills(db);
|
|
@@ -1823,7 +2149,7 @@ function importManifest(db, manifest, sourceUrl) {
|
|
|
1823
2149
|
db.prepare(
|
|
1824
2150
|
`INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
|
|
1825
2151
|
VALUES (?, ?, ?, ?)`
|
|
1826
|
-
).run(
|
|
2152
|
+
).run(nanoid4(), sourceUrl ?? null, JSON.stringify(manifest), now());
|
|
1827
2153
|
return {
|
|
1828
2154
|
advisorsEnabled: enabled,
|
|
1829
2155
|
advisorsDisabled: disabled,
|
|
@@ -1884,7 +2210,7 @@ function snapshotExport(db, manifest) {
|
|
|
1884
2210
|
return rowToVersion(existing);
|
|
1885
2211
|
}
|
|
1886
2212
|
}
|
|
1887
|
-
const id =
|
|
2213
|
+
const id = nanoid4();
|
|
1888
2214
|
const ts2 = now();
|
|
1889
2215
|
const ordinal = (last?.ordinal ?? 0) + 1;
|
|
1890
2216
|
db.prepare(
|
|
@@ -2278,6 +2604,30 @@ function createHttpApp(opts) {
|
|
|
2278
2604
|
}
|
|
2279
2605
|
});
|
|
2280
2606
|
app.get("/api/galaxy/imports", (c) => c.json(listImportHistory(opts.db)));
|
|
2607
|
+
app.post("/api/wrappers/register", async (c) => {
|
|
2608
|
+
const body = await c.req.json().catch(() => null);
|
|
2609
|
+
if (!body?.wrapperId || !body.socketPath || !body.cwd) {
|
|
2610
|
+
return c.json({ error: "wrapperId, socketPath, cwd required" }, 400);
|
|
2611
|
+
}
|
|
2612
|
+
registerWrapper({
|
|
2613
|
+
wrapperId: body.wrapperId,
|
|
2614
|
+
socketPath: body.socketPath,
|
|
2615
|
+
cwd: body.cwd,
|
|
2616
|
+
registeredAt: Date.now()
|
|
2617
|
+
});
|
|
2618
|
+
return c.json({ ok: true });
|
|
2619
|
+
});
|
|
2620
|
+
app.post("/api/wrappers/:id/unregister", (c) => {
|
|
2621
|
+
const sessionId = unregisterWrapper(c.req.param("id"));
|
|
2622
|
+
if (sessionId) {
|
|
2623
|
+
const cleared = clearSessionWrapper(opts.db, sessionId);
|
|
2624
|
+
if (cleared) {
|
|
2625
|
+
opts.router.broadcastSessionUpsert(cleared);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return c.json({ ok: true });
|
|
2629
|
+
});
|
|
2630
|
+
app.get("/api/wrappers", (c) => c.json(listWrappers()));
|
|
2281
2631
|
let preflightCache = null;
|
|
2282
2632
|
app.get("/api/system/preflight", (c) => {
|
|
2283
2633
|
if (preflightCache) return c.json(preflightCache);
|
|
@@ -2307,17 +2657,17 @@ function createHttpApp(opts) {
|
|
|
2307
2657
|
return c.notFound();
|
|
2308
2658
|
}
|
|
2309
2659
|
const safe = url.pathname.replace(/\.\.+/g, ".");
|
|
2310
|
-
const candidate =
|
|
2660
|
+
const candidate = join11(webDist, safe === "/" ? "index.html" : safe);
|
|
2311
2661
|
let filePath = candidate;
|
|
2312
2662
|
try {
|
|
2313
|
-
if (!
|
|
2314
|
-
filePath =
|
|
2663
|
+
if (!existsSync8(filePath) || statSync4(filePath).isDirectory()) {
|
|
2664
|
+
filePath = join11(webDist, "index.html");
|
|
2315
2665
|
}
|
|
2316
2666
|
} catch {
|
|
2317
|
-
filePath =
|
|
2667
|
+
filePath = join11(webDist, "index.html");
|
|
2318
2668
|
}
|
|
2319
|
-
if (!
|
|
2320
|
-
const data =
|
|
2669
|
+
if (!existsSync8(filePath)) return c.notFound();
|
|
2670
|
+
const data = readFileSync6(filePath);
|
|
2321
2671
|
return new Response(data, {
|
|
2322
2672
|
headers: { "Content-Type": mimeFor(filePath) }
|
|
2323
2673
|
});
|
|
@@ -2375,7 +2725,7 @@ function createHttpApp(opts) {
|
|
|
2375
2725
|
}
|
|
2376
2726
|
function findWebDist() {
|
|
2377
2727
|
if (process.env.SOLIX_WEB_DIST) {
|
|
2378
|
-
return
|
|
2728
|
+
return existsSync8(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
|
|
2379
2729
|
}
|
|
2380
2730
|
const here = dirname4(fileURLToPath4(import.meta.url));
|
|
2381
2731
|
const candidates = [
|
|
@@ -2388,7 +2738,7 @@ function findWebDist() {
|
|
|
2388
2738
|
resolve3(process.cwd(), "packages", "web", "dist")
|
|
2389
2739
|
];
|
|
2390
2740
|
for (const c of candidates) {
|
|
2391
|
-
if (
|
|
2741
|
+
if (existsSync8(join11(c, "index.html"))) return c;
|
|
2392
2742
|
}
|
|
2393
2743
|
return null;
|
|
2394
2744
|
}
|
|
@@ -2414,10 +2764,10 @@ function mimeFor(filePath) {
|
|
|
2414
2764
|
|
|
2415
2765
|
// ../server/src/launcher.ts
|
|
2416
2766
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
2417
|
-
import { existsSync as
|
|
2418
|
-
import { homedir as
|
|
2419
|
-
import { basename as
|
|
2420
|
-
import { nanoid as
|
|
2767
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
|
|
2768
|
+
import { homedir as homedir8 } from "os";
|
|
2769
|
+
import { basename as basename3, join as join12 } from "path";
|
|
2770
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2421
2771
|
function ensureWorktree(opts) {
|
|
2422
2772
|
const repoRoot = (() => {
|
|
2423
2773
|
const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -2429,10 +2779,10 @@ function ensureWorktree(opts) {
|
|
|
2429
2779
|
}
|
|
2430
2780
|
return (r.stdout ?? "").trim();
|
|
2431
2781
|
})();
|
|
2432
|
-
const repoName =
|
|
2782
|
+
const repoName = basename3(repoRoot);
|
|
2433
2783
|
const safeBranch = opts.branch.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
2434
|
-
const worktreesDir =
|
|
2435
|
-
const path =
|
|
2784
|
+
const worktreesDir = join12(homedir8(), ".solix", "worktrees");
|
|
2785
|
+
const path = join12(worktreesDir, `${repoName}-${safeBranch}`);
|
|
2436
2786
|
const list = spawnSync2("git", ["worktree", "list", "--porcelain"], {
|
|
2437
2787
|
cwd: repoRoot,
|
|
2438
2788
|
encoding: "utf8"
|
|
@@ -2440,7 +2790,7 @@ function ensureWorktree(opts) {
|
|
|
2440
2790
|
if (list.status === 0 && (list.stdout ?? "").includes(`worktree ${path}`)) {
|
|
2441
2791
|
return { path, created: false };
|
|
2442
2792
|
}
|
|
2443
|
-
|
|
2793
|
+
mkdirSync4(worktreesDir, { recursive: true });
|
|
2444
2794
|
const branchProbe = spawnSync2(
|
|
2445
2795
|
"git",
|
|
2446
2796
|
["rev-parse", "--verify", "--quiet", `refs/heads/${opts.branch}`],
|
|
@@ -2535,7 +2885,7 @@ var Launcher = class {
|
|
|
2535
2885
|
}
|
|
2536
2886
|
pinSynthetic(advisorId, codename, cwd) {
|
|
2537
2887
|
const project = ensureProject(this.db, cwd);
|
|
2538
|
-
const sessionId = `advisor-${advisorId}-${
|
|
2888
|
+
const sessionId = `advisor-${advisorId}-${nanoid5(6)}`;
|
|
2539
2889
|
const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
|
|
2540
2890
|
const session = upsertSession(this.db, {
|
|
2541
2891
|
id: sessionId,
|
|
@@ -2644,7 +2994,7 @@ var Launcher = class {
|
|
|
2644
2994
|
worktreePath
|
|
2645
2995
|
});
|
|
2646
2996
|
}
|
|
2647
|
-
if (!
|
|
2997
|
+
if (!existsSync9(spawnCwd)) {
|
|
2648
2998
|
this.broadcaster.broadcast({
|
|
2649
2999
|
type: "toast",
|
|
2650
3000
|
level: "error",
|
|
@@ -2655,7 +3005,7 @@ var Launcher = class {
|
|
|
2655
3005
|
const args = ["--print"];
|
|
2656
3006
|
if (opts.model) args.push("--model", String(opts.model));
|
|
2657
3007
|
args.push(opts.initialPrompt);
|
|
2658
|
-
const sessionId = `task-${
|
|
3008
|
+
const sessionId = `task-${nanoid5(8)}`;
|
|
2659
3009
|
return this.spawnPrint({
|
|
2660
3010
|
sessionId,
|
|
2661
3011
|
cwd: spawnCwd,
|
|
@@ -2783,7 +3133,7 @@ var Launcher = class {
|
|
|
2783
3133
|
}
|
|
2784
3134
|
launchSynthetic(opts) {
|
|
2785
3135
|
const project = ensureProject(this.db, opts.cwd);
|
|
2786
|
-
const sessionId = `task-${
|
|
3136
|
+
const sessionId = `task-${nanoid5(8)}`;
|
|
2787
3137
|
const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
|
|
2788
3138
|
upsertSession(this.db, {
|
|
2789
3139
|
id: sessionId,
|
|
@@ -2829,12 +3179,12 @@ var Launcher = class {
|
|
|
2829
3179
|
};
|
|
2830
3180
|
|
|
2831
3181
|
// ../server/src/router.ts
|
|
2832
|
-
import { nanoid as
|
|
3182
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
2833
3183
|
|
|
2834
3184
|
// ../server/src/state/toolcalls.ts
|
|
2835
|
-
import { nanoid as
|
|
3185
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
2836
3186
|
function recordToolCall(db, input) {
|
|
2837
|
-
const id =
|
|
3187
|
+
const id = nanoid6();
|
|
2838
3188
|
const ts2 = now();
|
|
2839
3189
|
const status = input.status ?? "running";
|
|
2840
3190
|
db.prepare(
|
|
@@ -2936,6 +3286,7 @@ var EventRouter = class {
|
|
|
2936
3286
|
const sessionId = this.extractSessionId(event);
|
|
2937
3287
|
const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
|
|
2938
3288
|
const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
|
|
3289
|
+
const wrapper = claimWrapperForCwd(event.cwd);
|
|
2939
3290
|
const session = upsertSession(this.db, {
|
|
2940
3291
|
id: sessionId,
|
|
2941
3292
|
pid: event.pid,
|
|
@@ -2946,8 +3297,10 @@ var EventRouter = class {
|
|
|
2946
3297
|
parentSessionId: this.extractParentSessionId(event),
|
|
2947
3298
|
kind: advisorRole ? "advisor" : "user",
|
|
2948
3299
|
advisorRole,
|
|
2949
|
-
worktreePath
|
|
3300
|
+
worktreePath,
|
|
3301
|
+
wrapperSocketPath: wrapper?.socketPath
|
|
2950
3302
|
});
|
|
3303
|
+
if (wrapper) bindWrapperToSession(wrapper.wrapperId, session.id);
|
|
2951
3304
|
this.broadcaster.broadcast({ type: "session_upsert", session });
|
|
2952
3305
|
if (!session.parentSessionId) {
|
|
2953
3306
|
this.transcripts?.startWatching(sessionId, event.cwd);
|
|
@@ -3013,7 +3366,7 @@ var EventRouter = class {
|
|
|
3013
3366
|
const parentSessionId = this.extractSessionId(event);
|
|
3014
3367
|
const parent = getSession(this.db, parentSessionId);
|
|
3015
3368
|
if (!parent) return;
|
|
3016
|
-
const subId =
|
|
3369
|
+
const subId = nanoid7();
|
|
3017
3370
|
const sub = upsertSession(this.db, {
|
|
3018
3371
|
id: subId,
|
|
3019
3372
|
pid: event.pid,
|
|
@@ -3090,7 +3443,7 @@ var EventRouter = class {
|
|
|
3090
3443
|
const p = event.payload;
|
|
3091
3444
|
const message = typeof p.message === "string" ? p.message : "Permission requested";
|
|
3092
3445
|
const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
|
|
3093
|
-
const requestId =
|
|
3446
|
+
const requestId = nanoid7();
|
|
3094
3447
|
this.permissions.set(requestId, {
|
|
3095
3448
|
requestId,
|
|
3096
3449
|
sessionId,
|
|
@@ -3230,6 +3583,34 @@ var EventRouter = class {
|
|
|
3230
3583
|
});
|
|
3231
3584
|
}
|
|
3232
3585
|
sendPromptToSession(sessionId, text) {
|
|
3586
|
+
const session = getSession(this.db, sessionId);
|
|
3587
|
+
if (!session) return false;
|
|
3588
|
+
if (session.wrapperSocketPath) {
|
|
3589
|
+
const ok = writeToWrapperSocket(session.wrapperSocketPath, text);
|
|
3590
|
+
if (ok) {
|
|
3591
|
+
this.broadcaster.broadcast({
|
|
3592
|
+
type: "chat_delta",
|
|
3593
|
+
sessionId,
|
|
3594
|
+
delta: {
|
|
3595
|
+
messageId: `u-${Date.now()}`,
|
|
3596
|
+
role: "user",
|
|
3597
|
+
content: text,
|
|
3598
|
+
ts: Date.now(),
|
|
3599
|
+
done: true
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
} else {
|
|
3603
|
+
const cleared = clearSessionWrapper(this.db, sessionId);
|
|
3604
|
+
if (cleared)
|
|
3605
|
+
this.broadcaster.broadcast({ type: "session_upsert", session: cleared });
|
|
3606
|
+
this.broadcaster.broadcast({
|
|
3607
|
+
type: "toast",
|
|
3608
|
+
level: "warn",
|
|
3609
|
+
message: `Wrapper for ${session.name ?? session.id.slice(0, 8)} exited \u2014 chat is now read-only. Restart with \`solix run\`.`
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
return ok;
|
|
3613
|
+
}
|
|
3233
3614
|
if (!this.launcher) return false;
|
|
3234
3615
|
return this.launcher.sendPromptToInternal(sessionId, text);
|
|
3235
3616
|
}
|
|
@@ -3238,6 +3619,11 @@ var EventRouter = class {
|
|
|
3238
3619
|
pendingPermissions() {
|
|
3239
3620
|
return [...this.permissions.values()];
|
|
3240
3621
|
}
|
|
3622
|
+
/** Public re-broadcast helper for cases where state mutates outside
|
|
3623
|
+
* the hook flow (e.g. wrapper unregister clearing the socket path). */
|
|
3624
|
+
broadcastSessionUpsert(session) {
|
|
3625
|
+
this.broadcaster.broadcast({ type: "session_upsert", session });
|
|
3626
|
+
}
|
|
3241
3627
|
broadcastGalaxyImported(manifest) {
|
|
3242
3628
|
this.broadcaster.broadcast({ type: "galaxy_imported", manifest });
|
|
3243
3629
|
this.broadcaster.broadcast({
|
|
@@ -3364,15 +3750,15 @@ function handleClientMessage(ctx, _ws, msg) {
|
|
|
3364
3750
|
// ../server/src/state/transcript.ts
|
|
3365
3751
|
import {
|
|
3366
3752
|
closeSync,
|
|
3367
|
-
existsSync as
|
|
3753
|
+
existsSync as existsSync10,
|
|
3368
3754
|
openSync,
|
|
3369
3755
|
readSync,
|
|
3370
3756
|
statSync as statSync5,
|
|
3371
3757
|
watch
|
|
3372
3758
|
} from "fs";
|
|
3373
|
-
import { homedir as
|
|
3374
|
-
import { join as
|
|
3375
|
-
var TRANSCRIPT_BASE =
|
|
3759
|
+
import { homedir as homedir9 } from "os";
|
|
3760
|
+
import { join as join13 } from "path";
|
|
3761
|
+
var TRANSCRIPT_BASE = join13(homedir9(), ".claude", "projects");
|
|
3376
3762
|
var CONTEXT_BUDGETS_BY_MODEL = {
|
|
3377
3763
|
"claude-opus-4-7": 2e5,
|
|
3378
3764
|
"claude-opus-4-6": 2e5,
|
|
@@ -3385,7 +3771,7 @@ function encodeProjectPath(cwd) {
|
|
|
3385
3771
|
return cwd.replace(/[/\\]/g, "-");
|
|
3386
3772
|
}
|
|
3387
3773
|
function transcriptPathFor(cwd, sessionId) {
|
|
3388
|
-
return
|
|
3774
|
+
return join13(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
|
|
3389
3775
|
}
|
|
3390
3776
|
var TranscriptWatcherManager = class {
|
|
3391
3777
|
constructor(db, broadcaster) {
|
|
@@ -3405,7 +3791,7 @@ var TranscriptWatcherManager = class {
|
|
|
3405
3791
|
startWatching(sessionId, cwd) {
|
|
3406
3792
|
if (this.records.has(sessionId)) return;
|
|
3407
3793
|
const filePath = transcriptPathFor(cwd, sessionId);
|
|
3408
|
-
if (!
|
|
3794
|
+
if (!existsSync10(filePath)) {
|
|
3409
3795
|
this.scheduleRetry(sessionId, cwd, 0);
|
|
3410
3796
|
return;
|
|
3411
3797
|
}
|
|
@@ -3416,7 +3802,7 @@ var TranscriptWatcherManager = class {
|
|
|
3416
3802
|
const t = setTimeout(() => {
|
|
3417
3803
|
this.deferredRetry.delete(sessionId);
|
|
3418
3804
|
const filePath = transcriptPathFor(cwd, sessionId);
|
|
3419
|
-
if (
|
|
3805
|
+
if (existsSync10(filePath)) {
|
|
3420
3806
|
this.attach(sessionId, filePath);
|
|
3421
3807
|
} else {
|
|
3422
3808
|
this.scheduleRetry(sessionId, cwd, attempt + 1);
|
|
@@ -3612,6 +3998,13 @@ async function createSolixServer(opts = {}) {
|
|
|
3612
3998
|
const db = getDb();
|
|
3613
3999
|
seedAdvisors(db);
|
|
3614
4000
|
discoverSkills(db);
|
|
4001
|
+
const cleared = cleanupOrphanedSockets();
|
|
4002
|
+
if (cleared > 0) {
|
|
4003
|
+
console.log(`[solix] cleaned up ${cleared} orphaned wrapper socket(s)`);
|
|
4004
|
+
}
|
|
4005
|
+
db.prepare(
|
|
4006
|
+
`UPDATE sessions SET wrapper_socket_path = NULL WHERE wrapper_socket_path IS NOT NULL`
|
|
4007
|
+
).run();
|
|
3615
4008
|
const broadcaster = new Broadcaster();
|
|
3616
4009
|
const launcher = new Launcher(db, broadcaster);
|
|
3617
4010
|
const transcripts = new TranscriptWatcherManager(db, broadcaster);
|
|
@@ -3678,19 +4071,20 @@ async function start(opts = {}) {
|
|
|
3678
4071
|
}
|
|
3679
4072
|
|
|
3680
4073
|
// src/uninstall.ts
|
|
3681
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
4074
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
3682
4075
|
function uninstall() {
|
|
3683
|
-
|
|
4076
|
+
uninstallShim();
|
|
4077
|
+
if (existsSync11(CLAUDE_BACKUP)) {
|
|
3684
4078
|
copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
|
|
3685
4079
|
console.log(`[solix] restored settings.json from backup`);
|
|
3686
4080
|
return;
|
|
3687
4081
|
}
|
|
3688
|
-
if (!
|
|
4082
|
+
if (!existsSync11(CLAUDE_SETTINGS)) {
|
|
3689
4083
|
console.log("[solix] nothing to uninstall (no settings.json found)");
|
|
3690
4084
|
return;
|
|
3691
4085
|
}
|
|
3692
4086
|
const cur = JSON.parse(
|
|
3693
|
-
|
|
4087
|
+
readFileSync7(CLAUDE_SETTINGS, "utf8")
|
|
3694
4088
|
);
|
|
3695
4089
|
if (cur.hooks) {
|
|
3696
4090
|
for (const [evt, entries] of Object.entries(cur.hooks)) {
|
|
@@ -3700,7 +4094,7 @@ function uninstall() {
|
|
|
3700
4094
|
if (cur.hooks[evt].length === 0) delete cur.hooks[evt];
|
|
3701
4095
|
}
|
|
3702
4096
|
}
|
|
3703
|
-
|
|
4097
|
+
writeFileSync4(CLAUDE_SETTINGS, JSON.stringify(cur, null, 2) + "\n");
|
|
3704
4098
|
console.log(`[solix] removed Solix hooks from ${CLAUDE_SETTINGS}`);
|
|
3705
4099
|
}
|
|
3706
4100
|
|
|
@@ -3717,6 +4111,16 @@ program.command("install").description("Install Solix hooks into ~/.claude/setti
|
|
|
3717
4111
|
program.command("uninstall").description("Restore ~/.claude/settings.json from backup").action(() => {
|
|
3718
4112
|
uninstall();
|
|
3719
4113
|
});
|
|
4114
|
+
program.command("run").description(
|
|
4115
|
+
"Wrap a claude session under a PTY so the Solix UI can send prompts to it. Pass any args you would normally pass to claude."
|
|
4116
|
+
).allowUnknownOption(true).helpOption(false).action(async (_opts, cmd) => {
|
|
4117
|
+
await runWrapped(cmd.args ?? []);
|
|
4118
|
+
});
|
|
4119
|
+
program.command("install-shim").description(
|
|
4120
|
+
"Add `alias claude='solix run'` to your shell rc so every claude session is wrapped automatically."
|
|
4121
|
+
).action(() => {
|
|
4122
|
+
installShim();
|
|
4123
|
+
});
|
|
3720
4124
|
program.command("doctor").description("Run diagnostics").action(async () => {
|
|
3721
4125
|
await doctor();
|
|
3722
4126
|
});
|