@synkro-sh/cli 1.10.3 → 1.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +2506 -507
- 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.5";
|
|
151
151
|
} catch {
|
|
152
152
|
}
|
|
153
153
|
const creds = loadCredentialsIdentity();
|
|
@@ -7840,7 +7840,7 @@ var init_dockerInstall = __esm({
|
|
|
7840
7840
|
HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
|
|
7841
7841
|
CONTAINER_NAME = resolveContainerName();
|
|
7842
7842
|
defaultImageVersion = () => {
|
|
7843
|
-
if (true) return "1.10.
|
|
7843
|
+
if (true) return "1.10.5";
|
|
7844
7844
|
try {
|
|
7845
7845
|
const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
|
|
7846
7846
|
if (pkg.version) return pkg.version;
|
|
@@ -8900,7 +8900,7 @@ function writeConfigEnv(opts) {
|
|
|
8900
8900
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
|
|
8901
8901
|
`SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
|
|
8902
8902
|
`SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
|
|
8903
|
-
`SYNKRO_VERSION=${shellQuoteSingle2("1.10.
|
|
8903
|
+
`SYNKRO_VERSION=${shellQuoteSingle2("1.10.5")}`
|
|
8904
8904
|
];
|
|
8905
8905
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
|
|
8906
8906
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
|
|
@@ -9647,7 +9647,7 @@ async function installCommand(opts = {}) {
|
|
|
9647
9647
|
await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
|
|
9648
9648
|
emit("install", {
|
|
9649
9649
|
phase: "started",
|
|
9650
|
-
cli_version_to: "1.10.
|
|
9650
|
+
cli_version_to: "1.10.5",
|
|
9651
9651
|
agents_detected: agents.map((a) => a.kind),
|
|
9652
9652
|
with_github: false,
|
|
9653
9653
|
with_local_cc: false,
|
|
@@ -10630,15 +10630,15 @@ async function discoverAndIngestSkills() {
|
|
|
10630
10630
|
}
|
|
10631
10631
|
const found = discoverSkillFiles(repoRoot3, excludeHashes, ingestedHashes, ingestedNames);
|
|
10632
10632
|
if (found.length === 0) return;
|
|
10633
|
-
const
|
|
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,1657 @@ async function releaseAgent(runner, pueueId) {
|
|
|
14963
15426
|
await run(runner, buildRemove(pueueId)).catch?.(() => {
|
|
14964
15427
|
});
|
|
14965
15428
|
}
|
|
14966
|
-
var PUEUE_GROUP;
|
|
14967
|
-
var init_pueue2 = __esm({
|
|
14968
|
-
"cli/ui/pueue.ts"() {
|
|
15429
|
+
var PUEUE_GROUP;
|
|
15430
|
+
var init_pueue2 = __esm({
|
|
15431
|
+
"cli/ui/pueue.ts"() {
|
|
15432
|
+
"use strict";
|
|
15433
|
+
init_tmux();
|
|
15434
|
+
PUEUE_GROUP = "synkro-ui";
|
|
15435
|
+
}
|
|
15436
|
+
});
|
|
15437
|
+
|
|
15438
|
+
// cli/ui/backend.ts
|
|
15439
|
+
async function detectHarnesses() {
|
|
15440
|
+
const probe = await run({ kind: "host" }, [
|
|
15441
|
+
"sh",
|
|
15442
|
+
"-c",
|
|
15443
|
+
'for c in claude codex cursor-agent; do command -v "$c" >/dev/null 2>&1 && echo "$c"; done'
|
|
15444
|
+
]);
|
|
15445
|
+
const found = new Set(probe.stdout.split("\n").map((line) => line.trim()));
|
|
15446
|
+
const available = [];
|
|
15447
|
+
if (found.has("claude")) available.push("claude");
|
|
15448
|
+
if (found.has("codex")) available.push("codex");
|
|
15449
|
+
if (found.has("cursor-agent")) available.push("cursor");
|
|
15450
|
+
return available;
|
|
15451
|
+
}
|
|
15452
|
+
async function detectContainerBackend() {
|
|
15453
|
+
const runner = { kind: "container", container: CONTAINER_NAME2 };
|
|
15454
|
+
const probe = await run({ kind: "host" }, [
|
|
15455
|
+
"docker",
|
|
15456
|
+
"exec",
|
|
15457
|
+
CONTAINER_NAME2,
|
|
15458
|
+
"sh",
|
|
15459
|
+
"-c",
|
|
15460
|
+
"command -v tmux >/dev/null && command -v claude >/dev/null && ls " + shellQuote3(AUTH_SEED) + " >/dev/null 2>&1 && echo ready"
|
|
15461
|
+
]);
|
|
15462
|
+
if (probe.ok && probe.stdout.includes("ready")) {
|
|
15463
|
+
return { runner, backend: "container", note: "runtime: container (" + CONTAINER_NAME2 + ")" };
|
|
15464
|
+
}
|
|
15465
|
+
return { runner: { kind: "host" }, backend: "host", note: "runtime: host (container unavailable)" };
|
|
15466
|
+
}
|
|
15467
|
+
async function provisionContainerWorkspace(runner, slug) {
|
|
15468
|
+
const dir = CONTAINER_WORK + "/ui-" + slug;
|
|
15469
|
+
await run(runner, [
|
|
15470
|
+
"sh",
|
|
15471
|
+
"-c",
|
|
15472
|
+
"mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
|
|
15473
|
+
]);
|
|
15474
|
+
return dir;
|
|
15475
|
+
}
|
|
15476
|
+
async function spawnAgent(info, request) {
|
|
15477
|
+
const slug = slugify(request.name);
|
|
15478
|
+
const session = agentSession(slug);
|
|
15479
|
+
const runner = request.backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15480
|
+
let cwd = request.cwd;
|
|
15481
|
+
if (request.backend === "container") {
|
|
15482
|
+
cwd = request.cwd.startsWith(CONTAINER_WORK) ? request.cwd : await provisionContainerWorkspace(runner, slug);
|
|
15483
|
+
}
|
|
15484
|
+
const command = request.resume ? resumeCommand(request.harness) : HARNESS_COMMANDS[request.harness] || "claude";
|
|
15485
|
+
for (const argv of buildSpawnAgent({ name: slug, cwd, command, harness: request.harness, space: cwd, backend: request.backend })) {
|
|
15486
|
+
const result = await run(runner, argv);
|
|
15487
|
+
if (!result.ok && argv[1] === "new-session") {
|
|
15488
|
+
return { ok: false, session, error: result.stderr.trim() || "tmux new-session failed" };
|
|
15489
|
+
}
|
|
15490
|
+
}
|
|
15491
|
+
const pueueId = await registerAgent(runner, session);
|
|
15492
|
+
if (pueueId) await run(runner, buildSetOption(session, "@synkro_pueue", pueueId));
|
|
15493
|
+
recordSession({
|
|
15494
|
+
session,
|
|
15495
|
+
name: slug,
|
|
15496
|
+
harness: request.harness,
|
|
15497
|
+
space: cwd,
|
|
15498
|
+
spaceName: request.spaceName,
|
|
15499
|
+
backend: request.backend
|
|
15500
|
+
});
|
|
15501
|
+
return { ok: true, session };
|
|
15502
|
+
}
|
|
15503
|
+
async function killAgent(backend, session, pueueId) {
|
|
15504
|
+
const runner = backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15505
|
+
await run(runner, buildKillSession(session));
|
|
15506
|
+
await releaseAgent(runner, pueueId);
|
|
15507
|
+
forgetSession(session);
|
|
15508
|
+
}
|
|
15509
|
+
var CONTAINER_NAME2, CONTAINER_WORK, AUTH_SEED, HARNESS_COMMANDS;
|
|
15510
|
+
var init_backend = __esm({
|
|
15511
|
+
"cli/ui/backend.ts"() {
|
|
15512
|
+
"use strict";
|
|
15513
|
+
init_tmux();
|
|
15514
|
+
init_pueue2();
|
|
15515
|
+
init_manifest();
|
|
15516
|
+
CONTAINER_NAME2 = "synkro-server";
|
|
15517
|
+
CONTAINER_WORK = "/home/synkro/work";
|
|
15518
|
+
AUTH_SEED = CONTAINER_WORK + "/claude-1/.claude.json";
|
|
15519
|
+
HARNESS_COMMANDS = {
|
|
15520
|
+
claude: "claude",
|
|
15521
|
+
codex: "codex",
|
|
15522
|
+
cursor: "cursor-agent"
|
|
15523
|
+
};
|
|
15524
|
+
}
|
|
15525
|
+
});
|
|
15526
|
+
|
|
15527
|
+
// cli/ui/awake.ts
|
|
15528
|
+
import { spawn as spawn8 } from "child_process";
|
|
15529
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync32, rmSync as rmSync6, writeFileSync as writeFileSync26 } from "fs";
|
|
15530
|
+
import { homedir as homedir35, platform as platform5 } from "os";
|
|
15531
|
+
import { dirname as dirname11, join as join35 } from "path";
|
|
15532
|
+
function shouldStayAwake(agents) {
|
|
15533
|
+
return agents.some((agent) => agent.status === "working");
|
|
15534
|
+
}
|
|
15535
|
+
function awakeCommand(os = platform5()) {
|
|
15536
|
+
if (os === "darwin") return ["caffeinate", "-i", "-s"];
|
|
15537
|
+
if (os === "linux") {
|
|
15538
|
+
return ["systemd-inhibit", "--what=idle:sleep", "--who=synkro", "--why=agent session running", "sleep", "infinity"];
|
|
15539
|
+
}
|
|
15540
|
+
return null;
|
|
15541
|
+
}
|
|
15542
|
+
function livePid() {
|
|
15543
|
+
try {
|
|
15544
|
+
const pid = Number(readFileSync32(PIDFILE, "utf8").trim());
|
|
15545
|
+
if (!pid) return 0;
|
|
15546
|
+
process.kill(pid, 0);
|
|
15547
|
+
return pid;
|
|
15548
|
+
} catch {
|
|
15549
|
+
return 0;
|
|
15550
|
+
}
|
|
15551
|
+
}
|
|
15552
|
+
function release() {
|
|
15553
|
+
const pid = livePid();
|
|
15554
|
+
if (pid) {
|
|
15555
|
+
try {
|
|
15556
|
+
process.kill(pid);
|
|
15557
|
+
} catch {
|
|
15558
|
+
}
|
|
15559
|
+
}
|
|
15560
|
+
try {
|
|
15561
|
+
if (existsSync35(PIDFILE)) rmSync6(PIDFILE);
|
|
15562
|
+
} catch {
|
|
15563
|
+
}
|
|
15564
|
+
}
|
|
15565
|
+
function hold() {
|
|
15566
|
+
if (livePid()) return;
|
|
15567
|
+
const command = awakeCommand();
|
|
15568
|
+
if (!command) return;
|
|
15569
|
+
try {
|
|
15570
|
+
const child = spawn8(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
15571
|
+
child.unref();
|
|
15572
|
+
if (!child.pid) return;
|
|
15573
|
+
mkdirSync23(dirname11(PIDFILE), { recursive: true });
|
|
15574
|
+
writeFileSync26(PIDFILE, String(child.pid));
|
|
15575
|
+
} catch {
|
|
15576
|
+
}
|
|
15577
|
+
}
|
|
15578
|
+
function syncAwake(active, now = Date.now()) {
|
|
15579
|
+
if (active) {
|
|
15580
|
+
lastActive = now;
|
|
15581
|
+
hold();
|
|
15582
|
+
return true;
|
|
15583
|
+
}
|
|
15584
|
+
if (lastActive > 0 && now - lastActive < GRACE_MS) return livePid() > 0;
|
|
15585
|
+
lastActive = 0;
|
|
15586
|
+
release();
|
|
15587
|
+
return false;
|
|
15588
|
+
}
|
|
15589
|
+
function releaseAwake() {
|
|
15590
|
+
release();
|
|
15591
|
+
}
|
|
15592
|
+
var PIDFILE, GRACE_MS, lastActive;
|
|
15593
|
+
var init_awake = __esm({
|
|
15594
|
+
"cli/ui/awake.ts"() {
|
|
15595
|
+
"use strict";
|
|
15596
|
+
PIDFILE = join35(homedir35(), ".synkro", "ui-awake.pid");
|
|
15597
|
+
GRACE_MS = 9e4;
|
|
15598
|
+
lastActive = 0;
|
|
15599
|
+
}
|
|
15600
|
+
});
|
|
15601
|
+
|
|
15602
|
+
// cli/ui/sidebar.ts
|
|
15603
|
+
function runnerFor(backend) {
|
|
15604
|
+
return backend === "container" ? { kind: "container", container: CONTAINER_NAME2 } : { kind: "host" };
|
|
15605
|
+
}
|
|
15606
|
+
async function runSidebar() {
|
|
15607
|
+
const centerPane = process.env.SYNKRO_UI_CENTER || "";
|
|
15608
|
+
const outerSession = process.env.SYNKRO_UI_OUTER || "synkro-ui";
|
|
15609
|
+
const bootPath = process.env.SYNKRO_UI_BOOT || process.argv[1];
|
|
15610
|
+
const repoCwd = process.env.SYNKRO_UI_REPO || process.cwd();
|
|
15611
|
+
const ownPane = process.env.TMUX_PANE || "";
|
|
15612
|
+
const [info, harnesses] = await Promise.all([detectContainerBackend(), detectHarnesses()]);
|
|
15613
|
+
const state = {
|
|
15614
|
+
spaces: [],
|
|
15615
|
+
agents: [],
|
|
15616
|
+
section: "spaces",
|
|
15617
|
+
spaceIndex: 0,
|
|
15618
|
+
agentIndex: 0,
|
|
15619
|
+
grouped: false,
|
|
15620
|
+
filter: "space",
|
|
15621
|
+
viewing: "",
|
|
15622
|
+
awake: false,
|
|
15623
|
+
collapsed: false,
|
|
15624
|
+
backendNote: info.note,
|
|
15625
|
+
message: ""
|
|
15626
|
+
};
|
|
15627
|
+
const COLLAPSED_COLS = 4;
|
|
15628
|
+
let allAgents = [];
|
|
15629
|
+
const repoCache = /* @__PURE__ */ new Map();
|
|
15630
|
+
async function agentRepo(path) {
|
|
15631
|
+
if (!path) return "";
|
|
15632
|
+
const cached5 = repoCache.get(path);
|
|
15633
|
+
if (cached5 !== void 0) return cached5;
|
|
15634
|
+
const root = await repoOf(path);
|
|
15635
|
+
repoCache.set(path, root);
|
|
15636
|
+
return root;
|
|
15637
|
+
}
|
|
15638
|
+
function applyFilter() {
|
|
15639
|
+
state.agents = visibleAgents(allAgents, state.spaces[state.spaceIndex], state.filter, state.grouped);
|
|
15640
|
+
state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
|
|
15641
|
+
}
|
|
15642
|
+
let memory = /* @__PURE__ */ new Map();
|
|
15643
|
+
let lastFrame = "";
|
|
15644
|
+
let lastTargets = [];
|
|
15645
|
+
let worldChanged = false;
|
|
15646
|
+
let boundSpace = false;
|
|
15647
|
+
const host = { kind: "host" };
|
|
15648
|
+
const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
|
|
15649
|
+
async function refresh() {
|
|
15650
|
+
const [hostSpaces, conductorTasks] = await Promise.all([
|
|
15651
|
+
discoverHostSpaces(repoCwd),
|
|
15652
|
+
fetchConductorTasks(CONDUCTOR_URL)
|
|
15653
|
+
]);
|
|
15654
|
+
state.spaces = hostSpaces;
|
|
15655
|
+
const hostAgents = await discoverAgents(host, "host", memory);
|
|
15656
|
+
const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", memory) : { agents: [], memory: /* @__PURE__ */ new Map() };
|
|
15657
|
+
memory = new Map([...hostAgents.memory, ...containerAgents.memory]);
|
|
15658
|
+
const live = [...hostAgents.agents, ...containerAgents.agents];
|
|
15659
|
+
const merged = [...live, ...offlineAgents(live, loadRecords())];
|
|
15660
|
+
allAgents = await Promise.all(merged.map(async (agent) => ({
|
|
15661
|
+
...agent,
|
|
15662
|
+
repo: await agentRepo(agent.space),
|
|
15663
|
+
linear: mapAgentToTask(agent.space, conductorTasks)
|
|
15664
|
+
})));
|
|
15665
|
+
const shown = await run(host, ["tmux", "display-message", "-p", "-t", ownPane, "#{@synkro_agent}#{l:|}#{@synkro_cwd}"]);
|
|
15666
|
+
const parts = (shown.ok ? shown.stdout.trim() : "").split("|");
|
|
15667
|
+
state.viewing = parts[0] || "";
|
|
15668
|
+
if (!boundSpace && state.spaces.length > 0 && parts[1]) {
|
|
15669
|
+
const tabRepo = await agentRepo(parts[1]);
|
|
15670
|
+
const index = state.spaces.findIndex((space) => space.path === tabRepo);
|
|
15671
|
+
if (index >= 0) state.spaceIndex = index;
|
|
15672
|
+
boundSpace = true;
|
|
15673
|
+
}
|
|
15674
|
+
state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
|
|
15675
|
+
applyFilter();
|
|
15676
|
+
state.awake = syncAwake(shouldStayAwake(allAgents));
|
|
15677
|
+
}
|
|
15678
|
+
function draw() {
|
|
15679
|
+
const rows = Number(process.stdout.rows || 42);
|
|
15680
|
+
const cols = Number(process.stdout.columns || 30);
|
|
15681
|
+
const layout = renderLayout(state, cols, rows);
|
|
15682
|
+
const frame = layout.lines.join("\n");
|
|
15683
|
+
lastTargets = layout.targets;
|
|
15684
|
+
if (frame === lastFrame) return;
|
|
15685
|
+
lastFrame = frame;
|
|
15686
|
+
process.stdout.write(CSI + "H" + frame);
|
|
15687
|
+
}
|
|
15688
|
+
function forceDraw() {
|
|
15689
|
+
lastFrame = "";
|
|
15690
|
+
draw();
|
|
15691
|
+
}
|
|
15692
|
+
let stripWidth = -1;
|
|
15693
|
+
async function syncChipStrip() {
|
|
15694
|
+
const asked = await run(host, ["tmux", "display-message", "-p", "-t", outerSession, "#{window_width}"]);
|
|
15695
|
+
const windowWidth = Number(asked.stdout.trim()) || Number(process.stdout.columns || 0);
|
|
15696
|
+
if (windowWidth <= 0) return;
|
|
15697
|
+
const target = state.collapsed ? COLLAPSED_COLS : Number(sidebarColumns(windowWidth));
|
|
15698
|
+
if (ownPane && Number(process.stdout.columns || 0) !== target) {
|
|
15699
|
+
await run(host, ["tmux", "resize-pane", "-t", ownPane, "-x", String(target)]);
|
|
15700
|
+
}
|
|
15701
|
+
if (target === stripWidth) return;
|
|
15702
|
+
stripWidth = target;
|
|
15703
|
+
await run(host, ["tmux", "set-option", "-t", outerSession, "status-format[0]", chipStrip(target)]);
|
|
15704
|
+
}
|
|
15705
|
+
async function setCollapsed(next) {
|
|
15706
|
+
if (state.collapsed === next) return;
|
|
15707
|
+
state.collapsed = next;
|
|
15708
|
+
stripWidth = -1;
|
|
15709
|
+
await run(host, ["tmux", "set-option", "-t", outerSession, "-q", "@synkro_collapsed", next ? "1" : "0"]);
|
|
15710
|
+
await syncChipStrip();
|
|
15711
|
+
redrawAfterResize();
|
|
15712
|
+
}
|
|
15713
|
+
async function toggleCollapsed() {
|
|
15714
|
+
await setCollapsed(!state.collapsed);
|
|
15715
|
+
}
|
|
15716
|
+
async function syncCollapsed() {
|
|
15717
|
+
const opt = await run(host, ["tmux", "show-options", "-t", outerSession, "-v", "@synkro_collapsed"]);
|
|
15718
|
+
await setCollapsed(opt.stdout.trim() === "1");
|
|
15719
|
+
}
|
|
15720
|
+
function redrawAfterResize() {
|
|
15721
|
+
process.stdout.write(CSI + "2J");
|
|
15722
|
+
forceDraw();
|
|
15723
|
+
void syncChipStrip();
|
|
15724
|
+
}
|
|
15725
|
+
async function attachedClient() {
|
|
15726
|
+
const listed = await run(host, ["tmux", "list-clients", "-t", outerSession, "-F", "#{client_name}"]);
|
|
15727
|
+
return listed.stdout.split("\n").map((line) => line.trim()).filter(Boolean)[0] || "";
|
|
15728
|
+
}
|
|
15729
|
+
function selectedSpace() {
|
|
15730
|
+
return state.spaces[state.spaceIndex] || state.spaces[0];
|
|
15731
|
+
}
|
|
15732
|
+
async function openDialog(kind, args2 = []) {
|
|
15733
|
+
const client = await attachedClient();
|
|
15734
|
+
if (!client) {
|
|
15735
|
+
state.message = "no attached client";
|
|
15736
|
+
return;
|
|
15737
|
+
}
|
|
15738
|
+
const cwd = selectedSpace()?.path || repoCwd;
|
|
15739
|
+
const opened = await run(host, buildDialogPopup({
|
|
15740
|
+
client,
|
|
15741
|
+
bootPath,
|
|
15742
|
+
kind,
|
|
15743
|
+
cwd,
|
|
15744
|
+
args: args2,
|
|
15745
|
+
width: "80%",
|
|
15746
|
+
height: "70%"
|
|
15747
|
+
}));
|
|
15748
|
+
if (!opened.ok) state.message = "dialog failed";
|
|
15749
|
+
worldChanged = true;
|
|
15750
|
+
}
|
|
15751
|
+
async function mainMenu() {
|
|
15752
|
+
await openDialog("menu", [outerSession, ownPane]);
|
|
15753
|
+
}
|
|
15754
|
+
function closeSelectedWorkspace() {
|
|
15755
|
+
const space = selectedSpace();
|
|
15756
|
+
if (!space || space.backend !== "host") return;
|
|
15757
|
+
forgetSpace(space.path);
|
|
15758
|
+
state.message = "closed " + space.name;
|
|
15759
|
+
}
|
|
15760
|
+
async function keybindsMenu() {
|
|
15761
|
+
await openDialog("keybinds");
|
|
15762
|
+
}
|
|
15763
|
+
async function restoreSelected() {
|
|
15764
|
+
const agent = state.agents[state.agentIndex];
|
|
15765
|
+
if (!agent || agent.status !== "offline") return;
|
|
15766
|
+
state.message = "restoring " + agent.name + "\u2026";
|
|
15767
|
+
forceDraw();
|
|
15768
|
+
const restored = await run(host, ["node", bootPath, "ui", "--restore", agent.session]);
|
|
15769
|
+
state.message = restored.ok ? "restored " + agent.name : "restore failed";
|
|
15770
|
+
}
|
|
15771
|
+
async function attachSelected() {
|
|
15772
|
+
const agent = state.agents[state.agentIndex];
|
|
15773
|
+
if (agent) await showAgent(agent);
|
|
15774
|
+
}
|
|
15775
|
+
async function showAgent(agent) {
|
|
15776
|
+
if (!centerPane) return;
|
|
15777
|
+
if (agent.status === "offline") {
|
|
15778
|
+
await restoreSelected();
|
|
15779
|
+
return;
|
|
15780
|
+
}
|
|
15781
|
+
const current = memory.get(agent.session);
|
|
15782
|
+
memory.set(agent.session, { hash: current?.hash || "", wasWorking: false, seen: true });
|
|
15783
|
+
if (agent.space) rememberLastAgent(await repoOf(agent.space), agent.session);
|
|
15784
|
+
state.viewing = agent.session;
|
|
15785
|
+
const windows = await run(host, ["tmux", "list-windows", "-t", outerSession, "-F", "#{window_id}|#{@synkro_agent}"]);
|
|
15786
|
+
const holder = windows.stdout.split("\n").map((line) => line.split("|")).find((cols) => cols[1] === agent.session);
|
|
15787
|
+
if (holder) {
|
|
15788
|
+
await run(host, ["tmux", "select-window", "-t", holder[0]]);
|
|
15789
|
+
state.message = agent.name;
|
|
15790
|
+
return;
|
|
15791
|
+
}
|
|
15792
|
+
await run(runnerFor(agent.backend), buildEnableMouse(agent.session));
|
|
15793
|
+
const command = buildCenterAttachCommand(runnerFor(agent.backend), agent.session);
|
|
15794
|
+
await run(host, ["tmux", "respawn-pane", "-k", "-t", centerPane, command]);
|
|
15795
|
+
const where = agent.space ? agent.space.split("/").filter(Boolean).pop() || "" : "";
|
|
15796
|
+
await run(host, ["tmux", "rename-window", "-t", centerPane, tabTitle(agent.harness, where)]);
|
|
15797
|
+
await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_agent", agent.session]);
|
|
15798
|
+
await run(host, ["tmux", "set-option", "-t", centerPane, "-w", "-q", "@synkro_kind", agent.harness]);
|
|
15799
|
+
await snapshotTabs();
|
|
15800
|
+
state.message = "attached " + agent.name;
|
|
15801
|
+
}
|
|
15802
|
+
async function confirmKillAgent() {
|
|
15803
|
+
const agent = state.agents[state.agentIndex];
|
|
15804
|
+
if (!agent) return;
|
|
15805
|
+
await openDialog("kill", [agent.backend, agent.session]);
|
|
15806
|
+
}
|
|
15807
|
+
async function consent(action) {
|
|
15808
|
+
const agent = state.agents[state.agentIndex];
|
|
15809
|
+
if (!agent || agent.status !== "blocked" || !agent.ask) return;
|
|
15810
|
+
if (!actionsForAsk(agent.ask).includes(action)) return;
|
|
15811
|
+
const runner = runnerFor(agent.backend);
|
|
15812
|
+
for (const argv of buildSendText(agent.session, CONSENT_PHRASES[action])) await run(runner, argv);
|
|
15813
|
+
state.message = action + " \u2192 " + agent.name;
|
|
15814
|
+
}
|
|
15815
|
+
function moveSelection(delta) {
|
|
15816
|
+
Object.assign(state, nextSelection(state, state.spaces.length, state.agents.length, delta));
|
|
15817
|
+
applyFilter();
|
|
15818
|
+
}
|
|
15819
|
+
async function activate(target) {
|
|
15820
|
+
if (target.kind === "space") {
|
|
15821
|
+
state.section = "spaces";
|
|
15822
|
+
state.spaceIndex = target.index;
|
|
15823
|
+
applyFilter();
|
|
15824
|
+
const space = state.spaces[target.index];
|
|
15825
|
+
const remembered = space ? lastAgentFor(space.path) : "";
|
|
15826
|
+
const agent = allAgents.find((row2) => row2.session === remembered && row2.status !== "offline");
|
|
15827
|
+
if (agent) await showAgent(agent);
|
|
15828
|
+
} else if (target.kind === "agent") {
|
|
15829
|
+
state.section = "agents";
|
|
15830
|
+
state.agentIndex = target.index;
|
|
15831
|
+
await attachSelected();
|
|
15832
|
+
} else if (target.kind === "new") await openDialog("new-tab");
|
|
15833
|
+
else if (target.kind === "menu") await mainMenu();
|
|
15834
|
+
else if (target.kind === "grouped") {
|
|
15835
|
+
state.filter = state.filter === "space" ? "all" : "space";
|
|
15836
|
+
applyFilter();
|
|
15837
|
+
} else if (target.kind === "collapse") await toggleCollapsed();
|
|
15838
|
+
}
|
|
15839
|
+
async function handleClick(x, y, rightButton) {
|
|
15840
|
+
const target = lastTargets[y - 1];
|
|
15841
|
+
if (!target) return;
|
|
15842
|
+
const cols = Number(process.stdout.columns || 30);
|
|
15843
|
+
if (target.kind === "new") {
|
|
15844
|
+
await (x > cols / 2 ? mainMenu() : openDialog("new-tab"));
|
|
15845
|
+
return;
|
|
15846
|
+
}
|
|
15847
|
+
if (target.kind === "grouped") {
|
|
15848
|
+
if (x > cols / 2) {
|
|
15849
|
+
state.filter = state.filter === "space" ? "all" : "space";
|
|
15850
|
+
applyFilter();
|
|
15851
|
+
}
|
|
15852
|
+
return;
|
|
15853
|
+
}
|
|
15854
|
+
if (rightButton) {
|
|
15855
|
+
if (target.kind === "space") {
|
|
15856
|
+
state.section = "spaces";
|
|
15857
|
+
state.spaceIndex = target.index;
|
|
15858
|
+
await openDialog("new-tab");
|
|
15859
|
+
}
|
|
15860
|
+
if (target.kind === "agent") {
|
|
15861
|
+
state.section = "agents";
|
|
15862
|
+
state.agentIndex = target.index;
|
|
15863
|
+
await confirmKillAgent();
|
|
15864
|
+
}
|
|
15865
|
+
return;
|
|
15866
|
+
}
|
|
15867
|
+
await activate(target);
|
|
15868
|
+
}
|
|
15869
|
+
async function handleKey(key) {
|
|
15870
|
+
if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
|
|
15871
|
+
else if (key === "j" || key === CSI + "B") moveSelection(1);
|
|
15872
|
+
else if (key === "k" || key === CSI + "A") moveSelection(-1);
|
|
15873
|
+
else if (key === "\r") {
|
|
15874
|
+
if (state.section === "agents") await attachSelected();
|
|
15875
|
+
else await openDialog("new-tab");
|
|
15876
|
+
} else if (key === "n" || key === "T") {
|
|
15877
|
+
worldChanged = true;
|
|
15878
|
+
await openDialog("new-tab");
|
|
15879
|
+
} else if (key === "m") await mainMenu();
|
|
15880
|
+
else if (key === "O") void openDialog("new-workspace");
|
|
15881
|
+
else if (key === "C") {
|
|
15882
|
+
worldChanged = true;
|
|
15883
|
+
closeSelectedWorkspace();
|
|
15884
|
+
} else if (key === "r") {
|
|
15885
|
+
worldChanged = true;
|
|
15886
|
+
await restoreSelected();
|
|
15887
|
+
} else if (key === "d") {
|
|
15888
|
+
const client = await attachedClient();
|
|
15889
|
+
await run(host, client ? ["tmux", "detach-client", "-t", client] : ["tmux", "detach-client"]);
|
|
15890
|
+
} else if (key === "K") await keybindsMenu();
|
|
15891
|
+
else if (key === "G") {
|
|
15892
|
+
state.grouped = !state.grouped;
|
|
15893
|
+
applyFilter();
|
|
15894
|
+
} else if (key === "a") {
|
|
15895
|
+
state.filter = state.filter === "space" ? "all" : "space";
|
|
15896
|
+
applyFilter();
|
|
15897
|
+
} else if (key === "<" || key === ">" || key === "," || key === ".") await toggleCollapsed();
|
|
15898
|
+
else if (key === "x") {
|
|
15899
|
+
worldChanged = true;
|
|
15900
|
+
await confirmKillAgent();
|
|
15901
|
+
} else if (key === "i") {
|
|
15902
|
+
const agent = state.agents[state.agentIndex];
|
|
15903
|
+
if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
|
|
15904
|
+
} else if (key === "g") await consent("track");
|
|
15905
|
+
else if (key === "s") await consent("skip");
|
|
15906
|
+
else if (key === "y") await consent("stay");
|
|
15907
|
+
else if (key === "q" || key === KEY_CTRL_C) {
|
|
15908
|
+
await snapshotTabs();
|
|
15909
|
+
releaseAwake();
|
|
15910
|
+
await run(host, ["tmux", "kill-session", "-t", outerSession]);
|
|
15911
|
+
process.exit(0);
|
|
15912
|
+
}
|
|
15913
|
+
}
|
|
15914
|
+
process.stdout.on("resize", redrawAfterResize);
|
|
15915
|
+
process.stdin.setRawMode?.(true);
|
|
15916
|
+
process.stdin.resume();
|
|
15917
|
+
process.stdout.write(CSI + "?1000h" + CSI + "?1006h");
|
|
15918
|
+
process.on("exit", () => process.stdout.write(CSI + "?1000l" + CSI + "?1006l"));
|
|
15919
|
+
let refreshing = false;
|
|
15920
|
+
async function refreshSafe() {
|
|
15921
|
+
if (refreshing) return;
|
|
15922
|
+
refreshing = true;
|
|
15923
|
+
try {
|
|
15924
|
+
await refresh();
|
|
15925
|
+
} finally {
|
|
15926
|
+
refreshing = false;
|
|
15927
|
+
}
|
|
15928
|
+
}
|
|
15929
|
+
const MOUSE = /\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
15930
|
+
process.stdin.on("data", (chunk) => {
|
|
15931
|
+
const input = chunk.toString("utf8");
|
|
15932
|
+
void (async () => {
|
|
15933
|
+
let sawMouse = false;
|
|
15934
|
+
let event;
|
|
15935
|
+
MOUSE.lastIndex = 0;
|
|
15936
|
+
while ((event = MOUSE.exec(input)) !== null) {
|
|
15937
|
+
sawMouse = true;
|
|
15938
|
+
const button = Number(event[1]);
|
|
15939
|
+
const x = Number(event[2]);
|
|
15940
|
+
const y = Number(event[3]);
|
|
15941
|
+
if (event[4] !== "M") continue;
|
|
15942
|
+
if (button === 0 || button === 2) await handleClick(x, y, button === 2);
|
|
15943
|
+
}
|
|
15944
|
+
if (!sawMouse) await handleKey(input);
|
|
15945
|
+
forceDraw();
|
|
15946
|
+
if (worldChanged) {
|
|
15947
|
+
worldChanged = false;
|
|
15948
|
+
void refreshSafe().then(draw);
|
|
15949
|
+
}
|
|
15950
|
+
})();
|
|
15951
|
+
});
|
|
15952
|
+
await refreshSafe();
|
|
15953
|
+
redrawAfterResize();
|
|
15954
|
+
await syncChipStrip();
|
|
15955
|
+
setInterval(() => {
|
|
15956
|
+
void syncCollapsed();
|
|
15957
|
+
void syncChipStrip();
|
|
15958
|
+
void refreshSafe().then(draw);
|
|
15959
|
+
}, POLL_MS);
|
|
15960
|
+
}
|
|
15961
|
+
var POLL_MS, CONDUCTOR_URL, CSI, KEY_CTRL_C;
|
|
15962
|
+
var init_sidebar = __esm({
|
|
15963
|
+
"cli/ui/sidebar.ts"() {
|
|
15964
|
+
"use strict";
|
|
15965
|
+
init_model();
|
|
15966
|
+
init_manifest();
|
|
15967
|
+
init_render();
|
|
15968
|
+
init_consent();
|
|
15969
|
+
init_backend();
|
|
15970
|
+
init_tmux();
|
|
15971
|
+
init_launch();
|
|
15972
|
+
init_awake();
|
|
15973
|
+
POLL_MS = 2e3;
|
|
15974
|
+
CONDUCTOR_URL = "http://127.0.0.1:" + (process.env.SYNKRO_HOST_MCP_PORT || "18931");
|
|
15975
|
+
CSI = "\x1B[";
|
|
15976
|
+
KEY_CTRL_C = "";
|
|
15977
|
+
}
|
|
15978
|
+
});
|
|
15979
|
+
|
|
15980
|
+
// cli/ui/repos.ts
|
|
15981
|
+
import { existsSync as existsSync36, readdirSync as readdirSync9 } from "fs";
|
|
15982
|
+
import { homedir as homedir36 } from "os";
|
|
15983
|
+
import { join as join36 } from "path";
|
|
15984
|
+
function isRepo(path) {
|
|
15985
|
+
return existsSync36(join36(path, ".git"));
|
|
15986
|
+
}
|
|
15987
|
+
function childDirectories(path) {
|
|
15988
|
+
try {
|
|
15989
|
+
return readdirSync9(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !SKIP.has(entry.name) && !entry.name.startsWith(".")).map((entry) => join36(path, entry.name));
|
|
15990
|
+
} catch {
|
|
15991
|
+
return [];
|
|
15992
|
+
}
|
|
15993
|
+
}
|
|
15994
|
+
function discoverRepos(extra = []) {
|
|
15995
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15996
|
+
const choices = [];
|
|
15997
|
+
const add = (path, note) => {
|
|
15998
|
+
const clean = path.replace(/\/+$/, "");
|
|
15999
|
+
if (!clean || seen.has(clean) || !isRepo(clean)) return;
|
|
16000
|
+
seen.add(clean);
|
|
16001
|
+
choices.push({ path: clean, name: clean.split("/").filter(Boolean).pop() || clean, note });
|
|
16002
|
+
};
|
|
16003
|
+
for (const path of extra) add(path, "current");
|
|
16004
|
+
for (const path of loadRepos()) add(path, "recent");
|
|
16005
|
+
const home = homedir36();
|
|
16006
|
+
for (const root of SCAN_ROOTS) {
|
|
16007
|
+
const base = root ? join36(home, root) : home;
|
|
16008
|
+
if (!existsSync36(base)) continue;
|
|
16009
|
+
if (isRepo(base)) {
|
|
16010
|
+
add(base, "");
|
|
16011
|
+
continue;
|
|
16012
|
+
}
|
|
16013
|
+
for (const child of childDirectories(base)) add(child, "");
|
|
16014
|
+
}
|
|
16015
|
+
return choices;
|
|
16016
|
+
}
|
|
16017
|
+
var SCAN_ROOTS, SKIP;
|
|
16018
|
+
var init_repos = __esm({
|
|
16019
|
+
"cli/ui/repos.ts"() {
|
|
16020
|
+
"use strict";
|
|
16021
|
+
init_manifest();
|
|
16022
|
+
SCAN_ROOTS = ["", "code", "src", "dev", "projects", "work", "repos", "git", "Developer", "Documents/GitHub"];
|
|
16023
|
+
SKIP = /* @__PURE__ */ new Set(["node_modules", "Library", "Applications", ".Trash", ".git"]);
|
|
16024
|
+
}
|
|
16025
|
+
});
|
|
16026
|
+
|
|
16027
|
+
// cli/ui/tabs.ts
|
|
16028
|
+
import { execSync as execSync7 } from "child_process";
|
|
16029
|
+
function repoRoot() {
|
|
16030
|
+
try {
|
|
16031
|
+
return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
|
|
16032
|
+
} catch {
|
|
16033
|
+
return process.cwd();
|
|
16034
|
+
}
|
|
16035
|
+
}
|
|
16036
|
+
async function createTab(bootPath, kind, spacePath) {
|
|
16037
|
+
const repo = repoRoot();
|
|
16038
|
+
const space = spacePath.split("/").filter(Boolean).pop() || "space";
|
|
16039
|
+
if (kind === "terminal") {
|
|
16040
|
+
await buildTab(bootPath, repo, {
|
|
16041
|
+
cwd: spacePath,
|
|
16042
|
+
center: makeTerminalCommand(bootPath),
|
|
16043
|
+
title: tabTitle("terminal", space),
|
|
16044
|
+
kind: "terminal",
|
|
16045
|
+
focus: "center"
|
|
16046
|
+
});
|
|
16047
|
+
return;
|
|
16048
|
+
}
|
|
16049
|
+
if (kind === "cursor-synkro") {
|
|
16050
|
+
await buildTab(bootPath, repo, {
|
|
16051
|
+
cwd: spacePath,
|
|
16052
|
+
center: ["node", bootPath, "ui", "--run", "cursor", spacePath].map(shellQuote3).join(" "),
|
|
16053
|
+
title: tabTitle("cursor", space),
|
|
16054
|
+
kind: "cursor-synkro",
|
|
16055
|
+
focus: "center"
|
|
16056
|
+
});
|
|
16057
|
+
return;
|
|
16058
|
+
}
|
|
16059
|
+
if (kind === "settings") {
|
|
16060
|
+
await buildTab(bootPath, repo, {
|
|
16061
|
+
cwd: spacePath,
|
|
16062
|
+
center: "sh -c " + shellQuote3('exec "${EDITOR:-vi}" synkro.toml'),
|
|
16063
|
+
title: tabTitle("settings", "settings"),
|
|
16064
|
+
kind: "settings",
|
|
16065
|
+
focus: "center"
|
|
16066
|
+
});
|
|
16067
|
+
return;
|
|
16068
|
+
}
|
|
16069
|
+
const info = await detectContainerBackend();
|
|
16070
|
+
const spaceName = space;
|
|
16071
|
+
const stamp = String(process.pid % 1e4);
|
|
16072
|
+
const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
|
|
16073
|
+
const reachable = info.backend === "container" && (await run(info.runner, ["test", "-d", spacePath])).ok;
|
|
16074
|
+
const backend = reachable ? "container" : "host";
|
|
16075
|
+
const spawned = await spawnAgent(info, {
|
|
16076
|
+
name: spaceName + "-" + harness + "-" + stamp,
|
|
16077
|
+
harness,
|
|
16078
|
+
spaceName,
|
|
16079
|
+
cwd: spacePath,
|
|
16080
|
+
backend
|
|
16081
|
+
});
|
|
16082
|
+
if (!spawned.ok) return;
|
|
16083
|
+
await buildTab(bootPath, repo, {
|
|
16084
|
+
cwd: spacePath,
|
|
16085
|
+
center: buildCenterAttachCommand(backend === "container" ? info.runner : HOST2, spawned.session),
|
|
16086
|
+
title: tabTitle(harness, spaceName),
|
|
16087
|
+
kind: harness,
|
|
16088
|
+
agentSession: spawned.session,
|
|
16089
|
+
focus: "center"
|
|
16090
|
+
});
|
|
16091
|
+
}
|
|
16092
|
+
async function openAgentTab(bootPath, session) {
|
|
16093
|
+
if (!session.startsWith("synkro-agent-")) return;
|
|
16094
|
+
const info = await detectContainerBackend();
|
|
16095
|
+
const meta = await run(HOST2, [
|
|
16096
|
+
"tmux",
|
|
16097
|
+
"display-message",
|
|
16098
|
+
"-p",
|
|
16099
|
+
"-t",
|
|
16100
|
+
session,
|
|
16101
|
+
["#{@synkro_harness}", "#{@synkro_space}", "#{@synkro_backend}"].join("|")
|
|
16102
|
+
]);
|
|
16103
|
+
const [harness, space, backend] = (meta.stdout.trim() || "||").split("|");
|
|
16104
|
+
const containerHosted = backend === "container";
|
|
16105
|
+
const runner = containerHosted ? info.runner : HOST2;
|
|
16106
|
+
const alive = (await run(runner, ["tmux", "has-session", "-t", session])).ok;
|
|
16107
|
+
if (!alive) return;
|
|
16108
|
+
const spaceName = (space || "").split("/").filter(Boolean).pop() || "space";
|
|
16109
|
+
await buildTab(bootPath, repoRoot(), {
|
|
16110
|
+
cwd: space || repoRoot(),
|
|
16111
|
+
center: buildCenterAttachCommand(runner, session),
|
|
16112
|
+
title: tabTitle(harness || "claude", spaceName),
|
|
16113
|
+
kind: harness || "claude",
|
|
16114
|
+
agentSession: session,
|
|
16115
|
+
focus: "center"
|
|
16116
|
+
});
|
|
16117
|
+
}
|
|
16118
|
+
async function restoreTabs(bootPath, repoCwd) {
|
|
16119
|
+
const tabs = loadTabs();
|
|
16120
|
+
if (tabs.length === 0) return false;
|
|
16121
|
+
const info = await detectContainerBackend();
|
|
16122
|
+
const hostSessions = (await run(HOST2, ["tmux", "list-sessions", "-F", "#{session_name}"])).stdout;
|
|
16123
|
+
const containerSessions = info.backend === "container" ? (await run(info.runner, ["tmux", "list-sessions", "-F", "#{session_name}"])).stdout : "";
|
|
16124
|
+
const alive = new Set((hostSessions + "\n" + containerSessions).split("\n").map((line) => line.trim()).filter(Boolean));
|
|
16125
|
+
let built = false;
|
|
16126
|
+
for (const tab of tabs) {
|
|
16127
|
+
const space = tab.cwd.split("/").filter(Boolean).pop() || "space";
|
|
16128
|
+
const containerHosted = info.backend === "container" && (await run(info.runner, ["test", "-d", tab.cwd])).ok;
|
|
16129
|
+
const harness = ["claude", "codex", "cursor"].includes(tab.kind) ? tab.kind : "";
|
|
16130
|
+
if (tab.agentSession && alive.has(tab.agentSession)) {
|
|
16131
|
+
await run(containerHosted ? info.runner : HOST2, buildEnableMouse(tab.agentSession));
|
|
16132
|
+
await buildTab(bootPath, repoCwd, {
|
|
16133
|
+
cwd: tab.cwd,
|
|
16134
|
+
center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, tab.agentSession),
|
|
16135
|
+
title: tab.title,
|
|
16136
|
+
kind: tab.kind,
|
|
16137
|
+
agentSession: tab.agentSession,
|
|
16138
|
+
focus: "center"
|
|
16139
|
+
});
|
|
16140
|
+
} else if (harness) {
|
|
16141
|
+
const spawned = await spawnAgent(info, {
|
|
16142
|
+
name: space + "-" + harness + "-" + String(process.pid % 1e4),
|
|
16143
|
+
harness,
|
|
16144
|
+
spaceName: space,
|
|
16145
|
+
cwd: tab.cwd,
|
|
16146
|
+
backend: containerHosted ? "container" : "host",
|
|
16147
|
+
resume: true
|
|
16148
|
+
});
|
|
16149
|
+
await buildTab(bootPath, repoCwd, spawned.ok ? {
|
|
16150
|
+
cwd: tab.cwd,
|
|
16151
|
+
center: buildCenterAttachCommand(containerHosted ? info.runner : HOST2, spawned.session),
|
|
16152
|
+
title: tabTitle(harness, space),
|
|
16153
|
+
kind: harness,
|
|
16154
|
+
agentSession: spawned.session,
|
|
16155
|
+
focus: "center"
|
|
16156
|
+
} : { cwd: tab.cwd, center: makeTerminalCommand(bootPath), title: tabTitle("terminal", space), kind: "terminal", focus: "center" });
|
|
16157
|
+
} else {
|
|
16158
|
+
await buildTab(bootPath, repoCwd, {
|
|
16159
|
+
cwd: tab.cwd,
|
|
16160
|
+
center: makeTerminalCommand(bootPath),
|
|
16161
|
+
title: tabTitle("terminal", space),
|
|
16162
|
+
kind: "terminal",
|
|
16163
|
+
focus: "center"
|
|
16164
|
+
});
|
|
16165
|
+
}
|
|
16166
|
+
built = true;
|
|
16167
|
+
}
|
|
16168
|
+
return built;
|
|
16169
|
+
}
|
|
16170
|
+
var HOST2;
|
|
16171
|
+
var init_tabs = __esm({
|
|
16172
|
+
"cli/ui/tabs.ts"() {
|
|
16173
|
+
"use strict";
|
|
16174
|
+
init_launch();
|
|
16175
|
+
init_backend();
|
|
16176
|
+
init_manifest();
|
|
16177
|
+
init_tmux();
|
|
16178
|
+
HOST2 = { kind: "host" };
|
|
16179
|
+
}
|
|
16180
|
+
});
|
|
16181
|
+
|
|
16182
|
+
// cli/ui/dialog.ts
|
|
16183
|
+
import { existsSync as existsSync37 } from "fs";
|
|
16184
|
+
import { homedir as homedir37 } from "os";
|
|
16185
|
+
function write(text) {
|
|
16186
|
+
process.stdout.write(text);
|
|
16187
|
+
}
|
|
16188
|
+
function visibleLength2(text) {
|
|
16189
|
+
let visible = 0;
|
|
16190
|
+
let index = 0;
|
|
16191
|
+
while (index < text.length) {
|
|
16192
|
+
if (text.startsWith(CSI2, index)) {
|
|
16193
|
+
const end = text.indexOf("m", index);
|
|
16194
|
+
if (end === -1) break;
|
|
16195
|
+
index = end + 1;
|
|
16196
|
+
} else {
|
|
16197
|
+
visible += 1;
|
|
16198
|
+
index += 1;
|
|
16199
|
+
}
|
|
16200
|
+
}
|
|
16201
|
+
return visible;
|
|
16202
|
+
}
|
|
16203
|
+
function clipPath(text, max) {
|
|
16204
|
+
const value = String(text || "");
|
|
16205
|
+
if (max <= 1) return value.slice(0, Math.max(0, max));
|
|
16206
|
+
return value.length <= max ? value : "\u2026" + value.slice(value.length - max + 1);
|
|
16207
|
+
}
|
|
16208
|
+
function clip2(text, max) {
|
|
16209
|
+
const value = String(text || "");
|
|
16210
|
+
if (max <= 1) return value.slice(0, Math.max(0, max));
|
|
16211
|
+
return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
|
|
16212
|
+
}
|
|
16213
|
+
function padRow(text, width) {
|
|
16214
|
+
const gap = Math.max(0, width - visibleLength2(text));
|
|
16215
|
+
return text + " ".repeat(gap);
|
|
16216
|
+
}
|
|
16217
|
+
function selectable(text, width, selected) {
|
|
16218
|
+
const body = padRow(text, width);
|
|
16219
|
+
if (!selected) return body;
|
|
16220
|
+
return S.select + body.split(S.reset).join(S.reset + S.select) + S.reset;
|
|
16221
|
+
}
|
|
16222
|
+
function isPrintable(input) {
|
|
16223
|
+
if (!input) return false;
|
|
16224
|
+
for (const char of input) {
|
|
16225
|
+
const code = char.codePointAt(0) ?? 0;
|
|
16226
|
+
if (code < 32 || code === 127) return false;
|
|
16227
|
+
}
|
|
16228
|
+
return true;
|
|
16229
|
+
}
|
|
16230
|
+
function matches(choice, filter) {
|
|
16231
|
+
if (!filter) return true;
|
|
16232
|
+
const needle = filter.toLowerCase();
|
|
16233
|
+
return (choice.label + " " + (choice.detail || "")).toLowerCase().includes(needle);
|
|
16234
|
+
}
|
|
16235
|
+
async function pick(opts) {
|
|
16236
|
+
let selected = Math.min(Math.max(0, opts.initial || 0), Math.max(0, opts.choices.length - 1));
|
|
16237
|
+
let filter = "";
|
|
16238
|
+
let top = 0;
|
|
16239
|
+
const width = Math.max(20, Number(process.stdout.columns || 80));
|
|
16240
|
+
const height = Math.max(8, Number(process.stdout.rows || 20));
|
|
16241
|
+
const perItem = opts.choices.some((choice) => choice.detail) ? 2 : 1;
|
|
16242
|
+
const visibleItems = Math.max(1, Math.floor(Math.max(2, height - 5) / perItem));
|
|
16243
|
+
const firstRow = 4;
|
|
16244
|
+
return new Promise((resolve7) => {
|
|
16245
|
+
const view = () => opts.choices.filter((choice) => matches(choice, filter));
|
|
16246
|
+
const draw = () => {
|
|
16247
|
+
const shown = view();
|
|
16248
|
+
if (selected >= shown.length) selected = Math.max(0, shown.length - 1);
|
|
16249
|
+
if (selected < top) top = selected;
|
|
16250
|
+
if (selected >= top + visibleItems) top = selected - visibleItems + 1;
|
|
16251
|
+
const lines = [];
|
|
16252
|
+
lines.push(" " + S.title + opts.title + S.reset);
|
|
16253
|
+
const left = filter && !opts.menu ? " " + S.accent + "/ " + S.reset + filter + "\u258C" : " " + S.dim + opts.hint + S.reset;
|
|
16254
|
+
const right = opts.menu ? "" : S.dim + String(shown.length) + (shown.length === 1 ? " match" : " matches") + S.reset;
|
|
16255
|
+
lines.push(padRow(left, Math.max(0, width - visibleLength2(right) - 1)) + right);
|
|
16256
|
+
lines.push("");
|
|
16257
|
+
if (shown.length === 0) lines.push(" " + S.dim + (opts.emptyNote || "nothing matches") + S.reset);
|
|
16258
|
+
shown.slice(top, top + visibleItems).forEach((choice, offset) => {
|
|
16259
|
+
const index = top + offset;
|
|
16260
|
+
const isSelected = index === selected;
|
|
16261
|
+
const marker = isSelected ? S.accent + "\u203A" + S.reset + " " : " ";
|
|
16262
|
+
const note = choice.note ? S.dim + choice.note + S.reset : "";
|
|
16263
|
+
const head = " " + marker + S.bold + clip2(choice.label, width - 6 - visibleLength2(note)) + S.reset;
|
|
16264
|
+
lines.push(selectable(padRow(head, Math.max(0, width - visibleLength2(note) - 1)) + note, width, isSelected));
|
|
16265
|
+
if (perItem === 2) {
|
|
16266
|
+
lines.push(selectable(" " + S.muted + clipPath(choice.detail || "", width - 6) + S.reset, width, isSelected));
|
|
16267
|
+
}
|
|
16268
|
+
});
|
|
16269
|
+
while (lines.length < height - 1) lines.push("");
|
|
16270
|
+
lines.push(" " + S.dim + opts.footer + S.reset);
|
|
16271
|
+
write(CSI2 + "H" + lines.slice(0, height).map((line) => padRow(line, width)).join("\n"));
|
|
16272
|
+
};
|
|
16273
|
+
const rowOfItem = (index) => firstRow + (index - top) * perItem;
|
|
16274
|
+
const MOUSE = /\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
16275
|
+
const onData = (chunk) => {
|
|
16276
|
+
const input = chunk.toString("utf8");
|
|
16277
|
+
const shown = view();
|
|
16278
|
+
let sawMouse = false;
|
|
16279
|
+
let event;
|
|
16280
|
+
MOUSE.lastIndex = 0;
|
|
16281
|
+
while ((event = MOUSE.exec(input)) !== null) {
|
|
16282
|
+
sawMouse = true;
|
|
16283
|
+
if (event[4] !== "M") continue;
|
|
16284
|
+
const button = Number(event[1]);
|
|
16285
|
+
const y = Number(event[3]);
|
|
16286
|
+
if (button === 64) selected = Math.max(0, selected - 1);
|
|
16287
|
+
else if (button === 65) selected = Math.min(shown.length - 1, selected + 1);
|
|
16288
|
+
else if (button === 0) {
|
|
16289
|
+
const hit = shown.findIndex((_, index) => y >= rowOfItem(index) && y < rowOfItem(index) + perItem);
|
|
16290
|
+
if (hit >= 0) {
|
|
16291
|
+
process.stdin.off("data", onData);
|
|
16292
|
+
resolve7(shown[hit].value);
|
|
16293
|
+
return;
|
|
16294
|
+
}
|
|
16295
|
+
}
|
|
16296
|
+
}
|
|
16297
|
+
if (!sawMouse) {
|
|
16298
|
+
if (input === KEY_ESC && !input.includes("[<") || input === KEY_CTRL_C2) {
|
|
16299
|
+
process.stdin.off("data", onData);
|
|
16300
|
+
resolve7(null);
|
|
16301
|
+
return;
|
|
16302
|
+
}
|
|
16303
|
+
if (input === "\r") {
|
|
16304
|
+
if (shown.length === 0) return;
|
|
16305
|
+
process.stdin.off("data", onData);
|
|
16306
|
+
resolve7(shown[selected].value);
|
|
16307
|
+
return;
|
|
16308
|
+
}
|
|
16309
|
+
const down = input === CSI2 + "B" || opts.menu && (input === "j" || input === CSI2 + "C");
|
|
16310
|
+
const up = input === CSI2 + "A" || opts.menu && (input === "k" || input === CSI2 + "D");
|
|
16311
|
+
if (down) selected = Math.min(shown.length - 1, selected + 1);
|
|
16312
|
+
else if (up) selected = Math.max(0, selected - 1);
|
|
16313
|
+
else if (!opts.menu && (input === KEY_BACKSPACE || input === "\b")) filter = filter.slice(0, -1);
|
|
16314
|
+
else if (!opts.menu && isPrintable(input)) {
|
|
16315
|
+
filter += input;
|
|
16316
|
+
selected = 0;
|
|
16317
|
+
top = 0;
|
|
16318
|
+
}
|
|
16319
|
+
}
|
|
16320
|
+
draw();
|
|
16321
|
+
};
|
|
16322
|
+
process.stdin.on("data", onData);
|
|
16323
|
+
draw();
|
|
16324
|
+
});
|
|
16325
|
+
}
|
|
16326
|
+
async function readLine(opts) {
|
|
16327
|
+
let value = "";
|
|
16328
|
+
let error = "";
|
|
16329
|
+
const width = Math.max(20, Number(process.stdout.columns || 80));
|
|
16330
|
+
return new Promise((resolve7) => {
|
|
16331
|
+
const draw = () => {
|
|
16332
|
+
const lines = [
|
|
16333
|
+
" " + S.title + opts.title + S.reset,
|
|
16334
|
+
"",
|
|
16335
|
+
" " + S.dim + opts.label + S.reset,
|
|
16336
|
+
"",
|
|
16337
|
+
" " + S.accent + "> " + S.reset + value + "\u258C",
|
|
16338
|
+
"",
|
|
16339
|
+
error ? " " + S.error + error + S.reset : "",
|
|
16340
|
+
"",
|
|
16341
|
+
" " + S.dim + opts.footer + S.reset
|
|
16342
|
+
];
|
|
16343
|
+
write(CSI2 + "2J" + CSI2 + "H" + lines.map((line) => padRow(line, width)).join("\n"));
|
|
16344
|
+
};
|
|
16345
|
+
const onData = (chunk) => {
|
|
16346
|
+
const input = chunk.toString("utf8");
|
|
16347
|
+
if (input === KEY_ESC || input === KEY_CTRL_C2) {
|
|
16348
|
+
process.stdin.off("data", onData);
|
|
16349
|
+
resolve7(null);
|
|
16350
|
+
return;
|
|
16351
|
+
}
|
|
16352
|
+
if (input === "\r") {
|
|
16353
|
+
void (async () => {
|
|
16354
|
+
const problem = await opts.validate(value.trim());
|
|
16355
|
+
if (problem) {
|
|
16356
|
+
error = problem.slice(0, width - 4);
|
|
16357
|
+
draw();
|
|
16358
|
+
return;
|
|
16359
|
+
}
|
|
16360
|
+
process.stdin.off("data", onData);
|
|
16361
|
+
resolve7(value.trim());
|
|
16362
|
+
})();
|
|
16363
|
+
return;
|
|
16364
|
+
}
|
|
16365
|
+
if (input === KEY_BACKSPACE || input === "\b") value = value.slice(0, -1);
|
|
16366
|
+
else if (isPrintable(input)) value += input;
|
|
16367
|
+
draw();
|
|
16368
|
+
};
|
|
16369
|
+
process.stdin.on("data", onData);
|
|
16370
|
+
draw();
|
|
16371
|
+
});
|
|
16372
|
+
}
|
|
16373
|
+
async function pickRepo(repoCwd) {
|
|
16374
|
+
const current = canonicalSpacePath(repoCwd);
|
|
16375
|
+
const choices = discoverRepos(current ? [current] : []).map((repo) => ({
|
|
16376
|
+
value: repo.path,
|
|
16377
|
+
label: repo.name,
|
|
16378
|
+
detail: repo.path,
|
|
16379
|
+
note: repo.note
|
|
16380
|
+
}));
|
|
16381
|
+
choices.push({ value: TYPE_PATH, label: "other path\u2026", detail: "type a repository path" });
|
|
16382
|
+
const picked = await pick({
|
|
16383
|
+
title: "open repository",
|
|
16384
|
+
hint: "type to filter repositories",
|
|
16385
|
+
choices,
|
|
16386
|
+
footer: "\u2191\u2193 select \u21B5 next esc close",
|
|
16387
|
+
emptyNote: 'no repositories found \u2014 choose "other path\u2026"'
|
|
16388
|
+
});
|
|
16389
|
+
if (!picked) return null;
|
|
16390
|
+
if (picked !== TYPE_PATH) return picked;
|
|
16391
|
+
let resolved = null;
|
|
16392
|
+
await readLine({
|
|
16393
|
+
title: "open repository",
|
|
16394
|
+
label: "repository path",
|
|
16395
|
+
footer: "\u21B5 open esc back",
|
|
16396
|
+
validate: async (value) => {
|
|
16397
|
+
const path = value.replace(/^~(?=\/|$)/, homedir37()).replace(/\/+$/, "");
|
|
16398
|
+
if (!path || !existsSync37(path + "/.git")) return "not a git repository: " + path.slice(-40);
|
|
16399
|
+
resolved = path;
|
|
16400
|
+
return null;
|
|
16401
|
+
}
|
|
16402
|
+
});
|
|
16403
|
+
return resolved;
|
|
16404
|
+
}
|
|
16405
|
+
async function pickWorktree(repoPath) {
|
|
16406
|
+
const worktrees = await listWorktrees(repoPath);
|
|
16407
|
+
const choices = worktrees.map((worktree) => ({
|
|
16408
|
+
value: worktree.path,
|
|
16409
|
+
label: worktree.branch || worktree.path.split("/").filter(Boolean).pop() || worktree.path,
|
|
16410
|
+
detail: worktree.path,
|
|
16411
|
+
note: worktree.note
|
|
16412
|
+
}));
|
|
16413
|
+
choices.push({ value: NEW_WORKTREE, label: "+ new worktree\u2026", detail: "branch off this repository" });
|
|
16414
|
+
const picked = await pick({
|
|
16415
|
+
title: "open worktree \u2014 " + (repoPath.split("/").filter(Boolean).pop() || repoPath),
|
|
16416
|
+
hint: "type to filter worktrees",
|
|
16417
|
+
choices,
|
|
16418
|
+
footer: "\u2191\u2193 select \u21B5 open esc back"
|
|
16419
|
+
});
|
|
16420
|
+
if (!picked) return null;
|
|
16421
|
+
if (picked !== NEW_WORKTREE) return picked;
|
|
16422
|
+
let created = null;
|
|
16423
|
+
await readLine({
|
|
16424
|
+
title: "new worktree",
|
|
16425
|
+
label: "branch name",
|
|
16426
|
+
footer: "\u21B5 create esc back",
|
|
16427
|
+
validate: async (branch) => {
|
|
16428
|
+
if (!SAFE_BRANCH.test(branch)) return "letters, digits, . _ - / only";
|
|
16429
|
+
const target = repoPath.replace(/\/+$/, "") + "-" + branch.replace(/\//g, "-");
|
|
16430
|
+
const fresh = await run({ kind: "host" }, ["git", "-C", repoPath, "worktree", "add", "-b", branch, target]);
|
|
16431
|
+
const made = fresh.ok ? fresh : await run({ kind: "host" }, ["git", "-C", repoPath, "worktree", "add", target, branch]);
|
|
16432
|
+
if (!made.ok) return made.stderr.trim() || "git refused";
|
|
16433
|
+
created = target;
|
|
16434
|
+
return null;
|
|
16435
|
+
}
|
|
16436
|
+
});
|
|
16437
|
+
return created;
|
|
16438
|
+
}
|
|
16439
|
+
async function runMenu(bootPath, repoCwd, outerSession, sidebarPane) {
|
|
16440
|
+
const choice = await pick({
|
|
16441
|
+
menu: true,
|
|
16442
|
+
title: "synkro",
|
|
16443
|
+
hint: "choose an action",
|
|
16444
|
+
choices: [
|
|
16445
|
+
{ value: "workspace", label: "Open workspace\u2026" },
|
|
16446
|
+
{ value: "settings", label: "Settings" },
|
|
16447
|
+
{ value: "keybinds", label: "Keybinds" },
|
|
16448
|
+
{ value: "reload", label: "Reload sidebar" },
|
|
16449
|
+
{ value: "detach", label: "Detach (everything keeps running)" },
|
|
16450
|
+
{ value: "quit", label: "Quit synkro ui (agents keep running)" }
|
|
16451
|
+
],
|
|
16452
|
+
footer: "\u2191\u2193 select \u21B5 choose esc close"
|
|
16453
|
+
});
|
|
16454
|
+
if (!choice) return;
|
|
16455
|
+
if (choice === "workspace") {
|
|
16456
|
+
const repo = await pickRepo(repoCwd);
|
|
16457
|
+
if (!repo) return;
|
|
16458
|
+
rememberRepo(repo);
|
|
16459
|
+
const worktree = await pickWorktree(repo);
|
|
16460
|
+
if (worktree) rememberSpace(await repoOf(worktree));
|
|
16461
|
+
return;
|
|
16462
|
+
}
|
|
16463
|
+
if (choice === "settings") {
|
|
16464
|
+
await createTab(bootPath, "settings", repoCwd);
|
|
16465
|
+
return;
|
|
16466
|
+
}
|
|
16467
|
+
if (choice === "keybinds") {
|
|
16468
|
+
await runKeybinds();
|
|
16469
|
+
return;
|
|
16470
|
+
}
|
|
16471
|
+
if (choice === "reload" && sidebarPane) {
|
|
16472
|
+
await run({ kind: "host" }, ["tmux", "respawn-pane", "-k", "-t", sidebarPane]);
|
|
16473
|
+
return;
|
|
16474
|
+
}
|
|
16475
|
+
if (choice === "detach") {
|
|
16476
|
+
await run({ kind: "host" }, ["tmux", "detach-client", "-s", outerSession]);
|
|
16477
|
+
return;
|
|
16478
|
+
}
|
|
16479
|
+
if (choice === "quit") await run({ kind: "host" }, ["tmux", "kill-session", "-t", outerSession]);
|
|
16480
|
+
}
|
|
16481
|
+
async function runKeybinds() {
|
|
16482
|
+
await pick({
|
|
16483
|
+
menu: true,
|
|
16484
|
+
title: "keybinds",
|
|
16485
|
+
hint: "sidebar",
|
|
16486
|
+
choices: [
|
|
16487
|
+
{ value: "", label: "\u2191 \u2193 move through spaces and agents" },
|
|
16488
|
+
{ value: "", label: "\u21B5 click open the selected agent" },
|
|
16489
|
+
{ value: "", label: "n new tab in this space" },
|
|
16490
|
+
{ value: "", label: "a all spaces / this space" },
|
|
16491
|
+
{ value: "", label: "G group by what needs you" },
|
|
16492
|
+
{ value: "", label: "g s y consent: track / skip / stay" },
|
|
16493
|
+
{ value: "", label: "r restore an offline session" },
|
|
16494
|
+
{ value: "", label: "x close agent i interrupt" },
|
|
16495
|
+
{ value: "", label: "\u2325t new tab \u2325\u2190\u2192 switch tab" },
|
|
16496
|
+
{ value: "", label: "\u2325s focus sidebar" },
|
|
16497
|
+
{ value: "", label: "< > collapse / expand sidebar" },
|
|
16498
|
+
{ value: "", label: "d detach q quit" }
|
|
16499
|
+
],
|
|
16500
|
+
footer: "esc close"
|
|
16501
|
+
});
|
|
16502
|
+
}
|
|
16503
|
+
async function runTabMenu(outerSession) {
|
|
16504
|
+
const host = { kind: "host" };
|
|
16505
|
+
const listed = await run(host, [
|
|
16506
|
+
"tmux",
|
|
16507
|
+
"list-windows",
|
|
16508
|
+
"-t",
|
|
16509
|
+
outerSession,
|
|
16510
|
+
"-F",
|
|
16511
|
+
["#{window_id}", "#{window_index}", "#{window_active}", "#{window_name}", "#{@synkro_agent}"].join("|")
|
|
16512
|
+
]);
|
|
16513
|
+
const tabs = listed.stdout.split("\n").map((line) => line.split("|")).filter((cols) => cols[0]);
|
|
16514
|
+
if (tabs.length === 0) return;
|
|
16515
|
+
const activeIndex = Math.max(0, tabs.findIndex((cols) => cols[2] === "1"));
|
|
16516
|
+
const chosen = await pick({
|
|
16517
|
+
menu: true,
|
|
16518
|
+
title: "tabs",
|
|
16519
|
+
hint: "pick a tab",
|
|
16520
|
+
initial: activeIndex,
|
|
16521
|
+
choices: tabs.map((cols) => ({
|
|
16522
|
+
value: cols[0],
|
|
16523
|
+
label: (cols[2] === "1" ? "\u203A " : " ") + (cols[3] || "tab " + cols[1]),
|
|
16524
|
+
detail: cols[4] ? cols[4].replace(/^synkro-agent-/, "") : "terminal"
|
|
16525
|
+
})),
|
|
16526
|
+
footer: "\u2191\u2193 select \u21B5 choose esc close"
|
|
16527
|
+
});
|
|
16528
|
+
if (!chosen) return;
|
|
16529
|
+
const tab = tabs.find((cols) => cols[0] === chosen);
|
|
16530
|
+
if (!tab) return;
|
|
16531
|
+
const [windowId, , , name, agentSession2] = tab;
|
|
16532
|
+
const action = await pick({
|
|
16533
|
+
menu: true,
|
|
16534
|
+
title: name || "tab",
|
|
16535
|
+
hint: "what to do with this tab",
|
|
16536
|
+
choices: [
|
|
16537
|
+
{ value: "rename", label: "Rename tab\u2026" },
|
|
16538
|
+
{ value: "close", label: "Close tab" }
|
|
16539
|
+
],
|
|
16540
|
+
footer: "\u2191\u2193 select \u21B5 choose esc back"
|
|
16541
|
+
});
|
|
16542
|
+
if (!action) return;
|
|
16543
|
+
if (action === "rename") {
|
|
16544
|
+
let renamed = "";
|
|
16545
|
+
await readLine({
|
|
16546
|
+
title: "rename tab",
|
|
16547
|
+
label: "tab name",
|
|
16548
|
+
footer: "\u21B5 save esc back",
|
|
16549
|
+
validate: async (value) => {
|
|
16550
|
+
if (!value.trim()) return "name cannot be empty";
|
|
16551
|
+
renamed = value.trim();
|
|
16552
|
+
return null;
|
|
16553
|
+
}
|
|
16554
|
+
});
|
|
16555
|
+
if (renamed) {
|
|
16556
|
+
await run(host, ["tmux", "rename-window", "-t", windowId, renamed]);
|
|
16557
|
+
await snapshotTabs();
|
|
16558
|
+
}
|
|
16559
|
+
return;
|
|
16560
|
+
}
|
|
16561
|
+
const confirm = await pick({
|
|
16562
|
+
menu: true,
|
|
16563
|
+
title: "Close " + (name || "this tab") + "?",
|
|
16564
|
+
hint: agentSession2 ? "the agent keeps running \u2014 reopen it from the sidebar" : "a terminal tab, nothing else is affected",
|
|
16565
|
+
choices: [
|
|
16566
|
+
{ value: "no", label: "Cancel" },
|
|
16567
|
+
{ value: "yes", label: "Close tab" }
|
|
16568
|
+
],
|
|
16569
|
+
footer: "\u2191\u2193 select \u21B5 choose esc cancel"
|
|
16570
|
+
});
|
|
16571
|
+
if (confirm !== "yes") return;
|
|
16572
|
+
await run(host, ["tmux", "kill-window", "-t", windowId]);
|
|
16573
|
+
await snapshotTabs();
|
|
16574
|
+
}
|
|
16575
|
+
async function runKillConfirm(backend, session) {
|
|
16576
|
+
const name = session.replace(/^synkro-agent-/, "");
|
|
16577
|
+
const choice = await pick({
|
|
16578
|
+
menu: true,
|
|
16579
|
+
title: "Close agent?",
|
|
16580
|
+
hint: name,
|
|
16581
|
+
choices: [
|
|
16582
|
+
{ value: "no", label: "Cancel" },
|
|
16583
|
+
{ value: "yes", label: "Close it (its work is not saved anywhere else)" }
|
|
16584
|
+
],
|
|
16585
|
+
footer: "\u2191\u2193 select \u21B5 choose esc cancel"
|
|
16586
|
+
});
|
|
16587
|
+
if (choice !== "yes") return;
|
|
16588
|
+
await killAgent(backend === "container" ? "container" : "host", session, "");
|
|
16589
|
+
}
|
|
16590
|
+
async function runDialog(kind, repoCwd, argA = "", argB = "") {
|
|
16591
|
+
const bootPath = String(process.argv[1] || "");
|
|
16592
|
+
process.stdin.setRawMode?.(true);
|
|
16593
|
+
process.stdin.resume();
|
|
16594
|
+
write(CSI2 + "?1000h" + CSI2 + "?1006h" + CSI2 + "?25l" + CSI2 + "2J");
|
|
16595
|
+
process.on("exit", () => write(CSI2 + "?1000l" + CSI2 + "?1006l" + CSI2 + "?25h"));
|
|
16596
|
+
if (kind === "menu") {
|
|
16597
|
+
await runMenu(bootPath, repoCwd, argA, argB);
|
|
16598
|
+
process.exit(0);
|
|
16599
|
+
}
|
|
16600
|
+
if (kind === "keybinds") {
|
|
16601
|
+
await runKeybinds();
|
|
16602
|
+
process.exit(0);
|
|
16603
|
+
}
|
|
16604
|
+
if (kind === "kill") {
|
|
16605
|
+
await runKillConfirm(argA, argB);
|
|
16606
|
+
process.exit(0);
|
|
16607
|
+
}
|
|
16608
|
+
if (kind === "tab") {
|
|
16609
|
+
await runTabMenu(argA || "synkro-ui");
|
|
16610
|
+
process.exit(0);
|
|
16611
|
+
}
|
|
16612
|
+
if (kind === "new-workspace") {
|
|
16613
|
+
const repo = await pickRepo(repoCwd);
|
|
16614
|
+
if (!repo) process.exit(0);
|
|
16615
|
+
rememberRepo(repo);
|
|
16616
|
+
const worktree = await pickWorktree(repo);
|
|
16617
|
+
if (worktree) rememberSpace(await repoOf(worktree));
|
|
16618
|
+
process.exit(0);
|
|
16619
|
+
}
|
|
16620
|
+
const harnesses = await detectHarnesses();
|
|
16621
|
+
const sessions = [
|
|
16622
|
+
{ value: "terminal", label: TAB_GLYPHS.terminal + " Terminal" },
|
|
16623
|
+
...harnesses.map((harness) => ({
|
|
16624
|
+
value: harness,
|
|
16625
|
+
label: (TAB_GLYPHS[harness] || "") + " " + (HARNESS_LABELS[harness] || harness)
|
|
16626
|
+
}))
|
|
16627
|
+
];
|
|
16628
|
+
if (harnesses.includes("cursor")) {
|
|
16629
|
+
sessions.push({ value: "cursor-synkro", label: TAB_GLYPHS.cursor + " Cursor in Synkro UX" });
|
|
16630
|
+
}
|
|
16631
|
+
for (; ; ) {
|
|
16632
|
+
const session = await pick({
|
|
16633
|
+
menu: true,
|
|
16634
|
+
title: "new tab",
|
|
16635
|
+
hint: "choose what to run",
|
|
16636
|
+
choices: sessions,
|
|
16637
|
+
footer: "\u2191\u2193 select \u21B5 next esc close"
|
|
16638
|
+
});
|
|
16639
|
+
if (!session) process.exit(0);
|
|
16640
|
+
const repo = await pickRepo(repoCwd);
|
|
16641
|
+
if (!repo) continue;
|
|
16642
|
+
rememberRepo(repo);
|
|
16643
|
+
const worktree = await pickWorktree(repo);
|
|
16644
|
+
if (!worktree) continue;
|
|
16645
|
+
const root = await repoOf(worktree);
|
|
16646
|
+
rememberRepo(root);
|
|
16647
|
+
rememberSpace(root);
|
|
16648
|
+
write(CSI2 + "2J" + CSI2 + "H " + S.dim + "starting " + session + "\u2026" + S.reset);
|
|
16649
|
+
await createTab(bootPath, session, worktree);
|
|
16650
|
+
process.exit(0);
|
|
16651
|
+
}
|
|
16652
|
+
}
|
|
16653
|
+
var CSI2, KEY_ESC, KEY_CTRL_C2, KEY_BACKSPACE, S, HARNESS_LABELS, TYPE_PATH, NEW_WORKTREE, SAFE_BRANCH;
|
|
16654
|
+
var init_dialog = __esm({
|
|
16655
|
+
"cli/ui/dialog.ts"() {
|
|
14969
16656
|
"use strict";
|
|
16657
|
+
init_backend();
|
|
16658
|
+
init_model();
|
|
16659
|
+
init_manifest();
|
|
16660
|
+
init_repos();
|
|
16661
|
+
init_tabs();
|
|
16662
|
+
init_launch();
|
|
14970
16663
|
init_tmux();
|
|
14971
|
-
|
|
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
|
-
"sh",
|
|
14995
|
-
"-c",
|
|
14996
|
-
"mkdir -p " + shellQuote3(dir) + " && cp -n " + shellQuote3(AUTH_SEED) + " " + shellQuote3(dir + "/.claude.json") + " 2>/dev/null; true"
|
|
14997
|
-
]);
|
|
14998
|
-
return dir;
|
|
16705
|
+
});
|
|
16706
|
+
|
|
16707
|
+
// cli/harness/cursor.ts
|
|
16708
|
+
function unwrapReplay(text) {
|
|
16709
|
+
return text.replace(/<\/?user_query>/gi, "").trim();
|
|
14999
16710
|
}
|
|
15000
|
-
|
|
15001
|
-
const
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
16711
|
+
function textOf(message) {
|
|
16712
|
+
const content = message?.content;
|
|
16713
|
+
if (typeof content === "string") return unwrapReplay(content);
|
|
16714
|
+
if (!Array.isArray(content)) return "";
|
|
16715
|
+
return unwrapReplay(content.filter((part) => part && (part.type === "text" || typeof part.text === "string")).map((part) => String(part.text || "")).join(""));
|
|
16716
|
+
}
|
|
16717
|
+
function toolPayload(toolCall) {
|
|
16718
|
+
if (!toolCall) return { kind: "other", body: {} };
|
|
16719
|
+
for (const [key, kind] of Object.entries(TOOL_KINDS)) {
|
|
16720
|
+
if (toolCall[key]) return { kind, body: toolCall[key] };
|
|
16721
|
+
}
|
|
16722
|
+
const fallback = Object.keys(toolCall).find((key) => key.endsWith("ToolCall"));
|
|
16723
|
+
return fallback ? { kind: "other", body: toolCall[fallback] } : { kind: "other", body: {} };
|
|
16724
|
+
}
|
|
16725
|
+
function targetOf(kind, body) {
|
|
16726
|
+
const args2 = body?.args || {};
|
|
16727
|
+
const candidate = args2.command ?? args2.path ?? args2.filePath ?? args2.file_path ?? args2.pattern ?? args2.query ?? args2.target ?? "";
|
|
16728
|
+
const text = typeof candidate === "string" ? candidate : JSON.stringify(candidate ?? "");
|
|
16729
|
+
return text || body?.description || kind;
|
|
16730
|
+
}
|
|
16731
|
+
function parseCursorLine(line) {
|
|
16732
|
+
const trimmed = String(line || "").trim();
|
|
16733
|
+
if (!trimmed) return [];
|
|
16734
|
+
let frame;
|
|
16735
|
+
try {
|
|
16736
|
+
frame = JSON.parse(trimmed);
|
|
16737
|
+
} catch {
|
|
16738
|
+
return [];
|
|
15007
16739
|
}
|
|
15008
|
-
const
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
16740
|
+
const type = frame.type;
|
|
16741
|
+
const subtype = frame.subtype;
|
|
16742
|
+
if (type === "system" && subtype === "init") {
|
|
16743
|
+
return [{
|
|
16744
|
+
type: "session-start",
|
|
16745
|
+
sessionId: String(frame.session_id || ""),
|
|
16746
|
+
model: String(frame.model || ""),
|
|
16747
|
+
cwd: String(frame.cwd || ""),
|
|
16748
|
+
authSource: String(frame.apiKeySource || "")
|
|
16749
|
+
}];
|
|
16750
|
+
}
|
|
16751
|
+
if (type === "user") {
|
|
16752
|
+
const text = textOf(frame.message);
|
|
16753
|
+
return text ? [{ type: "user-message", text }] : [];
|
|
16754
|
+
}
|
|
16755
|
+
if (type === "assistant") {
|
|
16756
|
+
const text = textOf(frame.message);
|
|
16757
|
+
return text ? [{ type: "assistant-message", text }] : [];
|
|
16758
|
+
}
|
|
16759
|
+
if (type === "thinking" && subtype === "delta" && frame.text) {
|
|
16760
|
+
return [{ type: "thinking", text: String(frame.text) }];
|
|
16761
|
+
}
|
|
16762
|
+
if (type === "tool_call") {
|
|
16763
|
+
const { kind, body } = toolPayload(frame.tool_call);
|
|
16764
|
+
const id = String(frame.call_id || frame.tool_call?.toolCallId || "");
|
|
16765
|
+
const target = targetOf(kind, body);
|
|
16766
|
+
const description = String(body?.description || frame.tool_call?.description || "");
|
|
16767
|
+
if (subtype === "started") {
|
|
16768
|
+
return [{ type: "tool-start", id, kind, target, description }];
|
|
16769
|
+
}
|
|
16770
|
+
if (subtype === "completed") {
|
|
16771
|
+
const result = body?.result || {};
|
|
16772
|
+
const success = result.success;
|
|
16773
|
+
const rejected = result.rejected;
|
|
16774
|
+
const rawReason = String(rejected?.reason || "");
|
|
16775
|
+
const blocked = Boolean(rejected) && BLOCK_MARKER.test(rawReason);
|
|
16776
|
+
return [{
|
|
16777
|
+
type: "tool-end",
|
|
16778
|
+
id,
|
|
16779
|
+
kind,
|
|
16780
|
+
target: target || String(rejected?.command || ""),
|
|
16781
|
+
ok: Boolean(success) && Number(success?.exitCode ?? 0) === 0,
|
|
16782
|
+
blocked,
|
|
16783
|
+
reason: rejected ? blocked ? blockReason(rawReason) : rawReason || "rejected" : "",
|
|
16784
|
+
exitCode: success ? Number(success.exitCode ?? 0) : null,
|
|
16785
|
+
output: String(success?.stdout || success?.stderr || "")
|
|
16786
|
+
}];
|
|
15013
16787
|
}
|
|
16788
|
+
return [];
|
|
15014
16789
|
}
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
16790
|
+
if (type === "retry") {
|
|
16791
|
+
if (subtype !== "starting") return [];
|
|
16792
|
+
const attempt = Number(frame.attempt || 0);
|
|
16793
|
+
return [{
|
|
16794
|
+
type: "notice",
|
|
16795
|
+
kind: "retry",
|
|
16796
|
+
text: "connection dropped, resuming" + (attempt ? " (attempt " + attempt + ")" : "") + "\u2026"
|
|
16797
|
+
}];
|
|
16798
|
+
}
|
|
16799
|
+
if (type === "result") {
|
|
16800
|
+
return [{
|
|
16801
|
+
type: "turn-end",
|
|
16802
|
+
ok: !frame.is_error,
|
|
16803
|
+
text: String(frame.result || "")
|
|
16804
|
+
}];
|
|
16805
|
+
}
|
|
16806
|
+
return [];
|
|
15018
16807
|
}
|
|
15019
|
-
|
|
15020
|
-
const
|
|
15021
|
-
|
|
15022
|
-
|
|
16808
|
+
function feed(buffer, chunk) {
|
|
16809
|
+
const combined = buffer + chunk;
|
|
16810
|
+
const parts = combined.split("\n");
|
|
16811
|
+
const rest = parts.pop() ?? "";
|
|
16812
|
+
return { lines: parts, rest };
|
|
15023
16813
|
}
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
16814
|
+
function cursorArgs(prompt) {
|
|
16815
|
+
return [
|
|
16816
|
+
"-p",
|
|
16817
|
+
prompt,
|
|
16818
|
+
"--output-format",
|
|
16819
|
+
"stream-json",
|
|
16820
|
+
// --force auto-runs tools but does NOT bypass hooks (verified live), so
|
|
16821
|
+
// Synkro's guards still gate every call; --trust loads workspace hooks.
|
|
16822
|
+
"--force",
|
|
16823
|
+
"--trust"
|
|
16824
|
+
];
|
|
16825
|
+
}
|
|
16826
|
+
var TOOL_KINDS;
|
|
16827
|
+
var init_cursor = __esm({
|
|
16828
|
+
"cli/harness/cursor.ts"() {
|
|
15027
16829
|
"use strict";
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
16830
|
+
init_events();
|
|
16831
|
+
TOOL_KINDS = {
|
|
16832
|
+
shellToolCall: "shell",
|
|
16833
|
+
readToolCall: "read",
|
|
16834
|
+
editToolCall: "edit",
|
|
16835
|
+
writeToolCall: "write",
|
|
16836
|
+
deleteToolCall: "delete",
|
|
16837
|
+
grepToolCall: "search",
|
|
16838
|
+
globToolCall: "search",
|
|
16839
|
+
lsToolCall: "list",
|
|
16840
|
+
todoToolCall: "todo",
|
|
16841
|
+
updateTodosToolCall: "todo"
|
|
16842
|
+
};
|
|
15033
16843
|
}
|
|
15034
16844
|
});
|
|
15035
16845
|
|
|
15036
|
-
// cli/
|
|
15037
|
-
function
|
|
15038
|
-
|
|
16846
|
+
// cli/harness/render.ts
|
|
16847
|
+
function clip3(text, max) {
|
|
16848
|
+
const value = String(text || "").replace(/\s+/g, " ").trim();
|
|
16849
|
+
return value.length <= max ? value : value.slice(0, max - 1) + "\u2026";
|
|
16850
|
+
}
|
|
16851
|
+
function renderEvent(event, width = 100) {
|
|
16852
|
+
const body = Math.max(30, width - 4);
|
|
16853
|
+
switch (event.type) {
|
|
16854
|
+
case "session-start":
|
|
16855
|
+
return [
|
|
16856
|
+
"",
|
|
16857
|
+
S2.dim + " " + event.model + " \xB7 " + clip3(event.cwd, body - 30) + (event.authSource === "login" ? " \xB7 subscription" : "") + S2.reset,
|
|
16858
|
+
""
|
|
16859
|
+
];
|
|
16860
|
+
case "user-message":
|
|
16861
|
+
return ["", S2.user + " \u276F " + S2.reset + S2.bold + clip3(event.text, body) + S2.reset, ""];
|
|
16862
|
+
case "thinking":
|
|
16863
|
+
return [S2.think + " \xB7 thinking\u2026" + S2.reset];
|
|
16864
|
+
case "assistant-message":
|
|
16865
|
+
return ["", ...wrap(event.text, body).map((line) => S2.agent + " " + line + S2.reset), ""];
|
|
16866
|
+
case "tool-start": {
|
|
16867
|
+
const glyph = GLYPH[event.kind] || GLYPH.other;
|
|
16868
|
+
return [S2.tool + " " + glyph + " " + S2.reset + S2.dim + clip3(event.target, body - 6) + S2.reset];
|
|
16869
|
+
}
|
|
16870
|
+
case "tool-end": {
|
|
16871
|
+
if (event.blocked) {
|
|
16872
|
+
return [
|
|
16873
|
+
S2.blocked + " \u26D4 blocked" + S2.reset + S2.dim + " " + clip3(event.target, body - 14) + S2.reset,
|
|
16874
|
+
...wrap(event.reason, body - 6).map((line) => " " + S2.rule + line + S2.reset)
|
|
16875
|
+
];
|
|
16876
|
+
}
|
|
16877
|
+
const mark = event.ok ? S2.ok + " \u2713" : S2.blocked + " \u2717";
|
|
16878
|
+
const code = event.exitCode === null || event.exitCode === 0 ? "" : " (exit " + event.exitCode + ")";
|
|
16879
|
+
const out = event.output.trim() ? S2.dim + " " + clip3(event.output, body - 20) + S2.reset : "";
|
|
16880
|
+
return [mark + S2.reset + code + out];
|
|
16881
|
+
}
|
|
16882
|
+
case "turn-end":
|
|
16883
|
+
return ["", S2.dim + " " + (event.ok ? "done" : "ended with an error") + S2.reset, ""];
|
|
16884
|
+
case "notice":
|
|
16885
|
+
return [S2.dim + " " + clip3(event.text, body) + S2.reset];
|
|
16886
|
+
default:
|
|
16887
|
+
return [];
|
|
16888
|
+
}
|
|
15039
16889
|
}
|
|
15040
|
-
|
|
15041
|
-
const
|
|
15042
|
-
|
|
15043
|
-
const
|
|
15044
|
-
|
|
15045
|
-
const
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
backendNote: info.note,
|
|
15053
|
-
message: ""
|
|
15054
|
-
};
|
|
15055
|
-
let hashes = /* @__PURE__ */ new Map();
|
|
15056
|
-
let lastFrame = "";
|
|
15057
|
-
let spawnCounter = 1;
|
|
15058
|
-
const host = { kind: "host" };
|
|
15059
|
-
const containerRunner = { kind: "container", container: CONTAINER_NAME2 };
|
|
15060
|
-
async function refresh() {
|
|
15061
|
-
const [hostSpaces, containerSpaces, conductorTasks] = await Promise.all([
|
|
15062
|
-
discoverHostSpaces(repoCwd),
|
|
15063
|
-
info.backend === "container" ? discoverContainerSpaces(containerRunner) : Promise.resolve([]),
|
|
15064
|
-
fetchConductorTasks(CONDUCTOR_URL)
|
|
15065
|
-
]);
|
|
15066
|
-
state.spaces = [...hostSpaces, ...containerSpaces];
|
|
15067
|
-
const hostAgents = await discoverAgents(host, "host", hashes);
|
|
15068
|
-
const containerAgents = info.backend === "container" ? await discoverAgents(containerRunner, "container", hashes) : { agents: [], hashes: /* @__PURE__ */ new Map() };
|
|
15069
|
-
hashes = new Map([...hostAgents.hashes, ...containerAgents.hashes]);
|
|
15070
|
-
state.agents = [...hostAgents.agents, ...containerAgents.agents].map((agent) => ({
|
|
15071
|
-
...agent,
|
|
15072
|
-
linear: mapAgentToTask(agent.space, conductorTasks)
|
|
15073
|
-
}));
|
|
15074
|
-
state.spaceIndex = Math.min(state.spaceIndex, Math.max(0, state.spaces.length - 1));
|
|
15075
|
-
state.agentIndex = Math.min(state.agentIndex, Math.max(0, state.agents.length - 1));
|
|
16890
|
+
function wrap(text, width) {
|
|
16891
|
+
const words = String(text || "").replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
16892
|
+
if (words.length === 0) return [];
|
|
16893
|
+
const lines = [];
|
|
16894
|
+
let line = "";
|
|
16895
|
+
for (const word of words) {
|
|
16896
|
+
if (!line) line = word;
|
|
16897
|
+
else if ((line + " " + word).length <= width) line += " " + word;
|
|
16898
|
+
else {
|
|
16899
|
+
lines.push(line);
|
|
16900
|
+
line = word;
|
|
16901
|
+
}
|
|
15076
16902
|
}
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
16903
|
+
if (line) lines.push(line);
|
|
16904
|
+
return lines;
|
|
16905
|
+
}
|
|
16906
|
+
var ESC2, S2, GLYPH;
|
|
16907
|
+
var init_render2 = __esm({
|
|
16908
|
+
"cli/harness/render.ts"() {
|
|
16909
|
+
"use strict";
|
|
16910
|
+
ESC2 = "\x1B[";
|
|
16911
|
+
S2 = {
|
|
16912
|
+
reset: ESC2 + "0m",
|
|
16913
|
+
dim: ESC2 + "2m",
|
|
16914
|
+
bold: ESC2 + "1m",
|
|
16915
|
+
user: ESC2 + "38;5;111m",
|
|
16916
|
+
agent: ESC2 + "38;5;252m",
|
|
16917
|
+
think: ESC2 + "38;5;244m",
|
|
16918
|
+
tool: ESC2 + "38;5;180m",
|
|
16919
|
+
ok: ESC2 + "38;5;114m",
|
|
16920
|
+
blocked: ESC2 + "38;5;203m",
|
|
16921
|
+
rule: ESC2 + "38;5;211m"
|
|
16922
|
+
};
|
|
16923
|
+
GLYPH = {
|
|
16924
|
+
shell: "\u276F",
|
|
16925
|
+
read: "\u25C4",
|
|
16926
|
+
edit: "\u270E",
|
|
16927
|
+
write: "\u270E",
|
|
16928
|
+
delete: "\u2716",
|
|
16929
|
+
search: "\u2315",
|
|
16930
|
+
list: "\u2630",
|
|
16931
|
+
todo: "\u2611",
|
|
16932
|
+
other: "\u2022"
|
|
16933
|
+
};
|
|
15084
16934
|
}
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
16935
|
+
});
|
|
16936
|
+
|
|
16937
|
+
// cli/harness/run.ts
|
|
16938
|
+
import { spawn as spawn9 } from "child_process";
|
|
16939
|
+
function replayKey(event) {
|
|
16940
|
+
switch (event.type) {
|
|
16941
|
+
case "user-message":
|
|
16942
|
+
return "u:" + event.text;
|
|
16943
|
+
case "assistant-message":
|
|
16944
|
+
return "a:" + event.text;
|
|
16945
|
+
case "tool-start":
|
|
16946
|
+
return "s:" + event.id;
|
|
16947
|
+
case "tool-end":
|
|
16948
|
+
return "e:" + event.id;
|
|
16949
|
+
default:
|
|
16950
|
+
return "";
|
|
15092
16951
|
}
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
16952
|
+
}
|
|
16953
|
+
function createTurnSink(opts) {
|
|
16954
|
+
const events = [];
|
|
16955
|
+
let blocked = 0;
|
|
16956
|
+
let lastWasThinking = false;
|
|
16957
|
+
let replaying = false;
|
|
16958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16959
|
+
let sawTurnEnd = false;
|
|
16960
|
+
let sawAnswer = false;
|
|
16961
|
+
let buffer = "";
|
|
16962
|
+
const emit2 = (event) => {
|
|
16963
|
+
events.push(event);
|
|
16964
|
+
if (event.type === "tool-end" && event.blocked) blocked += 1;
|
|
16965
|
+
opts.onEvent?.(event);
|
|
16966
|
+
if (event.type === "thinking") {
|
|
16967
|
+
if (lastWasThinking) return;
|
|
16968
|
+
lastWasThinking = true;
|
|
16969
|
+
} else {
|
|
16970
|
+
lastWasThinking = false;
|
|
15098
16971
|
}
|
|
15099
|
-
const
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
if (
|
|
15111
|
-
|
|
15112
|
-
|
|
16972
|
+
const rendered = renderEvent(event, opts.width);
|
|
16973
|
+
if (rendered.length) opts.write(rendered.join("\n") + "\n");
|
|
16974
|
+
};
|
|
16975
|
+
const take = (event) => {
|
|
16976
|
+
if (event.type === "notice" && event.kind === "retry") replaying = true;
|
|
16977
|
+
const key = replayKey(event);
|
|
16978
|
+
if (key) {
|
|
16979
|
+
if (replaying && seen.has(key)) return;
|
|
16980
|
+
seen.add(key);
|
|
16981
|
+
}
|
|
16982
|
+
if (event.type === "turn-end") sawTurnEnd = true;
|
|
16983
|
+
if (event.type === "assistant-message") sawAnswer = true;
|
|
16984
|
+
emit2(event);
|
|
16985
|
+
};
|
|
16986
|
+
return {
|
|
16987
|
+
chunk(text) {
|
|
16988
|
+
const { lines, rest } = feed(buffer, text);
|
|
16989
|
+
buffer = rest;
|
|
16990
|
+
for (const line of lines) for (const event of parseCursorLine(line)) take(event);
|
|
16991
|
+
},
|
|
16992
|
+
notice(text) {
|
|
16993
|
+
emit2({ type: "notice", text });
|
|
16994
|
+
},
|
|
16995
|
+
finish(exit) {
|
|
16996
|
+
if (!sawTurnEnd) {
|
|
16997
|
+
if (exit !== 0) {
|
|
16998
|
+
emit2({
|
|
16999
|
+
type: "notice",
|
|
17000
|
+
text: sawAnswer ? "cursor-agent exited " + exit + " after retrying; the reply above is complete" : "cursor-agent exited " + exit + " without completing the turn"
|
|
17001
|
+
});
|
|
17002
|
+
}
|
|
17003
|
+
emit2({ type: "turn-end", ok: sawAnswer, text: "" });
|
|
17004
|
+
}
|
|
17005
|
+
return { events, blocked, exitCode: sawAnswer ? 0 : exit };
|
|
15113
17006
|
}
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
17007
|
+
};
|
|
17008
|
+
}
|
|
17009
|
+
async function runCursorTurn(opts) {
|
|
17010
|
+
const write2 = opts.write || ((text) => process.stdout.write(text));
|
|
17011
|
+
const width = opts.width || Number(process.stdout.columns || 100);
|
|
17012
|
+
const sink = createTurnSink({ write: write2, onEvent: opts.onEvent, width });
|
|
17013
|
+
return new Promise((resolve7) => {
|
|
17014
|
+
const child = spawn9("cursor-agent", cursorArgs(opts.prompt), {
|
|
17015
|
+
cwd: opts.cwd,
|
|
17016
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17017
|
+
});
|
|
17018
|
+
child.stdout.on("data", (chunk) => sink.chunk(chunk.toString("utf8")));
|
|
17019
|
+
child.stderr.on("data", (chunk) => {
|
|
17020
|
+
const text = chunk.toString("utf8").trim();
|
|
17021
|
+
if (/error|fatal/i.test(text) && !/RetriableError/i.test(text)) sink.notice(text);
|
|
17022
|
+
});
|
|
17023
|
+
child.on("close", (code) => resolve7(sink.finish(code ?? 0)));
|
|
17024
|
+
child.on("error", (error) => {
|
|
17025
|
+
sink.notice("failed to start cursor-agent: " + String(error));
|
|
17026
|
+
resolve7(sink.finish(1));
|
|
17027
|
+
});
|
|
17028
|
+
});
|
|
17029
|
+
}
|
|
17030
|
+
var init_run = __esm({
|
|
17031
|
+
"cli/harness/run.ts"() {
|
|
17032
|
+
"use strict";
|
|
17033
|
+
init_cursor();
|
|
17034
|
+
init_render2();
|
|
15117
17035
|
}
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
17036
|
+
});
|
|
17037
|
+
|
|
17038
|
+
// cli/harness/session.ts
|
|
17039
|
+
import { createInterface as createInterface5 } from "readline";
|
|
17040
|
+
async function runOnce(harness, cwd, prompt) {
|
|
17041
|
+
if (harness !== "cursor") {
|
|
17042
|
+
process.stdout.write(S2.dim + " " + harness + " sessions are not embedded yet\n" + S2.reset);
|
|
17043
|
+
return 1;
|
|
17044
|
+
}
|
|
17045
|
+
const result = await runCursorTurn({ prompt, cwd });
|
|
17046
|
+
if (result.blocked > 0) {
|
|
17047
|
+
process.stdout.write(
|
|
17048
|
+
S2.blocked + " " + result.blocked + " action" + (result.blocked === 1 ? "" : "s") + " blocked by policy" + S2.reset + "\n"
|
|
17049
|
+
);
|
|
15125
17050
|
}
|
|
15126
|
-
|
|
15127
|
-
|
|
17051
|
+
return result.exitCode;
|
|
17052
|
+
}
|
|
17053
|
+
async function runGovernedSession(harness, cwd, prompt) {
|
|
17054
|
+
if (prompt) return runOnce(harness, cwd, prompt);
|
|
17055
|
+
process.stdout.write(BANNER.join("\n") + "\n");
|
|
17056
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
17057
|
+
const ask3 = () => new Promise((resolve7) => rl.question(S2.user + " \u276F " + S2.reset, resolve7));
|
|
17058
|
+
for (; ; ) {
|
|
17059
|
+
const line = (await ask3()).trim();
|
|
17060
|
+
if (!line) continue;
|
|
17061
|
+
if (line === "exit" || line === "quit") break;
|
|
17062
|
+
await runOnce(harness, cwd, line);
|
|
15128
17063
|
}
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
process.stdin.on("data", (chunk) => {
|
|
15132
|
-
const key = chunk.toString("utf8");
|
|
15133
|
-
void (async () => {
|
|
15134
|
-
const list = state.section === "spaces" ? state.spaces : state.agents;
|
|
15135
|
-
if (key === " ") state.section = state.section === "spaces" ? "agents" : "spaces";
|
|
15136
|
-
else if (key === "j" || key === "\x1B[B") {
|
|
15137
|
-
if (state.section === "spaces") state.spaceIndex = Math.min(state.spaceIndex + 1, Math.max(0, list.length - 1));
|
|
15138
|
-
else state.agentIndex = Math.min(state.agentIndex + 1, Math.max(0, list.length - 1));
|
|
15139
|
-
} else if (key === "k" || key === "\x1B[A") {
|
|
15140
|
-
if (state.section === "spaces") state.spaceIndex = Math.max(0, state.spaceIndex - 1);
|
|
15141
|
-
else state.agentIndex = Math.max(0, state.agentIndex - 1);
|
|
15142
|
-
} else if (key === "\r") {
|
|
15143
|
-
if (state.section === "agents") await attachSelected();
|
|
15144
|
-
else state.message = "space: " + (state.spaces[state.spaceIndex]?.name || "");
|
|
15145
|
-
} else if (key === "n") await spawnInSelectedSpace();
|
|
15146
|
-
else if (key === "c") await spawnContainerAgent();
|
|
15147
|
-
else if (key === "x") {
|
|
15148
|
-
const agent = state.agents[state.agentIndex];
|
|
15149
|
-
if (agent) {
|
|
15150
|
-
await killAgent(agent.backend, agent.session, "");
|
|
15151
|
-
state.message = "killed " + agent.name;
|
|
15152
|
-
}
|
|
15153
|
-
} else if (key === "i") {
|
|
15154
|
-
const agent = state.agents[state.agentIndex];
|
|
15155
|
-
if (agent) await run(runnerFor(agent.backend), buildInterrupt(agent.session));
|
|
15156
|
-
} else if (key === "g") await consent("track");
|
|
15157
|
-
else if (key === "s") await consent("skip");
|
|
15158
|
-
else if (key === "y") await consent("stay");
|
|
15159
|
-
else if (key === "T") await newTab();
|
|
15160
|
-
else if (key === "q" || key === "") {
|
|
15161
|
-
await run(host, ["tmux", "kill-session", "-t", outerSession]);
|
|
15162
|
-
process.exit(0);
|
|
15163
|
-
}
|
|
15164
|
-
await refresh();
|
|
15165
|
-
draw();
|
|
15166
|
-
})();
|
|
15167
|
-
});
|
|
15168
|
-
await refresh();
|
|
15169
|
-
draw();
|
|
15170
|
-
setInterval(() => {
|
|
15171
|
-
void refresh().then(draw);
|
|
15172
|
-
}, POLL_MS);
|
|
17064
|
+
rl.close();
|
|
17065
|
+
return 0;
|
|
15173
17066
|
}
|
|
15174
|
-
var
|
|
15175
|
-
var
|
|
15176
|
-
"cli/
|
|
17067
|
+
var BANNER;
|
|
17068
|
+
var init_session = __esm({
|
|
17069
|
+
"cli/harness/session.ts"() {
|
|
15177
17070
|
"use strict";
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
17071
|
+
init_run();
|
|
17072
|
+
init_render2();
|
|
17073
|
+
BANNER = [
|
|
17074
|
+
"",
|
|
17075
|
+
S2.bold + " synkro" + S2.reset + S2.dim + " governed session" + S2.reset,
|
|
17076
|
+
S2.dim + " every tool call passes Synkro policy before it runs" + S2.reset,
|
|
17077
|
+
S2.dim + " ctrl-c to leave" + S2.reset,
|
|
17078
|
+
""
|
|
17079
|
+
];
|
|
15185
17080
|
}
|
|
15186
17081
|
});
|
|
15187
17082
|
|
|
@@ -15190,22 +17085,68 @@ var ui_exports = {};
|
|
|
15190
17085
|
__export(ui_exports, {
|
|
15191
17086
|
uiCommand: () => uiCommand
|
|
15192
17087
|
});
|
|
15193
|
-
import { execSync as
|
|
15194
|
-
function repoRoot() {
|
|
15195
|
-
try {
|
|
15196
|
-
return execSync7("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim() || process.cwd();
|
|
15197
|
-
} catch {
|
|
15198
|
-
return process.cwd();
|
|
15199
|
-
}
|
|
15200
|
-
}
|
|
17088
|
+
import { execSync as execSync8 } from "child_process";
|
|
15201
17089
|
function tmuxPresent() {
|
|
15202
17090
|
try {
|
|
15203
|
-
|
|
17091
|
+
execSync8("tmux -V", { stdio: ["pipe", "pipe", "pipe"] });
|
|
15204
17092
|
return true;
|
|
15205
17093
|
} catch {
|
|
15206
17094
|
return false;
|
|
15207
17095
|
}
|
|
15208
17096
|
}
|
|
17097
|
+
async function takeover(kind, cwd) {
|
|
17098
|
+
const harness = ["claude", "codex", "cursor"].includes(kind) ? kind : "claude";
|
|
17099
|
+
const info = await detectContainerBackend();
|
|
17100
|
+
const reachable = info.backend === "container" ? (await run(info.runner, ["test", "-d", cwd])).ok : false;
|
|
17101
|
+
const backend = reachable ? "container" : "host";
|
|
17102
|
+
const spaceName = cwd.split("/").filter(Boolean).pop() || "space";
|
|
17103
|
+
const spawned = await spawnAgent(info, {
|
|
17104
|
+
name: spaceName + "-" + harness + "-" + String(process.pid % 1e4),
|
|
17105
|
+
harness,
|
|
17106
|
+
spaceName,
|
|
17107
|
+
cwd,
|
|
17108
|
+
backend
|
|
17109
|
+
});
|
|
17110
|
+
if (!spawned.ok) {
|
|
17111
|
+
console.error("synkro: spawn failed: " + (spawned.error || "unknown"));
|
|
17112
|
+
return 1;
|
|
17113
|
+
}
|
|
17114
|
+
if (process.env.TMUX_PANE) {
|
|
17115
|
+
await run(HOST3, ["tmux", "rename-window", "-t", process.env.TMUX_PANE, tabTitle(harness, spaceName)]);
|
|
17116
|
+
}
|
|
17117
|
+
const runner = backend === "container" ? info.runner : HOST3;
|
|
17118
|
+
return runInherit(["env", "TMUX=", ...runnerInteractiveArgs(runner, ["tmux", "attach-session", "-t", spawned.session])]);
|
|
17119
|
+
}
|
|
17120
|
+
async function restoreSession(session) {
|
|
17121
|
+
const record = loadRecords().find((row2) => row2.session === session);
|
|
17122
|
+
if (!record) return;
|
|
17123
|
+
const info = await detectContainerBackend();
|
|
17124
|
+
await spawnAgent(info, {
|
|
17125
|
+
name: record.name,
|
|
17126
|
+
harness: record.harness,
|
|
17127
|
+
spaceName: record.spaceName,
|
|
17128
|
+
cwd: record.space,
|
|
17129
|
+
backend: record.backend,
|
|
17130
|
+
resume: true
|
|
17131
|
+
});
|
|
17132
|
+
}
|
|
17133
|
+
async function printStatus() {
|
|
17134
|
+
const info = await detectContainerBackend();
|
|
17135
|
+
const [hostAgents, containerAgents] = await Promise.all([
|
|
17136
|
+
discoverAgents({ kind: "host" }, "host", /* @__PURE__ */ new Map()),
|
|
17137
|
+
info.backend === "container" ? discoverAgents(info.runner, "container", /* @__PURE__ */ new Map()) : Promise.resolve({ agents: [], hashes: /* @__PURE__ */ new Map() })
|
|
17138
|
+
]);
|
|
17139
|
+
const live = [...hostAgents.agents, ...containerAgents.agents];
|
|
17140
|
+
const rows = [...live, ...offlineAgents(live, loadRecords())];
|
|
17141
|
+
if (rows.length === 0) {
|
|
17142
|
+
console.log("no sessions. `synkro ui` to start one.");
|
|
17143
|
+
return;
|
|
17144
|
+
}
|
|
17145
|
+
for (const agent of rows) {
|
|
17146
|
+
console.log([agent.status.padEnd(8), agent.harness.padEnd(7), agent.name, agent.space || ""].join(" "));
|
|
17147
|
+
}
|
|
17148
|
+
console.log("\n`synkro ui` to attach" + (rows.some((agent) => agent.status === "offline") ? "; offline sessions restore with r" : ""));
|
|
17149
|
+
}
|
|
15209
17150
|
async function uiCommand(args2) {
|
|
15210
17151
|
const bootPath = String(process.argv[1] || "");
|
|
15211
17152
|
if (args2.includes("--sidebar")) {
|
|
@@ -15214,8 +17155,53 @@ async function uiCommand(args2) {
|
|
|
15214
17155
|
});
|
|
15215
17156
|
return;
|
|
15216
17157
|
}
|
|
15217
|
-
|
|
15218
|
-
|
|
17158
|
+
const dialogAt = args2.indexOf("--dialog");
|
|
17159
|
+
if (dialogAt !== -1) {
|
|
17160
|
+
await runDialog(
|
|
17161
|
+
args2[dialogAt + 1] || "new-tab",
|
|
17162
|
+
args2[dialogAt + 2] || repoRoot(),
|
|
17163
|
+
args2[dialogAt + 3] || "",
|
|
17164
|
+
args2[dialogAt + 4] || ""
|
|
17165
|
+
);
|
|
17166
|
+
return;
|
|
17167
|
+
}
|
|
17168
|
+
const tabAt = args2.indexOf("--tab");
|
|
17169
|
+
if (tabAt !== -1) {
|
|
17170
|
+
await createTab(bootPath, args2[tabAt + 1] || "terminal", args2[tabAt + 2] || repoRoot());
|
|
17171
|
+
return;
|
|
17172
|
+
}
|
|
17173
|
+
const openAt = args2.indexOf("--open-agent");
|
|
17174
|
+
if (openAt !== -1) {
|
|
17175
|
+
await openAgentTab(bootPath, args2[openAt + 1] || "");
|
|
17176
|
+
return;
|
|
17177
|
+
}
|
|
17178
|
+
const runAt = args2.indexOf("--run");
|
|
17179
|
+
if (runAt !== -1) {
|
|
17180
|
+
const harness = args2[runAt + 1] || "cursor";
|
|
17181
|
+
const cwd = args2[runAt + 2] || repoRoot();
|
|
17182
|
+
const prompt = args2.slice(runAt + 3).join(" ").trim();
|
|
17183
|
+
process.exitCode = await runGovernedSession(harness, cwd, prompt);
|
|
17184
|
+
return;
|
|
17185
|
+
}
|
|
17186
|
+
const takeoverAt = args2.indexOf("--takeover");
|
|
17187
|
+
if (takeoverAt !== -1) {
|
|
17188
|
+
process.exitCode = await takeover(args2[takeoverAt + 1] || "claude", args2[takeoverAt + 2] || process.cwd());
|
|
17189
|
+
return;
|
|
17190
|
+
}
|
|
17191
|
+
const restoreAt = args2.indexOf("--restore");
|
|
17192
|
+
if (restoreAt !== -1) {
|
|
17193
|
+
await restoreSession(args2[restoreAt + 1] || "");
|
|
17194
|
+
return;
|
|
17195
|
+
}
|
|
17196
|
+
if (args2.includes("--status")) {
|
|
17197
|
+
await printStatus();
|
|
17198
|
+
return;
|
|
17199
|
+
}
|
|
17200
|
+
const killAt = args2.indexOf("--kill-agent");
|
|
17201
|
+
if (killAt !== -1) {
|
|
17202
|
+
const backend = args2[killAt + 1] === "container" ? "container" : "host";
|
|
17203
|
+
const session = args2[killAt + 2] || "";
|
|
17204
|
+
if (session.startsWith("synkro-agent-")) await killAgent(backend, session, "");
|
|
15219
17205
|
return;
|
|
15220
17206
|
}
|
|
15221
17207
|
if (!tmuxPresent()) {
|
|
@@ -15223,14 +17209,26 @@ async function uiCommand(args2) {
|
|
|
15223
17209
|
process.exitCode = 1;
|
|
15224
17210
|
return;
|
|
15225
17211
|
}
|
|
15226
|
-
const
|
|
17212
|
+
const repo = repoRoot();
|
|
17213
|
+
if (!await uiSessionExists()) await restoreTabs(bootPath, repo);
|
|
17214
|
+
const code = await launchUi(bootPath, repo);
|
|
15227
17215
|
process.exitCode = code;
|
|
15228
17216
|
}
|
|
17217
|
+
var HOST3;
|
|
15229
17218
|
var init_ui = __esm({
|
|
15230
17219
|
"cli/commands/ui.ts"() {
|
|
15231
17220
|
"use strict";
|
|
15232
17221
|
init_launch();
|
|
15233
17222
|
init_sidebar();
|
|
17223
|
+
init_dialog();
|
|
17224
|
+
init_tabs();
|
|
17225
|
+
init_session();
|
|
17226
|
+
init_launch();
|
|
17227
|
+
init_backend();
|
|
17228
|
+
init_model();
|
|
17229
|
+
init_manifest();
|
|
17230
|
+
init_tmux();
|
|
17231
|
+
HOST3 = { kind: "host" };
|
|
15234
17232
|
}
|
|
15235
17233
|
});
|
|
15236
17234
|
|
|
@@ -15270,12 +17268,12 @@ __export(linear_exports, {
|
|
|
15270
17268
|
formatLinks: () => formatLinks,
|
|
15271
17269
|
linearCommand: () => linearCommand
|
|
15272
17270
|
});
|
|
15273
|
-
import { readFileSync as
|
|
15274
|
-
import { homedir as
|
|
15275
|
-
import { join as
|
|
17271
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
17272
|
+
import { homedir as homedir38 } from "os";
|
|
17273
|
+
import { join as join37 } from "path";
|
|
15276
17274
|
function mcpJwt() {
|
|
15277
17275
|
try {
|
|
15278
|
-
return
|
|
17276
|
+
return readFileSync33(join37(SYNKRO_DIR14, ".mcp-jwt"), "utf-8").trim();
|
|
15279
17277
|
} catch {
|
|
15280
17278
|
return "";
|
|
15281
17279
|
}
|
|
@@ -15314,7 +17312,7 @@ var SYNKRO_DIR14, PORT2, BASE;
|
|
|
15314
17312
|
var init_linear = __esm({
|
|
15315
17313
|
"cli/commands/linear.ts"() {
|
|
15316
17314
|
"use strict";
|
|
15317
|
-
SYNKRO_DIR14 =
|
|
17315
|
+
SYNKRO_DIR14 = join37(homedir38(), ".synkro");
|
|
15318
17316
|
PORT2 = process.env.SYNKRO_MCP_PORT || "18931";
|
|
15319
17317
|
BASE = `http://127.0.0.1:${PORT2}`;
|
|
15320
17318
|
}
|
|
@@ -15322,7 +17320,7 @@ var init_linear = __esm({
|
|
|
15322
17320
|
|
|
15323
17321
|
// cli/scanning/cveReachability.ts
|
|
15324
17322
|
import { parse } from "@babel/parser";
|
|
15325
|
-
import { readFileSync as
|
|
17323
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
15326
17324
|
function walk(node, visit) {
|
|
15327
17325
|
if (!node || typeof node.type !== "string") return;
|
|
15328
17326
|
visit(node);
|
|
@@ -15464,12 +17462,12 @@ var init_cveReachability = __esm({
|
|
|
15464
17462
|
|
|
15465
17463
|
// cli/reachability/reachabilityScan.ts
|
|
15466
17464
|
import { spawnSync as spawnSync12, execFileSync as execFileSync5 } from "child_process";
|
|
15467
|
-
import { readFileSync as
|
|
15468
|
-
import { join as
|
|
15469
|
-
import { homedir as
|
|
17465
|
+
import { readFileSync as readFileSync35, writeFileSync as writeFileSync27, existsSync as existsSync38, readdirSync as readdirSync10 } from "fs";
|
|
17466
|
+
import { join as join38 } from "path";
|
|
17467
|
+
import { homedir as homedir39 } from "os";
|
|
15470
17468
|
import { createRequire } from "module";
|
|
15471
17469
|
function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
|
|
15472
|
-
const
|
|
17470
|
+
const SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".turbo", "out", ".cache", ".synkro", ".claude", "vendor", "__tests__", "test-results"]);
|
|
15473
17471
|
const EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
15474
17472
|
const files = [];
|
|
15475
17473
|
const stack = [repoRoot3];
|
|
@@ -15477,21 +17475,21 @@ function walkSourceFiles(repoRoot3, maxFiles = 4e3, maxBytes = 5e5) {
|
|
|
15477
17475
|
const dir = stack.pop();
|
|
15478
17476
|
let ents;
|
|
15479
17477
|
try {
|
|
15480
|
-
ents =
|
|
17478
|
+
ents = readdirSync10(dir, { withFileTypes: true });
|
|
15481
17479
|
} catch {
|
|
15482
17480
|
continue;
|
|
15483
17481
|
}
|
|
15484
17482
|
for (const e of ents) {
|
|
15485
17483
|
if (files.length >= maxFiles) break;
|
|
15486
|
-
const full =
|
|
17484
|
+
const full = join38(dir, e.name);
|
|
15487
17485
|
if (e.isDirectory()) {
|
|
15488
|
-
if (!
|
|
17486
|
+
if (!SKIP2.has(e.name) && !e.name.startsWith(".")) stack.push(full);
|
|
15489
17487
|
continue;
|
|
15490
17488
|
}
|
|
15491
17489
|
if (!EXT.test(e.name) || e.name.endsWith(".d.ts")) continue;
|
|
15492
17490
|
const rel = full.startsWith(repoRoot3 + "/") ? full.slice(repoRoot3.length + 1) : full;
|
|
15493
17491
|
try {
|
|
15494
|
-
const content =
|
|
17492
|
+
const content = readFileSync35(full, "utf8");
|
|
15495
17493
|
if (content.length <= maxBytes) files.push({ path: rel, content });
|
|
15496
17494
|
} catch {
|
|
15497
17495
|
}
|
|
@@ -15510,12 +17508,12 @@ function cleanVersion(spec) {
|
|
|
15510
17508
|
function gatherManifestVersions(repoRoot3) {
|
|
15511
17509
|
const out = {};
|
|
15512
17510
|
const dirs = [repoRoot3];
|
|
15513
|
-
const pkgsDir =
|
|
15514
|
-
if (
|
|
17511
|
+
const pkgsDir = join38(repoRoot3, "packages");
|
|
17512
|
+
if (existsSync38(pkgsDir)) {
|
|
15515
17513
|
try {
|
|
15516
|
-
for (const d of
|
|
15517
|
-
const pd =
|
|
15518
|
-
if (
|
|
17514
|
+
for (const d of readdirSync10(pkgsDir)) {
|
|
17515
|
+
const pd = join38(pkgsDir, d);
|
|
17516
|
+
if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
|
|
15519
17517
|
}
|
|
15520
17518
|
} catch {
|
|
15521
17519
|
}
|
|
@@ -15524,7 +17522,7 @@ function gatherManifestVersions(repoRoot3) {
|
|
|
15524
17522
|
for (const dir of dirs) {
|
|
15525
17523
|
let pkg;
|
|
15526
17524
|
try {
|
|
15527
|
-
pkg = JSON.parse(
|
|
17525
|
+
pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
|
|
15528
17526
|
} catch {
|
|
15529
17527
|
continue;
|
|
15530
17528
|
}
|
|
@@ -15544,28 +17542,28 @@ function findJelly(repoRoot3) {
|
|
|
15544
17542
|
try {
|
|
15545
17543
|
const pkgJson = require2.resolve("@cs-au-dk/jelly/package.json");
|
|
15546
17544
|
const dir = pkgJson.slice(0, pkgJson.length - "package.json".length);
|
|
15547
|
-
const pkg = JSON.parse(
|
|
17545
|
+
const pkg = JSON.parse(readFileSync35(pkgJson, "utf8"));
|
|
15548
17546
|
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && (pkg.bin.jelly || pkg.bin[Object.keys(pkg.bin)[0]]);
|
|
15549
17547
|
if (bin) {
|
|
15550
|
-
const p =
|
|
15551
|
-
if (
|
|
17548
|
+
const p = join38(dir, bin);
|
|
17549
|
+
if (existsSync38(p)) return p;
|
|
15552
17550
|
}
|
|
15553
17551
|
} catch {
|
|
15554
17552
|
}
|
|
15555
17553
|
for (const base of [repoRoot3, process.cwd()]) {
|
|
15556
|
-
const b =
|
|
15557
|
-
if (
|
|
17554
|
+
const b = join38(base, "node_modules", ".bin", "jelly");
|
|
17555
|
+
if (existsSync38(b)) return b;
|
|
15558
17556
|
}
|
|
15559
17557
|
return null;
|
|
15560
17558
|
}
|
|
15561
17559
|
function findEntries(repoRoot3) {
|
|
15562
17560
|
const dirs = [repoRoot3];
|
|
15563
|
-
const pkgsDir =
|
|
15564
|
-
if (
|
|
17561
|
+
const pkgsDir = join38(repoRoot3, "packages");
|
|
17562
|
+
if (existsSync38(pkgsDir)) {
|
|
15565
17563
|
try {
|
|
15566
|
-
for (const d of
|
|
15567
|
-
const pd =
|
|
15568
|
-
if (
|
|
17564
|
+
for (const d of readdirSync10(pkgsDir)) {
|
|
17565
|
+
const pd = join38(pkgsDir, d);
|
|
17566
|
+
if (existsSync38(join38(pd, "package.json"))) dirs.push(pd);
|
|
15569
17567
|
}
|
|
15570
17568
|
} catch {
|
|
15571
17569
|
}
|
|
@@ -15573,12 +17571,12 @@ function findEntries(repoRoot3) {
|
|
|
15573
17571
|
const entries = [];
|
|
15574
17572
|
for (const dir of dirs) {
|
|
15575
17573
|
try {
|
|
15576
|
-
const pkg = JSON.parse(
|
|
17574
|
+
const pkg = JSON.parse(readFileSync35(join38(dir, "package.json"), "utf8"));
|
|
15577
17575
|
const cands = [pkg.source, pkg.module, pkg.main, "src/index.ts", "src/index.js", "src/main.ts", "src/server.ts", "index.ts", "index.js"];
|
|
15578
17576
|
for (const c of cands) {
|
|
15579
17577
|
if (typeof c !== "string") continue;
|
|
15580
|
-
const f =
|
|
15581
|
-
if (
|
|
17578
|
+
const f = join38(dir, c);
|
|
17579
|
+
if (existsSync38(f)) {
|
|
15582
17580
|
entries.push(f);
|
|
15583
17581
|
break;
|
|
15584
17582
|
}
|
|
@@ -15611,9 +17609,9 @@ function parseApiUsage(log) {
|
|
|
15611
17609
|
}
|
|
15612
17610
|
function runReachabilityScan(repoRoot3, opts = {}) {
|
|
15613
17611
|
const commit = currentCommit(repoRoot3);
|
|
15614
|
-
if (!opts.force && commit &&
|
|
17612
|
+
if (!opts.force && commit && existsSync38(REACHABILITY_PATH)) {
|
|
15615
17613
|
try {
|
|
15616
|
-
const prev = JSON.parse(
|
|
17614
|
+
const prev = JSON.parse(readFileSync35(REACHABILITY_PATH, "utf8"));
|
|
15617
17615
|
if (prev.commit === commit) return { ok: true, cached: true, packages: Object.keys(prev.packages || {}).length };
|
|
15618
17616
|
} catch {
|
|
15619
17617
|
}
|
|
@@ -15702,7 +17700,7 @@ function runReachabilityScan(repoRoot3, opts = {}) {
|
|
|
15702
17700
|
if (Object.keys(packages).length === 0) return { ok: false, reason: "no package usage found (no jelly output, no AST imports)" };
|
|
15703
17701
|
const file = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), commit, tool, packages, versions: gatherManifestVersions(repoRoot3) };
|
|
15704
17702
|
try {
|
|
15705
|
-
|
|
17703
|
+
writeFileSync27(REACHABILITY_PATH, JSON.stringify(file, null, 2));
|
|
15706
17704
|
} catch (e) {
|
|
15707
17705
|
return { ok: false, reason: "write failed: " + String(e.message || e) };
|
|
15708
17706
|
}
|
|
@@ -15714,7 +17712,7 @@ var init_reachabilityScan = __esm({
|
|
|
15714
17712
|
"use strict";
|
|
15715
17713
|
init_cveReachability();
|
|
15716
17714
|
require2 = createRequire(import.meta.url);
|
|
15717
|
-
REACHABILITY_PATH =
|
|
17715
|
+
REACHABILITY_PATH = join38(homedir39(), ".synkro", "reachability.json");
|
|
15718
17716
|
}
|
|
15719
17717
|
});
|
|
15720
17718
|
|
|
@@ -15723,15 +17721,15 @@ var reachabilityScan_exports = {};
|
|
|
15723
17721
|
__export(reachabilityScan_exports, {
|
|
15724
17722
|
reachabilityScanCommand: () => reachabilityScanCommand
|
|
15725
17723
|
});
|
|
15726
|
-
import { readFileSync as
|
|
15727
|
-
import { join as
|
|
15728
|
-
import { homedir as
|
|
17724
|
+
import { readFileSync as readFileSync36, existsSync as existsSync39 } from "fs";
|
|
17725
|
+
import { join as join39 } from "path";
|
|
17726
|
+
import { homedir as homedir40 } from "os";
|
|
15729
17727
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
15730
17728
|
function readConfigEnv4() {
|
|
15731
|
-
const p =
|
|
15732
|
-
if (!
|
|
17729
|
+
const p = join39(SYNKRO_DIR15, "config.env");
|
|
17730
|
+
if (!existsSync39(p)) return {};
|
|
15733
17731
|
const out = {};
|
|
15734
|
-
for (const line of
|
|
17732
|
+
for (const line of readFileSync36(p, "utf-8").split("\n")) {
|
|
15735
17733
|
const t = line.trim();
|
|
15736
17734
|
if (!t || t.startsWith("#")) continue;
|
|
15737
17735
|
const eq = t.indexOf("=");
|
|
@@ -15763,11 +17761,11 @@ async function pushToCloud(cfg, repo) {
|
|
|
15763
17761
|
while (gwBase.endsWith("/")) gwBase = gwBase.slice(0, -1);
|
|
15764
17762
|
let jwt2 = "";
|
|
15765
17763
|
try {
|
|
15766
|
-
jwt2 =
|
|
17764
|
+
jwt2 = readFileSync36(join39(SYNKRO_DIR15, ".mcp-jwt"), "utf-8").trim();
|
|
15767
17765
|
} catch {
|
|
15768
17766
|
}
|
|
15769
|
-
if (!jwt2 || !
|
|
15770
|
-
const body =
|
|
17767
|
+
if (!jwt2 || !existsSync39(REACHABILITY_PATH)) return;
|
|
17768
|
+
const body = readFileSync36(REACHABILITY_PATH, "utf-8");
|
|
15771
17769
|
try {
|
|
15772
17770
|
const resp = await fetch(gwBase + "/api/v1/reachability?repo=" + encodeURIComponent(repo), {
|
|
15773
17771
|
method: "POST",
|
|
@@ -15799,7 +17797,7 @@ var init_reachabilityScan2 = __esm({
|
|
|
15799
17797
|
"cli/commands/reachabilityScan.ts"() {
|
|
15800
17798
|
"use strict";
|
|
15801
17799
|
init_reachabilityScan();
|
|
15802
|
-
SYNKRO_DIR15 =
|
|
17800
|
+
SYNKRO_DIR15 = join39(homedir40(), ".synkro");
|
|
15803
17801
|
}
|
|
15804
17802
|
});
|
|
15805
17803
|
|
|
@@ -15929,13 +17927,13 @@ var config_exports = {};
|
|
|
15929
17927
|
__export(config_exports, {
|
|
15930
17928
|
configCommand: () => configCommand
|
|
15931
17929
|
});
|
|
15932
|
-
import { readFileSync as
|
|
15933
|
-
import { join as
|
|
15934
|
-
import { homedir as
|
|
17930
|
+
import { readFileSync as readFileSync37, writeFileSync as writeFileSync28, existsSync as existsSync40 } from "fs";
|
|
17931
|
+
import { join as join40 } from "path";
|
|
17932
|
+
import { homedir as homedir41 } from "os";
|
|
15935
17933
|
function readConfigEnv5() {
|
|
15936
|
-
if (!
|
|
17934
|
+
if (!existsSync40(CONFIG_PATH9)) return {};
|
|
15937
17935
|
const out = {};
|
|
15938
|
-
for (const line of
|
|
17936
|
+
for (const line of readFileSync37(CONFIG_PATH9, "utf-8").split("\n")) {
|
|
15939
17937
|
const t = line.trim();
|
|
15940
17938
|
if (!t || t.startsWith("#")) continue;
|
|
15941
17939
|
const eq = t.indexOf("=");
|
|
@@ -15944,11 +17942,11 @@ function readConfigEnv5() {
|
|
|
15944
17942
|
return out;
|
|
15945
17943
|
}
|
|
15946
17944
|
function updateConfigValue(key, value) {
|
|
15947
|
-
if (!
|
|
17945
|
+
if (!existsSync40(CONFIG_PATH9)) {
|
|
15948
17946
|
console.error("No config found. Run `synkro install` first.");
|
|
15949
17947
|
process.exit(1);
|
|
15950
17948
|
}
|
|
15951
|
-
const lines =
|
|
17949
|
+
const lines = readFileSync37(CONFIG_PATH9, "utf-8").split("\n");
|
|
15952
17950
|
const pattern = new RegExp(`^${key}=`);
|
|
15953
17951
|
let found = false;
|
|
15954
17952
|
const updated = lines.map((line) => {
|
|
@@ -15959,7 +17957,7 @@ function updateConfigValue(key, value) {
|
|
|
15959
17957
|
return line;
|
|
15960
17958
|
});
|
|
15961
17959
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
15962
|
-
|
|
17960
|
+
writeFileSync28(CONFIG_PATH9, updated.join("\n"), "utf-8");
|
|
15963
17961
|
}
|
|
15964
17962
|
function resolveInferenceMode(cfg) {
|
|
15965
17963
|
if ((cfg.SYNKRO_GRADING_MODE || "local") === "byok") return "byok";
|
|
@@ -16117,8 +18115,8 @@ var init_config = __esm({
|
|
|
16117
18115
|
"use strict";
|
|
16118
18116
|
init_stub();
|
|
16119
18117
|
init_optout();
|
|
16120
|
-
SYNKRO_DIR16 =
|
|
16121
|
-
CONFIG_PATH9 =
|
|
18118
|
+
SYNKRO_DIR16 = join40(homedir41(), ".synkro");
|
|
18119
|
+
CONFIG_PATH9 = join40(SYNKRO_DIR16, "config.env");
|
|
16122
18120
|
}
|
|
16123
18121
|
});
|
|
16124
18122
|
|
|
@@ -16127,7 +18125,7 @@ var telemetry_exports2 = {};
|
|
|
16127
18125
|
__export(telemetry_exports2, {
|
|
16128
18126
|
telemetryCommand: () => telemetryCommand
|
|
16129
18127
|
});
|
|
16130
|
-
import { createInterface as
|
|
18128
|
+
import { createInterface as createInterface6 } from "readline";
|
|
16131
18129
|
function parseFlag(args2, name) {
|
|
16132
18130
|
const prefix = `--${name}=`;
|
|
16133
18131
|
for (const a of args2) if (a.startsWith(prefix)) return a.slice(prefix.length);
|
|
@@ -16209,7 +18207,7 @@ async function runExport(args2) {
|
|
|
16209
18207
|
function confirmYesNo(question) {
|
|
16210
18208
|
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
16211
18209
|
return new Promise((resolve7) => {
|
|
16212
|
-
const rl =
|
|
18210
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
16213
18211
|
rl.question(`${question} (y/N): `, (answer) => {
|
|
16214
18212
|
rl.close();
|
|
16215
18213
|
const t = answer.trim().toLowerCase();
|
|
@@ -16308,11 +18306,11 @@ Usage:
|
|
|
16308
18306
|
|
|
16309
18307
|
// cli/inventory/identity.ts
|
|
16310
18308
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
16311
|
-
import { existsSync as
|
|
16312
|
-
import { homedir as
|
|
16313
|
-
import { dirname as
|
|
18309
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync38, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "fs";
|
|
18310
|
+
import { homedir as homedir42 } from "os";
|
|
18311
|
+
import { dirname as dirname12, join as join41 } from "path";
|
|
16314
18312
|
function operationalIdentityPath() {
|
|
16315
|
-
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH ||
|
|
18313
|
+
return process.env.SYNKRO_OPERATIONAL_IDENTITY_PATH || join41(homedir42(), ".synkro", "installation.json");
|
|
16316
18314
|
}
|
|
16317
18315
|
function validIdentity(value) {
|
|
16318
18316
|
if (!value || typeof value !== "object") return false;
|
|
@@ -16320,17 +18318,17 @@ function validIdentity(value) {
|
|
|
16320
18318
|
return typeof row2.installation_id === "string" && UUID_RE.test(row2.installation_id) && typeof row2.created_at === "string" && Number.isFinite(Date.parse(row2.created_at));
|
|
16321
18319
|
}
|
|
16322
18320
|
function writeIdentity(path, identity) {
|
|
16323
|
-
|
|
18321
|
+
mkdirSync24(dirname12(path), { recursive: true, mode: 448 });
|
|
16324
18322
|
const temp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
16325
|
-
|
|
18323
|
+
writeFileSync29(temp, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
16326
18324
|
renameSync9(temp, path);
|
|
16327
18325
|
}
|
|
16328
18326
|
function getOperationalInstallationIdentity(path = operationalIdentityPath()) {
|
|
16329
18327
|
const prior = cached4.get(path);
|
|
16330
18328
|
if (prior) return prior;
|
|
16331
|
-
if (
|
|
18329
|
+
if (existsSync41(path)) {
|
|
16332
18330
|
try {
|
|
16333
|
-
const parsed = JSON.parse(
|
|
18331
|
+
const parsed = JSON.parse(readFileSync38(path, "utf8"));
|
|
16334
18332
|
if (validIdentity(parsed)) {
|
|
16335
18333
|
cached4.set(path, parsed);
|
|
16336
18334
|
return parsed;
|
|
@@ -16355,13 +18353,13 @@ var init_identity2 = __esm({
|
|
|
16355
18353
|
// cli/inventory/collector.ts
|
|
16356
18354
|
import { createHash as createHash5 } from "crypto";
|
|
16357
18355
|
import {
|
|
16358
|
-
existsSync as
|
|
16359
|
-
readFileSync as
|
|
16360
|
-
readdirSync as
|
|
18356
|
+
existsSync as existsSync42,
|
|
18357
|
+
readFileSync as readFileSync39,
|
|
18358
|
+
readdirSync as readdirSync11,
|
|
16361
18359
|
statSync as statSync5
|
|
16362
18360
|
} from "fs";
|
|
16363
|
-
import { arch, homedir as
|
|
16364
|
-
import { basename as basename3, join as
|
|
18361
|
+
import { arch, homedir as homedir43, hostname as hostname2, platform as platform6, release as release2 } from "os";
|
|
18362
|
+
import { basename as basename3, join as join42, relative, resolve as resolve5 } from "path";
|
|
16365
18363
|
import { fileURLToPath } from "url";
|
|
16366
18364
|
function sha256(value) {
|
|
16367
18365
|
return createHash5("sha256").update(value).digest("hex");
|
|
@@ -16371,15 +18369,15 @@ function pseudonymousHostnameHash(installationId, host) {
|
|
|
16371
18369
|
}
|
|
16372
18370
|
function cliVersion() {
|
|
16373
18371
|
try {
|
|
16374
|
-
return "1.10.
|
|
18372
|
+
return "1.10.5";
|
|
16375
18373
|
} catch {
|
|
16376
18374
|
return "0.0.0";
|
|
16377
18375
|
}
|
|
16378
18376
|
}
|
|
16379
18377
|
function readJson(path) {
|
|
16380
18378
|
try {
|
|
16381
|
-
if (!
|
|
16382
|
-
const parsed = JSON.parse(
|
|
18379
|
+
if (!existsSync42(path)) return null;
|
|
18380
|
+
const parsed = JSON.parse(readFileSync39(path, "utf8"));
|
|
16383
18381
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
16384
18382
|
} catch {
|
|
16385
18383
|
return null;
|
|
@@ -16387,8 +18385,8 @@ function readJson(path) {
|
|
|
16387
18385
|
}
|
|
16388
18386
|
function readText(path) {
|
|
16389
18387
|
try {
|
|
16390
|
-
if (!
|
|
16391
|
-
return
|
|
18388
|
+
if (!existsSync42(path)) return "";
|
|
18389
|
+
return readFileSync39(path, "utf8");
|
|
16392
18390
|
} catch {
|
|
16393
18391
|
return "";
|
|
16394
18392
|
}
|
|
@@ -16474,16 +18472,16 @@ function mcpArtifactsFromJson(harness, config, configScope = "user") {
|
|
|
16474
18472
|
}
|
|
16475
18473
|
function claudeDesktopConfigCandidates(home, targetPlatform) {
|
|
16476
18474
|
if (targetPlatform === "darwin") {
|
|
16477
|
-
return [
|
|
18475
|
+
return [join42(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
16478
18476
|
}
|
|
16479
18477
|
if (targetPlatform === "linux") {
|
|
16480
18478
|
return [
|
|
16481
|
-
|
|
16482
|
-
|
|
18479
|
+
join42(home, ".config", "Claude", "claude_desktop_config.json"),
|
|
18480
|
+
join42(home, ".config", "claude", "claude_desktop_config.json")
|
|
16483
18481
|
];
|
|
16484
18482
|
}
|
|
16485
18483
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
16486
|
-
return [
|
|
18484
|
+
return [join42(process.env.APPDATA, "Claude", "claude_desktop_config.json")];
|
|
16487
18485
|
}
|
|
16488
18486
|
return [];
|
|
16489
18487
|
}
|
|
@@ -16491,7 +18489,7 @@ function claudeManagedMcpConfigCandidates(targetPlatform) {
|
|
|
16491
18489
|
if (targetPlatform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-mcp.json"];
|
|
16492
18490
|
if (targetPlatform === "linux") return ["/etc/claude-code/managed-mcp.json"];
|
|
16493
18491
|
if (targetPlatform === "win32" && process.env.ProgramFiles) {
|
|
16494
|
-
return [
|
|
18492
|
+
return [join42(process.env.ProgramFiles, "ClaudeCode", "managed-mcp.json")];
|
|
16495
18493
|
}
|
|
16496
18494
|
return [];
|
|
16497
18495
|
}
|
|
@@ -16500,7 +18498,7 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
16500
18498
|
const add = (value) => {
|
|
16501
18499
|
if (typeof value !== "string" || !value.trim()) return;
|
|
16502
18500
|
const path = resolve5(value);
|
|
16503
|
-
if (
|
|
18501
|
+
if (existsSync42(path)) roots.add(path);
|
|
16504
18502
|
};
|
|
16505
18503
|
add(currentDirectory);
|
|
16506
18504
|
for (const path of explicit) add(path);
|
|
@@ -16511,31 +18509,31 @@ function discoveredProjectRoots(claudeState, currentDirectory, explicit = [], cu
|
|
|
16511
18509
|
return [...roots];
|
|
16512
18510
|
}
|
|
16513
18511
|
function cursorWorkspaceStorageCandidates(home, targetPlatform) {
|
|
16514
|
-
if (targetPlatform === "darwin") return [
|
|
16515
|
-
if (targetPlatform === "linux") return [
|
|
18512
|
+
if (targetPlatform === "darwin") return [join42(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage")];
|
|
18513
|
+
if (targetPlatform === "linux") return [join42(home, ".config", "Cursor", "User", "workspaceStorage")];
|
|
16516
18514
|
if (targetPlatform === "win32" && process.env.APPDATA) {
|
|
16517
|
-
return [
|
|
18515
|
+
return [join42(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
|
|
16518
18516
|
}
|
|
16519
18517
|
return [];
|
|
16520
18518
|
}
|
|
16521
18519
|
function cursorWorkspaceRoots(home, targetPlatform) {
|
|
16522
18520
|
const roots = /* @__PURE__ */ new Set();
|
|
16523
18521
|
for (const storage of cursorWorkspaceStorageCandidates(home, targetPlatform)) {
|
|
16524
|
-
if (!
|
|
18522
|
+
if (!existsSync42(storage)) continue;
|
|
16525
18523
|
let entries = [];
|
|
16526
18524
|
try {
|
|
16527
|
-
entries =
|
|
18525
|
+
entries = readdirSync11(storage, { withFileTypes: true });
|
|
16528
18526
|
} catch {
|
|
16529
18527
|
continue;
|
|
16530
18528
|
}
|
|
16531
18529
|
for (const entry of entries) {
|
|
16532
18530
|
if (!entry.isDirectory() || entry.isSymbolicLink?.()) continue;
|
|
16533
|
-
const state = readJson(
|
|
18531
|
+
const state = readJson(join42(storage, entry.name, "workspace.json"));
|
|
16534
18532
|
const raw = state?.folder;
|
|
16535
18533
|
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
16536
18534
|
try {
|
|
16537
18535
|
const path = raw.startsWith("file:") ? fileURLToPath(raw) : raw;
|
|
16538
|
-
if (
|
|
18536
|
+
if (existsSync42(path)) roots.add(resolve5(path));
|
|
16539
18537
|
} catch {
|
|
16540
18538
|
}
|
|
16541
18539
|
}
|
|
@@ -16629,18 +18627,18 @@ function parseFrontmatter(content) {
|
|
|
16629
18627
|
return { name: value("name"), version: value("version") };
|
|
16630
18628
|
}
|
|
16631
18629
|
function skillArtifacts(harness, root) {
|
|
16632
|
-
if (!
|
|
18630
|
+
if (!existsSync42(root)) return [];
|
|
16633
18631
|
const manifests = [];
|
|
16634
18632
|
const visit = (dir) => {
|
|
16635
18633
|
let entries;
|
|
16636
18634
|
try {
|
|
16637
|
-
entries =
|
|
18635
|
+
entries = readdirSync11(dir, { withFileTypes: true });
|
|
16638
18636
|
} catch {
|
|
16639
18637
|
return;
|
|
16640
18638
|
}
|
|
16641
18639
|
for (const entry of entries) {
|
|
16642
18640
|
if (entry.isSymbolicLink?.()) continue;
|
|
16643
|
-
const path =
|
|
18641
|
+
const path = join42(dir, entry.name);
|
|
16644
18642
|
if (entry.isFile() && entry.name === "SKILL.md") manifests.push(path);
|
|
16645
18643
|
else if (entry.isDirectory()) visit(path);
|
|
16646
18644
|
}
|
|
@@ -16650,7 +18648,7 @@ function skillArtifacts(harness, root) {
|
|
|
16650
18648
|
const content = readText(path);
|
|
16651
18649
|
const frontmatter = parseFrontmatter(content);
|
|
16652
18650
|
const rel = relative(root, path).replaceAll("\\", "/");
|
|
16653
|
-
const name = frontmatter.name || basename3(
|
|
18651
|
+
const name = frontmatter.name || basename3(join42(path, "..")) || "skill";
|
|
16654
18652
|
return {
|
|
16655
18653
|
harness,
|
|
16656
18654
|
type: "skill",
|
|
@@ -16665,16 +18663,16 @@ function skillArtifacts(harness, root) {
|
|
|
16665
18663
|
});
|
|
16666
18664
|
}
|
|
16667
18665
|
function cursorExtensionArtifacts(root) {
|
|
16668
|
-
if (!
|
|
18666
|
+
if (!existsSync42(root)) return [];
|
|
16669
18667
|
let dirs = [];
|
|
16670
18668
|
try {
|
|
16671
|
-
dirs =
|
|
18669
|
+
dirs = readdirSync11(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink());
|
|
16672
18670
|
} catch {
|
|
16673
18671
|
return [];
|
|
16674
18672
|
}
|
|
16675
18673
|
const artifacts = [];
|
|
16676
18674
|
for (const dir of dirs) {
|
|
16677
|
-
const pkg = readJson(
|
|
18675
|
+
const pkg = readJson(join42(root, dir.name, "package.json"));
|
|
16678
18676
|
if (!pkg) continue;
|
|
16679
18677
|
const publisher = typeof pkg.publisher === "string" ? pkg.publisher : void 0;
|
|
16680
18678
|
const name = typeof pkg.name === "string" ? pkg.name : dir.name;
|
|
@@ -16694,21 +18692,21 @@ function cursorExtensionArtifacts(root) {
|
|
|
16694
18692
|
return artifacts;
|
|
16695
18693
|
}
|
|
16696
18694
|
function deploymentMode2(home) {
|
|
16697
|
-
const raw = readText(
|
|
18695
|
+
const raw = readText(join42(home, ".synkro", "config.env"));
|
|
16698
18696
|
const value = (key) => raw.match(new RegExp(`^${key}=['"]?([^'"\\n]*)`, "m"))?.[1]?.toLowerCase();
|
|
16699
18697
|
if (value("SYNKRO_GRADING_MODE") === "byok") return "byok";
|
|
16700
18698
|
if (value("SYNKRO_STORAGE_MODE") === "cloud") return "cloud";
|
|
16701
18699
|
return "local";
|
|
16702
18700
|
}
|
|
16703
18701
|
function telemetryHealth(home) {
|
|
16704
|
-
const meta = readJson(
|
|
18702
|
+
const meta = readJson(join42(home, ".synkro", "telemetry-meta.json"));
|
|
16705
18703
|
const health = {};
|
|
16706
18704
|
if (meta?.last_flush_ok_at && Number.isFinite(Date.parse(meta.last_flush_ok_at))) health.telemetry_last_flush_at = meta.last_flush_ok_at;
|
|
16707
18705
|
if (meta?.last_flush_error) health.telemetry_last_error = "flush_failed";
|
|
16708
|
-
const queue =
|
|
18706
|
+
const queue = join42(home, ".synkro", "telemetry-pending.jsonl");
|
|
16709
18707
|
try {
|
|
16710
18708
|
const size = statSync5(queue).size;
|
|
16711
|
-
health.telemetry_backlog = size <= 5 * 1024 * 1024 ?
|
|
18709
|
+
health.telemetry_backlog = size <= 5 * 1024 * 1024 ? readFileSync39(queue, "utf8").split("\n").filter(Boolean).length : Math.ceil(size / 1024);
|
|
16712
18710
|
} catch {
|
|
16713
18711
|
}
|
|
16714
18712
|
return health;
|
|
@@ -16749,7 +18747,7 @@ function harnessSnapshot(agent) {
|
|
|
16749
18747
|
}
|
|
16750
18748
|
const config = readJson(agent.settingsPath);
|
|
16751
18749
|
const coverage = inspectCodexHooks(agent.settingsPath);
|
|
16752
|
-
const toml = readText(
|
|
18750
|
+
const toml = readText(join42(agent.configDir, "config.toml"));
|
|
16753
18751
|
const permission = toml.match(/^\s*approval_policy\s*=\s*["']([^"']+)/m)?.[1];
|
|
16754
18752
|
return {
|
|
16755
18753
|
row: {
|
|
@@ -16766,11 +18764,11 @@ function harnessSnapshot(agent) {
|
|
|
16766
18764
|
};
|
|
16767
18765
|
}
|
|
16768
18766
|
function collectOperationalInventory(options = {}) {
|
|
16769
|
-
const home = options.homeDir ??
|
|
18767
|
+
const home = options.homeDir ?? homedir43();
|
|
16770
18768
|
const detected = options.detectedAgents ?? detectAgents();
|
|
16771
18769
|
const identity = getOperationalInstallationIdentity(options.identityPath);
|
|
16772
|
-
const targetPlatform = options.platformName ??
|
|
16773
|
-
const codexHome = options.homeDir ?
|
|
18770
|
+
const targetPlatform = options.platformName ?? platform6();
|
|
18771
|
+
const codexHome = options.homeDir ? join42(home, ".codex") : process.env.CODEX_HOME || join42(home, ".codex");
|
|
16774
18772
|
const harnesses = [];
|
|
16775
18773
|
const artifacts = [];
|
|
16776
18774
|
for (const agent of detected) {
|
|
@@ -16778,7 +18776,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
16778
18776
|
harnesses.push(row2);
|
|
16779
18777
|
artifacts.push(...hookArtifacts(row2.harness, config));
|
|
16780
18778
|
}
|
|
16781
|
-
const claudeJson = readJson(
|
|
18779
|
+
const claudeJson = readJson(join42(home, ".claude.json"));
|
|
16782
18780
|
artifacts.push(...mcpArtifactsFromJson("claude_code", claudeJson));
|
|
16783
18781
|
if (claudeJson?.projects && typeof claudeJson.projects === "object") {
|
|
16784
18782
|
for (const [projectPath, project] of Object.entries(claudeJson.projects)) {
|
|
@@ -16786,8 +18784,8 @@ function collectOperationalInventory(options = {}) {
|
|
|
16786
18784
|
artifacts.push(...mcpArtifactsFromJson("claude_code", project, `local:${sha256(projectPath).slice(0, 16)}`));
|
|
16787
18785
|
}
|
|
16788
18786
|
}
|
|
16789
|
-
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(
|
|
16790
|
-
artifacts.push(...codexMcpArtifacts(readText(
|
|
18787
|
+
artifacts.push(...mcpArtifactsFromJson("cursor", readJson(join42(home, ".cursor", "mcp.json"))));
|
|
18788
|
+
artifacts.push(...codexMcpArtifacts(readText(join42(codexHome, "config.toml"))));
|
|
16791
18789
|
const projectRoots = discoveredProjectRoots(
|
|
16792
18790
|
claudeJson,
|
|
16793
18791
|
options.currentDirectory ?? process.cwd(),
|
|
@@ -16798,11 +18796,11 @@ function collectOperationalInventory(options = {}) {
|
|
|
16798
18796
|
const scopeHash = sha256(projectRoot).slice(0, 16);
|
|
16799
18797
|
artifacts.push(...mcpArtifactsFromJson(
|
|
16800
18798
|
"claude_code",
|
|
16801
|
-
readJson(
|
|
18799
|
+
readJson(join42(projectRoot, ".mcp.json")),
|
|
16802
18800
|
`project:${scopeHash}`
|
|
16803
18801
|
));
|
|
16804
|
-
const cursorProjectConfig =
|
|
16805
|
-
if (resolve5(cursorProjectConfig) !== resolve5(
|
|
18802
|
+
const cursorProjectConfig = join42(projectRoot, ".cursor", "mcp.json");
|
|
18803
|
+
if (resolve5(cursorProjectConfig) !== resolve5(join42(home, ".cursor", "mcp.json"))) {
|
|
16806
18804
|
artifacts.push(...mcpArtifactsFromJson(
|
|
16807
18805
|
"cursor",
|
|
16808
18806
|
readJson(cursorProjectConfig),
|
|
@@ -16813,7 +18811,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
16813
18811
|
for (const managedPath of claudeManagedMcpConfigCandidates(targetPlatform)) {
|
|
16814
18812
|
artifacts.push(...mcpArtifactsFromJson("claude_code", readJson(managedPath), "managed"));
|
|
16815
18813
|
}
|
|
16816
|
-
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) =>
|
|
18814
|
+
const desktopConfigPath = claudeDesktopConfigCandidates(home, targetPlatform).find((path) => existsSync42(path));
|
|
16817
18815
|
if (desktopConfigPath) {
|
|
16818
18816
|
const desktopConfig = readJson(desktopConfigPath);
|
|
16819
18817
|
harnesses.push({
|
|
@@ -16824,7 +18822,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
16824
18822
|
});
|
|
16825
18823
|
artifacts.push(...mcpArtifactsFromJson("claude_desktop", desktopConfig));
|
|
16826
18824
|
}
|
|
16827
|
-
const claudeSettings = readJson(
|
|
18825
|
+
const claudeSettings = readJson(join42(home, ".claude", "settings.json"));
|
|
16828
18826
|
if (claudeSettings?.enabledPlugins && typeof claudeSettings.enabledPlugins === "object") {
|
|
16829
18827
|
for (const [name, enabled] of Object.entries(claudeSettings.enabledPlugins)) {
|
|
16830
18828
|
artifacts.push({
|
|
@@ -16838,10 +18836,10 @@ function collectOperationalInventory(options = {}) {
|
|
|
16838
18836
|
});
|
|
16839
18837
|
}
|
|
16840
18838
|
}
|
|
16841
|
-
artifacts.push(...skillArtifacts("claude_code",
|
|
16842
|
-
artifacts.push(...skillArtifacts("cursor",
|
|
16843
|
-
artifacts.push(...skillArtifacts("codex",
|
|
16844
|
-
artifacts.push(...cursorExtensionArtifacts(
|
|
18839
|
+
artifacts.push(...skillArtifacts("claude_code", join42(home, ".claude", "skills")));
|
|
18840
|
+
artifacts.push(...skillArtifacts("cursor", join42(home, ".cursor", "skills")));
|
|
18841
|
+
artifacts.push(...skillArtifacts("codex", join42(codexHome, "skills")));
|
|
18842
|
+
artifacts.push(...cursorExtensionArtifacts(join42(home, ".cursor", "extensions")));
|
|
16845
18843
|
const uniqueArtifacts = /* @__PURE__ */ new Map();
|
|
16846
18844
|
for (const artifact of artifacts) {
|
|
16847
18845
|
const key = `${artifact.harness || "global"}:${artifact.type}:${artifact.canonical_id}`;
|
|
@@ -16859,7 +18857,7 @@ function collectOperationalInventory(options = {}) {
|
|
|
16859
18857
|
install_id: identity.installation_id,
|
|
16860
18858
|
hostname_hash: pseudonymousHostnameHash(identity.installation_id, hostname2()),
|
|
16861
18859
|
platform: targetPlatform,
|
|
16862
|
-
os_version:
|
|
18860
|
+
os_version: release2(),
|
|
16863
18861
|
arch: arch(),
|
|
16864
18862
|
cli_version: cliVersion(),
|
|
16865
18863
|
node_version: process.version,
|
|
@@ -16894,22 +18892,22 @@ __export(sync_exports2, {
|
|
|
16894
18892
|
syncOperationalInventoryDetached: () => syncOperationalInventoryDetached
|
|
16895
18893
|
});
|
|
16896
18894
|
import { createHash as createHash6, randomUUID as randomUUID6 } from "crypto";
|
|
16897
|
-
import { spawn as
|
|
18895
|
+
import { spawn as spawn10 } from "child_process";
|
|
16898
18896
|
import {
|
|
16899
|
-
existsSync as
|
|
16900
|
-
mkdirSync as
|
|
16901
|
-
readFileSync as
|
|
18897
|
+
existsSync as existsSync43,
|
|
18898
|
+
mkdirSync as mkdirSync25,
|
|
18899
|
+
readFileSync as readFileSync40,
|
|
16902
18900
|
renameSync as renameSync10,
|
|
16903
|
-
writeFileSync as
|
|
18901
|
+
writeFileSync as writeFileSync30
|
|
16904
18902
|
} from "fs";
|
|
16905
|
-
import { homedir as
|
|
16906
|
-
import { dirname as
|
|
18903
|
+
import { homedir as homedir44 } from "os";
|
|
18904
|
+
import { dirname as dirname13, join as join43 } from "path";
|
|
16907
18905
|
function syncStatePath() {
|
|
16908
|
-
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH ||
|
|
18906
|
+
return process.env.SYNKRO_INVENTORY_SYNC_STATE_PATH || join43(homedir44(), ".synkro", "inventory-sync.json");
|
|
16909
18907
|
}
|
|
16910
18908
|
function readState(path = syncStatePath()) {
|
|
16911
18909
|
try {
|
|
16912
|
-
const parsed = JSON.parse(
|
|
18910
|
+
const parsed = JSON.parse(readFileSync40(path, "utf8"));
|
|
16913
18911
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
16914
18912
|
} catch {
|
|
16915
18913
|
return {};
|
|
@@ -16917,9 +18915,9 @@ function readState(path = syncStatePath()) {
|
|
|
16917
18915
|
}
|
|
16918
18916
|
function writeState(state, path = syncStatePath()) {
|
|
16919
18917
|
try {
|
|
16920
|
-
|
|
18918
|
+
mkdirSync25(dirname13(path), { recursive: true, mode: 448 });
|
|
16921
18919
|
const temp = `${path}.${process.pid}.tmp`;
|
|
16922
|
-
|
|
18920
|
+
writeFileSync30(temp, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
16923
18921
|
renameSync10(temp, path);
|
|
16924
18922
|
} catch {
|
|
16925
18923
|
}
|
|
@@ -16932,10 +18930,10 @@ function shouldSyncInventory(state, now = Date.now(), target) {
|
|
|
16932
18930
|
return !Number.isFinite(lastAttempt) || lastAttempt <= 0 || now - lastAttempt >= FAILURE_RETRY_MS;
|
|
16933
18931
|
}
|
|
16934
18932
|
function readConfig() {
|
|
16935
|
-
const path =
|
|
18933
|
+
const path = join43(homedir44(), ".synkro", "config.env");
|
|
16936
18934
|
const out = {};
|
|
16937
18935
|
try {
|
|
16938
|
-
for (const rawLine of
|
|
18936
|
+
for (const rawLine of readFileSync40(path, "utf8").split("\n")) {
|
|
16939
18937
|
const line = rawLine.trim();
|
|
16940
18938
|
if (!line || line.startsWith("#")) continue;
|
|
16941
18939
|
const index = line.indexOf("=");
|
|
@@ -16974,7 +18972,7 @@ function resolveInventoryGateway(raw) {
|
|
|
16974
18972
|
}
|
|
16975
18973
|
async function loadToken() {
|
|
16976
18974
|
try {
|
|
16977
|
-
const durable =
|
|
18975
|
+
const durable = readFileSync40(join43(homedir44(), ".synkro", ".mcp-jwt"), "utf8").trim();
|
|
16978
18976
|
if (durable) return durable;
|
|
16979
18977
|
} catch {
|
|
16980
18978
|
}
|
|
@@ -17092,8 +19090,8 @@ function syncOperationalInventoryDetached() {
|
|
|
17092
19090
|
writeState({ ...state, last_attempt_at: (/* @__PURE__ */ new Date()).toISOString(), last_target: target }, path);
|
|
17093
19091
|
try {
|
|
17094
19092
|
const script = process.argv[1];
|
|
17095
|
-
if (!script || !
|
|
17096
|
-
const child =
|
|
19093
|
+
if (!script || !existsSync43(script)) return;
|
|
19094
|
+
const child = spawn10(process.execPath, [script, "inventory-sync", "--detached"], {
|
|
17097
19095
|
detached: true,
|
|
17098
19096
|
stdio: "ignore",
|
|
17099
19097
|
env: { ...process.env, SYNKRO_INVENTORY_DETACHED: "1" }
|
|
@@ -17116,14 +19114,15 @@ var init_sync2 = __esm({
|
|
|
17116
19114
|
});
|
|
17117
19115
|
|
|
17118
19116
|
// cli/bootstrap.js
|
|
17119
|
-
import { readFileSync as
|
|
19117
|
+
import { readFileSync as readFileSync41, existsSync as existsSync44 } from "fs";
|
|
17120
19118
|
import { resolve as resolve6 } from "path";
|
|
19119
|
+
process.title = "synkro";
|
|
17121
19120
|
var envCandidates = [
|
|
17122
19121
|
resolve6(process.env.HOME ?? "", ".synkro", "config.env")
|
|
17123
19122
|
];
|
|
17124
19123
|
for (const envPath of envCandidates) {
|
|
17125
|
-
if (!
|
|
17126
|
-
const envContent =
|
|
19124
|
+
if (!existsSync44(envPath)) continue;
|
|
19125
|
+
const envContent = readFileSync41(envPath, "utf-8");
|
|
17127
19126
|
for (const line of envContent.split("\n")) {
|
|
17128
19127
|
const trimmed = line.trim();
|
|
17129
19128
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -17140,7 +19139,7 @@ var subArgs = args.slice(1);
|
|
|
17140
19139
|
var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
|
|
17141
19140
|
var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "inventory-sync", "version", "--version", "-v", "help", "--help", "-h", ""]);
|
|
17142
19141
|
function printVersion() {
|
|
17143
|
-
console.log("1.10.
|
|
19142
|
+
console.log("1.10.5");
|
|
17144
19143
|
}
|
|
17145
19144
|
function printHelp2() {
|
|
17146
19145
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|