@synkro-sh/cli 1.6.54 → 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 +210 -52
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
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.
|
|
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}`);
|
|
@@ -9953,15 +10118,7 @@ async function installCommand(opts = {}) {
|
|
|
9953
10118
|
}
|
|
9954
10119
|
console.log("\u2713 Synkro installed.");
|
|
9955
10120
|
if (process.stdin.isTTY && claudeDesktopInstalled()) {
|
|
9956
|
-
|
|
9957
|
-
const answer = await new Promise((resolve4) => rl.question("\nWould you like Claude Desktop observability? This routes Claude Desktop through a local proxy so its conversations appear in your dashboard (you can stop it anytime with Ctrl+C). [y/N]: ", resolve4));
|
|
9958
|
-
rl.close();
|
|
9959
|
-
if (/^y(es)?$/i.test(answer.trim())) {
|
|
9960
|
-
console.log("\nStarting Claude Desktop observability \u2014 press Ctrl+C to stop.\n");
|
|
9961
|
-
await runClaudeDesktopTap();
|
|
9962
|
-
} else {
|
|
9963
|
-
console.log(" Skipped. You can enable it later by re-running `synkro install`.");
|
|
9964
|
-
}
|
|
10121
|
+
console.log("\n Claude Desktop detected \u2014 to monitor its conversations in your dashboard, run: synkro claude-desktop");
|
|
9965
10122
|
}
|
|
9966
10123
|
}
|
|
9967
10124
|
function parseTomlValue2(raw) {
|
|
@@ -10109,7 +10266,8 @@ function reconcileHarness() {
|
|
|
10109
10266
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
10110
10267
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
10111
10268
|
installScanScriptPath: scripts.installScanScript,
|
|
10112
|
-
taskActivateIntentScriptPath: scripts.taskActivateIntentScript
|
|
10269
|
+
taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
|
|
10270
|
+
mcpGateScriptPath: scripts.mcpGateScript
|
|
10113
10271
|
});
|
|
10114
10272
|
console.log(" \u2713 Claude Code hooks registered");
|
|
10115
10273
|
try {
|
|
@@ -10653,14 +10811,14 @@ rl.on('line', async (line) => {
|
|
|
10653
10811
|
});
|
|
10654
10812
|
|
|
10655
10813
|
// cli/local-cc/install.ts
|
|
10656
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
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";
|
|
10657
10815
|
import { join as join10 } from "path";
|
|
10658
10816
|
import { homedir as homedir10 } from "os";
|
|
10659
10817
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
10660
10818
|
function writePluginFiles() {
|
|
10661
10819
|
for (const c of CHANNELS) {
|
|
10662
|
-
|
|
10663
|
-
|
|
10820
|
+
mkdirSync10(c.sessionDir, { recursive: true });
|
|
10821
|
+
mkdirSync10(c.pluginSettingsDir, { recursive: true });
|
|
10664
10822
|
writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
10665
10823
|
writeFileSync9(
|
|
10666
10824
|
c.pluginSettingsPath,
|
|
@@ -11262,7 +11420,7 @@ var init_disconnect = __esm({
|
|
|
11262
11420
|
});
|
|
11263
11421
|
|
|
11264
11422
|
// cli/local-cc/turnLog.ts
|
|
11265
|
-
import { appendFileSync, existsSync as existsSync14, mkdirSync as
|
|
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";
|
|
11266
11424
|
import { dirname as dirname6, join as join12 } from "path";
|
|
11267
11425
|
import { homedir as homedir12 } from "os";
|
|
11268
11426
|
function truncate(s, max = PREVIEW_MAX) {
|
|
@@ -11284,7 +11442,7 @@ function extractSeverity(result) {
|
|
|
11284
11442
|
}
|
|
11285
11443
|
function appendTurn(args2) {
|
|
11286
11444
|
try {
|
|
11287
|
-
|
|
11445
|
+
mkdirSync11(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
11288
11446
|
const entry = {
|
|
11289
11447
|
ts: new Date(args2.startedAt).toISOString(),
|
|
11290
11448
|
role: args2.role,
|
|
@@ -11320,7 +11478,7 @@ function readRecentTurns(n = 20) {
|
|
|
11320
11478
|
}
|
|
11321
11479
|
function followTurns(onEntry) {
|
|
11322
11480
|
try {
|
|
11323
|
-
|
|
11481
|
+
mkdirSync11(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
11324
11482
|
if (!existsSync14(TURN_LOG_PATH)) {
|
|
11325
11483
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
11326
11484
|
}
|
|
@@ -12613,7 +12771,7 @@ var args = process.argv.slice(2);
|
|
|
12613
12771
|
var cmd = args[0] || "";
|
|
12614
12772
|
var subArgs = args.slice(1);
|
|
12615
12773
|
function printVersion() {
|
|
12616
|
-
console.log("1.6.
|
|
12774
|
+
console.log("1.6.56");
|
|
12617
12775
|
}
|
|
12618
12776
|
function printHelp2() {
|
|
12619
12777
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|