@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/skills.ts
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(`${BASE4}${path}`, init);
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 ${BASE4}: ${String(err)}`);
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 homedir3 } from "os";
769
- import { join as join5 } from "path";
770
- import { mkdirSync as mkdirSync2 } from "fs";
771
- var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join5(homedir3(), ".solix");
772
- var DB_PATH = join5(SOLIX_HOME2, "solix.db");
773
- var HOOKS_DIR2 = join5(SOLIX_HOME2, "hooks");
774
- var LOG_PATH = join5(SOLIX_HOME2, "solix.log");
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
- mkdirSync2(SOLIX_HOME2, { recursive: true });
777
- mkdirSync2(HOOKS_DIR2, { recursive: true });
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 existsSync6, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
938
- import { dirname as dirname4, extname, join as join8, resolve as resolve3 } from "path";
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 basename(cwd) || cwd;
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 = nanoid();
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 nanoid2 } from "nanoid";
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: nanoid2(),
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 existsSync4, readFileSync as readFileSync3 } from "fs";
1439
- import { dirname as dirname2, join as join6, resolve } from "path";
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 && existsSync4(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 (existsSync4(join6(c, "manifest.json"))) return c;
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 = join6(AGENTS_DIR, "manifest.json");
1462
- if (!existsSync4(path)) {
1719
+ const path = join8(AGENTS_DIR, "manifest.json");
1720
+ if (!existsSync5(path)) {
1463
1721
  return { version: 1, advisors: [] };
1464
1722
  }
1465
- return JSON.parse(readFileSync3(path, "utf8"));
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 = join6(AGENTS_DIR, a.agentMd);
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 (!existsSync4(advisor.agentMdPath)) {
1824
+ if (!existsSync5(advisor.agentMdPath)) {
1567
1825
  return "";
1568
1826
  }
1569
- return readFileSync3(advisor.agentMdPath, "utf8");
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 existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
1660
- import { dirname as dirname3, join as join7, resolve as resolve2 } from "path";
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 homedir4 } from "os";
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 (existsSync5(c)) return c;
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 = join7(homedir4(), ".claude", "skills");
2002
+ var ANTHROPIC_SKILLS_DIR = join10(homedir7(), ".claude", "skills");
1677
2003
  function parseSkillManifest(manifestPath, fallbackId) {
1678
2004
  try {
1679
- const txt = readFileSync4(manifestPath, "utf8");
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 (!existsSync5(dir)) continue;
1729
- for (const entry of readdirSync3(dir)) {
1730
- const full = join7(dir, entry);
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 = join7(full, "SKILL.md");
1739
- if (!existsSync5(manifestPath)) continue;
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 (!existsSync5(skill.manifestPath)) return "";
1765
- return readFileSync4(skill.manifestPath, "utf8");
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 nanoid3 } from "nanoid";
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(nanoid3(), sourceUrl ?? null, JSON.stringify(manifest), now());
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 = nanoid3();
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 = join8(webDist, safe === "/" ? "index.html" : safe);
2660
+ const candidate = join11(webDist, safe === "/" ? "index.html" : safe);
2311
2661
  let filePath = candidate;
2312
2662
  try {
2313
- if (!existsSync6(filePath) || statSync4(filePath).isDirectory()) {
2314
- filePath = join8(webDist, "index.html");
2663
+ if (!existsSync8(filePath) || statSync4(filePath).isDirectory()) {
2664
+ filePath = join11(webDist, "index.html");
2315
2665
  }
2316
2666
  } catch {
2317
- filePath = join8(webDist, "index.html");
2667
+ filePath = join11(webDist, "index.html");
2318
2668
  }
2319
- if (!existsSync6(filePath)) return c.notFound();
2320
- const data = readFileSync5(filePath);
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 existsSync6(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
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 (existsSync6(join8(c, "index.html"))) return c;
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 existsSync7, mkdirSync as mkdirSync3 } from "fs";
2418
- import { homedir as homedir5 } from "os";
2419
- import { basename as basename2, join as join9 } from "path";
2420
- import { nanoid as nanoid4 } from "nanoid";
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 = basename2(repoRoot);
2782
+ const repoName = basename3(repoRoot);
2433
2783
  const safeBranch = opts.branch.replace(/[^a-zA-Z0-9._-]+/g, "-");
2434
- const worktreesDir = join9(homedir5(), ".solix", "worktrees");
2435
- const path = join9(worktreesDir, `${repoName}-${safeBranch}`);
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
- mkdirSync3(worktreesDir, { recursive: true });
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}-${nanoid4(6)}`;
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 (!existsSync7(spawnCwd)) {
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-${nanoid4(8)}`;
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-${nanoid4(8)}`;
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 nanoid6 } from "nanoid";
3182
+ import { nanoid as nanoid7 } from "nanoid";
2833
3183
 
2834
3184
  // ../server/src/state/toolcalls.ts
2835
- import { nanoid as nanoid5 } from "nanoid";
3185
+ import { nanoid as nanoid6 } from "nanoid";
2836
3186
  function recordToolCall(db, input) {
2837
- const id = nanoid5();
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 = nanoid6();
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 = nanoid6();
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 existsSync8,
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 homedir6 } from "os";
3374
- import { join as join10 } from "path";
3375
- var TRANSCRIPT_BASE = join10(homedir6(), ".claude", "projects");
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 join10(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
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 (!existsSync8(filePath)) {
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 (existsSync8(filePath)) {
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 existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
4074
+ import { copyFileSync as copyFileSync2, existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3682
4075
  function uninstall() {
3683
- if (existsSync9(CLAUDE_BACKUP)) {
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 (!existsSync9(CLAUDE_SETTINGS)) {
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
- readFileSync6(CLAUDE_SETTINGS, "utf8")
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
- writeFileSync3(CLAUDE_SETTINGS, JSON.stringify(cur, null, 2) + "\n");
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
  });