@synkro-sh/cli 1.6.53 → 1.6.55
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 +777 -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.55")}`
|
|
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,9 @@ 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
|
+
console.log("\n Claude Desktop detected \u2014 to monitor its conversations in your dashboard, run: synkro claude-desktop");
|
|
9957
|
+
}
|
|
9435
9958
|
}
|
|
9436
9959
|
function parseTomlValue2(raw) {
|
|
9437
9960
|
const v = raw.trim();
|
|
@@ -9476,8 +9999,8 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
9476
9999
|
try {
|
|
9477
10000
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9478
10001
|
if (!root) return;
|
|
9479
|
-
if (root ===
|
|
9480
|
-
const fp =
|
|
10002
|
+
if (root === homedir9()) return;
|
|
10003
|
+
const fp = join9(root, "synkro.toml");
|
|
9481
10004
|
let hasFile = false;
|
|
9482
10005
|
try {
|
|
9483
10006
|
hasFile = statSync2(fp).isFile();
|
|
@@ -9516,7 +10039,7 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
9516
10039
|
"cve = true",
|
|
9517
10040
|
""
|
|
9518
10041
|
].join("\n");
|
|
9519
|
-
|
|
10042
|
+
writeFileSync8(fp, toml, "utf-8");
|
|
9520
10043
|
console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
|
|
9521
10044
|
} catch {
|
|
9522
10045
|
}
|
|
@@ -9525,9 +10048,9 @@ function readFullSynkroFile() {
|
|
|
9525
10048
|
try {
|
|
9526
10049
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9527
10050
|
if (!root) return null;
|
|
9528
|
-
const fp =
|
|
9529
|
-
if (!
|
|
9530
|
-
const parsed = parseSynkroToml2(
|
|
10051
|
+
const fp = join9(root, "synkro.toml");
|
|
10052
|
+
if (!existsSync11(fp)) return null;
|
|
10053
|
+
const parsed = parseSynkroToml2(readFileSync9(fp, "utf-8"));
|
|
9531
10054
|
const valid = ["claude-code", "cursor"];
|
|
9532
10055
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
9533
10056
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -9563,7 +10086,7 @@ function reconcileHarness() {
|
|
|
9563
10086
|
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
9564
10087
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9565
10088
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9566
|
-
const ccSettings =
|
|
10089
|
+
const ccSettings = join9(homedir9(), ".claude", "settings.json");
|
|
9567
10090
|
if (wantCC) {
|
|
9568
10091
|
installCCHooks(ccSettings, {
|
|
9569
10092
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -9582,7 +10105,7 @@ function reconcileHarness() {
|
|
|
9582
10105
|
});
|
|
9583
10106
|
console.log(" \u2713 Claude Code hooks registered");
|
|
9584
10107
|
try {
|
|
9585
|
-
const mcpJwt =
|
|
10108
|
+
const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9586
10109
|
if (mcpJwt) {
|
|
9587
10110
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
9588
10111
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -9593,7 +10116,7 @@ function reconcileHarness() {
|
|
|
9593
10116
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
9594
10117
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
9595
10118
|
}
|
|
9596
|
-
const cursorHooks =
|
|
10119
|
+
const cursorHooks = join9(homedir9(), ".cursor", "hooks.json");
|
|
9597
10120
|
if (wantCursor) {
|
|
9598
10121
|
installCursorHooks(cursorHooks, {
|
|
9599
10122
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -9646,7 +10169,7 @@ async function syncSkillFiles() {
|
|
|
9646
10169
|
if (resolved.length === 0) return;
|
|
9647
10170
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
9648
10171
|
const tasks = resolved.map((fp) => {
|
|
9649
|
-
const content =
|
|
10172
|
+
const content = readFileSync9(fp, "utf-8");
|
|
9650
10173
|
const source = `skill:${fp.split("/").pop()}`;
|
|
9651
10174
|
if (!content.trim()) {
|
|
9652
10175
|
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
@@ -9692,17 +10215,17 @@ function detectGitRepo2() {
|
|
|
9692
10215
|
function getClaudeProjectsFolder() {
|
|
9693
10216
|
const cwd = process.cwd();
|
|
9694
10217
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
9695
|
-
const projectsDir =
|
|
9696
|
-
return
|
|
10218
|
+
const projectsDir = join9(homedir9(), ".claude", "projects", sanitized);
|
|
10219
|
+
return existsSync11(projectsDir) ? projectsDir : null;
|
|
9697
10220
|
}
|
|
9698
10221
|
function extractSessionInsights(projectsDir) {
|
|
9699
10222
|
const insights = [];
|
|
9700
10223
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9701
10224
|
for (const file of files) {
|
|
9702
10225
|
const sessionId = file.replace(".jsonl", "");
|
|
9703
|
-
const filePath =
|
|
10226
|
+
const filePath = join9(projectsDir, file);
|
|
9704
10227
|
try {
|
|
9705
|
-
const content =
|
|
10228
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9706
10229
|
const lines = content.split("\n").filter(Boolean);
|
|
9707
10230
|
for (let i = 0; i < lines.length; i++) {
|
|
9708
10231
|
try {
|
|
@@ -9781,14 +10304,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
9781
10304
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9782
10305
|
}
|
|
9783
10306
|
function getCursorTranscriptsDir() {
|
|
9784
|
-
const dir =
|
|
9785
|
-
return
|
|
10307
|
+
const dir = join9(homedir9(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10308
|
+
return existsSync11(dir) ? dir : null;
|
|
9786
10309
|
}
|
|
9787
10310
|
function isSafeConvId(id) {
|
|
9788
10311
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
9789
10312
|
}
|
|
9790
10313
|
function parseCursorTranscriptFile(filePath) {
|
|
9791
|
-
const content =
|
|
10314
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9792
10315
|
const lines = content.split("\n").filter(Boolean);
|
|
9793
10316
|
const messages = [];
|
|
9794
10317
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9820,8 +10343,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9820
10343
|
for (let i = 0; i < convDirs.length; i++) {
|
|
9821
10344
|
const convId = convDirs[i];
|
|
9822
10345
|
if (!isSafeConvId(convId)) continue;
|
|
9823
|
-
const filePath =
|
|
9824
|
-
if (!
|
|
10346
|
+
const filePath = join9(dir, convId, `${convId}.jsonl`);
|
|
10347
|
+
if (!existsSync11(filePath)) continue;
|
|
9825
10348
|
try {
|
|
9826
10349
|
const all = parseCursorTranscriptFile(filePath);
|
|
9827
10350
|
const messages = all.length > 500 ? all.slice(-500) : all;
|
|
@@ -9843,8 +10366,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9843
10366
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
9844
10367
|
}
|
|
9845
10368
|
try {
|
|
9846
|
-
const lc =
|
|
9847
|
-
|
|
10369
|
+
const lc = readFileSync9(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
10370
|
+
writeFileSync8(join9(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
9848
10371
|
} catch {
|
|
9849
10372
|
}
|
|
9850
10373
|
}
|
|
@@ -9852,7 +10375,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9852
10375
|
return { sessions: totalSessions, messages: totalMessages };
|
|
9853
10376
|
}
|
|
9854
10377
|
function parseTranscriptFile(filePath) {
|
|
9855
|
-
const content =
|
|
10378
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9856
10379
|
const lines = content.split("\n").filter(Boolean);
|
|
9857
10380
|
const messages = [];
|
|
9858
10381
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9900,7 +10423,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9900
10423
|
for (let i = 0; i < files.length; i++) {
|
|
9901
10424
|
const file = files[i];
|
|
9902
10425
|
const sessionId = file.replace(".jsonl", "");
|
|
9903
|
-
const filePath =
|
|
10426
|
+
const filePath = join9(projectsDir, file);
|
|
9904
10427
|
try {
|
|
9905
10428
|
const allMessages = parseTranscriptFile(filePath);
|
|
9906
10429
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -9922,9 +10445,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9922
10445
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
9923
10446
|
}
|
|
9924
10447
|
try {
|
|
9925
|
-
const content =
|
|
10448
|
+
const content = readFileSync9(join9(projectsDir, file), "utf-8");
|
|
9926
10449
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9927
|
-
|
|
10450
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9928
10451
|
} catch {
|
|
9929
10452
|
}
|
|
9930
10453
|
}
|
|
@@ -9945,7 +10468,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9945
10468
|
const sessions = [];
|
|
9946
10469
|
for (const file of batch) {
|
|
9947
10470
|
const sessionId = file.replace(".jsonl", "");
|
|
9948
|
-
const filePath =
|
|
10471
|
+
const filePath = join9(projectsDir, file);
|
|
9949
10472
|
try {
|
|
9950
10473
|
const allMessages = parseTranscriptFile(filePath);
|
|
9951
10474
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -9974,11 +10497,11 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9974
10497
|
}
|
|
9975
10498
|
for (const file of batch) {
|
|
9976
10499
|
const sessionId = file.replace(".jsonl", "");
|
|
9977
|
-
const filePath =
|
|
10500
|
+
const filePath = join9(projectsDir, file);
|
|
9978
10501
|
try {
|
|
9979
|
-
const content =
|
|
10502
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9980
10503
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9981
|
-
|
|
10504
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9982
10505
|
} catch {
|
|
9983
10506
|
}
|
|
9984
10507
|
}
|
|
@@ -10001,11 +10524,12 @@ var init_install = __esm({
|
|
|
10001
10524
|
init_repoConnect();
|
|
10002
10525
|
init_projects();
|
|
10003
10526
|
init_promptFetcher();
|
|
10527
|
+
init_claudeDesktopTap();
|
|
10004
10528
|
init_dockerInstall();
|
|
10005
|
-
SYNKRO_DIR4 =
|
|
10006
|
-
HOOKS_DIR =
|
|
10007
|
-
BIN_DIR =
|
|
10008
|
-
CONFIG_PATH2 =
|
|
10529
|
+
SYNKRO_DIR4 = join9(homedir9(), ".synkro");
|
|
10530
|
+
HOOKS_DIR = join9(SYNKRO_DIR4, "hooks");
|
|
10531
|
+
BIN_DIR = join9(SYNKRO_DIR4, "bin");
|
|
10532
|
+
CONFIG_PATH2 = join9(SYNKRO_DIR4, "config.env");
|
|
10009
10533
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
10010
10534
|
import { readFileSync } from 'node:fs';
|
|
10011
10535
|
import { homedir } from 'node:os';
|
|
@@ -10116,21 +10640,21 @@ rl.on('line', async (line) => {
|
|
|
10116
10640
|
}
|
|
10117
10641
|
});
|
|
10118
10642
|
`;
|
|
10119
|
-
OFFSETS_DIR =
|
|
10643
|
+
OFFSETS_DIR = join9(SYNKRO_DIR4, ".transcript-offsets");
|
|
10120
10644
|
}
|
|
10121
10645
|
});
|
|
10122
10646
|
|
|
10123
10647
|
// cli/local-cc/install.ts
|
|
10124
|
-
import { existsSync as
|
|
10125
|
-
import { join as
|
|
10126
|
-
import { homedir as
|
|
10648
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync10, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
10649
|
+
import { join as join10 } from "path";
|
|
10650
|
+
import { homedir as homedir10 } from "os";
|
|
10127
10651
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
10128
10652
|
function writePluginFiles() {
|
|
10129
10653
|
for (const c of CHANNELS) {
|
|
10130
10654
|
mkdirSync9(c.sessionDir, { recursive: true });
|
|
10131
10655
|
mkdirSync9(c.pluginSettingsDir, { recursive: true });
|
|
10132
|
-
|
|
10133
|
-
|
|
10656
|
+
writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
10657
|
+
writeFileSync9(
|
|
10134
10658
|
c.pluginSettingsPath,
|
|
10135
10659
|
JSON.stringify({
|
|
10136
10660
|
fastMode: true,
|
|
@@ -10138,7 +10662,7 @@ function writePluginFiles() {
|
|
|
10138
10662
|
}, null, 2) + "\n",
|
|
10139
10663
|
"utf-8"
|
|
10140
10664
|
);
|
|
10141
|
-
|
|
10665
|
+
writeFileSync9(c.runScriptPath, c.runScriptSource, "utf-8");
|
|
10142
10666
|
chmodSync3(c.runScriptPath, 493);
|
|
10143
10667
|
}
|
|
10144
10668
|
}
|
|
@@ -10157,10 +10681,10 @@ function runBunInstall() {
|
|
|
10157
10681
|
}
|
|
10158
10682
|
}
|
|
10159
10683
|
function safelyMutateClaudeJson(mutator) {
|
|
10160
|
-
if (!
|
|
10684
|
+
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
10161
10685
|
return;
|
|
10162
10686
|
}
|
|
10163
|
-
const originalText =
|
|
10687
|
+
const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
|
|
10164
10688
|
let parsed;
|
|
10165
10689
|
try {
|
|
10166
10690
|
parsed = JSON.parse(originalText);
|
|
@@ -10192,7 +10716,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
10192
10716
|
copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
|
|
10193
10717
|
const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
|
|
10194
10718
|
try {
|
|
10195
|
-
|
|
10719
|
+
writeFileSync9(tmpPath, newText, "utf-8");
|
|
10196
10720
|
const fd = openSync(tmpPath, "r");
|
|
10197
10721
|
try {
|
|
10198
10722
|
fsyncSync(fd);
|
|
@@ -10225,7 +10749,7 @@ function writeProjectMcpJson() {
|
|
|
10225
10749
|
}
|
|
10226
10750
|
}
|
|
10227
10751
|
};
|
|
10228
|
-
|
|
10752
|
+
writeFileSync9(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
10229
10753
|
}
|
|
10230
10754
|
}
|
|
10231
10755
|
function patchClaudeJson() {
|
|
@@ -10302,42 +10826,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
10302
10826
|
var init_install2 = __esm({
|
|
10303
10827
|
"cli/local-cc/install.ts"() {
|
|
10304
10828
|
"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 =
|
|
10829
|
+
CLAUDE_JSON_BACKUP_PATH = join10(homedir10(), ".claude.json.synkro-bak");
|
|
10830
|
+
SESSION_DIR = join10(homedir10(), ".synkro", "cc_sessions");
|
|
10831
|
+
PLUGIN_PATH = join10(SESSION_DIR, "synkro-channel.ts");
|
|
10832
|
+
PLUGIN_PKG_PATH = join10(SESSION_DIR, "package.json");
|
|
10833
|
+
PLUGIN_SETTINGS_DIR = join10(SESSION_DIR, ".claude");
|
|
10834
|
+
PLUGIN_SETTINGS_PATH = join10(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
10835
|
+
PROJECT_MCP_PATH = join10(SESSION_DIR, ".mcp.json");
|
|
10836
|
+
CLAUDE_JSON_PATH = join10(homedir10(), ".claude.json");
|
|
10837
|
+
RUN_SCRIPT_PATH = join10(SESSION_DIR, "run-claude.sh");
|
|
10314
10838
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
10315
10839
|
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 =
|
|
10840
|
+
SESSION_DIR_2 = join10(homedir10(), ".synkro", "cc_sessions_2");
|
|
10841
|
+
PLUGIN_PATH_2 = join10(SESSION_DIR_2, "synkro-channel.ts");
|
|
10842
|
+
PLUGIN_PKG_PATH_2 = join10(SESSION_DIR_2, "package.json");
|
|
10843
|
+
PLUGIN_SETTINGS_DIR_2 = join10(SESSION_DIR_2, ".claude");
|
|
10844
|
+
PLUGIN_SETTINGS_PATH_2 = join10(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
10845
|
+
PROJECT_MCP_PATH_2 = join10(SESSION_DIR_2, ".mcp.json");
|
|
10846
|
+
RUN_SCRIPT_PATH_2 = join10(SESSION_DIR_2, "run-claude.sh");
|
|
10323
10847
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
10324
10848
|
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 =
|
|
10849
|
+
SESSION_DIR_3 = join10(homedir10(), ".synkro", "cc_sessions_3");
|
|
10850
|
+
PLUGIN_PATH_3 = join10(SESSION_DIR_3, "synkro-channel.ts");
|
|
10851
|
+
PLUGIN_PKG_PATH_3 = join10(SESSION_DIR_3, "package.json");
|
|
10852
|
+
PLUGIN_SETTINGS_DIR_3 = join10(SESSION_DIR_3, ".claude");
|
|
10853
|
+
PLUGIN_SETTINGS_PATH_3 = join10(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
10854
|
+
PROJECT_MCP_PATH_3 = join10(SESSION_DIR_3, ".mcp.json");
|
|
10855
|
+
RUN_SCRIPT_PATH_3 = join10(SESSION_DIR_3, "run-claude.sh");
|
|
10332
10856
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
10333
10857
|
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 =
|
|
10858
|
+
SESSION_DIR_4 = join10(homedir10(), ".synkro", "cc_sessions_4");
|
|
10859
|
+
PLUGIN_PATH_4 = join10(SESSION_DIR_4, "synkro-channel.ts");
|
|
10860
|
+
PLUGIN_PKG_PATH_4 = join10(SESSION_DIR_4, "package.json");
|
|
10861
|
+
PLUGIN_SETTINGS_DIR_4 = join10(SESSION_DIR_4, ".claude");
|
|
10862
|
+
PLUGIN_SETTINGS_PATH_4 = join10(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
10863
|
+
PROJECT_MCP_PATH_4 = join10(SESSION_DIR_4, ".mcp.json");
|
|
10864
|
+
RUN_SCRIPT_PATH_4 = join10(SESSION_DIR_4, "run-claude.sh");
|
|
10341
10865
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
10342
10866
|
CHANNEL_4_PORT = 8952;
|
|
10343
10867
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -10611,9 +11135,9 @@ var disconnect_exports = {};
|
|
|
10611
11135
|
__export(disconnect_exports, {
|
|
10612
11136
|
disconnectCommand: () => disconnectCommand
|
|
10613
11137
|
});
|
|
10614
|
-
import { existsSync as
|
|
10615
|
-
import { homedir as
|
|
10616
|
-
import { join as
|
|
11138
|
+
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11139
|
+
import { homedir as homedir11 } from "os";
|
|
11140
|
+
import { join as join11 } from "path";
|
|
10617
11141
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
10618
11142
|
import { createInterface as createInterface4 } from "readline";
|
|
10619
11143
|
async function tearDownLocalCC() {
|
|
@@ -10687,15 +11211,15 @@ async function disconnectCommand(args2 = []) {
|
|
|
10687
11211
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
10688
11212
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
10689
11213
|
}
|
|
10690
|
-
if (
|
|
11214
|
+
if (existsSync13(SYNKRO_DIR5)) {
|
|
10691
11215
|
if (purge) {
|
|
10692
11216
|
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
10693
11217
|
console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
|
|
10694
11218
|
} else {
|
|
10695
|
-
const keep = /* @__PURE__ */ new Set([
|
|
11219
|
+
const keep = /* @__PURE__ */ new Set([join11(SYNKRO_DIR5, "pgdata"), join11(SYNKRO_DIR5, "pgdata-backups")]);
|
|
10696
11220
|
const preserved = [];
|
|
10697
11221
|
for (const entry of readdirSync4(SYNKRO_DIR5)) {
|
|
10698
|
-
const full =
|
|
11222
|
+
const full = join11(SYNKRO_DIR5, entry);
|
|
10699
11223
|
if (keep.has(full)) {
|
|
10700
11224
|
preserved.push(entry);
|
|
10701
11225
|
continue;
|
|
@@ -10725,14 +11249,14 @@ var init_disconnect = __esm({
|
|
|
10725
11249
|
init_install2();
|
|
10726
11250
|
init_dockerInstall();
|
|
10727
11251
|
init_macKeychain();
|
|
10728
|
-
SYNKRO_DIR5 =
|
|
11252
|
+
SYNKRO_DIR5 = join11(homedir11(), ".synkro");
|
|
10729
11253
|
}
|
|
10730
11254
|
});
|
|
10731
11255
|
|
|
10732
11256
|
// cli/local-cc/turnLog.ts
|
|
10733
|
-
import { appendFileSync, existsSync as
|
|
10734
|
-
import { dirname as dirname6, join as
|
|
10735
|
-
import { homedir as
|
|
11257
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync as statSync3, watchFile, unwatchFile } from "fs";
|
|
11258
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
11259
|
+
import { homedir as homedir12 } from "os";
|
|
10736
11260
|
function truncate(s, max = PREVIEW_MAX) {
|
|
10737
11261
|
if (s.length <= max) return s;
|
|
10738
11262
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -10768,11 +11292,11 @@ function appendTurn(args2) {
|
|
|
10768
11292
|
}
|
|
10769
11293
|
}
|
|
10770
11294
|
function readRecentTurns(n = 20) {
|
|
10771
|
-
if (!
|
|
11295
|
+
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
10772
11296
|
try {
|
|
10773
11297
|
const size = statSync3(TURN_LOG_PATH).size;
|
|
10774
11298
|
if (size === 0) return [];
|
|
10775
|
-
const text =
|
|
11299
|
+
const text = readFileSync11(TURN_LOG_PATH, "utf-8");
|
|
10776
11300
|
const lines = text.split("\n").filter(Boolean);
|
|
10777
11301
|
const lastN = lines.slice(-n).reverse();
|
|
10778
11302
|
return lastN.map((line) => {
|
|
@@ -10789,7 +11313,7 @@ function readRecentTurns(n = 20) {
|
|
|
10789
11313
|
function followTurns(onEntry) {
|
|
10790
11314
|
try {
|
|
10791
11315
|
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
10792
|
-
if (!
|
|
11316
|
+
if (!existsSync14(TURN_LOG_PATH)) {
|
|
10793
11317
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
10794
11318
|
}
|
|
10795
11319
|
} catch {
|
|
@@ -10851,7 +11375,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
10851
11375
|
var init_turnLog = __esm({
|
|
10852
11376
|
"cli/local-cc/turnLog.ts"() {
|
|
10853
11377
|
"use strict";
|
|
10854
|
-
TURN_LOG_PATH =
|
|
11378
|
+
TURN_LOG_PATH = join12(homedir12(), ".synkro", "cc_sessions", "turns.log");
|
|
10855
11379
|
PREVIEW_MAX = 400;
|
|
10856
11380
|
}
|
|
10857
11381
|
});
|
|
@@ -11000,9 +11524,9 @@ var init_grade = __esm({
|
|
|
11000
11524
|
});
|
|
11001
11525
|
|
|
11002
11526
|
// 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
|
|
11527
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn as spawn2 } from "child_process";
|
|
11528
|
+
import { homedir as homedir13 } from "os";
|
|
11529
|
+
import { join as join13 } from "path";
|
|
11006
11530
|
import { connect as connect2 } from "net";
|
|
11007
11531
|
function pueueAvailable() {
|
|
11008
11532
|
const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
@@ -11068,7 +11592,7 @@ function startTask(opts = {}) {
|
|
|
11068
11592
|
spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
11069
11593
|
existing = findTask(ch);
|
|
11070
11594
|
}
|
|
11071
|
-
const runScript =
|
|
11595
|
+
const runScript = join13(cwd, "run-claude.sh");
|
|
11072
11596
|
const args2 = [
|
|
11073
11597
|
"add",
|
|
11074
11598
|
"--label",
|
|
@@ -11165,7 +11689,7 @@ function assertPueueInstalled() {
|
|
|
11165
11689
|
const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
11166
11690
|
if (status.status !== 0) {
|
|
11167
11691
|
console.log(" Starting pueued daemon...");
|
|
11168
|
-
const child =
|
|
11692
|
+
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
11169
11693
|
child.unref();
|
|
11170
11694
|
spawnSync6("sleep", ["1"]);
|
|
11171
11695
|
const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -11198,12 +11722,12 @@ var init_pueue = __esm({
|
|
|
11198
11722
|
"use strict";
|
|
11199
11723
|
TASK_LABEL = "synkro-local-cc";
|
|
11200
11724
|
TMUX_SESSION = "synkro-local-cc";
|
|
11201
|
-
SESSION_DIR2 =
|
|
11725
|
+
SESSION_DIR2 = join13(homedir13(), ".synkro", "cc_sessions");
|
|
11202
11726
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
11203
11727
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
11204
|
-
SESSION_DIR_22 =
|
|
11205
|
-
SESSION_DIR_32 =
|
|
11206
|
-
SESSION_DIR_42 =
|
|
11728
|
+
SESSION_DIR_22 = join13(homedir13(), ".synkro", "cc_sessions_2");
|
|
11729
|
+
SESSION_DIR_32 = join13(homedir13(), ".synkro", "cc_sessions_3");
|
|
11730
|
+
SESSION_DIR_42 = join13(homedir13(), ".synkro", "cc_sessions_4");
|
|
11207
11731
|
PueueError = class extends Error {
|
|
11208
11732
|
constructor(message, cause) {
|
|
11209
11733
|
super(message);
|
|
@@ -11218,13 +11742,13 @@ var init_pueue = __esm({
|
|
|
11218
11742
|
});
|
|
11219
11743
|
|
|
11220
11744
|
// cli/local-cc/settings.ts
|
|
11221
|
-
import { existsSync as
|
|
11222
|
-
import { homedir as
|
|
11223
|
-
import { join as
|
|
11745
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
|
|
11746
|
+
import { homedir as homedir14 } from "os";
|
|
11747
|
+
import { join as join14 } from "path";
|
|
11224
11748
|
function isLocalCCEnabled() {
|
|
11225
|
-
if (!
|
|
11749
|
+
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
11226
11750
|
try {
|
|
11227
|
-
const content =
|
|
11751
|
+
const content = readFileSync12(CONFIG_PATH3, "utf-8");
|
|
11228
11752
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
11229
11753
|
return match?.[1] === "yes";
|
|
11230
11754
|
} catch {
|
|
@@ -11235,7 +11759,7 @@ var CONFIG_PATH3;
|
|
|
11235
11759
|
var init_settings = __esm({
|
|
11236
11760
|
"cli/local-cc/settings.ts"() {
|
|
11237
11761
|
"use strict";
|
|
11238
|
-
CONFIG_PATH3 =
|
|
11762
|
+
CONFIG_PATH3 = join14(homedir14(), ".synkro", "config.env");
|
|
11239
11763
|
}
|
|
11240
11764
|
});
|
|
11241
11765
|
|
|
@@ -11245,10 +11769,10 @@ __export(localCc_exports, {
|
|
|
11245
11769
|
localCcCommand: () => localCcCommand
|
|
11246
11770
|
});
|
|
11247
11771
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
11248
|
-
import { homedir as
|
|
11249
|
-
import { join as
|
|
11772
|
+
import { homedir as homedir15 } from "os";
|
|
11773
|
+
import { join as join15 } from "path";
|
|
11250
11774
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
11251
|
-
import { existsSync as
|
|
11775
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
11252
11776
|
function deploymentMode() {
|
|
11253
11777
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
11254
11778
|
if (env === "docker") return "docker";
|
|
@@ -11354,15 +11878,15 @@ TROUBLESHOOTING
|
|
|
11354
11878
|
`);
|
|
11355
11879
|
}
|
|
11356
11880
|
function readGatewayUrl() {
|
|
11357
|
-
if (
|
|
11358
|
-
const m =
|
|
11881
|
+
if (existsSync16(CONFIG_PATH4)) {
|
|
11882
|
+
const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
11359
11883
|
if (m) return m[1];
|
|
11360
11884
|
}
|
|
11361
11885
|
return "https://api.synkro.sh";
|
|
11362
11886
|
}
|
|
11363
11887
|
function updateLocalInferenceFlag(enabled) {
|
|
11364
|
-
if (!
|
|
11365
|
-
let content =
|
|
11888
|
+
if (!existsSync16(CONFIG_PATH4)) return;
|
|
11889
|
+
let content = readFileSync13(CONFIG_PATH4, "utf-8");
|
|
11366
11890
|
const flag = enabled ? "yes" : "no";
|
|
11367
11891
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
11368
11892
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -11371,7 +11895,7 @@ function updateLocalInferenceFlag(enabled) {
|
|
|
11371
11895
|
SYNKRO_LOCAL_INFERENCE='${flag}'
|
|
11372
11896
|
`;
|
|
11373
11897
|
}
|
|
11374
|
-
|
|
11898
|
+
writeFileSync10(CONFIG_PATH4, content, "utf-8");
|
|
11375
11899
|
}
|
|
11376
11900
|
async function setServerGradingProvider(provider) {
|
|
11377
11901
|
await ensureValidToken();
|
|
@@ -11801,8 +12325,8 @@ var init_localCc = __esm({
|
|
|
11801
12325
|
init_install();
|
|
11802
12326
|
init_client();
|
|
11803
12327
|
init_stub();
|
|
11804
|
-
SYNKRO_CONFIG_PATH =
|
|
11805
|
-
CONFIG_PATH4 =
|
|
12328
|
+
SYNKRO_CONFIG_PATH = join15(homedir15(), ".synkro", "config.env");
|
|
12329
|
+
CONFIG_PATH4 = join15(homedir15(), ".synkro", "config.env");
|
|
11806
12330
|
}
|
|
11807
12331
|
});
|
|
11808
12332
|
|
|
@@ -11913,13 +12437,13 @@ var config_exports = {};
|
|
|
11913
12437
|
__export(config_exports, {
|
|
11914
12438
|
configCommand: () => configCommand
|
|
11915
12439
|
});
|
|
11916
|
-
import { readFileSync as
|
|
11917
|
-
import { join as
|
|
11918
|
-
import { homedir as
|
|
12440
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
12441
|
+
import { join as join16 } from "path";
|
|
12442
|
+
import { homedir as homedir16 } from "os";
|
|
11919
12443
|
function readConfigEnv() {
|
|
11920
|
-
if (!
|
|
12444
|
+
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
11921
12445
|
const out = {};
|
|
11922
|
-
for (const line of
|
|
12446
|
+
for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
11923
12447
|
const t = line.trim();
|
|
11924
12448
|
if (!t || t.startsWith("#")) continue;
|
|
11925
12449
|
const eq = t.indexOf("=");
|
|
@@ -11928,11 +12452,11 @@ function readConfigEnv() {
|
|
|
11928
12452
|
return out;
|
|
11929
12453
|
}
|
|
11930
12454
|
function updateConfigValue(key, value) {
|
|
11931
|
-
if (!
|
|
12455
|
+
if (!existsSync17(CONFIG_PATH5)) {
|
|
11932
12456
|
console.error("No config found. Run `synkro install` first.");
|
|
11933
12457
|
process.exit(1);
|
|
11934
12458
|
}
|
|
11935
|
-
const lines =
|
|
12459
|
+
const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
|
|
11936
12460
|
const pattern = new RegExp(`^${key}=`);
|
|
11937
12461
|
let found = false;
|
|
11938
12462
|
const updated = lines.map((line) => {
|
|
@@ -11943,7 +12467,7 @@ function updateConfigValue(key, value) {
|
|
|
11943
12467
|
return line;
|
|
11944
12468
|
});
|
|
11945
12469
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
11946
|
-
|
|
12470
|
+
writeFileSync11(CONFIG_PATH5, updated.join("\n"), "utf-8");
|
|
11947
12471
|
}
|
|
11948
12472
|
async function reconcileContainer() {
|
|
11949
12473
|
const cfg = readConfigEnv();
|
|
@@ -12053,20 +12577,20 @@ var init_config = __esm({
|
|
|
12053
12577
|
"cli/commands/config.ts"() {
|
|
12054
12578
|
"use strict";
|
|
12055
12579
|
init_stub();
|
|
12056
|
-
SYNKRO_DIR6 =
|
|
12057
|
-
CONFIG_PATH5 =
|
|
12580
|
+
SYNKRO_DIR6 = join16(homedir16(), ".synkro");
|
|
12581
|
+
CONFIG_PATH5 = join16(SYNKRO_DIR6, "config.env");
|
|
12058
12582
|
}
|
|
12059
12583
|
});
|
|
12060
12584
|
|
|
12061
12585
|
// cli/bootstrap.js
|
|
12062
|
-
import { readFileSync as
|
|
12586
|
+
import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
|
|
12063
12587
|
import { resolve as resolve3 } from "path";
|
|
12064
12588
|
var envCandidates = [
|
|
12065
12589
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
12066
12590
|
];
|
|
12067
12591
|
for (const envPath of envCandidates) {
|
|
12068
|
-
if (!
|
|
12069
|
-
const envContent =
|
|
12592
|
+
if (!existsSync18(envPath)) continue;
|
|
12593
|
+
const envContent = readFileSync15(envPath, "utf-8");
|
|
12070
12594
|
for (const line of envContent.split("\n")) {
|
|
12071
12595
|
const trimmed = line.trim();
|
|
12072
12596
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -12081,7 +12605,7 @@ var args = process.argv.slice(2);
|
|
|
12081
12605
|
var cmd = args[0] || "";
|
|
12082
12606
|
var subArgs = args.slice(1);
|
|
12083
12607
|
function printVersion() {
|
|
12084
|
-
console.log("1.6.
|
|
12608
|
+
console.log("1.6.55");
|
|
12085
12609
|
}
|
|
12086
12610
|
function printHelp2() {
|
|
12087
12611
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -12097,6 +12621,7 @@ Commands:
|
|
|
12097
12621
|
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
12098
12622
|
update Pull the latest container image and safely restart
|
|
12099
12623
|
config Show or change grading + storage modes
|
|
12624
|
+
claude-desktop Monitor Claude Desktop conversations (local, macOS)
|
|
12100
12625
|
version Show version
|
|
12101
12626
|
|
|
12102
12627
|
config:
|
|
@@ -12192,6 +12717,11 @@ async function main() {
|
|
|
12192
12717
|
await configCommand2(args.slice(1));
|
|
12193
12718
|
break;
|
|
12194
12719
|
}
|
|
12720
|
+
case "claude-desktop": {
|
|
12721
|
+
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
12722
|
+
await runClaudeDesktopTap2();
|
|
12723
|
+
break;
|
|
12724
|
+
}
|
|
12195
12725
|
default: {
|
|
12196
12726
|
console.error(`Unknown command: ${cmd}`);
|
|
12197
12727
|
printHelp2();
|