@synkro-sh/cli 1.6.53 → 1.6.54
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 +785 -247
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1670,6 +1670,13 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
|
1670
1670
|
'jq','yq','sort','uniq','cut','tr','xxd','hexdump','od','column',
|
|
1671
1671
|
'node','npm','pnpm','yarn','bun','python','python3','ruby','go','rustc','cargo',
|
|
1672
1672
|
'git',
|
|
1673
|
+
// Read-only system / inspection utilities (no mutation, no exec). Deliberately
|
|
1674
|
+
// EXCLUDES curl/wget (exfil + -o write), sed/awk (w/e write+exec), env (command
|
|
1675
|
+
// runner), tree (-o write) \u2014 those stay graded. Keep in sync with scanning/safeRead.ts.
|
|
1676
|
+
'lsof','ps','df','du','uname','hostname','uptime',
|
|
1677
|
+
'basename','dirname','realpath','readlink',
|
|
1678
|
+
'shasum','md5','md5sum','sha256sum','cksum',
|
|
1679
|
+
'docker',
|
|
1673
1680
|
]);
|
|
1674
1681
|
const tokens = seg.trim().split(' ').filter(t => t.length > 0);
|
|
1675
1682
|
const verb = tokens[0] || '';
|
|
@@ -1699,6 +1706,15 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
|
1699
1706
|
} else if (['node','python','python3','ruby','rustc'].includes(verb)) {
|
|
1700
1707
|
const sub = tokens[1] || '';
|
|
1701
1708
|
if (sub !== '--version' && sub !== '-v' && sub !== '-V') return false;
|
|
1709
|
+
} else if (verb === 'docker') {
|
|
1710
|
+
// Read-only docker subcommands only \u2014 same gating pattern as git. Excludes
|
|
1711
|
+
// run/exec/rm/rmi/kill/stop/start/build/buildx/push/pull/cp/commit/save/load/
|
|
1712
|
+
// compose and every mutating verb; those fall through to the judge.
|
|
1713
|
+
const SAFE_DOCKER = new Set([
|
|
1714
|
+
'ps','logs','images','inspect','version','info','top','port','history','diff',
|
|
1715
|
+
]);
|
|
1716
|
+
const sub = tokens[1] || '';
|
|
1717
|
+
if (!SAFE_DOCKER.has(sub)) return false;
|
|
1702
1718
|
}
|
|
1703
1719
|
if (!repoRoot) return false;
|
|
1704
1720
|
for (let i = 1; i < tokens.length; i++) {
|
|
@@ -7341,6 +7357,510 @@ var init_promptFetcher = __esm({
|
|
|
7341
7357
|
}
|
|
7342
7358
|
});
|
|
7343
7359
|
|
|
7360
|
+
// cli/installer/claudeDesktopTap.ts
|
|
7361
|
+
var claudeDesktopTap_exports = {};
|
|
7362
|
+
__export(claudeDesktopTap_exports, {
|
|
7363
|
+
claudeDesktopInstalled: () => claudeDesktopInstalled,
|
|
7364
|
+
runClaudeDesktopTap: () => runClaudeDesktopTap
|
|
7365
|
+
});
|
|
7366
|
+
import { spawn } from "child_process";
|
|
7367
|
+
import { writeFileSync as writeFileSync6, mkdtempSync, readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
|
|
7368
|
+
import { join as join5 } from "path";
|
|
7369
|
+
import { tmpdir, homedir as homedir5 } from "os";
|
|
7370
|
+
function claudeDesktopInstalled() {
|
|
7371
|
+
return process.platform === "darwin" && existsSync7("/Applications/Claude.app/Contents/MacOS/Claude");
|
|
7372
|
+
}
|
|
7373
|
+
function buildRunner(sessionDir) {
|
|
7374
|
+
return `#!/bin/bash
|
|
7375
|
+
set -e
|
|
7376
|
+
SESSION_DIR=${JSON.stringify(sessionDir)}
|
|
7377
|
+
PROXY_PORT=8080
|
|
7378
|
+
MITM_CA_DIR="$SESSION_DIR/ca"
|
|
7379
|
+
TAP_SCRIPT="$SESSION_DIR/tap.py"
|
|
7380
|
+
CA_FINGERPRINT=""
|
|
7381
|
+
MITM_PID=""
|
|
7382
|
+
SUDO_KEEPALIVE_PID=""
|
|
7383
|
+
MCP_PROXY="$SESSION_DIR/mcp_proxy.py"
|
|
7384
|
+
MCP_PATCH="$SESSION_DIR/mcp_patch.py"
|
|
7385
|
+
CLAUDE_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
7386
|
+
CLAUDE_CONFIG_BACKUP="$SESSION_DIR/claude_desktop_config.backup.json"
|
|
7387
|
+
MCP_PATCHED=false
|
|
7388
|
+
|
|
7389
|
+
cleanup() {
|
|
7390
|
+
echo ""
|
|
7391
|
+
echo "Stopping Claude Desktop observability..."
|
|
7392
|
+
[ -n "$MITM_PID" ] && kill "$MITM_PID" 2>/dev/null
|
|
7393
|
+
[ -n "$SUDO_KEEPALIVE_PID" ] && kill "$SUDO_KEEPALIVE_PID" 2>/dev/null
|
|
7394
|
+
# Restore the original MCP config (unwrap the proxy) on exit.
|
|
7395
|
+
if [ "$MCP_PATCHED" = "true" ] && [ -f "$CLAUDE_CONFIG_BACKUP" ]; then
|
|
7396
|
+
cp "$CLAUDE_CONFIG_BACKUP" "$CLAUDE_CONFIG" && echo " MCP config restored"
|
|
7397
|
+
fi
|
|
7398
|
+
if [ -n "$CA_FINGERPRINT" ]; then
|
|
7399
|
+
# Non-interactive (-n) so teardown can NEVER hang on a sudo password prompt
|
|
7400
|
+
# (which would swallow the user's Ctrl+C). The keepalive below keeps creds
|
|
7401
|
+
# warm so this normally succeeds without a prompt.
|
|
7402
|
+
if sudo -n security delete-certificate -Z "$CA_FINGERPRINT" -t /Library/Keychains/System.keychain 2>/dev/null; then
|
|
7403
|
+
echo " Session CA removed from keychain"
|
|
7404
|
+
else
|
|
7405
|
+
echo " Could not auto-remove the session CA (sudo timed out)."
|
|
7406
|
+
echo " Remove it manually: sudo security delete-certificate -c mitmproxy /Library/Keychains/System.keychain"
|
|
7407
|
+
fi
|
|
7408
|
+
fi
|
|
7409
|
+
echo "Done."
|
|
7410
|
+
exit 0
|
|
7411
|
+
}
|
|
7412
|
+
trap cleanup INT TERM EXIT
|
|
7413
|
+
|
|
7414
|
+
if ! command -v mitmdump &>/dev/null; then
|
|
7415
|
+
echo "Installing mitmproxy..."
|
|
7416
|
+
if command -v brew &>/dev/null; then brew install mitmproxy; else echo "mitmproxy required: brew install mitmproxy"; exit 1; fi
|
|
7417
|
+
fi
|
|
7418
|
+
|
|
7419
|
+
mkdir -p "$MITM_CA_DIR"
|
|
7420
|
+
echo "Generating session CA certificate..."
|
|
7421
|
+
mitmdump --set confdir="$MITM_CA_DIR" --listen-port 0 </dev/null &>/dev/null &
|
|
7422
|
+
TMPPID=$!; sleep 2; kill $TMPPID 2>/dev/null || true
|
|
7423
|
+
|
|
7424
|
+
echo "Trusting session CA (sudo \u2014 removed automatically on exit)..."
|
|
7425
|
+
sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" "$MITM_CA_DIR/mitmproxy-ca-cert.pem"
|
|
7426
|
+
CA_FINGERPRINT=$(openssl x509 -in "$MITM_CA_DIR/mitmproxy-ca-cert.pem" -noout -fingerprint -sha1 2>/dev/null | sed "s/.*=//;s/://g")
|
|
7427
|
+
|
|
7428
|
+
# Keep the sudo timestamp warm for the life of the session so teardown's CA
|
|
7429
|
+
# removal never blocks on a password prompt (a prompt would eat Ctrl+C). Exits
|
|
7430
|
+
# itself if creds are ever revoked. Killed in cleanup().
|
|
7431
|
+
( while true; do sudo -n true 2>/dev/null || exit 0; sleep 30; done ) &
|
|
7432
|
+
SUDO_KEEPALIVE_PID=$!
|
|
7433
|
+
|
|
7434
|
+
lsof -ti:$PROXY_PORT 2>/dev/null | xargs kill 2>/dev/null || true
|
|
7435
|
+
osascript -e 'tell app "Claude" to quit' 2>/dev/null || true
|
|
7436
|
+
sleep 1
|
|
7437
|
+
|
|
7438
|
+
# Patch Claude Desktop's MCP config to wrap each server with the Synkro stdio
|
|
7439
|
+
# proxy (restored on exit). Capture URL + token come from the inherited env.
|
|
7440
|
+
if [ -f "$CLAUDE_CONFIG" ]; then
|
|
7441
|
+
cp "$CLAUDE_CONFIG" "$CLAUDE_CONFIG_BACKUP"
|
|
7442
|
+
if python3 "$MCP_PATCH" "$CLAUDE_CONFIG" "$MCP_PROXY" "$SYNKRO_CAPTURE_URL" "$SYNKRO_TAP_TOKEN" 2>/dev/null; then
|
|
7443
|
+
MCP_PATCHED=true
|
|
7444
|
+
echo "MCP servers wrapped with Synkro proxy"
|
|
7445
|
+
else
|
|
7446
|
+
echo "No MCP servers configured \u2014 skipping MCP proxy"
|
|
7447
|
+
cp "$CLAUDE_CONFIG_BACKUP" "$CLAUDE_CONFIG" 2>/dev/null || true
|
|
7448
|
+
fi
|
|
7449
|
+
fi
|
|
7450
|
+
|
|
7451
|
+
echo "Starting proxy on :$PROXY_PORT..."
|
|
7452
|
+
# stdin from /dev/null so mitmdump can't grab the TTY in raw mode \u2014 otherwise it
|
|
7453
|
+
# 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 &
|
|
7455
|
+
MITM_PID=$!
|
|
7456
|
+
sleep 2
|
|
7457
|
+
|
|
7458
|
+
CLAUDE_BIN=""
|
|
7459
|
+
for p in "/Applications/Claude.app/Contents/MacOS/Claude" "/Volumes/Claude/Claude.app/Contents/MacOS/Claude"; do
|
|
7460
|
+
[ -f "$p" ] && CLAUDE_BIN="$p" && break
|
|
7461
|
+
done
|
|
7462
|
+
if [ -z "$CLAUDE_BIN" ]; then
|
|
7463
|
+
CLAUDE_BIN="$(mdfind 'kMDItemFSName == "Claude.app"' 2>/dev/null | head -1)/Contents/MacOS/Claude"
|
|
7464
|
+
fi
|
|
7465
|
+
if [ ! -f "$CLAUDE_BIN" ]; then echo "Claude Desktop not found."; exit 1; fi
|
|
7466
|
+
|
|
7467
|
+
echo "Launching Claude Desktop..."
|
|
7468
|
+
"$CLAUDE_BIN" --proxy-server="http://localhost:$PROXY_PORT" &>/dev/null &
|
|
7469
|
+
|
|
7470
|
+
echo ""
|
|
7471
|
+
echo " \u2713 Claude Desktop is being monitored by Synkro (local)."
|
|
7472
|
+
echo " Your conversations will appear in the dashboard Events page."
|
|
7473
|
+
echo " Press Ctrl+C to stop (this removes the session CA cleanly)."
|
|
7474
|
+
echo ""
|
|
7475
|
+
wait $MITM_PID
|
|
7476
|
+
`;
|
|
7477
|
+
}
|
|
7478
|
+
async function runClaudeDesktopTap() {
|
|
7479
|
+
if (process.platform !== "darwin") {
|
|
7480
|
+
console.log(" Claude Desktop observability is macOS-only \u2014 skipped.");
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
let token = "";
|
|
7484
|
+
try {
|
|
7485
|
+
token = readFileSync5(JWT_PATH, "utf-8").trim();
|
|
7486
|
+
} catch {
|
|
7487
|
+
}
|
|
7488
|
+
if (!token) {
|
|
7489
|
+
console.log(" Could not read local auth token (~/.synkro/.mcp-jwt). Run `synkro install` first.");
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
7492
|
+
const sessionDir = mkdtempSync(join5(tmpdir(), "synkro-cd-"));
|
|
7493
|
+
writeFileSync6(join5(sessionDir, "tap.py"), ADDON_PY, "utf-8");
|
|
7494
|
+
writeFileSync6(join5(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
|
|
7495
|
+
writeFileSync6(join5(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
|
|
7496
|
+
const runnerPath = join5(sessionDir, "run.sh");
|
|
7497
|
+
writeFileSync6(runnerPath, buildRunner(sessionDir), { mode: 493 });
|
|
7498
|
+
await new Promise((resolve4) => {
|
|
7499
|
+
const child = spawn("bash", [runnerPath], {
|
|
7500
|
+
stdio: "inherit",
|
|
7501
|
+
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_TAP_TOKEN: token }
|
|
7502
|
+
});
|
|
7503
|
+
const forward = (sig) => {
|
|
7504
|
+
try {
|
|
7505
|
+
child.kill(sig);
|
|
7506
|
+
} catch {
|
|
7507
|
+
}
|
|
7508
|
+
};
|
|
7509
|
+
process.on("SIGINT", forward);
|
|
7510
|
+
process.on("SIGTERM", forward);
|
|
7511
|
+
child.on("exit", () => {
|
|
7512
|
+
process.off("SIGINT", forward);
|
|
7513
|
+
process.off("SIGTERM", forward);
|
|
7514
|
+
resolve4();
|
|
7515
|
+
});
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
var CAPTURE_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7519
|
+
var init_claudeDesktopTap = __esm({
|
|
7520
|
+
"cli/installer/claudeDesktopTap.ts"() {
|
|
7521
|
+
"use strict";
|
|
7522
|
+
CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
|
|
7523
|
+
JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
|
|
7524
|
+
ADDON_PY = `import os, json, threading, urllib.request, re
|
|
7525
|
+
from mitmproxy import http
|
|
7526
|
+
|
|
7527
|
+
CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
|
|
7528
|
+
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7529
|
+
|
|
7530
|
+
# Governance signals parsed from the completion SSE (usage window + web-search
|
|
7531
|
+
# sources), keyed by conversation id. The completion POST always precedes the
|
|
7532
|
+
# tree GET for the same turn, so the tree capture below reads + clears this.
|
|
7533
|
+
# Guarded by a lock \u2014 tap callbacks can run concurrently across flows (CWE-362).
|
|
7534
|
+
_sse_meta = {}
|
|
7535
|
+
_sse_lock = threading.Lock()
|
|
7536
|
+
# Session-global MCP server registry (uuid -> {name,url,connected,tools}), parsed
|
|
7537
|
+
# from the mcp/v2/bootstrap SSE. All access guarded by _mcp_lock (CWE-362).
|
|
7538
|
+
_mcp_registry = {}
|
|
7539
|
+
_mcp_lock = threading.Lock()
|
|
7540
|
+
|
|
7541
|
+
class SynkroTap:
|
|
7542
|
+
def responseheaders(self, flow):
|
|
7543
|
+
# Buffer the small conversation-tree GET (parsed for the dashboard).
|
|
7544
|
+
# For the completion POST, TAP the SSE chunk-by-chunk and pass each
|
|
7545
|
+
# through unchanged \u2014 Claude Desktop keeps streaming normally while we
|
|
7546
|
+
# accumulate a bounded copy for the raw diagnostic dump. Everything else
|
|
7547
|
+
# streams untouched.
|
|
7548
|
+
r = flow.request
|
|
7549
|
+
if "claude.ai" in (r.pretty_host or "") and "chat_conversations" in r.path:
|
|
7550
|
+
if r.method == "GET" and "tree" in r.pretty_url:
|
|
7551
|
+
return
|
|
7552
|
+
if r.method == "POST" and "completion" in r.path:
|
|
7553
|
+
cm = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7554
|
+
cid = cm.group(1) if cm else ""
|
|
7555
|
+
# Incremental parse: scan complete SSE lines for the small governance
|
|
7556
|
+
# signals AS THEY STREAM and keep only the extracted results \u2014 never
|
|
7557
|
+
# buffer the body. The carry holds at most one partial line (bounded),
|
|
7558
|
+
# so there's NO cap on total stream length and no source is dropped.
|
|
7559
|
+
st = {"carry": "", "usage": None, "sources": [], "seen": set(), "citations": [], "cseen": set()}
|
|
7560
|
+
def _scan(line, _st=st):
|
|
7561
|
+
if _st["usage"] is None:
|
|
7562
|
+
um = re.search(r'"5h":\\{"status":"([^"]*)","resets_at":(\\d+),"utilization":([0-9.]+)', line)
|
|
7563
|
+
if um:
|
|
7564
|
+
_st["usage"] = {"window": "5h", "status": um.group(1), "resets_at": int(um.group(2)), "utilization": float(um.group(3))}
|
|
7565
|
+
if len(_st["sources"]) < 50:
|
|
7566
|
+
for sm in re.finditer(r'"webpage_metadata","site_domain":"([^"]*)"(?:,"favicon_url":"[^"]*")?,"site_name":"([^"]*)"', line):
|
|
7567
|
+
d = sm.group(1)
|
|
7568
|
+
if d and d not in _st["seen"] and len(_st["sources"]) < 50:
|
|
7569
|
+
_st["seen"].add(d)
|
|
7570
|
+
_st["sources"].append({"domain": d, "name": sm.group(2)})
|
|
7571
|
+
# Citations: the specific pages the model cited (title + url +
|
|
7572
|
+
# domain) \u2014 the real "tool result" for web search.
|
|
7573
|
+
if "citation_start_delta" in line and line.startswith("data: ") and len(_st["citations"]) < 50:
|
|
7574
|
+
try:
|
|
7575
|
+
cit = json.loads(line[6:]).get("delta", {}).get("citation", {})
|
|
7576
|
+
url = cit.get("url", "")
|
|
7577
|
+
if url and url not in _st["cseen"]:
|
|
7578
|
+
_st["cseen"].add(url)
|
|
7579
|
+
md = cit.get("metadata") or {}
|
|
7580
|
+
_st["citations"].append({"title": cit.get("title", ""), "url": url, "domain": md.get("site_domain", "")})
|
|
7581
|
+
except Exception:
|
|
7582
|
+
pass
|
|
7583
|
+
def _tap(data, _st=st, _cid=cid):
|
|
7584
|
+
if data:
|
|
7585
|
+
try:
|
|
7586
|
+
txt = _st["carry"] + data.decode("utf-8", "replace")
|
|
7587
|
+
except Exception:
|
|
7588
|
+
txt = _st["carry"]
|
|
7589
|
+
lines = txt.split("\\n")
|
|
7590
|
+
_st["carry"] = lines[-1][-200000:] # bound the carried partial line (CWE-400)
|
|
7591
|
+
for ln in lines[:-1]:
|
|
7592
|
+
_scan(ln)
|
|
7593
|
+
else: # end of stream \u2014 flush carry + publish
|
|
7594
|
+
_scan(_st["carry"])
|
|
7595
|
+
if (_st["usage"] or _st["sources"] or _st["citations"]) and _cid:
|
|
7596
|
+
with _sse_lock:
|
|
7597
|
+
_sse_meta[_cid] = {"usage": _st["usage"], "sources": _st["sources"], "citations": _st["citations"]}
|
|
7598
|
+
return data
|
|
7599
|
+
flow.response.stream = _tap
|
|
7600
|
+
return
|
|
7601
|
+
if "claude.ai" in (r.pretty_host or "") and "mcp" in r.path and "bootstrap" in r.path:
|
|
7602
|
+
return # buffer the MCP registry bootstrap (parsed in response())
|
|
7603
|
+
flow.response.stream = True
|
|
7604
|
+
|
|
7605
|
+
def response(self, flow):
|
|
7606
|
+
r = flow.request
|
|
7607
|
+
if "claude.ai" not in (r.pretty_host or ""): return
|
|
7608
|
+
# MCP server registry from the bootstrap SSE stream.
|
|
7609
|
+
if "mcp" in r.path and "bootstrap" in r.path and flow.response:
|
|
7610
|
+
try:
|
|
7611
|
+
servers = {}
|
|
7612
|
+
for line in (flow.response.get_text() or "").split("\\n"):
|
|
7613
|
+
if not line.startswith("data: "): continue
|
|
7614
|
+
try: dd = json.loads(line[6:])
|
|
7615
|
+
except: continue
|
|
7616
|
+
if isinstance(dd.get("servers"), list):
|
|
7617
|
+
for s in dd["servers"]:
|
|
7618
|
+
sid = s.get("uuid", "")
|
|
7619
|
+
if sid: servers[sid] = {"name": s.get("name", ""), "url": s.get("url", ""), "connected": False, "tools": []}
|
|
7620
|
+
if "uuid" in dd and "connected" in dd and dd.get("uuid") in servers:
|
|
7621
|
+
servers[dd["uuid"]]["connected"] = dd.get("connected", False)
|
|
7622
|
+
if "server_uuid" in dd and isinstance(dd.get("tools"), list) and dd.get("server_uuid") in servers:
|
|
7623
|
+
for t in dd["tools"]:
|
|
7624
|
+
servers[dd["server_uuid"]]["tools"].append({"name": t.get("name", ""), "description": (t.get("description") or "")[:200]})
|
|
7625
|
+
if servers:
|
|
7626
|
+
with _mcp_lock:
|
|
7627
|
+
_mcp_registry.clear()
|
|
7628
|
+
_mcp_registry.update(servers)
|
|
7629
|
+
print("[synkro] MCP servers: " + str(len(servers)))
|
|
7630
|
+
except Exception:
|
|
7631
|
+
pass
|
|
7632
|
+
return
|
|
7633
|
+
if not ("chat_conversations" in r.path and r.method == "GET" and "tree" in r.pretty_url and flow.response): return
|
|
7634
|
+
if not CAPTURE_URL or not TOKEN: return
|
|
7635
|
+
try: convo = json.loads(flow.response.get_text())
|
|
7636
|
+
except: return
|
|
7637
|
+
m = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7638
|
+
convo_id = m.group(1) if m else ""
|
|
7639
|
+
model = convo.get("model", "unknown")
|
|
7640
|
+
settings = convo.get("settings") or {}
|
|
7641
|
+
emt = settings.get("enabled_mcp_tools")
|
|
7642
|
+
enabled_mcp = list(emt.keys()) if isinstance(emt, dict) else []
|
|
7643
|
+
web_search_on = bool(settings.get("enabled_web_search", False))
|
|
7644
|
+
platform = convo.get("platform", "")
|
|
7645
|
+
all_msgs = convo.get("chat_messages", [])
|
|
7646
|
+
if not all_msgs: return
|
|
7647
|
+
# Current turn only: last human message and everything after it.
|
|
7648
|
+
last_human = -1
|
|
7649
|
+
for i in range(len(all_msgs) - 1, -1, -1):
|
|
7650
|
+
if all_msgs[i].get("sender") == "human": last_human = i; break
|
|
7651
|
+
chat = all_msgs[last_human:] if last_human >= 0 else all_msgs[-2:]
|
|
7652
|
+
messages = []
|
|
7653
|
+
for msg in chat:
|
|
7654
|
+
sender = msg.get("sender", "")
|
|
7655
|
+
role = "user" if sender == "human" else "assistant"
|
|
7656
|
+
blocks = msg.get("content", [])
|
|
7657
|
+
text_parts = []
|
|
7658
|
+
tool_calls = []
|
|
7659
|
+
tool_results = []
|
|
7660
|
+
thinking = ""
|
|
7661
|
+
if isinstance(blocks, list):
|
|
7662
|
+
for b in blocks:
|
|
7663
|
+
if isinstance(b, str): text_parts.append(b); continue
|
|
7664
|
+
if not isinstance(b, dict): continue
|
|
7665
|
+
bt = b.get("type", "")
|
|
7666
|
+
if bt == "text":
|
|
7667
|
+
text_parts.append(b.get("text", ""))
|
|
7668
|
+
elif bt == "thinking":
|
|
7669
|
+
thinking += b.get("thinking", "") or b.get("text", "")
|
|
7670
|
+
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", {}))})
|
|
7672
|
+
elif bt.endswith("_tool_result") or bt == "tool_result":
|
|
7673
|
+
rc = b.get("content", "")
|
|
7674
|
+
if isinstance(rc, list):
|
|
7675
|
+
rparts = []
|
|
7676
|
+
for x in rc:
|
|
7677
|
+
if not isinstance(x, dict): continue
|
|
7678
|
+
if x.get("type") == "web_search_result":
|
|
7679
|
+
sn = x.get("page_content", "") or x.get("snippet", "") or x.get("content", "") or ""
|
|
7680
|
+
if isinstance(sn, dict): sn = sn.get("text", "")
|
|
7681
|
+
rparts.append((x.get("title", "") + " (" + x.get("url", "") + ")" + ((" - " + str(sn)[:200]) if sn else "")).strip())
|
|
7682
|
+
elif x.get("text"):
|
|
7683
|
+
rparts.append(x["text"])
|
|
7684
|
+
elif isinstance(x.get("content"), str):
|
|
7685
|
+
rparts.append(x["content"])
|
|
7686
|
+
rc = " | ".join(p for p in rparts if p and p.strip())
|
|
7687
|
+
dc = b.get("display_content")
|
|
7688
|
+
if isinstance(dc, dict):
|
|
7689
|
+
rc = rc or dc.get("json_block", "") or dc.get("text", "")
|
|
7690
|
+
# web_search results are server-side (content:[]); we surface the
|
|
7691
|
+
# sources via sse_meta instead. Only record a non-empty result.
|
|
7692
|
+
if rc and str(rc).strip():
|
|
7693
|
+
tool_results.append({"name": b.get("name", ""), "content": str(rc)})
|
|
7694
|
+
elif isinstance(blocks, str):
|
|
7695
|
+
text_parts.append(blocks)
|
|
7696
|
+
text = " ".join(p for p in text_parts if p).strip()
|
|
7697
|
+
if role == "assistant":
|
|
7698
|
+
am = {"role": "assistant", "content": text}
|
|
7699
|
+
if tool_calls: am["tool_calls"] = tool_calls
|
|
7700
|
+
if thinking: am["thinking"] = thinking
|
|
7701
|
+
if text or tool_calls or thinking: messages.append(am)
|
|
7702
|
+
for tr in tool_results:
|
|
7703
|
+
messages.append({"role": "tool", "content": tr["content"], "name": tr.get("name", "")})
|
|
7704
|
+
elif role == "user" and text:
|
|
7705
|
+
messages.append({"role": "user", "content": text})
|
|
7706
|
+
if not messages: return
|
|
7707
|
+
data = {"request": {"model": model, "messages": messages}, "conversation_id": convo_id}
|
|
7708
|
+
meta = {}
|
|
7709
|
+
if platform: meta["platform"] = platform
|
|
7710
|
+
if web_search_on: meta["web_search_enabled"] = True
|
|
7711
|
+
if enabled_mcp: meta["enabled_mcp_tools"] = enabled_mcp[:20]
|
|
7712
|
+
if meta: data["conversation_metadata"] = meta
|
|
7713
|
+
with _sse_lock:
|
|
7714
|
+
sm = _sse_meta.pop(convo_id, None)
|
|
7715
|
+
if sm: data["sse_meta"] = sm
|
|
7716
|
+
with _mcp_lock:
|
|
7717
|
+
reg = list(_mcp_registry.values())
|
|
7718
|
+
if reg: data["mcp_servers"] = reg
|
|
7719
|
+
ntools = 0
|
|
7720
|
+
for mm in messages:
|
|
7721
|
+
ntools += len(mm.get("tool_calls") or [])
|
|
7722
|
+
nsrc = len((sm or {}).get("sources") or [])
|
|
7723
|
+
print("[synkro] captured chat (msgs=" + str(len(messages)) + " tools=" + str(ntools) + " sources=" + str(nsrc) + " model=" + model + ")")
|
|
7724
|
+
def send(_d=data):
|
|
7725
|
+
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)
|
|
7728
|
+
except Exception as e:
|
|
7729
|
+
print("[synkro] capture error: " + str(e))
|
|
7730
|
+
threading.Thread(target=send, daemon=True).start()
|
|
7731
|
+
|
|
7732
|
+
addons = [SynkroTap()]
|
|
7733
|
+
`;
|
|
7734
|
+
MCP_PROXY_PY = `#!/usr/bin/env python3
|
|
7735
|
+
"""Synkro MCP Proxy (local) \u2014 stdio passthrough that taps MCP tool calls."""
|
|
7736
|
+
import sys, os, json, subprocess, threading, urllib.request, time
|
|
7737
|
+
|
|
7738
|
+
CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
|
|
7739
|
+
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7740
|
+
SERVER_NAME = os.environ.get("SYNKRO_MCP_SERVER", "unknown")
|
|
7741
|
+
|
|
7742
|
+
pending = {}
|
|
7743
|
+
pending_lock = threading.Lock()
|
|
7744
|
+
|
|
7745
|
+
def send_capture(tool_name, tool_args, tool_result, latency_ms):
|
|
7746
|
+
if not CAPTURE_URL or not TOKEN: return
|
|
7747
|
+
try:
|
|
7748
|
+
payload = json.dumps({
|
|
7749
|
+
"request": {
|
|
7750
|
+
"model": "mcp:" + SERVER_NAME,
|
|
7751
|
+
"messages": [
|
|
7752
|
+
{"role": "assistant", "content": "", "tool_calls": [{"name": tool_name, "arguments": json.dumps(tool_args)}]},
|
|
7753
|
+
{"role": "tool", "content": json.dumps(tool_result), "name": tool_name},
|
|
7754
|
+
]
|
|
7755
|
+
},
|
|
7756
|
+
"mcp": {"server": SERVER_NAME, "tool": tool_name, "latency_ms": latency_ms}
|
|
7757
|
+
}).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)
|
|
7760
|
+
except Exception as e:
|
|
7761
|
+
print("[synkro-mcp] capture error: " + str(e), file=sys.stderr)
|
|
7762
|
+
|
|
7763
|
+
def tap_message(data, direction):
|
|
7764
|
+
if not isinstance(data, dict): return
|
|
7765
|
+
method = data.get("method")
|
|
7766
|
+
msg_id = data.get("id")
|
|
7767
|
+
if direction == "request" and method == "tools/call":
|
|
7768
|
+
params = data.get("params", {})
|
|
7769
|
+
tool_name = params.get("name", "unknown")
|
|
7770
|
+
tool_args = params.get("arguments", {})
|
|
7771
|
+
if msg_id is not None:
|
|
7772
|
+
with pending_lock:
|
|
7773
|
+
if len(pending) < 1000:
|
|
7774
|
+
pending[msg_id] = {"name": tool_name, "args": tool_args, "start": time.time()}
|
|
7775
|
+
print("[synkro-mcp] " + SERVER_NAME + "." + str(tool_name), file=sys.stderr)
|
|
7776
|
+
if direction == "response" and msg_id is not None:
|
|
7777
|
+
with pending_lock:
|
|
7778
|
+
call = pending.pop(msg_id, None)
|
|
7779
|
+
if call:
|
|
7780
|
+
latency_ms = int((time.time() - call["start"]) * 1000)
|
|
7781
|
+
result = data.get("result", data.get("error", {}))
|
|
7782
|
+
threading.Thread(target=send_capture, args=(call["name"], call["args"], result, latency_ms), daemon=True).start()
|
|
7783
|
+
|
|
7784
|
+
def read_jsonrpc(stream):
|
|
7785
|
+
buf = b""
|
|
7786
|
+
while True:
|
|
7787
|
+
chunk = stream.read(1)
|
|
7788
|
+
if not chunk: return None
|
|
7789
|
+
if len(buf) < 5000000: # bound the line buffer (CWE-400)
|
|
7790
|
+
buf += chunk
|
|
7791
|
+
if chunk == b"\\n":
|
|
7792
|
+
line = buf.strip()
|
|
7793
|
+
buf = b""
|
|
7794
|
+
if not line: continue
|
|
7795
|
+
try: return json.loads(line)
|
|
7796
|
+
except: continue
|
|
7797
|
+
|
|
7798
|
+
def forward_stdin(proc):
|
|
7799
|
+
while True:
|
|
7800
|
+
msg = read_jsonrpc(sys.stdin.buffer)
|
|
7801
|
+
if msg is None: break
|
|
7802
|
+
tap_message(msg, "request")
|
|
7803
|
+
line = json.dumps(msg) + "\\n"
|
|
7804
|
+
try:
|
|
7805
|
+
proc.stdin.write(line.encode())
|
|
7806
|
+
proc.stdin.flush()
|
|
7807
|
+
except: break
|
|
7808
|
+
|
|
7809
|
+
def main():
|
|
7810
|
+
args = sys.argv[1:]
|
|
7811
|
+
if "--" in args:
|
|
7812
|
+
sep = args.index("--")
|
|
7813
|
+
args = args[sep + 1:]
|
|
7814
|
+
if not args:
|
|
7815
|
+
print("Usage: mcp_proxy.py -- <command> [args...]", file=sys.stderr)
|
|
7816
|
+
sys.exit(1)
|
|
7817
|
+
# Spawn the user's own configured MCP server (from claude_desktop_config.json).
|
|
7818
|
+
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
|
|
7819
|
+
t = threading.Thread(target=forward_stdin, args=(proc,), daemon=True)
|
|
7820
|
+
t.start()
|
|
7821
|
+
while True:
|
|
7822
|
+
msg = read_jsonrpc(proc.stdout)
|
|
7823
|
+
if msg is None: break
|
|
7824
|
+
tap_message(msg, "response")
|
|
7825
|
+
line = json.dumps(msg) + "\\n"
|
|
7826
|
+
try:
|
|
7827
|
+
sys.stdout.buffer.write(line.encode())
|
|
7828
|
+
sys.stdout.buffer.flush()
|
|
7829
|
+
except: break
|
|
7830
|
+
proc.wait()
|
|
7831
|
+
sys.exit(proc.returncode)
|
|
7832
|
+
|
|
7833
|
+
if __name__ == "__main__":
|
|
7834
|
+
main()
|
|
7835
|
+
`;
|
|
7836
|
+
MCP_PATCH_PY = `import json, sys
|
|
7837
|
+
config_path, proxy_path, capture_url, token = sys.argv[1:5]
|
|
7838
|
+
with open(config_path) as f: config = json.load(f)
|
|
7839
|
+
servers = config.get("mcpServers", {})
|
|
7840
|
+
if not servers:
|
|
7841
|
+
print("No MCP servers configured")
|
|
7842
|
+
sys.exit(1)
|
|
7843
|
+
patched = 0
|
|
7844
|
+
for name, server in servers.items():
|
|
7845
|
+
if server.get("_synkro_original"): continue
|
|
7846
|
+
orig_cmd = server.get("command", "")
|
|
7847
|
+
orig_args = server.get("args", [])
|
|
7848
|
+
if not orig_cmd: continue
|
|
7849
|
+
server["_synkro_original"] = {"command": orig_cmd, "args": orig_args}
|
|
7850
|
+
server["command"] = "python3"
|
|
7851
|
+
server["args"] = [proxy_path, "--", orig_cmd] + orig_args
|
|
7852
|
+
env = server.get("env", {})
|
|
7853
|
+
env["SYNKRO_CAPTURE_URL"] = capture_url
|
|
7854
|
+
env["SYNKRO_TAP_TOKEN"] = token
|
|
7855
|
+
env["SYNKRO_MCP_SERVER"] = name
|
|
7856
|
+
server["env"] = env
|
|
7857
|
+
patched += 1
|
|
7858
|
+
with open(config_path, "w") as f: json.dump(config, f, indent=2)
|
|
7859
|
+
print("Patched " + str(patched) + " MCP servers")
|
|
7860
|
+
`;
|
|
7861
|
+
}
|
|
7862
|
+
});
|
|
7863
|
+
|
|
7344
7864
|
// cli/local-cc/macKeychain.ts
|
|
7345
7865
|
var macKeychain_exports = {};
|
|
7346
7866
|
__export(macKeychain_exports, {
|
|
@@ -7363,9 +7883,9 @@ __export(macKeychain_exports, {
|
|
|
7363
7883
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
7364
7884
|
writeRefreshAgent: () => writeRefreshAgent
|
|
7365
7885
|
});
|
|
7366
|
-
import { existsSync as
|
|
7367
|
-
import { homedir as
|
|
7368
|
-
import { join as
|
|
7886
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
|
|
7887
|
+
import { homedir as homedir6, platform as platform2 } from "os";
|
|
7888
|
+
import { join as join6 } from "path";
|
|
7369
7889
|
import { spawnSync } from "child_process";
|
|
7370
7890
|
function needsKeychainBridge() {
|
|
7371
7891
|
return platform2() === "darwin";
|
|
@@ -7385,13 +7905,13 @@ function exportKeychainCreds() {
|
|
|
7385
7905
|
if (!blob) return null;
|
|
7386
7906
|
mkdirSync6(CLAUDE_CREDS_DIR, { recursive: true });
|
|
7387
7907
|
chmodSync(CLAUDE_CREDS_DIR, 448);
|
|
7388
|
-
|
|
7908
|
+
writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
|
|
7389
7909
|
chmodSync(CLAUDE_CREDS_FILE, 384);
|
|
7390
7910
|
return CLAUDE_CREDS_FILE;
|
|
7391
7911
|
}
|
|
7392
7912
|
function cursorApiKeyConfigured() {
|
|
7393
7913
|
try {
|
|
7394
|
-
return
|
|
7914
|
+
return existsSync8(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
|
|
7395
7915
|
} catch {
|
|
7396
7916
|
return false;
|
|
7397
7917
|
}
|
|
@@ -7401,13 +7921,13 @@ function writeCursorApiKey(key) {
|
|
|
7401
7921
|
if (!trimmed) return;
|
|
7402
7922
|
mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
|
|
7403
7923
|
chmodSync(CURSOR_CREDS_DIR, 448);
|
|
7404
|
-
|
|
7924
|
+
writeFileSync7(CURSOR_API_KEY_FILE, trimmed, "utf-8");
|
|
7405
7925
|
chmodSync(CURSOR_API_KEY_FILE, 384);
|
|
7406
7926
|
}
|
|
7407
7927
|
async function validateCursorApiKey() {
|
|
7408
7928
|
let key;
|
|
7409
7929
|
try {
|
|
7410
|
-
key =
|
|
7930
|
+
key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
|
|
7411
7931
|
} catch {
|
|
7412
7932
|
return null;
|
|
7413
7933
|
}
|
|
@@ -7426,7 +7946,7 @@ async function validateCursorApiKey() {
|
|
|
7426
7946
|
}
|
|
7427
7947
|
}
|
|
7428
7948
|
function credsAreStale() {
|
|
7429
|
-
if (!
|
|
7949
|
+
if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
|
|
7430
7950
|
try {
|
|
7431
7951
|
const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
|
|
7432
7952
|
return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
|
|
@@ -7438,7 +7958,7 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
7438
7958
|
if (platform2() !== "darwin") {
|
|
7439
7959
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
7440
7960
|
}
|
|
7441
|
-
mkdirSync6(
|
|
7961
|
+
mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
7442
7962
|
const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
|
|
7443
7963
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7444
7964
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -7457,13 +7977,13 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
7457
7977
|
<key>RunAtLoad</key>
|
|
7458
7978
|
<true/>
|
|
7459
7979
|
<key>StandardErrorPath</key>
|
|
7460
|
-
<string>${
|
|
7980
|
+
<string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
|
|
7461
7981
|
<key>StandardOutPath</key>
|
|
7462
|
-
<string>${
|
|
7982
|
+
<string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
|
|
7463
7983
|
</dict>
|
|
7464
7984
|
</plist>
|
|
7465
7985
|
`;
|
|
7466
|
-
|
|
7986
|
+
writeFileSync7(LAUNCHD_PLIST, plist, "utf-8");
|
|
7467
7987
|
return LAUNCHD_PLIST;
|
|
7468
7988
|
}
|
|
7469
7989
|
function loadRefreshAgent() {
|
|
@@ -7489,7 +8009,7 @@ function uninstallRefreshAgent() {
|
|
|
7489
8009
|
timeout: 5e3
|
|
7490
8010
|
});
|
|
7491
8011
|
try {
|
|
7492
|
-
if (
|
|
8012
|
+
if (existsSync8(LAUNCHD_PLIST)) {
|
|
7493
8013
|
__require("fs").unlinkSync(LAUNCHD_PLIST);
|
|
7494
8014
|
}
|
|
7495
8015
|
} catch {
|
|
@@ -7501,7 +8021,7 @@ function refreshCreds() {
|
|
|
7501
8021
|
}
|
|
7502
8022
|
function readExportedCreds() {
|
|
7503
8023
|
try {
|
|
7504
|
-
return
|
|
8024
|
+
return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
|
|
7505
8025
|
} catch {
|
|
7506
8026
|
return null;
|
|
7507
8027
|
}
|
|
@@ -7510,14 +8030,14 @@ var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_AP
|
|
|
7510
8030
|
var init_macKeychain = __esm({
|
|
7511
8031
|
"cli/local-cc/macKeychain.ts"() {
|
|
7512
8032
|
"use strict";
|
|
7513
|
-
SYNKRO_DIR =
|
|
7514
|
-
CLAUDE_CREDS_DIR =
|
|
7515
|
-
CLAUDE_CREDS_FILE =
|
|
7516
|
-
CURSOR_CREDS_DIR =
|
|
7517
|
-
CURSOR_API_KEY_FILE =
|
|
8033
|
+
SYNKRO_DIR = join6(homedir6(), ".synkro");
|
|
8034
|
+
CLAUDE_CREDS_DIR = join6(SYNKRO_DIR, "claude-creds");
|
|
8035
|
+
CLAUDE_CREDS_FILE = join6(CLAUDE_CREDS_DIR, ".credentials.json");
|
|
8036
|
+
CURSOR_CREDS_DIR = join6(SYNKRO_DIR, "cursor-creds");
|
|
8037
|
+
CURSOR_API_KEY_FILE = join6(CURSOR_CREDS_DIR, "api-key");
|
|
7518
8038
|
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
7519
8039
|
LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
|
|
7520
|
-
LAUNCHD_PLIST =
|
|
8040
|
+
LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
7521
8041
|
REFRESH_INTERVAL_SECONDS = 45 * 60;
|
|
7522
8042
|
KeychainExportError = class extends Error {
|
|
7523
8043
|
constructor(message, cause) {
|
|
@@ -7554,9 +8074,9 @@ __export(dockerInstall_exports, {
|
|
|
7554
8074
|
waitForContainerReady: () => waitForContainerReady,
|
|
7555
8075
|
waitForWorkersReady: () => waitForWorkersReady
|
|
7556
8076
|
});
|
|
7557
|
-
import { copyFileSync, existsSync as
|
|
7558
|
-
import { homedir as
|
|
7559
|
-
import { join as
|
|
8077
|
+
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
8078
|
+
import { homedir as homedir7 } from "os";
|
|
8079
|
+
import { join as join7 } from "path";
|
|
7560
8080
|
import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
|
|
7561
8081
|
function splitWorkers(total, providers) {
|
|
7562
8082
|
const t = Math.max(0, Math.floor(total));
|
|
@@ -7651,9 +8171,9 @@ function readSynkroFileConfig() {
|
|
|
7651
8171
|
try {
|
|
7652
8172
|
const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7653
8173
|
if (!root) return { pool: "auto", warnings: [] };
|
|
7654
|
-
const fp =
|
|
7655
|
-
if (!
|
|
7656
|
-
const parsed = parseSynkroToml(
|
|
8174
|
+
const fp = join7(root, "synkro.toml");
|
|
8175
|
+
if (!existsSync9(fp)) return { pool: "auto", warnings: [] };
|
|
8176
|
+
const parsed = parseSynkroToml(readFileSync7(fp, "utf-8"));
|
|
7657
8177
|
return resolveGraderPool(parsed);
|
|
7658
8178
|
} catch {
|
|
7659
8179
|
}
|
|
@@ -7728,7 +8248,7 @@ function assertDockerAvailable() {
|
|
|
7728
8248
|
}
|
|
7729
8249
|
function claudeCredsHostDir() {
|
|
7730
8250
|
if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
|
|
7731
|
-
return
|
|
8251
|
+
return join7(homedir7(), ".claude");
|
|
7732
8252
|
}
|
|
7733
8253
|
function resolveSynkroBin() {
|
|
7734
8254
|
const which2 = spawnSync2("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -7784,11 +8304,11 @@ async function dockerInstall(opts = {}) {
|
|
|
7784
8304
|
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
7785
8305
|
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
7786
8306
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
7787
|
-
const hostClaudeJson =
|
|
7788
|
-
if (
|
|
8307
|
+
const hostClaudeJson = join7(homedir7(), ".claude.json");
|
|
8308
|
+
if (existsSync9(hostClaudeJson)) {
|
|
7789
8309
|
copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
|
|
7790
8310
|
}
|
|
7791
|
-
if (!
|
|
8311
|
+
if (!existsSync9(MCP_JWT_PATH)) {
|
|
7792
8312
|
throw new DockerInstallError(
|
|
7793
8313
|
`MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
|
|
7794
8314
|
);
|
|
@@ -7816,7 +8336,7 @@ async function dockerInstall(opts = {}) {
|
|
|
7816
8336
|
console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
|
|
7817
8337
|
}
|
|
7818
8338
|
} else {
|
|
7819
|
-
mkdirSync7(
|
|
8339
|
+
mkdirSync7(join7(homedir7(), ".claude"), { recursive: true });
|
|
7820
8340
|
}
|
|
7821
8341
|
const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
7822
8342
|
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
@@ -7869,7 +8389,7 @@ async function dockerInstall(opts = {}) {
|
|
|
7869
8389
|
"-v",
|
|
7870
8390
|
`${credsDir}:/home/synkro/.claude:rw`,
|
|
7871
8391
|
"-v",
|
|
7872
|
-
`${
|
|
8392
|
+
`${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
|
|
7873
8393
|
"-v",
|
|
7874
8394
|
`${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
|
|
7875
8395
|
// Cursor creds — mounted RW so the in-container refresher can rotate the
|
|
@@ -8049,7 +8569,7 @@ async function dockerSafeStart() {
|
|
|
8049
8569
|
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
8050
8570
|
}
|
|
8051
8571
|
const pgCheck = checkPgdata();
|
|
8052
|
-
if (
|
|
8572
|
+
if (existsSync9(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
|
|
8053
8573
|
if (pgCheck.healthy) {
|
|
8054
8574
|
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
8055
8575
|
} else {
|
|
@@ -8089,7 +8609,7 @@ async function dockerSafeRestart() {
|
|
|
8089
8609
|
return { ok: startResult.ok, stop: stopResult, start: startResult };
|
|
8090
8610
|
}
|
|
8091
8611
|
function checkPgdata() {
|
|
8092
|
-
if (!
|
|
8612
|
+
if (!existsSync9(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
8093
8613
|
const entries = readdirSync2(PGDATA_PATH);
|
|
8094
8614
|
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
8095
8615
|
const hasPidFile = entries.includes("postmaster.pid");
|
|
@@ -8106,11 +8626,11 @@ var init_dockerInstall = __esm({
|
|
|
8106
8626
|
"use strict";
|
|
8107
8627
|
init_agentDetect();
|
|
8108
8628
|
init_macKeychain();
|
|
8109
|
-
SYNKRO_DIR2 =
|
|
8110
|
-
MCP_JWT_PATH =
|
|
8111
|
-
PGDATA_PATH =
|
|
8112
|
-
CLAUDE_HOST_STATE_DIR =
|
|
8113
|
-
CLAUDE_HOST_STATE_FILE =
|
|
8629
|
+
SYNKRO_DIR2 = join7(homedir7(), ".synkro");
|
|
8630
|
+
MCP_JWT_PATH = join7(SYNKRO_DIR2, ".mcp-jwt");
|
|
8631
|
+
PGDATA_PATH = join7(SYNKRO_DIR2, "pgdata");
|
|
8632
|
+
CLAUDE_HOST_STATE_DIR = join7(SYNKRO_DIR2, "claude-host-state");
|
|
8633
|
+
CLAUDE_HOST_STATE_FILE = join7(CLAUDE_HOST_STATE_DIR, ".claude.json");
|
|
8114
8634
|
HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
|
|
8115
8635
|
HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
|
|
8116
8636
|
HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
|
|
@@ -8125,7 +8645,7 @@ var init_dockerInstall = __esm({
|
|
|
8125
8645
|
}
|
|
8126
8646
|
cause;
|
|
8127
8647
|
};
|
|
8128
|
-
BACKUP_DIR =
|
|
8648
|
+
BACKUP_DIR = join7(SYNKRO_DIR2, "pgdata-backups");
|
|
8129
8649
|
}
|
|
8130
8650
|
});
|
|
8131
8651
|
|
|
@@ -8138,14 +8658,14 @@ __export(setupGithub_exports, {
|
|
|
8138
8658
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8139
8659
|
import { stdin as input, stdout as output } from "process";
|
|
8140
8660
|
import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
|
|
8141
|
-
import { existsSync as
|
|
8142
|
-
import { homedir as
|
|
8143
|
-
import { join as
|
|
8661
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
8662
|
+
import { homedir as homedir8, platform as platform3 } from "os";
|
|
8663
|
+
import { join as join8 } from "path";
|
|
8144
8664
|
import { execFile as execFile2 } from "child_process";
|
|
8145
8665
|
function readConfig() {
|
|
8146
|
-
if (!
|
|
8666
|
+
if (!existsSync10(CONFIG_PATH)) return {};
|
|
8147
8667
|
const out = {};
|
|
8148
|
-
for (const line of
|
|
8668
|
+
for (const line of readFileSync8(CONFIG_PATH, "utf-8").split("\n")) {
|
|
8149
8669
|
const t = line.trim();
|
|
8150
8670
|
if (!t || t.startsWith("#")) continue;
|
|
8151
8671
|
const eq = t.indexOf("=");
|
|
@@ -8206,7 +8726,7 @@ function sleep(ms) {
|
|
|
8206
8726
|
return new Promise((r) => setTimeout(r, ms));
|
|
8207
8727
|
}
|
|
8208
8728
|
function captureClaudeSetupToken() {
|
|
8209
|
-
const tmpFile =
|
|
8729
|
+
const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
8210
8730
|
return new Promise((resolve4, reject) => {
|
|
8211
8731
|
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
8212
8732
|
stdio: "inherit"
|
|
@@ -8215,7 +8735,7 @@ function captureClaudeSetupToken() {
|
|
|
8215
8735
|
proc.on("close", (code) => {
|
|
8216
8736
|
let raw = "";
|
|
8217
8737
|
try {
|
|
8218
|
-
raw =
|
|
8738
|
+
raw = readFileSync8(tmpFile, "utf-8");
|
|
8219
8739
|
} catch (e) {
|
|
8220
8740
|
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
8221
8741
|
return;
|
|
@@ -8487,8 +9007,8 @@ var init_setupGithub = __esm({
|
|
|
8487
9007
|
"use strict";
|
|
8488
9008
|
init_githubSetup();
|
|
8489
9009
|
init_stub();
|
|
8490
|
-
SYNKRO_DIR3 =
|
|
8491
|
-
CONFIG_PATH =
|
|
9010
|
+
SYNKRO_DIR3 = join8(homedir8(), ".synkro");
|
|
9011
|
+
CONFIG_PATH = join8(SYNKRO_DIR3, "config.env");
|
|
8492
9012
|
}
|
|
8493
9013
|
});
|
|
8494
9014
|
|
|
@@ -8502,16 +9022,16 @@ __export(install_exports, {
|
|
|
8502
9022
|
syncSkillFiles: () => syncSkillFiles,
|
|
8503
9023
|
writeHookScripts: () => writeHookScripts
|
|
8504
9024
|
});
|
|
8505
|
-
import { existsSync as
|
|
8506
|
-
import { homedir as
|
|
8507
|
-
import { join as
|
|
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";
|
|
9026
|
+
import { homedir as homedir9 } from "os";
|
|
9027
|
+
import { join as join9 } from "path";
|
|
8508
9028
|
import { execSync as execSync6 } from "child_process";
|
|
8509
9029
|
import { createInterface as createInterface3 } from "readline";
|
|
8510
9030
|
function resolvePersistedHookMode() {
|
|
8511
9031
|
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
8512
9032
|
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
8513
9033
|
try {
|
|
8514
|
-
const env =
|
|
9034
|
+
const env = readFileSync9(CONFIG_PATH2, "utf-8");
|
|
8515
9035
|
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
8516
9036
|
} catch {
|
|
8517
9037
|
}
|
|
@@ -8664,31 +9184,31 @@ function ensureSynkroDir() {
|
|
|
8664
9184
|
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
8665
9185
|
mkdirSync8(BIN_DIR, { recursive: true });
|
|
8666
9186
|
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
8667
|
-
mkdirSync8(
|
|
9187
|
+
mkdirSync8(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
8668
9188
|
}
|
|
8669
9189
|
function writeHookScripts(mode = "stub") {
|
|
8670
|
-
const installExtractCorePath =
|
|
8671
|
-
const bashScriptPath =
|
|
8672
|
-
const bashFollowupScriptPath =
|
|
8673
|
-
const editPrecheckScriptPath =
|
|
8674
|
-
const cwePrecheckScriptPath =
|
|
8675
|
-
const cvePrecheckScriptPath =
|
|
8676
|
-
const planJudgeScriptPath =
|
|
8677
|
-
const agentJudgeScriptPath =
|
|
8678
|
-
const stopSummaryScriptPath =
|
|
8679
|
-
const sessionStartScriptPath =
|
|
8680
|
-
const transcriptSyncScriptPath =
|
|
8681
|
-
const userPromptSubmitScriptPath =
|
|
8682
|
-
const commonScriptPath =
|
|
8683
|
-
const commonBashScriptPath =
|
|
8684
|
-
const installScanScriptPath =
|
|
8685
|
-
const cursorBashJudgePath =
|
|
8686
|
-
const cursorEditCapturePath =
|
|
8687
|
-
const cursorAgentCapturePath =
|
|
8688
|
-
const mcpStdioProxyPath =
|
|
8689
|
-
const taskActivateIntentScriptPath =
|
|
9190
|
+
const installExtractCorePath = join9(HOOKS_DIR, "installExtractCore.ts");
|
|
9191
|
+
const bashScriptPath = join9(HOOKS_DIR, "cc-bash-judge.ts");
|
|
9192
|
+
const bashFollowupScriptPath = join9(HOOKS_DIR, "cc-bash-followup.ts");
|
|
9193
|
+
const editPrecheckScriptPath = join9(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
9194
|
+
const cwePrecheckScriptPath = join9(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
9195
|
+
const cvePrecheckScriptPath = join9(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
9196
|
+
const planJudgeScriptPath = join9(HOOKS_DIR, "cc-plan-judge.ts");
|
|
9197
|
+
const agentJudgeScriptPath = join9(HOOKS_DIR, "cc-agent-judge.ts");
|
|
9198
|
+
const stopSummaryScriptPath = join9(HOOKS_DIR, "cc-stop-summary.ts");
|
|
9199
|
+
const sessionStartScriptPath = join9(HOOKS_DIR, "cc-session-start.ts");
|
|
9200
|
+
const transcriptSyncScriptPath = join9(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
9201
|
+
const userPromptSubmitScriptPath = join9(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
9202
|
+
const commonScriptPath = join9(HOOKS_DIR, "_synkro-common.ts");
|
|
9203
|
+
const commonBashScriptPath = join9(HOOKS_DIR, "_synkro-common.sh");
|
|
9204
|
+
const installScanScriptPath = join9(HOOKS_DIR, "cc-install-scan.ts");
|
|
9205
|
+
const cursorBashJudgePath = join9(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
9206
|
+
const cursorEditCapturePath = join9(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
9207
|
+
const cursorAgentCapturePath = join9(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
9208
|
+
const mcpStdioProxyPath = join9(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
9209
|
+
const taskActivateIntentScriptPath = join9(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
8690
9210
|
if (mode === "stub") {
|
|
8691
|
-
const stubCommonPath =
|
|
9211
|
+
const stubCommonPath = join9(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8692
9212
|
const stubFiles = [
|
|
8693
9213
|
[stubCommonPath, STUB_COMMON_TS],
|
|
8694
9214
|
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
@@ -8709,14 +9229,14 @@ function writeHookScripts(mode = "stub") {
|
|
|
8709
9229
|
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
8710
9230
|
];
|
|
8711
9231
|
for (const [p, content] of stubFiles) {
|
|
8712
|
-
|
|
9232
|
+
writeFileSync8(p, content, "utf-8");
|
|
8713
9233
|
chmodSync2(p, 493);
|
|
8714
9234
|
}
|
|
8715
|
-
|
|
9235
|
+
writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8716
9236
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8717
9237
|
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
8718
9238
|
try {
|
|
8719
|
-
unlinkSync4(
|
|
9239
|
+
unlinkSync4(join9(HOOKS_DIR, stale));
|
|
8720
9240
|
} catch {
|
|
8721
9241
|
}
|
|
8722
9242
|
}
|
|
@@ -8739,26 +9259,26 @@ function writeHookScripts(mode = "stub") {
|
|
|
8739
9259
|
taskActivateIntentScript: taskActivateIntentScriptPath
|
|
8740
9260
|
};
|
|
8741
9261
|
}
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
writeFileSync7(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");
|
|
8761
|
-
|
|
9262
|
+
writeFileSync8(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
9263
|
+
writeFileSync8(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
9264
|
+
writeFileSync8(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
9265
|
+
writeFileSync8(cwePrecheckScriptPath, CWE_PRECHECK_TS, "utf-8");
|
|
9266
|
+
writeFileSync8(cvePrecheckScriptPath, CVE_PRECHECK_TS, "utf-8");
|
|
9267
|
+
writeFileSync8(planJudgeScriptPath, PLAN_JUDGE_TS, "utf-8");
|
|
9268
|
+
writeFileSync8(agentJudgeScriptPath, AGENT_JUDGE_TS, "utf-8");
|
|
9269
|
+
writeFileSync8(stopSummaryScriptPath, STOP_SUMMARY_TS, "utf-8");
|
|
9270
|
+
writeFileSync8(sessionStartScriptPath, SESSION_START_TS, "utf-8");
|
|
9271
|
+
writeFileSync8(transcriptSyncScriptPath, TRANSCRIPT_SYNC_TS, "utf-8");
|
|
9272
|
+
writeFileSync8(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
|
|
9273
|
+
writeFileSync8(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
9274
|
+
writeFileSync8(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
9275
|
+
writeFileSync8(installScanScriptPath, INSTALL_SCAN_TS, "utf-8");
|
|
9276
|
+
writeFileSync8(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
9277
|
+
writeFileSync8(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
9278
|
+
writeFileSync8(cursorAgentCapturePath, CURSOR_AGENT_CAPTURE_TS, "utf-8");
|
|
9279
|
+
writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
9280
|
+
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
|
+
writeFileSync8(taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS, "utf-8");
|
|
8762
9282
|
chmodSync2(bashScriptPath, 493);
|
|
8763
9283
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
8764
9284
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -8780,7 +9300,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
8780
9300
|
chmodSync2(installExtractCorePath, 493);
|
|
8781
9301
|
chmodSync2(taskActivateIntentScriptPath, 493);
|
|
8782
9302
|
try {
|
|
8783
|
-
unlinkSync4(
|
|
9303
|
+
unlinkSync4(join9(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
8784
9304
|
} catch {
|
|
8785
9305
|
}
|
|
8786
9306
|
return {
|
|
@@ -8811,11 +9331,11 @@ function shellQuoteSingle(value) {
|
|
|
8811
9331
|
}
|
|
8812
9332
|
function resolveSynkroBundle() {
|
|
8813
9333
|
const scriptPath = process.argv[1];
|
|
8814
|
-
if (scriptPath &&
|
|
9334
|
+
if (scriptPath && existsSync11(scriptPath)) return scriptPath;
|
|
8815
9335
|
return null;
|
|
8816
9336
|
}
|
|
8817
9337
|
function writeConfigEnv(opts) {
|
|
8818
|
-
const credsPath =
|
|
9338
|
+
const credsPath = join9(SYNKRO_DIR4, "credentials.json");
|
|
8819
9339
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
8820
9340
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
8821
9341
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -8831,7 +9351,7 @@ function writeConfigEnv(opts) {
|
|
|
8831
9351
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8832
9352
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8833
9353
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8834
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
9354
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.54")}`
|
|
8835
9355
|
];
|
|
8836
9356
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8837
9357
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8847,15 +9367,15 @@ function writeConfigEnv(opts) {
|
|
|
8847
9367
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
8848
9368
|
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
8849
9369
|
lines.push("");
|
|
8850
|
-
|
|
9370
|
+
writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
8851
9371
|
chmodSync2(CONFIG_PATH2, 384);
|
|
8852
9372
|
}
|
|
8853
9373
|
function resolveDeploymentMode() {
|
|
8854
9374
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
8855
9375
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
8856
9376
|
try {
|
|
8857
|
-
if (
|
|
8858
|
-
const m =
|
|
9377
|
+
if (existsSync11(CONFIG_PATH2)) {
|
|
9378
|
+
const m = readFileSync9(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
8859
9379
|
const val = m?.[1]?.toLowerCase();
|
|
8860
9380
|
if (val === "bare-host" || val === "docker") return val;
|
|
8861
9381
|
}
|
|
@@ -8882,16 +9402,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8882
9402
|
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
8883
9403
|
} catch {
|
|
8884
9404
|
}
|
|
8885
|
-
const claudeDir =
|
|
9405
|
+
const claudeDir = join9(homedir9(), ".claude");
|
|
8886
9406
|
try {
|
|
8887
|
-
const settings = JSON.parse(
|
|
9407
|
+
const settings = JSON.parse(readFileSync9(join9(claudeDir, "settings.json"), "utf-8"));
|
|
8888
9408
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
8889
9409
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
8890
9410
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
8891
9411
|
} catch {
|
|
8892
9412
|
}
|
|
8893
9413
|
try {
|
|
8894
|
-
const mcpCache = JSON.parse(
|
|
9414
|
+
const mcpCache = JSON.parse(readFileSync9(join9(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
8895
9415
|
const mcpNames = Object.keys(mcpCache);
|
|
8896
9416
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
8897
9417
|
} catch {
|
|
@@ -8903,10 +9423,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8903
9423
|
} catch {
|
|
8904
9424
|
}
|
|
8905
9425
|
try {
|
|
8906
|
-
const sessionsDir =
|
|
9426
|
+
const sessionsDir = join9(claudeDir, "sessions");
|
|
8907
9427
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
8908
9428
|
for (const f of files) {
|
|
8909
|
-
const s = JSON.parse(
|
|
9429
|
+
const s = JSON.parse(readFileSync9(join9(sessionsDir, f), "utf-8"));
|
|
8910
9430
|
if (s.version) {
|
|
8911
9431
|
meta.cc_version = meta.cc_version || s.version;
|
|
8912
9432
|
break;
|
|
@@ -9081,9 +9601,9 @@ async function installCommand(opts = {}) {
|
|
|
9081
9601
|
const scripts = writeHookScripts(hookMode);
|
|
9082
9602
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
9083
9603
|
for (const mode of ["edit", "bash"]) {
|
|
9084
|
-
const pidFile =
|
|
9604
|
+
const pidFile = join9(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
9085
9605
|
try {
|
|
9086
|
-
const pid = parseInt(
|
|
9606
|
+
const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
|
|
9087
9607
|
if (pid > 0) {
|
|
9088
9608
|
process.kill(pid, "SIGTERM");
|
|
9089
9609
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -9163,7 +9683,7 @@ async function installCommand(opts = {}) {
|
|
|
9163
9683
|
if (mintResp.ok) {
|
|
9164
9684
|
const minted = await mintResp.json();
|
|
9165
9685
|
mcpJwt = minted.token;
|
|
9166
|
-
|
|
9686
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
9167
9687
|
} else {
|
|
9168
9688
|
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
9169
9689
|
}
|
|
@@ -9191,7 +9711,7 @@ async function installCommand(opts = {}) {
|
|
|
9191
9711
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
9192
9712
|
}
|
|
9193
9713
|
const minted = await mintResp.json();
|
|
9194
|
-
|
|
9714
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
9195
9715
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
9196
9716
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
9197
9717
|
console.log(` url: ${mcp.url}`);
|
|
@@ -9208,8 +9728,8 @@ async function installCommand(opts = {}) {
|
|
|
9208
9728
|
if (hasCursor && !opts.noMcp) {
|
|
9209
9729
|
try {
|
|
9210
9730
|
if (useLocalMcp) {
|
|
9211
|
-
const jwtPath =
|
|
9212
|
-
if (!
|
|
9731
|
+
const jwtPath = join9(SYNKRO_DIR4, ".mcp-jwt");
|
|
9732
|
+
if (!existsSync11(jwtPath)) {
|
|
9213
9733
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
9214
9734
|
method: "POST",
|
|
9215
9735
|
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -9217,7 +9737,7 @@ async function installCommand(opts = {}) {
|
|
|
9217
9737
|
});
|
|
9218
9738
|
if (mintResp.ok) {
|
|
9219
9739
|
const minted = await mintResp.json();
|
|
9220
|
-
|
|
9740
|
+
writeFileSync8(jwtPath, minted.token + "\n", { mode: 384 });
|
|
9221
9741
|
}
|
|
9222
9742
|
}
|
|
9223
9743
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
|
|
@@ -9237,7 +9757,7 @@ async function installCommand(opts = {}) {
|
|
|
9237
9757
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
9238
9758
|
}
|
|
9239
9759
|
const minted = await mintResp.json();
|
|
9240
|
-
|
|
9760
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
9241
9761
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
9242
9762
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
9243
9763
|
console.log(` url: ${mcp.url}`);
|
|
@@ -9313,7 +9833,7 @@ async function installCommand(opts = {}) {
|
|
|
9313
9833
|
const ready = await waitForContainerReady(6e4);
|
|
9314
9834
|
if (ready) {
|
|
9315
9835
|
console.log(" \u2713 container ready");
|
|
9316
|
-
const mcpJwt =
|
|
9836
|
+
const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9317
9837
|
try {
|
|
9318
9838
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
9319
9839
|
method: "POST",
|
|
@@ -9351,7 +9871,7 @@ async function installCommand(opts = {}) {
|
|
|
9351
9871
|
try {
|
|
9352
9872
|
let mcpToken = "";
|
|
9353
9873
|
try {
|
|
9354
|
-
mcpToken =
|
|
9874
|
+
mcpToken = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9355
9875
|
} catch {
|
|
9356
9876
|
}
|
|
9357
9877
|
if (mcpToken) {
|
|
@@ -9432,6 +9952,17 @@ async function installCommand(opts = {}) {
|
|
|
9432
9952
|
await setupGithubCommand2({ nonInteractive: true, githubToken: ghToken });
|
|
9433
9953
|
}
|
|
9434
9954
|
console.log("\u2713 Synkro installed.");
|
|
9955
|
+
if (process.stdin.isTTY && claudeDesktopInstalled()) {
|
|
9956
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
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
|
+
}
|
|
9965
|
+
}
|
|
9435
9966
|
}
|
|
9436
9967
|
function parseTomlValue2(raw) {
|
|
9437
9968
|
const v = raw.trim();
|
|
@@ -9476,8 +10007,8 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
9476
10007
|
try {
|
|
9477
10008
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9478
10009
|
if (!root) return;
|
|
9479
|
-
if (root ===
|
|
9480
|
-
const fp =
|
|
10010
|
+
if (root === homedir9()) return;
|
|
10011
|
+
const fp = join9(root, "synkro.toml");
|
|
9481
10012
|
let hasFile = false;
|
|
9482
10013
|
try {
|
|
9483
10014
|
hasFile = statSync2(fp).isFile();
|
|
@@ -9516,7 +10047,7 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
9516
10047
|
"cve = true",
|
|
9517
10048
|
""
|
|
9518
10049
|
].join("\n");
|
|
9519
|
-
|
|
10050
|
+
writeFileSync8(fp, toml, "utf-8");
|
|
9520
10051
|
console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
|
|
9521
10052
|
} catch {
|
|
9522
10053
|
}
|
|
@@ -9525,9 +10056,9 @@ function readFullSynkroFile() {
|
|
|
9525
10056
|
try {
|
|
9526
10057
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9527
10058
|
if (!root) return null;
|
|
9528
|
-
const fp =
|
|
9529
|
-
if (!
|
|
9530
|
-
const parsed = parseSynkroToml2(
|
|
10059
|
+
const fp = join9(root, "synkro.toml");
|
|
10060
|
+
if (!existsSync11(fp)) return null;
|
|
10061
|
+
const parsed = parseSynkroToml2(readFileSync9(fp, "utf-8"));
|
|
9531
10062
|
const valid = ["claude-code", "cursor"];
|
|
9532
10063
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
9533
10064
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -9563,7 +10094,7 @@ function reconcileHarness() {
|
|
|
9563
10094
|
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
9564
10095
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9565
10096
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9566
|
-
const ccSettings =
|
|
10097
|
+
const ccSettings = join9(homedir9(), ".claude", "settings.json");
|
|
9567
10098
|
if (wantCC) {
|
|
9568
10099
|
installCCHooks(ccSettings, {
|
|
9569
10100
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -9582,7 +10113,7 @@ function reconcileHarness() {
|
|
|
9582
10113
|
});
|
|
9583
10114
|
console.log(" \u2713 Claude Code hooks registered");
|
|
9584
10115
|
try {
|
|
9585
|
-
const mcpJwt =
|
|
10116
|
+
const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9586
10117
|
if (mcpJwt) {
|
|
9587
10118
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
9588
10119
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -9593,7 +10124,7 @@ function reconcileHarness() {
|
|
|
9593
10124
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
9594
10125
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
9595
10126
|
}
|
|
9596
|
-
const cursorHooks =
|
|
10127
|
+
const cursorHooks = join9(homedir9(), ".cursor", "hooks.json");
|
|
9597
10128
|
if (wantCursor) {
|
|
9598
10129
|
installCursorHooks(cursorHooks, {
|
|
9599
10130
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -9646,7 +10177,7 @@ async function syncSkillFiles() {
|
|
|
9646
10177
|
if (resolved.length === 0) return;
|
|
9647
10178
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
9648
10179
|
const tasks = resolved.map((fp) => {
|
|
9649
|
-
const content =
|
|
10180
|
+
const content = readFileSync9(fp, "utf-8");
|
|
9650
10181
|
const source = `skill:${fp.split("/").pop()}`;
|
|
9651
10182
|
if (!content.trim()) {
|
|
9652
10183
|
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
@@ -9692,17 +10223,17 @@ function detectGitRepo2() {
|
|
|
9692
10223
|
function getClaudeProjectsFolder() {
|
|
9693
10224
|
const cwd = process.cwd();
|
|
9694
10225
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
9695
|
-
const projectsDir =
|
|
9696
|
-
return
|
|
10226
|
+
const projectsDir = join9(homedir9(), ".claude", "projects", sanitized);
|
|
10227
|
+
return existsSync11(projectsDir) ? projectsDir : null;
|
|
9697
10228
|
}
|
|
9698
10229
|
function extractSessionInsights(projectsDir) {
|
|
9699
10230
|
const insights = [];
|
|
9700
10231
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9701
10232
|
for (const file of files) {
|
|
9702
10233
|
const sessionId = file.replace(".jsonl", "");
|
|
9703
|
-
const filePath =
|
|
10234
|
+
const filePath = join9(projectsDir, file);
|
|
9704
10235
|
try {
|
|
9705
|
-
const content =
|
|
10236
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9706
10237
|
const lines = content.split("\n").filter(Boolean);
|
|
9707
10238
|
for (let i = 0; i < lines.length; i++) {
|
|
9708
10239
|
try {
|
|
@@ -9781,14 +10312,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
9781
10312
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9782
10313
|
}
|
|
9783
10314
|
function getCursorTranscriptsDir() {
|
|
9784
|
-
const dir =
|
|
9785
|
-
return
|
|
10315
|
+
const dir = join9(homedir9(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10316
|
+
return existsSync11(dir) ? dir : null;
|
|
9786
10317
|
}
|
|
9787
10318
|
function isSafeConvId(id) {
|
|
9788
10319
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
9789
10320
|
}
|
|
9790
10321
|
function parseCursorTranscriptFile(filePath) {
|
|
9791
|
-
const content =
|
|
10322
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9792
10323
|
const lines = content.split("\n").filter(Boolean);
|
|
9793
10324
|
const messages = [];
|
|
9794
10325
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9820,8 +10351,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9820
10351
|
for (let i = 0; i < convDirs.length; i++) {
|
|
9821
10352
|
const convId = convDirs[i];
|
|
9822
10353
|
if (!isSafeConvId(convId)) continue;
|
|
9823
|
-
const filePath =
|
|
9824
|
-
if (!
|
|
10354
|
+
const filePath = join9(dir, convId, `${convId}.jsonl`);
|
|
10355
|
+
if (!existsSync11(filePath)) continue;
|
|
9825
10356
|
try {
|
|
9826
10357
|
const all = parseCursorTranscriptFile(filePath);
|
|
9827
10358
|
const messages = all.length > 500 ? all.slice(-500) : all;
|
|
@@ -9843,8 +10374,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9843
10374
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
9844
10375
|
}
|
|
9845
10376
|
try {
|
|
9846
|
-
const lc =
|
|
9847
|
-
|
|
10377
|
+
const lc = readFileSync9(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
10378
|
+
writeFileSync8(join9(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
9848
10379
|
} catch {
|
|
9849
10380
|
}
|
|
9850
10381
|
}
|
|
@@ -9852,7 +10383,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9852
10383
|
return { sessions: totalSessions, messages: totalMessages };
|
|
9853
10384
|
}
|
|
9854
10385
|
function parseTranscriptFile(filePath) {
|
|
9855
|
-
const content =
|
|
10386
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9856
10387
|
const lines = content.split("\n").filter(Boolean);
|
|
9857
10388
|
const messages = [];
|
|
9858
10389
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9900,7 +10431,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9900
10431
|
for (let i = 0; i < files.length; i++) {
|
|
9901
10432
|
const file = files[i];
|
|
9902
10433
|
const sessionId = file.replace(".jsonl", "");
|
|
9903
|
-
const filePath =
|
|
10434
|
+
const filePath = join9(projectsDir, file);
|
|
9904
10435
|
try {
|
|
9905
10436
|
const allMessages = parseTranscriptFile(filePath);
|
|
9906
10437
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -9922,9 +10453,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9922
10453
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
9923
10454
|
}
|
|
9924
10455
|
try {
|
|
9925
|
-
const content =
|
|
10456
|
+
const content = readFileSync9(join9(projectsDir, file), "utf-8");
|
|
9926
10457
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9927
|
-
|
|
10458
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9928
10459
|
} catch {
|
|
9929
10460
|
}
|
|
9930
10461
|
}
|
|
@@ -9945,7 +10476,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9945
10476
|
const sessions = [];
|
|
9946
10477
|
for (const file of batch) {
|
|
9947
10478
|
const sessionId = file.replace(".jsonl", "");
|
|
9948
|
-
const filePath =
|
|
10479
|
+
const filePath = join9(projectsDir, file);
|
|
9949
10480
|
try {
|
|
9950
10481
|
const allMessages = parseTranscriptFile(filePath);
|
|
9951
10482
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -9974,11 +10505,11 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9974
10505
|
}
|
|
9975
10506
|
for (const file of batch) {
|
|
9976
10507
|
const sessionId = file.replace(".jsonl", "");
|
|
9977
|
-
const filePath =
|
|
10508
|
+
const filePath = join9(projectsDir, file);
|
|
9978
10509
|
try {
|
|
9979
|
-
const content =
|
|
10510
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9980
10511
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9981
|
-
|
|
10512
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9982
10513
|
} catch {
|
|
9983
10514
|
}
|
|
9984
10515
|
}
|
|
@@ -10001,11 +10532,12 @@ var init_install = __esm({
|
|
|
10001
10532
|
init_repoConnect();
|
|
10002
10533
|
init_projects();
|
|
10003
10534
|
init_promptFetcher();
|
|
10535
|
+
init_claudeDesktopTap();
|
|
10004
10536
|
init_dockerInstall();
|
|
10005
|
-
SYNKRO_DIR4 =
|
|
10006
|
-
HOOKS_DIR =
|
|
10007
|
-
BIN_DIR =
|
|
10008
|
-
CONFIG_PATH2 =
|
|
10537
|
+
SYNKRO_DIR4 = join9(homedir9(), ".synkro");
|
|
10538
|
+
HOOKS_DIR = join9(SYNKRO_DIR4, "hooks");
|
|
10539
|
+
BIN_DIR = join9(SYNKRO_DIR4, "bin");
|
|
10540
|
+
CONFIG_PATH2 = join9(SYNKRO_DIR4, "config.env");
|
|
10009
10541
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
10010
10542
|
import { readFileSync } from 'node:fs';
|
|
10011
10543
|
import { homedir } from 'node:os';
|
|
@@ -10116,21 +10648,21 @@ rl.on('line', async (line) => {
|
|
|
10116
10648
|
}
|
|
10117
10649
|
});
|
|
10118
10650
|
`;
|
|
10119
|
-
OFFSETS_DIR =
|
|
10651
|
+
OFFSETS_DIR = join9(SYNKRO_DIR4, ".transcript-offsets");
|
|
10120
10652
|
}
|
|
10121
10653
|
});
|
|
10122
10654
|
|
|
10123
10655
|
// cli/local-cc/install.ts
|
|
10124
|
-
import { existsSync as
|
|
10125
|
-
import { join as
|
|
10126
|
-
import { homedir as
|
|
10656
|
+
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";
|
|
10657
|
+
import { join as join10 } from "path";
|
|
10658
|
+
import { homedir as homedir10 } from "os";
|
|
10127
10659
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
10128
10660
|
function writePluginFiles() {
|
|
10129
10661
|
for (const c of CHANNELS) {
|
|
10130
10662
|
mkdirSync9(c.sessionDir, { recursive: true });
|
|
10131
10663
|
mkdirSync9(c.pluginSettingsDir, { recursive: true });
|
|
10132
|
-
|
|
10133
|
-
|
|
10664
|
+
writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
10665
|
+
writeFileSync9(
|
|
10134
10666
|
c.pluginSettingsPath,
|
|
10135
10667
|
JSON.stringify({
|
|
10136
10668
|
fastMode: true,
|
|
@@ -10138,7 +10670,7 @@ function writePluginFiles() {
|
|
|
10138
10670
|
}, null, 2) + "\n",
|
|
10139
10671
|
"utf-8"
|
|
10140
10672
|
);
|
|
10141
|
-
|
|
10673
|
+
writeFileSync9(c.runScriptPath, c.runScriptSource, "utf-8");
|
|
10142
10674
|
chmodSync3(c.runScriptPath, 493);
|
|
10143
10675
|
}
|
|
10144
10676
|
}
|
|
@@ -10157,10 +10689,10 @@ function runBunInstall() {
|
|
|
10157
10689
|
}
|
|
10158
10690
|
}
|
|
10159
10691
|
function safelyMutateClaudeJson(mutator) {
|
|
10160
|
-
if (!
|
|
10692
|
+
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
10161
10693
|
return;
|
|
10162
10694
|
}
|
|
10163
|
-
const originalText =
|
|
10695
|
+
const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
|
|
10164
10696
|
let parsed;
|
|
10165
10697
|
try {
|
|
10166
10698
|
parsed = JSON.parse(originalText);
|
|
@@ -10192,7 +10724,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
10192
10724
|
copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
|
|
10193
10725
|
const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
|
|
10194
10726
|
try {
|
|
10195
|
-
|
|
10727
|
+
writeFileSync9(tmpPath, newText, "utf-8");
|
|
10196
10728
|
const fd = openSync(tmpPath, "r");
|
|
10197
10729
|
try {
|
|
10198
10730
|
fsyncSync(fd);
|
|
@@ -10225,7 +10757,7 @@ function writeProjectMcpJson() {
|
|
|
10225
10757
|
}
|
|
10226
10758
|
}
|
|
10227
10759
|
};
|
|
10228
|
-
|
|
10760
|
+
writeFileSync9(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
10229
10761
|
}
|
|
10230
10762
|
}
|
|
10231
10763
|
function patchClaudeJson() {
|
|
@@ -10302,42 +10834,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
10302
10834
|
var init_install2 = __esm({
|
|
10303
10835
|
"cli/local-cc/install.ts"() {
|
|
10304
10836
|
"use strict";
|
|
10305
|
-
CLAUDE_JSON_BACKUP_PATH =
|
|
10306
|
-
SESSION_DIR =
|
|
10307
|
-
PLUGIN_PATH =
|
|
10308
|
-
PLUGIN_PKG_PATH =
|
|
10309
|
-
PLUGIN_SETTINGS_DIR =
|
|
10310
|
-
PLUGIN_SETTINGS_PATH =
|
|
10311
|
-
PROJECT_MCP_PATH =
|
|
10312
|
-
CLAUDE_JSON_PATH =
|
|
10313
|
-
RUN_SCRIPT_PATH =
|
|
10837
|
+
CLAUDE_JSON_BACKUP_PATH = join10(homedir10(), ".claude.json.synkro-bak");
|
|
10838
|
+
SESSION_DIR = join10(homedir10(), ".synkro", "cc_sessions");
|
|
10839
|
+
PLUGIN_PATH = join10(SESSION_DIR, "synkro-channel.ts");
|
|
10840
|
+
PLUGIN_PKG_PATH = join10(SESSION_DIR, "package.json");
|
|
10841
|
+
PLUGIN_SETTINGS_DIR = join10(SESSION_DIR, ".claude");
|
|
10842
|
+
PLUGIN_SETTINGS_PATH = join10(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
10843
|
+
PROJECT_MCP_PATH = join10(SESSION_DIR, ".mcp.json");
|
|
10844
|
+
CLAUDE_JSON_PATH = join10(homedir10(), ".claude.json");
|
|
10845
|
+
RUN_SCRIPT_PATH = join10(SESSION_DIR, "run-claude.sh");
|
|
10314
10846
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
10315
10847
|
CHANNEL_1_PORT = 8941;
|
|
10316
|
-
SESSION_DIR_2 =
|
|
10317
|
-
PLUGIN_PATH_2 =
|
|
10318
|
-
PLUGIN_PKG_PATH_2 =
|
|
10319
|
-
PLUGIN_SETTINGS_DIR_2 =
|
|
10320
|
-
PLUGIN_SETTINGS_PATH_2 =
|
|
10321
|
-
PROJECT_MCP_PATH_2 =
|
|
10322
|
-
RUN_SCRIPT_PATH_2 =
|
|
10848
|
+
SESSION_DIR_2 = join10(homedir10(), ".synkro", "cc_sessions_2");
|
|
10849
|
+
PLUGIN_PATH_2 = join10(SESSION_DIR_2, "synkro-channel.ts");
|
|
10850
|
+
PLUGIN_PKG_PATH_2 = join10(SESSION_DIR_2, "package.json");
|
|
10851
|
+
PLUGIN_SETTINGS_DIR_2 = join10(SESSION_DIR_2, ".claude");
|
|
10852
|
+
PLUGIN_SETTINGS_PATH_2 = join10(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
10853
|
+
PROJECT_MCP_PATH_2 = join10(SESSION_DIR_2, ".mcp.json");
|
|
10854
|
+
RUN_SCRIPT_PATH_2 = join10(SESSION_DIR_2, "run-claude.sh");
|
|
10323
10855
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
10324
10856
|
CHANNEL_2_PORT = 8951;
|
|
10325
|
-
SESSION_DIR_3 =
|
|
10326
|
-
PLUGIN_PATH_3 =
|
|
10327
|
-
PLUGIN_PKG_PATH_3 =
|
|
10328
|
-
PLUGIN_SETTINGS_DIR_3 =
|
|
10329
|
-
PLUGIN_SETTINGS_PATH_3 =
|
|
10330
|
-
PROJECT_MCP_PATH_3 =
|
|
10331
|
-
RUN_SCRIPT_PATH_3 =
|
|
10857
|
+
SESSION_DIR_3 = join10(homedir10(), ".synkro", "cc_sessions_3");
|
|
10858
|
+
PLUGIN_PATH_3 = join10(SESSION_DIR_3, "synkro-channel.ts");
|
|
10859
|
+
PLUGIN_PKG_PATH_3 = join10(SESSION_DIR_3, "package.json");
|
|
10860
|
+
PLUGIN_SETTINGS_DIR_3 = join10(SESSION_DIR_3, ".claude");
|
|
10861
|
+
PLUGIN_SETTINGS_PATH_3 = join10(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
10862
|
+
PROJECT_MCP_PATH_3 = join10(SESSION_DIR_3, ".mcp.json");
|
|
10863
|
+
RUN_SCRIPT_PATH_3 = join10(SESSION_DIR_3, "run-claude.sh");
|
|
10332
10864
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
10333
10865
|
CHANNEL_3_PORT = 8942;
|
|
10334
|
-
SESSION_DIR_4 =
|
|
10335
|
-
PLUGIN_PATH_4 =
|
|
10336
|
-
PLUGIN_PKG_PATH_4 =
|
|
10337
|
-
PLUGIN_SETTINGS_DIR_4 =
|
|
10338
|
-
PLUGIN_SETTINGS_PATH_4 =
|
|
10339
|
-
PROJECT_MCP_PATH_4 =
|
|
10340
|
-
RUN_SCRIPT_PATH_4 =
|
|
10866
|
+
SESSION_DIR_4 = join10(homedir10(), ".synkro", "cc_sessions_4");
|
|
10867
|
+
PLUGIN_PATH_4 = join10(SESSION_DIR_4, "synkro-channel.ts");
|
|
10868
|
+
PLUGIN_PKG_PATH_4 = join10(SESSION_DIR_4, "package.json");
|
|
10869
|
+
PLUGIN_SETTINGS_DIR_4 = join10(SESSION_DIR_4, ".claude");
|
|
10870
|
+
PLUGIN_SETTINGS_PATH_4 = join10(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
10871
|
+
PROJECT_MCP_PATH_4 = join10(SESSION_DIR_4, ".mcp.json");
|
|
10872
|
+
RUN_SCRIPT_PATH_4 = join10(SESSION_DIR_4, "run-claude.sh");
|
|
10341
10873
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
10342
10874
|
CHANNEL_4_PORT = 8952;
|
|
10343
10875
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -10611,9 +11143,9 @@ var disconnect_exports = {};
|
|
|
10611
11143
|
__export(disconnect_exports, {
|
|
10612
11144
|
disconnectCommand: () => disconnectCommand
|
|
10613
11145
|
});
|
|
10614
|
-
import { existsSync as
|
|
10615
|
-
import { homedir as
|
|
10616
|
-
import { join as
|
|
11146
|
+
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11147
|
+
import { homedir as homedir11 } from "os";
|
|
11148
|
+
import { join as join11 } from "path";
|
|
10617
11149
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
10618
11150
|
import { createInterface as createInterface4 } from "readline";
|
|
10619
11151
|
async function tearDownLocalCC() {
|
|
@@ -10687,15 +11219,15 @@ async function disconnectCommand(args2 = []) {
|
|
|
10687
11219
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
10688
11220
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
10689
11221
|
}
|
|
10690
|
-
if (
|
|
11222
|
+
if (existsSync13(SYNKRO_DIR5)) {
|
|
10691
11223
|
if (purge) {
|
|
10692
11224
|
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
10693
11225
|
console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
|
|
10694
11226
|
} else {
|
|
10695
|
-
const keep = /* @__PURE__ */ new Set([
|
|
11227
|
+
const keep = /* @__PURE__ */ new Set([join11(SYNKRO_DIR5, "pgdata"), join11(SYNKRO_DIR5, "pgdata-backups")]);
|
|
10696
11228
|
const preserved = [];
|
|
10697
11229
|
for (const entry of readdirSync4(SYNKRO_DIR5)) {
|
|
10698
|
-
const full =
|
|
11230
|
+
const full = join11(SYNKRO_DIR5, entry);
|
|
10699
11231
|
if (keep.has(full)) {
|
|
10700
11232
|
preserved.push(entry);
|
|
10701
11233
|
continue;
|
|
@@ -10725,14 +11257,14 @@ var init_disconnect = __esm({
|
|
|
10725
11257
|
init_install2();
|
|
10726
11258
|
init_dockerInstall();
|
|
10727
11259
|
init_macKeychain();
|
|
10728
|
-
SYNKRO_DIR5 =
|
|
11260
|
+
SYNKRO_DIR5 = join11(homedir11(), ".synkro");
|
|
10729
11261
|
}
|
|
10730
11262
|
});
|
|
10731
11263
|
|
|
10732
11264
|
// cli/local-cc/turnLog.ts
|
|
10733
|
-
import { appendFileSync, existsSync as
|
|
10734
|
-
import { dirname as dirname6, join as
|
|
10735
|
-
import { homedir as
|
|
11265
|
+
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";
|
|
11266
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
11267
|
+
import { homedir as homedir12 } from "os";
|
|
10736
11268
|
function truncate(s, max = PREVIEW_MAX) {
|
|
10737
11269
|
if (s.length <= max) return s;
|
|
10738
11270
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -10768,11 +11300,11 @@ function appendTurn(args2) {
|
|
|
10768
11300
|
}
|
|
10769
11301
|
}
|
|
10770
11302
|
function readRecentTurns(n = 20) {
|
|
10771
|
-
if (!
|
|
11303
|
+
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
10772
11304
|
try {
|
|
10773
11305
|
const size = statSync3(TURN_LOG_PATH).size;
|
|
10774
11306
|
if (size === 0) return [];
|
|
10775
|
-
const text =
|
|
11307
|
+
const text = readFileSync11(TURN_LOG_PATH, "utf-8");
|
|
10776
11308
|
const lines = text.split("\n").filter(Boolean);
|
|
10777
11309
|
const lastN = lines.slice(-n).reverse();
|
|
10778
11310
|
return lastN.map((line) => {
|
|
@@ -10789,7 +11321,7 @@ function readRecentTurns(n = 20) {
|
|
|
10789
11321
|
function followTurns(onEntry) {
|
|
10790
11322
|
try {
|
|
10791
11323
|
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
10792
|
-
if (!
|
|
11324
|
+
if (!existsSync14(TURN_LOG_PATH)) {
|
|
10793
11325
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
10794
11326
|
}
|
|
10795
11327
|
} catch {
|
|
@@ -10851,7 +11383,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
10851
11383
|
var init_turnLog = __esm({
|
|
10852
11384
|
"cli/local-cc/turnLog.ts"() {
|
|
10853
11385
|
"use strict";
|
|
10854
|
-
TURN_LOG_PATH =
|
|
11386
|
+
TURN_LOG_PATH = join12(homedir12(), ".synkro", "cc_sessions", "turns.log");
|
|
10855
11387
|
PREVIEW_MAX = 400;
|
|
10856
11388
|
}
|
|
10857
11389
|
});
|
|
@@ -11000,9 +11532,9 @@ var init_grade = __esm({
|
|
|
11000
11532
|
});
|
|
11001
11533
|
|
|
11002
11534
|
// cli/local-cc/pueue.ts
|
|
11003
|
-
import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn } from "child_process";
|
|
11004
|
-
import { homedir as
|
|
11005
|
-
import { join as
|
|
11535
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn as spawn2 } from "child_process";
|
|
11536
|
+
import { homedir as homedir13 } from "os";
|
|
11537
|
+
import { join as join13 } from "path";
|
|
11006
11538
|
import { connect as connect2 } from "net";
|
|
11007
11539
|
function pueueAvailable() {
|
|
11008
11540
|
const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
@@ -11068,7 +11600,7 @@ function startTask(opts = {}) {
|
|
|
11068
11600
|
spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
11069
11601
|
existing = findTask(ch);
|
|
11070
11602
|
}
|
|
11071
|
-
const runScript =
|
|
11603
|
+
const runScript = join13(cwd, "run-claude.sh");
|
|
11072
11604
|
const args2 = [
|
|
11073
11605
|
"add",
|
|
11074
11606
|
"--label",
|
|
@@ -11165,7 +11697,7 @@ function assertPueueInstalled() {
|
|
|
11165
11697
|
const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
11166
11698
|
if (status.status !== 0) {
|
|
11167
11699
|
console.log(" Starting pueued daemon...");
|
|
11168
|
-
const child =
|
|
11700
|
+
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
11169
11701
|
child.unref();
|
|
11170
11702
|
spawnSync6("sleep", ["1"]);
|
|
11171
11703
|
const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -11198,12 +11730,12 @@ var init_pueue = __esm({
|
|
|
11198
11730
|
"use strict";
|
|
11199
11731
|
TASK_LABEL = "synkro-local-cc";
|
|
11200
11732
|
TMUX_SESSION = "synkro-local-cc";
|
|
11201
|
-
SESSION_DIR2 =
|
|
11733
|
+
SESSION_DIR2 = join13(homedir13(), ".synkro", "cc_sessions");
|
|
11202
11734
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
11203
11735
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
11204
|
-
SESSION_DIR_22 =
|
|
11205
|
-
SESSION_DIR_32 =
|
|
11206
|
-
SESSION_DIR_42 =
|
|
11736
|
+
SESSION_DIR_22 = join13(homedir13(), ".synkro", "cc_sessions_2");
|
|
11737
|
+
SESSION_DIR_32 = join13(homedir13(), ".synkro", "cc_sessions_3");
|
|
11738
|
+
SESSION_DIR_42 = join13(homedir13(), ".synkro", "cc_sessions_4");
|
|
11207
11739
|
PueueError = class extends Error {
|
|
11208
11740
|
constructor(message, cause) {
|
|
11209
11741
|
super(message);
|
|
@@ -11218,13 +11750,13 @@ var init_pueue = __esm({
|
|
|
11218
11750
|
});
|
|
11219
11751
|
|
|
11220
11752
|
// cli/local-cc/settings.ts
|
|
11221
|
-
import { existsSync as
|
|
11222
|
-
import { homedir as
|
|
11223
|
-
import { join as
|
|
11753
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
|
|
11754
|
+
import { homedir as homedir14 } from "os";
|
|
11755
|
+
import { join as join14 } from "path";
|
|
11224
11756
|
function isLocalCCEnabled() {
|
|
11225
|
-
if (!
|
|
11757
|
+
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
11226
11758
|
try {
|
|
11227
|
-
const content =
|
|
11759
|
+
const content = readFileSync12(CONFIG_PATH3, "utf-8");
|
|
11228
11760
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
11229
11761
|
return match?.[1] === "yes";
|
|
11230
11762
|
} catch {
|
|
@@ -11235,7 +11767,7 @@ var CONFIG_PATH3;
|
|
|
11235
11767
|
var init_settings = __esm({
|
|
11236
11768
|
"cli/local-cc/settings.ts"() {
|
|
11237
11769
|
"use strict";
|
|
11238
|
-
CONFIG_PATH3 =
|
|
11770
|
+
CONFIG_PATH3 = join14(homedir14(), ".synkro", "config.env");
|
|
11239
11771
|
}
|
|
11240
11772
|
});
|
|
11241
11773
|
|
|
@@ -11245,10 +11777,10 @@ __export(localCc_exports, {
|
|
|
11245
11777
|
localCcCommand: () => localCcCommand
|
|
11246
11778
|
});
|
|
11247
11779
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
11248
|
-
import { homedir as
|
|
11249
|
-
import { join as
|
|
11780
|
+
import { homedir as homedir15 } from "os";
|
|
11781
|
+
import { join as join15 } from "path";
|
|
11250
11782
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
11251
|
-
import { existsSync as
|
|
11783
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
11252
11784
|
function deploymentMode() {
|
|
11253
11785
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
11254
11786
|
if (env === "docker") return "docker";
|
|
@@ -11354,15 +11886,15 @@ TROUBLESHOOTING
|
|
|
11354
11886
|
`);
|
|
11355
11887
|
}
|
|
11356
11888
|
function readGatewayUrl() {
|
|
11357
|
-
if (
|
|
11358
|
-
const m =
|
|
11889
|
+
if (existsSync16(CONFIG_PATH4)) {
|
|
11890
|
+
const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
11359
11891
|
if (m) return m[1];
|
|
11360
11892
|
}
|
|
11361
11893
|
return "https://api.synkro.sh";
|
|
11362
11894
|
}
|
|
11363
11895
|
function updateLocalInferenceFlag(enabled) {
|
|
11364
|
-
if (!
|
|
11365
|
-
let content =
|
|
11896
|
+
if (!existsSync16(CONFIG_PATH4)) return;
|
|
11897
|
+
let content = readFileSync13(CONFIG_PATH4, "utf-8");
|
|
11366
11898
|
const flag = enabled ? "yes" : "no";
|
|
11367
11899
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
11368
11900
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -11371,7 +11903,7 @@ function updateLocalInferenceFlag(enabled) {
|
|
|
11371
11903
|
SYNKRO_LOCAL_INFERENCE='${flag}'
|
|
11372
11904
|
`;
|
|
11373
11905
|
}
|
|
11374
|
-
|
|
11906
|
+
writeFileSync10(CONFIG_PATH4, content, "utf-8");
|
|
11375
11907
|
}
|
|
11376
11908
|
async function setServerGradingProvider(provider) {
|
|
11377
11909
|
await ensureValidToken();
|
|
@@ -11801,8 +12333,8 @@ var init_localCc = __esm({
|
|
|
11801
12333
|
init_install();
|
|
11802
12334
|
init_client();
|
|
11803
12335
|
init_stub();
|
|
11804
|
-
SYNKRO_CONFIG_PATH =
|
|
11805
|
-
CONFIG_PATH4 =
|
|
12336
|
+
SYNKRO_CONFIG_PATH = join15(homedir15(), ".synkro", "config.env");
|
|
12337
|
+
CONFIG_PATH4 = join15(homedir15(), ".synkro", "config.env");
|
|
11806
12338
|
}
|
|
11807
12339
|
});
|
|
11808
12340
|
|
|
@@ -11913,13 +12445,13 @@ var config_exports = {};
|
|
|
11913
12445
|
__export(config_exports, {
|
|
11914
12446
|
configCommand: () => configCommand
|
|
11915
12447
|
});
|
|
11916
|
-
import { readFileSync as
|
|
11917
|
-
import { join as
|
|
11918
|
-
import { homedir as
|
|
12448
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
12449
|
+
import { join as join16 } from "path";
|
|
12450
|
+
import { homedir as homedir16 } from "os";
|
|
11919
12451
|
function readConfigEnv() {
|
|
11920
|
-
if (!
|
|
12452
|
+
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
11921
12453
|
const out = {};
|
|
11922
|
-
for (const line of
|
|
12454
|
+
for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
11923
12455
|
const t = line.trim();
|
|
11924
12456
|
if (!t || t.startsWith("#")) continue;
|
|
11925
12457
|
const eq = t.indexOf("=");
|
|
@@ -11928,11 +12460,11 @@ function readConfigEnv() {
|
|
|
11928
12460
|
return out;
|
|
11929
12461
|
}
|
|
11930
12462
|
function updateConfigValue(key, value) {
|
|
11931
|
-
if (!
|
|
12463
|
+
if (!existsSync17(CONFIG_PATH5)) {
|
|
11932
12464
|
console.error("No config found. Run `synkro install` first.");
|
|
11933
12465
|
process.exit(1);
|
|
11934
12466
|
}
|
|
11935
|
-
const lines =
|
|
12467
|
+
const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
|
|
11936
12468
|
const pattern = new RegExp(`^${key}=`);
|
|
11937
12469
|
let found = false;
|
|
11938
12470
|
const updated = lines.map((line) => {
|
|
@@ -11943,7 +12475,7 @@ function updateConfigValue(key, value) {
|
|
|
11943
12475
|
return line;
|
|
11944
12476
|
});
|
|
11945
12477
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
11946
|
-
|
|
12478
|
+
writeFileSync11(CONFIG_PATH5, updated.join("\n"), "utf-8");
|
|
11947
12479
|
}
|
|
11948
12480
|
async function reconcileContainer() {
|
|
11949
12481
|
const cfg = readConfigEnv();
|
|
@@ -12053,20 +12585,20 @@ var init_config = __esm({
|
|
|
12053
12585
|
"cli/commands/config.ts"() {
|
|
12054
12586
|
"use strict";
|
|
12055
12587
|
init_stub();
|
|
12056
|
-
SYNKRO_DIR6 =
|
|
12057
|
-
CONFIG_PATH5 =
|
|
12588
|
+
SYNKRO_DIR6 = join16(homedir16(), ".synkro");
|
|
12589
|
+
CONFIG_PATH5 = join16(SYNKRO_DIR6, "config.env");
|
|
12058
12590
|
}
|
|
12059
12591
|
});
|
|
12060
12592
|
|
|
12061
12593
|
// cli/bootstrap.js
|
|
12062
|
-
import { readFileSync as
|
|
12594
|
+
import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
|
|
12063
12595
|
import { resolve as resolve3 } from "path";
|
|
12064
12596
|
var envCandidates = [
|
|
12065
12597
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
12066
12598
|
];
|
|
12067
12599
|
for (const envPath of envCandidates) {
|
|
12068
|
-
if (!
|
|
12069
|
-
const envContent =
|
|
12600
|
+
if (!existsSync18(envPath)) continue;
|
|
12601
|
+
const envContent = readFileSync15(envPath, "utf-8");
|
|
12070
12602
|
for (const line of envContent.split("\n")) {
|
|
12071
12603
|
const trimmed = line.trim();
|
|
12072
12604
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -12081,7 +12613,7 @@ var args = process.argv.slice(2);
|
|
|
12081
12613
|
var cmd = args[0] || "";
|
|
12082
12614
|
var subArgs = args.slice(1);
|
|
12083
12615
|
function printVersion() {
|
|
12084
|
-
console.log("1.6.
|
|
12616
|
+
console.log("1.6.54");
|
|
12085
12617
|
}
|
|
12086
12618
|
function printHelp2() {
|
|
12087
12619
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -12097,6 +12629,7 @@ Commands:
|
|
|
12097
12629
|
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
12098
12630
|
update Pull the latest container image and safely restart
|
|
12099
12631
|
config Show or change grading + storage modes
|
|
12632
|
+
claude-desktop Monitor Claude Desktop conversations (local, macOS)
|
|
12100
12633
|
version Show version
|
|
12101
12634
|
|
|
12102
12635
|
config:
|
|
@@ -12192,6 +12725,11 @@ async function main() {
|
|
|
12192
12725
|
await configCommand2(args.slice(1));
|
|
12193
12726
|
break;
|
|
12194
12727
|
}
|
|
12728
|
+
case "claude-desktop": {
|
|
12729
|
+
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
12730
|
+
await runClaudeDesktopTap2();
|
|
12731
|
+
break;
|
|
12732
|
+
}
|
|
12195
12733
|
default: {
|
|
12196
12734
|
console.error(`Unknown command: ${cmd}`);
|
|
12197
12735
|
printHelp2();
|