@synkro-sh/cli 1.10.3 → 1.10.4

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/bootstrap.js CHANGED
@@ -147,7 +147,7 @@ function getIdentity() {
147
147
  if (cached2) return cached2;
148
148
  let cliVersion2 = "0.0.0";
149
149
  try {
150
- cliVersion2 = "1.10.3";
150
+ cliVersion2 = "1.10.4";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -7840,7 +7840,7 @@ var init_dockerInstall = __esm({
7840
7840
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
7841
7841
  CONTAINER_NAME = resolveContainerName();
7842
7842
  defaultImageVersion = () => {
7843
- if (true) return "1.10.3";
7843
+ if (true) return "1.10.4";
7844
7844
  try {
7845
7845
  const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
7846
7846
  if (pkg.version) return pkg.version;
@@ -8900,7 +8900,7 @@ function writeConfigEnv(opts) {
8900
8900
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
8901
8901
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
8902
8902
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
8903
- `SYNKRO_VERSION=${shellQuoteSingle2("1.10.3")}`
8903
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.10.4")}`
8904
8904
  ];
8905
8905
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
8906
8906
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -9647,7 +9647,7 @@ async function installCommand(opts = {}) {
9647
9647
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
9648
9648
  emit("install", {
9649
9649
  phase: "started",
9650
- cli_version_to: "1.10.3",
9650
+ cli_version_to: "1.10.4",
9651
9651
  agents_detected: agents.map((a) => a.kind),
9652
9652
  with_github: false,
9653
9653
  with_local_cc: false,
@@ -10630,15 +10630,15 @@ async function discoverAndIngestSkills() {
10630
10630
  }
10631
10631
  const found = discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames);
10632
10632
  if (found.length === 0) return;
10633
- const selectable = found.filter((f) => !f.ingested);
10634
- if (selectable.length === 0) {
10633
+ const selectable2 = found.filter((f) => !f.ingested);
10634
+ if (selectable2.length === 0) {
10635
10635
  const names = found.map((f) => f.name);
10636
10636
  const shown = names.slice(0, 5).join(", ") + (names.length > 5 ? `, +${names.length - 5} more` : "");
10637
10637
  console.log(`
10638
10638
  \u2713 ${found.length} skill${found.length === 1 ? "" : "s"} already ingested (${shown}) \u2014 nothing new to add.`);
10639
10639
  return;
10640
10640
  }
10641
- const setHash = discoverySetHash(selectable);
10641
+ const setHash = discoverySetHash(selectable2);
10642
10642
  let prev = "";
10643
10643
  try {
10644
10644
  prev = readFileSync21(SKILLS_DISCOVERED_PATH, "utf-8").trim();
@@ -14540,6 +14540,10 @@ function buildSpawnAgent(opts) {
14540
14540
  return [
14541
14541
  ["tmux", "new-session", "-d", "-s", session, "-c", opts.cwd, opts.command],
14542
14542
  ["tmux", "set-option", "-t", session, "status", "off"],
14543
+ // Mouse on inside the agent session too, so the wheel reaches the harness
14544
+ // and its own history scrolls — without it only the last screen is
14545
+ // reachable once the session is nested inside the layout.
14546
+ ["tmux", "set-option", "-t", session, "mouse", "on"],
14543
14547
  ["tmux", "set-option", "-t", session, "-q", "@synkro_harness", opts.harness],
14544
14548
  ["tmux", "set-option", "-t", session, "-q", "@synkro_space", opts.space],
14545
14549
  ["tmux", "set-option", "-t", session, "-q", "@synkro_backend", opts.backend],
@@ -14549,10 +14553,32 @@ function buildSpawnAgent(opts) {
14549
14553
  ];
14550
14554
  }
14551
14555
  function buildListAgents() {
14552
- return ["tmux", "list-sessions", "-F", ["#{session_name}", "#{?pane_dead,dead,alive}", "#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}", "#{@synkro_pueue}"].join(FIELD_SEP)];
14553
- }
14554
- function buildCapture(session, lines = 14) {
14555
- return ["tmux", "capture-pane", "-p", "-t", session, "-S", String(-lines)];
14556
+ return ["tmux", "list-sessions", "-F", ["#{session_name}", "#{?pane_dead,dead,alive}", "#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}", "#{@synkro_pueue}", "#{pane_current_path}"].join(FIELD_SEP)];
14557
+ }
14558
+ function buildAgentSnapshot() {
14559
+ const list = buildListAgents().map(shellQuote3).join(" ");
14560
+ const script = list + ' 2>/dev/null; tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^' + AGENT_PREFIX + '" | while IFS= read -r s; do printf "===%s\\n" "$s"; tmux capture-pane -p -t "$s" 2>/dev/null; done';
14561
+ return ["sh", "-c", script];
14562
+ }
14563
+ function parseAgentSnapshot(output) {
14564
+ const lines = String(output || "").split("\n");
14565
+ const captures = /* @__PURE__ */ new Map();
14566
+ const listLines = [];
14567
+ let current = null;
14568
+ let chunk = [];
14569
+ const flush2 = () => {
14570
+ if (current) captures.set(current, chunk.join("\n"));
14571
+ };
14572
+ for (const line of lines) {
14573
+ if (line.startsWith("===")) {
14574
+ flush2();
14575
+ current = line.slice(3).trim();
14576
+ chunk = [];
14577
+ } else if (current) chunk.push(line);
14578
+ else listLines.push(line);
14579
+ }
14580
+ flush2();
14581
+ return { list: listLines.join("\n"), captures };
14556
14582
  }
14557
14583
  function buildKillSession(session) {
14558
14584
  return ["tmux", "kill-session", "-t", session];
@@ -14566,9 +14592,31 @@ function buildSendText(session, text) {
14566
14592
  ["tmux", "send-keys", "-t", session, "Enter"]
14567
14593
  ];
14568
14594
  }
14595
+ function buildEnableMouse(session) {
14596
+ return ["tmux", "set-option", "-t", session, "mouse", "on"];
14597
+ }
14569
14598
  function buildInterrupt(session) {
14570
14599
  return ["tmux", "send-keys", "-t", session, "Escape"];
14571
14600
  }
14601
+ function buildDialogPopup(opts) {
14602
+ const command = ["node", opts.bootPath, "ui", "--dialog", opts.kind, opts.cwd, ...opts.args || []].map(shellQuote3).join(" ");
14603
+ return [
14604
+ "tmux",
14605
+ "display-popup",
14606
+ "-c",
14607
+ opts.client,
14608
+ "-E",
14609
+ "-w",
14610
+ opts.width,
14611
+ "-h",
14612
+ opts.height,
14613
+ "-S",
14614
+ "fg=colour111",
14615
+ "-d",
14616
+ opts.cwd,
14617
+ command
14618
+ ];
14619
+ }
14572
14620
  function buildCenterAttachCommand(runner, session) {
14573
14621
  const argv = runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", session]);
14574
14622
  return "env TMUX= " + argv.map(shellQuote3).join(" ");
@@ -14582,7 +14630,9 @@ function parseAgentSessions(output, backend) {
14582
14630
  name: cols[0].slice(AGENT_PREFIX.length),
14583
14631
  dead: cols[1] === "dead",
14584
14632
  harness: cols[2] || "claude",
14585
- space: cols[3] || "",
14633
+ // Only an absolute path is usable; anything else falls back to where
14634
+ // the pane actually is.
14635
+ space: (cols[3] || "").startsWith("/") ? cols[3] : cols[6] || "",
14586
14636
  backend: cols[4] || backend,
14587
14637
  pueue: cols[5] || ""
14588
14638
  }));
@@ -14623,99 +14673,10 @@ var init_tmux = __esm({
14623
14673
  }
14624
14674
  });
14625
14675
 
14626
- // cli/ui/launch.ts
14627
- function welcomeCommand() {
14628
- const banner = [
14629
- "",
14630
- " synkro ui",
14631
- " governed agents, one screen",
14632
- "",
14633
- " enter attach selected agent",
14634
- " n new agent in selected space",
14635
- " c new container agent",
14636
- " g/s/y consent: track / skip / stay",
14637
- " T new tab q quit",
14638
- ""
14639
- ].join("\\n");
14640
- return "printf " + shellQuote3(banner + "\\n") + "; tail -f /dev/null";
14641
- }
14642
- function sidebarCommand(bootPath, centerPane, repoCwd) {
14643
- const env = [
14644
- "SYNKRO_UI_CENTER=" + shellQuote3(centerPane),
14645
- "SYNKRO_UI_OUTER=" + UI_SESSION,
14646
- "SYNKRO_UI_BOOT=" + shellQuote3(bootPath),
14647
- "SYNKRO_UI_REPO=" + shellQuote3(repoCwd)
14648
- ].join(" ");
14649
- return "env " + env + " node " + shellQuote3(bootPath) + " ui --sidebar";
14650
- }
14651
- async function styleOuterSession() {
14652
- const style = [
14653
- ["set-option", "-t", UI_SESSION, "status-position", "top"],
14654
- ["set-option", "-t", UI_SESSION, "status-style", "bg=colour233,fg=colour245"],
14655
- ["set-option", "-t", UI_SESSION, "status-left", " synkro "],
14656
- ["set-option", "-t", UI_SESSION, "status-left-style", "fg=colour135,bold"],
14657
- ["set-option", "-t", UI_SESSION, "status-right", " + (T new tab) "],
14658
- ["set-option", "-t", UI_SESSION, "status-right-style", "fg=colour240"],
14659
- ["set-option", "-t", UI_SESSION, "-w", "window-status-format", " #W "],
14660
- ["set-option", "-t", UI_SESSION, "-w", "window-status-current-format", "#[bg=colour135,fg=colour233,bold] #W #[default]"],
14661
- ["set-option", "-t", UI_SESSION, "pane-border-style", "fg=colour236"],
14662
- ["set-option", "-t", UI_SESSION, "pane-active-border-style", "fg=colour135"],
14663
- // Tab keys without the prefix: Alt+t new tab, Alt+arrows to move.
14664
- ["bind-key", "-n", "M-t", "new-window"],
14665
- ["bind-key", "-n", "M-Right", "next-window"],
14666
- ["bind-key", "-n", "M-Left", "previous-window"]
14667
- ];
14668
- for (const argv of style) await run(HOST, ["tmux", ...argv]);
14669
- }
14670
- async function buildTab(bootPath, repoCwd, windowTarget) {
14671
- if (windowTarget === void 0) {
14672
- await run(HOST, ["tmux", "new-session", "-d", "-s", UI_SESSION, "-x", "220", "-y", "55", welcomeCommand()]);
14673
- windowTarget = UI_SESSION + ":0";
14674
- } else {
14675
- const created = await run(HOST, ["tmux", "new-window", "-t", UI_SESSION, "-P", "-F", "#{window_id}", welcomeCommand()]);
14676
- windowTarget = created.stdout.trim() || windowTarget;
14677
- }
14678
- await run(HOST, ["tmux", "rename-window", "-t", windowTarget, "space"]);
14679
- const split = await run(HOST, [
14680
- "tmux",
14681
- "split-window",
14682
- "-hb",
14683
- "-t",
14684
- windowTarget,
14685
- "-l",
14686
- SIDEBAR_WIDTH,
14687
- "-P",
14688
- "-F",
14689
- "#{pane_id}",
14690
- "tail -f /dev/null"
14691
- ]);
14692
- const sidebarPane = split.stdout.trim();
14693
- const panes = await run(HOST, ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_id}"]);
14694
- const centerPane = panes.stdout.split("\n").map((line) => line.trim()).filter(Boolean).find((id) => id !== sidebarPane) || "";
14695
- await run(HOST, ["tmux", "respawn-pane", "-k", "-t", sidebarPane, sidebarCommand(bootPath, centerPane, repoCwd)]);
14696
- }
14697
- async function uiSessionExists() {
14698
- const result = await run(HOST, ["tmux", "has-session", "-t", UI_SESSION]);
14699
- return result.ok;
14700
- }
14701
- async function launchUi(bootPath, repoCwd) {
14702
- if (!await uiSessionExists()) {
14703
- await buildTab(bootPath, repoCwd);
14704
- await styleOuterSession();
14705
- }
14706
- return runInherit(process.env.TMUX ? ["tmux", "switch-client", "-t", UI_SESSION] : ["tmux", "attach-session", "-t", UI_SESSION]);
14707
- }
14708
- var SIDEBAR_WIDTH, HOST;
14709
- var init_launch = __esm({
14710
- "cli/ui/launch.ts"() {
14711
- "use strict";
14712
- init_tmux();
14713
- SIDEBAR_WIDTH = "34";
14714
- HOST = { kind: "host" };
14715
- }
14716
- });
14717
-
14718
14676
  // cli/ui/model.ts
14677
+ import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync30, writeFileSync as writeFileSync23 } from "fs";
14678
+ import { homedir as homedir32 } from "os";
14679
+ import { dirname as dirname9, join as join32 } from "path";
14719
14680
  function detectAsk(tail2) {
14720
14681
  for (const marker of BLOCK_MARKERS) {
14721
14682
  if (marker.pattern.test(String(tail2 || ""))) return marker.ask;
@@ -14726,7 +14687,9 @@ function deriveStatus(input) {
14726
14687
  if (input.dead) return { status: "done" };
14727
14688
  const ask3 = detectAsk(input.tail);
14728
14689
  if (ask3) return { status: "blocked", ask: ask3 };
14729
- return { status: input.changed ? "working" : "idle" };
14690
+ if (input.changed) return { status: "working" };
14691
+ if (input.wasWorking && !input.seen) return { status: "replied" };
14692
+ return { status: "idle" };
14730
14693
  }
14731
14694
  function hashTail(tail2) {
14732
14695
  let hash = 0;
@@ -14759,39 +14722,110 @@ async function fetchConductorTasks(baseUrl) {
14759
14722
  return [];
14760
14723
  }
14761
14724
  }
14762
- async function discoverHostSpaces(repoCwd) {
14763
- const result = await run({ kind: "host" }, ["git", "-C", repoCwd, "worktree", "list", "--porcelain"]);
14725
+ function parseTrack(raw) {
14726
+ const ahead = String(raw || "").match(/ahead (\d+)/);
14727
+ const behind = String(raw || "").match(/behind (\d+)/);
14728
+ return [ahead && ahead[1] !== "0" ? "\u2191" + ahead[1] : "", behind && behind[1] !== "0" ? "\u2193" + behind[1] : ""].filter(Boolean).join(" ");
14729
+ }
14730
+ function canonicalSpacePath(path) {
14731
+ const clean = String(path || "").replace(/\/+$/, "");
14732
+ const managed = clean.match(/^(.+?)\/\.(?:claude|codex)\//);
14733
+ if (managed) return existsSync34(join32(managed[1], ".git")) ? managed[1] : "";
14734
+ return clean;
14735
+ }
14736
+ function loadSpaceList() {
14737
+ try {
14738
+ const parsed = JSON.parse(readFileSync30(SPACES_FILE, "utf8"));
14739
+ return Array.isArray(parsed) ? [...new Set(parsed.filter((path) => typeof path === "string").map((path) => path.replace(/\/+$/, "")).filter(Boolean))] : [];
14740
+ } catch {
14741
+ return [];
14742
+ }
14743
+ }
14744
+ function saveSpaceList(list) {
14745
+ try {
14746
+ mkdirSync20(dirname9(SPACES_FILE), { recursive: true });
14747
+ writeFileSync23(SPACES_FILE, JSON.stringify(list, null, 2));
14748
+ } catch {
14749
+ }
14750
+ }
14751
+ function rememberSpace(path) {
14752
+ const clean = String(path || "").replace(/\/+$/, "");
14753
+ if (!clean) return;
14754
+ const list = loadSpaceList();
14755
+ if (list.includes(clean)) return;
14756
+ saveSpaceList([...list, clean]);
14757
+ }
14758
+ function forgetSpace(path) {
14759
+ const clean = canonicalSpacePath(path);
14760
+ saveSpaceList(loadSpaceList().filter((entry) => entry !== clean));
14761
+ }
14762
+ async function listWorktrees(repoPath) {
14763
+ const result = await run({ kind: "host" }, ["git", "-C", repoPath, "worktree", "list", "--porcelain"]);
14764
14764
  if (!result.ok) return [];
14765
- return parseWorktrees(result.stdout).map((row2) => ({
14766
- name: row2.name,
14767
- branch: row2.branch,
14765
+ return parseWorktrees(result.stdout).map((row2, index) => ({
14768
14766
  path: row2.path,
14769
- backend: "host"
14767
+ branch: row2.branch,
14768
+ note: index === 0 ? "checkout" : row2.branch === "detached" ? "detached" : "",
14769
+ primary: index === 0
14770
14770
  }));
14771
14771
  }
14772
- async function discoverContainerSpaces(runner) {
14773
- if (runner.kind !== "container") return [];
14774
- const result = await run(runner, ["sh", "-c", "ls -1 /home/synkro/work 2>/dev/null"]);
14775
- if (!result.ok) return [];
14776
- return result.stdout.split("\n").map((line) => line.trim()).filter((name) => /^ui-/.test(name)).map((name) => ({
14777
- name,
14778
- branch: "container",
14779
- path: "/home/synkro/work/" + name,
14780
- backend: "container"
14781
- }));
14772
+ async function repoOf(path) {
14773
+ const result = await run({ kind: "host" }, ["git", "-C", path, "worktree", "list", "--porcelain"]);
14774
+ if (!result.ok) return canonicalSpacePath(path);
14775
+ const first = parseWorktrees(result.stdout)[0];
14776
+ return first ? first.path.replace(/\/+$/, "") : canonicalSpacePath(path);
14777
+ }
14778
+ async function discoverHostSpaces(repoCwd) {
14779
+ const host = { kind: "host" };
14780
+ const paths = [repoCwd, ...loadSpaceList()].map((path) => String(path || "").replace(/\/+$/, "")).filter(Boolean);
14781
+ const spaces = [];
14782
+ const seen = /* @__PURE__ */ new Set();
14783
+ for (const path of paths) {
14784
+ const root = await repoOf(path);
14785
+ if (!root || seen.has(root)) continue;
14786
+ seen.add(root);
14787
+ const status = await run(host, ["git", "-C", root, "status", "--porcelain=v2", "--branch", "--no-renames"]);
14788
+ const branch = status.stdout.match(/^# branch\.head (.+)$/m)?.[1] || "";
14789
+ const ab = status.stdout.match(/^# branch\.ab \+(\d+) -(\d+)$/m);
14790
+ spaces.push({
14791
+ name: root.split("/").filter(Boolean).pop() || root,
14792
+ branch: branch === "(detached)" ? "detached" : branch,
14793
+ path: root,
14794
+ backend: "host",
14795
+ track: ab ? parseTrack("ahead " + ab[1] + ", behind " + ab[2]) : ""
14796
+ });
14797
+ }
14798
+ return spaces;
14782
14799
  }
14783
- async function discoverAgents(runner, backend, previousHashes) {
14784
- const listed = await run(runner, buildListAgents());
14785
- const rows = listed.ok ? parseAgentSessions(listed.stdout, backend) : [];
14786
- const hashes = /* @__PURE__ */ new Map();
14800
+ function visibleAgents(all, space, filter, grouped) {
14801
+ if (grouped || filter === "all" || !space) return all;
14802
+ return all.filter((agent) => agent.repo === space.path);
14803
+ }
14804
+ async function discoverAgents(runner, backend, memory) {
14805
+ const snapshot = await run(runner, buildAgentSnapshot());
14806
+ const { list, captures } = parseAgentSnapshot(snapshot.ok ? snapshot.stdout : "");
14807
+ const rows = parseAgentSessions(list, backend);
14808
+ const next = /* @__PURE__ */ new Map();
14787
14809
  const agents = [];
14788
14810
  for (const row2 of rows) {
14789
- const capture = row2.dead ? { ok: true, stdout: "" } : await run(runner, buildCapture(row2.session));
14790
- const tail2 = capture.ok ? capture.stdout : "";
14811
+ const tail2 = row2.dead ? "" : captures.get(row2.session) || "";
14791
14812
  const nextHash = hashTail(tail2);
14792
- const changed = previousHashes.has(row2.session) && previousHashes.get(row2.session) !== nextHash;
14793
- hashes.set(row2.session, nextHash);
14794
- const derived = deriveStatus({ dead: row2.dead, tail: tail2, changed });
14813
+ const previous = memory.get(row2.session);
14814
+ const changed = previous !== void 0 && previous.hash !== nextHash;
14815
+ const seen = previous?.seen ?? false;
14816
+ const derived = deriveStatus({
14817
+ dead: row2.dead,
14818
+ tail: tail2,
14819
+ changed,
14820
+ // Remember a turn was running until the user actually looks at it.
14821
+ wasWorking: changed || (previous?.wasWorking ?? false),
14822
+ seen
14823
+ });
14824
+ next.set(row2.session, {
14825
+ hash: nextHash,
14826
+ wasWorking: seen ? false : changed || (previous?.wasWorking ?? false),
14827
+ seen: changed ? false : seen
14828
+ });
14795
14829
  agents.push({
14796
14830
  name: row2.name,
14797
14831
  session: row2.session,
@@ -14802,9 +14836,20 @@ async function discoverAgents(runner, backend, previousHashes) {
14802
14836
  ask: derived.ask
14803
14837
  });
14804
14838
  }
14805
- return { agents, hashes };
14839
+ return { agents, memory: next };
14840
+ }
14841
+ function offlineAgents(live, records) {
14842
+ const alive = new Set(live.map((agent) => agent.session));
14843
+ return records.filter((record) => !alive.has(record.session)).map((record) => ({
14844
+ name: record.name,
14845
+ session: record.session,
14846
+ harness: record.harness,
14847
+ space: record.space,
14848
+ backend: record.backend,
14849
+ status: "offline"
14850
+ }));
14806
14851
  }
14807
- var BLOCK_MARKERS;
14852
+ var BLOCK_MARKERS, SPACES_FILE;
14808
14853
  var init_model = __esm({
14809
14854
  "cli/ui/model.ts"() {
14810
14855
  "use strict";
@@ -14814,6 +14859,301 @@ var init_model = __esm({
14814
14859
  { pattern: /\[synkro:task-workspace|\[synkro:scm|keep working in the current workspace/i, ask: "workspace" },
14815
14860
  { pattern: /⛔/, ask: "tracking" }
14816
14861
  ];
14862
+ SPACES_FILE = join32(homedir32(), ".synkro", "ui-spaces.json");
14863
+ }
14864
+ });
14865
+
14866
+ // cli/ui/manifest.ts
14867
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync31, writeFileSync as writeFileSync24 } from "fs";
14868
+ import { homedir as homedir33 } from "os";
14869
+ import { dirname as dirname10, join as join33 } from "path";
14870
+ function loadRecords() {
14871
+ try {
14872
+ const parsed = JSON.parse(readFileSync31(MANIFEST_FILE, "utf8"));
14873
+ return Array.isArray(parsed) ? parsed.filter((row2) => row2 && typeof row2.session === "string" && typeof row2.space === "string") : [];
14874
+ } catch {
14875
+ return [];
14876
+ }
14877
+ }
14878
+ function saveRecords(records) {
14879
+ try {
14880
+ mkdirSync21(dirname10(MANIFEST_FILE), { recursive: true });
14881
+ writeFileSync24(MANIFEST_FILE, JSON.stringify(records, null, 2));
14882
+ } catch {
14883
+ }
14884
+ }
14885
+ function recordSession(record) {
14886
+ saveRecords([...loadRecords().filter((row2) => row2.session !== record.session), record]);
14887
+ }
14888
+ function forgetSession(session) {
14889
+ saveRecords(loadRecords().filter((row2) => row2.session !== session));
14890
+ }
14891
+ function resumeCommand(harness) {
14892
+ return RESUME_COMMANDS[harness] || RESUME_COMMANDS.claude;
14893
+ }
14894
+ function loadTabs() {
14895
+ try {
14896
+ const parsed = JSON.parse(readFileSync31(TABS_FILE, "utf8"));
14897
+ return Array.isArray(parsed) ? parsed.filter((row2) => row2 && typeof row2.cwd === "string" && typeof row2.kind === "string") : [];
14898
+ } catch {
14899
+ return [];
14900
+ }
14901
+ }
14902
+ function saveTabs(tabs) {
14903
+ try {
14904
+ mkdirSync21(dirname10(TABS_FILE), { recursive: true });
14905
+ writeFileSync24(TABS_FILE, JSON.stringify(tabs, null, 2));
14906
+ } catch {
14907
+ }
14908
+ }
14909
+ function loadLastAgents() {
14910
+ try {
14911
+ const parsed = JSON.parse(readFileSync31(LAST_AGENT_FILE, "utf8"));
14912
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
14913
+ } catch {
14914
+ return {};
14915
+ }
14916
+ }
14917
+ function lastAgentFor(space) {
14918
+ return loadLastAgents()[String(space || "").replace(/\/+$/, "")] || "";
14919
+ }
14920
+ function rememberLastAgent(space, session) {
14921
+ const key = String(space || "").replace(/\/+$/, "");
14922
+ if (!key || !session) return;
14923
+ try {
14924
+ mkdirSync21(dirname10(LAST_AGENT_FILE), { recursive: true });
14925
+ writeFileSync24(LAST_AGENT_FILE, JSON.stringify({ ...loadLastAgents(), [key]: session }, null, 2));
14926
+ } catch {
14927
+ }
14928
+ }
14929
+ function loadRepos() {
14930
+ try {
14931
+ const parsed = JSON.parse(readFileSync31(REPOS_FILE, "utf8"));
14932
+ return Array.isArray(parsed) ? parsed.filter((path) => typeof path === "string") : [];
14933
+ } catch {
14934
+ return [];
14935
+ }
14936
+ }
14937
+ function rememberRepo(path) {
14938
+ const clean = String(path || "").replace(/\/+$/, "");
14939
+ if (!clean) return;
14940
+ const next = [clean, ...loadRepos().filter((entry) => entry !== clean)].slice(0, 40);
14941
+ try {
14942
+ mkdirSync21(dirname10(REPOS_FILE), { recursive: true });
14943
+ writeFileSync24(REPOS_FILE, JSON.stringify(next, null, 2));
14944
+ } catch {
14945
+ }
14946
+ }
14947
+ var MANIFEST_FILE, RESUME_COMMANDS, TABS_FILE, LAST_AGENT_FILE, REPOS_FILE;
14948
+ var init_manifest = __esm({
14949
+ "cli/ui/manifest.ts"() {
14950
+ "use strict";
14951
+ MANIFEST_FILE = join33(homedir33(), ".synkro", "ui-sessions.json");
14952
+ RESUME_COMMANDS = {
14953
+ claude: "claude --continue",
14954
+ codex: "codex resume --last",
14955
+ cursor: "cursor-agent --continue"
14956
+ };
14957
+ TABS_FILE = join33(homedir33(), ".synkro", "ui-tabs.json");
14958
+ LAST_AGENT_FILE = join33(homedir33(), ".synkro", "ui-last-agent.json");
14959
+ REPOS_FILE = join33(homedir33(), ".synkro", "ui-repos.json");
14960
+ }
14961
+ });
14962
+
14963
+ // cli/ui/launch.ts
14964
+ import { mkdirSync as mkdirSync22, writeFileSync as writeFileSync25 } from "fs";
14965
+ import { homedir as homedir34 } from "os";
14966
+ import { join as join34 } from "path";
14967
+ function sidebarColumns(totalColumns) {
14968
+ return String(Math.max(20, Math.min(34, Math.round(totalColumns * 0.24))));
14969
+ }
14970
+ async function currentWindowColumns(windowTarget) {
14971
+ const asked = await run(HOST, ["tmux", "display-message", "-p", "-t", windowTarget, "#{window_width}"]);
14972
+ return Number(asked.stdout.trim()) || Number(process.stdout.columns || 0) || 220;
14973
+ }
14974
+ function makeTerminalCommand(bootPath) {
14975
+ if (!(process.env.SHELL || "").endsWith("zsh")) return void 0;
14976
+ const dir = join34(homedir34(), ".synkro", "ui-shell");
14977
+ const wrap2 = (command, kind) => command + "() { if whence -p " + command + ' >/dev/null 2>&1; then exec node "$SYNKRO_UI_BOOT" ui --takeover ' + kind + ' "$PWD"; else echo "zsh: command not found: ' + command + '" >&2; return 127; fi; }';
14978
+ try {
14979
+ mkdirSync22(dir, { recursive: true });
14980
+ writeFileSync25(join34(dir, ".zshenv"), '[ -f "$HOME/.zshenv" ] && source "$HOME/.zshenv"\n');
14981
+ writeFileSync25(join34(dir, ".zshrc"), [
14982
+ '[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"',
14983
+ wrap2("claude", "claude"),
14984
+ wrap2("codex", "codex"),
14985
+ wrap2("cursor-agent", "cursor"),
14986
+ ""
14987
+ ].join("\n"));
14988
+ } catch {
14989
+ return void 0;
14990
+ }
14991
+ return "env ZDOTDIR=" + shellQuote3(dir) + " SYNKRO_UI_BOOT=" + shellQuote3(bootPath) + " zsh -i";
14992
+ }
14993
+ function sidebarCommand(bootPath, centerPane, repoCwd) {
14994
+ const env = [
14995
+ "SYNKRO_UI_CENTER=" + shellQuote3(centerPane),
14996
+ "SYNKRO_UI_OUTER=" + UI_SESSION,
14997
+ "SYNKRO_UI_BOOT=" + shellQuote3(bootPath),
14998
+ "SYNKRO_UI_REPO=" + shellQuote3(repoCwd)
14999
+ ].join(" ");
15000
+ return "env " + env + " node " + shellQuote3(bootPath) + " ui --sidebar";
15001
+ }
15002
+ function writeDialogLauncher(name, bootPath, kind, repoCwd, args2 = []) {
15003
+ const dir = join34(homedir34(), ".synkro", "ui-shell");
15004
+ const target = join34(dir, name);
15005
+ try {
15006
+ mkdirSync22(dir, { recursive: true });
15007
+ writeFileSync25(target, [
15008
+ "#!/bin/sh",
15009
+ ["exec node", shellQuote3(bootPath), "ui --dialog", kind, shellQuote3(repoCwd), ...args2.map(shellQuote3)].join(" "),
15010
+ ""
15011
+ ].join("\n"), { mode: 493 });
15012
+ } catch {
15013
+ }
15014
+ return target;
15015
+ }
15016
+ function shortSpace(name) {
15017
+ const value = String(name || "");
15018
+ return value.length <= 14 ? value : value.slice(0, 13) + "\u2026";
15019
+ }
15020
+ function tabTitle(kind, spaceName) {
15021
+ const glyph = TAB_GLYPHS[kind] || TAB_GLYPHS.terminal;
15022
+ if (kind === "terminal" || kind === "settings") return glyph + " " + shortSpace(spaceName);
15023
+ return glyph + " " + kind + (spaceName ? " " + shortSpace(spaceName) : "");
15024
+ }
15025
+ function chipStrip(sidebarWidth) {
15026
+ const chip = "#[range=window|#{window_index}]#{?window_active,#[bg=colour111]#[fg=colour233]#[bold] #W #[default],#[fg=colour245] #W }#[norange]";
15027
+ const brand = " \u29C9 synkro ";
15028
+ const gap = " ".repeat(Math.max(1, sidebarWidth + 1 - brand.length));
15029
+ return "#[align=left]#[fg=colour111]#[bold]" + brand + "#[default]" + gap + "#{W:" + chip + "}#[range=user|tabmenu]#[fg=colour245] \xD7 #[norange]#[range=user|newtab]#[fg=colour240] + #[norange]#[default]";
15030
+ }
15031
+ async function styleOuterSession(bootPath, repoCwd, sidebarWidth) {
15032
+ const launcher = writeDialogLauncher("new-tab", bootPath, "new-tab", repoCwd);
15033
+ const tabLauncher = writeDialogLauncher("tab-menu", bootPath, "tab", repoCwd, [UI_SESSION]);
15034
+ const popup = "display-popup -E -w 80% -h 70% -S fg=colour111 " + shellQuote3(launcher);
15035
+ const tabPopup = "display-popup -E -w 60 -h 18 -S fg=colour111 " + shellQuote3(tabLauncher);
15036
+ const dispatch = 'if-shell -F "#{==:#{mouse_status_range},tabmenu}" "' + tabPopup + '" "select-window -t="';
15037
+ const style = [
15038
+ // Mouse: drag the pane border to resize the sidebar, click a pane to
15039
+ // focus it, click rows/chips. Without this the fixed split reads as
15040
+ // "blocked in".
15041
+ ["set-option", "-t", UI_SESSION, "mouse", "on"],
15042
+ ["set-option", "-t", UI_SESSION, "status-position", "top"],
15043
+ ["set-option", "-t", UI_SESSION, "status-style", "bg=colour233,fg=colour245"],
15044
+ ["set-option", "-t", UI_SESSION, "status-format[0]", chipStrip(sidebarWidth)],
15045
+ // The window/tab title the terminal app displays — 'synkro', never the
15046
+ // name of whatever process happens to be in the foreground.
15047
+ ["set-option", "-t", UI_SESSION, "set-titles", "on"],
15048
+ ["set-option", "-t", UI_SESSION, "set-titles-string", "synkro"],
15049
+ ["set-option", "-t", UI_SESSION, "pane-border-style", "fg=colour236"],
15050
+ ["set-option", "-t", UI_SESSION, "pane-active-border-style", "fg=colour111"],
15051
+ // Clicking + opens the picker; clicking a name chip selects that tab.
15052
+ // A `range=user|X` reports mouse_status_range as bare X — comparing
15053
+ // against 'user|newtab' silently never matches and the click dead-ends.
15054
+ ["bind-key", "-n", "MouseDown1Status", "if-shell", "-F", "#{==:#{mouse_status_range},newtab}", popup, dispatch],
15055
+ // Right-click anywhere on the strip opens the same tab picker.
15056
+ ["bind-key", "-n", "MouseDown3Status", "display-popup", "-E", "-w", "60", "-h", "18", "-S", "fg=colour111", tabLauncher],
15057
+ // Tab keys without the prefix: Alt+t new tab picker, Alt+arrows move.
15058
+ ["bind-key", "-n", "M-t", "display-popup", "-E", "-w", "80%", "-h", "70%", "-S", "fg=colour111", launcher],
15059
+ ["bind-key", "-n", "M-Right", "next-window"],
15060
+ ["bind-key", "-n", "M-Left", "previous-window"],
15061
+ // Alt+s always returns focus to the sidebar (the leftmost pane).
15062
+ ["bind-key", "-n", "M-s", "select-pane", "-L"]
15063
+ ];
15064
+ for (const argv of style) await run(HOST, ["tmux", ...argv]);
15065
+ }
15066
+ async function buildTab(bootPath, repoCwd, spec) {
15067
+ let windowTarget;
15068
+ if (!await uiSessionExists()) {
15069
+ const cols = String(Number(process.stdout.columns || 0) || 220);
15070
+ const rows = String(Number(process.stdout.rows || 0) || 55);
15071
+ const create = ["tmux", "new-session", "-d", "-s", UI_SESSION, "-x", cols, "-y", rows, "-c", spec.cwd];
15072
+ if (spec.center) create.push(spec.center);
15073
+ const made = await run(HOST, create);
15074
+ if (!made.ok) {
15075
+ throw new Error("tmux new-session failed: " + made.stderr.trim());
15076
+ }
15077
+ await run(HOST, ["tmux", "set-option", "-t", UI_SESSION, "base-index", "1"]);
15078
+ await run(HOST, ["tmux", "move-window", "-r", "-s", UI_SESSION]);
15079
+ const first = await run(HOST, ["tmux", "list-windows", "-t", UI_SESSION, "-F", "#{window_id}"]);
15080
+ windowTarget = first.stdout.split("\n")[0]?.trim() || UI_SESSION + ":1";
15081
+ } else {
15082
+ const create = ["tmux", "new-window", "-t", UI_SESSION, "-P", "-F", "#{window_id}", "-c", spec.cwd];
15083
+ if (spec.center) create.push(spec.center);
15084
+ const created = await run(HOST, create);
15085
+ windowTarget = created.stdout.trim();
15086
+ if (!created.ok || !windowTarget) return;
15087
+ }
15088
+ const sidebarCols = sidebarColumns(await currentWindowColumns(windowTarget));
15089
+ await styleOuterSession(bootPath, repoCwd, Number(sidebarCols));
15090
+ const split = await run(HOST, [
15091
+ "tmux",
15092
+ "split-window",
15093
+ "-hb",
15094
+ "-t",
15095
+ windowTarget,
15096
+ "-l",
15097
+ sidebarCols,
15098
+ "-P",
15099
+ "-F",
15100
+ "#{pane_id}",
15101
+ "tail -f /dev/null"
15102
+ ]);
15103
+ const sidebarPane = split.stdout.trim();
15104
+ const panes = await run(HOST, ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_id}"]);
15105
+ const centerPane = panes.stdout.split("\n").map((line) => line.trim()).filter(Boolean).find((id) => id !== sidebarPane) || "";
15106
+ if (spec.agentSession) {
15107
+ await run(HOST, ["tmux", "set-option", "-t", windowTarget, "-w", "-q", "@synkro_agent", spec.agentSession]);
15108
+ }
15109
+ await run(HOST, ["tmux", "set-option", "-t", windowTarget, "-w", "-q", "@synkro_cwd", spec.cwd]);
15110
+ await run(HOST, ["tmux", "set-option", "-t", windowTarget, "-w", "-q", "@synkro_kind", spec.kind || "terminal"]);
15111
+ await run(HOST, ["tmux", "respawn-pane", "-k", "-t", sidebarPane, sidebarCommand(bootPath, centerPane, repoCwd)]);
15112
+ const title = spec.title || tabTitle("terminal", spec.cwd.split("/").filter(Boolean).pop() || "space");
15113
+ await run(HOST, ["tmux", "set-option", "-t", windowTarget, "-w", "automatic-rename", "off"]);
15114
+ await run(HOST, ["tmux", "rename-window", "-t", windowTarget, title]);
15115
+ await snapshotTabs();
15116
+ await run(HOST, ["tmux", "select-pane", "-t", spec.focus === "sidebar" ? sidebarPane : centerPane]);
15117
+ }
15118
+ async function snapshotTabs() {
15119
+ const listed = await run(HOST, [
15120
+ "tmux",
15121
+ "list-windows",
15122
+ "-t",
15123
+ UI_SESSION,
15124
+ "-F",
15125
+ ["#{window_name}", "#{@synkro_cwd}", "#{@synkro_kind}", "#{@synkro_agent}"].join("|")
15126
+ ]);
15127
+ if (!listed.ok) return;
15128
+ const tabs = listed.stdout.split("\n").map((line) => line.split("|")).filter((cols) => cols.length >= 4 && cols[1]).map((cols) => ({ title: cols[0], cwd: cols[1], kind: cols[2] || "terminal", agentSession: cols[3] || "" }));
15129
+ if (tabs.length > 0) saveTabs(tabs);
15130
+ }
15131
+ async function uiSessionExists() {
15132
+ const result = await run(HOST, ["tmux", "has-session", "-t", UI_SESSION]);
15133
+ return result.ok;
15134
+ }
15135
+ async function launchUi(bootPath, repoCwd) {
15136
+ rememberSpace(repoCwd);
15137
+ if (!await uiSessionExists()) {
15138
+ await buildTab(bootPath, repoCwd, { cwd: repoCwd, center: makeTerminalCommand(bootPath), focus: "sidebar" });
15139
+ }
15140
+ return runInherit(process.env.TMUX ? ["tmux", "switch-client", "-t", UI_SESSION] : ["tmux", "attach-session", "-t", UI_SESSION]);
15141
+ }
15142
+ var HOST, TAB_GLYPHS;
15143
+ var init_launch = __esm({
15144
+ "cli/ui/launch.ts"() {
15145
+ "use strict";
15146
+ init_tmux();
15147
+ init_model();
15148
+ init_manifest();
15149
+ HOST = { kind: "host" };
15150
+ TAB_GLYPHS = {
15151
+ claude: "\u2733",
15152
+ codex: "\u2B21",
15153
+ cursor: "\u27A4",
15154
+ terminal: "\u276F",
15155
+ settings: "\u2699"
15156
+ };
14817
15157
  }
14818
15158
  });
14819
15159
 
@@ -14840,9 +15180,15 @@ var init_consent = __esm({
14840
15180
  function pad(text, width) {
14841
15181
  return text.length >= width ? text.slice(0, width) : text + " ".repeat(width - text.length);
14842
15182
  }
15183
+ function clip(text, max) {
15184
+ const value = String(text || "");
15185
+ if (max <= 1) return value.slice(0, Math.max(0, max));
15186
+ return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
15187
+ }
14843
15188
  function row(selected, width, content) {
14844
15189
  const body = stripForPad(" " + content, width);
14845
- return selected ? STYLE.select + body + STYLE.reset : body;
15190
+ if (!selected) return body;
15191
+ return STYLE.select + body.split(STYLE.reset).join(STYLE.reset + STYLE.select) + STYLE.reset;
14846
15192
  }
14847
15193
  function stripForPad(text, width) {
14848
15194
  let visible = 0;
@@ -14862,49 +15208,143 @@ function stripForPad(text, width) {
14862
15208
  }
14863
15209
  return out + " ".repeat(Math.max(0, width - visible));
14864
15210
  }
14865
- function renderSidebar(state, width = 32, height = 40) {
15211
+ function visibleLength(text) {
15212
+ let visible = 0;
15213
+ let index = 0;
15214
+ while (index < text.length) {
15215
+ if (text.startsWith(ESC, index)) {
15216
+ const end = text.indexOf("m", index);
15217
+ if (end === -1) break;
15218
+ index = end + 1;
15219
+ } else {
15220
+ visible += 1;
15221
+ index += 1;
15222
+ }
15223
+ }
15224
+ return visible;
15225
+ }
15226
+ function splitRow(width, left, right) {
15227
+ const gap = Math.max(1, width - 2 - visibleLength(left) - visibleLength(right));
15228
+ return stripForPad(" " + STYLE.dim + left + STYLE.reset + " ".repeat(gap) + STYLE.header + right + STYLE.reset, width);
15229
+ }
15230
+ function windowAround(count, selected, capacity) {
15231
+ if (count <= capacity) return { start: 0, end: count };
15232
+ const start = Math.min(Math.max(0, selected - Math.floor(capacity / 2)), count - capacity);
15233
+ return { start, end: start + capacity };
15234
+ }
15235
+ function renderCollapsed(state, width, height) {
14866
15236
  const lines = [];
14867
- lines.push("");
14868
- lines.push(" " + STYLE.header + "spaces" + STYLE.reset);
14869
- lines.push("");
15237
+ const targets = [];
15238
+ const push2 = (text, target = null) => {
15239
+ lines.push(stripForPad(" " + text, width));
15240
+ targets.push(target);
15241
+ };
15242
+ push2("");
14870
15243
  state.spaces.forEach((space, index) => {
14871
- const selected = state.section === "spaces" && index === state.spaceIndex;
14872
- const badge = space.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
14873
- lines.push(row(selected, width, STYLE.done + "\u25CF" + STYLE.reset + " " + badge + STYLE.bold + space.name + STYLE.reset));
14874
- lines.push(row(selected, width, " " + STYLE.branch + space.branch + STYLE.reset));
15244
+ const on = index === state.spaceIndex;
15245
+ const glyph = index < 9 ? String(index + 1) : "+";
15246
+ push2((on ? STYLE.select : "") + (space.backend === "container" ? STYLE.accent : STYLE.bold) + glyph + STYLE.reset, { kind: "space", index });
14875
15247
  });
14876
- if (state.spaces.length === 0) lines.push(row(false, width, STYLE.dim + "no spaces found" + STYLE.reset));
14877
- lines.push("");
14878
- lines.push(" " + STYLE.header + "agents" + STYLE.reset);
14879
- lines.push("");
15248
+ push2(STYLE.dim + "\u2500" + STYLE.reset);
14880
15249
  state.agents.forEach((agent, index) => {
15250
+ const dot = DOT[agent.status] || DOT.idle;
15251
+ const viewing = agent.session === state.viewing && state.viewing !== "";
15252
+ push2((viewing ? STYLE.select : "") + dot, { kind: "agent", index });
15253
+ });
15254
+ push2("");
15255
+ push2(STYLE.dim + "+" + STYLE.reset, { kind: "new" });
15256
+ while (lines.length < height - 1) push2("");
15257
+ push2(STYLE.dim + "\u203A\u203A" + STYLE.reset, { kind: "collapse" });
15258
+ return { lines: lines.slice(0, height), targets: targets.slice(0, height) };
15259
+ }
15260
+ function renderLayout(state, width = 30, height = 40) {
15261
+ if (state.collapsed) return renderCollapsed(state, width, height);
15262
+ const lines = [];
15263
+ const targets = [];
15264
+ const push2 = (line, target = null) => {
15265
+ lines.push(line);
15266
+ targets.push(target);
15267
+ };
15268
+ const chrome = 8;
15269
+ const listRoom = Math.max(4, height - chrome);
15270
+ const spacesCapacity = Math.max(1, Math.floor(Math.floor(listRoom / 2) / 2));
15271
+ const agentsCapacity = Math.max(1, Math.floor(Math.ceil(listRoom / 2) / 2));
15272
+ const spaceView = windowAround(state.spaces.length, state.spaceIndex, spacesCapacity);
15273
+ const agentView = windowAround(state.agents.length, state.agentIndex, agentsCapacity);
15274
+ push2("");
15275
+ push2(" " + STYLE.header + "sessions" + STYLE.reset);
15276
+ push2("");
15277
+ state.spaces.slice(spaceView.start, spaceView.end).forEach((space, offset) => {
15278
+ const index = spaceView.start + offset;
15279
+ const selected = index === state.spaceIndex;
15280
+ const badge = space.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
15281
+ const target = { kind: "space", index };
15282
+ push2(row(selected, width, STYLE.done + "\xB7" + STYLE.reset + " " + badge + STYLE.bold + clip(space.name, width - 6) + STYLE.reset), target);
15283
+ const drift = space.track ? " " + STYLE.track + space.track + STYLE.reset : "";
15284
+ push2(row(selected, width, " " + STYLE.branch + clip(space.branch, width - 6 - (space.track ? space.track.length + 1 : 0)) + STYLE.reset + drift), target);
15285
+ });
15286
+ if (state.spaces.length === 0) push2(row(false, width, STYLE.dim + "no spaces found" + STYLE.reset));
15287
+ while (lines.length < 3 + spacesCapacity * 2) push2(pad("", width));
15288
+ push2("");
15289
+ push2(splitRow(width, "new", "menu"), { kind: "new" });
15290
+ push2(STYLE.dim + "\u2500".repeat(Math.max(0, width)) + STYLE.reset);
15291
+ const blocked = state.agents.filter((agent) => agent.status === "blocked").length;
15292
+ const replied = state.agents.filter((agent) => agent.status === "replied").length;
15293
+ const header = blocked > 0 ? "agents " + STYLE.blocked + blocked + " waiting" + STYLE.reset : replied > 0 ? "agents " + STYLE.reply + replied + " replied" + STYLE.reset : "agents";
15294
+ const filterLabel = state.grouped ? "attention" : state.filter === "space" ? state.spaces[state.spaceIndex]?.name || "space" : "all";
15295
+ push2(splitRow(width, header, clip(filterLabel, 12)), { kind: "grouped" });
15296
+ push2("");
15297
+ const ordered = state.grouped ? [...state.agents].sort((a, b) => ATTENTION_ORDER.indexOf(a.status) - ATTENTION_ORDER.indexOf(b.status) || a.name.localeCompare(b.name)) : state.agents;
15298
+ let lastGroup = "";
15299
+ ordered.slice(agentView.start, agentView.end).forEach((agent) => {
15300
+ const index = state.agents.indexOf(agent);
15301
+ if (state.grouped && agent.status !== lastGroup) {
15302
+ lastGroup = agent.status;
15303
+ const label = ATTENTION_LABEL[agent.status] || agent.status;
15304
+ const count = ordered.filter((row2) => row2.status === agent.status).length;
15305
+ push2(row(false, width, (ATTENTION_STYLE[agent.status] || STYLE.dim) + clip(label, width - 8) + STYLE.reset + STYLE.dim + " " + count + STYLE.reset));
15306
+ }
14881
15307
  const selected = state.section === "agents" && index === state.agentIndex;
15308
+ const isViewing = agent.session === state.viewing && state.viewing !== "";
14882
15309
  const dot = DOT[agent.status] || DOT.idle;
14883
15310
  const badge = agent.backend === "container" ? STYLE.accent + "\u25A3 " + STYLE.reset : "";
14884
15311
  const linear = agent.linear ? " " + STYLE.dim + agent.linear + STYLE.reset : "";
14885
- lines.push(row(selected, width, dot + " " + badge + STYLE.bold + agent.name + STYLE.reset + linear));
14886
- const statusStyle = agent.status === "blocked" ? STYLE.blocked : STYLE.dim;
14887
- lines.push(row(selected, width, " " + statusStyle + agent.status + STYLE.reset + STYLE.dim + " \xB7 " + agent.harness + STYLE.reset));
15312
+ const target = { kind: "agent", index };
15313
+ const nameStyle = isViewing ? STYLE.accent + STYLE.bold : STYLE.bold;
15314
+ push2(row(selected, width, dot + " " + badge + nameStyle + clip(agent.name, width - 6 - (agent.linear ? agent.linear.length + 2 : 0)) + STYLE.reset + linear), target);
15315
+ const detail = state.grouped ? agent.space ? agent.space.split("/").filter(Boolean).pop() || agent.space : agent.backend : agent.status;
15316
+ const detailStyle = !state.grouped && agent.status === "blocked" ? STYLE.blocked : STYLE.dim;
15317
+ push2(row(selected, width, " " + detailStyle + clip(detail, width - 8 - agent.harness.length) + STYLE.reset + STYLE.dim + " \xB7 " + agent.harness + STYLE.reset), target);
14888
15318
  });
14889
- if (state.agents.length === 0) lines.push(row(false, width, STYLE.dim + "no agents \u2014 n to spawn" + STYLE.reset));
15319
+ if (state.agents.length === 0) {
15320
+ push2(row(false, width, STYLE.dim + (state.filter === "space" ? "none in this space \u2014 n to spawn" : "no agents \u2014 n to spawn") + STYLE.reset));
15321
+ }
14890
15322
  const selectedAgent = state.section === "agents" ? state.agents[state.agentIndex] : void 0;
15323
+ if (selectedAgent?.status === "offline") {
15324
+ push2("");
15325
+ push2(row(false, width, STYLE.accent + "\u23FB session offline" + STYLE.reset));
15326
+ push2(row(false, width, STYLE.dim + " r \u2014 restore, resumes the work" + STYLE.reset));
15327
+ }
14891
15328
  if (selectedAgent?.status === "blocked" && selectedAgent.ask) {
14892
- lines.push("");
14893
- lines.push(row(false, width, STYLE.blocked + "\u26D4 needs consent" + STYLE.reset));
15329
+ push2("");
15330
+ push2(row(false, width, STYLE.blocked + "\u26D4 needs consent" + STYLE.reset));
14894
15331
  for (const action of actionsForAsk(selectedAgent.ask)) {
14895
15332
  const key = action === "track" ? "g" : action === "skip" ? "s" : "y";
14896
- lines.push(row(false, width, STYLE.dim + " " + key + " \u2014 " + action + STYLE.reset));
15333
+ push2(row(false, width, STYLE.dim + " " + key + " \u2014 " + action + STYLE.reset));
14897
15334
  }
14898
15335
  }
14899
- while (lines.length < height - 4) lines.push(pad("", width));
14900
- lines.push(pad("", width));
14901
- if (state.message) lines.push(row(false, width, STYLE.accent + state.message.slice(0, width - 2) + STYLE.reset));
14902
- else lines.push(row(false, width, STYLE.dim + "n new \xB7 enter attach \xB7 x kill" + STYLE.reset));
14903
- lines.push(row(false, width, STYLE.dim + "T tab \xB7 i interrupt \xB7 q quit" + STYLE.reset));
14904
- lines.push(row(false, width, STYLE.dim + state.backendNote + STYLE.reset));
14905
- return lines.slice(0, height).join("\n");
15336
+ while (lines.length < height - 1) push2(pad("", width));
15337
+ push2(stripForPad(pad("", width - 3) + STYLE.dim + "\u2039\u2039 " + STYLE.reset, width), { kind: "collapse" });
15338
+ return { lines: lines.slice(0, height), targets: targets.slice(0, height) };
15339
+ }
15340
+ function nextSelection(state, spaces, agents, delta) {
15341
+ const flat = state.section === "spaces" ? state.spaceIndex : spaces + state.agentIndex;
15342
+ const next = Math.min(Math.max(0, flat + delta), Math.max(0, spaces + agents - 1));
15343
+ if (next < spaces) return { section: "spaces", spaceIndex: next, agentIndex: state.agentIndex };
15344
+ if (agents > 0) return { section: "agents", spaceIndex: state.spaceIndex, agentIndex: next - spaces };
15345
+ return { ...state };
14906
15346
  }
14907
- var ESC, STYLE, DOT;
15347
+ var ESC, STYLE, ATTENTION_ORDER, ATTENTION_LABEL, ATTENTION_STYLE, DOT;
14908
15348
  var init_render = __esm({
14909
15349
  "cli/ui/render.ts"() {
14910
15350
  "use strict";
@@ -14915,19 +15355,42 @@ var init_render = __esm({
14915
15355
  dim: ESC + "2m",
14916
15356
  bold: ESC + "1m",
14917
15357
  header: ESC + "38;5;245m",
14918
- select: ESC + "48;5;236m",
15358
+ select: ESC + "48;5;238m",
14919
15359
  working: ESC + "38;5;214m",
14920
15360
  idle: ESC + "38;5;244m",
14921
15361
  blocked: ESC + "38;5;203m",
14922
15362
  done: ESC + "38;5;114m",
14923
- accent: ESC + "38;5;135m",
14924
- branch: ESC + "38;5;140m"
15363
+ accent: ESC + "38;5;111m",
15364
+ branch: ESC + "38;5;140m",
15365
+ track: ESC + "38;5;211m",
15366
+ // A finished turn waiting on you: loud enough to catch the eye from across
15367
+ // the screen, distinct from the red of a hard block.
15368
+ reply: ESC + "38;5;155m"
15369
+ };
15370
+ ATTENTION_ORDER = ["blocked", "replied", "idle", "offline", "working", "done"];
15371
+ ATTENTION_LABEL = {
15372
+ blocked: "needs you",
15373
+ replied: "replied",
15374
+ idle: "idle",
15375
+ offline: "offline",
15376
+ working: "working",
15377
+ done: "finished"
15378
+ };
15379
+ ATTENTION_STYLE = {
15380
+ blocked: STYLE.blocked,
15381
+ replied: STYLE.reply,
15382
+ idle: STYLE.header,
15383
+ offline: STYLE.dim,
15384
+ working: STYLE.working,
15385
+ done: STYLE.done
14925
15386
  };
14926
15387
  DOT = {
14927
15388
  working: STYLE.working + "\u25CF" + STYLE.reset,
14928
- idle: STYLE.idle + "\u25CB" + STYLE.reset,
15389
+ replied: STYLE.reply + "\u25C6" + STYLE.reset,
15390
+ idle: STYLE.idle + "\xB7" + STYLE.reset,
14929
15391
  blocked: STYLE.blocked + "\u25CF" + STYLE.reset,
14930
- done: STYLE.done + "\u25CF" + STYLE.reset
15392
+ done: STYLE.done + "\u25CF" + STYLE.reset,
15393
+ offline: STYLE.dim + "\u25CB" + STYLE.reset
14931
15394
  };
14932
15395
  }
14933
15396
  });
@@ -14963,225 +15426,1600 @@ async function releaseAgent(runner, pueueId) {
14963
15426
  await run(runner, buildRemove(pueueId)).catch?.(() => {
14964
15427
  });
14965
15428
  }
14966
- var PUEUE_GROUP;
14967
- var init_pueue2 = __esm({
14968
- "cli/ui/pueue.ts"() {
15429
+ var PUEUE_GROUP;
15430
+ var init_pueue2 = __esm({
15431
+ "cli/ui/pueue.ts"() {
15432
+ "use strict";
15433
+ init_tmux();
15434
+ PUEUE_GROUP = "synkro-ui";
15435
+ }
15436
+ });
15437
+
15438
+ // cli/ui/backend.ts
15439
+ async function detectHarnesses() {
15440
+ const probe = await run({ kind: "host" }, [
15441
+ "sh",
15442
+ "-c",
15443
+ 'for c in claude codex cursor-agent; do command -v "$c" >/dev/null 2>&1 && echo "$c"; done'
15444
+ ]);
15445
+ const found = new Set(probe.stdout.split("\n").map((line) => line.trim()));
15446
+ const available = [];
15447
+ if (found.has("claude")) available.push("claude");
15448
+ if (found.has("codex")) available.push("codex");
15449
+ if (found.has("cursor-agent")) available.push("cursor");
15450
+ return available;
15451
+ }
15452
+ async function detectContainerBackend() {
15453
+ const runner = { kind: "container", container: CONTAINER_NAME2 };
15454
+ const probe = await run({ kind: "host" }, [
15455
+ "docker",
15456
+ "exec",
15457
+ CONTAINER_NAME2,
15458
+ "sh",
15459
+ "-c",
15460
+ "command -v tmux >/dev/null && command -v claude >/dev/null && ls " + shellQuote3(AUTH_SEED) + " >/dev/null 2>&1 && echo ready"
15461
+ ]);
15462
+ if (probe.ok && probe.stdout.includes("ready")) {
15463
+ return { runner, backend: "container", note: "runtime: container (" + CONTAINER_NAME2 + ")" };
15464
+ }
15465
+ return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
15466
+ }
15467
+ async function provisionContainerWorkspace(runner, slug) {
15468
+ const dir = CONTAINER_WORK + "/ui-" + slug;
15469
+ await run(runner, [
15470
+ "sh",
15471
+ "-c",
15472
+ "mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
15473
+ ]);
15474
+ return dir;
15475
+ }
15476
+ async function spawnAgent(info, request) {
15477
+ const slug = slugify(request.name);
15478
+ const session = agentSession(slug);
15479
+ const runner = request.backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15480
+ let cwd = request.cwd;
15481
+ if (request.backend === "container") {
15482
+ cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
15483
+ }
15484
+ const command = request.resume ? resumeCommand(request.harness) : HARNESS_COMMANDS[request.harness] || "claude";
15485
+ for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: cwd, backend: request.backend })) {
15486
+ const result = await run(runner, argv);
15487
+ if (!result.ok && argv[1] === "new-session") {
15488
+ return { ok: false, session, error: result.stderr.trim() || "tmux new-session failed" };
15489
+ }
15490
+ }
15491
+ const pueueId = await registerAgent(runner, session);
15492
+ if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
15493
+ recordSession({
15494
+ session,
15495
+ name: slug,
15496
+ harness: request.harness,
15497
+ space: cwd,
15498
+ spaceName: request.spaceName,
15499
+ backend: request.backend
15500
+ });
15501
+ return { ok: true, session };
15502
+ }
15503
+ async function killAgent(backend, session, pueueId) {
15504
+ const runner = backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15505
+ await run(runner, buildKillSession(session));
15506
+ await releaseAgent(runner, pueueId);
15507
+ forgetSession(session);
15508
+ }
15509
+ var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED, HARNESS_COMMANDS;
15510
+ var init_backend = __esm({
15511
+ "cli/ui/backend.ts"() {
15512
+ "use strict";
15513
+ init_tmux();
15514
+ init_pueue2();
15515
+ init_manifest();
15516
+ CONTAINER_NAME2 = "synkro-server";
15517
+ CONTAINER_WORK = "/home/synkro/work";
15518
+ AUTH_SEED = CONTAINER_WORK + "/claude-1/.claude.json";
15519
+ HARNESS_COMMANDS = {
15520
+ claude: "claude",
15521
+ codex: "codex",
15522
+ cursor: "cursor-agent"
15523
+ };
15524
+ }
15525
+ });
15526
+
15527
+ // cli/ui/awake.ts
15528
+ import { spawn as spawn8 } from "child_process";
15529
+ import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync32, rmSync as rmSync6, writeFileSync as writeFileSync26 } from "fs";
15530
+ import { homedir as homedir35, platform as platform5 } from "os";
15531
+ import { dirname as dirname11, join as join35 } from "path";
15532
+ function shouldStayAwake(agents) {
15533
+ return agents.some((agent) => agent.status === "working");
15534
+ }
15535
+ function awakeCommand(os = platform5()) {
15536
+ if (os === "darwin") return ["caffeinate", "-i", "-s"];
15537
+ if (os === "linux") {
15538
+ return ["systemd-inhibit", "--what=idle:sleep", "--who=synkro", "--why=agent session running", "sleep", "infinity"];
15539
+ }
15540
+ return null;
15541
+ }
15542
+ function livePid() {
15543
+ try {
15544
+ const pid = Number(readFileSync32(PIDFILE, "utf8").trim());
15545
+ if (!pid) return 0;
15546
+ process.kill(pid, 0);
15547
+ return pid;
15548
+ } catch {
15549
+ return 0;
15550
+ }
15551
+ }
15552
+ function release() {
15553
+ const pid = livePid();
15554
+ if (pid) {
15555
+ try {
15556
+ process.kill(pid);
15557
+ } catch {
15558
+ }
15559
+ }
15560
+ try {
15561
+ if (existsSync35(PIDFILE)) rmSync6(PIDFILE);
15562
+ } catch {
15563
+ }
15564
+ }
15565
+ function hold() {
15566
+ if (livePid()) return;
15567
+ const command = awakeCommand();
15568
+ if (!command) return;
15569
+ try {
15570
+ const child = spawn8(command[0], command.slice(1), { detached: true, stdio: "ignore" });
15571
+ child.unref();
15572
+ if (!child.pid) return;
15573
+ mkdirSync23(dirname11(PIDFILE), { recursive: true });
15574
+ writeFileSync26(PIDFILE, String(child.pid));
15575
+ } catch {
15576
+ }
15577
+ }
15578
+ function syncAwake(active, now = Date.now()) {
15579
+ if (active) {
15580
+ lastActive = now;
15581
+ hold();
15582
+ return true;
15583
+ }
15584
+ if (lastActive > 0 && now - lastActive < GRACE_MS) return livePid() > 0;
15585
+ lastActive = 0;
15586
+ release();
15587
+ return false;
15588
+ }
15589
+ function releaseAwake() {
15590
+ release();
15591
+ }
15592
+ var PIDFILE, GRACE_MS, lastActive;
15593
+ var init_awake = __esm({
15594
+ "cli/ui/awake.ts"() {
15595
+ "use strict";
15596
+ PIDFILE = join35(homedir35(), ".synkro", "ui-awake.pid");
15597
+ GRACE_MS = 9e4;
15598
+ lastActive = 0;
15599
+ }
15600
+ });
15601
+
15602
+ // cli/ui/sidebar.ts
15603
+ function runnerFor(backend) {
15604
+ return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15605
+ }
15606
+ async function runSidebar() {
15607
+ const centerPane = process.env.SYNKRO_UI_CENTER || "";
15608
+ const outerSession = process.env.SYNKRO_UI_OUTER || "synkro-ui";
15609
+ const bootPath = process.env.SYNKRO_UI_BOOT || process.argv[1];
15610
+ const repoCwd = process.env.SYNKRO_UI_REPO || process.cwd();
15611
+ const ownPane = process.env.TMUX_PANE || "";
15612
+ const [info, harnesses] = await Promise.all([detectContainerBackend(), detectHarnesses()]);
15613
+ const state = {
15614
+ spaces: [],
15615
+ agents: [],
15616
+ section: "spaces",
15617
+ spaceIndex: 0,
15618
+ agentIndex: 0,
15619
+ grouped: false,
15620
+ filter: "space",
15621
+ viewing: "",
15622
+ awake: false,
15623
+ collapsed: false,
15624
+ backendNote: info.note,
15625
+ message: ""
15626
+ };
15627
+ const COLLAPSED_COLS = 4;
15628
+ let allAgents = [];
15629
+ const repoCache = /* @__PURE__ */ new Map();
15630
+ async function agentRepo(path) {
15631
+ if (!path) return "";
15632
+ const cached5 = repoCache.get(path);
15633
+ if (cached5 !== void 0) return cached5;
15634
+ const root = await repoOf(path);
15635
+ repoCache.set(path, root);
15636
+ return root;
15637
+ }
15638
+ function applyFilter() {
15639
+ state.agents = visibleAgents(allAgents, state.spaces[state.spaceIndex], state.filter, state.grouped);
15640
+ state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
15641
+ }
15642
+ let memory = /* @__PURE__ */ new Map();
15643
+ let lastFrame = "";
15644
+ let lastTargets = [];
15645
+ let worldChanged = false;
15646
+ let boundSpace = false;
15647
+ const host = { kind: "host" };
15648
+ const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
15649
+ async function refresh() {
15650
+ const [hostSpaces, conductorTasks] = await Promise.all([
15651
+ discoverHostSpaces(repoCwd),
15652
+ fetchConductorTasks(CONDUCTOR_URL)
15653
+ ]);
15654
+ state.spaces = hostSpaces;
15655
+ const hostAgents = await discoverAgents(host, "host", memory);
15656
+ const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", memory) : { agents: [], memory: /* @__PURE__ */ new Map() };
15657
+ memory = new Map([...hostAgents.memory, ...containerAgents.memory]);
15658
+ const live = [...hostAgents.agents, ...containerAgents.agents];
15659
+ const merged = [...live, ...offlineAgents(live, loadRecords())];
15660
+ allAgents = await Promise.all(merged.map(async (agent) => ({
15661
+ ...agent,
15662
+ repo: await agentRepo(agent.space),
15663
+ linear: mapAgentToTask(agent.space, conductorTasks)
15664
+ })));
15665
+ const shown = await run(host, ["tmux", "display-message", "-p", "-t", ownPane, "#{@synkro_agent}#{l:|}#{@synkro_cwd}"]);
15666
+ const parts = (shown.ok ? shown.stdout.trim() : "").split("|");
15667
+ state.viewing = parts[0] || "";
15668
+ if (!boundSpace && state.spaces.length > 0 && parts[1]) {
15669
+ const tabRepo = await agentRepo(parts[1]);
15670
+ const index = state.spaces.findIndex((space) => space.path === tabRepo);
15671
+ if (index >= 0) state.spaceIndex = index;
15672
+ boundSpace = true;
15673
+ }
15674
+ state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
15675
+ applyFilter();
15676
+ state.awake = syncAwake(shouldStayAwake(allAgents));
15677
+ }
15678
+ function draw() {
15679
+ const rows = Number(process.stdout.rows || 42);
15680
+ const cols = Number(process.stdout.columns || 30);
15681
+ const layout = renderLayout(state, cols, rows);
15682
+ const frame = layout.lines.join("\n");
15683
+ lastTargets = layout.targets;
15684
+ if (frame === lastFrame) return;
15685
+ lastFrame = frame;
15686
+ process.stdout.write(CSI + "H" + frame);
15687
+ }
15688
+ function forceDraw() {
15689
+ lastFrame = "";
15690
+ draw();
15691
+ }
15692
+ let stripWidth = -1;
15693
+ async function syncChipStrip() {
15694
+ const asked = await run(host, ["tmux", "display-message", "-p", "-t", outerSession, "#{window_width}"]);
15695
+ const windowWidth = Number(asked.stdout.trim()) || Number(process.stdout.columns || 0);
15696
+ if (windowWidth <= 0) return;
15697
+ const target = state.collapsed ? COLLAPSED_COLS : Number(sidebarColumns(windowWidth));
15698
+ if (ownPane && Number(process.stdout.columns || 0) !== target) {
15699
+ await run(host, ["tmux", "resize-pane", "-t", ownPane, "-x", String(target)]);
15700
+ }
15701
+ if (target === stripWidth) return;
15702
+ stripWidth = target;
15703
+ await run(host, ["tmux", "set-option", "-t", outerSession, "status-format[0]", chipStrip(target)]);
15704
+ }
15705
+ async function setCollapsed(next) {
15706
+ if (state.collapsed === next) return;
15707
+ state.collapsed = next;
15708
+ stripWidth = -1;
15709
+ await run(host, ["tmux", "set-option", "-t", outerSession, "-q", "@synkro_collapsed", next ? "1" : "0"]);
15710
+ await syncChipStrip();
15711
+ redrawAfterResize();
15712
+ }
15713
+ async function toggleCollapsed() {
15714
+ await setCollapsed(!state.collapsed);
15715
+ }
15716
+ async function syncCollapsed() {
15717
+ const opt = await run(host, ["tmux", "show-options", "-t", outerSession, "-v", "@synkro_collapsed"]);
15718
+ await setCollapsed(opt.stdout.trim() === "1");
15719
+ }
15720
+ function redrawAfterResize() {
15721
+ process.stdout.write(CSI + "2J");
15722
+ forceDraw();
15723
+ void syncChipStrip();
15724
+ }
15725
+ async function attachedClient() {
15726
+ const listed = await run(host, ["tmux", "list-clients", "-t", outerSession, "-F", "#{client_name}"]);
15727
+ return listed.stdout.split("\n").map((line) => line.trim()).filter(Boolean)[0] || "";
15728
+ }
15729
+ function selectedSpace() {
15730
+ return state.spaces[state.spaceIndex] || state.spaces[0];
15731
+ }
15732
+ async function openDialog(kind, args2 = []) {
15733
+ const client = await attachedClient();
15734
+ if (!client) {
15735
+ state.message = "no attached client";
15736
+ return;
15737
+ }
15738
+ const cwd = selectedSpace()?.path || repoCwd;
15739
+ const opened = await run(host, buildDialogPopup({
15740
+ client,
15741
+ bootPath,
15742
+ kind,
15743
+ cwd,
15744
+ args: args2,
15745
+ width: "80%",
15746
+ height: "70%"
15747
+ }));
15748
+ if (!opened.ok) state.message = "dialog failed";
15749
+ worldChanged = true;
15750
+ }
15751
+ async function mainMenu() {
15752
+ await openDialog("menu", [outerSession, ownPane]);
15753
+ }
15754
+ function closeSelectedWorkspace() {
15755
+ const space = selectedSpace();
15756
+ if (!space || space.backend !== "host") return;
15757
+ forgetSpace(space.path);
15758
+ state.message = "closed " + space.name;
15759
+ }
15760
+ async function keybindsMenu() {
15761
+ await openDialog("keybinds");
15762
+ }
15763
+ async function restoreSelected() {
15764
+ const agent = state.agents[state.agentIndex];
15765
+ if (!agent || agent.status !== "offline") return;
15766
+ state.message = "restoring " + agent.name + "\u2026";
15767
+ forceDraw();
15768
+ const restored = await run(host, ["node", bootPath, "ui", "--restore", agent.session]);
15769
+ state.message = restored.ok ? "restored " + agent.name : "restore failed";
15770
+ }
15771
+ async function attachSelected() {
15772
+ const agent = state.agents[state.agentIndex];
15773
+ if (agent) await showAgent(agent);
15774
+ }
15775
+ async function showAgent(agent) {
15776
+ if (!centerPane) return;
15777
+ if (agent.status === "offline") {
15778
+ await restoreSelected();
15779
+ return;
15780
+ }
15781
+ const current = memory.get(agent.session);
15782
+ memory.set(agent.session, { hash: current?.hash || "", wasWorking: false, seen: true });
15783
+ if (agent.space) rememberLastAgent(await repoOf(agent.space), agent.session);
15784
+ state.viewing = agent.session;
15785
+ const windows = await run(host, ["tmux", "list-windows", "-t", outerSession, "-F", "#{window_id}|#{@synkro_agent}"]);
15786
+ const holder = windows.stdout.split("\n").map((line) => line.split("|")).find((cols) => cols[1] === agent.session);
15787
+ if (holder) {
15788
+ await run(host, ["tmux", "select-window", "-t", holder[0]]);
15789
+ state.message = agent.name;
15790
+ return;
15791
+ }
15792
+ await run(runnerFor(agent.backend), buildEnableMouse(agent.session));
15793
+ const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
15794
+ await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
15795
+ const where = agent.space ? agent.space.split("/").filter(Boolean).pop() || "" : "";
15796
+ await run(host, ["tmux", "rename-window", "-t", centerPane, tabTitle(agent.harness, where)]);
15797
+ await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_agent", agent.session]);
15798
+ await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_kind", agent.harness]);
15799
+ await snapshotTabs();
15800
+ state.message = "attached " + agent.name;
15801
+ }
15802
+ async function confirmKillAgent() {
15803
+ const agent = state.agents[state.agentIndex];
15804
+ if (!agent) return;
15805
+ await openDialog("kill", [agent.backend, agent.session]);
15806
+ }
15807
+ async function consent(action) {
15808
+ const agent = state.agents[state.agentIndex];
15809
+ if (!agent || agent.status !== "blocked" || !agent.ask) return;
15810
+ if (!actionsForAsk(agent.ask).includes(action)) return;
15811
+ const runner = runnerFor(agent.backend);
15812
+ for (const argv of buildSendText(agent.session, CONSENT_PHRASES[action])) await run(runner, argv);
15813
+ state.message = action + " \u2192 " + agent.name;
15814
+ }
15815
+ function moveSelection(delta) {
15816
+ Object.assign(state, nextSelection(state, state.spaces.length, state.agents.length, delta));
15817
+ applyFilter();
15818
+ }
15819
+ async function activate(target) {
15820
+ if (target.kind === "space") {
15821
+ state.section = "spaces";
15822
+ state.spaceIndex = target.index;
15823
+ applyFilter();
15824
+ const space = state.spaces[target.index];
15825
+ const remembered = space ? lastAgentFor(space.path) : "";
15826
+ const agent = allAgents.find((row2) => row2.session === remembered && row2.status !== "offline");
15827
+ if (agent) await showAgent(agent);
15828
+ } else if (target.kind === "agent") {
15829
+ state.section = "agents";
15830
+ state.agentIndex = target.index;
15831
+ await attachSelected();
15832
+ } else if (target.kind === "new") await openDialog("new-tab");
15833
+ else if (target.kind === "menu") await mainMenu();
15834
+ else if (target.kind === "grouped") {
15835
+ state.filter = state.filter === "space" ? "all" : "space";
15836
+ applyFilter();
15837
+ } else if (target.kind === "collapse") await toggleCollapsed();
15838
+ }
15839
+ async function handleClick(x, y, rightButton) {
15840
+ const target = lastTargets[y - 1];
15841
+ if (!target) return;
15842
+ const cols = Number(process.stdout.columns || 30);
15843
+ if (target.kind === "new") {
15844
+ await (x > cols / 2 ? mainMenu() : openDialog("new-tab"));
15845
+ return;
15846
+ }
15847
+ if (target.kind === "grouped") {
15848
+ if (x > cols / 2) {
15849
+ state.filter = state.filter === "space" ? "all" : "space";
15850
+ applyFilter();
15851
+ }
15852
+ return;
15853
+ }
15854
+ if (rightButton) {
15855
+ if (target.kind === "space") {
15856
+ state.section = "spaces";
15857
+ state.spaceIndex = target.index;
15858
+ await openDialog("new-tab");
15859
+ }
15860
+ if (target.kind === "agent") {
15861
+ state.section = "agents";
15862
+ state.agentIndex = target.index;
15863
+ await confirmKillAgent();
15864
+ }
15865
+ return;
15866
+ }
15867
+ await activate(target);
15868
+ }
15869
+ async function handleKey(key) {
15870
+ if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
15871
+ else if (key === "j" || key === CSI + "B") moveSelection(1);
15872
+ else if (key === "k" || key === CSI + "A") moveSelection(-1);
15873
+ else if (key === "\r") {
15874
+ if (state.section === "agents") await attachSelected();
15875
+ else await openDialog("new-tab");
15876
+ } else if (key === "n" || key === "T") {
15877
+ worldChanged = true;
15878
+ await openDialog("new-tab");
15879
+ } else if (key === "m") await mainMenu();
15880
+ else if (key === "O") void openDialog("new-workspace");
15881
+ else if (key === "C") {
15882
+ worldChanged = true;
15883
+ closeSelectedWorkspace();
15884
+ } else if (key === "r") {
15885
+ worldChanged = true;
15886
+ await restoreSelected();
15887
+ } else if (key === "d") {
15888
+ const client = await attachedClient();
15889
+ await run(host, client ? ["tmux", "detach-client", "-t", client] : ["tmux", "detach-client"]);
15890
+ } else if (key === "K") await keybindsMenu();
15891
+ else if (key === "G") {
15892
+ state.grouped = !state.grouped;
15893
+ applyFilter();
15894
+ } else if (key === "a") {
15895
+ state.filter = state.filter === "space" ? "all" : "space";
15896
+ applyFilter();
15897
+ } else if (key === "<" || key === ">" || key === "," || key === ".") await toggleCollapsed();
15898
+ else if (key === "x") {
15899
+ worldChanged = true;
15900
+ await confirmKillAgent();
15901
+ } else if (key === "i") {
15902
+ const agent = state.agents[state.agentIndex];
15903
+ if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
15904
+ } else if (key === "g") await consent("track");
15905
+ else if (key === "s") await consent("skip");
15906
+ else if (key === "y") await consent("stay");
15907
+ else if (key === "q" || key === KEY_CTRL_C) {
15908
+ await snapshotTabs();
15909
+ releaseAwake();
15910
+ await run(host, ["tmux", "kill-session", "-t", outerSession]);
15911
+ process.exit(0);
15912
+ }
15913
+ }
15914
+ process.stdout.on("resize", redrawAfterResize);
15915
+ process.stdin.setRawMode?.(true);
15916
+ process.stdin.resume();
15917
+ process.stdout.write(CSI + "?1000h" + CSI + "?1006h");
15918
+ process.on("exit", () => process.stdout.write(CSI + "?1000l" + CSI + "?1006l"));
15919
+ let refreshing = false;
15920
+ async function refreshSafe() {
15921
+ if (refreshing) return;
15922
+ refreshing = true;
15923
+ try {
15924
+ await refresh();
15925
+ } finally {
15926
+ refreshing = false;
15927
+ }
15928
+ }
15929
+ const MOUSE = /\[<(\d+);(\d+);(\d+)([Mm])/g;
15930
+ process.stdin.on("data", (chunk) => {
15931
+ const input = chunk.toString("utf8");
15932
+ void (async () => {
15933
+ let sawMouse = false;
15934
+ let event;
15935
+ MOUSE.lastIndex = 0;
15936
+ while ((event = MOUSE.exec(input)) !== null) {
15937
+ sawMouse = true;
15938
+ const button = Number(event[1]);
15939
+ const x = Number(event[2]);
15940
+ const y = Number(event[3]);
15941
+ if (event[4] !== "M") continue;
15942
+ if (button === 0 || button === 2) await handleClick(x, y, button === 2);
15943
+ }
15944
+ if (!sawMouse) await handleKey(input);
15945
+ forceDraw();
15946
+ if (worldChanged) {
15947
+ worldChanged = false;
15948
+ void refreshSafe().then(draw);
15949
+ }
15950
+ })();
15951
+ });
15952
+ await refreshSafe();
15953
+ redrawAfterResize();
15954
+ await syncChipStrip();
15955
+ setInterval(() => {
15956
+ void syncCollapsed();
15957
+ void syncChipStrip();
15958
+ void refreshSafe().then(draw);
15959
+ }, POLL_MS);
15960
+ }
15961
+ var POLL_MS, CONDUCTOR_URL, CSI, KEY_CTRL_C;
15962
+ var init_sidebar = __esm({
15963
+ "cli/ui/sidebar.ts"() {
15964
+ "use strict";
15965
+ init_model();
15966
+ init_manifest();
15967
+ init_render();
15968
+ init_consent();
15969
+ init_backend();
15970
+ init_tmux();
15971
+ init_launch();
15972
+ init_awake();
15973
+ POLL_MS = 2e3;
15974
+ CONDUCTOR_URL = "http://127.0.0.1:" + (process.env.SYNKRO_HOST_MCP_PORT || "18931");
15975
+ CSI = "\x1B[";
15976
+ KEY_CTRL_C = "";
15977
+ }
15978
+ });
15979
+
15980
+ // cli/ui/repos.ts
15981
+ import { existsSync as existsSync36, readdirSync as readdirSync9 } from "fs";
15982
+ import { homedir as homedir36 } from "os";
15983
+ import { join as join36 } from "path";
15984
+ function isRepo(path) {
15985
+ return existsSync36(join36(path, ".git"));
15986
+ }
15987
+ function childDirectories(path) {
15988
+ try {
15989
+ return readdirSync9(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.has(entry.name) && !entry.name.startsWith(".")).map((entry) => join36(path, entry.name));
15990
+ } catch {
15991
+ return [];
15992
+ }
15993
+ }
15994
+ function discoverRepos(extra = []) {
15995
+ const seen = /* @__PURE__ */ new Set();
15996
+ const choices = [];
15997
+ const add = (path, note) => {
15998
+ const clean = path.replace(/\/+$/, "");
15999
+ if (!clean || seen.has(clean) || !isRepo(clean)) return;
16000
+ seen.add(clean);
16001
+ choices.push({ path: clean, name: clean.split("/").filter(Boolean).pop() || clean, note });
16002
+ };
16003
+ for (const path of extra) add(path, "current");
16004
+ for (const path of loadRepos()) add(path, "recent");
16005
+ const home = homedir36();
16006
+ for (const root of SCAN_ROOTS) {
16007
+ const base = root ? join36(home, root) : home;
16008
+ if (!existsSync36(base)) continue;
16009
+ if (isRepo(base)) {
16010
+ add(base, "");
16011
+ continue;
16012
+ }
16013
+ for (const child of childDirectories(base)) add(child, "");
16014
+ }
16015
+ return choices;
16016
+ }
16017
+ var SCAN_ROOTS, SKIP;
16018
+ var init_repos = __esm({
16019
+ "cli/ui/repos.ts"() {
16020
+ "use strict";
16021
+ init_manifest();
16022
+ SCAN_ROOTS = ["", "code", "src", "dev", "projects", "work", "repos", "git", "Developer", "Documents/GitHub"];
16023
+ SKIP = /* @__PURE__ */ new Set(["node_modules", "Library", "Applications", ".Trash", ".git"]);
16024
+ }
16025
+ });
16026
+
16027
+ // cli/ui/tabs.ts
16028
+ import { execSync as execSync7 } from "child_process";
16029
+ function repoRoot() {
16030
+ try {
16031
+ return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
16032
+ } catch {
16033
+ return process.cwd();
16034
+ }
16035
+ }
16036
+ async function createTab(bootPath, kind, spacePath) {
16037
+ const repo = repoRoot();
16038
+ const space = spacePath.split("/").filter(Boolean).pop() || "space";
16039
+ if (kind === "terminal") {
16040
+ await buildTab(bootPath, repo, {
16041
+ cwd: spacePath,
16042
+ center: makeTerminalCommand(bootPath),
16043
+ title: tabTitle("terminal", space),
16044
+ kind: "terminal",
16045
+ focus: "center"
16046
+ });
16047
+ return;
16048
+ }
16049
+ if (kind === "cursor-synkro") {
16050
+ await buildTab(bootPath, repo, {
16051
+ cwd: spacePath,
16052
+ center: ["node", bootPath, "ui", "--run", "cursor", spacePath].map(shellQuote3).join(" "),
16053
+ title: tabTitle("cursor", space),
16054
+ kind: "cursor-synkro",
16055
+ focus: "center"
16056
+ });
16057
+ return;
16058
+ }
16059
+ if (kind === "settings") {
16060
+ await buildTab(bootPath, repo, {
16061
+ cwd: spacePath,
16062
+ center: "sh -c " + shellQuote3('exec "${EDITOR:-vi}" synkro.toml'),
16063
+ title: tabTitle("settings", "settings"),
16064
+ kind: "settings",
16065
+ focus: "center"
16066
+ });
16067
+ return;
16068
+ }
16069
+ const info = await detectContainerBackend();
16070
+ const spaceName = space;
16071
+ const stamp = String(process.pid % 1e4);
16072
+ const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
16073
+ const reachable = info.backend === "container" && (await run(info.runner, ["test", "-d", spacePath])).ok;
16074
+ const backend = reachable ? "container" : "host";
16075
+ const spawned = await spawnAgent(info, {
16076
+ name: spaceName + "-" + harness + "-" + stamp,
16077
+ harness,
16078
+ spaceName,
16079
+ cwd: spacePath,
16080
+ backend
16081
+ });
16082
+ if (!spawned.ok) return;
16083
+ await buildTab(bootPath, repo, {
16084
+ cwd: spacePath,
16085
+ center: buildCenterAttachCommand(backend === "container" ? info.runner : HOST2, spawned.session),
16086
+ title: tabTitle(harness, spaceName),
16087
+ kind: harness,
16088
+ agentSession: spawned.session,
16089
+ focus: "center"
16090
+ });
16091
+ }
16092
+ async function openAgentTab(bootPath, session) {
16093
+ if (!session.startsWith("synkro-agent-")) return;
16094
+ const info = await detectContainerBackend();
16095
+ const meta = await run(HOST2, [
16096
+ "tmux",
16097
+ "display-message",
16098
+ "-p",
16099
+ "-t",
16100
+ session,
16101
+ ["#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}"].join("|")
16102
+ ]);
16103
+ const [harness, space, backend] = (meta.stdout.trim() || "||").split("|");
16104
+ const containerHosted = backend === "container";
16105
+ const runner = containerHosted ? info.runner : HOST2;
16106
+ const alive = (await run(runner, ["tmux", "has-session", "-t", session])).ok;
16107
+ if (!alive) return;
16108
+ const spaceName = (space || "").split("/").filter(Boolean).pop() || "space";
16109
+ await buildTab(bootPath, repoRoot(), {
16110
+ cwd: space || repoRoot(),
16111
+ center: buildCenterAttachCommand(runner, session),
16112
+ title: tabTitle(harness || "claude", spaceName),
16113
+ kind: harness || "claude",
16114
+ agentSession: session,
16115
+ focus: "center"
16116
+ });
16117
+ }
16118
+ async function restoreTabs(bootPath, repoCwd) {
16119
+ const tabs = loadTabs();
16120
+ if (tabs.length === 0) return false;
16121
+ const info = await detectContainerBackend();
16122
+ const hostSessions = (await run(HOST2, ["tmux", "list-sessions", "-F", "#{session_name}"])).stdout;
16123
+ const containerSessions = info.backend === "container" ? (await run(info.runner, ["tmux", "list-sessions", "-F", "#{session_name}"])).stdout : "";
16124
+ const alive = new Set((hostSessions + "\n" + containerSessions).split("\n").map((line) => line.trim()).filter(Boolean));
16125
+ let built = false;
16126
+ for (const tab of tabs) {
16127
+ const space = tab.cwd.split("/").filter(Boolean).pop() || "space";
16128
+ const containerHosted = info.backend === "container" && (await run(info.runner, ["test", "-d", tab.cwd])).ok;
16129
+ const harness = ["claude", "codex", "cursor"].includes(tab.kind) ? tab.kind : "";
16130
+ if (tab.agentSession && alive.has(tab.agentSession)) {
16131
+ await run(containerHosted ? info.runner : HOST2, buildEnableMouse(tab.agentSession));
16132
+ await buildTab(bootPath, repoCwd, {
16133
+ cwd: tab.cwd,
16134
+ center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, tab.agentSession),
16135
+ title: tab.title,
16136
+ kind: tab.kind,
16137
+ agentSession: tab.agentSession,
16138
+ focus: "center"
16139
+ });
16140
+ } else if (harness) {
16141
+ const spawned = await spawnAgent(info, {
16142
+ name: space + "-" + harness + "-" + String(process.pid % 1e4),
16143
+ harness,
16144
+ spaceName: space,
16145
+ cwd: tab.cwd,
16146
+ backend: containerHosted ? "container" : "host",
16147
+ resume: true
16148
+ });
16149
+ await buildTab(bootPath, repoCwd, spawned.ok ? {
16150
+ cwd: tab.cwd,
16151
+ center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, spawned.session),
16152
+ title: tabTitle(harness, space),
16153
+ kind: harness,
16154
+ agentSession: spawned.session,
16155
+ focus: "center"
16156
+ } : { cwd: tab.cwd, center: makeTerminalCommand(bootPath), title: tabTitle("terminal", space), kind: "terminal", focus: "center" });
16157
+ } else {
16158
+ await buildTab(bootPath, repoCwd, {
16159
+ cwd: tab.cwd,
16160
+ center: makeTerminalCommand(bootPath),
16161
+ title: tabTitle("terminal", space),
16162
+ kind: "terminal",
16163
+ focus: "center"
16164
+ });
16165
+ }
16166
+ built = true;
16167
+ }
16168
+ return built;
16169
+ }
16170
+ var HOST2;
16171
+ var init_tabs = __esm({
16172
+ "cli/ui/tabs.ts"() {
16173
+ "use strict";
16174
+ init_launch();
16175
+ init_backend();
16176
+ init_manifest();
16177
+ init_tmux();
16178
+ HOST2 = { kind: "host" };
16179
+ }
16180
+ });
16181
+
16182
+ // cli/ui/dialog.ts
16183
+ import { existsSync as existsSync37 } from "fs";
16184
+ import { homedir as homedir37 } from "os";
16185
+ function write(text) {
16186
+ process.stdout.write(text);
16187
+ }
16188
+ function visibleLength2(text) {
16189
+ let visible = 0;
16190
+ let index = 0;
16191
+ while (index < text.length) {
16192
+ if (text.startsWith(CSI2, index)) {
16193
+ const end = text.indexOf("m", index);
16194
+ if (end === -1) break;
16195
+ index = end + 1;
16196
+ } else {
16197
+ visible += 1;
16198
+ index += 1;
16199
+ }
16200
+ }
16201
+ return visible;
16202
+ }
16203
+ function clipPath(text, max) {
16204
+ const value = String(text || "");
16205
+ if (max <= 1) return value.slice(0, Math.max(0, max));
16206
+ return value.length <= max ? value : "\u2026" + value.slice(value.length - max + 1);
16207
+ }
16208
+ function clip2(text, max) {
16209
+ const value = String(text || "");
16210
+ if (max <= 1) return value.slice(0, Math.max(0, max));
16211
+ return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
16212
+ }
16213
+ function padRow(text, width) {
16214
+ const gap = Math.max(0, width - visibleLength2(text));
16215
+ return text + " ".repeat(gap);
16216
+ }
16217
+ function selectable(text, width, selected) {
16218
+ const body = padRow(text, width);
16219
+ if (!selected) return body;
16220
+ return S.select + body.split(S.reset).join(S.reset + S.select) + S.reset;
16221
+ }
16222
+ function isPrintable(input) {
16223
+ if (!input) return false;
16224
+ for (const char of input) {
16225
+ const code = char.codePointAt(0) ?? 0;
16226
+ if (code < 32 || code === 127) return false;
16227
+ }
16228
+ return true;
16229
+ }
16230
+ function matches(choice, filter) {
16231
+ if (!filter) return true;
16232
+ const needle = filter.toLowerCase();
16233
+ return (choice.label + " " + (choice.detail || "")).toLowerCase().includes(needle);
16234
+ }
16235
+ async function pick(opts) {
16236
+ let selected = Math.min(Math.max(0, opts.initial || 0), Math.max(0, opts.choices.length - 1));
16237
+ let filter = "";
16238
+ let top = 0;
16239
+ const width = Math.max(20, Number(process.stdout.columns || 80));
16240
+ const height = Math.max(8, Number(process.stdout.rows || 20));
16241
+ const perItem = opts.choices.some((choice) => choice.detail) ? 2 : 1;
16242
+ const visibleItems = Math.max(1, Math.floor(Math.max(2, height - 5) / perItem));
16243
+ const firstRow = 4;
16244
+ return new Promise((resolve7) => {
16245
+ const view = () => opts.choices.filter((choice) => matches(choice, filter));
16246
+ const draw = () => {
16247
+ const shown = view();
16248
+ if (selected >= shown.length) selected = Math.max(0, shown.length - 1);
16249
+ if (selected < top) top = selected;
16250
+ if (selected >= top + visibleItems) top = selected - visibleItems + 1;
16251
+ const lines = [];
16252
+ lines.push(" " + S.title + opts.title + S.reset);
16253
+ const left = filter && !opts.menu ? " " + S.accent + "/ " + S.reset + filter + "\u258C" : " " + S.dim + opts.hint + S.reset;
16254
+ const right = opts.menu ? "" : S.dim + String(shown.length) + (shown.length === 1 ? " match" : " matches") + S.reset;
16255
+ lines.push(padRow(left, Math.max(0, width - visibleLength2(right) - 1)) + right);
16256
+ lines.push("");
16257
+ if (shown.length === 0) lines.push(" " + S.dim + (opts.emptyNote || "nothing matches") + S.reset);
16258
+ shown.slice(top, top + visibleItems).forEach((choice, offset) => {
16259
+ const index = top + offset;
16260
+ const isSelected = index === selected;
16261
+ const marker = isSelected ? S.accent + "\u203A" + S.reset + " " : " ";
16262
+ const note = choice.note ? S.dim + choice.note + S.reset : "";
16263
+ const head = " " + marker + S.bold + clip2(choice.label, width - 6 - visibleLength2(note)) + S.reset;
16264
+ lines.push(selectable(padRow(head, Math.max(0, width - visibleLength2(note) - 1)) + note, width, isSelected));
16265
+ if (perItem === 2) {
16266
+ lines.push(selectable(" " + S.muted + clipPath(choice.detail || "", width - 6) + S.reset, width, isSelected));
16267
+ }
16268
+ });
16269
+ while (lines.length < height - 1) lines.push("");
16270
+ lines.push(" " + S.dim + opts.footer + S.reset);
16271
+ write(CSI2 + "H" + lines.slice(0, height).map((line) => padRow(line, width)).join("\n"));
16272
+ };
16273
+ const rowOfItem = (index) => firstRow + (index - top) * perItem;
16274
+ const MOUSE = /\[<(\d+);(\d+);(\d+)([Mm])/g;
16275
+ const onData = (chunk) => {
16276
+ const input = chunk.toString("utf8");
16277
+ const shown = view();
16278
+ let sawMouse = false;
16279
+ let event;
16280
+ MOUSE.lastIndex = 0;
16281
+ while ((event = MOUSE.exec(input)) !== null) {
16282
+ sawMouse = true;
16283
+ if (event[4] !== "M") continue;
16284
+ const button = Number(event[1]);
16285
+ const y = Number(event[3]);
16286
+ if (button === 64) selected = Math.max(0, selected - 1);
16287
+ else if (button === 65) selected = Math.min(shown.length - 1, selected + 1);
16288
+ else if (button === 0) {
16289
+ const hit = shown.findIndex((_, index) => y >= rowOfItem(index) && y < rowOfItem(index) + perItem);
16290
+ if (hit >= 0) {
16291
+ process.stdin.off("data", onData);
16292
+ resolve7(shown[hit].value);
16293
+ return;
16294
+ }
16295
+ }
16296
+ }
16297
+ if (!sawMouse) {
16298
+ if (input === KEY_ESC && !input.includes("[<") || input === KEY_CTRL_C2) {
16299
+ process.stdin.off("data", onData);
16300
+ resolve7(null);
16301
+ return;
16302
+ }
16303
+ if (input === "\r") {
16304
+ if (shown.length === 0) return;
16305
+ process.stdin.off("data", onData);
16306
+ resolve7(shown[selected].value);
16307
+ return;
16308
+ }
16309
+ const down = input === CSI2 + "B" || opts.menu && (input === "j" || input === CSI2 + "C");
16310
+ const up = input === CSI2 + "A" || opts.menu && (input === "k" || input === CSI2 + "D");
16311
+ if (down) selected = Math.min(shown.length - 1, selected + 1);
16312
+ else if (up) selected = Math.max(0, selected - 1);
16313
+ else if (!opts.menu && (input === KEY_BACKSPACE || input === "\b")) filter = filter.slice(0, -1);
16314
+ else if (!opts.menu && isPrintable(input)) {
16315
+ filter += input;
16316
+ selected = 0;
16317
+ top = 0;
16318
+ }
16319
+ }
16320
+ draw();
16321
+ };
16322
+ process.stdin.on("data", onData);
16323
+ draw();
16324
+ });
16325
+ }
16326
+ async function readLine(opts) {
16327
+ let value = "";
16328
+ let error = "";
16329
+ const width = Math.max(20, Number(process.stdout.columns || 80));
16330
+ return new Promise((resolve7) => {
16331
+ const draw = () => {
16332
+ const lines = [
16333
+ " " + S.title + opts.title + S.reset,
16334
+ "",
16335
+ " " + S.dim + opts.label + S.reset,
16336
+ "",
16337
+ " " + S.accent + "> " + S.reset + value + "\u258C",
16338
+ "",
16339
+ error ? " " + S.error + error + S.reset : "",
16340
+ "",
16341
+ " " + S.dim + opts.footer + S.reset
16342
+ ];
16343
+ write(CSI2 + "2J" + CSI2 + "H" + lines.map((line) => padRow(line, width)).join("\n"));
16344
+ };
16345
+ const onData = (chunk) => {
16346
+ const input = chunk.toString("utf8");
16347
+ if (input === KEY_ESC || input === KEY_CTRL_C2) {
16348
+ process.stdin.off("data", onData);
16349
+ resolve7(null);
16350
+ return;
16351
+ }
16352
+ if (input === "\r") {
16353
+ void (async () => {
16354
+ const problem = await opts.validate(value.trim());
16355
+ if (problem) {
16356
+ error = problem.slice(0, width - 4);
16357
+ draw();
16358
+ return;
16359
+ }
16360
+ process.stdin.off("data", onData);
16361
+ resolve7(value.trim());
16362
+ })();
16363
+ return;
16364
+ }
16365
+ if (input === KEY_BACKSPACE || input === "\b") value = value.slice(0, -1);
16366
+ else if (isPrintable(input)) value += input;
16367
+ draw();
16368
+ };
16369
+ process.stdin.on("data", onData);
16370
+ draw();
16371
+ });
16372
+ }
16373
+ async function pickRepo(repoCwd) {
16374
+ const current = canonicalSpacePath(repoCwd);
16375
+ const choices = discoverRepos(current ? [current] : []).map((repo) => ({
16376
+ value: repo.path,
16377
+ label: repo.name,
16378
+ detail: repo.path,
16379
+ note: repo.note
16380
+ }));
16381
+ choices.push({ value: TYPE_PATH, label: "other path\u2026", detail: "type a repository path" });
16382
+ const picked = await pick({
16383
+ title: "open repository",
16384
+ hint: "type to filter repositories",
16385
+ choices,
16386
+ footer: "\u2191\u2193 select \u21B5 next esc close",
16387
+ emptyNote: 'no repositories found \u2014 choose "other path\u2026"'
16388
+ });
16389
+ if (!picked) return null;
16390
+ if (picked !== TYPE_PATH) return picked;
16391
+ let resolved = null;
16392
+ await readLine({
16393
+ title: "open repository",
16394
+ label: "repository path",
16395
+ footer: "\u21B5 open esc back",
16396
+ validate: async (value) => {
16397
+ const path = value.replace(/^~(?=\/|$)/, homedir37()).replace(/\/+$/, "");
16398
+ if (!path || !existsSync37(path + "/.git")) return "not a git repository: " + path.slice(-40);
16399
+ resolved = path;
16400
+ return null;
16401
+ }
16402
+ });
16403
+ return resolved;
16404
+ }
16405
+ async function pickWorktree(repoPath) {
16406
+ const worktrees = await listWorktrees(repoPath);
16407
+ const choices = worktrees.map((worktree) => ({
16408
+ value: worktree.path,
16409
+ label: worktree.branch || worktree.path.split("/").filter(Boolean).pop() || worktree.path,
16410
+ detail: worktree.path,
16411
+ note: worktree.note
16412
+ }));
16413
+ choices.push({ value: NEW_WORKTREE, label: "+ new worktree\u2026", detail: "branch off this repository" });
16414
+ const picked = await pick({
16415
+ title: "open worktree \u2014 " + (repoPath.split("/").filter(Boolean).pop() || repoPath),
16416
+ hint: "type to filter worktrees",
16417
+ choices,
16418
+ footer: "\u2191\u2193 select \u21B5 open esc back"
16419
+ });
16420
+ if (!picked) return null;
16421
+ if (picked !== NEW_WORKTREE) return picked;
16422
+ let created = null;
16423
+ await readLine({
16424
+ title: "new worktree",
16425
+ label: "branch name",
16426
+ footer: "\u21B5 create esc back",
16427
+ validate: async (branch) => {
16428
+ if (!SAFE_BRANCH.test(branch)) return "letters, digits, . _ - / only";
16429
+ const target = repoPath.replace(/\/+$/, "") + "-" + branch.replace(/\//g, "-");
16430
+ const fresh = await run({ kind: "host" }, ["git", "-C", repoPath, "worktree", "add", "-b", branch, target]);
16431
+ const made = fresh.ok ? fresh : await run({ kind: "host" }, ["git", "-C", repoPath, "worktree", "add", target, branch]);
16432
+ if (!made.ok) return made.stderr.trim() || "git refused";
16433
+ created = target;
16434
+ return null;
16435
+ }
16436
+ });
16437
+ return created;
16438
+ }
16439
+ async function runMenu(bootPath, repoCwd, outerSession, sidebarPane) {
16440
+ const choice = await pick({
16441
+ menu: true,
16442
+ title: "synkro",
16443
+ hint: "choose an action",
16444
+ choices: [
16445
+ { value: "workspace", label: "Open workspace\u2026" },
16446
+ { value: "settings", label: "Settings" },
16447
+ { value: "keybinds", label: "Keybinds" },
16448
+ { value: "reload", label: "Reload sidebar" },
16449
+ { value: "detach", label: "Detach (everything keeps running)" },
16450
+ { value: "quit", label: "Quit synkro ui (agents keep running)" }
16451
+ ],
16452
+ footer: "\u2191\u2193 select \u21B5 choose esc close"
16453
+ });
16454
+ if (!choice) return;
16455
+ if (choice === "workspace") {
16456
+ const repo = await pickRepo(repoCwd);
16457
+ if (!repo) return;
16458
+ rememberRepo(repo);
16459
+ const worktree = await pickWorktree(repo);
16460
+ if (worktree) rememberSpace(await repoOf(worktree));
16461
+ return;
16462
+ }
16463
+ if (choice === "settings") {
16464
+ await createTab(bootPath, "settings", repoCwd);
16465
+ return;
16466
+ }
16467
+ if (choice === "keybinds") {
16468
+ await runKeybinds();
16469
+ return;
16470
+ }
16471
+ if (choice === "reload" && sidebarPane) {
16472
+ await run({ kind: "host" }, ["tmux", "respawn-pane", "-k", "-t", sidebarPane]);
16473
+ return;
16474
+ }
16475
+ if (choice === "detach") {
16476
+ await run({ kind: "host" }, ["tmux", "detach-client", "-s", outerSession]);
16477
+ return;
16478
+ }
16479
+ if (choice === "quit") await run({ kind: "host" }, ["tmux", "kill-session", "-t", outerSession]);
16480
+ }
16481
+ async function runKeybinds() {
16482
+ await pick({
16483
+ menu: true,
16484
+ title: "keybinds",
16485
+ hint: "sidebar",
16486
+ choices: [
16487
+ { value: "", label: "\u2191 \u2193 move through spaces and agents" },
16488
+ { value: "", label: "\u21B5 click open the selected agent" },
16489
+ { value: "", label: "n new tab in this space" },
16490
+ { value: "", label: "a all spaces / this space" },
16491
+ { value: "", label: "G group by what needs you" },
16492
+ { value: "", label: "g s y consent: track / skip / stay" },
16493
+ { value: "", label: "r restore an offline session" },
16494
+ { value: "", label: "x close agent i interrupt" },
16495
+ { value: "", label: "\u2325t new tab \u2325\u2190\u2192 switch tab" },
16496
+ { value: "", label: "\u2325s focus sidebar" },
16497
+ { value: "", label: "< > collapse / expand sidebar" },
16498
+ { value: "", label: "d detach q quit" }
16499
+ ],
16500
+ footer: "esc close"
16501
+ });
16502
+ }
16503
+ async function runTabMenu(outerSession) {
16504
+ const host = { kind: "host" };
16505
+ const listed = await run(host, [
16506
+ "tmux",
16507
+ "list-windows",
16508
+ "-t",
16509
+ outerSession,
16510
+ "-F",
16511
+ ["#{window_id}", "#{window_index}", "#{window_active}", "#{window_name}", "#{@synkro_agent}"].join("|")
16512
+ ]);
16513
+ const tabs = listed.stdout.split("\n").map((line) => line.split("|")).filter((cols) => cols[0]);
16514
+ if (tabs.length === 0) return;
16515
+ const activeIndex = Math.max(0, tabs.findIndex((cols) => cols[2] === "1"));
16516
+ const chosen = await pick({
16517
+ menu: true,
16518
+ title: "tabs",
16519
+ hint: "pick a tab",
16520
+ initial: activeIndex,
16521
+ choices: tabs.map((cols) => ({
16522
+ value: cols[0],
16523
+ label: (cols[2] === "1" ? "\u203A " : " ") + (cols[3] || "tab " + cols[1]),
16524
+ detail: cols[4] ? cols[4].replace(/^synkro-agent-/, "") : "terminal"
16525
+ })),
16526
+ footer: "\u2191\u2193 select \u21B5 choose esc close"
16527
+ });
16528
+ if (!chosen) return;
16529
+ const tab = tabs.find((cols) => cols[0] === chosen);
16530
+ if (!tab) return;
16531
+ const [windowId, , , name, agentSession2] = tab;
16532
+ const action = await pick({
16533
+ menu: true,
16534
+ title: name || "tab",
16535
+ hint: "what to do with this tab",
16536
+ choices: [
16537
+ { value: "rename", label: "Rename tab\u2026" },
16538
+ { value: "close", label: "Close tab" }
16539
+ ],
16540
+ footer: "\u2191\u2193 select \u21B5 choose esc back"
16541
+ });
16542
+ if (!action) return;
16543
+ if (action === "rename") {
16544
+ let renamed = "";
16545
+ await readLine({
16546
+ title: "rename tab",
16547
+ label: "tab name",
16548
+ footer: "\u21B5 save esc back",
16549
+ validate: async (value) => {
16550
+ if (!value.trim()) return "name cannot be empty";
16551
+ renamed = value.trim();
16552
+ return null;
16553
+ }
16554
+ });
16555
+ if (renamed) {
16556
+ await run(host, ["tmux", "rename-window", "-t", windowId, renamed]);
16557
+ await snapshotTabs();
16558
+ }
16559
+ return;
16560
+ }
16561
+ const confirm = await pick({
16562
+ menu: true,
16563
+ title: "Close " + (name || "this tab") + "?",
16564
+ hint: agentSession2 ? "the agent keeps running \u2014 reopen it from the sidebar" : "a terminal tab, nothing else is affected",
16565
+ choices: [
16566
+ { value: "no", label: "Cancel" },
16567
+ { value: "yes", label: "Close tab" }
16568
+ ],
16569
+ footer: "\u2191\u2193 select \u21B5 choose esc cancel"
16570
+ });
16571
+ if (confirm !== "yes") return;
16572
+ await run(host, ["tmux", "kill-window", "-t", windowId]);
16573
+ await snapshotTabs();
16574
+ }
16575
+ async function runKillConfirm(backend, session) {
16576
+ const name = session.replace(/^synkro-agent-/, "");
16577
+ const choice = await pick({
16578
+ menu: true,
16579
+ title: "Close agent?",
16580
+ hint: name,
16581
+ choices: [
16582
+ { value: "no", label: "Cancel" },
16583
+ { value: "yes", label: "Close it (its work is not saved anywhere else)" }
16584
+ ],
16585
+ footer: "\u2191\u2193 select \u21B5 choose esc cancel"
16586
+ });
16587
+ if (choice !== "yes") return;
16588
+ await killAgent(backend === "container" ? "container" : "host", session, "");
16589
+ }
16590
+ async function runDialog(kind, repoCwd, argA = "", argB = "") {
16591
+ const bootPath = String(process.argv[1] || "");
16592
+ process.stdin.setRawMode?.(true);
16593
+ process.stdin.resume();
16594
+ write(CSI2 + "?1000h" + CSI2 + "?1006h" + CSI2 + "?25l" + CSI2 + "2J");
16595
+ process.on("exit", () => write(CSI2 + "?1000l" + CSI2 + "?1006l" + CSI2 + "?25h"));
16596
+ if (kind === "menu") {
16597
+ await runMenu(bootPath, repoCwd, argA, argB);
16598
+ process.exit(0);
16599
+ }
16600
+ if (kind === "keybinds") {
16601
+ await runKeybinds();
16602
+ process.exit(0);
16603
+ }
16604
+ if (kind === "kill") {
16605
+ await runKillConfirm(argA, argB);
16606
+ process.exit(0);
16607
+ }
16608
+ if (kind === "tab") {
16609
+ await runTabMenu(argA || "synkro-ui");
16610
+ process.exit(0);
16611
+ }
16612
+ if (kind === "new-workspace") {
16613
+ const repo = await pickRepo(repoCwd);
16614
+ if (!repo) process.exit(0);
16615
+ rememberRepo(repo);
16616
+ const worktree = await pickWorktree(repo);
16617
+ if (worktree) rememberSpace(await repoOf(worktree));
16618
+ process.exit(0);
16619
+ }
16620
+ const harnesses = await detectHarnesses();
16621
+ const sessions = [
16622
+ { value: "terminal", label: TAB_GLYPHS.terminal + " Terminal" },
16623
+ ...harnesses.map((harness) => ({
16624
+ value: harness,
16625
+ label: (TAB_GLYPHS[harness] || "") + " " + (HARNESS_LABELS[harness] || harness)
16626
+ }))
16627
+ ];
16628
+ if (harnesses.includes("cursor")) {
16629
+ sessions.push({ value: "cursor-synkro", label: TAB_GLYPHS.cursor + " Cursor in Synkro UX" });
16630
+ }
16631
+ for (; ; ) {
16632
+ const session = await pick({
16633
+ menu: true,
16634
+ title: "new tab",
16635
+ hint: "choose what to run",
16636
+ choices: sessions,
16637
+ footer: "\u2191\u2193 select \u21B5 next esc close"
16638
+ });
16639
+ if (!session) process.exit(0);
16640
+ const repo = await pickRepo(repoCwd);
16641
+ if (!repo) continue;
16642
+ rememberRepo(repo);
16643
+ const worktree = await pickWorktree(repo);
16644
+ if (!worktree) continue;
16645
+ const root = await repoOf(worktree);
16646
+ rememberRepo(root);
16647
+ rememberSpace(root);
16648
+ write(CSI2 + "2J" + CSI2 + "H " + S.dim + "starting " + session + "\u2026" + S.reset);
16649
+ await createTab(bootPath, session, worktree);
16650
+ process.exit(0);
16651
+ }
16652
+ }
16653
+ var CSI2, KEY_ESC, KEY_CTRL_C2, KEY_BACKSPACE, S, HARNESS_LABELS, TYPE_PATH, NEW_WORKTREE, SAFE_BRANCH;
16654
+ var init_dialog = __esm({
16655
+ "cli/ui/dialog.ts"() {
14969
16656
  "use strict";
16657
+ init_backend();
16658
+ init_model();
16659
+ init_manifest();
16660
+ init_repos();
16661
+ init_tabs();
16662
+ init_launch();
14970
16663
  init_tmux();
14971
- PUEUE_GROUP = "synkro-ui";
16664
+ CSI2 = "\x1B[";
16665
+ KEY_ESC = "\x1B";
16666
+ KEY_CTRL_C2 = "";
16667
+ KEY_BACKSPACE = "\x7F";
16668
+ S = {
16669
+ reset: CSI2 + "0m",
16670
+ dim: CSI2 + "2m",
16671
+ bold: CSI2 + "1m",
16672
+ title: CSI2 + "1m" + CSI2 + "38;5;111m",
16673
+ select: CSI2 + "48;5;238m",
16674
+ accent: CSI2 + "38;5;111m",
16675
+ muted: CSI2 + "38;5;245m",
16676
+ error: CSI2 + "38;5;203m"
16677
+ };
16678
+ HARNESS_LABELS = {
16679
+ claude: "Claude Code",
16680
+ codex: "Codex",
16681
+ cursor: "Cursor"
16682
+ };
16683
+ TYPE_PATH = "::type-path";
16684
+ NEW_WORKTREE = "::new-worktree";
16685
+ SAFE_BRANCH = /^[A-Za-z0-9._/-]{1,80}$/;
14972
16686
  }
14973
16687
  });
14974
16688
 
14975
- // cli/ui/backend.ts
14976
- async function detectContainerBackend() {
14977
- const runner = { kind: "container", container: CONTAINER_NAME2 };
14978
- const probe = await run({ kind: "host" }, [
14979
- "docker",
14980
- "exec",
14981
- CONTAINER_NAME2,
14982
- "sh",
14983
- "-c",
14984
- "command -v tmux >/dev/null && command -v claude >/dev/null && ls " + shellQuote3(AUTH_SEED) + " >/dev/null 2>&1 && echo ready"
14985
- ]);
14986
- if (probe.ok && probe.stdout.includes("ready")) {
14987
- return { runner, backend: "container", note: "runtime: container (" + CONTAINER_NAME2 + ")" };
16689
+ // cli/harness/events.ts
16690
+ function blockReason(raw) {
16691
+ const text = String(raw || "").trim();
16692
+ if (!text) return "blocked by policy";
16693
+ const afterTag = text.match(/\[synkro:[^\]]*\]\s*(.+)/is);
16694
+ if (afterTag) return afterTag[1].trim();
16695
+ const afterHook = text.match(/blocked by a hook:\s*(.+)/is);
16696
+ if (afterHook) return afterHook[1].trim();
16697
+ return text;
16698
+ }
16699
+ var BLOCK_MARKER;
16700
+ var init_events = __esm({
16701
+ "cli/harness/events.ts"() {
16702
+ "use strict";
16703
+ BLOCK_MARKER = /blocked by a hook|\[synkro:/i;
14988
16704
  }
14989
- return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
14990
- }
14991
- async function provisionContainerWorkspace(runner, slug) {
14992
- const dir = CONTAINER_WORK + "/ui-" + slug;
14993
- await run(runner, [
14994
- "sh",
14995
- "-c",
14996
- "mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
14997
- ]);
14998
- return dir;
14999
- }
15000
- async function spawnAgent(info, request) {
15001
- const slug = slugify(request.name);
15002
- const session = agentSession(slug);
15003
- const runner = request.backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15004
- let cwd = request.cwd;
15005
- if (request.backend === "container") {
15006
- cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
16705
+ });
16706
+
16707
+ // cli/harness/cursor.ts
16708
+ function textOf(message) {
16709
+ const content = message?.content;
16710
+ if (typeof content === "string") return content.trim();
16711
+ if (!Array.isArray(content)) return "";
16712
+ return content.filter((part) => part && (part.type === "text" || typeof part.text === "string")).map((part) => String(part.text || "")).join("").trim();
16713
+ }
16714
+ function toolPayload(toolCall) {
16715
+ if (!toolCall) return { kind: "other", body: {} };
16716
+ for (const [key, kind] of Object.entries(TOOL_KINDS)) {
16717
+ if (toolCall[key]) return { kind, body: toolCall[key] };
16718
+ }
16719
+ const fallback = Object.keys(toolCall).find((key) => key.endsWith("ToolCall"));
16720
+ return fallback ? { kind: "other", body: toolCall[fallback] } : { kind: "other", body: {} };
16721
+ }
16722
+ function targetOf(kind, body) {
16723
+ const args2 = body?.args || {};
16724
+ const candidate = args2.command ?? args2.path ?? args2.filePath ?? args2.file_path ?? args2.pattern ?? args2.query ?? args2.target ?? "";
16725
+ const text = typeof candidate === "string" ? candidate : JSON.stringify(candidate ?? "");
16726
+ return text || body?.description || kind;
16727
+ }
16728
+ function parseCursorLine(line) {
16729
+ const trimmed = String(line || "").trim();
16730
+ if (!trimmed) return [];
16731
+ let frame;
16732
+ try {
16733
+ frame = JSON.parse(trimmed);
16734
+ } catch {
16735
+ return [];
15007
16736
  }
15008
- const command = request.harness === "codex" ? "codex" : "claude";
15009
- for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: request.spaceName, backend: request.backend })) {
15010
- const result = await run(runner, argv);
15011
- if (!result.ok && argv[1] === "new-session") {
15012
- return { ok: false, session, error: result.stderr.trim() || "tmux new-session failed" };
16737
+ const type = frame.type;
16738
+ const subtype = frame.subtype;
16739
+ if (type === "system" && subtype === "init") {
16740
+ return [{
16741
+ type: "session-start",
16742
+ sessionId: String(frame.session_id || ""),
16743
+ model: String(frame.model || ""),
16744
+ cwd: String(frame.cwd || ""),
16745
+ authSource: String(frame.apiKeySource || "")
16746
+ }];
16747
+ }
16748
+ if (type === "user") {
16749
+ const text = textOf(frame.message);
16750
+ return text ? [{ type: "user-message", text }] : [];
16751
+ }
16752
+ if (type === "assistant") {
16753
+ const text = textOf(frame.message);
16754
+ return text ? [{ type: "assistant-message", text }] : [];
16755
+ }
16756
+ if (type === "thinking" && subtype === "delta" && frame.text) {
16757
+ return [{ type: "thinking", text: String(frame.text) }];
16758
+ }
16759
+ if (type === "tool_call") {
16760
+ const { kind, body } = toolPayload(frame.tool_call);
16761
+ const id = String(frame.call_id || frame.tool_call?.toolCallId || "");
16762
+ const target = targetOf(kind, body);
16763
+ const description = String(body?.description || frame.tool_call?.description || "");
16764
+ if (subtype === "started") {
16765
+ return [{ type: "tool-start", id, kind, target, description }];
16766
+ }
16767
+ if (subtype === "completed") {
16768
+ const result = body?.result || {};
16769
+ const success = result.success;
16770
+ const rejected = result.rejected;
16771
+ const rawReason = String(rejected?.reason || "");
16772
+ const blocked = Boolean(rejected) && BLOCK_MARKER.test(rawReason);
16773
+ return [{
16774
+ type: "tool-end",
16775
+ id,
16776
+ kind,
16777
+ target: target || String(rejected?.command || ""),
16778
+ ok: Boolean(success) && Number(success?.exitCode ?? 0) === 0,
16779
+ blocked,
16780
+ reason: rejected ? blocked ? blockReason(rawReason) : rawReason || "rejected" : "",
16781
+ exitCode: success ? Number(success.exitCode ?? 0) : null,
16782
+ output: String(success?.stdout || success?.stderr || "")
16783
+ }];
15013
16784
  }
16785
+ return [];
15014
16786
  }
15015
- const pueueId = await registerAgent(runner, session);
15016
- if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
15017
- return { ok: true, session };
16787
+ if (type === "result") {
16788
+ return [{
16789
+ type: "turn-end",
16790
+ ok: !frame.is_error,
16791
+ text: String(frame.result || "")
16792
+ }];
16793
+ }
16794
+ return [];
15018
16795
  }
15019
- async function killAgent(backend, session, pueueId) {
15020
- const runner = backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15021
- await run(runner, buildKillSession(session));
15022
- await releaseAgent(runner, pueueId);
16796
+ function feed(buffer, chunk) {
16797
+ const combined = buffer + chunk;
16798
+ const parts = combined.split("\n");
16799
+ const rest = parts.pop() ?? "";
16800
+ return { lines: parts, rest };
15023
16801
  }
15024
- var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED;
15025
- var init_backend = __esm({
15026
- "cli/ui/backend.ts"() {
16802
+ function cursorArgs(prompt) {
16803
+ return [
16804
+ "-p",
16805
+ prompt,
16806
+ "--output-format",
16807
+ "stream-json",
16808
+ // --force auto-runs tools but does NOT bypass hooks (verified live), so
16809
+ // Synkro's guards still gate every call; --trust loads workspace hooks.
16810
+ "--force",
16811
+ "--trust"
16812
+ ];
16813
+ }
16814
+ var TOOL_KINDS;
16815
+ var init_cursor = __esm({
16816
+ "cli/harness/cursor.ts"() {
15027
16817
  "use strict";
15028
- init_tmux();
15029
- init_pueue2();
15030
- CONTAINER_NAME2 = "synkro-server";
15031
- CONTAINER_WORK = "/home/synkro/work";
15032
- AUTH_SEED = CONTAINER_WORK + "/claude-1/.claude.json";
16818
+ init_events();
16819
+ TOOL_KINDS = {
16820
+ shellToolCall: "shell",
16821
+ readToolCall: "read",
16822
+ editToolCall: "edit",
16823
+ writeToolCall: "write",
16824
+ deleteToolCall: "delete",
16825
+ grepToolCall: "search",
16826
+ globToolCall: "search",
16827
+ lsToolCall: "list",
16828
+ todoToolCall: "todo",
16829
+ updateTodosToolCall: "todo"
16830
+ };
15033
16831
  }
15034
16832
  });
15035
16833
 
15036
- // cli/ui/sidebar.ts
15037
- function runnerFor(backend) {
15038
- return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
15039
- }
15040
- async function runSidebar() {
15041
- const centerPane = process.env.SYNKRO_UI_CENTER || "";
15042
- const outerSession = process.env.SYNKRO_UI_OUTER || "synkro-ui";
15043
- const bootPath = process.env.SYNKRO_UI_BOOT || process.argv[1];
15044
- const repoCwd = process.env.SYNKRO_UI_REPO || process.cwd();
15045
- const info = await detectContainerBackend();
15046
- const state = {
15047
- spaces: [],
15048
- agents: [],
15049
- section: "agents",
15050
- spaceIndex: 0,
15051
- agentIndex: 0,
15052
- backendNote: info.note,
15053
- message: ""
15054
- };
15055
- let hashes = /* @__PURE__ */ new Map();
15056
- let lastFrame = "";
15057
- let spawnCounter = 1;
15058
- const host = { kind: "host" };
15059
- const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
15060
- async function refresh() {
15061
- const [hostSpaces, containerSpaces, conductorTasks] = await Promise.all([
15062
- discoverHostSpaces(repoCwd),
15063
- info.backend === "container" ? discoverContainerSpaces(containerRunner) : Promise.resolve([]),
15064
- fetchConductorTasks(CONDUCTOR_URL)
15065
- ]);
15066
- state.spaces = [...hostSpaces, ...containerSpaces];
15067
- const hostAgents = await discoverAgents(host, "host", hashes);
15068
- const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", hashes) : { agents: [], hashes: /* @__PURE__ */ new Map() };
15069
- hashes = new Map([...hostAgents.hashes, ...containerAgents.hashes]);
15070
- state.agents = [...hostAgents.agents, ...containerAgents.agents].map((agent) => ({
15071
- ...agent,
15072
- linear: mapAgentToTask(agent.space, conductorTasks)
15073
- }));
15074
- state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
15075
- state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
15076
- }
15077
- function draw() {
15078
- const rows = Number(process.stdout.rows || 42);
15079
- const cols = Number(process.stdout.columns || 32);
15080
- const frame = renderSidebar(state, cols, rows);
15081
- if (frame === lastFrame) return;
15082
- lastFrame = frame;
15083
- process.stdout.write("\x1B[2J\x1B[H" + frame);
15084
- }
15085
- async function attachSelected() {
15086
- const agent = state.agents[state.agentIndex];
15087
- if (!agent || !centerPane) return;
15088
- const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
15089
- await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
15090
- await run(host, ["tmux", "rename-window", "-t", outerSession, agent.name]);
15091
- state.message = "attached " + agent.name;
15092
- }
15093
- async function spawnInSelectedSpace() {
15094
- const space = state.spaces[state.spaceIndex] || state.spaces[0];
15095
- if (!space) {
15096
- state.message = "no space selected";
15097
- return;
15098
- }
15099
- const name = space.name + "-" + spawnCounter++;
15100
- const result = await spawnAgent(info, {
15101
- name,
15102
- harness: "claude",
15103
- spaceName: space.name,
15104
- cwd: space.path,
15105
- backend: space.backend === "container" && info.backend === "container" ? "container" : "host"
15106
- });
15107
- state.message = result.ok ? "spawned " + name : "spawn failed: " + (result.error || "").slice(0, 24);
16834
+ // cli/harness/render.ts
16835
+ function clip3(text, max) {
16836
+ const value = String(text || "").replace(/\s+/g, " ").trim();
16837
+ return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
16838
+ }
16839
+ function renderEvent(event, width = 100) {
16840
+ const body = Math.max(30, width - 4);
16841
+ switch (event.type) {
16842
+ case "session-start":
16843
+ return [
16844
+ "",
16845
+ S2.dim + " " + event.model + " \xB7 " + clip3(event.cwd, body - 30) + (event.authSource === "login" ? " \xB7 subscription" : "") + S2.reset,
16846
+ ""
16847
+ ];
16848
+ case "user-message":
16849
+ return ["", S2.user + " \u276F " + S2.reset + S2.bold + clip3(event.text, body) + S2.reset, ""];
16850
+ case "thinking":
16851
+ return [S2.think + " \xB7 thinking\u2026" + S2.reset];
16852
+ case "assistant-message":
16853
+ return ["", ...wrap(event.text, body).map((line) => S2.agent + " " + line + S2.reset), ""];
16854
+ case "tool-start": {
16855
+ const glyph = GLYPH[event.kind] || GLYPH.other;
16856
+ return [S2.tool + " " + glyph + " " + S2.reset + S2.dim + clip3(event.target, body - 6) + S2.reset];
16857
+ }
16858
+ case "tool-end": {
16859
+ if (event.blocked) {
16860
+ return [
16861
+ S2.blocked + " \u26D4 blocked" + S2.reset + S2.dim + " " + clip3(event.target, body - 14) + S2.reset,
16862
+ ...wrap(event.reason, body - 6).map((line) => " " + S2.rule + line + S2.reset)
16863
+ ];
16864
+ }
16865
+ const mark = event.ok ? S2.ok + " \u2713" : S2.blocked + " \u2717";
16866
+ const code = event.exitCode === null || event.exitCode === 0 ? "" : " (exit " + event.exitCode + ")";
16867
+ const out = event.output.trim() ? S2.dim + " " + clip3(event.output, body - 20) + S2.reset : "";
16868
+ return [mark + S2.reset + code + out];
16869
+ }
16870
+ case "turn-end":
16871
+ return ["", S2.dim + " " + (event.ok ? "done" : "ended with an error") + S2.reset, ""];
16872
+ case "notice":
16873
+ return [S2.dim + " " + clip3(event.text, body) + S2.reset];
16874
+ default:
16875
+ return [];
15108
16876
  }
15109
- async function spawnContainerAgent() {
15110
- if (info.backend !== "container") {
15111
- state.message = "container runtime unavailable";
15112
- return;
16877
+ }
16878
+ function wrap(text, width) {
16879
+ const words = String(text || "").replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
16880
+ if (words.length === 0) return [];
16881
+ const lines = [];
16882
+ let line = "";
16883
+ for (const word of words) {
16884
+ if (!line) line = word;
16885
+ else if ((line + " " + word).length <= width) line += " " + word;
16886
+ else {
16887
+ lines.push(line);
16888
+ line = word;
15113
16889
  }
15114
- const name = "box-" + spawnCounter++;
15115
- const result = await spawnAgent(info, { name, harness: "claude", spaceName: name, cwd: "", backend: "container" });
15116
- state.message = result.ok ? "spawned \u25A3 " + name : "spawn failed: " + (result.error || "").slice(0, 24);
15117
- }
15118
- async function consent(action) {
15119
- const agent = state.agents[state.agentIndex];
15120
- if (!agent || agent.status !== "blocked" || !agent.ask) return;
15121
- if (!actionsForAsk(agent.ask).includes(action)) return;
15122
- const runner = runnerFor(agent.backend);
15123
- for (const argv of buildSendText(agent.session, CONSENT_PHRASES[action])) await run(runner, argv);
15124
- state.message = action + " \u2192 " + agent.name;
15125
16890
  }
15126
- async function newTab() {
15127
- await run(host, ["node", String(bootPath), "ui", "--new-tab", outerSession]);
16891
+ if (line) lines.push(line);
16892
+ return lines;
16893
+ }
16894
+ var ESC2, S2, GLYPH;
16895
+ var init_render2 = __esm({
16896
+ "cli/harness/render.ts"() {
16897
+ "use strict";
16898
+ ESC2 = "\x1B[";
16899
+ S2 = {
16900
+ reset: ESC2 + "0m",
16901
+ dim: ESC2 + "2m",
16902
+ bold: ESC2 + "1m",
16903
+ user: ESC2 + "38;5;111m",
16904
+ agent: ESC2 + "38;5;252m",
16905
+ think: ESC2 + "38;5;244m",
16906
+ tool: ESC2 + "38;5;180m",
16907
+ ok: ESC2 + "38;5;114m",
16908
+ blocked: ESC2 + "38;5;203m",
16909
+ rule: ESC2 + "38;5;211m"
16910
+ };
16911
+ GLYPH = {
16912
+ shell: "\u276F",
16913
+ read: "\u25C4",
16914
+ edit: "\u270E",
16915
+ write: "\u270E",
16916
+ delete: "\u2716",
16917
+ search: "\u2315",
16918
+ list: "\u2630",
16919
+ todo: "\u2611",
16920
+ other: "\u2022"
16921
+ };
15128
16922
  }
15129
- process.stdin.setRawMode?.(true);
15130
- process.stdin.resume();
15131
- process.stdin.on("data", (chunk) => {
15132
- const key = chunk.toString("utf8");
15133
- void (async () => {
15134
- const list = state.section === "spaces" ? state.spaces : state.agents;
15135
- if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
15136
- else if (key === "j" || key === "\x1B[B") {
15137
- if (state.section === "spaces") state.spaceIndex = Math.min(state.spaceIndex + 1, Math.max(0, list.length - 1));
15138
- else state.agentIndex = Math.min(state.agentIndex + 1, Math.max(0, list.length - 1));
15139
- } else if (key === "k" || key === "\x1B[A") {
15140
- if (state.section === "spaces") state.spaceIndex = Math.max(0, state.spaceIndex - 1);
15141
- else state.agentIndex = Math.max(0, state.agentIndex - 1);
15142
- } else if (key === "\r") {
15143
- if (state.section === "agents") await attachSelected();
15144
- else state.message = "space: " + (state.spaces[state.spaceIndex]?.name || "");
15145
- } else if (key === "n") await spawnInSelectedSpace();
15146
- else if (key === "c") await spawnContainerAgent();
15147
- else if (key === "x") {
15148
- const agent = state.agents[state.agentIndex];
15149
- if (agent) {
15150
- await killAgent(agent.backend, agent.session, "");
15151
- state.message = "killed " + agent.name;
16923
+ });
16924
+
16925
+ // cli/harness/run.ts
16926
+ import { spawn as spawn9 } from "child_process";
16927
+ async function runCursorTurn(opts) {
16928
+ const write2 = opts.write || ((text) => process.stdout.write(text));
16929
+ const width = opts.width || Number(process.stdout.columns || 100);
16930
+ const events = [];
16931
+ let blocked = 0;
16932
+ let lastWasThinking = false;
16933
+ return new Promise((resolve7) => {
16934
+ const child = spawn9("cursor-agent", cursorArgs(opts.prompt), {
16935
+ cwd: opts.cwd,
16936
+ stdio: ["ignore", "pipe", "pipe"]
16937
+ });
16938
+ let buffer = "";
16939
+ child.stdout.on("data", (chunk) => {
16940
+ const { lines, rest } = feed(buffer, chunk.toString("utf8"));
16941
+ buffer = rest;
16942
+ for (const line of lines) {
16943
+ for (const event of parseCursorLine(line)) {
16944
+ events.push(event);
16945
+ if (event.type === "tool-end" && event.blocked) blocked += 1;
16946
+ opts.onEvent?.(event);
16947
+ if (event.type === "thinking") {
16948
+ if (lastWasThinking) continue;
16949
+ lastWasThinking = true;
16950
+ } else {
16951
+ lastWasThinking = false;
16952
+ }
16953
+ const rendered = renderEvent(event, width);
16954
+ if (rendered.length) write2(rendered.join("\n") + "\n");
15152
16955
  }
15153
- } else if (key === "i") {
15154
- const agent = state.agents[state.agentIndex];
15155
- if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
15156
- } else if (key === "g") await consent("track");
15157
- else if (key === "s") await consent("skip");
15158
- else if (key === "y") await consent("stay");
15159
- else if (key === "T") await newTab();
15160
- else if (key === "q" || key === "") {
15161
- await run(host, ["tmux", "kill-session", "-t", outerSession]);
15162
- process.exit(0);
15163
16956
  }
15164
- await refresh();
15165
- draw();
15166
- })();
16957
+ });
16958
+ child.stderr.on("data", (chunk) => {
16959
+ const text = chunk.toString("utf8").trim();
16960
+ if (/error|fatal/i.test(text) && !/RetriableError/i.test(text)) {
16961
+ write2(renderEvent({ type: "notice", text }, width).join("\n") + "\n");
16962
+ }
16963
+ });
16964
+ child.on("close", (code) => {
16965
+ resolve7({ events, blocked, exitCode: code ?? 0 });
16966
+ });
16967
+ child.on("error", (error) => {
16968
+ write2(renderEvent({ type: "notice", text: "failed to start cursor-agent: " + String(error) }, width).join("\n") + "\n");
16969
+ resolve7({ events, blocked, exitCode: 1 });
16970
+ });
15167
16971
  });
15168
- await refresh();
15169
- draw();
15170
- setInterval(() => {
15171
- void refresh().then(draw);
15172
- }, POLL_MS);
15173
16972
  }
15174
- var POLL_MS, CONDUCTOR_URL;
15175
- var init_sidebar = __esm({
15176
- "cli/ui/sidebar.ts"() {
16973
+ var init_run = __esm({
16974
+ "cli/harness/run.ts"() {
15177
16975
  "use strict";
15178
- init_model();
15179
- init_render();
15180
- init_consent();
15181
- init_backend();
15182
- init_tmux();
15183
- POLL_MS = 2e3;
15184
- CONDUCTOR_URL = "http://127.0.0.1:" + (process.env.SYNKRO_HOST_MCP_PORT || "18931");
16976
+ init_cursor();
16977
+ init_render2();
16978
+ }
16979
+ });
16980
+
16981
+ // cli/harness/session.ts
16982
+ import { createInterface as createInterface5 } from "readline";
16983
+ async function runOnce(harness, cwd, prompt) {
16984
+ if (harness !== "cursor") {
16985
+ process.stdout.write(S2.dim + " " + harness + " sessions are not embedded yet\n" + S2.reset);
16986
+ return 1;
16987
+ }
16988
+ const result = await runCursorTurn({ prompt, cwd });
16989
+ if (result.blocked > 0) {
16990
+ process.stdout.write(
16991
+ S2.blocked + " " + result.blocked + " action" + (result.blocked === 1 ? "" : "s") + " blocked by policy" + S2.reset + "\n"
16992
+ );
16993
+ }
16994
+ return result.exitCode;
16995
+ }
16996
+ async function runGovernedSession(harness, cwd, prompt) {
16997
+ if (prompt) return runOnce(harness, cwd, prompt);
16998
+ process.stdout.write(BANNER.join("\n") + "\n");
16999
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
17000
+ const ask3 = () => new Promise((resolve7) => rl.question(S2.user + " \u276F " + S2.reset, resolve7));
17001
+ for (; ; ) {
17002
+ const line = (await ask3()).trim();
17003
+ if (!line) continue;
17004
+ if (line === "exit" || line === "quit") break;
17005
+ await runOnce(harness, cwd, line);
17006
+ }
17007
+ rl.close();
17008
+ return 0;
17009
+ }
17010
+ var BANNER;
17011
+ var init_session = __esm({
17012
+ "cli/harness/session.ts"() {
17013
+ "use strict";
17014
+ init_run();
17015
+ init_render2();
17016
+ BANNER = [
17017
+ "",
17018
+ S2.bold + " synkro" + S2.reset + S2.dim + " governed session" + S2.reset,
17019
+ S2.dim + " every tool call passes Synkro policy before it runs" + S2.reset,
17020
+ S2.dim + " ctrl-c to leave" + S2.reset,
17021
+ ""
17022
+ ];
15185
17023
  }
15186
17024
  });
15187
17025
 
@@ -15190,22 +17028,68 @@ var ui_exports = {};
15190
17028
  __export(ui_exports, {
15191
17029
  uiCommand: () => uiCommand
15192
17030
  });
15193
- import { execSync as execSync7 } from "child_process";
15194
- function repoRoot() {
15195
- try {
15196
- return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
15197
- } catch {
15198
- return process.cwd();
15199
- }
15200
- }
17031
+ import { execSync as execSync8 } from "child_process";
15201
17032
  function tmuxPresent() {
15202
17033
  try {
15203
- execSync7("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
17034
+ execSync8("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
15204
17035
  return true;
15205
17036
  } catch {
15206
17037
  return false;
15207
17038
  }
15208
17039
  }
17040
+ async function takeover(kind, cwd) {
17041
+ const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
17042
+ const info = await detectContainerBackend();
17043
+ const reachable = info.backend === "container" ? (await run(info.runner, ["test", "-d", cwd])).ok : false;
17044
+ const backend = reachable ? "container" : "host";
17045
+ const spaceName = cwd.split("/").filter(Boolean).pop() || "space";
17046
+ const spawned = await spawnAgent(info, {
17047
+ name: spaceName + "-" + harness + "-" + String(process.pid % 1e4),
17048
+ harness,
17049
+ spaceName,
17050
+ cwd,
17051
+ backend
17052
+ });
17053
+ if (!spawned.ok) {
17054
+ console.error("synkro: spawn failed: " + (spawned.error || "unknown"));
17055
+ return 1;
17056
+ }
17057
+ if (process.env.TMUX_PANE) {
17058
+ await run(HOST3, ["tmux", "rename-window", "-t", process.env.TMUX_PANE, tabTitle(harness, spaceName)]);
17059
+ }
17060
+ const runner = backend === "container" ? info.runner : HOST3;
17061
+ return runInherit(["env", "TMUX=", ...runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", spawned.session])]);
17062
+ }
17063
+ async function restoreSession(session) {
17064
+ const record = loadRecords().find((row2) => row2.session === session);
17065
+ if (!record) return;
17066
+ const info = await detectContainerBackend();
17067
+ await spawnAgent(info, {
17068
+ name: record.name,
17069
+ harness: record.harness,
17070
+ spaceName: record.spaceName,
17071
+ cwd: record.space,
17072
+ backend: record.backend,
17073
+ resume: true
17074
+ });
17075
+ }
17076
+ async function printStatus() {
17077
+ const info = await detectContainerBackend();
17078
+ const [hostAgents, containerAgents] = await Promise.all([
17079
+ discoverAgents({ kind: "host" }, "host", /* @__PURE__ */ new Map()),
17080
+ info.backend === "container" ? discoverAgents(info.runner, "container", /* @__PURE__ */ new Map()) : Promise.resolve({ agents: [], hashes: /* @__PURE__ */ new Map() })
17081
+ ]);
17082
+ const live = [...hostAgents.agents, ...containerAgents.agents];
17083
+ const rows = [...live, ...offlineAgents(live, loadRecords())];
17084
+ if (rows.length === 0) {
17085
+ console.log("no sessions. `synkro ui` to start one.");
17086
+ return;
17087
+ }
17088
+ for (const agent of rows) {
17089
+ console.log([agent.status.padEnd(8), agent.harness.padEnd(7), agent.name, agent.space || ""].join(" "));
17090
+ }
17091
+ console.log("\n`synkro ui` to attach" + (rows.some((agent) => agent.status === "offline") ? "; offline sessions restore with r" : ""));
17092
+ }
15209
17093
  async function uiCommand(args2) {
15210
17094
  const bootPath = String(process.argv[1] || "");
15211
17095
  if (args2.includes("--sidebar")) {
@@ -15214,8 +17098,53 @@ async function uiCommand(args2) {
15214
17098
  });
15215
17099
  return;
15216
17100
  }
15217
- if (args2.includes("--new-tab")) {
15218
- await buildTab(bootPath, repoRoot(), "new");
17101
+ const dialogAt = args2.indexOf("--dialog");
17102
+ if (dialogAt !== -1) {
17103
+ await runDialog(
17104
+ args2[dialogAt + 1] || "new-tab",
17105
+ args2[dialogAt + 2] || repoRoot(),
17106
+ args2[dialogAt + 3] || "",
17107
+ args2[dialogAt + 4] || ""
17108
+ );
17109
+ return;
17110
+ }
17111
+ const tabAt = args2.indexOf("--tab");
17112
+ if (tabAt !== -1) {
17113
+ await createTab(bootPath, args2[tabAt + 1] || "terminal", args2[tabAt + 2] || repoRoot());
17114
+ return;
17115
+ }
17116
+ const openAt = args2.indexOf("--open-agent");
17117
+ if (openAt !== -1) {
17118
+ await openAgentTab(bootPath, args2[openAt + 1] || "");
17119
+ return;
17120
+ }
17121
+ const runAt = args2.indexOf("--run");
17122
+ if (runAt !== -1) {
17123
+ const harness = args2[runAt + 1] || "cursor";
17124
+ const cwd = args2[runAt + 2] || repoRoot();
17125
+ const prompt = args2.slice(runAt + 3).join(" ").trim();
17126
+ process.exitCode = await runGovernedSession(harness, cwd, prompt);
17127
+ return;
17128
+ }
17129
+ const takeoverAt = args2.indexOf("--takeover");
17130
+ if (takeoverAt !== -1) {
17131
+ process.exitCode = await takeover(args2[takeoverAt + 1] || "claude", args2[takeoverAt + 2] || process.cwd());
17132
+ return;
17133
+ }
17134
+ const restoreAt = args2.indexOf("--restore");
17135
+ if (restoreAt !== -1) {
17136
+ await restoreSession(args2[restoreAt + 1] || "");
17137
+ return;
17138
+ }
17139
+ if (args2.includes("--status")) {
17140
+ await printStatus();
17141
+ return;
17142
+ }
17143
+ const killAt = args2.indexOf("--kill-agent");
17144
+ if (killAt !== -1) {
17145
+ const backend = args2[killAt + 1] === "container" ? "container" : "host";
17146
+ const session = args2[killAt + 2] || "";
17147
+ if (session.startsWith("synkro-agent-")) await killAgent(backend, session, "");
15219
17148
  return;
15220
17149
  }
15221
17150
  if (!tmuxPresent()) {
@@ -15223,14 +17152,26 @@ async function uiCommand(args2) {
15223
17152
  process.exitCode = 1;
15224
17153
  return;
15225
17154
  }
15226
- const code = await launchUi(bootPath, repoRoot());
17155
+ const repo = repoRoot();
17156
+ if (!await uiSessionExists()) await restoreTabs(bootPath, repo);
17157
+ const code = await launchUi(bootPath, repo);
15227
17158
  process.exitCode = code;
15228
17159
  }
17160
+ var HOST3;
15229
17161
  var init_ui = __esm({
15230
17162
  "cli/commands/ui.ts"() {
15231
17163
  "use strict";
15232
17164
  init_launch();
15233
17165
  init_sidebar();
17166
+ init_dialog();
17167
+ init_tabs();
17168
+ init_session();
17169
+ init_launch();
17170
+ init_backend();
17171
+ init_model();
17172
+ init_manifest();
17173
+ init_tmux();
17174
+ HOST3 = { kind: "host" };
15234
17175
  }
15235
17176
  });
15236
17177
 
@@ -15270,12 +17211,12 @@ __export(linear_exports, {
15270
17211
  formatLinks: () => formatLinks,
15271
17212
  linearCommand: () => linearCommand
15272
17213
  });
15273
- import { readFileSync as readFileSync30 } from "fs";
15274
- import { homedir as homedir32 } from "os";
15275
- import { join as join32 } from "path";
17214
+ import { readFileSync as readFileSync33 } from "fs";
17215
+ import { homedir as homedir38 } from "os";
17216
+ import { join as join37 } from "path";
15276
17217
  function mcpJwt() {
15277
17218
  try {
15278
- return readFileSync30(join32(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
17219
+ return readFileSync33(join37(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
15279
17220
  } catch {
15280
17221
  return "";
15281
17222
  }
@@ -15314,7 +17255,7 @@ var SYNKRO_DIR14, PORT2, BASE;
15314
17255
  var init_linear = __esm({
15315
17256
  "cli/commands/linear.ts"() {
15316
17257
  "use strict";
15317
- SYNKRO_DIR14 = join32(homedir32(), ".synkro");
17258
+ SYNKRO_DIR14 = join37(homedir38(), ".synkro");
15318
17259
  PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
15319
17260
  BASE = `http://127.0.0.1:${PORT2}`;
15320
17261
  }
@@ -15322,7 +17263,7 @@ var init_linear = __esm({
15322
17263
 
15323
17264
  // cli/scanning/cveReachability.ts
15324
17265
  import { parse } from "@babel/parser";
15325
- import { readFileSync as readFileSync31 } from "fs";
17266
+ import { readFileSync as readFileSync34 } from "fs";
15326
17267
  function walk(node, visit) {
15327
17268
  if (!node || typeof node.type !== "string") return;
15328
17269
  visit(node);
@@ -15464,12 +17405,12 @@ var init_cveReachability = __esm({
15464
17405
 
15465
17406
  // cli/reachability/reachabilityScan.ts
15466
17407
  import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
15467
- import { readFileSync as readFileSync32, writeFileSync as writeFileSync23, existsSync as existsSync34, readdirSync as readdirSync9 } from "fs";
15468
- import { join as join33 } from "path";
15469
- import { homedir as homedir33 } from "os";
17408
+ import { readFileSync as readFileSync35, writeFileSync as writeFileSync27, existsSync as existsSync38, readdirSync as readdirSync10 } from "fs";
17409
+ import { join as join38 } from "path";
17410
+ import { homedir as homedir39 } from "os";
15470
17411
  import { createRequire } from "module";
15471
17412
  function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
15472
- const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
17413
+ const SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
15473
17414
  const EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
15474
17415
  const files = [];
15475
17416
  const stack = [repoRoot3];
@@ -15477,21 +17418,21 @@ function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
15477
17418
  const dir = stack.pop();
15478
17419
  let ents;
15479
17420
  try {
15480
- ents = readdirSync9(dir, { withFileTypes: true });
17421
+ ents = readdirSync10(dir, { withFileTypes: true });
15481
17422
  } catch {
15482
17423
  continue;
15483
17424
  }
15484
17425
  for (const e of ents) {
15485
17426
  if (files.length >= maxFiles) break;
15486
- const full = join33(dir, e.name);
17427
+ const full = join38(dir, e.name);
15487
17428
  if (e.isDirectory()) {
15488
- if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(full);
17429
+ if (!SKIP2.has(e.name) && !e.name.startsWith(".")) stack.push(full);
15489
17430
  continue;
15490
17431
  }
15491
17432
  if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
15492
17433
  const rel = full.startsWith(repoRoot3 + "/") ? full.slice(repoRoot3.length + 1) : full;
15493
17434
  try {
15494
- const content = readFileSync32(full, "utf8");
17435
+ const content = readFileSync35(full, "utf8");
15495
17436
  if (content.length <= maxBytes) files.push({ path: rel, content });
15496
17437
  } catch {
15497
17438
  }
@@ -15510,12 +17451,12 @@ function cleanVersion(spec) {
15510
17451
  function gatherManifestVersions(repoRoot3) {
15511
17452
  const out = {};
15512
17453
  const dirs = [repoRoot3];
15513
- const pkgsDir = join33(repoRoot3, "packages");
15514
- if (existsSync34(pkgsDir)) {
17454
+ const pkgsDir = join38(repoRoot3, "packages");
17455
+ if (existsSync38(pkgsDir)) {
15515
17456
  try {
15516
- for (const d of readdirSync9(pkgsDir)) {
15517
- const pd = join33(pkgsDir, d);
15518
- if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
17457
+ for (const d of readdirSync10(pkgsDir)) {
17458
+ const pd = join38(pkgsDir, d);
17459
+ if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
15519
17460
  }
15520
17461
  } catch {
15521
17462
  }
@@ -15524,7 +17465,7 @@ function gatherManifestVersions(repoRoot3) {
15524
17465
  for (const dir of dirs) {
15525
17466
  let pkg;
15526
17467
  try {
15527
- pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
17468
+ pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
15528
17469
  } catch {
15529
17470
  continue;
15530
17471
  }
@@ -15544,28 +17485,28 @@ function findJelly(repoRoot3) {
15544
17485
  try {
15545
17486
  const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
15546
17487
  const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
15547
- const pkg = JSON.parse(readFileSync32(pkgJson, "utf8"));
17488
+ const pkg = JSON.parse(readFileSync35(pkgJson, "utf8"));
15548
17489
  const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
15549
17490
  if (bin) {
15550
- const p = join33(dir, bin);
15551
- if (existsSync34(p)) return p;
17491
+ const p = join38(dir, bin);
17492
+ if (existsSync38(p)) return p;
15552
17493
  }
15553
17494
  } catch {
15554
17495
  }
15555
17496
  for (const base of [repoRoot3, process.cwd()]) {
15556
- const b = join33(base, "node_modules", ".bin", "jelly");
15557
- if (existsSync34(b)) return b;
17497
+ const b = join38(base, "node_modules", ".bin", "jelly");
17498
+ if (existsSync38(b)) return b;
15558
17499
  }
15559
17500
  return null;
15560
17501
  }
15561
17502
  function findEntries(repoRoot3) {
15562
17503
  const dirs = [repoRoot3];
15563
- const pkgsDir = join33(repoRoot3, "packages");
15564
- if (existsSync34(pkgsDir)) {
17504
+ const pkgsDir = join38(repoRoot3, "packages");
17505
+ if (existsSync38(pkgsDir)) {
15565
17506
  try {
15566
- for (const d of readdirSync9(pkgsDir)) {
15567
- const pd = join33(pkgsDir, d);
15568
- if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
17507
+ for (const d of readdirSync10(pkgsDir)) {
17508
+ const pd = join38(pkgsDir, d);
17509
+ if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
15569
17510
  }
15570
17511
  } catch {
15571
17512
  }
@@ -15573,12 +17514,12 @@ function findEntries(repoRoot3) {
15573
17514
  const entries = [];
15574
17515
  for (const dir of dirs) {
15575
17516
  try {
15576
- const pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
17517
+ const pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
15577
17518
  const cands = [pkg.source, pkg.module, pkg.main, "src/index.ts", "src/index.js", "src/main.ts", "src/server.ts", "index.ts", "index.js"];
15578
17519
  for (const c of cands) {
15579
17520
  if (typeof c !== "string") continue;
15580
- const f = join33(dir, c);
15581
- if (existsSync34(f)) {
17521
+ const f = join38(dir, c);
17522
+ if (existsSync38(f)) {
15582
17523
  entries.push(f);
15583
17524
  break;
15584
17525
  }
@@ -15611,9 +17552,9 @@ function parseApiUsage(log) {
15611
17552
  }
15612
17553
  function runReachabilityScan(repoRoot3, opts = {}) {
15613
17554
  const commit = currentCommit(repoRoot3);
15614
- if (!opts.force && commit && existsSync34(REACHABILITY_PATH)) {
17555
+ if (!opts.force && commit && existsSync38(REACHABILITY_PATH)) {
15615
17556
  try {
15616
- const prev = JSON.parse(readFileSync32(REACHABILITY_PATH, "utf8"));
17557
+ const prev = JSON.parse(readFileSync35(REACHABILITY_PATH, "utf8"));
15617
17558
  if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
15618
17559
  } catch {
15619
17560
  }
@@ -15702,7 +17643,7 @@ function runReachabilityScan(repoRoot3, opts = {}) {
15702
17643
  if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
15703
17644
  const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
15704
17645
  try {
15705
- writeFileSync23(REACHABILITY_PATH, JSON.stringify(file, null, 2));
17646
+ writeFileSync27(REACHABILITY_PATH, JSON.stringify(file, null, 2));
15706
17647
  } catch (e) {
15707
17648
  return { ok: false, reason: "write failed: " + String(e.message || e) };
15708
17649
  }
@@ -15714,7 +17655,7 @@ var init_reachabilityScan = __esm({
15714
17655
  "use strict";
15715
17656
  init_cveReachability();
15716
17657
  require2 = createRequire(import.meta.url);
15717
- REACHABILITY_PATH = join33(homedir33(), ".synkro", "reachability.json");
17658
+ REACHABILITY_PATH = join38(homedir39(), ".synkro", "reachability.json");
15718
17659
  }
15719
17660
  });
15720
17661
 
@@ -15723,15 +17664,15 @@ var reachabilityScan_exports = {};
15723
17664
  __export(reachabilityScan_exports, {
15724
17665
  reachabilityScanCommand: () => reachabilityScanCommand
15725
17666
  });
15726
- import { readFileSync as readFileSync33, existsSync as existsSync35 } from "fs";
15727
- import { join as join34 } from "path";
15728
- import { homedir as homedir34 } from "os";
17667
+ import { readFileSync as readFileSync36, existsSync as existsSync39 } from "fs";
17668
+ import { join as join39 } from "path";
17669
+ import { homedir as homedir40 } from "os";
15729
17670
  import { execFileSync as execFileSync6 } from "child_process";
15730
17671
  function readConfigEnv4() {
15731
- const p = join34(SYNKRO_DIR15, "config.env");
15732
- if (!existsSync35(p)) return {};
17672
+ const p = join39(SYNKRO_DIR15, "config.env");
17673
+ if (!existsSync39(p)) return {};
15733
17674
  const out = {};
15734
- for (const line of readFileSync33(p, "utf-8").split("\n")) {
17675
+ for (const line of readFileSync36(p, "utf-8").split("\n")) {
15735
17676
  const t = line.trim();
15736
17677
  if (!t || t.startsWith("#")) continue;
15737
17678
  const eq = t.indexOf("=");
@@ -15763,11 +17704,11 @@ async function pushToCloud(cfg, repo) {
15763
17704
  while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
15764
17705
  let jwt2 = "";
15765
17706
  try {
15766
- jwt2 = readFileSync33(join34(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
17707
+ jwt2 = readFileSync36(join39(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
15767
17708
  } catch {
15768
17709
  }
15769
- if (!jwt2 || !existsSync35(REACHABILITY_PATH)) return;
15770
- const body = readFileSync33(REACHABILITY_PATH, "utf-8");
17710
+ if (!jwt2 || !existsSync39(REACHABILITY_PATH)) return;
17711
+ const body = readFileSync36(REACHABILITY_PATH, "utf-8");
15771
17712
  try {
15772
17713
  const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
15773
17714
  method: "POST",
@@ -15799,7 +17740,7 @@ var init_reachabilityScan2 = __esm({
15799
17740
  "cli/commands/reachabilityScan.ts"() {
15800
17741
  "use strict";
15801
17742
  init_reachabilityScan();
15802
- SYNKRO_DIR15 = join34(homedir34(), ".synkro");
17743
+ SYNKRO_DIR15 = join39(homedir40(), ".synkro");
15803
17744
  }
15804
17745
  });
15805
17746
 
@@ -15929,13 +17870,13 @@ var config_exports = {};
15929
17870
  __export(config_exports, {
15930
17871
  configCommand: () => configCommand
15931
17872
  });
15932
- import { readFileSync as readFileSync34, writeFileSync as writeFileSync24, existsSync as existsSync36 } from "fs";
15933
- import { join as join35 } from "path";
15934
- import { homedir as homedir35 } from "os";
17873
+ import { readFileSync as readFileSync37, writeFileSync as writeFileSync28, existsSync as existsSync40 } from "fs";
17874
+ import { join as join40 } from "path";
17875
+ import { homedir as homedir41 } from "os";
15935
17876
  function readConfigEnv5() {
15936
- if (!existsSync36(CONFIG_PATH9)) return {};
17877
+ if (!existsSync40(CONFIG_PATH9)) return {};
15937
17878
  const out = {};
15938
- for (const line of readFileSync34(CONFIG_PATH9, "utf-8").split("\n")) {
17879
+ for (const line of readFileSync37(CONFIG_PATH9, "utf-8").split("\n")) {
15939
17880
  const t = line.trim();
15940
17881
  if (!t || t.startsWith("#")) continue;
15941
17882
  const eq = t.indexOf("=");
@@ -15944,11 +17885,11 @@ function readConfigEnv5() {
15944
17885
  return out;
15945
17886
  }
15946
17887
  function updateConfigValue(key, value) {
15947
- if (!existsSync36(CONFIG_PATH9)) {
17888
+ if (!existsSync40(CONFIG_PATH9)) {
15948
17889
  console.error("No config found. Run `synkro install` first.");
15949
17890
  process.exit(1);
15950
17891
  }
15951
- const lines = readFileSync34(CONFIG_PATH9, "utf-8").split("\n");
17892
+ const lines = readFileSync37(CONFIG_PATH9, "utf-8").split("\n");
15952
17893
  const pattern = new RegExp(`^${key}=`);
15953
17894
  let found = false;
15954
17895
  const updated = lines.map((line) => {
@@ -15959,7 +17900,7 @@ function updateConfigValue(key, value) {
15959
17900
  return line;
15960
17901
  });
15961
17902
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
15962
- writeFileSync24(CONFIG_PATH9, updated.join("\n"), "utf-8");
17903
+ writeFileSync28(CONFIG_PATH9, updated.join("\n"), "utf-8");
15963
17904
  }
15964
17905
  function resolveInferenceMode(cfg) {
15965
17906
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -16117,8 +18058,8 @@ var init_config = __esm({
16117
18058
  "use strict";
16118
18059
  init_stub();
16119
18060
  init_optout();
16120
- SYNKRO_DIR16 = join35(homedir35(), ".synkro");
16121
- CONFIG_PATH9 = join35(SYNKRO_DIR16, "config.env");
18061
+ SYNKRO_DIR16 = join40(homedir41(), ".synkro");
18062
+ CONFIG_PATH9 = join40(SYNKRO_DIR16, "config.env");
16122
18063
  }
16123
18064
  });
16124
18065
 
@@ -16127,7 +18068,7 @@ var telemetry_exports2 = {};
16127
18068
  __export(telemetry_exports2, {
16128
18069
  telemetryCommand: () => telemetryCommand
16129
18070
  });
16130
- import { createInterface as createInterface5 } from "readline";
18071
+ import { createInterface as createInterface6 } from "readline";
16131
18072
  function parseFlag(args2, name) {
16132
18073
  const prefix = `--${name}=`;
16133
18074
  for (const a of args2) if (a.startsWith(prefix)) return a.slice(prefix.length);
@@ -16209,7 +18150,7 @@ async function runExport(args2) {
16209
18150
  function confirmYesNo(question) {
16210
18151
  if (!process.stdin.isTTY) return Promise.resolve(false);
16211
18152
  return new Promise((resolve7) => {
16212
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
18153
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
16213
18154
  rl.question(`${question} (y/N): `, (answer) => {
16214
18155
  rl.close();
16215
18156
  const t = answer.trim().toLowerCase();
@@ -16308,11 +18249,11 @@ Usage:
16308
18249
 
16309
18250
  // cli/inventory/identity.ts
16310
18251
  import { randomUUID as randomUUID5 } from "crypto";
16311
- import { existsSync as existsSync37, mkdirSync as mkdirSync20, readFileSync as readFileSync35, renameSync as renameSync9, writeFileSync as writeFileSync25 } from "fs";
16312
- import { homedir as homedir36 } from "os";
16313
- import { dirname as dirname9, join as join36 } from "path";
18252
+ import { existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync38, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "fs";
18253
+ import { homedir as homedir42 } from "os";
18254
+ import { dirname as dirname12, join as join41 } from "path";
16314
18255
  function operationalIdentityPath() {
16315
- return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join36(homedir36(), ".synkro", "installation.json");
18256
+ return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join41(homedir42(), ".synkro", "installation.json");
16316
18257
  }
16317
18258
  function validIdentity(value) {
16318
18259
  if (!value || typeof value !== "object") return false;
@@ -16320,17 +18261,17 @@ function validIdentity(value) {
16320
18261
  return typeof row2.installation_id === "string" && UUID_RE.test(row2.installation_id) && typeof row2.created_at === "string" && Number.isFinite(Date.parse(row2.created_at));
16321
18262
  }
16322
18263
  function writeIdentity(path, identity) {
16323
- mkdirSync20(dirname9(path), { recursive: true, mode: 448 });
18264
+ mkdirSync24(dirname12(path), { recursive: true, mode: 448 });
16324
18265
  const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
16325
- writeFileSync25(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18266
+ writeFileSync29(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
16326
18267
  renameSync9(temp, path);
16327
18268
  }
16328
18269
  function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
16329
18270
  const prior = cached4.get(path);
16330
18271
  if (prior) return prior;
16331
- if (existsSync37(path)) {
18272
+ if (existsSync41(path)) {
16332
18273
  try {
16333
- const parsed = JSON.parse(readFileSync35(path, "utf8"));
18274
+ const parsed = JSON.parse(readFileSync38(path, "utf8"));
16334
18275
  if (validIdentity(parsed)) {
16335
18276
  cached4.set(path, parsed);
16336
18277
  return parsed;
@@ -16355,13 +18296,13 @@ var init_identity2 = __esm({
16355
18296
  // cli/inventory/collector.ts
16356
18297
  import { createHash as createHash5 } from "crypto";
16357
18298
  import {
16358
- existsSync as existsSync38,
16359
- readFileSync as readFileSync36,
16360
- readdirSync as readdirSync10,
18299
+ existsSync as existsSync42,
18300
+ readFileSync as readFileSync39,
18301
+ readdirSync as readdirSync11,
16361
18302
  statSync as statSync5
16362
18303
  } from "fs";
16363
- import { arch, homedir as homedir37, hostname as hostname2, platform as platform5, release } from "os";
16364
- import { basename as basename3, join as join37, relative, resolve as resolve5 } from "path";
18304
+ import { arch, homedir as homedir43, hostname as hostname2, platform as platform6, release as release2 } from "os";
18305
+ import { basename as basename3, join as join42, relative, resolve as resolve5 } from "path";
16365
18306
  import { fileURLToPath } from "url";
16366
18307
  function sha256(value) {
16367
18308
  return createHash5("sha256").update(value).digest("hex");
@@ -16371,15 +18312,15 @@ function pseudonymousHostnameHash(installationId, host) {
16371
18312
  }
16372
18313
  function cliVersion() {
16373
18314
  try {
16374
- return "1.10.3";
18315
+ return "1.10.4";
16375
18316
  } catch {
16376
18317
  return "0.0.0";
16377
18318
  }
16378
18319
  }
16379
18320
  function readJson(path) {
16380
18321
  try {
16381
- if (!existsSync38(path)) return null;
16382
- const parsed = JSON.parse(readFileSync36(path, "utf8"));
18322
+ if (!existsSync42(path)) return null;
18323
+ const parsed = JSON.parse(readFileSync39(path, "utf8"));
16383
18324
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
16384
18325
  } catch {
16385
18326
  return null;
@@ -16387,8 +18328,8 @@ function readJson(path) {
16387
18328
  }
16388
18329
  function readText(path) {
16389
18330
  try {
16390
- if (!existsSync38(path)) return "";
16391
- return readFileSync36(path, "utf8");
18331
+ if (!existsSync42(path)) return "";
18332
+ return readFileSync39(path, "utf8");
16392
18333
  } catch {
16393
18334
  return "";
16394
18335
  }
@@ -16474,16 +18415,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
16474
18415
  }
16475
18416
  function claudeDesktopConfigCandidates(home, targetPlatform) {
16476
18417
  if (targetPlatform === "darwin") {
16477
- return [join37(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
18418
+ return [join42(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
16478
18419
  }
16479
18420
  if (targetPlatform === "linux") {
16480
18421
  return [
16481
- join37(home, ".config", "Claude", "claude_desktop_config.json"),
16482
- join37(home, ".config", "claude", "claude_desktop_config.json")
18422
+ join42(home, ".config", "Claude", "claude_desktop_config.json"),
18423
+ join42(home, ".config", "claude", "claude_desktop_config.json")
16483
18424
  ];
16484
18425
  }
16485
18426
  if (targetPlatform === "win32" && process.env.APPDATA) {
16486
- return [join37(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
18427
+ return [join42(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
16487
18428
  }
16488
18429
  return [];
16489
18430
  }
@@ -16491,7 +18432,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
16491
18432
  if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
16492
18433
  if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
16493
18434
  if (targetPlatform === "win32" && process.env.ProgramFiles) {
16494
- return [join37(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
18435
+ return [join42(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
16495
18436
  }
16496
18437
  return [];
16497
18438
  }
@@ -16500,7 +18441,7 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
16500
18441
  const add = (value) => {
16501
18442
  if (typeof value !== "string" || !value.trim()) return;
16502
18443
  const path = resolve5(value);
16503
- if (existsSync38(path)) roots.add(path);
18444
+ if (existsSync42(path)) roots.add(path);
16504
18445
  };
16505
18446
  add(currentDirectory);
16506
18447
  for (const path of explicit) add(path);
@@ -16511,31 +18452,31 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
16511
18452
  return [...roots];
16512
18453
  }
16513
18454
  function cursorWorkspaceStorageCandidates(home, targetPlatform) {
16514
- if (targetPlatform === "darwin") return [join37(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
16515
- if (targetPlatform === "linux") return [join37(home, ".config", "Cursor", "User", "workspaceStorage")];
18455
+ if (targetPlatform === "darwin") return [join42(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
18456
+ if (targetPlatform === "linux") return [join42(home, ".config", "Cursor", "User", "workspaceStorage")];
16516
18457
  if (targetPlatform === "win32" && process.env.APPDATA) {
16517
- return [join37(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
18458
+ return [join42(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
16518
18459
  }
16519
18460
  return [];
16520
18461
  }
16521
18462
  function cursorWorkspaceRoots(home, targetPlatform) {
16522
18463
  const roots = /* @__PURE__ */ new Set();
16523
18464
  for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
16524
- if (!existsSync38(storage)) continue;
18465
+ if (!existsSync42(storage)) continue;
16525
18466
  let entries = [];
16526
18467
  try {
16527
- entries = readdirSync10(storage, { withFileTypes: true });
18468
+ entries = readdirSync11(storage, { withFileTypes: true });
16528
18469
  } catch {
16529
18470
  continue;
16530
18471
  }
16531
18472
  for (const entry of entries) {
16532
18473
  if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
16533
- const state = readJson(join37(storage, entry.name, "workspace.json"));
18474
+ const state = readJson(join42(storage, entry.name, "workspace.json"));
16534
18475
  const raw = state?.folder;
16535
18476
  if (typeof raw !== "string" || !raw.trim()) continue;
16536
18477
  try {
16537
18478
  const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
16538
- if (existsSync38(path)) roots.add(resolve5(path));
18479
+ if (existsSync42(path)) roots.add(resolve5(path));
16539
18480
  } catch {
16540
18481
  }
16541
18482
  }
@@ -16629,18 +18570,18 @@ function parseFrontmatter(content) {
16629
18570
  return { name: value("name"), version: value("version") };
16630
18571
  }
16631
18572
  function skillArtifacts(harness, root) {
16632
- if (!existsSync38(root)) return [];
18573
+ if (!existsSync42(root)) return [];
16633
18574
  const manifests = [];
16634
18575
  const visit = (dir) => {
16635
18576
  let entries;
16636
18577
  try {
16637
- entries = readdirSync10(dir, { withFileTypes: true });
18578
+ entries = readdirSync11(dir, { withFileTypes: true });
16638
18579
  } catch {
16639
18580
  return;
16640
18581
  }
16641
18582
  for (const entry of entries) {
16642
18583
  if (entry.isSymbolicLink?.()) continue;
16643
- const path = join37(dir, entry.name);
18584
+ const path = join42(dir, entry.name);
16644
18585
  if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
16645
18586
  else if (entry.isDirectory()) visit(path);
16646
18587
  }
@@ -16650,7 +18591,7 @@ function skillArtifacts(harness, root) {
16650
18591
  const content = readText(path);
16651
18592
  const frontmatter = parseFrontmatter(content);
16652
18593
  const rel = relative(root, path).replaceAll("\\", "/");
16653
- const name = frontmatter.name || basename3(join37(path, "..")) || "skill";
18594
+ const name = frontmatter.name || basename3(join42(path, "..")) || "skill";
16654
18595
  return {
16655
18596
  harness,
16656
18597
  type: "skill",
@@ -16665,16 +18606,16 @@ function skillArtifacts(harness, root) {
16665
18606
  });
16666
18607
  }
16667
18608
  function cursorExtensionArtifacts(root) {
16668
- if (!existsSync38(root)) return [];
18609
+ if (!existsSync42(root)) return [];
16669
18610
  let dirs = [];
16670
18611
  try {
16671
- dirs = readdirSync10(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
18612
+ dirs = readdirSync11(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
16672
18613
  } catch {
16673
18614
  return [];
16674
18615
  }
16675
18616
  const artifacts = [];
16676
18617
  for (const dir of dirs) {
16677
- const pkg = readJson(join37(root, dir.name, "package.json"));
18618
+ const pkg = readJson(join42(root, dir.name, "package.json"));
16678
18619
  if (!pkg) continue;
16679
18620
  const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
16680
18621
  const name = typeof pkg.name === "string" ? pkg.name : dir.name;
@@ -16694,21 +18635,21 @@ function cursorExtensionArtifacts(root) {
16694
18635
  return artifacts;
16695
18636
  }
16696
18637
  function deploymentMode2(home) {
16697
- const raw = readText(join37(home, ".synkro", "config.env"));
18638
+ const raw = readText(join42(home, ".synkro", "config.env"));
16698
18639
  const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
16699
18640
  if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
16700
18641
  if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
16701
18642
  return "local";
16702
18643
  }
16703
18644
  function telemetryHealth(home) {
16704
- const meta = readJson(join37(home, ".synkro", "telemetry-meta.json"));
18645
+ const meta = readJson(join42(home, ".synkro", "telemetry-meta.json"));
16705
18646
  const health = {};
16706
18647
  if (meta?.last_flush_ok_at && Number.isFinite(Date.parse(meta.last_flush_ok_at))) health.telemetry_last_flush_at = meta.last_flush_ok_at;
16707
18648
  if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
16708
- const queue = join37(home, ".synkro", "telemetry-pending.jsonl");
18649
+ const queue = join42(home, ".synkro", "telemetry-pending.jsonl");
16709
18650
  try {
16710
18651
  const size = statSync5(queue).size;
16711
- health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync36(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
18652
+ health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync39(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
16712
18653
  } catch {
16713
18654
  }
16714
18655
  return health;
@@ -16749,7 +18690,7 @@ function harnessSnapshot(agent) {
16749
18690
  }
16750
18691
  const config = readJson(agent.settingsPath);
16751
18692
  const coverage = inspectCodexHooks(agent.settingsPath);
16752
- const toml = readText(join37(agent.configDir, "config.toml"));
18693
+ const toml = readText(join42(agent.configDir, "config.toml"));
16753
18694
  const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
16754
18695
  return {
16755
18696
  row: {
@@ -16766,11 +18707,11 @@ function harnessSnapshot(agent) {
16766
18707
  };
16767
18708
  }
16768
18709
  function collectOperationalInventory(options = {}) {
16769
- const home = options.homeDir ?? homedir37();
18710
+ const home = options.homeDir ?? homedir43();
16770
18711
  const detected = options.detectedAgents ?? detectAgents();
16771
18712
  const identity = getOperationalInstallationIdentity(options.identityPath);
16772
- const targetPlatform = options.platformName ?? platform5();
16773
- const codexHome = options.homeDir ? join37(home, ".codex") : process.env.CODEX_HOME || join37(home, ".codex");
18713
+ const targetPlatform = options.platformName ?? platform6();
18714
+ const codexHome = options.homeDir ? join42(home, ".codex") : process.env.CODEX_HOME || join42(home, ".codex");
16774
18715
  const harnesses = [];
16775
18716
  const artifacts = [];
16776
18717
  for (const agent of detected) {
@@ -16778,7 +18719,7 @@ function collectOperationalInventory(options = {}) {
16778
18719
  harnesses.push(row2);
16779
18720
  artifacts.push(...hookArtifacts(row2.harness, config));
16780
18721
  }
16781
- const claudeJson = readJson(join37(home, ".claude.json"));
18722
+ const claudeJson = readJson(join42(home, ".claude.json"));
16782
18723
  artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
16783
18724
  if (claudeJson?.projects && typeof claudeJson.projects === "object") {
16784
18725
  for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
@@ -16786,8 +18727,8 @@ function collectOperationalInventory(options = {}) {
16786
18727
  artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
16787
18728
  }
16788
18729
  }
16789
- artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join37(home, ".cursor", "mcp.json"))));
16790
- artifacts.push(...codexMcpArtifacts(readText(join37(codexHome, "config.toml"))));
18730
+ artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join42(home, ".cursor", "mcp.json"))));
18731
+ artifacts.push(...codexMcpArtifacts(readText(join42(codexHome, "config.toml"))));
16791
18732
  const projectRoots = discoveredProjectRoots(
16792
18733
  claudeJson,
16793
18734
  options.currentDirectory ?? process.cwd(),
@@ -16798,11 +18739,11 @@ function collectOperationalInventory(options = {}) {
16798
18739
  const scopeHash = sha256(projectRoot).slice(0, 16);
16799
18740
  artifacts.push(...mcpArtifactsFromJson(
16800
18741
  "claude_code",
16801
- readJson(join37(projectRoot, ".mcp.json")),
18742
+ readJson(join42(projectRoot, ".mcp.json")),
16802
18743
  `project:${scopeHash}`
16803
18744
  ));
16804
- const cursorProjectConfig = join37(projectRoot, ".cursor", "mcp.json");
16805
- if (resolve5(cursorProjectConfig) !== resolve5(join37(home, ".cursor", "mcp.json"))) {
18745
+ const cursorProjectConfig = join42(projectRoot, ".cursor", "mcp.json");
18746
+ if (resolve5(cursorProjectConfig) !== resolve5(join42(home, ".cursor", "mcp.json"))) {
16806
18747
  artifacts.push(...mcpArtifactsFromJson(
16807
18748
  "cursor",
16808
18749
  readJson(cursorProjectConfig),
@@ -16813,7 +18754,7 @@ function collectOperationalInventory(options = {}) {
16813
18754
  for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
16814
18755
  artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
16815
18756
  }
16816
- const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync38(path));
18757
+ const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync42(path));
16817
18758
  if (desktopConfigPath) {
16818
18759
  const desktopConfig = readJson(desktopConfigPath);
16819
18760
  harnesses.push({
@@ -16824,7 +18765,7 @@ function collectOperationalInventory(options = {}) {
16824
18765
  });
16825
18766
  artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
16826
18767
  }
16827
- const claudeSettings = readJson(join37(home, ".claude", "settings.json"));
18768
+ const claudeSettings = readJson(join42(home, ".claude", "settings.json"));
16828
18769
  if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
16829
18770
  for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
16830
18771
  artifacts.push({
@@ -16838,10 +18779,10 @@ function collectOperationalInventory(options = {}) {
16838
18779
  });
16839
18780
  }
16840
18781
  }
16841
- artifacts.push(...skillArtifacts("claude_code", join37(home, ".claude", "skills")));
16842
- artifacts.push(...skillArtifacts("cursor", join37(home, ".cursor", "skills")));
16843
- artifacts.push(...skillArtifacts("codex", join37(codexHome, "skills")));
16844
- artifacts.push(...cursorExtensionArtifacts(join37(home, ".cursor", "extensions")));
18782
+ artifacts.push(...skillArtifacts("claude_code", join42(home, ".claude", "skills")));
18783
+ artifacts.push(...skillArtifacts("cursor", join42(home, ".cursor", "skills")));
18784
+ artifacts.push(...skillArtifacts("codex", join42(codexHome, "skills")));
18785
+ artifacts.push(...cursorExtensionArtifacts(join42(home, ".cursor", "extensions")));
16845
18786
  const uniqueArtifacts = /* @__PURE__ */ new Map();
16846
18787
  for (const artifact of artifacts) {
16847
18788
  const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
@@ -16859,7 +18800,7 @@ function collectOperationalInventory(options = {}) {
16859
18800
  install_id: identity.installation_id,
16860
18801
  hostname_hash: pseudonymousHostnameHash(identity.installation_id, hostname2()),
16861
18802
  platform: targetPlatform,
16862
- os_version: release(),
18803
+ os_version: release2(),
16863
18804
  arch: arch(),
16864
18805
  cli_version: cliVersion(),
16865
18806
  node_version: process.version,
@@ -16894,22 +18835,22 @@ __export(sync_exports2, {
16894
18835
  syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
16895
18836
  });
16896
18837
  import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
16897
- import { spawn as spawn8 } from "child_process";
18838
+ import { spawn as spawn10 } from "child_process";
16898
18839
  import {
16899
- existsSync as existsSync39,
16900
- mkdirSync as mkdirSync21,
16901
- readFileSync as readFileSync37,
18840
+ existsSync as existsSync43,
18841
+ mkdirSync as mkdirSync25,
18842
+ readFileSync as readFileSync40,
16902
18843
  renameSync as renameSync10,
16903
- writeFileSync as writeFileSync26
18844
+ writeFileSync as writeFileSync30
16904
18845
  } from "fs";
16905
- import { homedir as homedir38 } from "os";
16906
- import { dirname as dirname10, join as join38 } from "path";
18846
+ import { homedir as homedir44 } from "os";
18847
+ import { dirname as dirname13, join as join43 } from "path";
16907
18848
  function syncStatePath() {
16908
- return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join38(homedir38(), ".synkro", "inventory-sync.json");
18849
+ return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join43(homedir44(), ".synkro", "inventory-sync.json");
16909
18850
  }
16910
18851
  function readState(path = syncStatePath()) {
16911
18852
  try {
16912
- const parsed = JSON.parse(readFileSync37(path, "utf8"));
18853
+ const parsed = JSON.parse(readFileSync40(path, "utf8"));
16913
18854
  return parsed && typeof parsed === "object" ? parsed : {};
16914
18855
  } catch {
16915
18856
  return {};
@@ -16917,9 +18858,9 @@ function readState(path = syncStatePath()) {
16917
18858
  }
16918
18859
  function writeState(state, path = syncStatePath()) {
16919
18860
  try {
16920
- mkdirSync21(dirname10(path), { recursive: true, mode: 448 });
18861
+ mkdirSync25(dirname13(path), { recursive: true, mode: 448 });
16921
18862
  const temp = `${path}.${process.pid}.tmp`;
16922
- writeFileSync26(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18863
+ writeFileSync30(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
16923
18864
  renameSync10(temp, path);
16924
18865
  } catch {
16925
18866
  }
@@ -16932,10 +18873,10 @@ function shouldSyncInventory(state, now = Date.now(), target) {
16932
18873
  return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
16933
18874
  }
16934
18875
  function readConfig() {
16935
- const path = join38(homedir38(), ".synkro", "config.env");
18876
+ const path = join43(homedir44(), ".synkro", "config.env");
16936
18877
  const out = {};
16937
18878
  try {
16938
- for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
18879
+ for (const rawLine of readFileSync40(path, "utf8").split("\n")) {
16939
18880
  const line = rawLine.trim();
16940
18881
  if (!line || line.startsWith("#")) continue;
16941
18882
  const index = line.indexOf("=");
@@ -16974,7 +18915,7 @@ function resolveInventoryGateway(raw) {
16974
18915
  }
16975
18916
  async function loadToken() {
16976
18917
  try {
16977
- const durable = readFileSync37(join38(homedir38(), ".synkro", ".mcp-jwt"), "utf8").trim();
18918
+ const durable = readFileSync40(join43(homedir44(), ".synkro", ".mcp-jwt"), "utf8").trim();
16978
18919
  if (durable) return durable;
16979
18920
  } catch {
16980
18921
  }
@@ -17092,8 +19033,8 @@ function syncOperationalInventoryDetached() {
17092
19033
  writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
17093
19034
  try {
17094
19035
  const script = process.argv[1];
17095
- if (!script || !existsSync39(script)) return;
17096
- const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
19036
+ if (!script || !existsSync43(script)) return;
19037
+ const child = spawn10(process.execPath, [script, "inventory-sync", "--detached"], {
17097
19038
  detached: true,
17098
19039
  stdio: "ignore",
17099
19040
  env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
@@ -17116,14 +19057,15 @@ var init_sync2 = __esm({
17116
19057
  });
17117
19058
 
17118
19059
  // cli/bootstrap.js
17119
- import { readFileSync as readFileSync38, existsSync as existsSync40 } from "fs";
19060
+ import { readFileSync as readFileSync41, existsSync as existsSync44 } from "fs";
17120
19061
  import { resolve as resolve6 } from "path";
19062
+ process.title = "synkro";
17121
19063
  var envCandidates = [
17122
19064
  resolve6(process.env.HOME ?? "", ".synkro", "config.env")
17123
19065
  ];
17124
19066
  for (const envPath of envCandidates) {
17125
- if (!existsSync40(envPath)) continue;
17126
- const envContent = readFileSync38(envPath, "utf-8");
19067
+ if (!existsSync44(envPath)) continue;
19068
+ const envContent = readFileSync41(envPath, "utf-8");
17127
19069
  for (const line of envContent.split("\n")) {
17128
19070
  const trimmed = line.trim();
17129
19071
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -17140,7 +19082,7 @@ var subArgs = args.slice(1);
17140
19082
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
17141
19083
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
17142
19084
  function printVersion() {
17143
- console.log("1.10.3");
19085
+ console.log("1.10.4");
17144
19086
  }
17145
19087
  function printHelp2() {
17146
19088
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents