@synkro-sh/cli 1.6.58 → 1.6.59

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
 
@@ -9620,16 +9981,16 @@ __export(install_exports, {
9620
9981
  syncSkillFiles: () => syncSkillFiles,
9621
9982
  writeHookScripts: () => writeHookScripts
9622
9983
  });
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";
9984
+ 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";
9985
+ import { homedir as homedir10 } from "os";
9986
+ import { join as join10 } from "path";
9626
9987
  import { execSync as execSync6 } from "child_process";
9627
9988
  import { createInterface as createInterface3 } from "readline";
9628
9989
  function resolvePersistedHookMode() {
9629
9990
  if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
9630
9991
  if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
9631
9992
  try {
9632
- const env = readFileSync9(CONFIG_PATH2, "utf-8");
9993
+ const env = readFileSync10(CONFIG_PATH2, "utf-8");
9633
9994
  if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
9634
9995
  } catch {
9635
9996
  }
@@ -9649,6 +10010,7 @@ function parseArgs(argv) {
9649
10010
  else if (a === "--no-mcp") opts.noMcp = true;
9650
10011
  else if (a === "--force" || a === "-f") opts.force = true;
9651
10012
  else if (a === "--link-repo") opts.linkRepo = true;
10013
+ else if (a === "--byok-gateway") opts.byokGateway = true;
9652
10014
  else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
9653
10015
  else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
9654
10016
  }
@@ -9721,25 +10083,12 @@ async function promptCursorApiKey(opts) {
9721
10083
  console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
9722
10084
  }
9723
10085
  }
