@shmulikdav/solix 1.1.0 → 1.2.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/index.js CHANGED
@@ -671,11 +671,231 @@ function install(opts = {}) {
671
671
  console.log(`[solix] merged hooks into ${CLAUDE_SETTINGS}`);
672
672
  }
673
673
 
674
- // src/skills.ts
674
+ // src/install-shim.ts
675
+ import {
676
+ appendFileSync,
677
+ existsSync as existsSync4,
678
+ readFileSync as readFileSync3,
679
+ writeFileSync as writeFileSync3
680
+ } from "fs";
681
+ import { homedir as homedir3 } from "os";
682
+ import { basename, join as join5 } from "path";
683
+ var BLOCK_START = "# >>> solix shim >>>";
684
+ var BLOCK_END = "# <<< solix shim <<<";
685
+ function detectShellRcPath() {
686
+ const shell = process.env.SHELL ?? "";
687
+ const home = homedir3();
688
+ if (shell.endsWith("zsh") || existsSync4(join5(home, ".zshrc"))) {
689
+ return join5(home, ".zshrc");
690
+ }
691
+ if (shell.endsWith("bash") || existsSync4(join5(home, ".bashrc"))) {
692
+ return join5(home, ".bashrc");
693
+ }
694
+ if (existsSync4(join5(home, ".bash_profile"))) {
695
+ return join5(home, ".bash_profile");
696
+ }
697
+ return null;
698
+ }
699
+ function readRc(rcPath) {
700
+ return existsSync4(rcPath) ? readFileSync3(rcPath, "utf8") : "";
701
+ }
702
+ function blockText() {
703
+ return [
704
+ BLOCK_START,
705
+ "# Aliases `claude` to `solix run` so every claude session is wrapped",
706
+ "# by Solix. The Solix UI composer becomes write-enabled for these.",
707
+ "# Remove with `solix uninstall` (or delete this block manually).",
708
+ "alias claude='solix run'",
709
+ BLOCK_END,
710
+ ""
711
+ ].join("\n");
712
+ }
713
+ function installShim() {
714
+ const rcPath = detectShellRcPath();
715
+ if (!rcPath) {
716
+ console.error(
717
+ "[solix] couldn't find a shell rc file (.zshrc / .bashrc). Add `alias claude='solix run'` manually to your shell config."
718
+ );
719
+ process.exitCode = 1;
720
+ return;
721
+ }
722
+ const current = readRc(rcPath);
723
+ if (current.includes(BLOCK_START)) {
724
+ console.log(`[solix] shim already installed in ${rcPath}`);
725
+ return;
726
+ }
727
+ const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
728
+ appendFileSync(rcPath, prefix + "\n" + blockText());
729
+ console.log(`[solix] shim added to ${basename(rcPath)}.`);
730
+ console.log(
731
+ `[solix] run \`exec $SHELL\` (or open a new terminal) to activate, then \`claude\` will route through \`solix run\`.`
732
+ );
733
+ }
734
+ function uninstallShim() {
735
+ const rcPath = detectShellRcPath();
736
+ if (!rcPath) return false;
737
+ const current = readRc(rcPath);
738
+ if (!current.includes(BLOCK_START)) return false;
739
+ const startIdx = current.indexOf(BLOCK_START);
740
+ const endIdx = current.indexOf(BLOCK_END);
741
+ if (endIdx < 0) return false;
742
+ const before = current.slice(0, startIdx).replace(/\n+$/, "\n");
743
+ const after = current.slice(endIdx + BLOCK_END.length).replace(/^\n+/, "");
744
+ writeFileSync3(rcPath, before + after);
745
+ console.log(`[solix] shim removed from ${basename(rcPath)}.`);
746
+ return true;
747
+ }
748
+
749
+ // src/run.ts
750
+ import { createServer as createUnixServer } from "net";
751
+ import { mkdirSync as mkdirSync2, unlinkSync } from "fs";
752
+ import { homedir as homedir4 } from "os";
753
+ import { join as join6 } from "path";
754
+ import { nanoid } from "nanoid";
675
755
  var PORT4 = process.env.SOLIX_PORT ?? "4242";
676
756
  var BASE4 = `http://127.0.0.1:${PORT4}`;
