@synkro-sh/cli 1.6.58 → 1.6.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bootstrap.js CHANGED
@@ -509,6 +509,8 @@ var init_cursorHookConfig = __esm({
509
509
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
510
510
  import { homedir as homedir3 } from "os";
511
511
  import { dirname as dirname3, join as join2 } from "path";
512
+ import { spawnSync } from "child_process";
513
+ import { randomBytes } from "crypto";
512
514
  function readClaudeJson() {
513
515
  if (!existsSync3(CC_CONFIG_PATH)) return {};
514
516
  try {
@@ -630,7 +632,69 @@ function uninstallCursorMcpConfig() {
630
632
  writeCursorMcpJsonAtomic(config);
631
633
  return true;
632
634
  }
633
- var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH;
635
+ function resolveBunBin() {
636
+ const r = spawnSync("which", ["bun"], { encoding: "utf-8", timeout: 5e3 });
637
+ const resolved = (r.stdout || "").split("\n")[0].trim();
638
+ if (resolved) return resolved;
639
+ for (const p of ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join2(homedir3(), ".bun", "bin", "bun")]) {
640
+ if (existsSync3(p)) return p;
641
+ }
642
+ return "bun";
643
+ }
644
+ function readDesktopJson() {
645
+ if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
646
+ try {
647
+ return JSON.parse(readFileSync3(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
648
+ } catch (err) {
649
+ throw new Error(`Failed to parse ${CLAUDE_DESKTOP_CONFIG_PATH}: ${err.message}`);
650
+ }
651
+ }
652
+ function writeDesktopJsonAtomic(config) {
653
+ mkdirSync3(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
654
+ const tmpPath = `${CLAUDE_DESKTOP_CONFIG_PATH}.synkro.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
655
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
656
+ renameSync3(tmpPath, CLAUDE_DESKTOP_CONFIG_PATH);
657
+ }
658
+ function installClaudeDesktopMcpConfig() {
659
+ if (!existsSync3(MCP_STDIO_PROXY_PATH)) {
660
+ return {
661
+ ok: false,
662
+ path: CLAUDE_DESKTOP_CONFIG_PATH,
663
+ proxy: MCP_STDIO_PROXY_PATH,
664
+ reason: "stdio proxy not found \u2014 run `synkro install` first"
665
+ };
666
+ }
667
+ const config = readDesktopJson();
668
+ config.mcpServers = config.mcpServers ?? {};
669
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
670
+ if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
671
+ }
672
+ config.mcpServers[SYNKRO_SERVER_NAME] = {
673
+ command: resolveBunBin(),
674
+ args: ["run", MCP_STDIO_PROXY_PATH],
675
+ env: { SYNKRO_MCP_PORT: process.env.SYNKRO_MCP_PORT || "18931" },
676
+ [SYNKRO_MARKER3]: true
677
+ };
678
+ writeDesktopJsonAtomic(config);
679
+ return { ok: true, path: CLAUDE_DESKTOP_CONFIG_PATH, proxy: MCP_STDIO_PROXY_PATH };
680
+ }
681
+ function uninstallClaudeDesktopMcpConfig() {
682
+ if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
683
+ const config = readDesktopJson();
684
+ if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
685
+ let removed = false;
686
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
687
+ if (entry?.[SYNKRO_MARKER3] === true) {
688
+ delete config.mcpServers[name];
689
+ removed = true;
690
+ }
691
+ }
692
+ if (!removed) return false;
693
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
694
+ writeDesktopJsonAtomic(config);
695
+ return true;
696
+ }
697
+ var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH, CLAUDE_DESKTOP_CONFIG_PATH, MCP_STDIO_PROXY_PATH;
634
698
  var init_mcpConfig = __esm({
635
699
  "cli/installer/mcpConfig.ts"() {
636
700
  "use strict";
@@ -638,6 +702,14 @@ var init_mcpConfig = __esm({
638
702
  SYNKRO_SERVER_NAME = "synkro-guardrails";
639
703
  CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
640
704
  CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
705
+ CLAUDE_DESKTOP_CONFIG_PATH = join2(
706
+ homedir3(),
707
+ "Library",
708
+ "Application Support",
709
+ "Claude",
710
+ "claude_desktop_config.json"
711
+ );
712
+ MCP_STDIO_PROXY_PATH = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
641
713
  }
642
714
  });
643
715
 
@@ -1595,6 +1667,28 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
1595
1667
  return String(data.verdict || '');
1596
1668
  }
1597
1669
 
1670
+ // Cloud-container grading \u2014 the org's grader runs on Cloudflare Containers. Same
1671
+ // request/response shape as channelGrade (the host-host Worker forwards verbatim
1672
+ // to the org's dispatcher /submit), but to the hosted ingress with the gateway
1673
+ // JWT (the Worker derives the org from the token and routes to that org's
1674
+ // container). Any non-2xx throws so the caller's catch fails open.
1675
+ const CONTAINERS_URL = process.env.SYNKRO_CONTAINERS_URL || 'https://containers.synkro.sh';
1676
+ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
1677
+ const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
1678
+ method: 'POST',
1679
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1680
+ body: JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind }),
1681
+ signal: AbortSignal.timeout(timeoutMs),
1682
+ });
1683
+ if (!resp.ok) {
1684
+ const text = await resp.text().catch(() => '');
1685
+ throw new Error('hosted grade ' + resp.status + ': ' + text.slice(0, 200));
1686
+ }
1687
+ const data = await resp.json() as { result?: string; error?: string };
1688
+ if (data.error) throw new Error(data.error);
1689
+ return String(data.result || '');
1690
+ }
1691
+
1598
1692
  // Default 24s \u2014 MUST stay below the CC hook timeout (30s for edit/bash/cwe in
1599
1693
  // ccHookConfig.ts) so the AbortSignal fires and the caller's catch fails open
1600
1694
  // cleanly. If this matches or exceeds the CC budget, CC force-kills the bun
@@ -1603,6 +1697,11 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
1603
1697
  export async function localGrade(surface: string, prompt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
1604
1698
  const jwt = loadJwt();
1605
1699
  if (!jwt) throw new Error('NO_JWT');
1700
+ // Cloud-container: the grader runs on Cloudflare Containers \u2014 route to the
1701
+ // hosted ingress instead of the local channel. Same prompt + parseVerdict.
1702
+ if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
1703
+ return hostedGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, timeoutMs, agentKind);
1704
+ }
1606
1705
  // BYOK grading mode routes the grade through an LLM API instead of the
1607
1706
  // on-device channel worker pool. The grader prompt + parseVerdict are shared.
1608
1707
  if ((process.env.SYNKRO_GRADING_MODE || 'local') === 'byok') {
@@ -1618,6 +1717,9 @@ export async function localGrade(surface: string, prompt: string, timeoutMs = 24
1618
1717
  export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code', timeoutMs = 24000): Promise<string> {
1619
1718
  const jwt = loadJwt();
1620
1719
  if (!jwt) throw new Error('NO_JWT');
1720
+ if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
1721
+ return hostedGrade('grade-cwe', prompt, jwt, timeoutMs, agentKind);
1722
+ }
1621
1723
  return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
1622
1724
  }
1623
1725
 
@@ -7491,7 +7593,7 @@ echo ""
7491
7593
  wait $MITM_PID
7492
7594
  `;
7493
7595
  }
7494
- async function runClaudeDesktopTap() {
7596
+ async function runClaudeDesktopTap(opts = {}) {
7495
7597
  if (process.platform !== "darwin") {
7496
7598
  console.log(" Claude Desktop observability is macOS-only \u2014 skipped.");
7497
7599
  return;
@@ -7505,6 +7607,20 @@ async function runClaudeDesktopTap() {
7505
7607
  console.log(" Could not read local auth token (~/.synkro/.mcp-jwt). Run `synkro install` first.");
7506
7608
  return;
7507
7609
  }
7610
+ if (opts.backfill) {
7611
+ console.log(" History backfill enabled \u2014 importing past conversations once this session.");
7612
+ }
7613
+ try {
7614
+ const mcp = installClaudeDesktopMcpConfig();
7615
+ if (mcp.ok) {
7616
+ console.log(` \u2713 Synkro standards MCP registered in Claude Desktop`);
7617
+ console.log(` (${mcp.path}) \u2014 restart Claude Desktop to load the synkro-guardrails tools.`);
7618
+ } else {
7619
+ console.log(` \u26A0 Claude Desktop MCP not registered: ${mcp.reason}`);
7620
+ }
7621
+ } catch (err) {
7622
+ console.log(` \u26A0 Could not register Claude Desktop MCP: ${err.message}`);
7623
+ }
7508
7624
  const cdRoot = join5(homedir5(), ".synkro", "cd-sessions");
7509
7625
  mkdirSync6(cdRoot, { recursive: true });
7510
7626
  const sessionDir = mkdtempSync(join5(cdRoot, "synkro-cd-"));
@@ -7516,7 +7632,7 @@ async function runClaudeDesktopTap() {
7516
7632
  await new Promise((resolve4) => {
7517
7633
  const child = spawn("bash", [runnerPath], {
7518
7634
  stdio: "inherit",
7519
- env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH }
7635
+ env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_MCP_EVENT_URL: MCP_EVENT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH, SYNKRO_CD_BACKFILL: opts.backfill ? "1" : "" }
7520
7636
  });
7521
7637
  const forward = (sig) => {
7522
7638
  try {
@@ -7533,16 +7649,18 @@ async function runClaudeDesktopTap() {
7533
7649
  });
7534
7650
  });
7535
7651
  }
7536
- var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_URL, TURN_VERDICTS_URL, TURN_VERDICT_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
7652
+ var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_URL, TURN_VERDICTS_URL, TURN_VERDICT_URL, MCP_EVENT_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
7537
7653
  var init_claudeDesktopTap = __esm({
7538
7654
  "cli/installer/claudeDesktopTap.ts"() {
7539
7655
  "use strict";
7656
+ init_mcpConfig();
7540
7657
  CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
7541
7658
  SCAN_URL = "http://127.0.0.1:18931/api/local/scan-upload";
7542
7659
  SCAN_TURN_URL = "http://127.0.0.1:18931/api/local/scan-turn";
7543
7660
  DLP_POLICY_URL = "http://127.0.0.1:18931/api/local/dlp-policy";
7544
7661
  TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
7545
7662
  TURN_VERDICT_URL = "http://127.0.0.1:18931/api/local/turn-verdict";
7663
+ MCP_EVENT_URL = "http://127.0.0.1:18931/api/local/mcp/event";
7546
7664
  JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
7547
7665
  ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
7548
7666
  from mitmproxy import http
@@ -7559,6 +7677,11 @@ SCAN_TURN_URL = _safe_url(os.environ.get("SYNKRO_SCAN_TURN_URL", ""))
7559
7677
  POLICY_URL = _safe_url(os.environ.get("SYNKRO_DLP_POLICY_URL", ""))
7560
7678
  TURN_VERDICTS_URL = _safe_url(os.environ.get("SYNKRO_TURN_VERDICTS_URL", ""))
7561
7679
  TURN_VERDICT_URL = _safe_url(os.environ.get("SYNKRO_TURN_VERDICT_URL", ""))
7680
+ MCP_EVENT_URL = _safe_url(os.environ.get("SYNKRO_MCP_EVENT_URL", ""))
7681
+ # History backfill is OPT-IN: only when the user runs synkro claude-desktop with
7682
+ # --backfill. Otherwise importing past conversations on every tap launch is wasted
7683
+ # work (and re-floods the feed). When set, it also bypasses the 12h throttle marker.
7684
+ BACKFILL_ENABLED = os.environ.get("SYNKRO_CD_BACKFILL", "") == "1"
7562
7685
  # Token file path \u2014 the SERVER_TOKEN rotates whenever the container is recreated,
7563
7686
  # so we re-read it from disk on a 401 instead of needing a tap restart.
7564
7687
  TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "")
@@ -7624,6 +7747,151 @@ def _fmt_tool_name(raw):
7624
7747
  pass
7625
7748
  return raw
7626
7749
 
7750
+ def _map_full_conversation(all_msgs):
7751
+ # Full conversation [{role, content, tool_calls?}] + 'tool' result messages, in
7752
+ # order, for the session-log tables (the Session Timeline card on the event page).
7753
+ # Lighter than the per-turn capture: role + text + tool_calls + tool results.
7754
+ out = []
7755
+ for msg in all_msgs:
7756
+ sender = msg.get("sender", "")
7757
+ role = "user" if sender == "human" else "assistant"
7758
+ blocks = msg.get("content", [])
7759
+ texts = []; tcs = []; results = []
7760
+ if isinstance(blocks, list):
7761
+ for b in blocks:
7762
+ if isinstance(b, str): texts.append(b); continue
7763
+ if not isinstance(b, dict): continue
7764
+ bt = b.get("type", "")
7765
+ if bt == "text":
7766
+ texts.append(b.get("text", ""))
7767
+ elif bt == "tool_use" or bt == "server_tool_use":
7768
+ tcs.append({"name": _fmt_tool_name(b.get("name", "")), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))[:2000]})
7769
+ elif bt.endswith("_tool_result") or bt == "tool_result":
7770
+ rc = b.get("content", "")
7771
+ if isinstance(rc, list):
7772
+ rc = " | ".join(str(x.get("text") or x.get("content") or "") for x in rc if isinstance(x, dict))
7773
+ if rc and str(rc).strip():
7774
+ results.append({"role": "tool", "content": str(rc)[:8000], "name": _fmt_tool_name(b.get("name", ""))})
7775
+ elif isinstance(blocks, str):
7776
+ texts.append(blocks)
7777
+ content = " ".join(t for t in texts if t).strip()
7778
+ if content or tcs:
7779
+ e = {"role": role, "content": content}
7780
+ if tcs: e["tool_calls"] = tcs
7781
+ out.append(e)
7782
+ for r in results: out.append(r)
7783
+ if len(out) >= 2000: break
7784
+ return out
7785
+
7786
+ def _emit_mcp_event(server, tool, ev_type, eid, convo_id=""):
7787
+ # Post a Claude Desktop connector (MCP) lifecycle/tool_call event to the same
7788
+ # mcp_events store the CC/Cursor mcp-gate writes, so Desktop connectors show on
7789
+ # the MCP Servers page. eid is deterministic so re-captures dedupe (server
7790
+ # ON CONFLICT DO NOTHING). Fire-and-forget; never blocks the proxied request.
7791
+ if not (MCP_EVENT_URL and TOKEN and server): return
7792
+ try:
7793
+ payload = json.dumps({
7794
+ "event_id": eid,
7795
+ "session_id": (str(convo_id)[:80] or None),
7796
+ "server_name": str(server)[:120],
7797
+ "event_type": ev_type,
7798
+ "tool_name": (str(tool)[:120] if tool else None),
7799
+ "decision": ("allowed" if ev_type == "tool_call" else None),
7800
+ "repo": "claude-desktop",
7801
+ }).encode("utf-8")
7802
+ _post_json(MCP_EVENT_URL, payload, 5)
7803
+ except Exception:
7804
+ pass
7805
+
7806
+ # \u2500\u2500 History backfill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7807
+ # One-time pull of the user's past claude.ai conversations into the local store, so
7808
+ # the dashboard shows full Desktop history (like CC's transcript import) \u2014 not just
7809
+ # turns tapped live. Auth (session cookie + org uuid) is captured from a live request
7810
+ # and kept IN MEMORY (never written to disk). Store-only: backfilled turns are not
7811
+ # graded (backfill=True). Runs at most once per 12h via a timestamp marker.
7812
+ #
7813
+ # SSRF (CWE-918): the only host we ever fetch is claude.ai, and org/conversation ids
7814
+ # must be UUID-shaped \u2014 both validated before any URL is built, so no untrusted value
7815
+ # can redirect the fetch to an arbitrary host/path.
7816
+ _backfill_lock = threading.Lock()
7817
+ _backfill_started = False
7818
+ _BACKFILL_MARKER = os.path.expanduser("~/.synkro/.cd-backfill-done")
7819
+
7820
+ def _is_claude_host(h):
7821
+ return h == "claude.ai" or (isinstance(h, str) and h.endswith(".claude.ai"))
7822
+
7823
+ def _is_uuidish(s):
7824
+ return bool(re.match(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", s or ""))
7825
+
7826
+ def _http_get(url, headers, timeout=25):
7827
+ req = urllib.request.Request(url, headers=headers, method="GET")
7828
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
7829
+ return resp.read()
7830
+
7831
+ def _post_backfill_convo(convo, cid):
7832
+ all_msgs = convo.get("chat_messages", [])
7833
+ if not all_msgs: return
7834
+ model = convo.get("model") or "claude-unknown"
7835
+ if not model.startswith("claude-"): model = "claude-" + model
7836
+ session_messages = _map_full_conversation(all_msgs)
7837
+ if not session_messages: return
7838
+ msgs = []
7839
+ for e in session_messages:
7840
+ mm = {"role": e.get("role", "user"), "content": e.get("content", "")}
7841
+ if e.get("tool_calls"): mm["tool_calls"] = e["tool_calls"]
7842
+ msgs.append(mm)
7843
+ # Real conversation timestamp (last activity, fallback created) so the server
7844
+ # stamps created_at to when the convo HAPPENED \u2014 not "now". Without this every
7845
+ # backfill run re-stamps all history to the present and floods it to the top of
7846
+ # the feed. Must be ISO8601; the server re-validates and only honors it for
7847
+ # backfill rows.
7848
+ cts = ""
7849
+ for k in ("updated_at", "created_at"):
7850
+ v = convo.get(k) if isinstance(convo, dict) else None
7851
+ if isinstance(v, str) and re.match(r"^d{4}-d{2}-d{2}T", v):
7852
+ cts = v; break
7853
+ data = {"request": {"model": model, "messages": msgs[-200:]}, "conversation_id": cid,
7854
+ "session_messages": session_messages, "turn_id": "cdbf_" + cid, "backfill": True}
7855
+ if cts: data["event_ts"] = cts
7856
+ _post_json(CAPTURE_URL, json.dumps(data).encode("utf-8"), 8)
7857
+
7858
+ def _backfill(host, org, cookie):
7859
+ if not (_is_claude_host(host) and _is_uuidish(org)): return # defence in depth
7860
+ base = "https://" + host
7861
+ headers = {"Cookie": cookie, "Accept": "application/json", "User-Agent": "synkro-tap"}
7862
+ try:
7863
+ convos = json.loads(_http_get(base + "/api/organizations/" + org + "/chat_conversations?limit=200", headers, 25))
7864
+ if not isinstance(convos, list): return
7865
+ except Exception:
7866
+ with _backfill_lock:
7867
+ globals()["_backfill_started"] = False # allow a later request to retry
7868
+ return
7869
+ print("[synkro] backfill: importing up to " + str(min(len(convos), 200)) + " conversations")
7870
+ n = 0
7871
+ for c in convos[:200]:
7872
+ cid = c.get("uuid", "") if isinstance(c, dict) else ""
7873
+ if not _is_uuidish(cid): continue
7874
+ try:
7875
+ convo = json.loads(_http_get(base + "/api/organizations/" + org + "/chat_conversations/" + cid + "?tree=True&rendering_mode=messages&render_all_tools=true", headers, 25))
7876
+ _post_backfill_convo(convo, cid)
7877
+ n += 1
7878
+ except Exception:
7879
+ pass
7880
+ time.sleep(0.6)
7881
+ try: open(_BACKFILL_MARKER, "w").write(str(int(time.time())))
7882
+ except Exception: pass
7883
+ print("[synkro] backfill done: " + str(n) + " conversations imported")
7884
+
7885
+ def _maybe_backfill(host, org, cookie):
7886
+ # Opt-in only: skip entirely unless the user launched with --backfill.
7887
+ if not BACKFILL_ENABLED: return
7888
+ if not (_is_claude_host(host) and _is_uuidish(org) and cookie and CAPTURE_URL and TOKEN): return
7889
+ with _backfill_lock:
7890
+ global _backfill_started
7891
+ if _backfill_started: return
7892
+ _backfill_started = True
7893
+ threading.Thread(target=_backfill, args=(host, org, cookie), daemon=True).start()
7894
+
7627
7895
  # \u2500\u2500 M3 live-hold state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7628
7896
  # Whether to HOLD connector turns (buffer the live completion until graded).
7629
7897
  # Fetched once from the server: True only when a BLOCK-mode rule exists (+ not
@@ -8094,9 +8362,16 @@ class SynkroTap:
8094
8362
  return
8095
8363
  if verdict.get("block"):
8096
8364
  reason = str(verdict.get("reason", "Blocked by Synkro policy"))[:300]
8365
+ msg = "Blocked by Synkro: " + reason
8366
+ # Return a TERMINAL 400 (invalid_request_error), NOT 403. Claude Desktop's
8367
+ # upload client treats 403 as transient/auth and auto-RETRIES it \u2014 that's the
8368
+ # loop the user sees. A 400 invalid_request_error is non-retryable (the SDK/UI
8369
+ # only retry 408/409/429/5xx), so the client stops and surfaces the message.
8370
+ # Anthropic's standard error envelope ({"type":"error","error":{...}}) is what
8371
+ # the UI knows how to render, so the user sees the reason instead of a spinner.
8097
8372
  flow.response = http.Response.make(
8098
- 403,
8099
- json.dumps({"error": {"type": "blocked_by_synkro", "message": "Blocked by Synkro: " + reason}}).encode(),
8373
+ 400,
8374
+ json.dumps({"type": "error", "error": {"type": "invalid_request_error", "message": msg}}).encode(),
8100
8375
  {"Content-Type": "application/json"},
8101
8376
  )
8102
8377
  print("[synkro] BLOCKED upload (size=" + str(payload.get("size", 0)) + " rule=" + str(verdict.get("ruleId", "")) + ")")
@@ -8173,6 +8448,12 @@ class SynkroTap:
8173
8448
  _mcp_registry.clear()
8174
8449
  _mcp_registry.update(servers)
8175
8450
  print("[synkro] MCP servers: " + str(len(servers)))
8451
+ # Surface each connector on the MCP Servers page (connect/disconnect).
8452
+ for sid, s in servers.items():
8453
+ nm = s.get("name", "")
8454
+ if nm:
8455
+ ev = "connect" if s.get("connected") else "disconnect"
8456
+ _emit_mcp_event(nm, "", ev, "cdmcp_" + ev + "_" + str(sid))
8176
8457
  except Exception:
8177
8458
  pass
8178
8459
  return
@@ -8182,6 +8463,12 @@ class SynkroTap:
8182
8463
  except: return
8183
8464
  m = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
8184
8465
  convo_id = m.group(1) if m else ""
8466
+ # One-time history backfill using THIS request's session auth (in-memory only).
8467
+ try:
8468
+ _om = re.search(r"/organizations/([0-9a-fA-F-]+)/", r.path)
8469
+ _maybe_backfill(r.pretty_host, _om.group(1) if _om else "", r.headers.get("cookie", "") or r.headers.get("Cookie", ""))
8470
+ except Exception:
8471
+ pass
8185
8472
  model = convo.get("model", "unknown")
8186
8473
  settings = convo.get("settings") or {}
8187
8474
  emt = settings.get("enabled_mcp_tools")
@@ -8214,7 +8501,14 @@ class SynkroTap:
8214
8501
  elif bt == "thinking":
8215
8502
  thinking += b.get("thinking", "") or b.get("text", "")
8216
8503
  elif bt == "tool_use" or bt == "server_tool_use":
8217
- tool_calls.append({"name": _fmt_tool_name(b.get("name", "")), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
8504
+ _nm = _fmt_tool_name(b.get("name", ""))
8505
+ tool_calls.append({"name": _nm, "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
8506
+ # A "Server:tool" name means a hosted MCP connector ran \u2192 log it
8507
+ # on the MCP Servers page (":" only present for registry matches,
8508
+ # so built-in tools like web_search are excluded).
8509
+ if ":" in _nm:
8510
+ _srv, _, _tl = _nm.partition(":")
8511
+ _emit_mcp_event(_srv, _tl, "tool_call", "cdmcp_tc_" + (b.get("id", "") or hashlib.sha256((convo_id + _nm).encode()).hexdigest()[:24]), convo_id)
8218
8512
  elif bt.endswith("_tool_result") or bt == "tool_result":
8219
8513
  rc = b.get("content", "")
8220
8514
  if isinstance(rc, list):
@@ -8251,6 +8545,8 @@ class SynkroTap:
8251
8545
  messages.append({"role": "user", "content": text})
8252
8546
  if not messages: return
8253
8547
  data = {"request": {"model": model, "messages": messages}, "conversation_id": convo_id}
8548
+ # Full conversation for the session-log tables (Session Timeline card).
8549
+ data["session_messages"] = _map_full_conversation(all_msgs)
8254
8550
  # PER-TURN key: the current turn's assistant message id (same key used for
8255
8551
  # dlp_turn_verdicts). Lets the server store each turn as its OWN event so a
8256
8552
  # blocked turn isn't overwritten/deleted by a later passing turn.
@@ -8481,16 +8777,16 @@ __export(macKeychain_exports, {
8481
8777
  writeCursorApiKey: () => writeCursorApiKey,
8482
8778
  writeRefreshAgent: () => writeRefreshAgent
8483
8779
  });
8484
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
8780
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6 } from "fs";
8485
8781
  import { homedir as homedir6, platform as platform2 } from "os";
8486
8782
  import { join as join6 } from "path";
8487
- import { spawnSync } from "child_process";
8783
+ import { spawnSync as spawnSync2 } from "child_process";
8488
8784
  function needsKeychainBridge() {
8489
8785
  return platform2() === "darwin";
8490
8786
  }
8491
8787
  function readKeychainCreds() {
8492
8788
  if (platform2() !== "darwin") return null;
8493
- const r = spawnSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
8789
+ const r = spawnSync2("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
8494
8790
  encoding: "utf-8",
8495
8791
  timeout: 5e3
8496
8792
  });
@@ -8503,7 +8799,14 @@ function exportKeychainCreds() {
8503
8799
  if (!blob) return null;
8504
8800
  mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
8505
8801
  chmodSync(CLAUDE_CREDS_DIR, 448);
8506
- writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
8802
+ let changed = true;
8803
+ try {
8804
+ if (existsSync8(CLAUDE_CREDS_FILE)) {
8805
+ changed = readFileSync6(CLAUDE_CREDS_FILE, "utf-8") !== blob;
8806
+ }
8807
+ } catch {
8808
+ }
8809
+ if (changed) writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
8507
8810
  chmodSync(CLAUDE_CREDS_FILE, 384);
8508
8811
  return CLAUDE_CREDS_FILE;
8509
8812
  }
@@ -8546,18 +8849,49 @@ async function validateCursorApiKey() {
8546
8849
  function credsAreStale() {
8547
8850
  if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
8548
8851
  try {
8549
- const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
8550
- return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
8852
+ const raw = readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
8853
+ const exp = JSON.parse(raw)?.claudeAiOauth?.expiresAt ?? 0;
8854
+ if (!exp) return true;
8855
+ return Date.now() >= exp - REFRESH_EXPIRY_BUFFER_SECONDS * 1e3;
8551
8856
  } catch {
8552
8857
  return true;
8553
8858
  }
8554
8859
  }
8860
+ function xmlEscape(s) {
8861
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
8862
+ }
8555
8863
  function writeRefreshAgent(synkroBinPath) {
8556
8864
  if (platform2() !== "darwin") {
8557
8865
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
8558
8866
  }
8559
8867
  mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
8560
- const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
8868
+ mkdirSync7(SYNKRO_DIR, { recursive: true });
8869
+ const script = `#!/bin/bash
8870
+ # Generated by synkro (writeRefreshAgent). Expiry-aware Claude creds refresher.
8871
+ # SYNKRO_BIN / SYNKRO_CF are injected by launchd as environment data \u2014 never
8872
+ # interpolated into this script \u2014 so there is no shell-injection surface.
8873
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
8874
+ prev=""
8875
+ while true; do
8876
+ { "$SYNKRO_BIN" local-cc refresh-creds || synkro local-cc refresh-creds; } >/dev/null 2>&1
8877
+ exp=$(sed -n 's/.*"expiresAt"[: ]*\\([0-9]\\{10,\\}\\).*/\\1/p' "$SYNKRO_CF" 2>/dev/null | head -1)
8878
+ now=$(date +%s)
8879
+ if [ -n "$exp" ]; then
8880
+ delay=$(( exp/1000 - now - ${REFRESH_EXPIRY_BUFFER_SECONDS} ))
8881
+ else
8882
+ delay=${MIN_REFRESH_INTERVAL_SECONDS}
8883
+ fi
8884
+ [ "$delay" -lt ${MIN_REFRESH_INTERVAL_SECONDS} ] && delay=${MIN_REFRESH_INTERVAL_SECONDS}
8885
+ [ "$delay" -gt ${MAX_REFRESH_INTERVAL_SECONDS} ] && delay=${MAX_REFRESH_INTERVAL_SECONDS}
8886
+ if [ "$exp" != "$prev" ]; then
8887
+ echo "[$(date '+%Y-%m-%dT%H:%M:%S')] claude creds refreshed (expiresAt=\${exp:-none}); next check in \${delay}s"
8888
+ prev="$exp"
8889
+ fi
8890
+ sleep "$delay"
8891
+ done
8892
+ `;
8893
+ writeFileSync7(REFRESH_SCRIPT, script, "utf-8");
8894
+ chmodSync(REFRESH_SCRIPT, 448);
8561
8895
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
8562
8896
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
8563
8897
  <plist version="1.0">
@@ -8567,17 +8901,25 @@ function writeRefreshAgent(synkroBinPath) {
8567
8901
  <key>ProgramArguments</key>
8568
8902
  <array>
8569
8903
  <string>/bin/bash</string>
8570
- <string>-c</string>
8571
- <string>${shellCmd}</string>
8904
+ <string>${xmlEscape(REFRESH_SCRIPT)}</string>
8572
8905
  </array>
8573
- <key>StartInterval</key>
8574
- <integer>${REFRESH_INTERVAL_SECONDS}</integer>
8906
+ <key>EnvironmentVariables</key>
8907
+ <dict>
8908
+ <key>SYNKRO_BIN</key>
8909
+ <string>${xmlEscape(synkroBinPath)}</string>
8910
+ <key>SYNKRO_CF</key>
8911
+ <string>${xmlEscape(CLAUDE_CREDS_FILE)}</string>
8912
+ </dict>
8913
+ <key>KeepAlive</key>
8914
+ <true/>
8575
8915
  <key>RunAtLoad</key>
8576
8916
  <true/>
8917
+ <key>ThrottleInterval</key>
8918
+ <integer>${MIN_REFRESH_INTERVAL_SECONDS}</integer>
8577
8919
  <key>StandardErrorPath</key>
8578
- <string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
8920
+ <string>${xmlEscape(REFRESH_LOG)}</string>
8579
8921
  <key>StandardOutPath</key>
8580
- <string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
8922
+ <string>${xmlEscape(REFRESH_LOG)}</string>
8581
8923
  </dict>
8582
8924
  </plist>
8583
8925
  `;
@@ -8586,11 +8928,11 @@ function writeRefreshAgent(synkroBinPath) {
8586
8928
  }
8587
8929
  function loadRefreshAgent() {
8588
8930
  if (platform2() !== "darwin") return;
8589
- spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8931
+ spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8590
8932
  encoding: "utf-8",
8591
8933
  timeout: 5e3
8592
8934
  });
8593
- const r = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8935
+ const r = spawnSync2("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8594
8936
  encoding: "utf-8",
8595
8937
  timeout: 5e3
8596
8938
  });
@@ -8602,14 +8944,14 @@ function loadRefreshAgent() {
8602
8944
  }
8603
8945
  function uninstallRefreshAgent() {
8604
8946
  if (platform2() !== "darwin") return;
8605
- spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8947
+ spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
8606
8948
  encoding: "utf-8",
8607
8949
  timeout: 5e3
8608
8950
  });
8609
8951
  try {
8610
- if (existsSync8(LAUNCHD_PLIST)) {
8611
- __require("fs").unlinkSync(LAUNCHD_PLIST);
8612
- }
8952
+ const fs = __require("fs");
8953
+ if (existsSync8(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
8954
+ if (existsSync8(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
8613
8955
  } catch {
8614
8956
  }
8615
8957
  }
@@ -8624,7 +8966,7 @@ function readExportedCreds() {
8624
8966
  return null;
8625
8967
  }
8626
8968
  }
8627
- var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_INTERVAL_SECONDS, KeychainExportError;
8969
+ var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_SCRIPT, REFRESH_LOG, REFRESH_EXPIRY_BUFFER_SECONDS, MIN_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, KeychainExportError;
8628
8970
  var init_macKeychain = __esm({
8629
8971
  "cli/local-cc/macKeychain.ts"() {
8630
8972
  "use strict";
@@ -8636,7 +8978,11 @@ var init_macKeychain = __esm({
8636
8978
  KEYCHAIN_SERVICE = "Claude Code-credentials";
8637
8979
  LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
8638
8980
  LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
8639
- REFRESH_INTERVAL_SECONDS = 45 * 60;
8981
+ REFRESH_SCRIPT = join6(SYNKRO_DIR, "claude-creds-refresh-loop.sh");
8982
+ REFRESH_LOG = join6(SYNKRO_DIR, "claude-creds-refresh.log");
8983
+ REFRESH_EXPIRY_BUFFER_SECONDS = 120;
8984
+ MIN_REFRESH_INTERVAL_SECONDS = 30;
8985
+ MAX_REFRESH_INTERVAL_SECONDS = 5 * 60;
8640
8986
  KeychainExportError = class extends Error {
8641
8987
  constructor(message, cause) {
8642
8988
  super(message);
@@ -8675,7 +9021,7 @@ __export(dockerInstall_exports, {
8675
9021
  import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
8676
9022
  import { homedir as homedir7 } from "os";
8677
9023
  import { join as join7 } from "path";
8678
- import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
9024
+ import { execSync as execSync4, spawnSync as spawnSync3 } from "child_process";
8679
9025
  function splitWorkers(total, providers) {
8680
9026
  const t = Math.max(0, Math.floor(total));
8681
9027
  const hasClaude = providers.includes("claude_code");
@@ -8834,7 +9180,7 @@ function imageTag() {
8834
9180
  return registry ? `${registry.replace(/\/+$/, "")}/${tag.replace(/^.*\//, "")}` : tag;
8835
9181
  }
8836
9182
  function assertDockerAvailable() {
8837
- const v = spawnSync2("docker", ["version", "--format", "{{.Server.Version}}"], {
9183
+ const v = spawnSync3("docker", ["version", "--format", "{{.Server.Version}}"], {
8838
9184
  encoding: "utf-8",
8839
9185
  timeout: 5e3
8840
9186
  });
@@ -8849,12 +9195,12 @@ function claudeCredsHostDir() {
8849
9195
  return join7(homedir7(), ".claude");
8850
9196
  }
8851
9197
  function resolveSynkroBin() {
8852
- const which2 = spawnSync2("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
9198
+ const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
8853
9199
  const resolved = (which2.stdout || "").split("\n")[0].trim();
8854
9200
  return resolved || "synkro";
8855
9201
  }
8856
9202
  function sweepHostPglite() {
8857
- const ps = spawnSync2("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
9203
+ const ps = spawnSync3("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
8858
9204
  if (ps.status !== 0) return;
8859
9205
  const targets = [];
8860
9206
  for (const line of (ps.stdout || "").split("\n")) {
@@ -8884,7 +9230,7 @@ function sweepHostPglite() {
8884
9230
  }
8885
9231
  });
8886
9232
  if (alive.length === 0) return;
8887
- spawnSync2("sleep", ["0.2"], { timeout: 1e3 });
9233
+ spawnSync3("sleep", ["0.2"], { timeout: 1e3 });
8888
9234
  }
8889
9235
  for (const pid of targets) {
8890
9236
  try {
@@ -8936,13 +9282,13 @@ async function dockerInstall(opts = {}) {
8936
9282
  } else {
8937
9283
  mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
8938
9284
  }
8939
- const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
9285
+ const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
8940
9286
  const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
8941
9287
  if (skipPull && imageExistsLocally()) {
8942
9288
  console.log(` Using local image ${image} (SYNKRO_SKIP_PULL set \u2014 pull skipped).`);
8943
9289
  } else {
8944
9290
  console.log(` Pulling ${image}...`);
8945
- const pull = spawnSync2("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
9291
+ const pull = spawnSync3("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
8946
9292
  if (pull.status !== 0) {
8947
9293
  if (imageExistsLocally()) {
8948
9294
  console.warn(` \u26A0 docker pull ${image} failed \u2014 falling back to the existing local image.`);
@@ -8956,7 +9302,7 @@ async function dockerInstall(opts = {}) {
8956
9302
  console.log(" Stopping existing container gracefully...");
8957
9303
  await dockerSafeStop();
8958
9304
  }
8959
- spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
9305
+ spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
8960
9306
  sweepHostPglite();
8961
9307
  const credsDir = claudeCredsHostDir();
8962
9308
  const args2 = [
@@ -9014,7 +9360,7 @@ async function dockerInstall(opts = {}) {
9014
9360
  ...process.env.SYNKRO_EMAIL ? ["-e", `SYNKRO_EMAIL=${process.env.SYNKRO_EMAIL}`] : [],
9015
9361
  image
9016
9362
  ];
9017
- const run = spawnSync2("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
9363
+ const run = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
9018
9364
  if (run.status !== 0) {
9019
9365
  throw new DockerInstallError(`docker run failed (image ${image})`);
9020
9366
  }
@@ -9050,11 +9396,11 @@ async function waitForWorkersReady(timeoutMs = 6e4) {
9050
9396
  return false;
9051
9397
  }
9052
9398
  function dockerRemove() {
9053
- spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
9399
+ spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
9054
9400
  }
9055
9401
  function dockerStop() {
9056
- spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
9057
- spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
9402
+ spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
9403
+ spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
9058
9404
  }
9059
9405
  async function dockerUpdate(opts = {}) {
9060
9406
  if (dockerStatus().running) {
@@ -9064,7 +9410,7 @@ async function dockerUpdate(opts = {}) {
9064
9410
  await dockerInstall(opts);
9065
9411
  }
9066
9412
  function dockerStatus() {
9067
- const r = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
9413
+ const r = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
9068
9414
  encoding: "utf-8",
9069
9415
  timeout: 5e3
9070
9416
  });
@@ -9077,7 +9423,7 @@ function dockerStatus() {
9077
9423
  };
9078
9424
  }
9079
9425
  function readContainerConfig() {
9080
- const r = spawnSync2("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
9426
+ const r = spawnSync3("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
9081
9427
  encoding: "utf-8",
9082
9428
  timeout: 5e3
9083
9429
  });
@@ -9131,11 +9477,11 @@ async function dockerSafeStop() {
9131
9477
  console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
9132
9478
  }
9133
9479
  console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
9134
- const stop = spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
9480
+ const stop = spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
9135
9481
  encoding: "utf-8",
9136
9482
  timeout: 45e3
9137
9483
  });
9138
- const inspect = spawnSync2("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
9484
+ const inspect = spawnSync3("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
9139
9485
  encoding: "utf-8",
9140
9486
  timeout: 5e3
9141
9487
  });
@@ -9159,7 +9505,7 @@ async function dockerSafeStart() {
9159
9505
  console.log(" Container is already running.");
9160
9506
  return { ok: true, pgdataState: "running" };
9161
9507
  }
9162
- const exists = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
9508
+ const exists = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
9163
9509
  encoding: "utf-8",
9164
9510
  timeout: 5e3
9165
9511
  });
@@ -9179,7 +9525,7 @@ async function dockerSafeStart() {
9179
9525
  mkdirSync8(PGDATA_PATH, { recursive: true });
9180
9526
  }
9181
9527
  console.log(" Starting container...");
9182
- const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
9528
+ const start = spawnSync3("docker", ["start", CONTAINER_NAME], {
9183
9529
  encoding: "utf-8",
9184
9530
  timeout: 3e4
9185
9531
  });
@@ -9247,6 +9593,56 @@ var init_dockerInstall = __esm({
9247
9593
  }
9248
9594
  });
9249
9595
 
9596
+ // cli/local-cc/setupToken.ts
9597
+ import { spawn as nodeSpawn } from "child_process";
9598
+ import { readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
9599
+ import { homedir as homedir8, platform as platform3 } from "os";
9600
+ import { join as join8 } from "path";
9601
+ function captureClaudeSetupToken() {
9602
+ const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
9603
+ const isMac = platform3() === "darwin";
9604
+ const bin = "script";
9605
+ const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
9606
+ return new Promise((resolve4, reject) => {
9607
+ const proc = nodeSpawn(bin, args2, { stdio: "inherit" });
9608
+ proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
9609
+ proc.on("close", (code) => {
9610
+ let raw = "";
9611
+ try {
9612
+ raw = readFileSync8(tmpFile, "utf-8");
9613
+ } catch (e) {
9614
+ reject(new Error(`Could not read script output file: ${e.message}`));
9615
+ return;
9616
+ }
9617
+ try {
9618
+ unlinkSync3(tmpFile);
9619
+ } catch {
9620
+ }
9621
+ if (code !== 0) {
9622
+ reject(new Error(`claude setup-token exited with code ${code}`));
9623
+ return;
9624
+ }
9625
+ const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
9626
+ let yellow = "";
9627
+ let m;
9628
+ while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
9629
+ const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
9630
+ if (!token) {
9631
+ reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
9632
+ return;
9633
+ }
9634
+ resolve4(token[0]);
9635
+ });
9636
+ });
9637
+ }
9638
+ var SYNKRO_DIR3;
9639
+ var init_setupToken = __esm({
9640
+ "cli/local-cc/setupToken.ts"() {
9641
+ "use strict";
9642
+ SYNKRO_DIR3 = join8(homedir8(), ".synkro");
9643
+ }
9644
+ });
9645
+
9250
9646
  // cli/commands/setupGithub.ts
9251
9647
  var setupGithub_exports = {};
9252
9648
  __export(setupGithub_exports, {
@@ -9255,15 +9651,15 @@ __export(setupGithub_exports, {
9255
9651
  });
9256
9652
  import { createInterface as createInterface2 } from "readline/promises";
9257
9653
  import { stdin as input, stdout as output } from "process";
9258
- import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
9259
- import { existsSync as existsSync10, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
9260
- import { homedir as homedir8, platform as platform3 } from "os";
9261
- import { join as join8 } from "path";
9654
+ import { execSync as execSync5 } from "child_process";
9655
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
9656
+ import { homedir as homedir9, platform as platform4 } from "os";
9657
+ import { join as join9 } from "path";
9262
9658
  import { execFile as execFile2 } from "child_process";
9263
9659
  function readConfig() {
9264
9660
  if (!existsSync10(CONFIG_PATH)) return {};
9265
9661
  const out = {};
9266
- for (const line of readFileSync8(CONFIG_PATH, "utf-8").split("\n")) {
9662
+ for (const line of readFileSync9(CONFIG_PATH, "utf-8").split("\n")) {
9267
9663
  const t = line.trim();
9268
9664
  if (!t || t.startsWith("#")) continue;
9269
9665
  const eq = t.indexOf("=");
@@ -9300,7 +9696,7 @@ async function prompt(rl, q, opts = {}) {
9300
9696
  return await rl.question(q);
9301
9697
  }
9302
9698
  function openBrowser3(url) {
9303
- const os = platform3();
9699
+ const os = platform4();
9304
9700
  let bin;
9305
9701
  let args2;
9306
9702
  switch (os) {
@@ -9323,42 +9719,6 @@ function openBrowser3(url) {
9323
9719
  function sleep(ms) {
9324
9720
  return new Promise((r) => setTimeout(r, ms));
9325
9721
  }
9326
- function captureClaudeSetupToken() {
9327
- const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
9328
- return new Promise((resolve4, reject) => {
9329
- const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
9330
- stdio: "inherit"
9331
- });
9332
- proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
9333
- proc.on("close", (code) => {
9334
- let raw = "";
9335
- try {
9336
- raw = readFileSync8(tmpFile, "utf-8");
9337
- } catch (e) {
9338
- reject(new Error(`Could not read script output file: ${e.message}`));
9339
- return;
9340
- }
9341
- try {
9342
- unlinkSync3(tmpFile);
9343
- } catch {
9344
- }
9345
- if (code !== 0) {
9346
- reject(new Error(`claude setup-token exited with code ${code}`));
9347
- return;
9348
- }
9349
- const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
9350
- let yellow = "";
9351
- let m;
9352
- while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
9353
- const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
9354
- if (!token) {
9355
- reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
9356
- return;
9357
- }
9358
- resolve4(token[0]);
9359
- });
9360
- });
9361
- }
9362
9722
  async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
9363
9723
  const resp = await fetch(`${gatewayUrl}${path}`, {
9364
9724
  ...opts,
@@ -9599,14 +9959,15 @@ Will push secrets to ${selected.length} repo(s):`);
9599
9959
  console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
9600
9960
  console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
9601
9961
  }
9602
- var SYNKRO_DIR3, CONFIG_PATH;
9962
+ var SYNKRO_DIR4, CONFIG_PATH;
9603
9963
  var init_setupGithub = __esm({
9604
9964
  "cli/commands/setupGithub.ts"() {
9605
9965
  "use strict";
9606
9966
  init_githubSetup();
9607
9967
  init_stub();
9608
- SYNKRO_DIR3 = join8(homedir8(), ".synkro");
9609
- CONFIG_PATH = join8(SYNKRO_DIR3, "config.env");
9968
+ init_setupToken();
9969
+ SYNKRO_DIR4 = join9(homedir9(), ".synkro");
9970
+ CONFIG_PATH = join9(SYNKRO_DIR4, "config.env");
9610
9971
  }
9611
9972
  });
9612
9973
 
@@ -9616,20 +9977,21 @@ __export(install_exports, {
9616
9977
  detectGitRepo: () => detectGitRepo2,
9617
9978
  installCommand: () => installCommand,
9618
9979
  parseArgs: () => parseArgs,
9980
+ reconcileDeployLocation: () => reconcileDeployLocation,
9619
9981
  reconcileHarness: () => reconcileHarness,
9620
9982
  syncSkillFiles: () => syncSkillFiles,
9621
9983
  writeHookScripts: () => writeHookScripts
9622
9984
  });
9623
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
9624
- import { homedir as homedir9 } from "os";
9625
- import { join as join9 } from "path";
9985
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync } from "fs";
9986
+ import { homedir as homedir10 } from "os";
9987
+ import { join as join10 } from "path";
9626
9988
  import { execSync as execSync6 } from "child_process";
9627
9989
  import { createInterface as createInterface3 } from "readline";
9628
9990
  function resolvePersistedHookMode() {
9629
9991
  if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
9630
9992
  if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
9631
9993
  try {
9632
- const env = readFileSync9(CONFIG_PATH2, "utf-8");
9994
+ const env = readFileSync10(CONFIG_PATH2, "utf-8");
9633
9995
  if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
9634
9996
  } catch {
9635
9997
  }
@@ -9649,6 +10011,7 @@ function parseArgs(argv) {
9649
10011
  else if (a === "--no-mcp") opts.noMcp = true;
9650
10012
  else if (a === "--force" || a === "-f") opts.force = true;
9651
10013
  else if (a === "--link-repo") opts.linkRepo = true;
10014
+ else if (a === "--byok-gateway") opts.byokGateway = true;
9652
10015
  else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
9653
10016
  else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
9654
10017
  }
@@ -9721,25 +10084,12 @@ async function promptCursorApiKey(opts) {
9721
10084
  console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
9722
10085
  }
9723
10086
  }
9724
- async function promptGradingMode() {
10087
+ async function promptDeployLocation() {
9725
10088
  if (!process.stdin.isTTY) return "local";
9726
10089
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
9727
10090
  return new Promise((resolve4) => {
9728
10091
  rl.question(
9729
- "Where should grading run?\n local \u2014 on this machine, via the Synkro container (default)\n byok \u2014 via an LLM API using your own provider key\nChoose [local] / byok: ",
9730
- (answer) => {
9731
- rl.close();
9732
- resolve4(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
9733
- }
9734
- );
9735
- });
9736
- }
9737
- async function promptStorageMode() {
9738
- if (!process.stdin.isTTY) return "local";
9739
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
9740
- return new Promise((resolve4) => {
9741
- rl.question(
9742
- "Where should telemetry be stored?\n local \u2014 on this machine only (default)\n cloud \u2014 sent to Synkro cloud\nChoose [local] / cloud: ",
10092
+ "Where should Synkro run?\n local \u2014 a grading container on this machine (Docker) (default)\n cloud \u2014 a private container hosted by Synkro (Cloudflare)\nBoth use your own Claude key. Choose [local] / cloud: ",
9743
10093
  (answer) => {
9744
10094
  rl.close();
9745
10095
  resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
@@ -9778,36 +10128,36 @@ coding patterns and the dashboard shows full history. (Y/n) `
9778
10128
  }
9779
10129
  }
9780
10130
  function ensureSynkroDir() {
9781
- mkdirSync9(SYNKRO_DIR4, { recursive: true });
10131
+ mkdirSync9(SYNKRO_DIR5, { recursive: true });
9782
10132
  mkdirSync9(HOOKS_DIR, { recursive: true });
9783
10133
  mkdirSync9(BIN_DIR, { recursive: true });
9784
10134
  mkdirSync9(OFFSETS_DIR, { recursive: true });
9785
- mkdirSync9(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
10135
+ mkdirSync9(join10(SYNKRO_DIR5, "sessions"), { recursive: true });
9786
10136
  }
9787
10137
  function writeHookScripts(mode = "stub") {
9788
- const installExtractCorePath = join9(HOOKS_DIR, "installExtractCore.ts");
9789
- const bashScriptPath = join9(HOOKS_DIR, "cc-bash-judge.ts");
9790
- const bashFollowupScriptPath = join9(HOOKS_DIR, "cc-bash-followup.ts");
9791
- const editPrecheckScriptPath = join9(HOOKS_DIR, "cc-edit-precheck.ts");
9792
- const cwePrecheckScriptPath = join9(HOOKS_DIR, "cc-cwe-precheck.ts");
9793
- const cvePrecheckScriptPath = join9(HOOKS_DIR, "cc-cve-precheck.ts");
9794
- const planJudgeScriptPath = join9(HOOKS_DIR, "cc-plan-judge.ts");
9795
- const agentJudgeScriptPath = join9(HOOKS_DIR, "cc-agent-judge.ts");
9796
- const stopSummaryScriptPath = join9(HOOKS_DIR, "cc-stop-summary.ts");
9797
- const sessionStartScriptPath = join9(HOOKS_DIR, "cc-session-start.ts");
9798
- const transcriptSyncScriptPath = join9(HOOKS_DIR, "cc-transcript-sync.ts");
9799
- const userPromptSubmitScriptPath = join9(HOOKS_DIR, "cc-user-prompt-submit.ts");
9800
- const commonScriptPath = join9(HOOKS_DIR, "_synkro-common.ts");
9801
- const commonBashScriptPath = join9(HOOKS_DIR, "_synkro-common.sh");
9802
- const installScanScriptPath = join9(HOOKS_DIR, "cc-install-scan.ts");
9803
- const cursorBashJudgePath = join9(HOOKS_DIR, "cursor-bash-judge.ts");
9804
- const cursorEditCapturePath = join9(HOOKS_DIR, "cursor-edit-capture.ts");
9805
- const cursorAgentCapturePath = join9(HOOKS_DIR, "cursor-agent-capture.ts");
9806
- const mcpStdioProxyPath = join9(HOOKS_DIR, "mcp-stdio-proxy.ts");
9807
- const taskActivateIntentScriptPath = join9(HOOKS_DIR, "cc-task-activate-intent.ts");
9808
- const mcpGateScriptPath = join9(HOOKS_DIR, "cc-mcp-gate.ts");
10138
+ const installExtractCorePath = join10(HOOKS_DIR, "installExtractCore.ts");
10139
+ const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.ts");
10140
+ const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.ts");
10141
+ const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.ts");
10142
+ const cwePrecheckScriptPath = join10(HOOKS_DIR, "cc-cwe-precheck.ts");
10143
+ const cvePrecheckScriptPath = join10(HOOKS_DIR, "cc-cve-precheck.ts");
10144
+ const planJudgeScriptPath = join10(HOOKS_DIR, "cc-plan-judge.ts");
10145
+ const agentJudgeScriptPath = join10(HOOKS_DIR, "cc-agent-judge.ts");
10146
+ const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.ts");
10147
+ const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.ts");
10148
+ const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.ts");
10149
+ const userPromptSubmitScriptPath = join10(HOOKS_DIR, "cc-user-prompt-submit.ts");
10150
+ const commonScriptPath = join10(HOOKS_DIR, "_synkro-common.ts");
10151
+ const commonBashScriptPath = join10(HOOKS_DIR, "_synkro-common.sh");
10152
+ const installScanScriptPath = join10(HOOKS_DIR, "cc-install-scan.ts");
10153
+ const cursorBashJudgePath = join10(HOOKS_DIR, "cursor-bash-judge.ts");
10154
+ const cursorEditCapturePath = join10(HOOKS_DIR, "cursor-edit-capture.ts");
10155
+ const cursorAgentCapturePath = join10(HOOKS_DIR, "cursor-agent-capture.ts");
10156
+ const mcpStdioProxyPath = join10(HOOKS_DIR, "mcp-stdio-proxy.ts");
10157
+ const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
10158
+ const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
9809
10159
  if (mode === "stub") {
9810
- const stubCommonPath = join9(HOOKS_DIR, "_synkro-stub-common.ts");
10160
+ const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
9811
10161
  const stubFiles = [
9812
10162
  [stubCommonPath, STUB_COMMON_TS],
9813
10163
  [bashScriptPath, STUB_BASH_JUDGE_TS],
@@ -9836,7 +10186,7 @@ function writeHookScripts(mode = "stub") {
9836
10186
  chmodSync2(mcpStdioProxyPath, 493);
9837
10187
  for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
9838
10188
  try {
9839
- unlinkSync4(join9(HOOKS_DIR, stale));
10189
+ unlinkSync4(join10(HOOKS_DIR, stale));
9840
10190
  } catch {
9841
10191
  }
9842
10192
  }
@@ -9903,7 +10253,7 @@ function writeHookScripts(mode = "stub") {
9903
10253
  chmodSync2(taskActivateIntentScriptPath, 493);
9904
10254
  chmodSync2(mcpGateScriptPath, 493);
9905
10255
  try {
9906
- unlinkSync4(join9(HOOKS_DIR, "_synkro-stub-common.ts"));
10256
+ unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
9907
10257
  } catch {
9908
10258
  }
9909
10259
  return {
@@ -9939,7 +10289,7 @@ function resolveSynkroBundle() {
9939
10289
  return null;
9940
10290
  }
9941
10291
  function writeConfigEnv(opts) {
9942
- const credsPath = join9(SYNKRO_DIR4, "credentials.json");
10292
+ const credsPath = join10(SYNKRO_DIR5, "credentials.json");
9943
10293
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
9944
10294
  const safeUserId = sanitizeConfigValue(opts.userId);
9945
10295
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -9955,7 +10305,7 @@ function writeConfigEnv(opts) {
9955
10305
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
9956
10306
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
9957
10307
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
9958
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.58")}`
10308
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.60")}`
9959
10309
  ];
9960
10310
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
9961
10311
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -9969,17 +10319,184 @@ function writeConfigEnv(opts) {
9969
10319
  lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
9970
10320
  lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
9971
10321
  lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
10322
+ lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
9972
10323
  lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
9973
10324
  lines.push("");
9974
10325
  writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
9975
10326
  chmodSync2(CONFIG_PATH2, 384);
9976
10327
  }
10328
+ async function provisionCloudContainer(opts) {
10329
+ let setupToken;
10330
+ try {
10331
+ console.log(" Authorize your Claude key for the hosted Synkro worker \u2014");
10332
+ console.log(" a browser window will open for approval...\n");
10333
+ setupToken = await captureClaudeSetupToken();
10334
+ } catch (err) {
10335
+ console.error(` \u2717 Could not capture Claude setup-token: ${err.message}`);
10336
+ console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
10337
+ process.exit(1);
10338
+ }
10339
+ const repo = detectGitRepo2() || void 0;
10340
+ const harness = [];
10341
+ if (opts.hasClaudeCode) harness.push("claude-code");
10342
+ if (opts.hasCursor) harness.push("cursor");
10343
+ const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
10344
+ try {
10345
+ const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
10346
+ method: "POST",
10347
+ headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
10348
+ body: JSON.stringify({
10349
+ org_id: opts.orgId,
10350
+ user_id: opts.userId,
10351
+ email: opts.email,
10352
+ harness,
10353
+ claude_workers: total,
10354
+ cursor_workers: 0,
10355
+ connected_repo: repo,
10356
+ ruleset: readFullSynkroFile()?.ruleset || "default",
10357
+ setup_token: setupToken
10358
+ // never logged; gateway stores it as the org secret
10359
+ })
10360
+ });
10361
+ if (!resp.ok) {
10362
+ const t = await resp.text().catch(() => "");
10363
+ throw new Error(`cloud-provision failed (${resp.status}): ${t.slice(0, 200)}`);
10364
+ }
10365
+ const out = await resp.json().catch(() => ({}));
10366
+ console.log(` \u2713 cloud container provisioned${out.container_id ? ` (${out.container_id})` : ""}`);
10367
+ console.log();
10368
+ } catch (err) {
10369
+ console.error(` \u2717 Cloud provisioning failed: ${err.message}`);
10370
+ console.error(" Hooks + MCP are installed; re-run `synkro install` \u2192 cloud to retry.");
10371
+ console.log();
10372
+ } finally {
10373
+ setupToken = "";
10374
+ }
10375
+ }
10376
+ function readPersistedDeployLocation() {
10377
+ try {
10378
+ if (existsSync11(CONFIG_PATH2)) {
10379
+ const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
10380
+ if (m?.[1] === "cloud") return "cloud";
10381
+ }
10382
+ } catch {
10383
+ }
10384
+ return "local";
10385
+ }
10386
+ function updateConfigEnvLocation(location) {
10387
+ if (!existsSync11(CONFIG_PATH2)) return;
10388
+ let env = readFileSync10(CONFIG_PATH2, "utf-8");
10389
+ const set = (k, v) => {
10390
+ const re = new RegExp(`^${k}=.*$`, "m");
10391
+ const line = `${k}='${v}'`;
10392
+ env = re.test(env) ? env.replace(re, line) : `${env.replace(/\n?$/, "\n")}${line}`;
10393
+ };
10394
+ set("SYNKRO_DEPLOY_LOCATION", location);
10395
+ set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
10396
+ writeFileSync8(CONFIG_PATH2, env, "utf-8");
10397
+ chmodSync2(CONFIG_PATH2, 384);
10398
+ }
10399
+ async function applyMcpConfig(opts) {
10400
+ if (!opts.hasClaudeCode && !opts.hasCursor) return;
10401
+ let mcpJwt = "";
10402
+ try {
10403
+ mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10404
+ } catch {
10405
+ }
10406
+ if (!mcpJwt) {
10407
+ try {
10408
+ const r = await fetch(`${opts.gatewayUrl}/api/v1/cli/mcp-token`, {
10409
+ method: "POST",
10410
+ headers: { Authorization: `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
10411
+ body: "{}"
10412
+ });
10413
+ if (r.ok) mcpJwt = (await r.json()).token;
10414
+ else console.warn(` \u26A0 MCP token mint failed (${r.status}) \u2014 run \`synkro install\` to set MCP up.`);
10415
+ } catch {
10416
+ console.warn(" \u26A0 MCP token mint failed (network) \u2014 run `synkro install` to set MCP up.");
10417
+ }
10418
+ }
10419
+ if (opts.hasClaudeCode) {
10420
+ try {
10421
+ const mcp = installMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: mcpJwt, local: opts.local });
10422
+ console.log(` \u2713 Claude Code MCP \u2192 ${mcp.url}`);
10423
+ } catch (err) {
10424
+ console.warn(` \u26A0 Claude Code MCP config failed: ${err.message}`);
10425
+ }
10426
+ }
10427
+ if (opts.hasCursor) {
10428
+ try {
10429
+ const mcp = installCursorMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: opts.local ? "" : mcpJwt, local: opts.local });
10430
+ console.log(` \u2713 Cursor MCP \u2192 ${mcp.url}`);
10431
+ } catch (err) {
10432
+ console.warn(` \u26A0 Cursor MCP config failed: ${err.message}`);
10433
+ }
10434
+ }
10435
+ }
10436
+ async function reconcileDeployLocation() {
10437
+ const sf = readFullSynkroFile();
10438
+ const desired = sf?.grader.location === "cloud" ? "cloud" : "local";
10439
+ const current = readPersistedDeployLocation();
10440
+ if (desired === current) return desired;
10441
+ const hasClaudeCode = sf ? sf.harness.includes("claude-code") : true;
10442
+ const hasCursor = sf ? sf.harness.includes("cursor") : false;
10443
+ console.log(`
10444
+ synkro.toml: deploy location changed ${current} \u2192 ${desired}
10445
+ `);
10446
+ if (!isAuthenticated()) {
10447
+ console.log(" Opening browser for Synkro auth...");
10448
+ const ok = await authenticate(() => {
10449
+ });
10450
+ if (!ok) {
10451
+ console.error(" \u2717 authentication failed");
10452
+ process.exit(1);
10453
+ }
10454
+ }
10455
+ await ensureValidToken();
10456
+ const token = getAccessToken();
10457
+ if (!token) {
10458
+ console.error(" \u2717 no access token \u2014 run `synkro login`");
10459
+ process.exit(1);
10460
+ }
10461
+ const gatewayUrl = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL) || "https://api.synkro.sh";
10462
+ try {
10463
+ assertGatewayAllowed(gatewayUrl);
10464
+ } catch (err) {
10465
+ console.error(err.message);
10466
+ process.exit(1);
10467
+ }
10468
+ if (desired === "cloud") {
10469
+ if (dockerStatus().running) {
10470
+ console.log(" Stopping local grader container (snapshotting state first)...");
10471
+ await dockerSafeStop();
10472
+ }
10473
+ dockerRemove();
10474
+ console.log(" \u2713 local container stopped + removed");
10475
+ let orgId, userId, email;
10476
+ try {
10477
+ const info = getUserInfo();
10478
+ userId = info.id;
10479
+ orgId = info.org_id;
10480
+ email = info.email;
10481
+ } catch {
10482
+ }
10483
+ await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
10484
+ updateConfigEnvLocation("cloud");
10485
+ await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: false });
10486
+ console.log(" \u2713 grading + rules/MCP now served from Synkro Cloud\n");
10487
+ } else {
10488
+ updateConfigEnvLocation("local");
10489
+ await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: true });
10490
+ console.log(" \u2713 switched to local \u2014 bringing the local grader + MCP up\n");
10491
+ }
10492
+ return desired;
10493
+ }
9977
10494
  function resolveDeploymentMode() {
9978
10495
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
9979
10496
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
9980
10497
  try {
9981
10498
  if (existsSync11(CONFIG_PATH2)) {
9982
- const m = readFileSync9(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
10499
+ const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
9983
10500
  const val = m?.[1]?.toLowerCase();
9984
10501
  if (val === "bare-host" || val === "docker") return val;
9985
10502
  }
@@ -10006,16 +10523,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
10006
10523
  meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
10007
10524
  } catch {
10008
10525
  }
10009
- const claudeDir = join9(homedir9(), ".claude");
10526
+ const claudeDir = join10(homedir10(), ".claude");
10010
10527
  try {
10011
- const settings = JSON.parse(readFileSync9(join9(claudeDir, "settings.json"), "utf-8"));
10528
+ const settings = JSON.parse(readFileSync10(join10(claudeDir, "settings.json"), "utf-8"));
10012
10529
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
10013
10530
  if (plugins.length) meta.enabled_plugins = plugins;
10014
10531
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
10015
10532
  } catch {
10016
10533
  }
10017
10534
  try {
10018
- const mcpCache = JSON.parse(readFileSync9(join9(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
10535
+ const mcpCache = JSON.parse(readFileSync10(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
10019
10536
  const mcpNames = Object.keys(mcpCache);
10020
10537
  if (mcpNames.length) meta.mcp_servers = mcpNames;
10021
10538
  } catch {
@@ -10027,10 +10544,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
10027
10544
  } catch {
10028
10545
  }
10029
10546
  try {
10030
- const sessionsDir = join9(claudeDir, "sessions");
10547
+ const sessionsDir = join10(claudeDir, "sessions");
10031
10548
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
10032
10549
  for (const f of files) {
10033
- const s = JSON.parse(readFileSync9(join9(sessionsDir, f), "utf-8"));
10550
+ const s = JSON.parse(readFileSync10(join10(sessionsDir, f), "utf-8"));
10034
10551
  if (s.version) {
10035
10552
  meta.cc_version = meta.cc_version || s.version;
10036
10553
  break;
@@ -10138,6 +10655,7 @@ async function installCommand(opts = {}) {
10138
10655
  let agents;
10139
10656
  let gradingMode;
10140
10657
  let storageMode;
10658
+ let deployLocation = "local";
10141
10659
  let transcriptCC = false;
10142
10660
  let transcriptCursor = false;
10143
10661
  if (existingSynkro) {
@@ -10147,8 +10665,9 @@ async function installCommand(opts = {}) {
10147
10665
  (a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
10148
10666
  );
10149
10667
  if (agents.length === 0 && detected.length > 0) agents = detected;
10668
+ deployLocation = existingSynkro.grader.location === "cloud" ? "cloud" : "local";
10150
10669
  gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
10151
- storageMode = "local";
10670
+ storageMode = deployLocation === "cloud" ? "cloud" : "local";
10152
10671
  console.log(`Using .synkro config:`);
10153
10672
  for (const w of existingSynkro.warnings) console.warn(` \u26A0 ${w}`);
10154
10673
  console.log(` harness: ${existingSynkro.harness.join(", ")}`);
@@ -10172,15 +10691,20 @@ async function installCommand(opts = {}) {
10172
10691
  console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
10173
10692
  `);
10174
10693
  }
10175
- gradingMode = await promptGradingMode();
10176
- storageMode = await promptStorageMode();
10177
- console.log(` grading: ${gradingMode} storage: ${storageMode}
10694
+ if (opts.byokGateway) {
10695
+ gradingMode = "byok";
10696
+ storageMode = "cloud";
10697
+ deployLocation = "local";
10698
+ console.log(" deploy: legacy BYOK-gateway grading (no container)\n");
10699
+ } else {
10700
+ deployLocation = await promptDeployLocation();
10701
+ gradingMode = "local";
10702
+ storageMode = deployLocation === "cloud" ? "cloud" : "local";
10703
+ console.log(` deploy: ${deployLocation}
10178
10704
  `);
10179
- if (gradingMode === "byok") {
10180
- console.log(" BYOK grading uses your own provider key \u2014 register one in the");
10181
- console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
10182
10705
  }
10183
10706
  }
10707
+ const target = deployLocation === "cloud" ? "cloud-container" : opts.byokGateway || gradingMode === "byok" ? "gateway-byok" : "local-docker";
10184
10708
  {
10185
10709
  const wantCC = agents.some((a) => a.kind === "claude_code");
10186
10710
  const wantCursor = agents.some((a) => a.kind === "cursor");
@@ -10205,9 +10729,9 @@ async function installCommand(opts = {}) {
10205
10729
  const scripts = writeHookScripts(hookMode);
10206
10730
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
10207
10731
  for (const mode of ["edit", "bash"]) {
10208
- const pidFile = join9(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
10732
+ const pidFile = join10(SYNKRO_DIR5, "daemon", mode, "daemon.pid");
10209
10733
  try {
10210
- const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
10734
+ const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
10211
10735
  if (pid > 0) {
10212
10736
  process.kill(pid, "SIGTERM");
10213
10737
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -10272,10 +10796,11 @@ async function installCommand(opts = {}) {
10272
10796
  } catch {
10273
10797
  }
10274
10798
  const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
10275
- const cloudOnly = gradingMode === "byok" && storageMode === "cloud";
10276
- const useLocalMcp = !cloudOnly;
10277
- if (cloudOnly) {
10278
- console.log("Cloud-only setup (BYOK grading + cloud storage) \u2014 skipping the local container.\n");
10799
+ const useLocalMcp = target === "local-docker";
10800
+ if (target === "cloud-container") {
10801
+ console.log("Cloud setup \u2014 Synkro will provision a private hosted container for your org.\n");
10802
+ } else if (target === "gateway-byok") {
10803
+ console.log("Cloud-only setup (BYOK grading via gateway) \u2014 skipping the local container.\n");
10279
10804
  }
10280
10805
  if (hasClaudeCode && !opts.noMcp) {
10281
10806
  if (useLocalMcp) {
@@ -10289,7 +10814,7 @@ async function installCommand(opts = {}) {
10289
10814
  if (mintResp.ok) {
10290
10815
  const minted = await mintResp.json();
10291
10816
  mcpJwt = minted.token;
10292
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
10817
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
10293
10818
  } else {
10294
10819
  console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
10295
10820
  }
@@ -10317,7 +10842,7 @@ async function installCommand(opts = {}) {
10317
10842
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
10318
10843
  }
10319
10844
  const minted = await mintResp.json();
10320
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10845
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10321
10846
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
10322
10847
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
10323
10848
  console.log(` url: ${mcp.url}`);
@@ -10334,7 +10859,7 @@ async function installCommand(opts = {}) {
10334
10859
  if (hasCursor && !opts.noMcp) {
10335
10860
  try {
10336
10861
  if (useLocalMcp) {
10337
- const jwtPath = join9(SYNKRO_DIR4, ".mcp-jwt");
10862
+ const jwtPath = join10(SYNKRO_DIR5, ".mcp-jwt");
10338
10863
  if (!existsSync11(jwtPath)) {
10339
10864
  const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
10340
10865
  method: "POST",
@@ -10363,7 +10888,7 @@ async function installCommand(opts = {}) {
10363
10888
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
10364
10889
  }
10365
10890
  const minted = await mintResp.json();
10366
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10891
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10367
10892
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
10368
10893
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
10369
10894
  console.log(` url: ${mcp.url}`);
@@ -10376,7 +10901,7 @@ async function installCommand(opts = {}) {
10376
10901
  }
10377
10902
  const synkroBundle = resolveSynkroBundle();
10378
10903
  const persistedMode = resolveDeploymentMode();
10379
- writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
10904
+ writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
10380
10905
  console.log(`Wrote config to ${CONFIG_PATH2}`);
10381
10906
  console.log(` inference: ${profile.inference} (server-side grading)`);
10382
10907
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
@@ -10388,7 +10913,7 @@ async function installCommand(opts = {}) {
10388
10913
  } catch (err) {
10389
10914
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
10390
10915
  }
10391
- writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
10916
+ writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
10392
10917
  console.log();
10393
10918
  if (useLocalMcp) {
10394
10919
  const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
@@ -10439,7 +10964,7 @@ async function installCommand(opts = {}) {
10439
10964
  const ready = await waitForContainerReady(6e4);
10440
10965
  if (ready) {
10441
10966
  console.log(" \u2713 container ready");
10442
- const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
10967
+ const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10443
10968
  try {
10444
10969
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
10445
10970
  method: "POST",
@@ -10469,6 +10994,8 @@ async function installCommand(opts = {}) {
10469
10994
  process.exit(1);
10470
10995
  }
10471
10996
  console.log();
10997
+ } else if (target === "cloud-container") {
10998
+ await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
10472
10999
  }
10473
11000
  if (transcriptConsent) {
10474
11001
  const repo = detectGitRepo2();
@@ -10477,7 +11004,7 @@ async function installCommand(opts = {}) {
10477
11004
  try {
10478
11005
  let mcpToken = "";
10479
11006
  try {
10480
- mcpToken = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
11007
+ mcpToken = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10481
11008
  } catch {
10482
11009
  }
10483
11010
  if (mcpToken) {
@@ -10605,11 +11132,11 @@ function writeSynkroFileIfMissing(opts) {
10605
11132
  try {
10606
11133
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
10607
11134
  if (!root) return;
10608
- if (root === homedir9()) return;
10609
- const fp = join9(root, "synkro.toml");
11135
+ if (root === homedir10()) return;
11136
+ const fp = join10(root, "synkro.toml");
10610
11137
  let hasFile = false;
10611
11138
  try {
10612
- hasFile = statSync2(fp).isFile();
11139
+ hasFile = statSync(fp).isFile();
10613
11140
  } catch {
10614
11141
  }
10615
11142
  if (hasFile) {
@@ -10637,6 +11164,7 @@ function writeSynkroFileIfMissing(opts) {
10637
11164
  "skills = []",
10638
11165
  "",
10639
11166
  "[grader]",
11167
+ `location = "${opts.deployLocation || "local"}"`,
10640
11168
  `pool = "${pool}"`,
10641
11169
  `mode = "${mode}"`,
10642
11170
  "",
@@ -10654,9 +11182,9 @@ function readFullSynkroFile() {
10654
11182
  try {
10655
11183
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
10656
11184
  if (!root) return null;
10657
- const fp = join9(root, "synkro.toml");
11185
+ const fp = join10(root, "synkro.toml");
10658
11186
  if (!existsSync11(fp)) return null;
10659
- const parsed = parseSynkroToml2(readFileSync9(fp, "utf-8"));
11187
+ const parsed = parseSynkroToml2(readFileSync10(fp, "utf-8"));
10660
11188
  const valid = ["claude-code", "cursor"];
10661
11189
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
10662
11190
  const resolved = resolveGraderPool(parsed);
@@ -10664,7 +11192,8 @@ function readFullSynkroFile() {
10664
11192
  harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
10665
11193
  grader: {
10666
11194
  pool: resolved.pool,
10667
- mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
11195
+ mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local",
11196
+ location: parsed.grader?.location === "cloud" ? "cloud" : "local"
10668
11197
  },
10669
11198
  workers: {
10670
11199
  ...resolved.claudeWorkers != null ? { claude: resolved.claudeWorkers } : {},
@@ -10692,7 +11221,7 @@ function reconcileHarness() {
10692
11221
  console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
10693
11222
  const scripts = writeHookScripts(resolvePersistedHookMode());
10694
11223
  console.log("Wrote hook scripts to ~/.synkro/hooks/");
10695
- const ccSettings = join9(homedir9(), ".claude", "settings.json");
11224
+ const ccSettings = join10(homedir10(), ".claude", "settings.json");
10696
11225
  if (wantCC) {
10697
11226
  installCCHooks(ccSettings, {
10698
11227
  bashJudgeScriptPath: scripts.bashScript,
@@ -10712,7 +11241,7 @@ function reconcileHarness() {
10712
11241
  });
10713
11242
  console.log(" \u2713 Claude Code hooks registered");
10714
11243
  try {
10715
- const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
11244
+ const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10716
11245
  if (mcpJwt) {
10717
11246
  installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
10718
11247
  console.log(" \u2713 Claude Code MCP registered");
@@ -10722,8 +11251,9 @@ function reconcileHarness() {
10722
11251
  } else {
10723
11252
  if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
10724
11253
  if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
11254
+ if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
10725
11255
  }
10726
- const cursorHooks = join9(homedir9(), ".cursor", "hooks.json");
11256
+ const cursorHooks = join10(homedir10(), ".cursor", "hooks.json");
10727
11257
  if (wantCursor) {
10728
11258
  installCursorHooks(cursorHooks, {
10729
11259
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
@@ -10777,7 +11307,7 @@ async function syncSkillFiles() {
10777
11307
  if (resolved.length === 0) return;
10778
11308
  const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
10779
11309
  const tasks = resolved.map((fp) => {
10780
- const content = readFileSync9(fp, "utf-8");
11310
+ const content = readFileSync10(fp, "utf-8");
10781
11311
  const source = `skill:${fp.split("/").pop()}`;
10782
11312
  if (!content.trim()) {
10783
11313
  console.log(` \u2298 skill ${source}: empty file, skipped`);
@@ -10823,7 +11353,7 @@ function detectGitRepo2() {
10823
11353
  function getClaudeProjectsFolder() {
10824
11354
  const cwd = process.cwd();
10825
11355
  const sanitized = "-" + cwd.replace(/\//g, "-");
10826
- const projectsDir = join9(homedir9(), ".claude", "projects", sanitized);
11356
+ const projectsDir = join10(homedir10(), ".claude", "projects", sanitized);
10827
11357
  return existsSync11(projectsDir) ? projectsDir : null;
10828
11358
  }
10829
11359
  function extractSessionInsights(projectsDir) {
@@ -10831,9 +11361,9 @@ function extractSessionInsights(projectsDir) {
10831
11361
  const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
10832
11362
  for (const file of files) {
10833
11363
  const sessionId = file.replace(".jsonl", "");
10834
- const filePath = join9(projectsDir, file);
11364
+ const filePath = join10(projectsDir, file);
10835
11365
  try {
10836
- const content = readFileSync9(filePath, "utf-8");
11366
+ const content = readFileSync10(filePath, "utf-8");
10837
11367
  const lines = content.split("\n").filter(Boolean);
10838
11368
  for (let i = 0; i < lines.length; i++) {
10839
11369
  try {
@@ -10912,14 +11442,14 @@ function cursorProjectSlug(workspaceRoot) {
10912
11442
  return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10913
11443
  }
10914
11444
  function getCursorTranscriptsDir() {
10915
- const dir = join9(homedir9(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
11445
+ const dir = join10(homedir10(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
10916
11446
  return existsSync11(dir) ? dir : null;
10917
11447
  }
10918
11448
  function isSafeConvId(id) {
10919
11449
  return /^[A-Za-z0-9_-]+$/.test(id);
10920
11450
  }
10921
11451
  function parseCursorTranscriptFile(filePath) {
10922
- const content = readFileSync9(filePath, "utf-8");
11452
+ const content = readFileSync10(filePath, "utf-8");
10923
11453
  const lines = content.split("\n").filter(Boolean);
10924
11454
  const messages = [];
10925
11455
  for (let i = 0; i < lines.length; i++) {
@@ -10951,7 +11481,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10951
11481
  for (let i = 0; i < convDirs.length; i++) {
10952
11482
  const convId = convDirs[i];
10953
11483
  if (!isSafeConvId(convId)) continue;
10954
- const filePath = join9(dir, convId, `${convId}.jsonl`);
11484
+ const filePath = join10(dir, convId, `${convId}.jsonl`);
10955
11485
  if (!existsSync11(filePath)) continue;
10956
11486
  try {
10957
11487
  const all = parseCursorTranscriptFile(filePath);
@@ -10974,8 +11504,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10974
11504
  process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
10975
11505
  }
10976
11506
  try {
10977
- const lc = readFileSync9(filePath, "utf-8").split("\n").filter(Boolean).length;
10978
- writeFileSync8(join9(OFFSETS_DIR, convId), String(lc), "utf-8");
11507
+ const lc = readFileSync10(filePath, "utf-8").split("\n").filter(Boolean).length;
11508
+ writeFileSync8(join10(OFFSETS_DIR, convId), String(lc), "utf-8");
10979
11509
  } catch {
10980
11510
  }
10981
11511
  }
@@ -10983,7 +11513,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10983
11513
  return { sessions: totalSessions, messages: totalMessages };
10984
11514
  }
10985
11515
  function parseTranscriptFile(filePath) {
10986
- const content = readFileSync9(filePath, "utf-8");
11516
+ const content = readFileSync10(filePath, "utf-8");
10987
11517
  const lines = content.split("\n").filter(Boolean);
10988
11518
  const messages = [];
10989
11519
  for (let i = 0; i < lines.length; i++) {
@@ -11031,7 +11561,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
11031
11561
  for (let i = 0; i < files.length; i++) {
11032
11562
  const file = files[i];
11033
11563
  const sessionId = file.replace(".jsonl", "");
11034
- const filePath = join9(projectsDir, file);
11564
+ const filePath = join10(projectsDir, file);
11035
11565
  try {
11036
11566
  const allMessages = parseTranscriptFile(filePath);
11037
11567
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
@@ -11053,9 +11583,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
11053
11583
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
11054
11584
  }
11055
11585
  try {
11056
- const content = readFileSync9(join9(projectsDir, file), "utf-8");
11586
+ const content = readFileSync10(join10(projectsDir, file), "utf-8");
11057
11587
  const lineCount = content.split("\n").filter(Boolean).length;
11058
- writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11588
+ writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11059
11589
  } catch {
11060
11590
  }
11061
11591
  }
@@ -11076,7 +11606,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
11076
11606
  const sessions = [];
11077
11607
  for (const file of batch) {
11078
11608
  const sessionId = file.replace(".jsonl", "");
11079
- const filePath = join9(projectsDir, file);
11609
+ const filePath = join10(projectsDir, file);
11080
11610
  try {
11081
11611
  const allMessages = parseTranscriptFile(filePath);
11082
11612
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -11105,18 +11635,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
11105
11635
  }
11106
11636
  for (const file of batch) {
11107
11637
  const sessionId = file.replace(".jsonl", "");
11108
- const filePath = join9(projectsDir, file);
11638
+ const filePath = join10(projectsDir, file);
11109
11639
  try {
11110
- const content = readFileSync9(filePath, "utf-8");
11640
+ const content = readFileSync10(filePath, "utf-8");
11111
11641
  const lineCount = content.split("\n").filter(Boolean).length;
11112
- writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11642
+ writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11113
11643
  } catch {
11114
11644
  }
11115
11645
  }
11116
11646
  }
11117
11647
  return { sessions: totalSessions, messages: totalMessages };
11118
11648
  }
11119
- var SYNKRO_DIR4, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
11649
+ var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
11120
11650
  var init_install = __esm({
11121
11651
  "cli/commands/install.ts"() {
11122
11652
  "use strict";
@@ -11134,10 +11664,11 @@ var init_install = __esm({
11134
11664
  init_promptFetcher();
11135
11665
  init_claudeDesktopTap();
11136
11666
  init_dockerInstall();
11137
- SYNKRO_DIR4 = join9(homedir9(), ".synkro");
11138
- HOOKS_DIR = join9(SYNKRO_DIR4, "hooks");
11139
- BIN_DIR = join9(SYNKRO_DIR4, "bin");
11140
- CONFIG_PATH2 = join9(SYNKRO_DIR4, "config.env");
11667
+ init_setupToken();
11668
+ SYNKRO_DIR5 = join10(homedir10(), ".synkro");
11669
+ HOOKS_DIR = join10(SYNKRO_DIR5, "hooks");
11670
+ BIN_DIR = join10(SYNKRO_DIR5, "bin");
11671
+ CONFIG_PATH2 = join10(SYNKRO_DIR5, "config.env");
11141
11672
  MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
11142
11673
  import { readFileSync } from 'node:fs';
11143
11674
  import { homedir } from 'node:os';
@@ -11248,15 +11779,15 @@ rl.on('line', async (line) => {
11248
11779
  }
11249
11780
  });
11250
11781
  `;
11251
- OFFSETS_DIR = join9(SYNKRO_DIR4, ".transcript-offsets");
11782
+ OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
11252
11783
  }
11253
11784
  });
11254
11785
 
11255
11786
  // cli/local-cc/install.ts
11256
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync10, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
11257
- import { join as join10 } from "path";
11258
- import { homedir as homedir10 } from "os";
11259
- import { spawnSync as spawnSync4 } from "child_process";
11787
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync11, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
11788
+ import { join as join11 } from "path";
11789
+ import { homedir as homedir11 } from "os";
11790
+ import { spawnSync as spawnSync5 } from "child_process";
11260
11791
  function writePluginFiles() {
11261
11792
  for (const c of CHANNELS) {
11262
11793
  mkdirSync10(c.sessionDir, { recursive: true });
@@ -11276,7 +11807,7 @@ function writePluginFiles() {
11276
11807
  }
11277
11808
  function runBunInstall() {
11278
11809
  for (const c of CHANNELS) {
11279
- const r = spawnSync4("bun", ["install", "--silent"], {
11810
+ const r = spawnSync5("bun", ["install", "--silent"], {
11280
11811
  cwd: c.sessionDir,
11281
11812
  encoding: "utf-8",
11282
11813
  timeout: 12e4
@@ -11292,7 +11823,7 @@ function safelyMutateClaudeJson(mutator) {
11292
11823
  if (!existsSync12(CLAUDE_JSON_PATH)) {
11293
11824
  return;
11294
11825
  }
11295
- const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
11826
+ const originalText = readFileSync11(CLAUDE_JSON_PATH, "utf-8");
11296
11827
  let parsed;
11297
11828
  try {
11298
11829
  parsed = JSON.parse(originalText);
@@ -11392,15 +11923,15 @@ function patchClaudeJson() {
11392
11923
  });
11393
11924
  }
11394
11925
  function installLocalCC() {
11395
- let bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
11926
+ let bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
11396
11927
  if (bunCheck.status !== 0) {
11397
11928
  if (process.platform === "darwin") {
11398
11929
  console.log(" Installing bun via brew...");
11399
- const brewR = spawnSync4("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
11930
+ const brewR = spawnSync5("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
11400
11931
  if (brewR.status !== 0) {
11401
11932
  throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
11402
11933
  }
11403
- bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
11934
+ bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
11404
11935
  if (bunCheck.status !== 0) {
11405
11936
  throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
11406
11937
  }
@@ -11434,42 +11965,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
11434
11965
  var init_install2 = __esm({
11435
11966
  "cli/local-cc/install.ts"() {
11436
11967
  "use strict";
11437
- CLAUDE_JSON_BACKUP_PATH = join10(homedir10(), ".claude.json.synkro-bak");
11438
- SESSION_DIR = join10(homedir10(), ".synkro", "cc_sessions");
11439
- PLUGIN_PATH = join10(SESSION_DIR, "synkro-channel.ts");
11440
- PLUGIN_PKG_PATH = join10(SESSION_DIR, "package.json");
11441
- PLUGIN_SETTINGS_DIR = join10(SESSION_DIR, ".claude");
11442
- PLUGIN_SETTINGS_PATH = join10(PLUGIN_SETTINGS_DIR, "settings.json");
11443
- PROJECT_MCP_PATH = join10(SESSION_DIR, ".mcp.json");
11444
- CLAUDE_JSON_PATH = join10(homedir10(), ".claude.json");
11445
- RUN_SCRIPT_PATH = join10(SESSION_DIR, "run-claude.sh");
11968
+ CLAUDE_JSON_BACKUP_PATH = join11(homedir11(), ".claude.json.synkro-bak");
11969
+ SESSION_DIR = join11(homedir11(), ".synkro", "cc_sessions");
11970
+ PLUGIN_PATH = join11(SESSION_DIR, "synkro-channel.ts");
11971
+ PLUGIN_PKG_PATH = join11(SESSION_DIR, "package.json");
11972
+ PLUGIN_SETTINGS_DIR = join11(SESSION_DIR, ".claude");
11973
+ PLUGIN_SETTINGS_PATH = join11(PLUGIN_SETTINGS_DIR, "settings.json");
11974
+ PROJECT_MCP_PATH = join11(SESSION_DIR, ".mcp.json");
11975
+ CLAUDE_JSON_PATH = join11(homedir11(), ".claude.json");
11976
+ RUN_SCRIPT_PATH = join11(SESSION_DIR, "run-claude.sh");
11446
11977
  TMUX_SESSION_NAME = "synkro-local-cc";
11447
11978
  CHANNEL_1_PORT = 8941;
11448
- SESSION_DIR_2 = join10(homedir10(), ".synkro", "cc_sessions_2");
11449
- PLUGIN_PATH_2 = join10(SESSION_DIR_2, "synkro-channel.ts");
11450
- PLUGIN_PKG_PATH_2 = join10(SESSION_DIR_2, "package.json");
11451
- PLUGIN_SETTINGS_DIR_2 = join10(SESSION_DIR_2, ".claude");
11452
- PLUGIN_SETTINGS_PATH_2 = join10(PLUGIN_SETTINGS_DIR_2, "settings.json");
11453
- PROJECT_MCP_PATH_2 = join10(SESSION_DIR_2, ".mcp.json");
11454
- RUN_SCRIPT_PATH_2 = join10(SESSION_DIR_2, "run-claude.sh");
11979
+ SESSION_DIR_2 = join11(homedir11(), ".synkro", "cc_sessions_2");
11980
+ PLUGIN_PATH_2 = join11(SESSION_DIR_2, "synkro-channel.ts");
11981
+ PLUGIN_PKG_PATH_2 = join11(SESSION_DIR_2, "package.json");
11982
+ PLUGIN_SETTINGS_DIR_2 = join11(SESSION_DIR_2, ".claude");
11983
+ PLUGIN_SETTINGS_PATH_2 = join11(PLUGIN_SETTINGS_DIR_2, "settings.json");
11984
+ PROJECT_MCP_PATH_2 = join11(SESSION_DIR_2, ".mcp.json");
11985
+ RUN_SCRIPT_PATH_2 = join11(SESSION_DIR_2, "run-claude.sh");
11455
11986
  TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
11456
11987
  CHANNEL_2_PORT = 8951;
11457
- SESSION_DIR_3 = join10(homedir10(), ".synkro", "cc_sessions_3");
11458
- PLUGIN_PATH_3 = join10(SESSION_DIR_3, "synkro-channel.ts");
11459
- PLUGIN_PKG_PATH_3 = join10(SESSION_DIR_3, "package.json");
11460
- PLUGIN_SETTINGS_DIR_3 = join10(SESSION_DIR_3, ".claude");
11461
- PLUGIN_SETTINGS_PATH_3 = join10(PLUGIN_SETTINGS_DIR_3, "settings.json");
11462
- PROJECT_MCP_PATH_3 = join10(SESSION_DIR_3, ".mcp.json");
11463
- RUN_SCRIPT_PATH_3 = join10(SESSION_DIR_3, "run-claude.sh");
11988
+ SESSION_DIR_3 = join11(homedir11(), ".synkro", "cc_sessions_3");
11989
+ PLUGIN_PATH_3 = join11(SESSION_DIR_3, "synkro-channel.ts");
11990
+ PLUGIN_PKG_PATH_3 = join11(SESSION_DIR_3, "package.json");
11991
+ PLUGIN_SETTINGS_DIR_3 = join11(SESSION_DIR_3, ".claude");
11992
+ PLUGIN_SETTINGS_PATH_3 = join11(PLUGIN_SETTINGS_DIR_3, "settings.json");
11993
+ PROJECT_MCP_PATH_3 = join11(SESSION_DIR_3, ".mcp.json");
11994
+ RUN_SCRIPT_PATH_3 = join11(SESSION_DIR_3, "run-claude.sh");
11464
11995
  TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
11465
11996
  CHANNEL_3_PORT = 8942;
11466
- SESSION_DIR_4 = join10(homedir10(), ".synkro", "cc_sessions_4");
11467
- PLUGIN_PATH_4 = join10(SESSION_DIR_4, "synkro-channel.ts");
11468
- PLUGIN_PKG_PATH_4 = join10(SESSION_DIR_4, "package.json");
11469
- PLUGIN_SETTINGS_DIR_4 = join10(SESSION_DIR_4, ".claude");
11470
- PLUGIN_SETTINGS_PATH_4 = join10(PLUGIN_SETTINGS_DIR_4, "settings.json");
11471
- PROJECT_MCP_PATH_4 = join10(SESSION_DIR_4, ".mcp.json");
11472
- RUN_SCRIPT_PATH_4 = join10(SESSION_DIR_4, "run-claude.sh");
11997
+ SESSION_DIR_4 = join11(homedir11(), ".synkro", "cc_sessions_4");
11998
+ PLUGIN_PATH_4 = join11(SESSION_DIR_4, "synkro-channel.ts");
11999
+ PLUGIN_PKG_PATH_4 = join11(SESSION_DIR_4, "package.json");
12000
+ PLUGIN_SETTINGS_DIR_4 = join11(SESSION_DIR_4, ".claude");
12001
+ PLUGIN_SETTINGS_PATH_4 = join11(PLUGIN_SETTINGS_DIR_4, "settings.json");
12002
+ PROJECT_MCP_PATH_4 = join11(SESSION_DIR_4, ".mcp.json");
12003
+ RUN_SCRIPT_PATH_4 = join11(SESSION_DIR_4, "run-claude.sh");
11473
12004
  TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
11474
12005
  CHANNEL_4_PORT = 8952;
11475
12006
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
@@ -11744,9 +12275,9 @@ __export(disconnect_exports, {
11744
12275
  disconnectCommand: () => disconnectCommand
11745
12276
  });
11746
12277
  import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
11747
- import { homedir as homedir11 } from "os";
11748
- import { join as join11 } from "path";
11749
- import { spawnSync as spawnSync5 } from "child_process";
12278
+ import { homedir as homedir12 } from "os";
12279
+ import { join as join12 } from "path";
12280
+ import { spawnSync as spawnSync6 } from "child_process";
11750
12281
  import { createInterface as createInterface4 } from "readline";
11751
12282
  async function tearDownLocalCC() {
11752
12283
  const docker = dockerStatus();
@@ -11760,7 +12291,7 @@ async function tearDownLocalCC() {
11760
12291
  console.log("\u2713 removed synkro-server container");
11761
12292
  try {
11762
12293
  const image = imageTag();
11763
- const r = spawnSync5("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
12294
+ const r = spawnSync6("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
11764
12295
  console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
11765
12296
  } catch {
11766
12297
  }
@@ -11819,15 +12350,19 @@ async function disconnectCommand(args2 = []) {
11819
12350
  const cursorMcpRemoved = uninstallCursorMcpConfig();
11820
12351
  console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
11821
12352
  }
11822
- if (existsSync13(SYNKRO_DIR5)) {
12353
+ {
12354
+ const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
12355
+ console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
12356
+ }
12357
+ if (existsSync13(SYNKRO_DIR6)) {
11823
12358
  if (purge) {
11824
- rmSync(SYNKRO_DIR5, { recursive: true, force: true });
11825
- console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
12359
+ rmSync(SYNKRO_DIR6, { recursive: true, force: true });
12360
+ console.log(`\u2713 wiped ${SYNKRO_DIR6} entirely \u2014 including all scan data and backups`);
11826
12361
  } else {
11827
- const keep = /* @__PURE__ */ new Set([join11(SYNKRO_DIR5, "pgdata"), join11(SYNKRO_DIR5, "pgdata-backups")]);
12362
+ const keep = /* @__PURE__ */ new Set([join12(SYNKRO_DIR6, "pgdata"), join12(SYNKRO_DIR6, "pgdata-backups")]);
11828
12363
  const preserved = [];
11829
- for (const entry of readdirSync4(SYNKRO_DIR5)) {
11830
- const full = join11(SYNKRO_DIR5, entry);
12364
+ for (const entry of readdirSync4(SYNKRO_DIR6)) {
12365
+ const full = join12(SYNKRO_DIR6, entry);
11831
12366
  if (keep.has(full)) {
11832
12367
  preserved.push(entry);
11833
12368
  continue;
@@ -11835,18 +12370,18 @@ async function disconnectCommand(args2 = []) {
11835
12370
  rmSync(full, { recursive: true, force: true });
11836
12371
  }
11837
12372
  if (preserved.length > 0) {
11838
- console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR5} (kept your scan data: ${preserved.join(", ")})`);
12373
+ console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR6} (kept your scan data: ${preserved.join(", ")})`);
11839
12374
  console.log(" run `synkro uninstall --purge` to delete that too");
11840
12375
  } else {
11841
- console.log(`\u2713 removed ${SYNKRO_DIR5}`);
12376
+ console.log(`\u2713 removed ${SYNKRO_DIR6}`);
11842
12377
  }
11843
12378
  }
11844
12379
  } else {
11845
- console.log(`\xB7 ${SYNKRO_DIR5} already gone`);
12380
+ console.log(`\xB7 ${SYNKRO_DIR6} already gone`);
11846
12381
  }
11847
12382
  console.log(purge ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
11848
12383
  }
11849
- var SYNKRO_DIR5;
12384
+ var SYNKRO_DIR6;
11850
12385
  var init_disconnect = __esm({
11851
12386
  "cli/commands/disconnect.ts"() {
11852
12387
  "use strict";
@@ -11857,14 +12392,14 @@ var init_disconnect = __esm({
11857
12392
  init_install2();
11858
12393
  init_dockerInstall();
11859
12394
  init_macKeychain();
11860
- SYNKRO_DIR5 = join11(homedir11(), ".synkro");
12395
+ SYNKRO_DIR6 = join12(homedir12(), ".synkro");
11861
12396
  }
11862
12397
  });
11863
12398
 
11864
12399
  // cli/local-cc/turnLog.ts
11865
- import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync as statSync3, watchFile, unwatchFile } from "fs";
11866
- import { dirname as dirname6, join as join12 } from "path";
11867
- import { homedir as homedir12 } from "os";
12400
+ import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as readFileSync12, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
12401
+ import { dirname as dirname6, join as join13 } from "path";
12402
+ import { homedir as homedir13 } from "os";
11868
12403
  function truncate(s, max = PREVIEW_MAX) {
11869
12404
  if (s.length <= max) return s;
11870
12405
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -11902,9 +12437,9 @@ function appendTurn(args2) {
11902
12437
  function readRecentTurns(n = 20) {
11903
12438
  if (!existsSync14(TURN_LOG_PATH)) return [];
11904
12439
  try {
11905
- const size = statSync3(TURN_LOG_PATH).size;
12440
+ const size = statSync2(TURN_LOG_PATH).size;
11906
12441
  if (size === 0) return [];
11907
- const text = readFileSync11(TURN_LOG_PATH, "utf-8");
12442
+ const text = readFileSync12(TURN_LOG_PATH, "utf-8");
11908
12443
  const lines = text.split("\n").filter(Boolean);
11909
12444
  const lastN = lines.slice(-n).reverse();
11910
12445
  return lastN.map((line) => {
@@ -11928,7 +12463,7 @@ function followTurns(onEntry) {
11928
12463
  }
11929
12464
  let lastSize = (() => {
11930
12465
  try {
11931
- return statSync3(TURN_LOG_PATH).size;
12466
+ return statSync2(TURN_LOG_PATH).size;
11932
12467
  } catch {
11933
12468
  return 0;
11934
12469
  }
@@ -11983,7 +12518,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
11983
12518
  var init_turnLog = __esm({
11984
12519
  "cli/local-cc/turnLog.ts"() {
11985
12520
  "use strict";
11986
- TURN_LOG_PATH = join12(homedir12(), ".synkro", "cc_sessions", "turns.log");
12521
+ TURN_LOG_PATH = join13(homedir13(), ".synkro", "cc_sessions", "turns.log");
11987
12522
  PREVIEW_MAX = 400;
11988
12523
  }
11989
12524
  });
@@ -12132,19 +12667,19 @@ var init_grade = __esm({
12132
12667
  });
12133
12668
 
12134
12669
  // cli/local-cc/pueue.ts
12135
- import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn as spawn2 } from "child_process";
12136
- import { homedir as homedir13 } from "os";
12137
- import { join as join13 } from "path";
12670
+ import { execFileSync as execFileSync3, spawnSync as spawnSync7, spawn as spawn2 } from "child_process";
12671
+ import { homedir as homedir14 } from "os";
12672
+ import { join as join14 } from "path";
12138
12673
  import { connect as connect2 } from "net";
12139
12674
  function pueueAvailable() {
12140
- const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12675
+ const r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12141
12676
  if (r.status !== 0) {
12142
12677
  throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
12143
12678
  }
12144
12679
  }
12145
12680
  function statusJson() {
12146
12681
  pueueAvailable();
12147
- const r = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8" });
12682
+ const r = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8" });
12148
12683
  if (r.status !== 0) {
12149
12684
  throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
12150
12685
  }
@@ -12189,18 +12724,18 @@ function startTask(opts = {}) {
12189
12724
  let existing = findTask(ch);
12190
12725
  while (existing) {
12191
12726
  if (existing.status === "Running" || existing.status === "Queued") {
12192
- spawnSync6("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
12193
- spawnSync6("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
12727
+ spawnSync7("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
12728
+ spawnSync7("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
12194
12729
  for (let i = 0; i < 10; i++) {
12195
12730
  const check = findTask(ch);
12196
12731
  if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
12197
- spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
12732
+ spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
12198
12733
  }
12199
12734
  }
12200
- spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
12735
+ spawnSync7("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
12201
12736
  existing = findTask(ch);
12202
12737
  }
12203
- const runScript = join13(cwd, "run-claude.sh");
12738
+ const runScript = join14(cwd, "run-claude.sh");
12204
12739
  const args2 = [
12205
12740
  "add",
12206
12741
  "--label",
@@ -12211,7 +12746,7 @@ function startTask(opts = {}) {
12211
12746
  "bash",
12212
12747
  runScript
12213
12748
  ];
12214
- const r = spawnSync6("pueue", args2, { encoding: "utf-8" });
12749
+ const r = spawnSync7("pueue", args2, { encoding: "utf-8" });
12215
12750
  if (r.status !== 0) {
12216
12751
  throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
12217
12752
  }
@@ -12222,25 +12757,25 @@ function startTask(opts = {}) {
12222
12757
  return created;
12223
12758
  }
12224
12759
  function stopTask(channel = CHANNEL_PRIMARY) {
12225
- spawnSync6("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
12760
+ spawnSync7("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
12226
12761
  let t = findTask(channel);
12227
12762
  while (t) {
12228
12763
  if (t.status === "Running" || t.status === "Queued") {
12229
- spawnSync6("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
12764
+ spawnSync7("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
12230
12765
  for (let i = 0; i < 10; i++) {
12231
12766
  const check = findTask(channel);
12232
12767
  if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
12233
- spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
12768
+ spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
12234
12769
  }
12235
12770
  }
12236
- spawnSync6("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
12771
+ spawnSync7("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
12237
12772
  t = findTask(channel);
12238
12773
  }
12239
12774
  }
12240
12775
  function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
12241
12776
  const t = findTask(channel);
12242
12777
  if (!t) return `(no ${channel.taskLabel} task)`;
12243
- const r = spawnSync6("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
12778
+ const r = spawnSync7("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
12244
12779
  return r.stdout || r.stderr || "(no output)";
12245
12780
  }
12246
12781
  function ensureRunning(opts = {}) {
@@ -12265,8 +12800,8 @@ function probePort(host, port, timeoutMs = 500) {
12265
12800
  });
12266
12801
  }
12267
12802
  function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
12268
- spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
12269
- spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
12803
+ spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
12804
+ spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
12270
12805
  }
12271
12806
  async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
12272
12807
  const deadline = Date.now() + timeoutMs;
@@ -12278,46 +12813,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
12278
12813
  return probePort(host, port);
12279
12814
  }
12280
12815
  function brewInstall(pkg) {
12281
- const brew = spawnSync6("brew", ["--version"], { encoding: "utf-8" });
12816
+ const brew = spawnSync7("brew", ["--version"], { encoding: "utf-8" });
12282
12817
  if (brew.status !== 0) return false;
12283
12818
  console.log(` Installing ${pkg} via brew...`);
12284
- const r = spawnSync6("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
12819
+ const r = spawnSync7("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
12285
12820
  return r.status === 0;
12286
12821
  }
12287
12822
  function assertPueueInstalled() {
12288
- let r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12823
+ let r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12289
12824
  if (r.status !== 0) {
12290
12825
  if (process.platform === "darwin" && brewInstall("pueue")) {
12291
- r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12826
+ r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12292
12827
  if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
12293
12828
  } else {
12294
12829
  throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
12295
12830
  }
12296
12831
  }
12297
- const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12832
+ const status = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12298
12833
  if (status.status !== 0) {
12299
12834
  console.log(" Starting pueued daemon...");
12300
12835
  const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
12301
12836
  child.unref();
12302
- spawnSync6("sleep", ["1"]);
12303
- const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12837
+ spawnSync7("sleep", ["1"]);
12838
+ const retry = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12304
12839
  if (retry.status !== 0) {
12305
12840
  throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
12306
12841
  }
12307
12842
  }
12308
- spawnSync6("pueue", ["parallel", "2"], { encoding: "utf-8" });
12843
+ spawnSync7("pueue", ["parallel", "2"], { encoding: "utf-8" });
12309
12844
  }
12310
12845
  function assertClaudeInstalled() {
12311
- const r = spawnSync6("claude", ["--version"], { encoding: "utf-8" });
12846
+ const r = spawnSync7("claude", ["--version"], { encoding: "utf-8" });
12312
12847
  if (r.status !== 0) {
12313
12848
  throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
12314
12849
  }
12315
12850
  }
12316
12851
  function assertTmuxInstalled() {
12317
- let r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
12852
+ let r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
12318
12853
  if (r.status !== 0) {
12319
12854
  if (process.platform === "darwin" && brewInstall("tmux")) {
12320
- r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
12855
+ r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
12321
12856
  if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
12322
12857
  } else {
12323
12858
  throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
@@ -12330,12 +12865,12 @@ var init_pueue = __esm({
12330
12865
  "use strict";
12331
12866
  TASK_LABEL = "synkro-local-cc";
12332
12867
  TMUX_SESSION = "synkro-local-cc";
12333
- SESSION_DIR2 = join13(homedir13(), ".synkro", "cc_sessions");
12868
+ SESSION_DIR2 = join14(homedir14(), ".synkro", "cc_sessions");
12334
12869
  TASK_LABEL_2 = "synkro-local-cc-2";
12335
12870
  TMUX_SESSION_2 = "synkro-local-cc-2";
12336
- SESSION_DIR_22 = join13(homedir13(), ".synkro", "cc_sessions_2");
12337
- SESSION_DIR_32 = join13(homedir13(), ".synkro", "cc_sessions_3");
12338
- SESSION_DIR_42 = join13(homedir13(), ".synkro", "cc_sessions_4");
12871
+ SESSION_DIR_22 = join14(homedir14(), ".synkro", "cc_sessions_2");
12872
+ SESSION_DIR_32 = join14(homedir14(), ".synkro", "cc_sessions_3");
12873
+ SESSION_DIR_42 = join14(homedir14(), ".synkro", "cc_sessions_4");
12339
12874
  PueueError = class extends Error {
12340
12875
  constructor(message, cause) {
12341
12876
  super(message);
@@ -12350,13 +12885,13 @@ var init_pueue = __esm({
12350
12885
  });
12351
12886
 
12352
12887
  // cli/local-cc/settings.ts
12353
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
12354
- import { homedir as homedir14 } from "os";
12355
- import { join as join14 } from "path";
12888
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
12889
+ import { homedir as homedir15 } from "os";
12890
+ import { join as join15 } from "path";
12356
12891
  function isLocalCCEnabled() {
12357
12892
  if (!existsSync15(CONFIG_PATH3)) return false;
12358
12893
  try {
12359
- const content = readFileSync12(CONFIG_PATH3, "utf-8");
12894
+ const content = readFileSync13(CONFIG_PATH3, "utf-8");
12360
12895
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
12361
12896
  return match?.[1] === "yes";
12362
12897
  } catch {
@@ -12367,7 +12902,7 @@ var CONFIG_PATH3;
12367
12902
  var init_settings = __esm({
12368
12903
  "cli/local-cc/settings.ts"() {
12369
12904
  "use strict";
12370
- CONFIG_PATH3 = join14(homedir14(), ".synkro", "config.env");
12905
+ CONFIG_PATH3 = join15(homedir15(), ".synkro", "config.env");
12371
12906
  }
12372
12907
  });
12373
12908
 
@@ -12376,11 +12911,11 @@ var localCc_exports = {};
12376
12911
  __export(localCc_exports, {
12377
12912
  localCcCommand: () => localCcCommand
12378
12913
  });
12379
- import { spawnSync as spawnSync7 } from "child_process";
12380
- import { homedir as homedir15 } from "os";
12381
- import { join as join15 } from "path";
12914
+ import { spawnSync as spawnSync8 } from "child_process";
12915
+ import { homedir as homedir16 } from "os";
12916
+ import { join as join16 } from "path";
12382
12917
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
12383
- import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
12918
+ import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
12384
12919
  function deploymentMode() {
12385
12920
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
12386
12921
  if (env === "docker") return "docker";
@@ -12487,14 +13022,14 @@ TROUBLESHOOTING
12487
13022
  }
12488
13023
  function readGatewayUrl() {
12489
13024
  if (existsSync16(CONFIG_PATH4)) {
12490
- const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
13025
+ const m = readFileSync14(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
12491
13026
  if (m) return m[1];
12492
13027
  }
12493
13028
  return "https://api.synkro.sh";
12494
13029
  }
12495
13030
  function updateLocalInferenceFlag(enabled) {
12496
13031
  if (!existsSync16(CONFIG_PATH4)) return;
12497
- let content = readFileSync13(CONFIG_PATH4, "utf-8");
13032
+ let content = readFileSync14(CONFIG_PATH4, "utf-8");
12498
13033
  const flag = enabled ? "yes" : "no";
12499
13034
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
12500
13035
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -12557,7 +13092,7 @@ async function cmdStatus() {
12557
13092
  }
12558
13093
  const ch1Up = await isChannelAvailable();
12559
13094
  console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
12560
- const tmux1 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
13095
+ const tmux1 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
12561
13096
  console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
12562
13097
  const t2 = findTask(CHANNEL_SECONDARY);
12563
13098
  if (!t2) {
@@ -12567,7 +13102,7 @@ async function cmdStatus() {
12567
13102
  }
12568
13103
  const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
12569
13104
  console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
12570
- const tmux2 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
13105
+ const tmux2 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
12571
13106
  console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
12572
13107
  }
12573
13108
  async function cmdEnable() {
@@ -12773,7 +13308,7 @@ function cmdLogs(rest) {
12773
13308
  }
12774
13309
  return "200";
12775
13310
  })();
12776
- spawnSync7("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
13311
+ spawnSync8("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
12777
13312
  return;
12778
13313
  }
12779
13314
  for (const arg of rest) {
@@ -12821,7 +13356,7 @@ function cmdLogs(rest) {
12821
13356
  function cmdAttach(rest) {
12822
13357
  assertTmuxInstalled();
12823
13358
  const readonly = rest.some((a) => a === "--readonly" || a === "-r");
12824
- const has = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
13359
+ const has = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
12825
13360
  if (has.status !== 0) {
12826
13361
  console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
12827
13362
  process.exit(1);
@@ -12834,7 +13369,7 @@ function cmdAttach(rest) {
12834
13369
  console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
12835
13370
  console.log();
12836
13371
  const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
12837
- const r = spawnSync7("tmux", args2, { stdio: "inherit" });
13372
+ const r = spawnSync8("tmux", args2, { stdio: "inherit" });
12838
13373
  process.exit(r.status ?? 0);
12839
13374
  }
12840
13375
  async function cmdTest() {
@@ -12933,8 +13468,8 @@ var init_localCc = __esm({
12933
13468
  init_install();
12934
13469
  init_client();
12935
13470
  init_stub();
12936
- SYNKRO_CONFIG_PATH = join15(homedir15(), ".synkro", "config.env");
12937
- CONFIG_PATH4 = join15(homedir15(), ".synkro", "config.env");
13471
+ SYNKRO_CONFIG_PATH = join16(homedir16(), ".synkro", "config.env");
13472
+ CONFIG_PATH4 = join16(homedir16(), ".synkro", "config.env");
12938
13473
  }
12939
13474
  });
12940
13475
 
@@ -13004,6 +13539,11 @@ async function updateCommand() {
13004
13539
  console.log("\nSynkro updated \u2014 now running the latest version.");
13005
13540
  }
13006
13541
  async function restartCommand(rest = []) {
13542
+ const location = await reconcileDeployLocation();
13543
+ if (location === "cloud") {
13544
+ console.log("Synkro: grading runs on Synkro Cloud \u2014 no local container to restart.");
13545
+ return;
13546
+ }
13007
13547
  assertDockerAvailable();
13008
13548
  const cfg = resolveWorkerConfig(rest);
13009
13549
  let claudeWorkers = cfg.claudeWorkers;
@@ -13045,13 +13585,13 @@ var config_exports = {};
13045
13585
  __export(config_exports, {
13046
13586
  configCommand: () => configCommand
13047
13587
  });
13048
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
13049
- import { join as join16 } from "path";
13050
- import { homedir as homedir16 } from "os";
13588
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
13589
+ import { join as join17 } from "path";
13590
+ import { homedir as homedir17 } from "os";
13051
13591
  function readConfigEnv() {
13052
13592
  if (!existsSync17(CONFIG_PATH5)) return {};
13053
13593
  const out = {};
13054
- for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
13594
+ for (const line of readFileSync15(CONFIG_PATH5, "utf-8").split("\n")) {
13055
13595
  const t = line.trim();
13056
13596
  if (!t || t.startsWith("#")) continue;
13057
13597
  const eq = t.indexOf("=");
@@ -13064,7 +13604,7 @@ function updateConfigValue(key, value) {
13064
13604
  console.error("No config found. Run `synkro install` first.");
13065
13605
  process.exit(1);
13066
13606
  }
13067
- const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
13607
+ const lines = readFileSync15(CONFIG_PATH5, "utf-8").split("\n");
13068
13608
  const pattern = new RegExp(`^${key}=`);
13069
13609
  let found = false;
13070
13610
  const updated = lines.map((line) => {
@@ -13180,25 +13720,25 @@ To change:`);
13180
13720
  updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
13181
13721
  console.log(`\u2713 Inference set to '${inferenceValue}'.`);
13182
13722
  }
13183
- var SYNKRO_DIR6, CONFIG_PATH5;
13723
+ var SYNKRO_DIR7, CONFIG_PATH5;
13184
13724
  var init_config = __esm({
13185
13725
  "cli/commands/config.ts"() {
13186
13726
  "use strict";
13187
13727
  init_stub();
13188
- SYNKRO_DIR6 = join16(homedir16(), ".synkro");
13189
- CONFIG_PATH5 = join16(SYNKRO_DIR6, "config.env");
13728
+ SYNKRO_DIR7 = join17(homedir17(), ".synkro");
13729
+ CONFIG_PATH5 = join17(SYNKRO_DIR7, "config.env");
13190
13730
  }
13191
13731
  });
13192
13732
 
13193
13733
  // cli/bootstrap.js
13194
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
13734
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
13195
13735
  import { resolve as resolve3 } from "path";
13196
13736
  var envCandidates = [
13197
13737
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
13198
13738
  ];
13199
13739
  for (const envPath of envCandidates) {
13200
13740
  if (!existsSync18(envPath)) continue;
13201
- const envContent = readFileSync15(envPath, "utf-8");
13741
+ const envContent = readFileSync16(envPath, "utf-8");
13202
13742
  for (const line of envContent.split("\n")) {
13203
13743
  const trimmed = line.trim();
13204
13744
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -13213,7 +13753,7 @@ var args = process.argv.slice(2);
13213
13753
  var cmd = args[0] || "";
13214
13754
  var subArgs = args.slice(1);
13215
13755
  function printVersion() {
13216
- console.log("1.6.58");
13756
+ console.log("1.6.60");
13217
13757
  }
13218
13758
  function printHelp2() {
13219
13759
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -13327,7 +13867,7 @@ async function main() {
13327
13867
  }
13328
13868
  case "claude-desktop": {
13329
13869
  const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
13330
- await runClaudeDesktopTap2();
13870
+ await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
13331
13871
  break;
13332
13872
  }
13333
13873
  default: {