9724
- async function promptGradingMode() {
10086
+ async function promptDeployLocation() {
9725
10087
  if (!process.stdin.isTTY) return "local";
9726
10088
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
9727
10089
  return new Promise((resolve4) => {
9728
10090
  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: ",
10091
+ "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
10092
  (answer) => {
9744
10093
  rl.close();
9745
10094
  resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
@@ -9778,36 +10127,36 @@ coding patterns and the dashboard shows full history. (Y/n) `
9778
10127
  }
9779
10128
  }
9780
10129
  function ensureSynkroDir() {
9781
- mkdirSync9(SYNKRO_DIR4, { recursive: true });
10130
+ mkdirSync9(SYNKRO_DIR5, { recursive: true });
9782
10131
  mkdirSync9(HOOKS_DIR, { recursive: true });
9783
10132
  mkdirSync9(BIN_DIR, { recursive: true });
9784
10133
  mkdirSync9(OFFSETS_DIR, { recursive: true });
9785
- mkdirSync9(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
10134
+ mkdirSync9(join10(SYNKRO_DIR5, "sessions"), { recursive: true });
9786
10135
  }
9787
10136
  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");
10137
+ const installExtractCorePath = join10(HOOKS_DIR, "installExtractCore.ts");
10138
+ const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.ts");
10139
+ const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.ts");
10140
+ const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.ts");
10141
+ const cwePrecheckScriptPath = join10(HOOKS_DIR, "cc-cwe-precheck.ts");
10142
+ const cvePrecheckScriptPath = join10(HOOKS_DIR, "cc-cve-precheck.ts");
10143
+ const planJudgeScriptPath = join10(HOOKS_DIR, "cc-plan-judge.ts");
10144
+ const agentJudgeScriptPath = join10(HOOKS_DIR, "cc-agent-judge.ts");
10145
+ const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.ts");
10146
+ const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.ts");
10147
+ const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.ts");
10148
+ const userPromptSubmitScriptPath = join10(HOOKS_DIR, "cc-user-prompt-submit.ts");
10149
+ const commonScriptPath = join10(HOOKS_DIR, "_synkro-common.ts");
10150
+ const commonBashScriptPath = join10(HOOKS_DIR, "_synkro-common.sh");
10151
+ const installScanScriptPath = join10(HOOKS_DIR, "cc-install-scan.ts");
10152
+ const cursorBashJudgePath = join10(HOOKS_DIR, "cursor-bash-judge.ts");
10153
+ const cursorEditCapturePath = join10(HOOKS_DIR, "cursor-edit-capture.ts");
10154
+ const cursorAgentCapturePath = join10(HOOKS_DIR, "cursor-agent-capture.ts");
10155
+ const mcpStdioProxyPath = join10(HOOKS_DIR, "mcp-stdio-proxy.ts");
10156
+ const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
10157
+ const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
9809
10158
  if (mode === "stub") {
9810
- const stubCommonPath = join9(HOOKS_DIR, "_synkro-stub-common.ts");
10159
+ const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
9811
10160
  const stubFiles = [
9812
10161
  [stubCommonPath, STUB_COMMON_TS],
9813
10162
  [bashScriptPath, STUB_BASH_JUDGE_TS],
@@ -9836,7 +10185,7 @@ function writeHookScripts(mode = "stub") {
9836
10185
  chmodSync2(mcpStdioProxyPath, 493);
9837
10186
  for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
9838
10187
  try {
9839
- unlinkSync4(join9(HOOKS_DIR, stale));
10188
+ unlinkSync4(join10(HOOKS_DIR, stale));
9840
10189
  } catch {
9841
10190
  }
9842
10191
  }
@@ -9903,7 +10252,7 @@ function writeHookScripts(mode = "stub") {
9903
10252
  chmodSync2(taskActivateIntentScriptPath, 493);
9904
10253
  chmodSync2(mcpGateScriptPath, 493);
9905
10254
  try {
9906
- unlinkSync4(join9(HOOKS_DIR, "_synkro-stub-common.ts"));
10255
+ unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
9907
10256
  } catch {
9908
10257
  }
9909
10258
  return {
@@ -9939,7 +10288,7 @@ function resolveSynkroBundle() {
9939
10288
  return null;
9940
10289
  }
9941
10290
  function writeConfigEnv(opts) {
9942
- const credsPath = join9(SYNKRO_DIR4, "credentials.json");
10291
+ const credsPath = join10(SYNKRO_DIR5, "credentials.json");
9943
10292
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
9944
10293
  const safeUserId = sanitizeConfigValue(opts.userId);
9945
10294
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -9955,7 +10304,7 @@ function writeConfigEnv(opts) {
9955
10304
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
9956
10305
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
9957
10306
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
9958
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.58")}`
10307
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.59")}`
9959
10308
  ];
9960
10309
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
9961
10310
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -9969,17 +10318,66 @@ function writeConfigEnv(opts) {
9969
10318
  lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
9970
10319
  lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
9971
10320
  lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
10321
+ lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
9972
10322
  lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
9973
10323
  lines.push("");
9974
10324
  writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
9975
10325
  chmodSync2(CONFIG_PATH2, 384);
9976
10326
  }
10327
+ async function provisionCloudContainer(opts) {
10328
+ let setupToken;
10329
+ try {
10330
+ console.log(" Authorize your Claude key for the hosted Synkro worker \u2014");
10331
+ console.log(" a browser window will open for approval...\n");
10332
+ setupToken = await captureClaudeSetupToken();
10333
+ } catch (err) {
10334
+ console.error(` \u2717 Could not capture Claude setup-token: ${err.message}`);
10335
+ console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
10336
+ process.exit(1);
10337
+ }
10338
+ const repo = detectGitRepo2() || void 0;
10339
+ const harness = [];
10340
+ if (opts.hasClaudeCode) harness.push("claude-code");
10341
+ if (opts.hasCursor) harness.push("cursor");
10342
+ const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
10343
+ try {
10344
+ const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
10345
+ method: "POST",
10346
+ headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
10347
+ body: JSON.stringify({
10348
+ org_id: opts.orgId,
10349
+ user_id: opts.userId,
10350
+ email: opts.email,
10351
+ harness,
10352
+ claude_workers: total,
10353
+ cursor_workers: 0,
10354
+ connected_repo: repo,
10355
+ ruleset: readFullSynkroFile()?.ruleset || "default",
10356
+ setup_token: setupToken
10357
+ // never logged; gateway stores it as the org secret
10358
+ })
10359
+ });
10360
+ if (!resp.ok) {
10361
+ const t = await resp.text().catch(() => "");
10362
+ throw new Error(`cloud-provision failed (${resp.status}): ${t.slice(0, 200)}`);
10363
+ }
10364
+ const out = await resp.json().catch(() => ({}));
10365
+ console.log(` \u2713 cloud container provisioned${out.container_id ? ` (${out.container_id})` : ""}`);
10366
+ console.log();
10367
+ } catch (err) {
10368
+ console.error(` \u2717 Cloud provisioning failed: ${err.message}`);
10369
+ console.error(" Hooks + MCP are installed; re-run `synkro install` \u2192 cloud to retry.");
10370
+ console.log();
10371
+ } finally {
10372
+ setupToken = "";
10373
+ }
10374
+ }
9977
10375
  function resolveDeploymentMode() {
9978
10376
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
9979
10377
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
9980
10378
  try {
9981
10379
  if (existsSync11(CONFIG_PATH2)) {
9982
- const m = readFileSync9(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
10380
+ const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
9983
10381
  const val = m?.[1]?.toLowerCase();
9984
10382
  if (val === "bare-host" || val === "docker") return val;
9985
10383
  }
@@ -10006,16 +10404,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
10006
10404
  meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
10007
10405
  } catch {
10008
10406
  }
10009
- const claudeDir = join9(homedir9(), ".claude");
10407
+ const claudeDir = join10(homedir10(), ".claude");
10010
10408
  try {
10011
- const settings = JSON.parse(readFileSync9(join9(claudeDir, "settings.json"), "utf-8"));
10409
+ const settings = JSON.parse(readFileSync10(join10(claudeDir, "settings.json"), "utf-8"));
10012
10410
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
10013
10411
  if (plugins.length) meta.enabled_plugins = plugins;
10014
10412
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
10015
10413
  } catch {
10016
10414
  }
10017
10415
  try {
10018
- const mcpCache = JSON.parse(readFileSync9(join9(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
10416
+ const mcpCache = JSON.parse(readFileSync10(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
10019
10417
  const mcpNames = Object.keys(mcpCache);
10020
10418
  if (mcpNames.length) meta.mcp_servers = mcpNames;
10021
10419
  } catch {
@@ -10027,10 +10425,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
10027
10425
  } catch {
10028
10426
  }
10029
10427
  try {
10030
- const sessionsDir = join9(claudeDir, "sessions");
10428
+ const sessionsDir = join10(claudeDir, "sessions");
10031
10429
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
10032
10430
  for (const f of files) {
10033
- const s = JSON.parse(readFileSync9(join9(sessionsDir, f), "utf-8"));
10431
+ const s = JSON.parse(readFileSync10(join10(sessionsDir, f), "utf-8"));
10034
10432
  if (s.version) {
10035
10433
  meta.cc_version = meta.cc_version || s.version;
10036
10434
  break;
@@ -10138,6 +10536,7 @@ async function installCommand(opts = {}) {
10138
10536
  let agents;
10139
10537
  let gradingMode;
10140
10538
  let storageMode;
10539
+ let deployLocation = "local";
10141
10540
  let transcriptCC = false;
10142
10541
  let transcriptCursor = false;
10143
10542
  if (existingSynkro) {
@@ -10147,8 +10546,9 @@ async function installCommand(opts = {}) {
10147
10546
  (a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
10148
10547
  );
10149
10548
  if (agents.length === 0 && detected.length > 0) agents = detected;
10549
+ deployLocation = existingSynkro.grader.location === "cloud" ? "cloud" : "local";
10150
10550
  gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
10151
- storageMode = "local";
10551
+ storageMode = deployLocation === "cloud" ? "cloud" : "local";
10152
10552
  console.log(`Using .synkro config:`);
10153
10553
  for (const w of existingSynkro.warnings) console.warn(` \u26A0 ${w}`);
10154
10554
  console.log(` harness: ${existingSynkro.harness.join(", ")}`);
@@ -10172,15 +10572,20 @@ async function installCommand(opts = {}) {
10172
10572
  console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
10173
10573
  `);
10174
10574
  }
10175
- gradingMode = await promptGradingMode();
10176
- storageMode = await promptStorageMode();
10177
- console.log(` grading: ${gradingMode} storage: ${storageMode}
10575
+ if (opts.byokGateway) {
10576
+ gradingMode = "byok";
10577
+ storageMode = "cloud";
10578
+ deployLocation = "local";
10579
+ console.log(" deploy: legacy BYOK-gateway grading (no container)\n");
10580
+ } else {
10581
+ deployLocation = await promptDeployLocation();
10582
+ gradingMode = "local";
10583
+ storageMode = deployLocation === "cloud" ? "cloud" : "local";
10584
+ console.log(` deploy: ${deployLocation}
10178
10585
  `);
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
10586
  }
10183
10587
  }
10588
+ const target = deployLocation === "cloud" ? "cloud-container" : opts.byokGateway || gradingMode === "byok" ? "gateway-byok" : "local-docker";
10184
10589
  {
10185
10590
  const wantCC = agents.some((a) => a.kind === "claude_code");
10186
10591
  const wantCursor = agents.some((a) => a.kind === "cursor");
@@ -10205,9 +10610,9 @@ async function installCommand(opts = {}) {
10205
10610
  const scripts = writeHookScripts(hookMode);
10206
10611
  console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
10207
10612
  for (const mode of ["edit", "bash"]) {
10208
- const pidFile = join9(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
10613
+ const pidFile = join10(SYNKRO_DIR5, "daemon", mode, "daemon.pid");
10209
10614
  try {
10210
- const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
10615
+ const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
10211
10616
  if (pid > 0) {
10212
10617
  process.kill(pid, "SIGTERM");
10213
10618
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -10272,10 +10677,11 @@ async function installCommand(opts = {}) {
10272
10677
  } catch {
10273
10678
  }
10274
10679
  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");
10680
+ const useLocalMcp = target === "local-docker";
10681
+ if (target === "cloud-container") {
10682
+ console.log("Cloud setup \u2014 Synkro will provision a private hosted container for your org.\n");
10683
+ } else if (target === "gateway-byok") {
10684
+ console.log("Cloud-only setup (BYOK grading via gateway) \u2014 skipping the local container.\n");
10279
10685
  }
10280
10686
  if (hasClaudeCode && !opts.noMcp) {
10281
10687
  if (useLocalMcp) {
@@ -10289,7 +10695,7 @@ async function installCommand(opts = {}) {
10289
10695
  if (mintResp.ok) {
10290
10696
  const minted = await mintResp.json();
10291
10697
  mcpJwt = minted.token;
10292
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
10698
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
10293
10699
  } else {
10294
10700
  console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
10295
10701
  }
@@ -10317,7 +10723,7 @@ async function installCommand(opts = {}) {
10317
10723
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
10318
10724
  }
10319
10725
  const minted = await mintResp.json();
10320
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10726
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10321
10727
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
10322
10728
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
10323
10729
  console.log(` url: ${mcp.url}`);
@@ -10334,7 +10740,7 @@ async function installCommand(opts = {}) {
10334
10740
  if (hasCursor && !opts.noMcp) {
10335
10741
  try {
10336
10742
  if (useLocalMcp) {
10337
- const jwtPath = join9(SYNKRO_DIR4, ".mcp-jwt");
10743
+ const jwtPath = join10(SYNKRO_DIR5, ".mcp-jwt");
10338
10744
  if (!existsSync11(jwtPath)) {
10339
10745
  const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
10340
10746
  method: "POST",
@@ -10363,7 +10769,7 @@ async function installCommand(opts = {}) {
10363
10769
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
10364
10770
  }
10365
10771
  const minted = await mintResp.json();
10366
- writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10772
+ writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
10367
10773
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
10368
10774
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
10369
10775
  console.log(` url: ${mcp.url}`);
@@ -10376,7 +10782,7 @@ async function installCommand(opts = {}) {
10376
10782
  }
10377
10783
  const synkroBundle = resolveSynkroBundle();
10378
10784
  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 });
10785
+ writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
10380
10786
  console.log(`Wrote config to ${CONFIG_PATH2}`);
10381
10787
  console.log(` inference: ${profile.inference} (server-side grading)`);
10382
10788
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
@@ -10388,7 +10794,7 @@ async function installCommand(opts = {}) {
10388
10794
  } catch (err) {
10389
10795
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
10390
10796
  }
10391
- writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
10797
+ writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
10392
10798
  console.log();
10393
10799
  if (useLocalMcp) {
10394
10800
  const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
@@ -10439,7 +10845,7 @@ async function installCommand(opts = {}) {
10439
10845
  const ready = await waitForContainerReady(6e4);
10440
10846
  if (ready) {
10441
10847
  console.log(" \u2713 container ready");
10442
- const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
10848
+ const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10443
10849
  try {
10444
10850
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
10445
10851
  method: "POST",
@@ -10469,6 +10875,8 @@ async function installCommand(opts = {}) {
10469
10875
  process.exit(1);
10470
10876
  }
10471
10877
  console.log();
10878
+ } else if (target === "cloud-container") {
10879
+ await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
10472
10880
  }
10473
10881
  if (transcriptConsent) {
10474
10882
  const repo = detectGitRepo2();
@@ -10477,7 +10885,7 @@ async function installCommand(opts = {}) {
10477
10885
  try {
10478
10886
  let mcpToken = "";
10479
10887
  try {
10480
- mcpToken = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
10888
+ mcpToken = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10481
10889
  } catch {
10482
10890
  }
10483
10891
  if (mcpToken) {
@@ -10605,11 +11013,11 @@ function writeSynkroFileIfMissing(opts) {
10605
11013
  try {
10606
11014
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
10607
11015
  if (!root) return;
10608
- if (root === homedir9()) return;
10609
- const fp = join9(root, "synkro.toml");
11016
+ if (root === homedir10()) return;
11017
+ const fp = join10(root, "synkro.toml");
10610
11018
  let hasFile = false;
10611
11019
  try {
10612
- hasFile = statSync2(fp).isFile();
11020
+ hasFile = statSync(fp).isFile();
10613
11021
  } catch {
10614
11022
  }
10615
11023
  if (hasFile) {
@@ -10637,6 +11045,7 @@ function writeSynkroFileIfMissing(opts) {
10637
11045
  "skills = []",
10638
11046
  "",
10639
11047
  "[grader]",
11048
+ `location = "${opts.deployLocation || "local"}"`,
10640
11049
  `pool = "${pool}"`,
10641
11050
  `mode = "${mode}"`,
10642
11051
  "",
@@ -10654,9 +11063,9 @@ function readFullSynkroFile() {
10654
11063
  try {
10655
11064
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
10656
11065
  if (!root) return null;
10657
- const fp = join9(root, "synkro.toml");
11066
+ const fp = join10(root, "synkro.toml");
10658
11067
  if (!existsSync11(fp)) return null;
10659
- const parsed = parseSynkroToml2(readFileSync9(fp, "utf-8"));
11068
+ const parsed = parseSynkroToml2(readFileSync10(fp, "utf-8"));
10660
11069
  const valid = ["claude-code", "cursor"];
10661
11070
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
10662
11071
  const resolved = resolveGraderPool(parsed);
@@ -10664,7 +11073,8 @@ function readFullSynkroFile() {
10664
11073
  harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
10665
11074
  grader: {
10666
11075
  pool: resolved.pool,
10667
- mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
11076
+ mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local",
11077
+ location: parsed.grader?.location === "cloud" ? "cloud" : "local"
10668
11078
  },
10669
11079
  workers: {
10670
11080
  ...resolved.claudeWorkers != null ? { claude: resolved.claudeWorkers } : {},
@@ -10692,7 +11102,7 @@ function reconcileHarness() {
10692
11102
  console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
10693
11103
  const scripts = writeHookScripts(resolvePersistedHookMode());
10694
11104
  console.log("Wrote hook scripts to ~/.synkro/hooks/");
10695
- const ccSettings = join9(homedir9(), ".claude", "settings.json");
11105
+ const ccSettings = join10(homedir10(), ".claude", "settings.json");
10696
11106
  if (wantCC) {
10697
11107
  installCCHooks(ccSettings, {
10698
11108
  bashJudgeScriptPath: scripts.bashScript,
@@ -10712,7 +11122,7 @@ function reconcileHarness() {
10712
11122
  });
10713
11123
  console.log(" \u2713 Claude Code hooks registered");
10714
11124
  try {
10715
- const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
11125
+ const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10716
11126
  if (mcpJwt) {
10717
11127
  installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
10718
11128
  console.log(" \u2713 Claude Code MCP registered");
@@ -10722,8 +11132,9 @@ function reconcileHarness() {
10722
11132
  } else {
10723
11133
  if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
10724
11134
  if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
11135
+ if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
10725
11136
  }
10726
- const cursorHooks = join9(homedir9(), ".cursor", "hooks.json");
11137
+ const cursorHooks = join10(homedir10(), ".cursor", "hooks.json");
10727
11138
  if (wantCursor) {
10728
11139
  installCursorHooks(cursorHooks, {
10729
11140
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
@@ -10777,7 +11188,7 @@ async function syncSkillFiles() {
10777
11188
  if (resolved.length === 0) return;
10778
11189
  const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
10779
11190
  const tasks = resolved.map((fp) => {
10780
- const content = readFileSync9(fp, "utf-8");
11191
+ const content = readFileSync10(fp, "utf-8");
10781
11192
  const source = `skill:${fp.split("/").pop()}`;
10782
11193
  if (!content.trim()) {
10783
11194
  console.log(` \u2298 skill ${source}: empty file, skipped`);
@@ -10823,7 +11234,7 @@ function detectGitRepo2() {
10823
11234
  function getClaudeProjectsFolder() {
10824
11235
  const cwd = process.cwd();
10825
11236
  const sanitized = "-" + cwd.replace(/\//g, "-");
10826
- const projectsDir = join9(homedir9(), ".claude", "projects", sanitized);
11237
+ const projectsDir = join10(homedir10(), ".claude", "projects", sanitized);
10827
11238
  return existsSync11(projectsDir) ? projectsDir : null;
10828
11239
  }
10829
11240
  function extractSessionInsights(projectsDir) {
@@ -10831,9 +11242,9 @@ function extractSessionInsights(projectsDir) {
10831
11242
  const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
10832
11243
  for (const file of files) {
10833
11244
  const sessionId = file.replace(".jsonl", "");
10834
- const filePath = join9(projectsDir, file);
11245
+ const filePath = join10(projectsDir, file);
10835
11246
  try {
10836
- const content = readFileSync9(filePath, "utf-8");
11247
+ const content = readFileSync10(filePath, "utf-8");
10837
11248
  const lines = content.split("\n").filter(Boolean);
10838
11249
  for (let i = 0; i < lines.length; i++) {
10839
11250
  try {
@@ -10912,14 +11323,14 @@ function cursorProjectSlug(workspaceRoot) {
10912
11323
  return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10913
11324
  }
10914
11325
  function getCursorTranscriptsDir() {
10915
- const dir = join9(homedir9(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
11326
+ const dir = join10(homedir10(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
10916
11327
  return existsSync11(dir) ? dir : null;
10917
11328
  }
10918
11329
  function isSafeConvId(id) {
10919
11330
  return /^[A-Za-z0-9_-]+$/.test(id);
10920
11331
  }
10921
11332
  function parseCursorTranscriptFile(filePath) {
10922
- const content = readFileSync9(filePath, "utf-8");
11333
+ const content = readFileSync10(filePath, "utf-8");
10923
11334
  const lines = content.split("\n").filter(Boolean);
10924
11335
  const messages = [];
10925
11336
  for (let i = 0; i < lines.length; i++) {
@@ -10951,7 +11362,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10951
11362
  for (let i = 0; i < convDirs.length; i++) {
10952
11363
  const convId = convDirs[i];
10953
11364
  if (!isSafeConvId(convId)) continue;
10954
- const filePath = join9(dir, convId, `${convId}.jsonl`);
11365
+ const filePath = join10(dir, convId, `${convId}.jsonl`);
10955
11366
  if (!existsSync11(filePath)) continue;
10956
11367
  try {
10957
11368
  const all = parseCursorTranscriptFile(filePath);
@@ -10974,8 +11385,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10974
11385
  process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
10975
11386
  }
10976
11387
  try {
10977
- const lc = readFileSync9(filePath, "utf-8").split("\n").filter(Boolean).length;
10978
- writeFileSync8(join9(OFFSETS_DIR, convId), String(lc), "utf-8");
11388
+ const lc = readFileSync10(filePath, "utf-8").split("\n").filter(Boolean).length;
11389
+ writeFileSync8(join10(OFFSETS_DIR, convId), String(lc), "utf-8");
10979
11390
  } catch {
10980
11391
  }
10981
11392
  }
@@ -10983,7 +11394,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
10983
11394
  return { sessions: totalSessions, messages: totalMessages };
10984
11395
  }
10985
11396
  function parseTranscriptFile(filePath) {
10986
- const content = readFileSync9(filePath, "utf-8");
11397
+ const content = readFileSync10(filePath, "utf-8");
10987
11398
  const lines = content.split("\n").filter(Boolean);
10988
11399
  const messages = [];
10989
11400
  for (let i = 0; i < lines.length; i++) {
@@ -11031,7 +11442,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
11031
11442
  for (let i = 0; i < files.length; i++) {
11032
11443
  const file = files[i];
11033
11444
  const sessionId = file.replace(".jsonl", "");
11034
- const filePath = join9(projectsDir, file);
11445
+ const filePath = join10(projectsDir, file);
11035
11446
  try {
11036
11447
  const allMessages = parseTranscriptFile(filePath);
11037
11448
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
@@ -11053,9 +11464,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
11053
11464
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
11054
11465
  }
11055
11466
  try {
11056
- const content = readFileSync9(join9(projectsDir, file), "utf-8");
11467
+ const content = readFileSync10(join10(projectsDir, file), "utf-8");
11057
11468
  const lineCount = content.split("\n").filter(Boolean).length;
11058
- writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11469
+ writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11059
11470
  } catch {
11060
11471
  }
11061
11472
  }
@@ -11076,7 +11487,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
11076
11487
  const sessions = [];
11077
11488
  for (const file of batch) {
11078
11489
  const sessionId = file.replace(".jsonl", "");
11079
- const filePath = join9(projectsDir, file);
11490
+ const filePath = join10(projectsDir, file);
11080
11491
  try {
11081
11492
  const allMessages = parseTranscriptFile(filePath);
11082
11493
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -11105,18 +11516,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
11105
11516
  }
11106
11517
  for (const file of batch) {
11107
11518
  const sessionId = file.replace(".jsonl", "");
11108
- const filePath = join9(projectsDir, file);
11519
+ const filePath = join10(projectsDir, file);
11109
11520
  try {
11110
- const content = readFileSync9(filePath, "utf-8");
11521
+ const content = readFileSync10(filePath, "utf-8");
11111
11522
  const lineCount = content.split("\n").filter(Boolean).length;
11112
- writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11523
+ writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
11113
11524
  } catch {
11114
11525
  }
11115
11526
  }
11116
11527
  }
11117
11528
  return { sessions: totalSessions, messages: totalMessages };
11118
11529
  }
11119
- var SYNKRO_DIR4, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
11530
+ var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
11120
11531
  var init_install = __esm({
11121
11532
  "cli/commands/install.ts"() {
11122
11533
  "use strict";
@@ -11134,10 +11545,11 @@ var init_install = __esm({
11134
11545
  init_promptFetcher();
11135
11546
  init_claudeDesktopTap();
11136
11547
  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");
11548
+ init_setupToken();
11549
+ SYNKRO_DIR5 = join10(homedir10(), ".synkro");
11550
+ HOOKS_DIR = join10(SYNKRO_DIR5, "hooks");
11551
+ BIN_DIR = join10(SYNKRO_DIR5, "bin");
11552
+ CONFIG_PATH2 = join10(SYNKRO_DIR5, "config.env");
11141
11553
  MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
11142
11554
  import { readFileSync } from 'node:fs';
11143
11555
  import { homedir } from 'node:os';
@@ -11248,15 +11660,15 @@ rl.on('line', async (line) => {
11248
11660
  }
11249
11661
  });
11250
11662
  `;
11251
- OFFSETS_DIR = join9(SYNKRO_DIR4, ".transcript-offsets");
11663
+ OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
11252
11664
  }
11253
11665
  });
11254
11666
 
11255
11667
  // 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";
11668
+ 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";
11669
+ import { join as join11 } from "path";
11670
+ import { homedir as homedir11 } from "os";
11671
+ import { spawnSync as spawnSync5 } from "child_process";
11260
11672
  function writePluginFiles() {
11261
11673
  for (const c of CHANNELS) {
11262
11674
  mkdirSync10(c.sessionDir, { recursive: true });
@@ -11276,7 +11688,7 @@ function writePluginFiles() {
11276
11688
  }
11277
11689
  function runBunInstall() {
11278
11690
  for (const c of CHANNELS) {
11279
- const r = spawnSync4("bun", ["install", "--silent"], {
11691
+ const r = spawnSync5("bun", ["install", "--silent"], {
11280
11692
  cwd: c.sessionDir,
11281
11693
  encoding: "utf-8",
11282
11694
  timeout: 12e4
@@ -11292,7 +11704,7 @@ function safelyMutateClaudeJson(mutator) {
11292
11704
  if (!existsSync12(CLAUDE_JSON_PATH)) {
11293
11705
  return;
11294
11706
  }
11295
- const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
11707
+ const originalText = readFileSync11(CLAUDE_JSON_PATH, "utf-8");
11296
11708
  let parsed;
11297
11709
  try {
11298
11710
  parsed = JSON.parse(originalText);
@@ -11392,15 +11804,15 @@ function patchClaudeJson() {
11392
11804
  });
11393
11805
  }
11394
11806
  function installLocalCC() {
11395
- let bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
11807
+ let bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
11396
11808
  if (bunCheck.status !== 0) {
11397
11809
  if (process.platform === "darwin") {
11398
11810
  console.log(" Installing bun via brew...");
11399
- const brewR = spawnSync4("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
11811
+ const brewR = spawnSync5("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
11400
11812
  if (brewR.status !== 0) {
11401
11813
  throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
11402
11814
  }
11403
- bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
11815
+ bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
11404
11816
  if (bunCheck.status !== 0) {
11405
11817
  throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
11406
11818
  }
@@ -11434,42 +11846,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
11434
11846
  var init_install2 = __esm({
11435
11847
  "cli/local-cc/install.ts"() {
11436
11848
  "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");
11849
+ CLAUDE_JSON_BACKUP_PATH = join11(homedir11(), ".claude.json.synkro-bak");
11850
+ SESSION_DIR = join11(homedir11(), ".synkro", "cc_sessions");
11851
+ PLUGIN_PATH = join11(SESSION_DIR, "synkro-channel.ts");
11852
+ PLUGIN_PKG_PATH = join11(SESSION_DIR, "package.json");
11853
+ PLUGIN_SETTINGS_DIR = join11(SESSION_DIR, ".claude");
11854
+ PLUGIN_SETTINGS_PATH = join11(PLUGIN_SETTINGS_DIR, "settings.json");
11855
+ PROJECT_MCP_PATH = join11(SESSION_DIR, ".mcp.json");
11856
+ CLAUDE_JSON_PATH = join11(homedir11(), ".claude.json");
11857
+ RUN_SCRIPT_PATH = join11(SESSION_DIR, "run-claude.sh");
11446
11858
  TMUX_SESSION_NAME = "synkro-local-cc";
11447
11859
  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");
11860
+ SESSION_DIR_2 = join11(homedir11(), ".synkro", "cc_sessions_2");
11861
+ PLUGIN_PATH_2 = join11(SESSION_DIR_2, "synkro-channel.ts");
11862
+ PLUGIN_PKG_PATH_2 = join11(SESSION_DIR_2, "package.json");
11863
+ PLUGIN_SETTINGS_DIR_2 = join11(SESSION_DIR_2, ".claude");
11864
+ PLUGIN_SETTINGS_PATH_2 = join11(PLUGIN_SETTINGS_DIR_2, "settings.json");
11865
+ PROJECT_MCP_PATH_2 = join11(SESSION_DIR_2, ".mcp.json");
11866
+ RUN_SCRIPT_PATH_2 = join11(SESSION_DIR_2, "run-claude.sh");
11455
11867
  TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
11456
11868
  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");
11869
+ SESSION_DIR_3 = join11(homedir11(), ".synkro", "cc_sessions_3");
11870
+ PLUGIN_PATH_3 = join11(SESSION_DIR_3, "synkro-channel.ts");
11871
+ PLUGIN_PKG_PATH_3 = join11(SESSION_DIR_3, "package.json");
11872
+ PLUGIN_SETTINGS_DIR_3 = join11(SESSION_DIR_3, ".claude");
11873
+ PLUGIN_SETTINGS_PATH_3 = join11(PLUGIN_SETTINGS_DIR_3, "settings.json");
11874
+ PROJECT_MCP_PATH_3 = join11(SESSION_DIR_3, ".mcp.json");
11875
+ RUN_SCRIPT_PATH_3 = join11(SESSION_DIR_3, "run-claude.sh");
11464
11876
  TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
11465
11877
  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");
11878
+ SESSION_DIR_4 = join11(homedir11(), ".synkro", "cc_sessions_4");
11879
+ PLUGIN_PATH_4 = join11(SESSION_DIR_4, "synkro-channel.ts");
11880
+ PLUGIN_PKG_PATH_4 = join11(SESSION_DIR_4, "package.json");
11881
+ PLUGIN_SETTINGS_DIR_4 = join11(SESSION_DIR_4, ".claude");
11882
+ PLUGIN_SETTINGS_PATH_4 = join11(PLUGIN_SETTINGS_DIR_4, "settings.json");
11883
+ PROJECT_MCP_PATH_4 = join11(SESSION_DIR_4, ".mcp.json");
11884
+ RUN_SCRIPT_PATH_4 = join11(SESSION_DIR_4, "run-claude.sh");
11473
11885
  TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
11474
11886
  CHANNEL_4_PORT = 8952;
11475
11887
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
@@ -11744,9 +12156,9 @@ __export(disconnect_exports, {
11744
12156
  disconnectCommand: () => disconnectCommand
11745
12157
  });
11746
12158
  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";
12159
+ import { homedir as homedir12 } from "os";
12160
+ import { join as join12 } from "path";
12161
+ import { spawnSync as spawnSync6 } from "child_process";
11750
12162
  import { createInterface as createInterface4 } from "readline";
11751
12163
  async function tearDownLocalCC() {
11752
12164
  const docker = dockerStatus();
@@ -11760,7 +12172,7 @@ async function tearDownLocalCC() {
11760
12172
  console.log("\u2713 removed synkro-server container");
11761
12173
  try {
11762
12174
  const image = imageTag();
11763
- const r = spawnSync5("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
12175
+ const r = spawnSync6("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
11764
12176
  console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
11765
12177
  } catch {
11766
12178
  }
@@ -11819,15 +12231,19 @@ async function disconnectCommand(args2 = []) {
11819
12231
  const cursorMcpRemoved = uninstallCursorMcpConfig();
11820
12232
  console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
11821
12233
  }
11822
- if (existsSync13(SYNKRO_DIR5)) {
12234
+ {
12235
+ const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
12236
+ console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
12237
+ }
12238
+ if (existsSync13(SYNKRO_DIR6)) {
11823
12239
  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`);
12240
+ rmSync(SYNKRO_DIR6, { recursive: true, force: true });
12241
+ console.log(`\u2713 wiped ${SYNKRO_DIR6} entirely \u2014 including all scan data and backups`);
11826
12242
  } else {
11827
- const keep = /* @__PURE__ */ new Set([join11(SYNKRO_DIR5, "pgdata"), join11(SYNKRO_DIR5, "pgdata-backups")]);
12243
+ const keep = /* @__PURE__ */ new Set([join12(SYNKRO_DIR6, "pgdata"), join12(SYNKRO_DIR6, "pgdata-backups")]);
11828
12244
  const preserved = [];
11829
- for (const entry of readdirSync4(SYNKRO_DIR5)) {
11830
- const full = join11(SYNKRO_DIR5, entry);
12245
+ for (const entry of readdirSync4(SYNKRO_DIR6)) {
12246
+ const full = join12(SYNKRO_DIR6, entry);
11831
12247
  if (keep.has(full)) {
11832
12248
  preserved.push(entry);
11833
12249
  continue;
@@ -11835,18 +12251,18 @@ async function disconnectCommand(args2 = []) {
11835
12251
  rmSync(full, { recursive: true, force: true });
11836
12252
  }
11837
12253
  if (preserved.length > 0) {
11838
- console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR5} (kept your scan data: ${preserved.join(", ")})`);
12254
+ console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR6} (kept your scan data: ${preserved.join(", ")})`);
11839
12255
  console.log(" run `synkro uninstall --purge` to delete that too");
11840
12256
  } else {
11841
- console.log(`\u2713 removed ${SYNKRO_DIR5}`);
12257
+ console.log(`\u2713 removed ${SYNKRO_DIR6}`);
11842
12258
  }
11843
12259
  }
11844
12260
  } else {
11845
- console.log(`\xB7 ${SYNKRO_DIR5} already gone`);
12261
+ console.log(`\xB7 ${SYNKRO_DIR6} already gone`);
11846
12262
  }
11847
12263
  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
12264
  }
11849
- var SYNKRO_DIR5;
12265
+ var SYNKRO_DIR6;
11850
12266
  var init_disconnect = __esm({
11851
12267
  "cli/commands/disconnect.ts"() {
11852
12268
  "use strict";
@@ -11857,14 +12273,14 @@ var init_disconnect = __esm({
11857
12273
  init_install2();
11858
12274
  init_dockerInstall();
11859
12275
  init_macKeychain();
11860
- SYNKRO_DIR5 = join11(homedir11(), ".synkro");
12276
+ SYNKRO_DIR6 = join12(homedir12(), ".synkro");
11861
12277
  }
11862
12278
  });
11863
12279
 
11864
12280
  // 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";
12281
+ 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";
12282
+ import { dirname as dirname6, join as join13 } from "path";
12283
+ import { homedir as homedir13 } from "os";
11868
12284
  function truncate(s, max = PREVIEW_MAX) {
11869
12285
  if (s.length <= max) return s;
11870
12286
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -11902,9 +12318,9 @@ function appendTurn(args2) {
11902
12318
  function readRecentTurns(n = 20) {
11903
12319
  if (!existsSync14(TURN_LOG_PATH)) return [];
11904
12320
  try {
11905
- const size = statSync3(TURN_LOG_PATH).size;
12321
+ const size = statSync2(TURN_LOG_PATH).size;
11906
12322
  if (size === 0) return [];
11907
- const text = readFileSync11(TURN_LOG_PATH, "utf-8");
12323
+ const text = readFileSync12(TURN_LOG_PATH, "utf-8");
11908
12324
  const lines = text.split("\n").filter(Boolean);
11909
12325
  const lastN = lines.slice(-n).reverse();
11910
12326
  return lastN.map((line) => {
@@ -11928,7 +12344,7 @@ function followTurns(onEntry) {
11928
12344
  }
11929
12345
  let lastSize = (() => {
11930
12346
  try {
11931
- return statSync3(TURN_LOG_PATH).size;
12347
+ return statSync2(TURN_LOG_PATH).size;
11932
12348
  } catch {
11933
12349
  return 0;
11934
12350
  }
@@ -11983,7 +12399,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
11983
12399
  var init_turnLog = __esm({
11984
12400
  "cli/local-cc/turnLog.ts"() {
11985
12401
  "use strict";
11986
- TURN_LOG_PATH = join12(homedir12(), ".synkro", "cc_sessions", "turns.log");
12402
+ TURN_LOG_PATH = join13(homedir13(), ".synkro", "cc_sessions", "turns.log");
11987
12403
  PREVIEW_MAX = 400;
11988
12404
  }
11989
12405
  });
@@ -12132,19 +12548,19 @@ var init_grade = __esm({
12132
12548
  });
12133
12549
 
12134
12550
  // 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";
12551
+ import { execFileSync as execFileSync3, spawnSync as spawnSync7, spawn as spawn2 } from "child_process";
12552
+ import { homedir as homedir14 } from "os";
12553
+ import { join as join14 } from "path";
12138
12554
  import { connect as connect2 } from "net";
12139
12555
  function pueueAvailable() {
12140
- const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12556
+ const r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12141
12557
  if (r.status !== 0) {
12142
12558
  throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
12143
12559
  }
12144
12560
  }
12145
12561
  function statusJson() {
12146
12562
  pueueAvailable();
12147
- const r = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8" });
12563
+ const r = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8" });
12148
12564
  if (r.status !== 0) {
12149
12565
  throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
12150
12566
  }
@@ -12189,18 +12605,18 @@ function startTask(opts = {}) {
12189
12605
  let existing = findTask(ch);
12190
12606
  while (existing) {
12191
12607
  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" });
12608
+ spawnSync7("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
12609
+ spawnSync7("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
12194
12610
  for (let i = 0; i < 10; i++) {
12195
12611
  const check = findTask(ch);
12196
12612
  if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
12197
- spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
12613
+ spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
12198
12614
  }
12199
12615
  }
12200
- spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
12616
+ spawnSync7("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
12201
12617
  existing = findTask(ch);
12202
12618
  }
12203
- const runScript = join13(cwd, "run-claude.sh");
12619
+ const runScript = join14(cwd, "run-claude.sh");
12204
12620
  const args2 = [
12205
12621
  "add",
12206
12622
  "--label",
@@ -12211,7 +12627,7 @@ function startTask(opts = {}) {
12211
12627
  "bash",
12212
12628
  runScript
12213
12629
  ];
12214
- const r = spawnSync6("pueue", args2, { encoding: "utf-8" });
12630
+ const r = spawnSync7("pueue", args2, { encoding: "utf-8" });
12215
12631
  if (r.status !== 0) {
12216
12632
  throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
12217
12633
  }
@@ -12222,25 +12638,25 @@ function startTask(opts = {}) {
12222
12638
  return created;
12223
12639
  }
12224
12640
  function stopTask(channel = CHANNEL_PRIMARY) {
12225
- spawnSync6("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
12641
+ spawnSync7("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
12226
12642
  let t = findTask(channel);
12227
12643
  while (t) {
12228
12644
  if (t.status === "Running" || t.status === "Queued") {
12229
- spawnSync6("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
12645
+ spawnSync7("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
12230
12646
  for (let i = 0; i < 10; i++) {
12231
12647
  const check = findTask(channel);
12232
12648
  if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
12233
- spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
12649
+ spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
12234
12650
  }
12235
12651
  }
12236
- spawnSync6("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
12652
+ spawnSync7("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
12237
12653
  t = findTask(channel);
12238
12654
  }
12239
12655
  }
12240
12656
  function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
12241
12657
  const t = findTask(channel);
12242
12658
  if (!t) return `(no ${channel.taskLabel} task)`;
12243
- const r = spawnSync6("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
12659
+ const r = spawnSync7("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
12244
12660
  return r.stdout || r.stderr || "(no output)";
12245
12661
  }
12246
12662
  function ensureRunning(opts = {}) {
@@ -12265,8 +12681,8 @@ function probePort(host, port, timeoutMs = 500) {
12265
12681
  });
12266
12682
  }
12267
12683
  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" });
12684
+ spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
12685
+ spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
12270
12686
  }
12271
12687
  async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
12272
12688
  const deadline = Date.now() + timeoutMs;
@@ -12278,46 +12694,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
12278
12694
  return probePort(host, port);
12279
12695
  }
12280
12696
  function brewInstall(pkg) {
12281
- const brew = spawnSync6("brew", ["--version"], { encoding: "utf-8" });
12697
+ const brew = spawnSync7("brew", ["--version"], { encoding: "utf-8" });
12282
12698
  if (brew.status !== 0) return false;
12283
12699
  console.log(` Installing ${pkg} via brew...`);
12284
- const r = spawnSync6("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
12700
+ const r = spawnSync7("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
12285
12701
  return r.status === 0;
12286
12702
  }
12287
12703
  function assertPueueInstalled() {
12288
- let r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12704
+ let r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12289
12705
  if (r.status !== 0) {
12290
12706
  if (process.platform === "darwin" && brewInstall("pueue")) {
12291
- r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
12707
+ r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
12292
12708
  if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
12293
12709
  } else {
12294
12710
  throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
12295
12711
  }
12296
12712
  }
12297
- const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12713
+ const status = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12298
12714
  if (status.status !== 0) {
12299
12715
  console.log(" Starting pueued daemon...");
12300
12716
  const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
12301
12717
  child.unref();
12302
- spawnSync6("sleep", ["1"]);
12303
- const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12718
+ spawnSync7("sleep", ["1"]);
12719
+ const retry = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
12304
12720
  if (retry.status !== 0) {
12305
12721
  throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
12306
12722
  }
12307
12723
  }
12308
- spawnSync6("pueue", ["parallel", "2"], { encoding: "utf-8" });
12724
+ spawnSync7("pueue", ["parallel", "2"], { encoding: "utf-8" });
12309
12725
  }
12310
12726
  function assertClaudeInstalled() {
12311
- const r = spawnSync6("claude", ["--version"], { encoding: "utf-8" });
12727
+ const r = spawnSync7("claude", ["--version"], { encoding: "utf-8" });
12312
12728
  if (r.status !== 0) {
12313
12729
  throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
12314
12730
  }
12315
12731
  }
12316
12732
  function assertTmuxInstalled() {
12317
- let r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
12733
+ let r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
12318
12734
  if (r.status !== 0) {
12319
12735
  if (process.platform === "darwin" && brewInstall("tmux")) {
12320
- r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
12736
+ r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
12321
12737
  if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
12322
12738
  } else {
12323
12739
  throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
@@ -12330,12 +12746,12 @@ var init_pueue = __esm({
12330
12746
  "use strict";
12331
12747
  TASK_LABEL = "synkro-local-cc";
12332
12748
  TMUX_SESSION = "synkro-local-cc";
12333
- SESSION_DIR2 = join13(homedir13(), ".synkro", "cc_sessions");
12749
+ SESSION_DIR2 = join14(homedir14(), ".synkro", "cc_sessions");
12334
12750
  TASK_LABEL_2 = "synkro-local-cc-2";
12335
12751
  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");
12752
+ SESSION_DIR_22 = join14(homedir14(), ".synkro", "cc_sessions_2");
12753
+ SESSION_DIR_32 = join14(homedir14(), ".synkro", "cc_sessions_3");
12754
+ SESSION_DIR_42 = join14(homedir14(), ".synkro", "cc_sessions_4");
12339
12755
  PueueError = class extends Error {
12340
12756
  constructor(message, cause) {
12341
12757
  super(message);
@@ -12350,13 +12766,13 @@ var init_pueue = __esm({
12350
12766
  });
12351
12767
 
12352
12768
  // 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";
12769
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
12770
+ import { homedir as homedir15 } from "os";
12771
+ import { join as join15 } from "path";
12356
12772
  function isLocalCCEnabled() {
12357
12773
  if (!existsSync15(CONFIG_PATH3)) return false;
12358
12774
  try {
12359
- const content = readFileSync12(CONFIG_PATH3, "utf-8");
12775
+ const content = readFileSync13(CONFIG_PATH3, "utf-8");
12360
12776
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
12361
12777
  return match?.[1] === "yes";
12362
12778
  } catch {
@@ -12367,7 +12783,7 @@ var CONFIG_PATH3;
12367
12783
  var init_settings = __esm({
12368
12784
  "cli/local-cc/settings.ts"() {
12369
12785
  "use strict";
12370
- CONFIG_PATH3 = join14(homedir14(), ".synkro", "config.env");
12786
+ CONFIG_PATH3 = join15(homedir15(), ".synkro", "config.env");
12371
12787
  }
12372
12788
  });
12373
12789
 
@@ -12376,11 +12792,11 @@ var localCc_exports = {};
12376
12792
  __export(localCc_exports, {
12377
12793
  localCcCommand: () => localCcCommand
12378
12794
  });
12379
- import { spawnSync as spawnSync7 } from "child_process";
12380
- import { homedir as homedir15 } from "os";
12381
- import { join as join15 } from "path";
12795
+ import { spawnSync as spawnSync8 } from "child_process";
12796
+ import { homedir as homedir16 } from "os";
12797
+ import { join as join16 } from "path";
12382
12798
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
12383
- import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
12799
+ import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
12384
12800
  function deploymentMode() {
12385
12801
  const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
12386
12802
  if (env === "docker") return "docker";
@@ -12487,14 +12903,14 @@ TROUBLESHOOTING
12487
12903
  }
12488
12904
  function readGatewayUrl() {
12489
12905
  if (existsSync16(CONFIG_PATH4)) {
12490
- const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
12906
+ const m = readFileSync14(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
12491
12907
  if (m) return m[1];
12492
12908
  }
12493
12909
  return "https://api.synkro.sh";
12494
12910
  }
12495
12911
  function updateLocalInferenceFlag(enabled) {
12496
12912
  if (!existsSync16(CONFIG_PATH4)) return;
12497
- let content = readFileSync13(CONFIG_PATH4, "utf-8");
12913
+ let content = readFileSync14(CONFIG_PATH4, "utf-8");
12498
12914
  const flag = enabled ? "yes" : "no";
12499
12915
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
12500
12916
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -12557,7 +12973,7 @@ async function cmdStatus() {
12557
12973
  }
12558
12974
  const ch1Up = await isChannelAvailable();
12559
12975
  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" });
12976
+ const tmux1 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
12561
12977
  console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
12562
12978
  const t2 = findTask(CHANNEL_SECONDARY);
12563
12979
  if (!t2) {
@@ -12567,7 +12983,7 @@ async function cmdStatus() {
12567
12983
  }
12568
12984
  const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
12569
12985
  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" });
12986
+ const tmux2 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
12571
12987
  console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
12572
12988
  }
12573
12989
  async function cmdEnable() {
@@ -12773,7 +13189,7 @@ function cmdLogs(rest) {
12773
13189
  }
12774
13190
  return "200";
12775
13191
  })();
12776
- spawnSync7("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
13192
+ spawnSync8("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
12777
13193
  return;
12778
13194
  }
12779
13195
  for (const arg of rest) {
@@ -12821,7 +13237,7 @@ function cmdLogs(rest) {
12821
13237
  function cmdAttach(rest) {
12822
13238
  assertTmuxInstalled();
12823
13239
  const readonly = rest.some((a) => a === "--readonly" || a === "-r");
12824
- const has = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
13240
+ const has = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
12825
13241
  if (has.status !== 0) {
12826
13242
  console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
12827
13243
  process.exit(1);
@@ -12834,7 +13250,7 @@ function cmdAttach(rest) {
12834
13250
  console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
12835
13251
  console.log();
12836
13252
  const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
12837
- const r = spawnSync7("tmux", args2, { stdio: "inherit" });
13253
+ const r = spawnSync8("tmux", args2, { stdio: "inherit" });
12838
13254
  process.exit(r.status ?? 0);
12839
13255
  }
12840
13256
  async function cmdTest() {
@@ -12933,8 +13349,8 @@ var init_localCc = __esm({
12933
13349
  init_install();
12934
13350
  init_client();
12935
13351
  init_stub();
12936
- SYNKRO_CONFIG_PATH = join15(homedir15(), ".synkro", "config.env");
12937
- CONFIG_PATH4 = join15(homedir15(), ".synkro", "config.env");
13352
+ SYNKRO_CONFIG_PATH = join16(homedir16(), ".synkro", "config.env");
13353
+ CONFIG_PATH4 = join16(homedir16(), ".synkro", "config.env");
12938
13354
  }
12939
13355
  });
12940
13356
 
@@ -13045,13 +13461,13 @@ var config_exports = {};
13045
13461
  __export(config_exports, {
13046
13462
  configCommand: () => configCommand
13047
13463
  });
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";
13464
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
13465
+ import { join as join17 } from "path";
13466
+ import { homedir as homedir17 } from "os";
13051
13467
  function readConfigEnv() {
13052
13468
  if (!existsSync17(CONFIG_PATH5)) return {};
13053
13469
  const out = {};
13054
- for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
13470
+ for (const line of readFileSync15(CONFIG_PATH5, "utf-8").split("\n")) {
13055
13471
  const t = line.trim();
13056
13472
  if (!t || t.startsWith("#")) continue;
13057
13473
  const eq = t.indexOf("=");
@@ -13064,7 +13480,7 @@ function updateConfigValue(key, value) {
13064
13480
  console.error("No config found. Run `synkro install` first.");
13065
13481
  process.exit(1);
13066
13482
  }
13067
- const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
13483
+ const lines = readFileSync15(CONFIG_PATH5, "utf-8").split("\n");
13068
13484
  const pattern = new RegExp(`^${key}=`);
13069
13485
  let found = false;
13070
13486
  const updated = lines.map((line) => {
@@ -13180,25 +13596,25 @@ To change:`);
13180
13596
  updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
13181
13597
  console.log(`\u2713 Inference set to '${inferenceValue}'.`);
13182
13598
  }
13183
- var SYNKRO_DIR6, CONFIG_PATH5;
13599
+ var SYNKRO_DIR7, CONFIG_PATH5;
13184
13600
  var init_config = __esm({
13185
13601
  "cli/commands/config.ts"() {
13186
13602
  "use strict";
13187
13603
  init_stub();
13188
- SYNKRO_DIR6 = join16(homedir16(), ".synkro");
13189
- CONFIG_PATH5 = join16(SYNKRO_DIR6, "config.env");
13604
+ SYNKRO_DIR7 = join17(homedir17(), ".synkro");
13605
+ CONFIG_PATH5 = join17(SYNKRO_DIR7, "config.env");
13190
13606
  }
13191
13607
  });
13192
13608
 
13193
13609
  // cli/bootstrap.js
13194
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
13610
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
13195
13611
  import { resolve as resolve3 } from "path";
13196
13612
  var envCandidates = [
13197
13613
  resolve3(process.env.HOME ?? "", ".synkro", "config.env")
13198
13614
  ];
13199
13615
  for (const envPath of envCandidates) {
13200
13616
  if (!existsSync18(envPath)) continue;
13201
- const envContent = readFileSync15(envPath, "utf-8");
13617
+ const envContent = readFileSync16(envPath, "utf-8");
13202
13618
  for (const line of envContent.split("\n")) {
13203
13619
  const trimmed = line.trim();
13204
13620
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -13213,7 +13629,7 @@ var args = process.argv.slice(2);
13213
13629
  var cmd = args[0] || "";
13214
13630
  var subArgs = args.slice(1);
13215
13631
  function printVersion() {
13216
- console.log("1.6.58");
13632
+ console.log("1.6.59");
13217
13633
  }
13218
13634
  function printHelp2() {
13219
13635
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -13327,7 +13743,7 @@ async function main() {
13327
13743
  }
13328
13744
  case "claude-desktop": {
13329
13745
  const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
13330
- await runClaudeDesktopTap2();
13746
+ await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
13331
13747
  break;
13332
13748
  }
13333
13749
  default: {