@synkro-sh/cli 1.10.3 → 1.10.5

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.5";
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.5";
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.5")}`
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.5",
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,1657 @@ 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;
16705
+ });
16706
+
16707
+ // cli/harness/cursor.ts
16708
+ function unwrapReplay(text) {
16709
+ return text.replace(/<\/?user_query>/gi, "").trim();
14999
16710
  }
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);
16711
+ function textOf(message) {
16712
+ const content = message?.content;
16713
+ if (typeof content === "string") return unwrapReplay(content);
16714
+ if (!Array.isArray(content)) return "";
16715
+ return unwrapReplay(content.filter((part) => part && (part.type === "text" || typeof part.text === "string")).map((part) => String(part.text || "")).join(""));
16716
+ }
16717
+ function toolPayload(toolCall) {
16718
+ if (!toolCall) return { kind: "other", body: {} };
16719
+ for (const [key, kind] of Object.entries(TOOL_KINDS)) {
16720
+ if (toolCall[key]) return { kind, body: toolCall[key] };
16721
+ }
16722
+ const fallback = Object.keys(toolCall).find((key) => key.endsWith("ToolCall"));
16723
+ return fallback ? { kind: "other", body: toolCall[fallback] } : { kind: "other", body: {} };
16724
+ }
16725
+ function targetOf(kind, body) {
16726
+ const args2 = body?.args || {};
16727
+ const candidate = args2.command ?? args2.path ?? args2.filePath ?? args2.file_path ?? args2.pattern ?? args2.query ?? args2.target ?? "";
16728
+ const text = typeof candidate === "string" ? candidate : JSON.stringify(candidate ?? "");
16729
+ return text || body?.description || kind;
16730
+ }
16731
+ function parseCursorLine(line) {
16732
+ const trimmed = String(line || "").trim();
16733
+ if (!trimmed) return [];
16734
+ let frame;
16735
+ try {
16736
+ frame = JSON.parse(trimmed);
16737
+ } catch {
16738
+ return [];
15007
16739
  }
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" };
16740
+ const type = frame.type;
16741
+ const subtype = frame.subtype;
16742
+ if (type === "system" && subtype === "init") {
16743
+ return [{
16744
+ type: "session-start",
16745
+ sessionId: String(frame.session_id || ""),
16746
+ model: String(frame.model || ""),
16747
+ cwd: String(frame.cwd || ""),
16748
+ authSource: String(frame.apiKeySource || "")
16749
+ }];
16750
+ }
16751
+ if (type === "user") {
16752
+ const text = textOf(frame.message);
16753
+ return text ? [{ type: "user-message", text }] : [];
16754
+ }
16755
+ if (type === "assistant") {
16756
+ const text = textOf(frame.message);
16757
+ return text ? [{ type: "assistant-message", text }] : [];
16758
+ }
16759
+ if (type === "thinking" && subtype === "delta" && frame.text) {
16760
+ return [{ type: "thinking", text: String(frame.text) }];
16761
+ }
16762
+ if (type === "tool_call") {
16763
+ const { kind, body } = toolPayload(frame.tool_call);
16764
+ const id = String(frame.call_id || frame.tool_call?.toolCallId || "");
16765
+ const target = targetOf(kind, body);
16766
+ const description = String(body?.description || frame.tool_call?.description || "");
16767
+ if (subtype === "started") {
16768
+ return [{ type: "tool-start", id, kind, target, description }];
16769
+ }
16770
+ if (subtype === "completed") {
16771
+ const result = body?.result || {};
16772
+ const success = result.success;
16773
+ const rejected = result.rejected;
16774
+ const rawReason = String(rejected?.reason || "");
16775
+ const blocked = Boolean(rejected) && BLOCK_MARKER.test(rawReason);
16776
+ return [{
16777
+ type: "tool-end",
16778
+ id,
16779
+ kind,
16780
+ target: target || String(rejected?.command || ""),
16781
+ ok: Boolean(success) && Number(success?.exitCode ?? 0) === 0,
16782
+ blocked,
16783
+ reason: rejected ? blocked ? blockReason(rawReason) : rawReason || "rejected" : "",
16784
+ exitCode: success ? Number(success.exitCode ?? 0) : null,
16785
+ output: String(success?.stdout || success?.stderr || "")
16786
+ }];
15013
16787
  }
16788
+ return [];
15014
16789
  }
15015
- const pueueId = await registerAgent(runner, session);
15016
- if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
15017
- return { ok: true, session };
16790
+ if (type === "retry") {
16791
+ if (subtype !== "starting") return [];
16792
+ const attempt = Number(frame.attempt || 0);
16793
+ return [{
16794
+ type: "notice",
16795
+ kind: "retry",
16796
+ text: "connection dropped, resuming" + (attempt ? " (attempt " + attempt + ")" : "") + "\u2026"
16797
+ }];
16798
+ }
16799
+ if (type === "result") {
16800
+ return [{
16801
+ type: "turn-end",
16802
+ ok: !frame.is_error,
16803
+ text: String(frame.result || "")
16804
+ }];
16805
+ }
16806
+ return [];
15018
16807
  }
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);
16808
+ function feed(buffer, chunk) {
16809
+ const combined = buffer + chunk;
16810
+ const parts = combined.split("\n");
16811
+ const rest = parts.pop() ?? "";
16812
+ return { lines: parts, rest };
15023
16813
  }
