@synkro-sh/cli 1.6.55 → 1.6.56

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
@@ -193,6 +193,19 @@ function installCCHooks(settingsPath, config) {
193
193
  ],
194
194
  [SYNKRO_MARKER]: true
195
195
  });
196
+ if (config.mcpGateScriptPath) {
197
+ settings.hooks.PreToolUse.push({
198
+ matcher: "mcp__.*",
199
+ hooks: [
200
+ {
201
+ type: "command",
202
+ command: config.mcpGateScriptPath,
203
+ timeout: 30
204
+ }
205
+ ],
206
+ [SYNKRO_MARKER]: true
207
+ });
208
+ }
196
209
  if (config.taskActivateIntentScriptPath) {
197
210
  settings.hooks.PreToolUse.push({
198
211
  matcher: "mcp__synkro-guardrails__activate_standard",
@@ -820,7 +833,7 @@ synkro_post_with_retry() {
820
833
  function stubHook(surface, optsLiteral) {
821
834
  return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
822
835
  }
823
- var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
836
+ var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_MCP_GATE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
824
837
  var init_hookScriptsTs = __esm({
825
838
  "cli/installer/hookScriptsTs.ts"() {
826
839
  "use strict";
@@ -6432,6 +6445,7 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
6432
6445
  STUB_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true }");
6433
6446
  STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
6434
6447
  STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
6448
+ STUB_MCP_GATE_TS = stubHook("mcp-gate", "{ needsTranscript: true }");
6435
6449
  STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
6436
6450
  STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true }");
6437
6451
  STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
@@ -7364,9 +7378,9 @@ __export(claudeDesktopTap_exports, {
7364
7378
  runClaudeDesktopTap: () => runClaudeDesktopTap
7365
7379
  });
7366
7380
  import { spawn } from "child_process";
7367
- import { writeFileSync as writeFileSync6, mkdtempSync, readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
7381
+ import { writeFileSync as writeFileSync6, mkdtempSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
7368
7382
  import { join as join5 } from "path";
7369
- import { tmpdir, homedir as homedir5 } from "os";
7383
+ import { homedir as homedir5 } from "os";
7370
7384
  function claudeDesktopInstalled() {
7371
7385
  return process.platform === "darwin" && existsSync7("/Applications/Claude.app/Contents/MacOS/Claude");
7372
7386
  }
@@ -7451,7 +7465,7 @@ fi
7451
7465
  echo "Starting proxy on :$PROXY_PORT..."
7452
7466
  # stdin from /dev/null so mitmdump can't grab the TTY in raw mode \u2014 otherwise it
7453
7467
  # captures keypresses (incl. Ctrl+C) instead of letting them become a signal.
7454
- mitmdump --set confdir="$MITM_CA_DIR" --listen-port $PROXY_PORT --mode regular -s "$TAP_SCRIPT" </dev/null &>/tmp/synkro-mitm.log &
7468
+ mitmdump --set confdir="$MITM_CA_DIR" --listen-port $PROXY_PORT --mode regular -s "$TAP_SCRIPT" </dev/null &>"$(dirname "$TAP_SCRIPT")/mitm.log" &
7455
7469
  MITM_PID=$!
7456
7470
  sleep 2
7457
7471
 
@@ -7489,7 +7503,9 @@ async function runClaudeDesktopTap() {
7489
7503
  console.log(" Could not read local auth token (~/.synkro/.mcp-jwt). Run `synkro install` first.");
7490
7504
  return;
7491
7505
  }
7492
- const sessionDir = mkdtempSync(join5(tmpdir(), "synkro-cd-"));
7506
+ const cdRoot = join5(homedir5(), ".synkro", "cd-sessions");
7507
+ mkdirSync6(cdRoot, { recursive: true });
7508
+ const sessionDir = mkdtempSync(join5(cdRoot, "synkro-cd-"));
7493
7509
  writeFileSync6(join5(sessionDir, "tap.py"), ADDON_PY, "utf-8");
7494
7510
  writeFileSync6(join5(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
7495
7511
  writeFileSync6(join5(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
@@ -7498,7 +7514,7 @@ async function runClaudeDesktopTap() {
7498
7514
  await new Promise((resolve4) => {
7499
7515
  const child = spawn("bash", [runnerPath], {
7500
7516
  stdio: "inherit",
7501
- env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_TAP_TOKEN: token }
7517
+ env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH }
7502
7518
  });
7503
7519
  const forward = (sig) => {
7504
7520
  try {
@@ -7515,17 +7531,51 @@ async function runClaudeDesktopTap() {
7515
7531
  });
7516
7532
  });
7517
7533
  }
7518
- var CAPTURE_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
7534
+ var CAPTURE_URL, SCAN_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
7519
7535
  var init_claudeDesktopTap = __esm({
7520
7536
  "cli/installer/claudeDesktopTap.ts"() {
7521
7537
  "use strict";
7522
7538
  CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
7539
+ SCAN_URL = "http://127.0.0.1:18931/api/local/scan-upload";
7523
7540
  JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
7524
- ADDON_PY = `import os, json, threading, urllib.request, re
7541
+ ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64
7525
7542
  from mitmproxy import http
7526
7543
 
7527
7544
  CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
7528
7545
  TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
7546
+ SCAN_URL = os.environ.get("SYNKRO_SCAN_URL", "")
7547
+ # Token file path \u2014 the SERVER_TOKEN rotates whenever the container is recreated,
7548
+ # so we re-read it from disk on a 401 instead of needing a tap restart.
7549
+ TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "")
7550
+ _token_lock = threading.Lock()
7551
+
7552
+ def _reload_token():
7553
+ global TOKEN
7554
+ if not TOKEN_FILE: return
7555
+ try:
7556
+ with open(TOKEN_FILE) as f:
7557
+ t = f.read().strip()
7558
+ if t:
7559
+ with _token_lock: TOKEN = t
7560
+ print("[synkro] auth token reloaded after 401 (container likely recreated)")
7561
+ except Exception:
7562
+ pass
7563
+
7564
+ def _post_json(url, payload_bytes, timeout):
7565
+ # POST JSON with auto-reauth: on a 401 (token rotated by a container recreate),
7566
+ # re-read the token file and retry ONCE. CWE-362: TOKEN is always snapshotted
7567
+ # under the lock before use, never read bare.
7568
+ for attempt in range(2):
7569
+ with _token_lock: tok = TOKEN
7570
+ try:
7571
+ req = urllib.request.Request(url, data=payload_bytes, headers={"Content-Type": "application/json", "Authorization": "Bearer " + tok})
7572
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
7573
+ return resp.read()
7574
+ except urllib.error.HTTPError as e:
7575
+ if e.code == 401 and attempt == 0:
7576
+ _reload_token(); continue
7577
+ raise
7578
+ return None
7529
7579
 
7530
7580
  # Governance signals parsed from the completion SSE (usage window + web-search
7531
7581
  # sources), keyed by conversation id. The completion POST always precedes the
@@ -7538,7 +7588,90 @@ _sse_lock = threading.Lock()
7538
7588
  _mcp_registry = {}
7539
7589
  _mcp_lock = threading.Lock()
7540
7590
 
7591
+ def _fmt_tool_name(raw):
7592
+ # Claude Desktop names connector tool_use blocks as "<ServerName><tool>" with
7593
+ # no delimiter (e.g. "Linearlist_issues"). Split it back to "Server:tool" using
7594
+ # the captured MCP registry so the dashboard can resolve the server favicon +
7595
+ # render a clean label. Exact server+tool match only \u2014 never a fuzzy guess.
7596
+ if not raw or ":" in raw: return raw
7597
+ try:
7598
+ with _mcp_lock:
7599
+ servers = list(_mcp_registry.values())
7600
+ for s in servers:
7601
+ sname = s.get("name", "")
7602
+ if not sname or not raw.startswith(sname): continue
7603
+ rest = raw[len(sname):]
7604
+ for t in s.get("tools", []):
7605
+ tn = t.get("name", "")
7606
+ if tn and rest == tn:
7607
+ return sname + ":" + tn
7608
+ except Exception:
7609
+ pass
7610
+ return raw
7611
+
7541
7612
  class SynkroTap:
7613
+ def request(self, flow):
7614
+ # \u2500\u2500 Upload DLP gate \u2500\u2500 Scan file uploads BEFORE they reach claude.ai and
7615
+ # refuse a blocked one (the file never enters the model). SYNCHRONOUS: this
7616
+ # upload waits for the verdict. Fail-OPEN \u2014 a scan error never breaks an
7617
+ # upload. CWE-200: never print the filename/content (size/rule only).
7618
+ r = flow.request
7619
+ if not SCAN_URL or not TOKEN: return
7620
+ if "claude.ai" not in (r.pretty_host or ""): return
7621
+ if r.method != "POST": return
7622
+ ct = r.headers.get("content-type", "")
7623
+ path = r.path or ""
7624
+ is_upload = ("multipart/form-data" in ct) or bool(re.search(r"/(files|upload|convert)(/|$|\\?)", path))
7625
+ if not is_upload: return
7626
+ # CWE-400: bound everything. Oversized uploads scan on name/type only \u2014 we
7627
+ # never pull a multi-MB body into memory or forward it to the scanner.
7628
+ MAX_RAW = 8 * 1024 * 1024
7629
+ try: clen = int(r.headers.get("content-length", "0") or "0")
7630
+ except Exception: clen = 0
7631
+ if clen and clen > MAX_RAW:
7632
+ payload = {"file_name": "", "size": clen, "content_b64": ""}
7633
+ else:
7634
+ try: body = r.get_content() or b""
7635
+ except Exception: return
7636
+ if not body or len(body) > MAX_RAW: return
7637
+ fname = ""
7638
+ fm = re.search(rb'filename="([^"]*)"', body)
7639
+ if fm:
7640
+ try: fname = fm.group(1).decode("utf-8", "replace")[:300]
7641
+ except Exception: fname = ""
7642
+ # Extract the uploaded file's RAW bytes from the multipart body so the
7643
+ # grader Reads the real file (a PDF/image must be intact, not the
7644
+ # multipart envelope). Fall back to the whole body if no part is found.
7645
+ file_bytes = None
7646
+ bm = re.search(r"boundary=([^;]+)", ct)
7647
+ if bm and fm:
7648
+ boundary = ("--" + bm.group(1).strip().strip('"')).encode()
7649
+ hdr_end = body.find(b"\\r\\n\\r\\n", fm.start())
7650
+ if hdr_end != -1:
7651
+ s = hdr_end + 4
7652
+ e = body.find(b"\\r\\n" + boundary, s)
7653
+ if e != -1: file_bytes = body[s:e]
7654
+ if file_bytes is None: file_bytes = body
7655
+ try: b64 = base64.b64encode(file_bytes).decode("ascii")
7656
+ except Exception: return
7657
+ payload = {"file_name": fname, "size": len(file_bytes), "content_b64": b64}
7658
+ try:
7659
+ respb = _post_json(SCAN_URL, json.dumps(payload).encode(), 55)
7660
+ verdict = json.loads((respb or b"{}").decode("utf-8", "replace"))
7661
+ except Exception as e:
7662
+ print("[synkro] upload scan error (fail-open): " + str(e))
7663
+ return
7664
+ if verdict.get("block"):
7665
+ reason = str(verdict.get("reason", "Blocked by Synkro DLP policy"))[:300]
7666
+ flow.response = http.Response.make(
7667
+ 403,
7668
+ json.dumps({"error": {"type": "blocked_by_synkro_dlp", "message": "Blocked by Synkro DLP: " + reason}}).encode(),
7669
+ {"Content-Type": "application/json"},
7670
+ )
7671
+ print("[synkro] BLOCKED upload (size=" + str(payload.get("size", 0)) + " rule=" + str(verdict.get("ruleId", "")) + ")")
7672
+ else:
7673
+ print("[synkro] upload scanned -> allow (size=" + str(payload.get("size", 0)) + ")")
7674
+
7542
7675
  def responseheaders(self, flow):
7543
7676
  # Buffer the small conversation-tree GET (parsed for the dashboard).
7544
7677
  # For the completion POST, TAP the SSE chunk-by-chunk and pass each
@@ -7668,7 +7801,7 @@ class SynkroTap:
7668
7801
  elif bt == "thinking":
7669
7802
  thinking += b.get("thinking", "") or b.get("text", "")
7670
7803
  elif bt == "tool_use" or bt == "server_tool_use":
7671
- tool_calls.append({"name": b.get("name", ""), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
7804
+ tool_calls.append({"name": _fmt_tool_name(b.get("name", "")), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
7672
7805
  elif bt.endswith("_tool_result") or bt == "tool_result":
7673
7806
  rc = b.get("content", "")
7674
7807
  if isinstance(rc, list):
@@ -7690,7 +7823,7 @@ class SynkroTap:
7690
7823
  # web_search results are server-side (content:[]); we surface the
7691
7824
  # sources via sse_meta instead. Only record a non-empty result.
7692
7825
  if rc and str(rc).strip():
7693
- tool_results.append({"name": b.get("name", ""), "content": str(rc)})
7826
+ tool_results.append({"name": _fmt_tool_name(b.get("name", "")), "content": str(rc)})
7694
7827
  elif isinstance(blocks, str):
7695
7828
  text_parts.append(blocks)
7696
7829
  text = " ".join(p for p in text_parts if p).strip()
@@ -7723,8 +7856,7 @@ class SynkroTap:
7723
7856
  print("[synkro] captured chat (msgs=" + str(len(messages)) + " tools=" + str(ntools) + " sources=" + str(nsrc) + " model=" + model + ")")
7724
7857
  def send(_d=data):
7725
7858
  try:
7726
- req = urllib.request.Request(CAPTURE_URL, data=json.dumps(_d).encode(), headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN})
7727
- urllib.request.urlopen(req, timeout=5)
7859
+ _post_json(CAPTURE_URL, json.dumps(_d).encode(), 5)
7728
7860
  except Exception as e:
7729
7861
  print("[synkro] capture error: " + str(e))
7730
7862
  threading.Thread(target=send, daemon=True).start()
@@ -7733,11 +7865,38 @@ addons = [SynkroTap()]
7733
7865
  `;
7734
7866
  MCP_PROXY_PY = `#!/usr/bin/env python3
7735
7867
  """Synkro MCP Proxy (local) \u2014 stdio passthrough that taps MCP tool calls."""
7736
- import sys, os, json, subprocess, threading, urllib.request, time
7868
+ import sys, os, json, subprocess, threading, urllib.request, urllib.error, time
7737
7869
 
7738
7870
  CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
7739
7871
  TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
7740
7872
  SERVER_NAME = os.environ.get("SYNKRO_MCP_SERVER", "unknown")
7873
+ # The SERVER_TOKEN rotates on container recreate; re-read it from disk on a 401.
7874
+ TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "") or os.path.expanduser("~/.synkro/.mcp-jwt")
7875
+ _token_lock = threading.Lock()
7876
+
7877
+ def _reload_token():
7878
+ global TOKEN
7879
+ try:
7880
+ with open(TOKEN_FILE) as f:
7881
+ t = f.read().strip()
7882
+ if t:
7883
+ with _token_lock: TOKEN = t
7884
+ except Exception:
7885
+ pass
7886
+
7887
+ def _post_json(url, payload_bytes, timeout):
7888
+ # CWE-362: TOKEN snapshotted under the lock. Auto-reauth retry once on 401.
7889
+ for attempt in range(2):
7890
+ with _token_lock: tok = TOKEN
7891
+ try:
7892
+ req = urllib.request.Request(url, data=payload_bytes, headers={"Content-Type": "application/json", "Authorization": "Bearer " + tok})
7893
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
7894
+ return resp.read()
7895
+ except urllib.error.HTTPError as e:
7896
+ if e.code == 401 and attempt == 0:
7897
+ _reload_token(); continue
7898
+ raise
7899
+ return None
7741
7900
 
7742
7901
  pending = {}
7743
7902
  pending_lock = threading.Lock()
@@ -7755,8 +7914,7 @@ def send_capture(tool_name, tool_args, tool_result, latency_ms):
7755
7914
  },
7756
7915
  "mcp": {"server": SERVER_NAME, "tool": tool_name, "latency_ms": latency_ms}
7757
7916
  }).encode()
7758
- req = urllib.request.Request(CAPTURE_URL, data=payload, headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN})
7759
- urllib.request.urlopen(req, timeout=5)
7917
+ _post_json(CAPTURE_URL, payload, 5)
7760
7918
  except Exception as e:
7761
7919
  print("[synkro-mcp] capture error: " + str(e), file=sys.stderr)
7762
7920
 
@@ -7883,7 +8041,7 @@ __export(macKeychain_exports, {
7883
8041
  writeCursorApiKey: () => writeCursorApiKey,
7884
8042
  writeRefreshAgent: () => writeRefreshAgent
7885
8043
  });
7886
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
8044
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
7887
8045
  import { homedir as homedir6, platform as platform2 } from "os";
7888
8046
  import { join as join6 } from "path";
7889
8047
  import { spawnSync } from "child_process";
@@ -7903,7 +8061,7 @@ function readKeychainCreds() {
7903
8061
  function exportKeychainCreds() {
7904
8062
  const blob = readKeychainCreds();
7905
8063
  if (!blob) return null;
7906
- mkdirSync6(CLAUDE_CREDS_DIR, { recursive: true });
8064
+ mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
7907
8065
  chmodSync(CLAUDE_CREDS_DIR, 448);
7908
8066
  writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
7909
8067
  chmodSync(CLAUDE_CREDS_FILE, 384);
@@ -7919,7 +8077,7 @@ function cursorApiKeyConfigured() {
7919
8077
  function writeCursorApiKey(key) {
7920
8078
  const trimmed = key.trim();
7921
8079
  if (!trimmed) return;
7922
- mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
8080
+ mkdirSync7(CURSOR_CREDS_DIR, { recursive: true });
7923
8081
  chmodSync(CURSOR_CREDS_DIR, 448);
7924
8082
  writeFileSync7(CURSOR_API_KEY_FILE, trimmed, "utf-8");
7925
8083
  chmodSync(CURSOR_API_KEY_FILE, 384);
@@ -7958,7 +8116,7 @@ function writeRefreshAgent(synkroBinPath) {
7958
8116
  if (platform2() !== "darwin") {
7959
8117
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
7960
8118
  }
7961
- mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
8119
+ mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
7962
8120
  const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
7963
8121
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
7964
8122
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -8074,7 +8232,7 @@ __export(dockerInstall_exports, {
8074
8232
  waitForContainerReady: () => waitForContainerReady,
8075
8233
  waitForWorkersReady: () => waitForWorkersReady
8076
8234
  });
8077
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
8235
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
8078
8236
  import { homedir as homedir7 } from "os";
8079
8237
  import { join as join7 } from "path";
8080
8238
  import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
@@ -8301,9 +8459,9 @@ async function dockerInstall(opts = {}) {
8301
8459
  const claudeWorkers = opts.claudeWorkers ?? opts.workersPerPool ?? 8;
8302
8460
  const cursorWorkers = opts.cursorWorkers ?? 0;
8303
8461
  const totalWorkers = claudeWorkers + cursorWorkers;
8304
- mkdirSync7(PGDATA_PATH, { recursive: true });
8305
- mkdirSync7(BACKUP_DIR, { recursive: true });
8306
- mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
8462
+ mkdirSync8(PGDATA_PATH, { recursive: true });
8463
+ mkdirSync8(BACKUP_DIR, { recursive: true });
8464
+ mkdirSync8(CLAUDE_HOST_STATE_DIR, { recursive: true });
8307
8465
  const hostClaudeJson = join7(homedir7(), ".claude.json");
8308
8466
  if (existsSync9(hostClaudeJson)) {
8309
8467
  copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
@@ -8313,7 +8471,7 @@ async function dockerInstall(opts = {}) {
8313
8471
  `MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
8314
8472
  );
8315
8473
  }
8316
- mkdirSync7(CURSOR_CREDS_DIR, { recursive: true });
8474
+ mkdirSync8(CURSOR_CREDS_DIR, { recursive: true });
8317
8475
  if (needsKeychainBridge()) {
8318
8476
  if (claudeWorkers > 0) {
8319
8477
  const path = exportKeychainCreds();
@@ -8336,7 +8494,7 @@ async function dockerInstall(opts = {}) {
8336
8494
  console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
8337
8495
  }
8338
8496
  } else {
8339
- mkdirSync7(join7(homedir7(), ".claude"), { recursive: true });
8497
+ mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
8340
8498
  }
8341
8499
  const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
8342
8500
  const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
@@ -8578,7 +8736,7 @@ async function dockerSafeStart() {
8578
8736
  }
8579
8737
  } else {
8580
8738
  console.log(" pgdata: no existing data \u2014 fresh start.");
8581
- mkdirSync7(PGDATA_PATH, { recursive: true });
8739
+ mkdirSync8(PGDATA_PATH, { recursive: true });
8582
8740
  }
8583
8741
  console.log(" Starting container...");
8584
8742
  const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
@@ -9022,7 +9180,7 @@ __export(install_exports, {
9022
9180
  syncSkillFiles: () => syncSkillFiles,
9023
9181
  writeHookScripts: () => writeHookScripts
9024
9182
  });
9025
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
9183
+ 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";
9026
9184
  import { homedir as homedir9 } from "os";
9027
9185
  import { join as join9 } from "path";
9028
9186
  import { execSync as execSync6 } from "child_process";
@@ -9180,11 +9338,11 @@ coding patterns and the dashboard shows full history. (Y/n) `
9180
9338
  }
9181
9339
  }
9182
9340
  function ensureSynkroDir() {
9183
- mkdirSync8(SYNKRO_DIR4, { recursive: true });
9184
- mkdirSync8(HOOKS_DIR, { recursive: true });
9185
- mkdirSync8(BIN_DIR, { recursive: true });
9186
- mkdirSync8(OFFSETS_DIR, { recursive: true });
9187
- mkdirSync8(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
9341
+ mkdirSync9(SYNKRO_DIR4, { recursive: true });
9342
+ mkdirSync9(HOOKS_DIR, { recursive: true });
9343
+ mkdirSync9(BIN_DIR, { recursive: true });
9344
+ mkdirSync9(OFFSETS_DIR, { recursive: true });
9345
+ mkdirSync9(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
9188
9346
  }
9189
9347
  function writeHookScripts(mode = "stub") {
9190
9348
  const installExtractCorePath = join9(HOOKS_DIR, "installExtractCore.ts");
@@ -9207,6 +9365,7 @@ function writeHookScripts(mode = "stub") {
9207
9365
  const cursorAgentCapturePath = join9(HOOKS_DIR, "cursor-agent-capture.ts");
9208
9366
  const mcpStdioProxyPath = join9(HOOKS_DIR, "mcp-stdio-proxy.ts");
9209
9367
  const taskActivateIntentScriptPath = join9(HOOKS_DIR, "cc-task-activate-intent.ts");
9368
+ const mcpGateScriptPath = join9(HOOKS_DIR, "cc-mcp-gate.ts");
9210
9369
  if (mode === "stub") {
9211
9370
  const stubCommonPath = join9(HOOKS_DIR, "_synkro-stub-common.ts");
9212
9371
  const stubFiles = [
@@ -9224,6 +9383,7 @@ function writeHookScripts(mode = "stub") {
9224
9383
  [userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
9225
9384
  [installScanScriptPath, STUB_INSTALL_SCAN_TS],
9226
9385
  [taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS],
9386
+ [mcpGateScriptPath, STUB_MCP_GATE_TS],
9227
9387
  [cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
9228
9388
  [cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
9229
9389
  [cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
@@ -9256,7 +9416,8 @@ function writeHookScripts(mode = "stub") {
9256
9416
  cursorBashJudgeScript: cursorBashJudgePath,
9257
9417
  cursorEditCaptureScript: cursorEditCapturePath,
9258
9418
  cursorAgentCaptureScript: cursorAgentCapturePath,
9259
- taskActivateIntentScript: taskActivateIntentScriptPath
9419
+ taskActivateIntentScript: taskActivateIntentScriptPath,
9420
+ mcpGateScript: mcpGateScriptPath
9260
9421
  };
9261
9422
  }
9262
9423
  writeFileSync8(bashScriptPath, BASH_JUDGE_TS, "utf-8");
@@ -9279,6 +9440,7 @@ function writeHookScripts(mode = "stub") {
9279
9440
  writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
9280
9441
  writeFileSync8(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
9281
9442
  writeFileSync8(taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS, "utf-8");
9443
+ writeFileSync8(mcpGateScriptPath, STUB_MCP_GATE_TS, "utf-8");
9282
9444
  chmodSync2(bashScriptPath, 493);
9283
9445
  chmodSync2(bashFollowupScriptPath, 493);
9284
9446
  chmodSync2(editPrecheckScriptPath, 493);
@@ -9299,6 +9461,7 @@ function writeHookScripts(mode = "stub") {
9299
9461
  chmodSync2(mcpStdioProxyPath, 493);
9300
9462
  chmodSync2(installExtractCorePath, 493);
9301
9463
  chmodSync2(taskActivateIntentScriptPath, 493);
9464
+ chmodSync2(mcpGateScriptPath, 493);
9302
9465
  try {
9303
9466
  unlinkSync4(join9(HOOKS_DIR, "_synkro-stub-common.ts"));
9304
9467
  } catch {
@@ -9319,7 +9482,8 @@ function writeHookScripts(mode = "stub") {
9319
9482
  cursorBashJudgeScript: cursorBashJudgePath,
9320
9483
  cursorEditCaptureScript: cursorEditCapturePath,
9321
9484
  cursorAgentCaptureScript: cursorAgentCapturePath,
9322
- taskActivateIntentScript: taskActivateIntentScriptPath
9485
+ taskActivateIntentScript: taskActivateIntentScriptPath,
9486
+ mcpGateScript: mcpGateScriptPath
9323
9487
  };
9324
9488
  }
9325
9489
  function sanitizeConfigValue(raw, maxLen = 256) {
@@ -9351,7 +9515,7 @@ function writeConfigEnv(opts) {
9351
9515
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
9352
9516
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
9353
9517
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
9354
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.55")}`
9518
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.56")}`
9355
9519
  ];
9356
9520
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
9357
9521
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -9630,6 +9794,7 @@ async function installCommand(opts = {}) {
9630
9794
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
9631
9795
  installScanScriptPath: scripts.installScanScript,
9632
9796
  taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
9797
+ mcpGateScriptPath: scripts.mcpGateScript,
9633
9798
  skipTranscriptSync: !transcriptConsent
9634
9799
  });
9635
9800
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
@@ -10101,7 +10266,8 @@ function reconcileHarness() {
10101
10266
  transcriptSyncScriptPath: scripts.transcriptSyncScript,
10102
10267
  userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
10103
10268
  installScanScriptPath: scripts.installScanScript,
10104
- taskActivateIntentScriptPath: scripts.taskActivateIntentScript
10269
+ taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
10270
+ mcpGateScriptPath: scripts.mcpGateScript
10105
10271
  });
10106
10272
  console.log(" \u2713 Claude Code hooks registered");
10107
10273
  try {
@@ -10645,14 +10811,14 @@ rl.on('line', async (line) => {
10645
10811
  });
10646
10812
 
10647
10813
  // cli/local-cc/install.ts
10648
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync10, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
10814
+ 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";
10649
10815
  import { join as join10 } from "path";
10650
10816
  import { homedir as homedir10 } from "os";
10651
10817
  import { spawnSync as spawnSync4 } from "child_process";
10652
10818
  function writePluginFiles() {
10653
10819
  for (const c of CHANNELS) {
10654
- mkdirSync9(c.sessionDir, { recursive: true });
10655
- mkdirSync9(c.pluginSettingsDir, { recursive: true });
10820
+ mkdirSync10(c.sessionDir, { recursive: true });
10821
+ mkdirSync10(c.pluginSettingsDir, { recursive: true });
10656
10822
  writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
10657
10823
  writeFileSync9(
10658
10824
  c.pluginSettingsPath,
@@ -11254,7 +11420,7 @@ var init_disconnect = __esm({
11254
11420
  });
11255
11421
 
11256
11422
  // cli/local-cc/turnLog.ts
11257
- import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync as statSync3, watchFile, unwatchFile } from "fs";
11423
+ 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";
11258
11424
  import { dirname as dirname6, join as join12 } from "path";
11259
11425
  import { homedir as homedir12 } from "os";
11260
11426
  function truncate(s, max = PREVIEW_MAX) {
@@ -11276,7 +11442,7 @@ function extractSeverity(result) {
11276
11442
  }
11277
11443
  function appendTurn(args2) {
11278
11444
  try {
11279
- mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
11445
+ mkdirSync11(dirname6(TURN_LOG_PATH), { recursive: true });
11280
11446
  const entry = {
11281
11447
  ts: new Date(args2.startedAt).toISOString(),
11282
11448
  role: args2.role,
@@ -11312,7 +11478,7 @@ function readRecentTurns(n = 20) {
11312
11478
  }
11313
11479
  function followTurns(onEntry) {
11314
11480
  try {
11315
- mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
11481
+ mkdirSync11(dirname6(TURN_LOG_PATH), { recursive: true });
11316
11482
  if (!existsSync14(TURN_LOG_PATH)) {
11317
11483
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
11318
11484
  }
@@ -12605,7 +12771,7 @@ var args = process.argv.slice(2);
12605
12771
  var cmd = args[0] || "";
12606
12772
  var subArgs = args.slice(1);
12607
12773
  function printVersion() {
12608
- console.log("1.6.55");
12774
+ console.log("1.6.56");
12609
12775
  }
12610
12776
  function printHelp2() {
12611
12777
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents