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