15024
- var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED;
15025
- var init_backend = __esm({
15026
- "cli/ui/backend.ts"() {
16814
+ function cursorArgs(prompt) {
16815
+ return [
16816
+ "-p",
16817
+ prompt,
16818
+ "--output-format",
16819
+ "stream-json",
16820
+ // --force auto-runs tools but does NOT bypass hooks (verified live), so
16821
+ // Synkro's guards still gate every call; --trust loads workspace hooks.
16822
+ "--force",
16823
+ "--trust"
16824
+ ];
16825
+ }
16826
+ var TOOL_KINDS;
16827
+ var init_cursor = __esm({
16828
+ "cli/harness/cursor.ts"() {
15027
16829
  "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";
16830
+ init_events();
16831
+ TOOL_KINDS = {
16832
+ shellToolCall: "shell",
16833
+ readToolCall: "read",
16834
+ editToolCall: "edit",
16835
+ writeToolCall: "write",
16836
+ deleteToolCall: "delete",
16837
+ grepToolCall: "search",
16838
+ globToolCall: "search",
16839
+ lsToolCall: "list",
16840
+ todoToolCall: "todo",
16841
+ updateTodosToolCall: "todo"
16842
+ };
15033
16843
  }
15034
16844
  });
15035
16845
 
15036
- // cli/ui/sidebar.ts
15037
- function runnerFor(backend) {
15038
- return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
16846
+ // cli/harness/render.ts
16847
+ function clip3(text, max) {
16848
+ const value = String(text || "").replace(/\s+/g, " ").trim();
16849
+ return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
16850
+ }
16851
+ function renderEvent(event, width = 100) {
16852
+ const body = Math.max(30, width - 4);
16853
+ switch (event.type) {
16854
+ case "session-start":
16855
+ return [
16856
+ "",
16857
+ S2.dim + " " + event.model + " \xB7 " + clip3(event.cwd, body - 30) + (event.authSource === "login" ? " \xB7 subscription" : "") + S2.reset,
16858
+ ""
16859
+ ];
16860
+ case "user-message":
16861
+ return ["", S2.user + " \u276F " + S2.reset + S2.bold + clip3(event.text, body) + S2.reset, ""];
16862
+ case "thinking":
16863
+ return [S2.think + " \xB7 thinking\u2026" + S2.reset];
16864
+ case "assistant-message":
16865
+ return ["", ...wrap(event.text, body).map((line) => S2.agent + " " + line + S2.reset), ""];
16866
+ case "tool-start": {
16867
+ const glyph = GLYPH[event.kind] || GLYPH.other;
16868
+ return [S2.tool + " " + glyph + " " + S2.reset + S2.dim + clip3(event.target, body - 6) + S2.reset];
16869
+ }
16870
+ case "tool-end": {
16871
+ if (event.blocked) {
16872
+ return [
16873
+ S2.blocked + " \u26D4 blocked" + S2.reset + S2.dim + " " + clip3(event.target, body - 14) + S2.reset,
16874
+ ...wrap(event.reason, body - 6).map((line) => " " + S2.rule + line + S2.reset)
16875
+ ];
16876
+ }
16877
+ const mark = event.ok ? S2.ok + " \u2713" : S2.blocked + " \u2717";
16878
+ const code = event.exitCode === null || event.exitCode === 0 ? "" : " (exit " + event.exitCode + ")";
16879
+ const out = event.output.trim() ? S2.dim + " " + clip3(event.output, body - 20) + S2.reset : "";
16880
+ return [mark + S2.reset + code + out];
16881
+ }
16882
+ case "turn-end":
16883
+ return ["", S2.dim + " " + (event.ok ? "done" : "ended with an error") + S2.reset, ""];
16884
+ case "notice":
16885
+ return [S2.dim + " " + clip3(event.text, body) + S2.reset];
16886
+ default:
16887
+ return [];
16888
+ }
15039
16889
  }
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));
16890
+ function wrap(text, width) {
16891
+ const words = String(text || "").replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
16892
+ if (words.length === 0) return [];
16893
+ const lines = [];
16894
+ let line = "";
16895
+ for (const word of words) {
16896
+ if (!line) line = word;
16897
+ else if ((line + " " + word).length <= width) line += " " + word;
16898
+ else {
16899
+ lines.push(line);
16900
+ line = word;
16901
+ }
15076
16902
  }
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);
16903
+ if (line) lines.push(line);
16904
+ return lines;
16905
+ }
16906
+ var ESC2, S2, GLYPH;
16907
+ var init_render2 = __esm({
16908
+ "cli/harness/render.ts"() {
16909
+ "use strict";
16910
+ ESC2 = "\x1B[";
16911
+ S2 = {
16912
+ reset: ESC2 + "0m",
16913
+ dim: ESC2 + "2m",
16914
+ bold: ESC2 + "1m",
16915
+ user: ESC2 + "38;5;111m",
16916
+ agent: ESC2 + "38;5;252m",
16917
+ think: ESC2 + "38;5;244m",
16918
+ tool: ESC2 + "38;5;180m",
16919
+ ok: ESC2 + "38;5;114m",
16920
+ blocked: ESC2 + "38;5;203m",
16921
+ rule: ESC2 + "38;5;211m"
16922
+ };
16923
+ GLYPH = {
16924
+ shell: "\u276F",
16925
+ read: "\u25C4",
16926
+ edit: "\u270E",
16927
+ write: "\u270E",
16928
+ delete: "\u2716",
16929
+ search: "\u2315",
16930
+ list: "\u2630",
16931
+ todo: "\u2611",
16932
+ other: "\u2022"
16933
+ };
15084
16934
  }
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;
16935
+ });
16936
+
16937
+ // cli/harness/run.ts
16938
+ import { spawn as spawn9 } from "child_process";
16939
+ function replayKey(event) {
16940
+ switch (event.type) {
16941
+ case "user-message":
16942
+ return "u:" + event.text;
16943
+ case "assistant-message":
16944
+ return "a:" + event.text;
16945
+ case "tool-start":
16946
+ return "s:" + event.id;
16947
+ case "tool-end":
16948
+ return "e:" + event.id;
16949
+ default:
16950
+ return "";
15092
16951
  }
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;
16952
+ }
16953
+ function createTurnSink(opts) {
16954
+ const events = [];
16955
+ let blocked = 0;
16956
+ let lastWasThinking = false;
16957
+ let replaying = false;
16958
+ const seen = /* @__PURE__ */ new Set();
16959
+ let sawTurnEnd = false;
16960
+ let sawAnswer = false;
16961
+ let buffer = "";
16962
+ const emit2 = (event) => {
16963
+ events.push(event);
16964
+ if (event.type === "tool-end" && event.blocked) blocked += 1;
16965
+ opts.onEvent?.(event);
16966
+ if (event.type === "thinking") {
16967
+ if (lastWasThinking) return;
16968
+ lastWasThinking = true;
16969
+ } else {
16970
+ lastWasThinking = false;
15098
16971
  }
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);
15108
- }
15109
- async function spawnContainerAgent() {
15110
- if (info.backend !== "container") {
15111
- state.message = "container runtime unavailable";
15112
- return;
16972
+ const rendered = renderEvent(event, opts.width);
16973
+ if (rendered.length) opts.write(rendered.join("\n") + "\n");
16974
+ };
16975
+ const take = (event) => {
16976
+ if (event.type === "notice" && event.kind === "retry") replaying = true;
16977
+ const key = replayKey(event);
16978
+ if (key) {
16979
+ if (replaying && seen.has(key)) return;
16980
+ seen.add(key);
16981
+ }
16982
+ if (event.type === "turn-end") sawTurnEnd = true;
16983
+ if (event.type === "assistant-message") sawAnswer = true;
16984
+ emit2(event);
16985
+ };
16986
+ return {
16987
+ chunk(text) {
16988
+ const { lines, rest } = feed(buffer, text);
16989
+ buffer = rest;
16990
+ for (const line of lines) for (const event of parseCursorLine(line)) take(event);
16991
+ },
16992
+ notice(text) {
16993
+ emit2({ type: "notice", text });
16994
+ },
16995
+ finish(exit) {
16996
+ if (!sawTurnEnd) {
16997
+ if (exit !== 0) {
16998
+ emit2({
16999
+ type: "notice",
17000
+ text: sawAnswer ? "cursor-agent exited " + exit + " after retrying; the reply above is complete" : "cursor-agent exited " + exit + " without completing the turn"
17001
+ });
17002
+ }
17003
+ emit2({ type: "turn-end", ok: sawAnswer, text: "" });
17004
+ }
17005
+ return { events, blocked, exitCode: sawAnswer ? 0 : exit };
15113
17006
  }
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);
17007
+ };
17008
+ }
17009
+ async function runCursorTurn(opts) {
17010
+ const write2 = opts.write || ((text) => process.stdout.write(text));
17011
+ const width = opts.width || Number(process.stdout.columns || 100);
17012
+ const sink = createTurnSink({ write: write2, onEvent: opts.onEvent, width });
17013
+ return new Promise((resolve7) => {
17014
+ const child = spawn9("cursor-agent", cursorArgs(opts.prompt), {
17015
+ cwd: opts.cwd,
17016
+ stdio: ["ignore", "pipe", "pipe"]
17017
+ });
17018
+ child.stdout.on("data", (chunk) => sink.chunk(chunk.toString("utf8")));
17019
+ child.stderr.on("data", (chunk) => {
17020
+ const text = chunk.toString("utf8").trim();
17021
+ if (/error|fatal/i.test(text) && !/RetriableError/i.test(text)) sink.notice(text);
17022
+ });
17023
+ child.on("close", (code) => resolve7(sink.finish(code ?? 0)));
17024
+ child.on("error", (error) => {
17025
+ sink.notice("failed to start cursor-agent: " + String(error));
17026
+ resolve7(sink.finish(1));
17027
+ });
17028
+ });
17029
+ }
17030
+ var init_run = __esm({
17031
+ "cli/harness/run.ts"() {
17032
+ "use strict";
17033
+ init_cursor();
17034
+ init_render2();
15117
17035
  }
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;
17036
+ });
17037
+
17038
+ // cli/harness/session.ts
17039
+ import { createInterface as createInterface5 } from "readline";
17040
+ async function runOnce(harness, cwd, prompt) {
17041
+ if (harness !== "cursor") {
17042
+ process.stdout.write(S2.dim + " " + harness + " sessions are not embedded yet\n" + S2.reset);
17043
+ return 1;
17044
+ }
17045
+ const result = await runCursorTurn({ prompt, cwd });
17046
+ if (result.blocked > 0) {
17047
+ process.stdout.write(
17048
+ S2.blocked + " " + result.blocked + " action" + (result.blocked === 1 ? "" : "s") + " blocked by policy" + S2.reset + "\n"
17049
+ );
15125
17050
  }
15126
- async function newTab() {
15127
- await run(host, ["node", String(bootPath), "ui", "--new-tab", outerSession]);
17051
+ return result.exitCode;
17052
+ }
17053
+ async function runGovernedSession(harness, cwd, prompt) {
17054
+ if (prompt) return runOnce(harness, cwd, prompt);
17055
+ process.stdout.write(BANNER.join("\n") + "\n");
17056
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
17057
+ const ask3 = () => new Promise((resolve7) => rl.question(S2.user + " \u276F " + S2.reset, resolve7));
17058
+ for (; ; ) {
17059
+ const line = (await ask3()).trim();
17060
+ if (!line) continue;
17061
+ if (line === "exit" || line === "quit") break;
17062
+ await runOnce(harness, cwd, line);
15128
17063
  }
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;
15152
- }
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
- }
15164
- await refresh();
15165
- draw();
15166
- })();
15167
- });
15168
- await refresh();
15169
- draw();
15170
- setInterval(() => {
15171
- void refresh().then(draw);
15172
- }, POLL_MS);
17064
+ rl.close();
17065
+ return 0;
15173
17066
  }