757
+ async function registerWithServer(payload) {
758
+ try {
759
+ const res = await fetch(`${BASE4}/api/wrappers/register`, {
760
+ method: "POST",
761
+ headers: { "Content-Type": "application/json" },
762
+ body: JSON.stringify(payload),
763
+ signal: AbortSignal.timeout(800)
764
+ });
765
+ return res.ok;
766
+ } catch {
767
+ return false;
768
+ }
769
+ }
770
+ async function unregisterFromServer(wrapperId) {
771
+ try {
772
+ await fetch(
773
+ `${BASE4}/api/wrappers/${encodeURIComponent(wrapperId)}/unregister`,
774
+ { method: "POST", signal: AbortSignal.timeout(800) }
775
+ );
776
+ } catch {
777
+ }
778
+ }
779
+ async function runWrapped(args) {
780
+ let pty;
781
+ try {
782
+ pty = await import("node-pty");
783
+ } catch (err) {
784
+ console.error(
785
+ "[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."
786
+ );
787
+ console.error(`[solix run] underlying error: ${err.message}`);
788
+ process.exit(1);
789
+ }
790
+ const wrapperId = nanoid(10);
791
+ const sockDir = join6(homedir4(), ".solix", "wrappers");
792
+ mkdirSync2(sockDir, { recursive: true });
793
+ const socketPath = join6(sockDir, `${wrapperId}.sock`);
794
+ const cwd = process.cwd();
795
+ const registered = await registerWithServer({ wrapperId, socketPath, cwd });
796
+ const cols = process.stdout.columns ?? 80;
797
+ const rows = process.stdout.rows ?? 24;
798
+ const term = pty.spawn("claude", args, {
799
+ name: process.env.TERM ?? "xterm-256color",
800
+ cols,
801
+ rows,
802
+ cwd,
803
+ env: { ...process.env, SOLIX_WRAPPER_ID: wrapperId }
804
+ });
805
+ const wasRaw = Boolean(process.stdin.isTTY);
806
+ if (wasRaw) process.stdin.setRawMode?.(true);
807
+ process.stdin.resume();
808
+ const onStdin = (chunk) => {
809
+ term.write(chunk.toString("utf8"));
810
+ };
811
+ process.stdin.on("data", onStdin);
812
+ term.onData((d) => {
813
+ process.stdout.write(d);
814
+ });
815
+ const onResize = () => {
816
+ term.resize(process.stdout.columns ?? 80, process.stdout.rows ?? 24);
817
+ };
818
+ process.stdout.on("resize", onResize);
819
+ const sockServer = createUnixServer((conn) => {
820
+ let buf = "";
821
+ conn.setEncoding("utf8");
822
+ conn.on("data", (chunk) => {
823
+ buf += chunk;
824
+ let i = buf.indexOf("\n");
825
+ while (i >= 0) {
826
+ const line = buf.slice(0, i).trim();
827
+ buf = buf.slice(i + 1);
828
+ if (line) {
829
+ try {
830
+ const msg = JSON.parse(line);
831
+ if (msg.type === "send_prompt" && typeof msg.text === "string") {
832
+ term.write(msg.text + "\r");
833
+ }
834
+ } catch {
835
+ }
836
+ }
837
+ i = buf.indexOf("\n");
838
+ }
839
+ });
840
+ conn.on("error", () => {
841
+ });
842
+ });
843
+ sockServer.on("error", (err) => {
844
+ console.error(`[solix run] socket error: ${err.message}`);
845
+ });
846
+ sockServer.listen(socketPath);
847
+ if (registered) {
848
+ process.stderr.write(
849
+ `[solix run] wrapped \u2014 UI prompts to this session will land here. Don't type a prompt while the UI is sending one.
850
+ `
851
+ );
852
+ } else {
853
+ process.stderr.write(
854
+ `[solix run] note: Solix server not reachable at ${BASE4}; claude will run normally, but the UI composer won't be active.
855
+ `
856
+ );
857
+ }
858
+ let cleaned = false;
859
+ const cleanup = async (exitCode = 0) => {
860
+ if (cleaned) return;
861
+ cleaned = true;
862
+ try {
863
+ sockServer.close();
864
+ } catch {
865
+ }
866
+ try {
867
+ unlinkSync(socketPath);
868
+ } catch {
869
+ }
870
+ if (registered) await unregisterFromServer(wrapperId);
871
+ process.stdin.removeListener("data", onStdin);
872
+ process.stdout.removeListener("resize", onResize);
873
+ if (wasRaw) {
874
+ try {
875
+ process.stdin.setRawMode?.(false);
876
+ } catch {
877
+ }
878
+ }
879
+ process.exit(exitCode);
880
+ };
881
+ term.onExit(({ exitCode }) => {
882
+ void cleanup(exitCode ?? 0);
883
+ });
884
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
885
+ process.on(sig, () => {
886
+ try {
887
+ term.kill();
888
+ } catch {
889
+ }
890
+ });
891
+ }
892
+ }
893
+
894
+ // src/skills.ts
895
+ var PORT5 = process.env.SOLIX_PORT ?? "4242";
896
+ var BASE5 = `http://127.0.0.1:${PORT5}`;
677
897
  async function api3(path, init) {
678
- const res = await fetch(`${BASE4}${path}`, init);
898
+ const res = await fetch(`${BASE5}${path}`, init);
679
899
  if (!res.ok) {
680
900
  const text = await res.text().catch(() => "");
681
901
  throw new Error(`HTTP ${res.status} on ${path}: ${text}`);
@@ -699,7 +919,7 @@ async function listSkillsCmd() {
699
919
  );
700
920
  }
701
921
  } catch (err) {
702
- console.error(`[solix] could not reach server at ${BASE4}: ${String(err)}`);
922
+ console.error(`[solix] could not reach server at ${BASE5}: ${String(err)}`);
703
923
  process.exitCode = 1;
704
924
  }
705
925
  }
@@ -765,16 +985,16 @@ var Broadcaster = class {
765
985
  import Database from "better-sqlite3";
766
986
 
767
987
  // ../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");
988
+ import { homedir as homedir5 } from "os";
989
+ import { join as join7 } from "path";
990
+ import { mkdirSync as mkdirSync3 } from "fs";
991
+ var SOLIX_HOME2 = process.env.SOLIX_HOME ?? join7(homedir5(), ".solix");
992
+ var DB_PATH = join7(SOLIX_HOME2, "solix.db");
993
+ var HOOKS_DIR2 = join7(SOLIX_HOME2, "hooks");
994
+ var LOG_PATH = join7(SOLIX_HOME2, "solix.log");
775
995
  function ensureSolixHome() {
776
- mkdirSync2(SOLIX_HOME2, { recursive: true });
777
- mkdirSync2(HOOKS_DIR2, { recursive: true });
996
+ mkdirSync3(SOLIX_HOME2, { recursive: true });
997
+ mkdirSync3(HOOKS_DIR2, { recursive: true });
778
998
  }
779
999
 
780
1000
  // ../server/src/db.ts
@@ -927,6 +1147,7 @@ function getDb() {
927
1147
  ensureColumn(db, "sessions", "kind", "kind TEXT NOT NULL DEFAULT 'user'");
928
1148
  ensureColumn(db, "sessions", "advisor_role", "advisor_role TEXT");
929
1149
  ensureColumn(db, "sessions", "worktree_path", "worktree_path TEXT");
1150
+ ensureColumn(db, "sessions", "wrapper_socket_path", "wrapper_socket_path TEXT");
930
1151
  ensureColumn(db, "advisors", "texture_pack", "texture_pack TEXT");
931
1152
  ensureColumn(db, "missions", "error_summary", "error_summary TEXT");
932
1153
  _db = db;
@@ -934,8 +1155,8 @@ function getDb() {
934
1155
  }
935
1156
 
936
1157
  // ../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";
1158
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
1159
+ import { dirname as dirname4, extname, join as join10, resolve as resolve3 } from "path";
939
1160
  import { fileURLToPath as fileURLToPath4 } from "url";
940
1161
  import { spawnSync } from "child_process";
941
1162
  import { Hono } from "hono";
@@ -943,12 +1164,12 @@ import { cors } from "hono/cors";
943
1164
 
944
1165
  // ../server/src/util.ts
945
1166
  import { createHash } from "crypto";
946
- import { basename } from "path";
1167
+ import { basename as basename2 } from "path";
947
1168
  function hashCwd(cwd) {
948
1169
  return createHash("sha1").update(cwd).digest("hex").slice(0, 12);
949
1170
  }
950
1171
  function projectNameFromCwd(cwd) {
951
- return basename(cwd) || cwd;
1172
+ return basename2(cwd) || cwd;
952
1173
  }
953
1174
  function now() {
954
1175
  return Date.now();
@@ -1013,7 +1234,8 @@ function rowToSession(row) {
1013
1234
  lastCompletedMissionId: row.last_completed_mission_id ?? void 0,
1014
1235
  orbitSlot: row.orbit_slot,
1015
1236
  name: row.name ?? void 0,
1016
- worktreePath: row.worktree_path ?? void 0
1237
+ worktreePath: row.worktree_path ?? void 0,
1238
+ wrapperSocketPath: row.wrapper_socket_path ?? void 0
1017
1239
  };
1018
1240
  }
1019
1241
  function nextOrbitSlot(db, projectId) {
@@ -1048,9 +1270,9 @@ function upsertSession(db, input) {
1048
1270
  `INSERT INTO sessions (
1049
1271
  id, pid, project_id, parent_session_id, origin, model, status,
1050
1272
  context_usage_pct, orbit_slot, cwd, name, kind, advisor_role,
1051
- worktree_path, created_at, updated_at
1273
+ worktree_path, wrapper_socket_path, created_at, updated_at
1052
1274
  )
1053
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?)`
1275
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, NULL, ?, ?, ?, ?, ?, ?)`
1054
1276
  ).run(
1055
1277
  input.id,
1056
1278
  input.pid,
@@ -1064,6 +1286,7 @@ function upsertSession(db, input) {
1064
1286
  kind,
1065
1287
  input.advisorRole ?? null,
1066
1288
  input.worktreePath ?? null,
1289
+ input.wrapperSocketPath ?? null,
1067
1290
  ts2,
1068
1291
  ts2
1069
1292
  );
@@ -1082,7 +1305,8 @@ function upsertSession(db, input) {
1082
1305
  parentSessionId: input.parentSessionId,
1083
1306
  contextUsagePct: 0,
1084
1307
  orbitSlot,
1085
- worktreePath: input.worktreePath
1308
+ worktreePath: input.worktreePath,
1309
+ wrapperSocketPath: input.wrapperSocketPath
1086
1310
  };
1087
1311
  }
1088
1312
  function setSessionStatus(db, sessionId, status) {
@@ -1135,7 +1359,7 @@ function listSessionsForProject(db, projectId) {
1135
1359
  }
1136
1360
 
1137
1361
  // ../server/src/state/missions.ts
1138
- import { nanoid } from "nanoid";
1362
+ import { nanoid as nanoid2 } from "nanoid";
1139
1363
  function rowToMission(row) {
1140
1364
  let filesTouched = [];
1141
1365
  try {
@@ -1178,7 +1402,7 @@ function shortNameFromPrompt(prompt) {
1178
1402
  ).filter(Boolean).join(" ") || "New Mission";
1179
1403
  }
1180
1404
  function startMission(db, sessionId, prompt) {
1181
- const id = nanoid();
1405
+ const id = nanoid2();
1182
1406
  const ts2 = now();
1183
1407
  const shortName = shortNameFromPrompt(prompt);
1184
1408
  db.prepare(
@@ -1357,7 +1581,7 @@ function loadTimeline(db, opts = {}) {
1357
1581
  }
1358
1582
 
1359
1583
  // ../server/src/state/audit.ts
1360
- import { nanoid as nanoid2 } from "nanoid";
1584
+ import { nanoid as nanoid3 } from "nanoid";
1361
1585
  function rowToAuditEvent(row) {
1362
1586
  let payload;
1363
1587
  if (row.payload_json) {
@@ -1380,7 +1604,7 @@ function rowToAuditEvent(row) {
1380
1604
  }
1381
1605
  function recordAudit(db, input) {
1382
1606
  const event = {
1383
- id: nanoid2(),
1607
+ id: nanoid3(),
1384
1608
  ts: Date.now(),
1385
1609
  kind: input.kind,
1386
1610
  sessionId: input.sessionId,
@@ -1435,11 +1659,11 @@ function listAudit(db, opts = {}) {
1435
1659
  }
1436
1660
 
1437
1661
  // ../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";
1662
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1663
+ import { dirname as dirname2, join as join8, resolve } from "path";
1440
1664
  import { fileURLToPath as fileURLToPath2 } from "url";
1441
1665
  function findAgentsDir() {
1442
- if (process.env.SOLIX_AGENTS_DIR && existsSync4(process.env.SOLIX_AGENTS_DIR)) {
1666
+ if (process.env.SOLIX_AGENTS_DIR && existsSync5(process.env.SOLIX_AGENTS_DIR)) {
1443
1667
  return process.env.SOLIX_AGENTS_DIR;
1444
1668
  }
1445
1669
  const here = dirname2(fileURLToPath2(import.meta.url));
@@ -1452,17 +1676,17 @@ function findAgentsDir() {
1452
1676
  resolve(process.cwd(), "packages", "agents")
1453
1677
  ];
1454
1678
  for (const c of candidates) {
1455
- if (existsSync4(join6(c, "manifest.json"))) return c;
1679
+ if (existsSync5(join8(c, "manifest.json"))) return c;
1456
1680
  }
1457
1681
  return candidates[0];
1458
1682
  }
1459
1683
  var AGENTS_DIR = findAgentsDir();
1460
1684
  function readManifest() {
1461
- const path = join6(AGENTS_DIR, "manifest.json");
1462
- if (!existsSync4(path)) {
1685
+ const path = join8(AGENTS_DIR, "manifest.json");
1686
+ if (!existsSync5(path)) {
1463
1687
  return { version: 1, advisors: [] };
1464
1688
  }
1465
- return JSON.parse(readFileSync3(path, "utf8"));
1689
+ return JSON.parse(readFileSync4(path, "utf8"));
1466
1690
  }
1467
1691
  function rowToAdvisor(row) {
1468
1692
  let requiredSkills = [];
@@ -1505,7 +1729,7 @@ function seedAdvisors(db) {
1505
1729
  WHERE id = ?`
1506
1730
  );
1507
1731
  for (const a of manifest.advisors) {
1508
- const md = join6(AGENTS_DIR, a.agentMd);
1732
+ const md = join8(AGENTS_DIR, a.agentMd);
1509
1733
  insert.run(
1510
1734
  a.id,
1511
1735
  a.role,
@@ -1563,10 +1787,55 @@ function setAdvisorPinned(db, id, pinned, sessionId) {
1563
1787
  return getAdvisor(db, id);
1564
1788
  }
1565
1789
  function readAdvisorAgentMd(advisor) {
1566
- if (!existsSync4(advisor.agentMdPath)) {
1790
+ if (!existsSync5(advisor.agentMdPath)) {
1567
1791
  return "";
1568
1792
  }
1569
- return readFileSync3(advisor.agentMdPath, "utf8");
1793
+ return readFileSync4(advisor.agentMdPath, "utf8");
1794
+ }
1795
+
1796
+ // ../server/src/state/wrappers.ts
1797
+ import { connect } from "net";
1798
+ var wrappers = /* @__PURE__ */ new Map();
1799
+ var FRESHNESS_WINDOW_MS = 6e4;
1800
+ function registerWrapper(rec) {
1801
+ wrappers.set(rec.wrapperId, rec);
1802
+ }
1803
+ function unregisterWrapper(wrapperId) {
1804
+ wrappers.delete(wrapperId);
1805
+ }
1806
+ function claimWrapperForCwd(cwd) {
1807
+ const now2 = Date.now();
1808
+ let best;
1809
+ for (const rec of wrappers.values()) {
1810
+ if (rec.cwd !== cwd) continue;
1811
+ if (now2 - rec.registeredAt > FRESHNESS_WINDOW_MS) continue;
1812
+ if (!best || rec.registeredAt > best.registeredAt) best = rec;
1813
+ }
1814
+ if (best) {
1815
+ wrappers.delete(best.wrapperId);
1816
+ }
1817
+ return best;
1818
+ }
1819
+ function writeToWrapperSocket(socketPath, text) {
1820
+ try {
1821
+ const client = connect(socketPath);
1822
+ let settled = false;
1823
+ client.on("error", () => {
1824
+ if (!settled) {
1825
+ settled = true;
1826
+ try {
1827
+ client.destroy();
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ });
1832
+ client.write(JSON.stringify({ type: "send_prompt", text }) + "\n");
1833
+ client.end();
1834
+ settled = true;
1835
+ return true;
1836
+ } catch {
1837
+ return false;
1838
+ }
1570
1839
  }
1571
1840
 
1572
1841
  // ../server/src/state/context.ts
@@ -1656,10 +1925,10 @@ function buildContextEnvelope(db, args) {
1656
1925
  }
1657
1926
 
1658
1927
  // ../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";
1928
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
1929
+ import { dirname as dirname3, join as join9, resolve as resolve2 } from "path";
1661
1930
  import { fileURLToPath as fileURLToPath3 } from "url";
1662
- import { homedir as homedir4 } from "os";
1931
+ import { homedir as homedir6 } from "os";
1663
1932
  function findSolixSkillsDir() {
1664
1933
  const here = dirname3(fileURLToPath3(import.meta.url));
1665
1934
  const candidates = [
@@ -1668,15 +1937,15 @@ function findSolixSkillsDir() {
1668
1937
  resolve2(process.cwd(), "packages", "skills")
1669
1938
  ];
1670
1939
  for (const c of candidates) {
1671
- if (existsSync5(c)) return c;
1940
+ if (existsSync6(c)) return c;
1672
1941
  }
1673
1942
  return candidates[0];
1674
1943
  }
1675
1944
  var SOLIX_SKILLS_DIR2 = findSolixSkillsDir();
1676
- var ANTHROPIC_SKILLS_DIR = join7(homedir4(), ".claude", "skills");
1945
+ var ANTHROPIC_SKILLS_DIR = join9(homedir6(), ".claude", "skills");
1677
1946
  function parseSkillManifest(manifestPath, fallbackId) {
1678
1947
  try {
1679
- const txt = readFileSync4(manifestPath, "utf8");
1948
+ const txt = readFileSync5(manifestPath, "utf8");
1680
1949
  const match = txt.match(/^---\n([\s\S]*?)\n---/);
1681
1950
  let name = fallbackId;
1682
1951
  let description = "";
@@ -1725,9 +1994,9 @@ function discoverSkills(db) {
1725
1994
  { dir: SOLIX_SKILLS_DIR2, source: "solix" }
1726
1995
  ];
1727
1996
  for (const { dir, source } of sources) {
1728
- if (!existsSync5(dir)) continue;
1997
+ if (!existsSync6(dir)) continue;
1729
1998
  for (const entry of readdirSync3(dir)) {
1730
- const full = join7(dir, entry);
1999
+ const full = join9(dir, entry);
1731
2000
  let isDir = false;
1732
2001
  try {
1733
2002
  isDir = statSync3(full).isDirectory();
@@ -1735,8 +2004,8 @@ function discoverSkills(db) {
1735
2004
  continue;
1736
2005
  }
1737
2006
  if (!isDir) continue;
1738
- const manifestPath = join7(full, "SKILL.md");
1739
- if (!existsSync5(manifestPath)) continue;
2007
+ const manifestPath = join9(full, "SKILL.md");
2008
+ if (!existsSync6(manifestPath)) continue;
1740
2009
  const parsed = parseSkillManifest(manifestPath, entry);
1741
2010
  if (!parsed) continue;
1742
2011
  const id = `${source}:${parsed.id}`;
@@ -1761,8 +2030,8 @@ function getSkill(db, id) {
1761
2030
  return row ? rowToSkill(row) : null;
1762
2031
  }
1763
2032
  function readSkillManifest(skill) {
1764
- if (!existsSync5(skill.manifestPath)) return "";
1765
- return readFileSync4(skill.manifestPath, "utf8");
2033
+ if (!existsSync6(skill.manifestPath)) return "";
2034
+ return readFileSync5(skill.manifestPath, "utf8");
1766
2035
  }
1767
2036
  function recordSkillInstall(db, skillId, projectId) {
1768
2037
  const skill = getSkill(db, skillId);
@@ -1775,7 +2044,7 @@ function recordSkillInstall(db, skillId, projectId) {
1775
2044
  }
1776
2045
 
1777
2046
  // ../server/src/state/galaxy.ts
1778
- import { nanoid as nanoid3 } from "nanoid";
2047
+ import { nanoid as nanoid4 } from "nanoid";
1779
2048
  function exportManifest(db, opts = {}) {
1780
2049
  const advisors2 = listAdvisors(db);
1781
2050
  const skills2 = listSkills(db);
@@ -1823,7 +2092,7 @@ function importManifest(db, manifest, sourceUrl) {
1823
2092
  db.prepare(
1824
2093
  `INSERT INTO galaxy_imports (id, source_url, manifest_json, imported_at)
1825
2094
  VALUES (?, ?, ?, ?)`
1826
- ).run(nanoid3(), sourceUrl ?? null, JSON.stringify(manifest), now());
2095
+ ).run(nanoid4(), sourceUrl ?? null, JSON.stringify(manifest), now());
1827
2096
  return {
1828
2097
  advisorsEnabled: enabled,
1829
2098
  advisorsDisabled: disabled,
@@ -1884,7 +2153,7 @@ function snapshotExport(db, manifest) {
1884
2153
  return rowToVersion(existing);
1885
2154
  }
1886
2155
  }
1887
- const id = nanoid3();
2156
+ const id = nanoid4();
1888
2157
  const ts2 = now();
1889
2158
  const ordinal = (last?.ordinal ?? 0) + 1;
1890
2159
  db.prepare(
@@ -2278,6 +2547,23 @@ function createHttpApp(opts) {
2278
2547
  }
2279
2548
  });
2280
2549
  app.get("/api/galaxy/imports", (c) => c.json(listImportHistory(opts.db)));
2550
+ app.post("/api/wrappers/register", async (c) => {
2551
+ const body = await c.req.json().catch(() => null);
2552
+ if (!body?.wrapperId || !body.socketPath || !body.cwd) {
2553
+ return c.json({ error: "wrapperId, socketPath, cwd required" }, 400);
2554
+ }
2555
+ registerWrapper({
2556
+ wrapperId: body.wrapperId,
2557
+ socketPath: body.socketPath,
2558
+ cwd: body.cwd,
2559
+ registeredAt: Date.now()
2560
+ });
2561
+ return c.json({ ok: true });
2562
+ });
2563
+ app.post("/api/wrappers/:id/unregister", (c) => {
2564
+ unregisterWrapper(c.req.param("id"));
2565
+ return c.json({ ok: true });
2566
+ });
2281
2567
  let preflightCache = null;
2282
2568
  app.get("/api/system/preflight", (c) => {
2283
2569
  if (preflightCache) return c.json(preflightCache);
@@ -2307,17 +2593,17 @@ function createHttpApp(opts) {
2307
2593
  return c.notFound();
2308
2594
  }
2309
2595
  const safe = url.pathname.replace(/\.\.+/g, ".");
2310
- const candidate = join8(webDist, safe === "/" ? "index.html" : safe);
2596
+ const candidate = join10(webDist, safe === "/" ? "index.html" : safe);
2311
2597
  let filePath = candidate;
2312
2598
  try {
2313
- if (!existsSync6(filePath) || statSync4(filePath).isDirectory()) {
2314
- filePath = join8(webDist, "index.html");
2599
+ if (!existsSync7(filePath) || statSync4(filePath).isDirectory()) {
2600
+ filePath = join10(webDist, "index.html");
2315
2601
  }
2316
2602
  } catch {
2317
- filePath = join8(webDist, "index.html");
2603
+ filePath = join10(webDist, "index.html");
2318
2604
  }
2319
- if (!existsSync6(filePath)) return c.notFound();
2320
- const data = readFileSync5(filePath);
2605
+ if (!existsSync7(filePath)) return c.notFound();
2606
+ const data = readFileSync6(filePath);
2321
2607
  return new Response(data, {
2322
2608
  headers: { "Content-Type": mimeFor(filePath) }
2323
2609
  });
@@ -2375,7 +2661,7 @@ function createHttpApp(opts) {
2375
2661
  }
2376
2662
  function findWebDist() {
2377
2663
  if (process.env.SOLIX_WEB_DIST) {
2378
- return existsSync6(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
2664
+ return existsSync7(process.env.SOLIX_WEB_DIST) ? process.env.SOLIX_WEB_DIST : null;
2379
2665
  }
2380
2666
  const here = dirname4(fileURLToPath4(import.meta.url));
2381
2667
  const candidates = [
@@ -2388,7 +2674,7 @@ function findWebDist() {
2388
2674
  resolve3(process.cwd(), "packages", "web", "dist")
2389
2675
  ];
2390
2676
  for (const c of candidates) {
2391
- if (existsSync6(join8(c, "index.html"))) return c;
2677
+ if (existsSync7(join10(c, "index.html"))) return c;
2392
2678
  }
2393
2679
  return null;
2394
2680
  }
@@ -2414,10 +2700,10 @@ function mimeFor(filePath) {
2414
2700
 
2415
2701
  // ../server/src/launcher.ts
2416
2702
  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";
2703
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
2704
+ import { homedir as homedir7 } from "os";
2705
+ import { basename as basename3, join as join11 } from "path";
2706
+ import { nanoid as nanoid5 } from "nanoid";
2421
2707
  function ensureWorktree(opts) {
2422
2708
  const repoRoot = (() => {
2423
2709
  const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
@@ -2429,10 +2715,10 @@ function ensureWorktree(opts) {
2429
2715
  }
2430
2716
  return (r.stdout ?? "").trim();
2431
2717
  })();
2432
- const repoName = basename2(repoRoot);
2718
+ const repoName = basename3(repoRoot);
2433
2719
  const safeBranch = opts.branch.replace(/[^a-zA-Z0-9._-]+/g, "-");
2434
- const worktreesDir = join9(homedir5(), ".solix", "worktrees");
2435
- const path = join9(worktreesDir, `${repoName}-${safeBranch}`);
2720
+ const worktreesDir = join11(homedir7(), ".solix", "worktrees");
2721
+ const path = join11(worktreesDir, `${repoName}-${safeBranch}`);
2436
2722
  const list = spawnSync2("git", ["worktree", "list", "--porcelain"], {
2437
2723
  cwd: repoRoot,
2438
2724
  encoding: "utf8"
@@ -2440,7 +2726,7 @@ function ensureWorktree(opts) {
2440
2726
  if (list.status === 0 && (list.stdout ?? "").includes(`worktree ${path}`)) {
2441
2727
  return { path, created: false };
2442
2728
  }
2443
- mkdirSync3(worktreesDir, { recursive: true });
2729
+ mkdirSync4(worktreesDir, { recursive: true });
2444
2730
  const branchProbe = spawnSync2(
2445
2731
  "git",
2446
2732
  ["rev-parse", "--verify", "--quiet", `refs/heads/${opts.branch}`],
@@ -2535,7 +2821,7 @@ var Launcher = class {
2535
2821
  }
2536
2822
  pinSynthetic(advisorId, codename, cwd) {
2537
2823
  const project = ensureProject(this.db, cwd);
2538
- const sessionId = `advisor-${advisorId}-${nanoid4(6)}`;
2824
+ const sessionId = `advisor-${advisorId}-${nanoid5(6)}`;
2539
2825
  const fakePid = 1e5 + Math.floor(Math.random() * 1e5);
2540
2826
  const session = upsertSession(this.db, {
2541
2827
  id: sessionId,
@@ -2644,7 +2930,7 @@ var Launcher = class {
2644
2930
  worktreePath
2645
2931
  });
2646
2932
  }
2647
- if (!existsSync7(spawnCwd)) {
2933
+ if (!existsSync8(spawnCwd)) {
2648
2934
  this.broadcaster.broadcast({
2649
2935
  type: "toast",
2650
2936
  level: "error",
@@ -2655,7 +2941,7 @@ var Launcher = class {
2655
2941
  const args = ["--print"];
2656
2942
  if (opts.model) args.push("--model", String(opts.model));
2657
2943
  args.push(opts.initialPrompt);
2658
- const sessionId = `task-${nanoid4(8)}`;
2944
+ const sessionId = `task-${nanoid5(8)}`;
2659
2945
  return this.spawnPrint({
2660
2946
  sessionId,
2661
2947
  cwd: spawnCwd,
@@ -2783,7 +3069,7 @@ var Launcher = class {
2783
3069
  }
2784
3070
  launchSynthetic(opts) {
2785
3071
  const project = ensureProject(this.db, opts.cwd);
2786
- const sessionId = `task-${nanoid4(8)}`;
3072
+ const sessionId = `task-${nanoid5(8)}`;
2787
3073
  const fakePid = 2e5 + Math.floor(Math.random() * 1e5);
2788
3074
  upsertSession(this.db, {
2789
3075
  id: sessionId,
@@ -2829,12 +3115,12 @@ var Launcher = class {
2829
3115
  };
2830
3116
 
2831
3117
  // ../server/src/router.ts
2832
- import { nanoid as nanoid6 } from "nanoid";
3118
+ import { nanoid as nanoid7 } from "nanoid";
2833
3119
 
2834
3120
  // ../server/src/state/toolcalls.ts
2835
- import { nanoid as nanoid5 } from "nanoid";
3121
+ import { nanoid as nanoid6 } from "nanoid";
2836
3122
  function recordToolCall(db, input) {
2837
- const id = nanoid5();
3123
+ const id = nanoid6();
2838
3124
  const ts2 = now();
2839
3125
  const status = input.status ?? "running";
2840
3126
  db.prepare(
@@ -2936,6 +3222,7 @@ var EventRouter = class {
2936
3222
  const sessionId = this.extractSessionId(event);
2937
3223
  const advisorRole = this.launcher?.advisorRoleForPid(event.pid);
2938
3224
  const worktreePath = this.launcher?.worktreePathForInternalCwd(event.cwd);
3225
+ const wrapper = claimWrapperForCwd(event.cwd);
2939
3226
  const session = upsertSession(this.db, {
2940
3227
  id: sessionId,
2941
3228
  pid: event.pid,
@@ -2946,7 +3233,8 @@ var EventRouter = class {
2946
3233
  parentSessionId: this.extractParentSessionId(event),
2947
3234
  kind: advisorRole ? "advisor" : "user",
2948
3235
  advisorRole,
2949
- worktreePath
3236
+ worktreePath,
3237
+ wrapperSocketPath: wrapper?.socketPath
2950
3238
  });
2951
3239
  this.broadcaster.broadcast({ type: "session_upsert", session });
2952
3240
  if (!session.parentSessionId) {
@@ -3013,7 +3301,7 @@ var EventRouter = class {
3013
3301
  const parentSessionId = this.extractSessionId(event);
3014
3302
  const parent = getSession(this.db, parentSessionId);
3015
3303
  if (!parent) return;
3016
- const subId = nanoid6();
3304
+ const subId = nanoid7();
3017
3305
  const sub = upsertSession(this.db, {
3018
3306
  id: subId,
3019
3307
  pid: event.pid,
@@ -3090,7 +3378,7 @@ var EventRouter = class {
3090
3378
  const p = event.payload;
3091
3379
  const message = typeof p.message === "string" ? p.message : "Permission requested";
3092
3380
  const tool = typeof p.tool_name === "string" ? p.tool_name : "unknown";
3093
- const requestId = nanoid6();
3381
+ const requestId = nanoid7();
3094
3382
  this.permissions.set(requestId, {
3095
3383
  requestId,
3096
3384
  sessionId,
@@ -3230,6 +3518,31 @@ var EventRouter = class {
3230
3518
  });
3231
3519
  }
3232
3520
  sendPromptToSession(sessionId, text) {
3521
+ const session = getSession(this.db, sessionId);
3522
+ if (!session) return false;
3523
+ if (session.wrapperSocketPath) {
3524
+ const ok = writeToWrapperSocket(session.wrapperSocketPath, text);
3525
+ if (ok) {
3526
+ this.broadcaster.broadcast({
3527
+ type: "chat_delta",
3528
+ sessionId,
3529
+ delta: {
3530
+ messageId: `u-${Date.now()}`,
3531
+ role: "user",
3532
+ content: text,
3533
+ ts: Date.now(),
3534
+ done: true
3535
+ }
3536
+ });
3537
+ } else {
3538
+ this.broadcaster.broadcast({
3539
+ type: "toast",
3540
+ level: "warn",
3541
+ message: "Wrapper socket unreachable \u2014 the `solix run` process may have exited."
3542
+ });
3543
+ }
3544
+ return ok;
3545
+ }
3233
3546
  if (!this.launcher) return false;
3234
3547
  return this.launcher.sendPromptToInternal(sessionId, text);
3235
3548
  }
@@ -3364,15 +3677,15 @@ function handleClientMessage(ctx, _ws, msg) {
3364
3677
  // ../server/src/state/transcript.ts
3365
3678
  import {
3366
3679
  closeSync,
3367
- existsSync as existsSync8,
3680
+ existsSync as existsSync9,
3368
3681
  openSync,
3369
3682
  readSync,
3370
3683
  statSync as statSync5,
3371
3684
  watch
3372
3685
  } from "fs";
3373
- import { homedir as homedir6 } from "os";
3374
- import { join as join10 } from "path";
3375
- var TRANSCRIPT_BASE = join10(homedir6(), ".claude", "projects");
3686
+ import { homedir as homedir8 } from "os";
3687
+ import { join as join12 } from "path";
3688
+ var TRANSCRIPT_BASE = join12(homedir8(), ".claude", "projects");
3376
3689
  var CONTEXT_BUDGETS_BY_MODEL = {
3377
3690
  "claude-opus-4-7": 2e5,
3378
3691
  "claude-opus-4-6": 2e5,
@@ -3385,7 +3698,7 @@ function encodeProjectPath(cwd) {
3385
3698
  return cwd.replace(/[/\\]/g, "-");
3386
3699
  }
3387
3700
  function transcriptPathFor(cwd, sessionId) {
3388
- return join10(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
3701
+ return join12(TRANSCRIPT_BASE, encodeProjectPath(cwd), `${sessionId}.jsonl`);
3389
3702
  }
3390
3703
  var TranscriptWatcherManager = class {
3391
3704
  constructor(db, broadcaster) {
@@ -3405,7 +3718,7 @@ var TranscriptWatcherManager = class {
3405
3718
  startWatching(sessionId, cwd) {
3406
3719
  if (this.records.has(sessionId)) return;
3407
3720
  const filePath = transcriptPathFor(cwd, sessionId);
3408
- if (!existsSync8(filePath)) {
3721
+ if (!existsSync9(filePath)) {
3409
3722
  this.scheduleRetry(sessionId, cwd, 0);
3410
3723
  return;
3411
3724
  }
@@ -3416,7 +3729,7 @@ var TranscriptWatcherManager = class {
3416
3729
  const t = setTimeout(() => {
3417
3730
  this.deferredRetry.delete(sessionId);
3418
3731
  const filePath = transcriptPathFor(cwd, sessionId);
3419
- if (existsSync8(filePath)) {
3732
+ if (existsSync9(filePath)) {
3420
3733
  this.attach(sessionId, filePath);
3421
3734
  } else {
3422
3735
  this.scheduleRetry(sessionId, cwd, attempt + 1);
@@ -3678,19 +3991,20 @@ async function start(opts = {}) {
3678
3991
  }
3679
3992
 
3680
3993
  // src/uninstall.ts
3681
- import { copyFileSync as copyFileSync2, existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
3994
+ import { copyFileSync as copyFileSync2, existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
3682
3995
  function uninstall() {
3683
- if (existsSync9(CLAUDE_BACKUP)) {
3996
+ uninstallShim();
3997
+ if (existsSync10(CLAUDE_BACKUP)) {
3684
3998
  copyFileSync2(CLAUDE_BACKUP, CLAUDE_SETTINGS);
3685
3999
  console.log(`[solix] restored settings.json from backup`);
3686
4000
  return;
3687
4001
  }
3688
- if (!existsSync9(CLAUDE_SETTINGS)) {
4002
+ if (!existsSync10(CLAUDE_SETTINGS)) {
3689
4003
  console.log("[solix] nothing to uninstall (no settings.json found)");
3690
4004
  return;
3691
4005
  }
3692
4006
  const cur = JSON.parse(
3693
- readFileSync6(CLAUDE_SETTINGS, "utf8")
4007
+ readFileSync7(CLAUDE_SETTINGS, "utf8")
3694
4008
  );
3695
4009
  if (cur.hooks) {
3696
4010
  for (const [evt, entries] of Object.entries(cur.hooks)) {
@@ -3700,7 +4014,7 @@ function uninstall() {
3700
4014
  if (cur.hooks[evt].length === 0) delete cur.hooks[evt];
3701
4015
  }
3702
4016
  }
3703
- writeFileSync3(CLAUDE_SETTINGS, JSON.stringify(cur, null, 2) + "\n");
4017
+ writeFileSync4(CLAUDE_SETTINGS, JSON.stringify(cur, null, 2) + "\n");
3704
4018
  console.log(`[solix] removed Solix hooks from ${CLAUDE_SETTINGS}`);
3705
4019
  }
3706
4020
 
@@ -3717,6 +4031,16 @@ program.command("install").description("Install Solix hooks into ~/.claude/setti
3717
4031
  program.command("uninstall").description("Restore ~/.claude/settings.json from backup").action(() => {
3718
4032
  uninstall();
3719
4033
  });
4034
+ program.command("run").description(
4035
+ "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."
4036
+ ).allowUnknownOption(true).helpOption(false).action(async (_opts, cmd) => {
4037
+ await runWrapped(cmd.args ?? []);
4038
+ });
4039
+ program.command("install-shim").description(
4040
+ "Add `alias claude='solix run'` to your shell rc so every claude session is wrapped automatically."
4041
+ ).action(() => {
4042
+ installShim();
4043
+ });
3720
4044
  program.command("doctor").description("Run diagnostics").action(async () => {
3721
4045
  await doctor();
3722
4046
  });