15174
- var POLL_MS, CONDUCTOR_URL;
15175
- var init_sidebar = __esm({
15176
- "cli/ui/sidebar.ts"() {
17067
+ var BANNER;
17068
+ var init_session = __esm({
17069
+ "cli/harness/session.ts"() {
15177
17070
  "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");
17071
+ init_run();
17072
+ init_render2();
17073
+ BANNER = [
17074
+ "",
17075
+ S2.bold + " synkro" + S2.reset + S2.dim + " governed session" + S2.reset,
17076
+ S2.dim + " every tool call passes Synkro policy before it runs" + S2.reset,
17077
+ S2.dim + " ctrl-c to leave" + S2.reset,
17078
+ ""
17079
+ ];
15185
17080
  }
15186
17081
  });
15187
17082
 
@@ -15190,22 +17085,68 @@ var ui_exports = {};
15190
17085
  __export(ui_exports, {
15191
17086
  uiCommand: () => uiCommand
15192
17087
  });
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
- }
17088
+ import { execSync as execSync8 } from "child_process";
15201
17089
  function tmuxPresent() {
15202
17090
  try {
15203
- execSync7("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
17091
+ execSync8("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
15204
17092
  return true;
15205
17093
  } catch {
15206
17094
  return false;
15207
17095
  }
15208
17096
  }
17097
+ async function takeover(kind, cwd) {
17098
+ const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
17099
+ const info = await detectContainerBackend();
17100
+ const reachable = info.backend === "container" ? (await run(info.runner, ["test", "-d", cwd])).ok : false;
17101
+ const backend = reachable ? "container" : "host";
17102
+ const spaceName = cwd.split("/").filter(Boolean).pop() || "space";
17103
+ const spawned = await spawnAgent(info, {
17104
+ name: spaceName + "-" + harness + "-" + String(process.pid % 1e4),
17105
+ harness,
17106
+ spaceName,
17107
+ cwd,
17108
+ backend
17109
+ });
17110
+ if (!spawned.ok) {
17111
+ console.error("synkro: spawn failed: " + (spawned.error || "unknown"));
17112
+ return 1;
17113
+ }
17114
+ if (process.env.TMUX_PANE) {
17115
+ await run(HOST3, ["tmux", "rename-window", "-t", process.env.TMUX_PANE, tabTitle(harness, spaceName)]);
17116
+ }
17117
+ const runner = backend === "container" ? info.runner : HOST3;
17118
+ return runInherit(["env", "TMUX=", ...runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", spawned.session])]);
17119
+ }
17120
+ async function restoreSession(session) {
17121
+ const record = loadRecords().find((row2) => row2.session === session);
17122
+ if (!record) return;
17123
+ const info = await detectContainerBackend();
17124
+ await spawnAgent(info, {
17125
+ name: record.name,
17126
+ harness: record.harness,
17127
+ spaceName: record.spaceName,
17128
+ cwd: record.space,
17129
+ backend: record.backend,
17130
+ resume: true
17131
+ });
17132
+ }
17133
+ async function printStatus() {
17134
+ const info = await detectContainerBackend();
17135
+ const [hostAgents, containerAgents] = await Promise.all([
17136
+ discoverAgents({ kind: "host" }, "host", /* @__PURE__ */ new Map()),
17137
+ info.backend === "container" ? discoverAgents(info.runner, "container", /* @__PURE__ */ new Map()) : Promise.resolve({ agents: [], hashes: /* @__PURE__ */ new Map() })
17138
+ ]);
17139
+ const live = [...hostAgents.agents, ...containerAgents.agents];
17140
+ const rows = [...live, ...offlineAgents(live, loadRecords())];
17141
+ if (rows.length === 0) {
17142
+ console.log("no sessions. `synkro ui` to start one.");
17143
+ return;
17144
+ }
17145
+ for (const agent of rows) {
17146
+ console.log([agent.status.padEnd(8), agent.harness.padEnd(7), agent.name, agent.space || ""].join(" "));
17147
+ }
17148
+ console.log("\n`synkro ui` to attach" + (rows.some((agent) => agent.status === "offline") ? "; offline sessions restore with r" : ""));
17149
+ }
15209
17150
  async function uiCommand(args2) {
15210
17151
  const bootPath = String(process.argv[1] || "");
15211
17152
  if (args2.includes("--sidebar")) {
@@ -15214,8 +17155,53 @@ async function uiCommand(args2) {
15214
17155
  });
15215
17156
  return;
15216
17157
  }
15217
- if (args2.includes("--new-tab")) {
15218
- await buildTab(bootPath, repoRoot(), "new");
17158
+ const dialogAt = args2.indexOf("--dialog");
17159
+ if (dialogAt !== -1) {
17160
+ await runDialog(
17161
+ args2[dialogAt + 1] || "new-tab",
17162
+ args2[dialogAt + 2] || repoRoot(),
17163
+ args2[dialogAt + 3] || "",
17164
+ args2[dialogAt + 4] || ""
17165
+ );
17166
+ return;
17167
+ }
17168
+ const tabAt = args2.indexOf("--tab");
17169
+ if (tabAt !== -1) {
17170
+ await createTab(bootPath, args2[tabAt + 1] || "terminal", args2[tabAt + 2] || repoRoot());
17171
+ return;
17172
+ }
17173
+ const openAt = args2.indexOf("--open-agent");
17174
+ if (openAt !== -1) {
17175
+ await openAgentTab(bootPath, args2[openAt + 1] || "");
17176
+ return;
17177
+ }
17178
+ const runAt = args2.indexOf("--run");
17179
+ if (runAt !== -1) {
17180
+ const harness = args2[runAt + 1] || "cursor";
17181
+ const cwd = args2[runAt + 2] || repoRoot();
17182
+ const prompt = args2.slice(runAt + 3).join(" ").trim();
17183
+ process.exitCode = await runGovernedSession(harness, cwd, prompt);
17184
+ return;
17185
+ }
17186
+ const takeoverAt = args2.indexOf("--takeover");
17187
+ if (takeoverAt !== -1) {
17188
+ process.exitCode = await takeover(args2[takeoverAt + 1] || "claude", args2[takeoverAt + 2] || process.cwd());
17189
+ return;
17190
+ }
17191
+ const restoreAt = args2.indexOf("--restore");
17192
+ if (restoreAt !== -1) {
17193
+ await restoreSession(args2[restoreAt + 1] || "");
17194
+ return;
17195
+ }
17196
+ if (args2.includes("--status")) {
17197
+ await printStatus();
17198
+ return;
17199
+ }
17200
+ const killAt = args2.indexOf("--kill-agent");
17201
+ if (killAt !== -1) {
17202
+ const backend = args2[killAt + 1] === "container" ? "container" : "host";
17203
+ const session = args2[killAt + 2] || "";
17204
+ if (session.startsWith("synkro-agent-")) await killAgent(backend, session, "");
15219
17205
  return;
15220
17206
  }
15221
17207
  if (!tmuxPresent()) {
@@ -15223,14 +17209,26 @@ async function uiCommand(args2) {
15223
17209
  process.exitCode = 1;
15224
17210
  return;
15225
17211
  }
15226
- const code = await launchUi(bootPath, repoRoot());
17212
+ const repo = repoRoot();
17213
+ if (!await uiSessionExists()) await restoreTabs(bootPath, repo);
17214
+ const code = await launchUi(bootPath, repo);
15227
17215
  process.exitCode = code;
15228
17216
  }
17217
+ var HOST3;
15229
17218
  var init_ui = __esm({
15230
17219
  "cli/commands/ui.ts"() {
15231
17220
  "use strict";
15232
17221
  init_launch();
15233
17222
  init_sidebar();
17223
+ init_dialog();
17224
+ init_tabs();
17225
+ init_session();
17226
+ init_launch();
17227
+ init_backend();
17228
+ init_model();
17229
+ init_manifest();
17230
+ init_tmux();
17231
+ HOST3 = { kind: "host" };
15234
17232
  }
15235
17233
  });
15236
17234
 
@@ -15270,12 +17268,12 @@ __export(linear_exports, {
15270
17268
  formatLinks: () => formatLinks,
15271
17269
  linearCommand: () => linearCommand
15272
17270
  });
15273
- import { readFileSync as readFileSync30 } from "fs";
15274
- import { homedir as homedir32 } from "os";
15275
- import { join as join32 } from "path";
17271
+ import { readFileSync as readFileSync33 } from "fs";
17272
+ import { homedir as homedir38 } from "os";
17273
+ import { join as join37 } from "path";
15276
17274
  function mcpJwt() {
15277
17275
  try {
15278
- return readFileSync30(join32(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
17276
+ return readFileSync33(join37(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
15279
17277
  } catch {
15280
17278
  return "";
15281
17279
  }
@@ -15314,7 +17312,7 @@ var SYNKRO_DIR14, PORT2, BASE;
15314
17312
  var init_linear = __esm({
15315
17313
  "cli/commands/linear.ts"() {
15316
17314
  "use strict";
15317
- SYNKRO_DIR14 = join32(homedir32(), ".synkro");
17315
+ SYNKRO_DIR14 = join37(homedir38(), ".synkro");
15318
17316
  PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
15319
17317
  BASE = `http://127.0.0.1:${PORT2}`;
15320
17318
  }
@@ -15322,7 +17320,7 @@ var init_linear = __esm({
15322
17320
 
15323
17321
  // cli/scanning/cveReachability.ts
15324
17322
  import { parse } from "@babel/parser";
15325
- import { readFileSync as readFileSync31 } from "fs";
17323
+ import { readFileSync as readFileSync34 } from "fs";
15326
17324
  function walk(node, visit) {
15327
17325
  if (!node || typeof node.type !== "string") return;
15328
17326
  visit(node);
@@ -15464,12 +17462,12 @@ var init_cveReachability = __esm({
15464
17462
 
15465
17463
  // cli/reachability/reachabilityScan.ts
15466
17464
  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";
17465
+ import { readFileSync as readFileSync35, writeFileSync as writeFileSync27, existsSync as existsSync38, readdirSync as readdirSync10 } from "fs";
17466
+ import { join as join38 } from "path";
17467
+ import { homedir as homedir39 } from "os";
15470
17468
  import { createRequire } from "module";
15471
17469
  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"]);
17470
+ const SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
15473
17471
  const EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
15474
17472
  const files = [];
15475
17473
  const stack = [repoRoot3];
@@ -15477,21 +17475,21 @@ function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
15477
17475
  const dir = stack.pop();
15478
17476
  let ents;
15479
17477
  try {
15480
- ents = readdirSync9(dir, { withFileTypes: true });
17478
+ ents = readdirSync10(dir, { withFileTypes: true });
15481
17479
  } catch {
15482
17480
  continue;
15483
17481
  }
15484
17482
  for (const e of ents) {
15485
17483
  if (files.length >= maxFiles) break;
15486
- const full = join33(dir, e.name);
17484
+ const full = join38(dir, e.name);
15487
17485
  if (e.isDirectory()) {
15488
- if (!SKIP.has(e.name) && !e.name.startsWith(".")) stack.push(full);
17486
+ if (!SKIP2.has(e.name) && !e.name.startsWith(".")) stack.push(full);
15489
17487
  continue;
15490
17488
  }
15491
17489
  if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
15492
17490
  const rel = full.startsWith(repoRoot3 + "/") ? full.slice(repoRoot3.length + 1) : full;
15493
17491
  try {
15494
- const content = readFileSync32(full, "utf8");
17492
+ const content = readFileSync35(full, "utf8");
15495
17493
  if (content.length <= maxBytes) files.push({ path: rel, content });
15496
17494
  } catch {
15497
17495
  }
@@ -15510,12 +17508,12 @@ function cleanVersion(spec) {
15510
17508
  function gatherManifestVersions(repoRoot3) {
15511
17509
  const out = {};
15512
17510
  const dirs = [repoRoot3];
15513
- const pkgsDir = join33(repoRoot3, "packages");
15514
- if (existsSync34(pkgsDir)) {
17511
+ const pkgsDir = join38(repoRoot3, "packages");
17512
+ if (existsSync38(pkgsDir)) {
15515
17513
  try {
15516
- for (const d of readdirSync9(pkgsDir)) {
15517
- const pd = join33(pkgsDir, d);
15518
- if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
17514
+ for (const d of readdirSync10(pkgsDir)) {
17515
+ const pd = join38(pkgsDir, d);
17516
+ if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
15519
17517
  }
15520
17518
  } catch {
15521
17519
  }
@@ -15524,7 +17522,7 @@ function gatherManifestVersions(repoRoot3) {
15524
17522
  for (const dir of dirs) {
15525
17523
  let pkg;
15526
17524
  try {
15527
- pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
17525
+ pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
15528
17526
  } catch {
15529
17527
  continue;
15530
17528
  }
@@ -15544,28 +17542,28 @@ function findJelly(repoRoot3) {
15544
17542
  try {
15545
17543
  const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
15546
17544
  const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
15547
- const pkg = JSON.parse(readFileSync32(pkgJson, "utf8"));
17545
+ const pkg = JSON.parse(readFileSync35(pkgJson, "utf8"));
15548
17546
  const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
15549
17547
  if (bin) {
15550
- const p = join33(dir, bin);
15551
- if (existsSync34(p)) return p;
17548
+ const p = join38(dir, bin);
17549
+ if (existsSync38(p)) return p;
15552
17550
  }
15553
17551
  } catch {
15554
17552
  }
15555
17553
  for (const base of [repoRoot3, process.cwd()]) {
15556
- const b = join33(base, "node_modules", ".bin", "jelly");
15557
- if (existsSync34(b)) return b;
17554
+ const b = join38(base, "node_modules", ".bin", "jelly");
17555
+ if (existsSync38(b)) return b;
15558
17556
  }
15559
17557
  return null;
15560
17558
  }
15561
17559
  function findEntries(repoRoot3) {
15562
17560
  const dirs = [repoRoot3];
15563
- const pkgsDir = join33(repoRoot3, "packages");
15564
- if (existsSync34(pkgsDir)) {
17561
+ const pkgsDir = join38(repoRoot3, "packages");
17562
+ if (existsSync38(pkgsDir)) {
15565
17563
  try {
15566
- for (const d of readdirSync9(pkgsDir)) {
15567
- const pd = join33(pkgsDir, d);
15568
- if (existsSync34(join33(pd, "package.json"))) dirs.push(pd);
17564
+ for (const d of readdirSync10(pkgsDir)) {
17565
+ const pd = join38(pkgsDir, d);
17566
+ if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
15569
17567
  }
15570
17568
  } catch {
15571
17569
  }
@@ -15573,12 +17571,12 @@ function findEntries(repoRoot3) {
15573
17571
  const entries = [];
15574
17572
  for (const dir of dirs) {
15575
17573
  try {
15576
- const pkg = JSON.parse(readFileSync32(join33(dir, "package.json"), "utf8"));
17574
+ const pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
15577
17575
  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
17576
  for (const c of cands) {
15579
17577
  if (typeof c !== "string") continue;
15580
- const f = join33(dir, c);
15581
- if (existsSync34(f)) {
17578
+ const f = join38(dir, c);
17579
+ if (existsSync38(f)) {
15582
17580
  entries.push(f);
15583
17581
  break;
15584
17582
  }
@@ -15611,9 +17609,9 @@ function parseApiUsage(log) {
15611
17609
  }
15612
17610
  function runReachabilityScan(repoRoot3, opts = {}) {
15613
17611
  const commit = currentCommit(repoRoot3);
15614
- if (!opts.force && commit && existsSync34(REACHABILITY_PATH)) {
17612
+ if (!opts.force && commit && existsSync38(REACHABILITY_PATH)) {
15615
17613
  try {
15616
- const prev = JSON.parse(readFileSync32(REACHABILITY_PATH, "utf8"));
17614
+ const prev = JSON.parse(readFileSync35(REACHABILITY_PATH, "utf8"));
15617
17615
  if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
15618
17616
  } catch {
15619
17617
  }
@@ -15702,7 +17700,7 @@ function runReachabilityScan(repoRoot3, opts = {}) {
15702
17700
  if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
15703
17701
  const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
15704
17702
  try {
15705
- writeFileSync23(REACHABILITY_PATH, JSON.stringify(file, null, 2));
17703
+ writeFileSync27(REACHABILITY_PATH, JSON.stringify(file, null, 2));
15706
17704
  } catch (e) {
15707
17705
  return { ok: false, reason: "write failed: " + String(e.message || e) };
15708
17706
  }
@@ -15714,7 +17712,7 @@ var init_reachabilityScan = __esm({
15714
17712
  "use strict";
15715
17713
  init_cveReachability();
15716
17714
  require2 = createRequire(import.meta.url);
15717
- REACHABILITY_PATH = join33(homedir33(), ".synkro", "reachability.json");
17715
+ REACHABILITY_PATH = join38(homedir39(), ".synkro", "reachability.json");
15718
17716
  }
15719
17717
  });
15720
17718
 
@@ -15723,15 +17721,15 @@ var reachabilityScan_exports = {};
15723
17721
  __export(reachabilityScan_exports, {
15724
17722
  reachabilityScanCommand: () => reachabilityScanCommand
15725
17723
  });
15726
- import { readFileSync as readFileSync33, existsSync as existsSync35 } from "fs";
15727
- import { join as join34 } from "path";
15728
- import { homedir as homedir34 } from "os";
17724
+ import { readFileSync as readFileSync36, existsSync as existsSync39 } from "fs";
17725
+ import { join as join39 } from "path";
17726
+ import { homedir as homedir40 } from "os";
15729
17727
  import { execFileSync as execFileSync6 } from "child_process";
15730
17728
  function readConfigEnv4() {
15731
- const p = join34(SYNKRO_DIR15, "config.env");
15732
- if (!existsSync35(p)) return {};
17729
+ const p = join39(SYNKRO_DIR15, "config.env");
17730
+ if (!existsSync39(p)) return {};
15733
17731
  const out = {};
15734
- for (const line of readFileSync33(p, "utf-8").split("\n")) {
17732
+ for (const line of readFileSync36(p, "utf-8").split("\n")) {
15735
17733
  const t = line.trim();
15736
17734
  if (!t || t.startsWith("#")) continue;
15737
17735
  const eq = t.indexOf("=");
@@ -15763,11 +17761,11 @@ async function pushToCloud(cfg, repo) {
15763
17761
  while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
15764
17762
  let jwt2 = "";
15765
17763
  try {
15766
- jwt2 = readFileSync33(join34(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
17764
+ jwt2 = readFileSync36(join39(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
15767
17765
  } catch {
15768
17766
  }
15769
- if (!jwt2 || !existsSync35(REACHABILITY_PATH)) return;
15770
- const body = readFileSync33(REACHABILITY_PATH, "utf-8");
17767
+ if (!jwt2 || !existsSync39(REACHABILITY_PATH)) return;
17768
+ const body = readFileSync36(REACHABILITY_PATH, "utf-8");
15771
17769
  try {
15772
17770
  const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
15773
17771
  method: "POST",
@@ -15799,7 +17797,7 @@ var init_reachabilityScan2 = __esm({
15799
17797
  "cli/commands/reachabilityScan.ts"() {
15800
17798
  "use strict";
15801
17799
  init_reachabilityScan();
15802
- SYNKRO_DIR15 = join34(homedir34(), ".synkro");
17800
+ SYNKRO_DIR15 = join39(homedir40(), ".synkro");
15803
17801
  }
15804
17802
  });
15805
17803
 
@@ -15929,13 +17927,13 @@ var config_exports = {};
15929
17927
  __export(config_exports, {
15930
17928
  configCommand: () => configCommand
15931
17929
  });
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";
17930
+ import { readFileSync as readFileSync37, writeFileSync as writeFileSync28, existsSync as existsSync40 } from "fs";
17931
+ import { join as join40 } from "path";
17932
+ import { homedir as homedir41 } from "os";
15935
17933
  function readConfigEnv5() {
15936
- if (!existsSync36(CONFIG_PATH9)) return {};
17934
+ if (!existsSync40(CONFIG_PATH9)) return {};
15937
17935
  const out = {};
15938
- for (const line of readFileSync34(CONFIG_PATH9, "utf-8").split("\n")) {
17936
+ for (const line of readFileSync37(CONFIG_PATH9, "utf-8").split("\n")) {
15939
17937
  const t = line.trim();
15940
17938
  if (!t || t.startsWith("#")) continue;
15941
17939
  const eq = t.indexOf("=");
@@ -15944,11 +17942,11 @@ function readConfigEnv5() {
15944
17942
  return out;
15945
17943
  }
15946
17944
  function updateConfigValue(key, value) {
15947
- if (!existsSync36(CONFIG_PATH9)) {
17945
+ if (!existsSync40(CONFIG_PATH9)) {
15948
17946
  console.error("No config found. Run `synkro install` first.");
15949
17947
  process.exit(1);
15950
17948
  }
15951
- const lines = readFileSync34(CONFIG_PATH9, "utf-8").split("\n");
17949
+ const lines = readFileSync37(CONFIG_PATH9, "utf-8").split("\n");
15952
17950
  const pattern = new RegExp(`^${key}=`);
15953
17951
  let found = false;
15954
17952
  const updated = lines.map((line) => {
@@ -15959,7 +17957,7 @@ function updateConfigValue(key, value) {
15959
17957
  return line;
15960
17958
  });
15961
17959
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
15962
- writeFileSync24(CONFIG_PATH9, updated.join("\n"), "utf-8");
17960
+ writeFileSync28(CONFIG_PATH9, updated.join("\n"), "utf-8");
15963
17961
  }
15964
17962
  function resolveInferenceMode(cfg) {
15965
17963
  if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
@@ -16117,8 +18115,8 @@ var init_config = __esm({
16117
18115
  "use strict";
16118
18116
  init_stub();
16119
18117
  init_optout();
16120
- SYNKRO_DIR16 = join35(homedir35(), ".synkro");
16121
- CONFIG_PATH9 = join35(SYNKRO_DIR16, "config.env");
18118
+ SYNKRO_DIR16 = join40(homedir41(), ".synkro");
18119
+ CONFIG_PATH9 = join40(SYNKRO_DIR16, "config.env");
16122
18120
  }
16123
18121
  });
16124
18122
 
@@ -16127,7 +18125,7 @@ var telemetry_exports2 = {};
16127
18125
  __export(telemetry_exports2, {
16128
18126
  telemetryCommand: () => telemetryCommand
16129
18127
  });
16130
- import { createInterface as createInterface5 } from "readline";
18128
+ import { createInterface as createInterface6 } from "readline";
16131
18129
  function parseFlag(args2, name) {
16132
18130
  const prefix = `--${name}=`;
16133
18131
  for (const a of args2) if (a.startsWith(prefix)) return a.slice(prefix.length);
@@ -16209,7 +18207,7 @@ async function runExport(args2) {
16209
18207
  function confirmYesNo(question) {
16210
18208
  if (!process.stdin.isTTY) return Promise.resolve(false);
16211
18209
  return new Promise((resolve7) => {
16212
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
18210
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
16213
18211
  rl.question(`${question} (y/N): `, (answer) => {
16214
18212
  rl.close();
16215
18213
  const t = answer.trim().toLowerCase();
@@ -16308,11 +18306,11 @@ Usage:
16308
18306
 
16309
18307
  // cli/inventory/identity.ts
16310
18308
  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";
18309
+ import { existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync38, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "fs";
18310
+ import { homedir as homedir42 } from "os";
18311
+ import { dirname as dirname12, join as join41 } from "path";
16314
18312
  function operationalIdentityPath() {
16315
- return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join36(homedir36(), ".synkro", "installation.json");
18313
+ return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join41(homedir42(), ".synkro", "installation.json");
16316
18314
  }
16317
18315
  function validIdentity(value) {
16318
18316
  if (!value || typeof value !== "object") return false;
@@ -16320,17 +18318,17 @@ function validIdentity(value) {
16320
18318
  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
18319
  }
16322
18320
  function writeIdentity(path, identity) {
16323
- mkdirSync20(dirname9(path), { recursive: true, mode: 448 });
18321
+ mkdirSync24(dirname12(path), { recursive: true, mode: 448 });
16324
18322
  const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
16325
- writeFileSync25(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18323
+ writeFileSync29(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
16326
18324
  renameSync9(temp, path);
16327
18325
  }
16328
18326
  function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
16329
18327
  const prior = cached4.get(path);
16330
18328
  if (prior) return prior;
16331
- if (existsSync37(path)) {
18329
+ if (existsSync41(path)) {
16332
18330
  try {
16333
- const parsed = JSON.parse(readFileSync35(path, "utf8"));
18331
+ const parsed = JSON.parse(readFileSync38(path, "utf8"));
16334
18332
  if (validIdentity(parsed)) {
16335
18333
  cached4.set(path, parsed);
16336
18334
  return parsed;
@@ -16355,13 +18353,13 @@ var init_identity2 = __esm({
16355
18353
  // cli/inventory/collector.ts
16356
18354
  import { createHash as createHash5 } from "crypto";
16357
18355
  import {
16358
- existsSync as existsSync38,
16359
- readFileSync as readFileSync36,
16360
- readdirSync as readdirSync10,
18356
+ existsSync as existsSync42,
18357
+ readFileSync as readFileSync39,
18358
+ readdirSync as readdirSync11,
16361
18359
  statSync as statSync5
16362
18360
  } 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";
18361
+ import { arch, homedir as homedir43, hostname as hostname2, platform as platform6, release as release2 } from "os";
18362
+ import { basename as basename3, join as join42, relative, resolve as resolve5 } from "path";
16365
18363
  import { fileURLToPath } from "url";
16366
18364
  function sha256(value) {
16367
18365
  return createHash5("sha256").update(value).digest("hex");
@@ -16371,15 +18369,15 @@ function pseudonymousHostnameHash(installationId, host) {
16371
18369
  }
16372
18370
  function cliVersion() {
16373
18371
  try {
16374
- return "1.10.3";
18372
+ return "1.10.5";
16375
18373
  } catch {
16376
18374
  return "0.0.0";
16377
18375
  }
16378
18376
  }
16379
18377
  function readJson(path) {
16380
18378
  try {
16381
- if (!existsSync38(path)) return null;
16382
- const parsed = JSON.parse(readFileSync36(path, "utf8"));
18379
+ if (!existsSync42(path)) return null;
18380
+ const parsed = JSON.parse(readFileSync39(path, "utf8"));
16383
18381
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
16384
18382
  } catch {
16385
18383
  return null;
@@ -16387,8 +18385,8 @@ function readJson(path) {
16387
18385
  }
16388
18386
  function readText(path) {
16389
18387
  try {
16390
- if (!existsSync38(path)) return "";
16391
- return readFileSync36(path, "utf8");
18388
+ if (!existsSync42(path)) return "";
18389
+ return readFileSync39(path, "utf8");
16392
18390
  } catch {
16393
18391
  return "";
16394
18392
  }
@@ -16474,16 +18472,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
16474
18472
  }
16475
18473
  function claudeDesktopConfigCandidates(home, targetPlatform) {
16476
18474
  if (targetPlatform === "darwin") {
16477
- return [join37(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
18475
+ return [join42(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
16478
18476
  }
16479
18477
  if (targetPlatform === "linux") {
16480
18478
  return [
16481
- join37(home, ".config", "Claude", "claude_desktop_config.json"),
16482
- join37(home, ".config", "claude", "claude_desktop_config.json")
18479
+ join42(home, ".config", "Claude", "claude_desktop_config.json"),
18480
+ join42(home, ".config", "claude", "claude_desktop_config.json")
16483
18481
  ];
16484
18482
  }
16485
18483
  if (targetPlatform === "win32" && process.env.APPDATA) {
16486
- return [join37(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
18484
+ return [join42(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
16487
18485
  }
16488
18486
  return [];
16489
18487
  }
@@ -16491,7 +18489,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
16491
18489
  if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
16492
18490
  if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
16493
18491
  if (targetPlatform === "win32" && process.env.ProgramFiles) {
16494
- return [join37(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
18492
+ return [join42(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
16495
18493
  }
16496
18494
  return [];
16497
18495
  }
@@ -16500,7 +18498,7 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
16500
18498
  const add = (value) => {
16501
18499
  if (typeof value !== "string" || !value.trim()) return;
16502
18500
  const path = resolve5(value);
16503
- if (existsSync38(path)) roots.add(path);
18501
+ if (existsSync42(path)) roots.add(path);
16504
18502
  };
16505
18503
  add(currentDirectory);
16506
18504
  for (const path of explicit) add(path);
@@ -16511,31 +18509,31 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
16511
18509
  return [...roots];
16512
18510
  }
16513
18511
  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")];
18512
+ if (targetPlatform === "darwin") return [join42(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
18513
+ if (targetPlatform === "linux") return [join42(home, ".config", "Cursor", "User", "workspaceStorage")];
16516
18514
  if (targetPlatform === "win32" && process.env.APPDATA) {
16517
- return [join37(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
18515
+ return [join42(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
16518
18516
  }
16519
18517
  return [];
16520
18518
  }
16521
18519
  function cursorWorkspaceRoots(home, targetPlatform) {
16522
18520
  const roots = /* @__PURE__ */ new Set();
16523
18521
  for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
16524
- if (!existsSync38(storage)) continue;
18522
+ if (!existsSync42(storage)) continue;
16525
18523
  let entries = [];
16526
18524
  try {
16527
- entries = readdirSync10(storage, { withFileTypes: true });
18525
+ entries = readdirSync11(storage, { withFileTypes: true });
16528
18526
  } catch {
16529
18527
  continue;
16530
18528
  }
16531
18529
  for (const entry of entries) {
16532
18530
  if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
16533
- const state = readJson(join37(storage, entry.name, "workspace.json"));
18531
+ const state = readJson(join42(storage, entry.name, "workspace.json"));
16534
18532
  const raw = state?.folder;
16535
18533
  if (typeof raw !== "string" || !raw.trim()) continue;
16536
18534
  try {
16537
18535
  const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
16538
- if (existsSync38(path)) roots.add(resolve5(path));
18536
+ if (existsSync42(path)) roots.add(resolve5(path));
16539
18537
  } catch {
16540
18538
  }
16541
18539
  }
@@ -16629,18 +18627,18 @@ function parseFrontmatter(content) {
16629
18627
  return { name: value("name"), version: value("version") };
16630
18628
  }
16631
18629
  function skillArtifacts(harness, root) {
16632
- if (!existsSync38(root)) return [];
18630
+ if (!existsSync42(root)) return [];
16633
18631
  const manifests = [];
16634
18632
  const visit = (dir) => {
16635
18633
  let entries;
16636
18634
  try {
16637
- entries = readdirSync10(dir, { withFileTypes: true });
18635
+ entries = readdirSync11(dir, { withFileTypes: true });
16638
18636
  } catch {
16639
18637
  return;
16640
18638
  }
16641
18639
  for (const entry of entries) {
16642
18640
  if (entry.isSymbolicLink?.()) continue;
16643
- const path = join37(dir, entry.name);
18641
+ const path = join42(dir, entry.name);
16644
18642
  if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
16645
18643
  else if (entry.isDirectory()) visit(path);
16646
18644
  }
@@ -16650,7 +18648,7 @@ function skillArtifacts(harness, root) {
16650
18648
  const content = readText(path);
16651
18649
  const frontmatter = parseFrontmatter(content);
16652
18650
  const rel = relative(root, path).replaceAll("\\", "/");
16653
- const name = frontmatter.name || basename3(join37(path, "..")) || "skill";
18651
+ const name = frontmatter.name || basename3(join42(path, "..")) || "skill";
16654
18652
  return {
16655
18653
  harness,
16656
18654
  type: "skill",
@@ -16665,16 +18663,16 @@ function skillArtifacts(harness, root) {
16665
18663
  });
16666
18664
  }
16667
18665
  function cursorExtensionArtifacts(root) {
16668
- if (!existsSync38(root)) return [];
18666
+ if (!existsSync42(root)) return [];
16669
18667
  let dirs = [];
16670
18668
  try {
16671
- dirs = readdirSync10(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
18669
+ dirs = readdirSync11(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
16672
18670
  } catch {
16673
18671
  return [];
16674
18672
  }
16675
18673
  const artifacts = [];
16676
18674
  for (const dir of dirs) {
16677
- const pkg = readJson(join37(root, dir.name, "package.json"));
18675
+ const pkg = readJson(join42(root, dir.name, "package.json"));
16678
18676
  if (!pkg) continue;
16679
18677
  const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
16680
18678
  const name = typeof pkg.name === "string" ? pkg.name : dir.name;
@@ -16694,21 +18692,21 @@ function cursorExtensionArtifacts(root) {
16694
18692
  return artifacts;
16695
18693
  }
16696
18694
  function deploymentMode2(home) {
16697
- const raw = readText(join37(home, ".synkro", "config.env"));
18695
+ const raw = readText(join42(home, ".synkro", "config.env"));
16698
18696
  const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
16699
18697
  if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
16700
18698
  if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
16701
18699
  return "local";
16702
18700
  }
16703
18701
  function telemetryHealth(home) {
16704
- const meta = readJson(join37(home, ".synkro", "telemetry-meta.json"));
18702
+ const meta = readJson(join42(home, ".synkro", "telemetry-meta.json"));
16705
18703
  const health = {};
16706
18704
  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
18705
  if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
16708
- const queue = join37(home, ".synkro", "telemetry-pending.jsonl");
18706
+ const queue = join42(home, ".synkro", "telemetry-pending.jsonl");
16709
18707
  try {
16710
18708
  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);
18709
+ health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync39(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
16712
18710
  } catch {
16713
18711
  }
16714
18712
  return health;
@@ -16749,7 +18747,7 @@ function harnessSnapshot(agent) {
16749
18747
  }
16750
18748
  const config = readJson(agent.settingsPath);
16751
18749
  const coverage = inspectCodexHooks(agent.settingsPath);
16752
- const toml = readText(join37(agent.configDir, "config.toml"));
18750
+ const toml = readText(join42(agent.configDir, "config.toml"));
16753
18751
  const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
16754
18752
  return {
16755
18753
  row: {
@@ -16766,11 +18764,11 @@ function harnessSnapshot(agent) {
16766
18764
  };
16767
18765
  }
16768
18766
  function collectOperationalInventory(options = {}) {
16769
- const home = options.homeDir ?? homedir37();
18767
+ const home = options.homeDir ?? homedir43();
16770
18768
  const detected = options.detectedAgents ?? detectAgents();
16771
18769
  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");
18770
+ const targetPlatform = options.platformName ?? platform6();
18771
+ const codexHome = options.homeDir ? join42(home, ".codex") : process.env.CODEX_HOME || join42(home, ".codex");
16774
18772
  const harnesses = [];
16775
18773
  const artifacts = [];
16776
18774
  for (const agent of detected) {
@@ -16778,7 +18776,7 @@ function collectOperationalInventory(options = {}) {
16778
18776
  harnesses.push(row2);
16779
18777
  artifacts.push(...hookArtifacts(row2.harness, config));
16780
18778
  }
16781
- const claudeJson = readJson(join37(home, ".claude.json"));
18779
+ const claudeJson = readJson(join42(home, ".claude.json"));
16782
18780
  artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
16783
18781
  if (claudeJson?.projects && typeof claudeJson.projects === "object") {
16784
18782
  for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
@@ -16786,8 +18784,8 @@ function collectOperationalInventory(options = {}) {
16786
18784
  artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
16787
18785
  }
16788
18786
  }
16789
- artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join37(home, ".cursor", "mcp.json"))));
16790
- artifacts.push(...codexMcpArtifacts(readText(join37(codexHome, "config.toml"))));
18787
+ artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join42(home, ".cursor", "mcp.json"))));
18788
+ artifacts.push(...codexMcpArtifacts(readText(join42(codexHome, "config.toml"))));
16791
18789
  const projectRoots = discoveredProjectRoots(
16792
18790
  claudeJson,
16793
18791
  options.currentDirectory ?? process.cwd(),
@@ -16798,11 +18796,11 @@ function collectOperationalInventory(options = {}) {
16798
18796
  const scopeHash = sha256(projectRoot).slice(0, 16);
16799
18797
  artifacts.push(...mcpArtifactsFromJson(
16800
18798
  "claude_code",
16801
- readJson(join37(projectRoot, ".mcp.json")),
18799
+ readJson(join42(projectRoot, ".mcp.json")),
16802
18800
  `project:${scopeHash}`
16803
18801
  ));
16804
- const cursorProjectConfig = join37(projectRoot, ".cursor", "mcp.json");
16805
- if (resolve5(cursorProjectConfig) !== resolve5(join37(home, ".cursor", "mcp.json"))) {
18802
+ const cursorProjectConfig = join42(projectRoot, ".cursor", "mcp.json");
18803
+ if (resolve5(cursorProjectConfig) !== resolve5(join42(home, ".cursor", "mcp.json"))) {
16806
18804
  artifacts.push(...mcpArtifactsFromJson(
16807
18805
  "cursor",
16808
18806
  readJson(cursorProjectConfig),
@@ -16813,7 +18811,7 @@ function collectOperationalInventory(options = {}) {
16813
18811
  for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
16814
18812
  artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
16815
18813
  }
16816
- const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync38(path));
18814
+ const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync42(path));
16817
18815
  if (desktopConfigPath) {
16818
18816
  const desktopConfig = readJson(desktopConfigPath);
16819
18817
  harnesses.push({
@@ -16824,7 +18822,7 @@ function collectOperationalInventory(options = {}) {
16824
18822
  });
16825
18823
  artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
16826
18824
  }
16827
- const claudeSettings = readJson(join37(home, ".claude", "settings.json"));
18825
+ const claudeSettings = readJson(join42(home, ".claude", "settings.json"));
16828
18826
  if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
16829
18827
  for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
16830
18828
  artifacts.push({
@@ -16838,10 +18836,10 @@ function collectOperationalInventory(options = {}) {
16838
18836
  });
16839
18837
  }
16840
18838
  }
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")));
18839
+ artifacts.push(...skillArtifacts("claude_code", join42(home, ".claude", "skills")));
18840
+ artifacts.push(...skillArtifacts("cursor", join42(home, ".cursor", "skills")));
18841
+ artifacts.push(...skillArtifacts("codex", join42(codexHome, "skills")));
18842
+ artifacts.push(...cursorExtensionArtifacts(join42(home, ".cursor", "extensions")));
16845
18843
  const uniqueArtifacts = /* @__PURE__ */ new Map();
16846
18844
  for (const artifact of artifacts) {
16847
18845
  const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
@@ -16859,7 +18857,7 @@ function collectOperationalInventory(options = {}) {
16859
18857
  install_id: identity.installation_id,
16860
18858
  hostname_hash: pseudonymousHostnameHash(identity.installation_id, hostname2()),
16861
18859
  platform: targetPlatform,
16862
- os_version: release(),
18860
+ os_version: release2(),
16863
18861
  arch: arch(),
16864
18862
  cli_version: cliVersion(),
16865
18863
  node_version: process.version,
@@ -16894,22 +18892,22 @@ __export(sync_exports2, {
16894
18892
  syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
16895
18893
  });
16896
18894
  import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
16897
- import { spawn as spawn8 } from "child_process";
18895
+ import { spawn as spawn10 } from "child_process";
16898
18896
  import {
16899
- existsSync as existsSync39,
16900
- mkdirSync as mkdirSync21,
16901
- readFileSync as readFileSync37,
18897
+ existsSync as existsSync43,
18898
+ mkdirSync as mkdirSync25,
18899
+ readFileSync as readFileSync40,
16902
18900
  renameSync as renameSync10,
16903
- writeFileSync as writeFileSync26
18901
+ writeFileSync as writeFileSync30
16904
18902
  } from "fs";
16905
- import { homedir as homedir38 } from "os";
16906
- import { dirname as dirname10, join as join38 } from "path";
18903
+ import { homedir as homedir44 } from "os";
18904
+ import { dirname as dirname13, join as join43 } from "path";
16907
18905
  function syncStatePath() {
16908
- return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join38(homedir38(), ".synkro", "inventory-sync.json");
18906
+ return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join43(homedir44(), ".synkro", "inventory-sync.json");
16909
18907
  }
16910
18908
  function readState(path = syncStatePath()) {
16911
18909
  try {
16912
- const parsed = JSON.parse(readFileSync37(path, "utf8"));
18910
+ const parsed = JSON.parse(readFileSync40(path, "utf8"));
16913
18911
  return parsed && typeof parsed === "object" ? parsed : {};
16914
18912
  } catch {
16915
18913
  return {};
@@ -16917,9 +18915,9 @@ function readState(path = syncStatePath()) {
16917
18915
  }
16918
18916
  function writeState(state, path = syncStatePath()) {
16919
18917
  try {
16920
- mkdirSync21(dirname10(path), { recursive: true, mode: 448 });
18918
+ mkdirSync25(dirname13(path), { recursive: true, mode: 448 });
16921
18919
  const temp = `${path}.${process.pid}.tmp`;
16922
- writeFileSync26(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
18920
+ writeFileSync30(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
16923
18921
  renameSync10(temp, path);
16924
18922
  } catch {
16925
18923
  }
@@ -16932,10 +18930,10 @@ function shouldSyncInventory(state, now = Date.now(), target) {
16932
18930
  return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
16933
18931
  }
16934
18932
  function readConfig() {
16935
- const path = join38(homedir38(), ".synkro", "config.env");
18933
+ const path = join43(homedir44(), ".synkro", "config.env");
16936
18934
  const out = {};
16937
18935
  try {
16938
- for (const rawLine of readFileSync37(path, "utf8").split("\n")) {
18936
+ for (const rawLine of readFileSync40(path, "utf8").split("\n")) {
16939
18937
  const line = rawLine.trim();
16940
18938
  if (!line || line.startsWith("#")) continue;
16941
18939
  const index = line.indexOf("=");
@@ -16974,7 +18972,7 @@ function resolveInventoryGateway(raw) {
16974
18972
  }
16975
18973
  async function loadToken() {
16976
18974
  try {
16977
- const durable = readFileSync37(join38(homedir38(), ".synkro", ".mcp-jwt"), "utf8").trim();
18975
+ const durable = readFileSync40(join43(homedir44(), ".synkro", ".mcp-jwt"), "utf8").trim();
16978
18976
  if (durable) return durable;
16979
18977
  } catch {
16980
18978
  }
@@ -17092,8 +19090,8 @@ function syncOperationalInventoryDetached() {
17092
19090
  writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
17093
19091
  try {
17094
19092
  const script = process.argv[1];
17095
- if (!script || !existsSync39(script)) return;
17096
- const child = spawn8(process.execPath, [script, "inventory-sync", "--detached"], {
19093
+ if (!script || !existsSync43(script)) return;
19094
+ const child = spawn10(process.execPath, [script, "inventory-sync", "--detached"], {
17097
19095
  detached: true,
17098
19096
  stdio: "ignore",
17099
19097
  env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
@@ -17116,14 +19114,15 @@ var init_sync2 = __esm({
17116
19114
  });
17117
19115
 
17118
19116
  // cli/bootstrap.js
17119
- import { readFileSync as readFileSync38, existsSync as existsSync40 } from "fs";
19117
+ import { readFileSync as readFileSync41, existsSync as existsSync44 } from "fs";
17120
19118
  import { resolve as resolve6 } from "path";
19119
+ process.title = "synkro";
17121
19120
  var envCandidates = [
17122
19121
  resolve6(process.env.HOME ?? "", ".synkro", "config.env")
17123
19122
  ];
17124
19123
  for (const envPath of envCandidates) {
17125
- if (!existsSync40(envPath)) continue;
17126
- const envContent = readFileSync38(envPath, "utf-8");
19124
+ if (!existsSync44(envPath)) continue;
19125
+ const envContent = readFileSync41(envPath, "utf-8");
17127
19126
  for (const line of envContent.split("\n")) {
17128
19127
  const trimmed = line.trim();
17129
19128
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -17140,7 +19139,7 @@ var subArgs = args.slice(1);
17140
19139
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
17141
19140
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
17142
19141
  function printVersion() {
17143
- console.log("1.10.3");
19142
+ console.log("1.10.5");
17144
19143
  }
17145
19144
  function printHelp2() {
17146
19145
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents