@synkro-sh/cli 1.6.58 → 1.6.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +737 -321
- package/dist/bootstrap.js.map +1 -1
- package/package.json +16 -15
package/dist/bootstrap.js
CHANGED
|
@@ -509,6 +509,8 @@ var init_cursorHookConfig = __esm({
|
|
|
509
509
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
510
510
|
import { homedir as homedir3 } from "os";
|
|
511
511
|
import { dirname as dirname3, join as join2 } from "path";
|
|
512
|
+
import { spawnSync } from "child_process";
|
|
513
|
+
import { randomBytes } from "crypto";
|
|
512
514
|
function readClaudeJson() {
|
|
513
515
|
if (!existsSync3(CC_CONFIG_PATH)) return {};
|
|
514
516
|
try {
|
|
@@ -630,7 +632,69 @@ function uninstallCursorMcpConfig() {
|
|
|
630
632
|
writeCursorMcpJsonAtomic(config);
|
|
631
633
|
return true;
|
|
632
634
|
}
|
|
633
|
-
|
|
635
|
+
function resolveBunBin() {
|
|
636
|
+
const r = spawnSync("which", ["bun"], { encoding: "utf-8", timeout: 5e3 });
|
|
637
|
+
const resolved = (r.stdout || "").split("\n")[0].trim();
|
|
638
|
+
if (resolved) return resolved;
|
|
639
|
+
for (const p of ["/opt/homebrew/bin/bun", "/usr/local/bin/bun", join2(homedir3(), ".bun", "bin", "bun")]) {
|
|
640
|
+
if (existsSync3(p)) return p;
|
|
641
|
+
}
|
|
642
|
+
return "bun";
|
|
643
|
+
}
|
|
644
|
+
function readDesktopJson() {
|
|
645
|
+
if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return {};
|
|
646
|
+
try {
|
|
647
|
+
return JSON.parse(readFileSync3(CLAUDE_DESKTOP_CONFIG_PATH, "utf-8"));
|
|
648
|
+
} catch (err) {
|
|
649
|
+
throw new Error(`Failed to parse ${CLAUDE_DESKTOP_CONFIG_PATH}: ${err.message}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function writeDesktopJsonAtomic(config) {
|
|
653
|
+
mkdirSync3(dirname3(CLAUDE_DESKTOP_CONFIG_PATH), { recursive: true });
|
|
654
|
+
const tmpPath = `${CLAUDE_DESKTOP_CONFIG_PATH}.synkro.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
655
|
+
writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
656
|
+
renameSync3(tmpPath, CLAUDE_DESKTOP_CONFIG_PATH);
|
|
657
|
+
}
|
|
658
|
+
function installClaudeDesktopMcpConfig() {
|
|
659
|
+
if (!existsSync3(MCP_STDIO_PROXY_PATH)) {
|
|
660
|
+
return {
|
|
661
|
+
ok: false,
|
|
662
|
+
path: CLAUDE_DESKTOP_CONFIG_PATH,
|
|
663
|
+
proxy: MCP_STDIO_PROXY_PATH,
|
|
664
|
+
reason: "stdio proxy not found \u2014 run `synkro install` first"
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const config = readDesktopJson();
|
|
668
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
669
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
670
|
+
if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
|
|
671
|
+
}
|
|
672
|
+
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
673
|
+
command: resolveBunBin(),
|
|
674
|
+
args: ["run", MCP_STDIO_PROXY_PATH],
|
|
675
|
+
env: { SYNKRO_MCP_PORT: process.env.SYNKRO_MCP_PORT || "18931" },
|
|
676
|
+
[SYNKRO_MARKER3]: true
|
|
677
|
+
};
|
|
678
|
+
writeDesktopJsonAtomic(config);
|
|
679
|
+
return { ok: true, path: CLAUDE_DESKTOP_CONFIG_PATH, proxy: MCP_STDIO_PROXY_PATH };
|
|
680
|
+
}
|
|
681
|
+
function uninstallClaudeDesktopMcpConfig() {
|
|
682
|
+
if (!existsSync3(CLAUDE_DESKTOP_CONFIG_PATH)) return false;
|
|
683
|
+
const config = readDesktopJson();
|
|
684
|
+
if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
|
|
685
|
+
let removed = false;
|
|
686
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
687
|
+
if (entry?.[SYNKRO_MARKER3] === true) {
|
|
688
|
+
delete config.mcpServers[name];
|
|
689
|
+
removed = true;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (!removed) return false;
|
|
693
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
694
|
+
writeDesktopJsonAtomic(config);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH, CLAUDE_DESKTOP_CONFIG_PATH, MCP_STDIO_PROXY_PATH;
|
|
634
698
|
var init_mcpConfig = __esm({
|
|
635
699
|
"cli/installer/mcpConfig.ts"() {
|
|
636
700
|
"use strict";
|
|
@@ -638,6 +702,14 @@ var init_mcpConfig = __esm({
|
|
|
638
702
|
SYNKRO_SERVER_NAME = "synkro-guardrails";
|
|
639
703
|
CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
|
|
640
704
|
CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
|
|
705
|
+
CLAUDE_DESKTOP_CONFIG_PATH = join2(
|
|
706
|
+
homedir3(),
|
|
707
|
+
"Library",
|
|
708
|
+
"Application Support",
|
|
709
|
+
"Claude",
|
|
710
|
+
"claude_desktop_config.json"
|
|
711
|
+
);
|
|
712
|
+
MCP_STDIO_PROXY_PATH = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
|
|
641
713
|
}
|
|
642
714
|
});
|
|
643
715
|
|
|
@@ -1595,6 +1667,28 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
|
|
|
1595
1667
|
return String(data.verdict || '');
|
|
1596
1668
|
}
|
|
1597
1669
|
|
|
1670
|
+
// Cloud-container grading \u2014 the org's grader runs on Cloudflare Containers. Same
|
|
1671
|
+
// request/response shape as channelGrade (the host-host Worker forwards verbatim
|
|
1672
|
+
// to the org's dispatcher /submit), but to the hosted ingress with the gateway
|
|
1673
|
+
// JWT (the Worker derives the org from the token and routes to that org's
|
|
1674
|
+
// container). Any non-2xx throws so the caller's catch fails open.
|
|
1675
|
+
const CONTAINERS_URL = process.env.SYNKRO_CONTAINERS_URL || 'https://containers.synkro.sh';
|
|
1676
|
+
async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1677
|
+
const resp = await fetch(CONTAINERS_URL + '/grade/submit', {
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
1680
|
+
body: JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind }),
|
|
1681
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1682
|
+
});
|
|
1683
|
+
if (!resp.ok) {
|
|
1684
|
+
const text = await resp.text().catch(() => '');
|
|
1685
|
+
throw new Error('hosted grade ' + resp.status + ': ' + text.slice(0, 200));
|
|
1686
|
+
}
|
|
1687
|
+
const data = await resp.json() as { result?: string; error?: string };
|
|
1688
|
+
if (data.error) throw new Error(data.error);
|
|
1689
|
+
return String(data.result || '');
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1598
1692
|
// Default 24s \u2014 MUST stay below the CC hook timeout (30s for edit/bash/cwe in
|
|
1599
1693
|
// ccHookConfig.ts) so the AbortSignal fires and the caller's catch fails open
|
|
1600
1694
|
// cleanly. If this matches or exceeds the CC budget, CC force-kills the bun
|
|
@@ -1603,6 +1697,11 @@ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutM
|
|
|
1603
1697
|
export async function localGrade(surface: string, prompt: string, timeoutMs = 24000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1604
1698
|
const jwt = loadJwt();
|
|
1605
1699
|
if (!jwt) throw new Error('NO_JWT');
|
|
1700
|
+
// Cloud-container: the grader runs on Cloudflare Containers \u2014 route to the
|
|
1701
|
+
// hosted ingress instead of the local channel. Same prompt + parseVerdict.
|
|
1702
|
+
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
1703
|
+
return hostedGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, timeoutMs, agentKind);
|
|
1704
|
+
}
|
|
1606
1705
|
// BYOK grading mode routes the grade through an LLM API instead of the
|
|
1607
1706
|
// on-device channel worker pool. The grader prompt + parseVerdict are shared.
|
|
1608
1707
|
if ((process.env.SYNKRO_GRADING_MODE || 'local') === 'byok') {
|
|
@@ -1618,6 +1717,9 @@ export async function localGrade(surface: string, prompt: string, timeoutMs = 24
|
|
|
1618
1717
|
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code', timeoutMs = 24000): Promise<string> {
|
|
1619
1718
|
const jwt = loadJwt();
|
|
1620
1719
|
if (!jwt) throw new Error('NO_JWT');
|
|
1720
|
+
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
1721
|
+
return hostedGrade('grade-cwe', prompt, jwt, timeoutMs, agentKind);
|
|
1722
|
+
}
|
|
1621
1723
|
return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
|
|
1622
1724
|
}
|
|
1623
1725
|
|
|
@@ -7491,7 +7593,7 @@ echo ""
|
|
|
7491
7593
|
wait $MITM_PID
|
|
7492
7594
|
`;
|
|
7493
7595
|
}
|
|
7494
|
-
async function runClaudeDesktopTap() {
|
|
7596
|
+
async function runClaudeDesktopTap(opts = {}) {
|
|
7495
7597
|
if (process.platform !== "darwin") {
|
|
7496
7598
|
console.log(" Claude Desktop observability is macOS-only \u2014 skipped.");
|
|
7497
7599
|
return;
|
|
@@ -7505,6 +7607,20 @@ async function runClaudeDesktopTap() {
|
|
|
7505
7607
|
console.log(" Could not read local auth token (~/.synkro/.mcp-jwt). Run `synkro install` first.");
|
|
7506
7608
|
return;
|
|
7507
7609
|
}
|
|
7610
|
+
if (opts.backfill) {
|
|
7611
|
+
console.log(" History backfill enabled \u2014 importing past conversations once this session.");
|
|
7612
|
+
}
|
|
7613
|
+
try {
|
|
7614
|
+
const mcp = installClaudeDesktopMcpConfig();
|
|
7615
|
+
if (mcp.ok) {
|
|
7616
|
+
console.log(` \u2713 Synkro standards MCP registered in Claude Desktop`);
|
|
7617
|
+
console.log(` (${mcp.path}) \u2014 restart Claude Desktop to load the synkro-guardrails tools.`);
|
|
7618
|
+
} else {
|
|
7619
|
+
console.log(` \u26A0 Claude Desktop MCP not registered: ${mcp.reason}`);
|
|
7620
|
+
}
|
|
7621
|
+
} catch (err) {
|
|
7622
|
+
console.log(` \u26A0 Could not register Claude Desktop MCP: ${err.message}`);
|
|
7623
|
+
}
|
|
7508
7624
|
const cdRoot = join5(homedir5(), ".synkro", "cd-sessions");
|
|
7509
7625
|
mkdirSync6(cdRoot, { recursive: true });
|
|
7510
7626
|
const sessionDir = mkdtempSync(join5(cdRoot, "synkro-cd-"));
|
|
@@ -7516,7 +7632,7 @@ async function runClaudeDesktopTap() {
|
|
|
7516
7632
|
await new Promise((resolve4) => {
|
|
7517
7633
|
const child = spawn("bash", [runnerPath], {
|
|
7518
7634
|
stdio: "inherit",
|
|
7519
|
-
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH }
|
|
7635
|
+
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_SCAN_URL: SCAN_URL, SYNKRO_SCAN_TURN_URL: SCAN_TURN_URL, SYNKRO_DLP_POLICY_URL: DLP_POLICY_URL, SYNKRO_TURN_VERDICTS_URL: TURN_VERDICTS_URL, SYNKRO_TURN_VERDICT_URL: TURN_VERDICT_URL, SYNKRO_MCP_EVENT_URL: MCP_EVENT_URL, SYNKRO_TAP_TOKEN: token, SYNKRO_TAP_TOKEN_FILE: JWT_PATH, SYNKRO_CD_BACKFILL: opts.backfill ? "1" : "" }
|
|
7520
7636
|
});
|
|
7521
7637
|
const forward = (sig) => {
|
|
7522
7638
|
try {
|
|
@@ -7533,16 +7649,18 @@ async function runClaudeDesktopTap() {
|
|
|
7533
7649
|
});
|
|
7534
7650
|
});
|
|
7535
7651
|
}
|
|
7536
|
-
var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_URL, TURN_VERDICTS_URL, TURN_VERDICT_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7652
|
+
var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_URL, TURN_VERDICTS_URL, TURN_VERDICT_URL, MCP_EVENT_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7537
7653
|
var init_claudeDesktopTap = __esm({
|
|
7538
7654
|
"cli/installer/claudeDesktopTap.ts"() {
|
|
7539
7655
|
"use strict";
|
|
7656
|
+
init_mcpConfig();
|
|
7540
7657
|
CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
|
|
7541
7658
|
SCAN_URL = "http://127.0.0.1:18931/api/local/scan-upload";
|
|
7542
7659
|
SCAN_TURN_URL = "http://127.0.0.1:18931/api/local/scan-turn";
|
|
7543
7660
|
DLP_POLICY_URL = "http://127.0.0.1:18931/api/local/dlp-policy";
|
|
7544
7661
|
TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
|
|
7545
7662
|
TURN_VERDICT_URL = "http://127.0.0.1:18931/api/local/turn-verdict";
|
|
7663
|
+
MCP_EVENT_URL = "http://127.0.0.1:18931/api/local/mcp/event";
|
|
7546
7664
|
JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
|
|
7547
7665
|
ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
|
|
7548
7666
|
from mitmproxy import http
|
|
@@ -7559,6 +7677,11 @@ SCAN_TURN_URL = _safe_url(os.environ.get("SYNKRO_SCAN_TURN_URL", ""))
|
|
|
7559
7677
|
POLICY_URL = _safe_url(os.environ.get("SYNKRO_DLP_POLICY_URL", ""))
|
|
7560
7678
|
TURN_VERDICTS_URL = _safe_url(os.environ.get("SYNKRO_TURN_VERDICTS_URL", ""))
|
|
7561
7679
|
TURN_VERDICT_URL = _safe_url(os.environ.get("SYNKRO_TURN_VERDICT_URL", ""))
|
|
7680
|
+
MCP_EVENT_URL = _safe_url(os.environ.get("SYNKRO_MCP_EVENT_URL", ""))
|
|
7681
|
+
# History backfill is OPT-IN: only when the user runs synkro claude-desktop with
|
|
7682
|
+
# --backfill. Otherwise importing past conversations on every tap launch is wasted
|
|
7683
|
+
# work (and re-floods the feed). When set, it also bypasses the 12h throttle marker.
|
|
7684
|
+
BACKFILL_ENABLED = os.environ.get("SYNKRO_CD_BACKFILL", "") == "1"
|
|
7562
7685
|
# Token file path \u2014 the SERVER_TOKEN rotates whenever the container is recreated,
|
|
7563
7686
|
# so we re-read it from disk on a 401 instead of needing a tap restart.
|
|
7564
7687
|
TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "")
|
|
@@ -7624,6 +7747,151 @@ def _fmt_tool_name(raw):
|
|
|
7624
7747
|
pass
|
|
7625
7748
|
return raw
|
|
7626
7749
|
|
|
7750
|
+
def _map_full_conversation(all_msgs):
|
|
7751
|
+
# Full conversation [{role, content, tool_calls?}] + 'tool' result messages, in
|
|
7752
|
+
# order, for the session-log tables (the Session Timeline card on the event page).
|
|
7753
|
+
# Lighter than the per-turn capture: role + text + tool_calls + tool results.
|
|
7754
|
+
out = []
|
|
7755
|
+
for msg in all_msgs:
|
|
7756
|
+
sender = msg.get("sender", "")
|
|
7757
|
+
role = "user" if sender == "human" else "assistant"
|
|
7758
|
+
blocks = msg.get("content", [])
|
|
7759
|
+
texts = []; tcs = []; results = []
|
|
7760
|
+
if isinstance(blocks, list):
|
|
7761
|
+
for b in blocks:
|
|
7762
|
+
if isinstance(b, str): texts.append(b); continue
|
|
7763
|
+
if not isinstance(b, dict): continue
|
|
7764
|
+
bt = b.get("type", "")
|
|
7765
|
+
if bt == "text":
|
|
7766
|
+
texts.append(b.get("text", ""))
|
|
7767
|
+
elif bt == "tool_use" or bt == "server_tool_use":
|
|
7768
|
+
tcs.append({"name": _fmt_tool_name(b.get("name", "")), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))[:2000]})
|
|
7769
|
+
elif bt.endswith("_tool_result") or bt == "tool_result":
|
|
7770
|
+
rc = b.get("content", "")
|
|
7771
|
+
if isinstance(rc, list):
|
|
7772
|
+
rc = " | ".join(str(x.get("text") or x.get("content") or "") for x in rc if isinstance(x, dict))
|
|
7773
|
+
if rc and str(rc).strip():
|
|
7774
|
+
results.append({"role": "tool", "content": str(rc)[:8000], "name": _fmt_tool_name(b.get("name", ""))})
|
|
7775
|
+
elif isinstance(blocks, str):
|
|
7776
|
+
texts.append(blocks)
|
|
7777
|
+
content = " ".join(t for t in texts if t).strip()
|
|
7778
|
+
if content or tcs:
|
|
7779
|
+
e = {"role": role, "content": content}
|
|
7780
|
+
if tcs: e["tool_calls"] = tcs
|
|
7781
|
+
out.append(e)
|
|
7782
|
+
for r in results: out.append(r)
|
|
7783
|
+
if len(out) >= 2000: break
|
|
7784
|
+
return out
|
|
7785
|
+
|
|
7786
|
+
def _emit_mcp_event(server, tool, ev_type, eid, convo_id=""):
|
|
7787
|
+
# Post a Claude Desktop connector (MCP) lifecycle/tool_call event to the same
|
|
7788
|
+
# mcp_events store the CC/Cursor mcp-gate writes, so Desktop connectors show on
|
|
7789
|
+
# the MCP Servers page. eid is deterministic so re-captures dedupe (server
|
|
7790
|
+
# ON CONFLICT DO NOTHING). Fire-and-forget; never blocks the proxied request.
|
|
7791
|
+
if not (MCP_EVENT_URL and TOKEN and server): return
|
|
7792
|
+
try:
|
|
7793
|
+
payload = json.dumps({
|
|
7794
|
+
"event_id": eid,
|
|
7795
|
+
"session_id": (str(convo_id)[:80] or None),
|
|
7796
|
+
"server_name": str(server)[:120],
|
|
7797
|
+
"event_type": ev_type,
|
|
7798
|
+
"tool_name": (str(tool)[:120] if tool else None),
|
|
7799
|
+
"decision": ("allowed" if ev_type == "tool_call" else None),
|
|
7800
|
+
"repo": "claude-desktop",
|
|
7801
|
+
}).encode("utf-8")
|
|
7802
|
+
_post_json(MCP_EVENT_URL, payload, 5)
|
|
7803
|
+
except Exception:
|
|
7804
|
+
pass
|
|
7805
|
+
|
|
7806
|
+
# \u2500\u2500 History backfill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7807
|
+
# One-time pull of the user's past claude.ai conversations into the local store, so
|
|
7808
|
+
# the dashboard shows full Desktop history (like CC's transcript import) \u2014 not just
|
|
7809
|
+
# turns tapped live. Auth (session cookie + org uuid) is captured from a live request
|
|
7810
|
+
# and kept IN MEMORY (never written to disk). Store-only: backfilled turns are not
|
|
7811
|
+
# graded (backfill=True). Runs at most once per 12h via a timestamp marker.
|
|
7812
|
+
#
|
|
7813
|
+
# SSRF (CWE-918): the only host we ever fetch is claude.ai, and org/conversation ids
|
|
7814
|
+
# must be UUID-shaped \u2014 both validated before any URL is built, so no untrusted value
|
|
7815
|
+
# can redirect the fetch to an arbitrary host/path.
|
|
7816
|
+
_backfill_lock = threading.Lock()
|
|
7817
|
+
_backfill_started = False
|
|
7818
|
+
_BACKFILL_MARKER = os.path.expanduser("~/.synkro/.cd-backfill-done")
|
|
7819
|
+
|
|
7820
|
+
def _is_claude_host(h):
|
|
7821
|
+
return h == "claude.ai" or (isinstance(h, str) and h.endswith(".claude.ai"))
|
|
7822
|
+
|
|
7823
|
+
def _is_uuidish(s):
|
|
7824
|
+
return bool(re.match(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", s or ""))
|
|
7825
|
+
|
|
7826
|
+
def _http_get(url, headers, timeout=25):
|
|
7827
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
7828
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
7829
|
+
return resp.read()
|
|
7830
|
+
|
|
7831
|
+
def _post_backfill_convo(convo, cid):
|
|
7832
|
+
all_msgs = convo.get("chat_messages", [])
|
|
7833
|
+
if not all_msgs: return
|
|
7834
|
+
model = convo.get("model") or "claude-unknown"
|
|
7835
|
+
if not model.startswith("claude-"): model = "claude-" + model
|
|
7836
|
+
session_messages = _map_full_conversation(all_msgs)
|
|
7837
|
+
if not session_messages: return
|
|
7838
|
+
msgs = []
|
|
7839
|
+
for e in session_messages:
|
|
7840
|
+
mm = {"role": e.get("role", "user"), "content": e.get("content", "")}
|
|
7841
|
+
if e.get("tool_calls"): mm["tool_calls"] = e["tool_calls"]
|
|
7842
|
+
msgs.append(mm)
|
|
7843
|
+
# Real conversation timestamp (last activity, fallback created) so the server
|
|
7844
|
+
# stamps created_at to when the convo HAPPENED \u2014 not "now". Without this every
|
|
7845
|
+
# backfill run re-stamps all history to the present and floods it to the top of
|
|
7846
|
+
# the feed. Must be ISO8601; the server re-validates and only honors it for
|
|
7847
|
+
# backfill rows.
|
|
7848
|
+
cts = ""
|
|
7849
|
+
for k in ("updated_at", "created_at"):
|
|
7850
|
+
v = convo.get(k) if isinstance(convo, dict) else None
|
|
7851
|
+
if isinstance(v, str) and re.match(r"^d{4}-d{2}-d{2}T", v):
|
|
7852
|
+
cts = v; break
|
|
7853
|
+
data = {"request": {"model": model, "messages": msgs[-200:]}, "conversation_id": cid,
|
|
7854
|
+
"session_messages": session_messages, "turn_id": "cdbf_" + cid, "backfill": True}
|
|
7855
|
+
if cts: data["event_ts"] = cts
|
|
7856
|
+
_post_json(CAPTURE_URL, json.dumps(data).encode("utf-8"), 8)
|
|
7857
|
+
|
|
7858
|
+
def _backfill(host, org, cookie):
|
|
7859
|
+
if not (_is_claude_host(host) and _is_uuidish(org)): return # defence in depth
|
|
7860
|
+
base = "https://" + host
|
|
7861
|
+
headers = {"Cookie": cookie, "Accept": "application/json", "User-Agent": "synkro-tap"}
|
|
7862
|
+
try:
|
|
7863
|
+
convos = json.loads(_http_get(base + "/api/organizations/" + org + "/chat_conversations?limit=200", headers, 25))
|
|
7864
|
+
if not isinstance(convos, list): return
|
|
7865
|
+
except Exception:
|
|
7866
|
+
with _backfill_lock:
|
|
7867
|
+
globals()["_backfill_started"] = False # allow a later request to retry
|
|
7868
|
+
return
|
|
7869
|
+
print("[synkro] backfill: importing up to " + str(min(len(convos), 200)) + " conversations")
|
|
7870
|
+
n = 0
|
|
7871
|
+
for c in convos[:200]:
|
|
7872
|
+
cid = c.get("uuid", "") if isinstance(c, dict) else ""
|
|
7873
|
+
if not _is_uuidish(cid): continue
|
|
7874
|
+
try:
|
|
7875
|
+
convo = json.loads(_http_get(base + "/api/organizations/" + org + "/chat_conversations/" + cid + "?tree=True&rendering_mode=messages&render_all_tools=true", headers, 25))
|
|
7876
|
+
_post_backfill_convo(convo, cid)
|
|
7877
|
+
n += 1
|
|
7878
|
+
except Exception:
|
|
7879
|
+
pass
|
|
7880
|
+
time.sleep(0.6)
|
|
7881
|
+
try: open(_BACKFILL_MARKER, "w").write(str(int(time.time())))
|
|
7882
|
+
except Exception: pass
|
|
7883
|
+
print("[synkro] backfill done: " + str(n) + " conversations imported")
|
|
7884
|
+
|
|
7885
|
+
def _maybe_backfill(host, org, cookie):
|
|
7886
|
+
# Opt-in only: skip entirely unless the user launched with --backfill.
|
|
7887
|
+
if not BACKFILL_ENABLED: return
|
|
7888
|
+
if not (_is_claude_host(host) and _is_uuidish(org) and cookie and CAPTURE_URL and TOKEN): return
|
|
7889
|
+
with _backfill_lock:
|
|
7890
|
+
global _backfill_started
|
|
7891
|
+
if _backfill_started: return
|
|
7892
|
+
_backfill_started = True
|
|
7893
|
+
threading.Thread(target=_backfill, args=(host, org, cookie), daemon=True).start()
|
|
7894
|
+
|
|
7627
7895
|
# \u2500\u2500 M3 live-hold state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7628
7896
|
# Whether to HOLD connector turns (buffer the live completion until graded).
|
|
7629
7897
|
# Fetched once from the server: True only when a BLOCK-mode rule exists (+ not
|
|
@@ -8094,9 +8362,16 @@ class SynkroTap:
|
|
|
8094
8362
|
return
|
|
8095
8363
|
if verdict.get("block"):
|
|
8096
8364
|
reason = str(verdict.get("reason", "Blocked by Synkro policy"))[:300]
|
|
8365
|
+
msg = "Blocked by Synkro: " + reason
|
|
8366
|
+
# Return a TERMINAL 400 (invalid_request_error), NOT 403. Claude Desktop's
|
|
8367
|
+
# upload client treats 403 as transient/auth and auto-RETRIES it \u2014 that's the
|
|
8368
|
+
# loop the user sees. A 400 invalid_request_error is non-retryable (the SDK/UI
|
|
8369
|
+
# only retry 408/409/429/5xx), so the client stops and surfaces the message.
|
|
8370
|
+
# Anthropic's standard error envelope ({"type":"error","error":{...}}) is what
|
|
8371
|
+
# the UI knows how to render, so the user sees the reason instead of a spinner.
|
|
8097
8372
|
flow.response = http.Response.make(
|
|
8098
|
-
|
|
8099
|
-
json.dumps({"error": {"type": "
|
|
8373
|
+
400,
|
|
8374
|
+
json.dumps({"type": "error", "error": {"type": "invalid_request_error", "message": msg}}).encode(),
|
|
8100
8375
|
{"Content-Type": "application/json"},
|
|
8101
8376
|
)
|
|
8102
8377
|
print("[synkro] BLOCKED upload (size=" + str(payload.get("size", 0)) + " rule=" + str(verdict.get("ruleId", "")) + ")")
|
|
@@ -8173,6 +8448,12 @@ class SynkroTap:
|
|
|
8173
8448
|
_mcp_registry.clear()
|
|
8174
8449
|
_mcp_registry.update(servers)
|
|
8175
8450
|
print("[synkro] MCP servers: " + str(len(servers)))
|
|
8451
|
+
# Surface each connector on the MCP Servers page (connect/disconnect).
|
|
8452
|
+
for sid, s in servers.items():
|
|
8453
|
+
nm = s.get("name", "")
|
|
8454
|
+
if nm:
|
|
8455
|
+
ev = "connect" if s.get("connected") else "disconnect"
|
|
8456
|
+
_emit_mcp_event(nm, "", ev, "cdmcp_" + ev + "_" + str(sid))
|
|
8176
8457
|
except Exception:
|
|
8177
8458
|
pass
|
|
8178
8459
|
return
|
|
@@ -8182,6 +8463,12 @@ class SynkroTap:
|
|
|
8182
8463
|
except: return
|
|
8183
8464
|
m = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
8184
8465
|
convo_id = m.group(1) if m else ""
|
|
8466
|
+
# One-time history backfill using THIS request's session auth (in-memory only).
|
|
8467
|
+
try:
|
|
8468
|
+
_om = re.search(r"/organizations/([0-9a-fA-F-]+)/", r.path)
|
|
8469
|
+
_maybe_backfill(r.pretty_host, _om.group(1) if _om else "", r.headers.get("cookie", "") or r.headers.get("Cookie", ""))
|
|
8470
|
+
except Exception:
|
|
8471
|
+
pass
|
|
8185
8472
|
model = convo.get("model", "unknown")
|
|
8186
8473
|
settings = convo.get("settings") or {}
|
|
8187
8474
|
emt = settings.get("enabled_mcp_tools")
|
|
@@ -8214,7 +8501,14 @@ class SynkroTap:
|
|
|
8214
8501
|
elif bt == "thinking":
|
|
8215
8502
|
thinking += b.get("thinking", "") or b.get("text", "")
|
|
8216
8503
|
elif bt == "tool_use" or bt == "server_tool_use":
|
|
8217
|
-
|
|
8504
|
+
_nm = _fmt_tool_name(b.get("name", ""))
|
|
8505
|
+
tool_calls.append({"name": _nm, "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
|
|
8506
|
+
# A "Server:tool" name means a hosted MCP connector ran \u2192 log it
|
|
8507
|
+
# on the MCP Servers page (":" only present for registry matches,
|
|
8508
|
+
# so built-in tools like web_search are excluded).
|
|
8509
|
+
if ":" in _nm:
|
|
8510
|
+
_srv, _, _tl = _nm.partition(":")
|
|
8511
|
+
_emit_mcp_event(_srv, _tl, "tool_call", "cdmcp_tc_" + (b.get("id", "") or hashlib.sha256((convo_id + _nm).encode()).hexdigest()[:24]), convo_id)
|
|
8218
8512
|
elif bt.endswith("_tool_result") or bt == "tool_result":
|
|
8219
8513
|
rc = b.get("content", "")
|
|
8220
8514
|
if isinstance(rc, list):
|
|
@@ -8251,6 +8545,8 @@ class SynkroTap:
|
|
|
8251
8545
|
messages.append({"role": "user", "content": text})
|
|
8252
8546
|
if not messages: return
|
|
8253
8547
|
data = {"request": {"model": model, "messages": messages}, "conversation_id": convo_id}
|
|
8548
|
+
# Full conversation for the session-log tables (Session Timeline card).
|
|
8549
|
+
data["session_messages"] = _map_full_conversation(all_msgs)
|
|
8254
8550
|
# PER-TURN key: the current turn's assistant message id (same key used for
|
|
8255
8551
|
# dlp_turn_verdicts). Lets the server store each turn as its OWN event so a
|
|
8256
8552
|
# blocked turn isn't overwritten/deleted by a later passing turn.
|
|
@@ -8481,16 +8777,16 @@ __export(macKeychain_exports, {
|
|
|
8481
8777
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
8482
8778
|
writeRefreshAgent: () => writeRefreshAgent
|
|
8483
8779
|
});
|
|
8484
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6
|
|
8780
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6 } from "fs";
|
|
8485
8781
|
import { homedir as homedir6, platform as platform2 } from "os";
|
|
8486
8782
|
import { join as join6 } from "path";
|
|
8487
|
-
import { spawnSync } from "child_process";
|
|
8783
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
8488
8784
|
function needsKeychainBridge() {
|
|
8489
8785
|
return platform2() === "darwin";
|
|
8490
8786
|
}
|
|
8491
8787
|
function readKeychainCreds() {
|
|
8492
8788
|
if (platform2() !== "darwin") return null;
|
|
8493
|
-
const r =
|
|
8789
|
+
const r = spawnSync2("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
|
|
8494
8790
|
encoding: "utf-8",
|
|
8495
8791
|
timeout: 5e3
|
|
8496
8792
|
});
|
|
@@ -8503,7 +8799,14 @@ function exportKeychainCreds() {
|
|
|
8503
8799
|
if (!blob) return null;
|
|
8504
8800
|
mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
|
|
8505
8801
|
chmodSync(CLAUDE_CREDS_DIR, 448);
|
|
8506
|
-
|
|
8802
|
+
let changed = true;
|
|
8803
|
+
try {
|
|
8804
|
+
if (existsSync8(CLAUDE_CREDS_FILE)) {
|
|
8805
|
+
changed = readFileSync6(CLAUDE_CREDS_FILE, "utf-8") !== blob;
|
|
8806
|
+
}
|
|
8807
|
+
} catch {
|
|
8808
|
+
}
|
|
8809
|
+
if (changed) writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
|
|
8507
8810
|
chmodSync(CLAUDE_CREDS_FILE, 384);
|
|
8508
8811
|
return CLAUDE_CREDS_FILE;
|
|
8509
8812
|
}
|
|
@@ -8546,18 +8849,49 @@ async function validateCursorApiKey() {
|
|
|
8546
8849
|
function credsAreStale() {
|
|
8547
8850
|
if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
|
|
8548
8851
|
try {
|
|
8549
|
-
const
|
|
8550
|
-
|
|
8852
|
+
const raw = readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
|
|
8853
|
+
const exp = JSON.parse(raw)?.claudeAiOauth?.expiresAt ?? 0;
|
|
8854
|
+
if (!exp) return true;
|
|
8855
|
+
return Date.now() >= exp - REFRESH_EXPIRY_BUFFER_SECONDS * 1e3;
|
|
8551
8856
|
} catch {
|
|
8552
8857
|
return true;
|
|
8553
8858
|
}
|
|
8554
8859
|
}
|
|
8860
|
+
function xmlEscape(s) {
|
|
8861
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8862
|
+
}
|
|
8555
8863
|
function writeRefreshAgent(synkroBinPath) {
|
|
8556
8864
|
if (platform2() !== "darwin") {
|
|
8557
8865
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
8558
8866
|
}
|
|
8559
8867
|
mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
8560
|
-
|
|
8868
|
+
mkdirSync7(SYNKRO_DIR, { recursive: true });
|
|
8869
|
+
const script = `#!/bin/bash
|
|
8870
|
+
# Generated by synkro (writeRefreshAgent). Expiry-aware Claude creds refresher.
|
|
8871
|
+
# SYNKRO_BIN / SYNKRO_CF are injected by launchd as environment data \u2014 never
|
|
8872
|
+
# interpolated into this script \u2014 so there is no shell-injection surface.
|
|
8873
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
8874
|
+
prev=""
|
|
8875
|
+
while true; do
|
|
8876
|
+
{ "$SYNKRO_BIN" local-cc refresh-creds || synkro local-cc refresh-creds; } >/dev/null 2>&1
|
|
8877
|
+
exp=$(sed -n 's/.*"expiresAt"[: ]*\\([0-9]\\{10,\\}\\).*/\\1/p' "$SYNKRO_CF" 2>/dev/null | head -1)
|
|
8878
|
+
now=$(date +%s)
|
|
8879
|
+
if [ -n "$exp" ]; then
|
|
8880
|
+
delay=$(( exp/1000 - now - ${REFRESH_EXPIRY_BUFFER_SECONDS} ))
|
|
8881
|
+
else
|
|
8882
|
+
delay=${MIN_REFRESH_INTERVAL_SECONDS}
|
|
8883
|
+
fi
|
|
8884
|
+
[ "$delay" -lt ${MIN_REFRESH_INTERVAL_SECONDS} ] && delay=${MIN_REFRESH_INTERVAL_SECONDS}
|
|
8885
|
+
[ "$delay" -gt ${MAX_REFRESH_INTERVAL_SECONDS} ] && delay=${MAX_REFRESH_INTERVAL_SECONDS}
|
|
8886
|
+
if [ "$exp" != "$prev" ]; then
|
|
8887
|
+
echo "[$(date '+%Y-%m-%dT%H:%M:%S')] claude creds refreshed (expiresAt=\${exp:-none}); next check in \${delay}s"
|
|
8888
|
+
prev="$exp"
|
|
8889
|
+
fi
|
|
8890
|
+
sleep "$delay"
|
|
8891
|
+
done
|
|
8892
|
+
`;
|
|
8893
|
+
writeFileSync7(REFRESH_SCRIPT, script, "utf-8");
|
|
8894
|
+
chmodSync(REFRESH_SCRIPT, 448);
|
|
8561
8895
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
8562
8896
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
8563
8897
|
<plist version="1.0">
|
|
@@ -8567,17 +8901,25 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
8567
8901
|
<key>ProgramArguments</key>
|
|
8568
8902
|
<array>
|
|
8569
8903
|
<string>/bin/bash</string>
|
|
8570
|
-
<string
|
|
8571
|
-
<string>${shellCmd}</string>
|
|
8904
|
+
<string>${xmlEscape(REFRESH_SCRIPT)}</string>
|
|
8572
8905
|
</array>
|
|
8573
|
-
<key>
|
|
8574
|
-
<
|
|
8906
|
+
<key>EnvironmentVariables</key>
|
|
8907
|
+
<dict>
|
|
8908
|
+
<key>SYNKRO_BIN</key>
|
|
8909
|
+
<string>${xmlEscape(synkroBinPath)}</string>
|
|
8910
|
+
<key>SYNKRO_CF</key>
|
|
8911
|
+
<string>${xmlEscape(CLAUDE_CREDS_FILE)}</string>
|
|
8912
|
+
</dict>
|
|
8913
|
+
<key>KeepAlive</key>
|
|
8914
|
+
<true/>
|
|
8575
8915
|
<key>RunAtLoad</key>
|
|
8576
8916
|
<true/>
|
|
8917
|
+
<key>ThrottleInterval</key>
|
|
8918
|
+
<integer>${MIN_REFRESH_INTERVAL_SECONDS}</integer>
|
|
8577
8919
|
<key>StandardErrorPath</key>
|
|
8578
|
-
<string>${
|
|
8920
|
+
<string>${xmlEscape(REFRESH_LOG)}</string>
|
|
8579
8921
|
<key>StandardOutPath</key>
|
|
8580
|
-
<string>${
|
|
8922
|
+
<string>${xmlEscape(REFRESH_LOG)}</string>
|
|
8581
8923
|
</dict>
|
|
8582
8924
|
</plist>
|
|
8583
8925
|
`;
|
|
@@ -8586,11 +8928,11 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
8586
8928
|
}
|
|
8587
8929
|
function loadRefreshAgent() {
|
|
8588
8930
|
if (platform2() !== "darwin") return;
|
|
8589
|
-
|
|
8931
|
+
spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8590
8932
|
encoding: "utf-8",
|
|
8591
8933
|
timeout: 5e3
|
|
8592
8934
|
});
|
|
8593
|
-
const r =
|
|
8935
|
+
const r = spawnSync2("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8594
8936
|
encoding: "utf-8",
|
|
8595
8937
|
timeout: 5e3
|
|
8596
8938
|
});
|
|
@@ -8602,14 +8944,14 @@ function loadRefreshAgent() {
|
|
|
8602
8944
|
}
|
|
8603
8945
|
function uninstallRefreshAgent() {
|
|
8604
8946
|
if (platform2() !== "darwin") return;
|
|
8605
|
-
|
|
8947
|
+
spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8606
8948
|
encoding: "utf-8",
|
|
8607
8949
|
timeout: 5e3
|
|
8608
8950
|
});
|
|
8609
8951
|
try {
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8952
|
+
const fs = __require("fs");
|
|
8953
|
+
if (existsSync8(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
|
|
8954
|
+
if (existsSync8(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
|
|
8613
8955
|
} catch {
|
|
8614
8956
|
}
|
|
8615
8957
|
}
|
|
@@ -8624,7 +8966,7 @@ function readExportedCreds() {
|
|
|
8624
8966
|
return null;
|
|
8625
8967
|
}
|
|
8626
8968
|
}
|
|
8627
|
-
var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST,
|
|
8969
|
+
var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_SCRIPT, REFRESH_LOG, REFRESH_EXPIRY_BUFFER_SECONDS, MIN_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, KeychainExportError;
|
|
8628
8970
|
var init_macKeychain = __esm({
|
|
8629
8971
|
"cli/local-cc/macKeychain.ts"() {
|
|
8630
8972
|
"use strict";
|
|
@@ -8636,7 +8978,11 @@ var init_macKeychain = __esm({
|
|
|
8636
8978
|
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
8637
8979
|
LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
|
|
8638
8980
|
LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
8639
|
-
|
|
8981
|
+
REFRESH_SCRIPT = join6(SYNKRO_DIR, "claude-creds-refresh-loop.sh");
|
|
8982
|
+
REFRESH_LOG = join6(SYNKRO_DIR, "claude-creds-refresh.log");
|
|
8983
|
+
REFRESH_EXPIRY_BUFFER_SECONDS = 120;
|
|
8984
|
+
MIN_REFRESH_INTERVAL_SECONDS = 30;
|
|
8985
|
+
MAX_REFRESH_INTERVAL_SECONDS = 5 * 60;
|
|
8640
8986
|
KeychainExportError = class extends Error {
|
|
8641
8987
|
constructor(message, cause) {
|
|
8642
8988
|
super(message);
|
|
@@ -8675,7 +9021,7 @@ __export(dockerInstall_exports, {
|
|
|
8675
9021
|
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
8676
9022
|
import { homedir as homedir7 } from "os";
|
|
8677
9023
|
import { join as join7 } from "path";
|
|
8678
|
-
import { execSync as execSync4, spawnSync as
|
|
9024
|
+
import { execSync as execSync4, spawnSync as spawnSync3 } from "child_process";
|
|
8679
9025
|
function splitWorkers(total, providers) {
|
|
8680
9026
|
const t = Math.max(0, Math.floor(total));
|
|
8681
9027
|
const hasClaude = providers.includes("claude_code");
|
|
@@ -8834,7 +9180,7 @@ function imageTag() {
|
|
|
8834
9180
|
return registry ? `${registry.replace(/\/+$/, "")}/${tag.replace(/^.*\//, "")}` : tag;
|
|
8835
9181
|
}
|
|
8836
9182
|
function assertDockerAvailable() {
|
|
8837
|
-
const v =
|
|
9183
|
+
const v = spawnSync3("docker", ["version", "--format", "{{.Server.Version}}"], {
|
|
8838
9184
|
encoding: "utf-8",
|
|
8839
9185
|
timeout: 5e3
|
|
8840
9186
|
});
|
|
@@ -8849,12 +9195,12 @@ function claudeCredsHostDir() {
|
|
|
8849
9195
|
return join7(homedir7(), ".claude");
|
|
8850
9196
|
}
|
|
8851
9197
|
function resolveSynkroBin() {
|
|
8852
|
-
const which2 =
|
|
9198
|
+
const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
|
|
8853
9199
|
const resolved = (which2.stdout || "").split("\n")[0].trim();
|
|
8854
9200
|
return resolved || "synkro";
|
|
8855
9201
|
}
|
|
8856
9202
|
function sweepHostPglite() {
|
|
8857
|
-
const ps =
|
|
9203
|
+
const ps = spawnSync3("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
|
|
8858
9204
|
if (ps.status !== 0) return;
|
|
8859
9205
|
const targets = [];
|
|
8860
9206
|
for (const line of (ps.stdout || "").split("\n")) {
|
|
@@ -8884,7 +9230,7 @@ function sweepHostPglite() {
|
|
|
8884
9230
|
}
|
|
8885
9231
|
});
|
|
8886
9232
|
if (alive.length === 0) return;
|
|
8887
|
-
|
|
9233
|
+
spawnSync3("sleep", ["0.2"], { timeout: 1e3 });
|
|
8888
9234
|
}
|
|
8889
9235
|
for (const pid of targets) {
|
|
8890
9236
|
try {
|
|
@@ -8936,13 +9282,13 @@ async function dockerInstall(opts = {}) {
|
|
|
8936
9282
|
} else {
|
|
8937
9283
|
mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
|
|
8938
9284
|
}
|
|
8939
|
-
const imageExistsLocally = () =>
|
|
9285
|
+
const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
8940
9286
|
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
8941
9287
|
if (skipPull && imageExistsLocally()) {
|
|
8942
9288
|
console.log(` Using local image ${image} (SYNKRO_SKIP_PULL set \u2014 pull skipped).`);
|
|
8943
9289
|
} else {
|
|
8944
9290
|
console.log(` Pulling ${image}...`);
|
|
8945
|
-
const pull =
|
|
9291
|
+
const pull = spawnSync3("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
|
|
8946
9292
|
if (pull.status !== 0) {
|
|
8947
9293
|
if (imageExistsLocally()) {
|
|
8948
9294
|
console.warn(` \u26A0 docker pull ${image} failed \u2014 falling back to the existing local image.`);
|
|
@@ -8956,7 +9302,7 @@ async function dockerInstall(opts = {}) {
|
|
|
8956
9302
|
console.log(" Stopping existing container gracefully...");
|
|
8957
9303
|
await dockerSafeStop();
|
|
8958
9304
|
}
|
|
8959
|
-
|
|
9305
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
8960
9306
|
sweepHostPglite();
|
|
8961
9307
|
const credsDir = claudeCredsHostDir();
|
|
8962
9308
|
const args2 = [
|
|
@@ -9014,7 +9360,7 @@ async function dockerInstall(opts = {}) {
|
|
|
9014
9360
|
...process.env.SYNKRO_EMAIL ? ["-e", `SYNKRO_EMAIL=${process.env.SYNKRO_EMAIL}`] : [],
|
|
9015
9361
|
image
|
|
9016
9362
|
];
|
|
9017
|
-
const run =
|
|
9363
|
+
const run = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
9018
9364
|
if (run.status !== 0) {
|
|
9019
9365
|
throw new DockerInstallError(`docker run failed (image ${image})`);
|
|
9020
9366
|
}
|
|
@@ -9050,11 +9396,11 @@ async function waitForWorkersReady(timeoutMs = 6e4) {
|
|
|
9050
9396
|
return false;
|
|
9051
9397
|
}
|
|
9052
9398
|
function dockerRemove() {
|
|
9053
|
-
|
|
9399
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
9054
9400
|
}
|
|
9055
9401
|
function dockerStop() {
|
|
9056
|
-
|
|
9057
|
-
|
|
9402
|
+
spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
|
|
9403
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
9058
9404
|
}
|
|
9059
9405
|
async function dockerUpdate(opts = {}) {
|
|
9060
9406
|
if (dockerStatus().running) {
|
|
@@ -9064,7 +9410,7 @@ async function dockerUpdate(opts = {}) {
|
|
|
9064
9410
|
await dockerInstall(opts);
|
|
9065
9411
|
}
|
|
9066
9412
|
function dockerStatus() {
|
|
9067
|
-
const r =
|
|
9413
|
+
const r = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
9068
9414
|
encoding: "utf-8",
|
|
9069
9415
|
timeout: 5e3
|
|
9070
9416
|
});
|
|
@@ -9077,7 +9423,7 @@ function dockerStatus() {
|
|
|
9077
9423
|
};
|
|
9078
9424
|
}
|
|
9079
9425
|
function readContainerConfig() {
|
|
9080
|
-
const r =
|
|
9426
|
+
const r = spawnSync3("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
|
|
9081
9427
|
encoding: "utf-8",
|
|
9082
9428
|
timeout: 5e3
|
|
9083
9429
|
});
|
|
@@ -9131,11 +9477,11 @@ async function dockerSafeStop() {
|
|
|
9131
9477
|
console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
|
|
9132
9478
|
}
|
|
9133
9479
|
console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
|
|
9134
|
-
const stop =
|
|
9480
|
+
const stop = spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
|
|
9135
9481
|
encoding: "utf-8",
|
|
9136
9482
|
timeout: 45e3
|
|
9137
9483
|
});
|
|
9138
|
-
const inspect =
|
|
9484
|
+
const inspect = spawnSync3("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
|
|
9139
9485
|
encoding: "utf-8",
|
|
9140
9486
|
timeout: 5e3
|
|
9141
9487
|
});
|
|
@@ -9159,7 +9505,7 @@ async function dockerSafeStart() {
|
|
|
9159
9505
|
console.log(" Container is already running.");
|
|
9160
9506
|
return { ok: true, pgdataState: "running" };
|
|
9161
9507
|
}
|
|
9162
|
-
const exists =
|
|
9508
|
+
const exists = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
9163
9509
|
encoding: "utf-8",
|
|
9164
9510
|
timeout: 5e3
|
|
9165
9511
|
});
|
|
@@ -9179,7 +9525,7 @@ async function dockerSafeStart() {
|
|
|
9179
9525
|
mkdirSync8(PGDATA_PATH, { recursive: true });
|
|
9180
9526
|
}
|
|
9181
9527
|
console.log(" Starting container...");
|
|
9182
|
-
const start =
|
|
9528
|
+
const start = spawnSync3("docker", ["start", CONTAINER_NAME], {
|
|
9183
9529
|
encoding: "utf-8",
|
|
9184
9530
|
timeout: 3e4
|
|
9185
9531
|
});
|
|
@@ -9247,6 +9593,56 @@ var init_dockerInstall = __esm({
|
|
|
9247
9593
|
}
|
|
9248
9594
|
});
|
|
9249
9595
|
|
|
9596
|
+
// cli/local-cc/setupToken.ts
|
|
9597
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
9598
|
+
import { readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
9599
|
+
import { homedir as homedir8, platform as platform3 } from "os";
|
|
9600
|
+
import { join as join8 } from "path";
|
|
9601
|
+
function captureClaudeSetupToken() {
|
|
9602
|
+
const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
9603
|
+
const isMac = platform3() === "darwin";
|
|
9604
|
+
const bin = "script";
|
|
9605
|
+
const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
|
|
9606
|
+
return new Promise((resolve4, reject) => {
|
|
9607
|
+
const proc = nodeSpawn(bin, args2, { stdio: "inherit" });
|
|
9608
|
+
proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
|
|
9609
|
+
proc.on("close", (code) => {
|
|
9610
|
+
let raw = "";
|
|
9611
|
+
try {
|
|
9612
|
+
raw = readFileSync8(tmpFile, "utf-8");
|
|
9613
|
+
} catch (e) {
|
|
9614
|
+
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
9615
|
+
return;
|
|
9616
|
+
}
|
|
9617
|
+
try {
|
|
9618
|
+
unlinkSync3(tmpFile);
|
|
9619
|
+
} catch {
|
|
9620
|
+
}
|
|
9621
|
+
if (code !== 0) {
|
|
9622
|
+
reject(new Error(`claude setup-token exited with code ${code}`));
|
|
9623
|
+
return;
|
|
9624
|
+
}
|
|
9625
|
+
const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
|
|
9626
|
+
let yellow = "";
|
|
9627
|
+
let m;
|
|
9628
|
+
while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
|
|
9629
|
+
const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
|
|
9630
|
+
if (!token) {
|
|
9631
|
+
reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9634
|
+
resolve4(token[0]);
|
|
9635
|
+
});
|
|
9636
|
+
});
|
|
9637
|
+
}
|
|
9638
|
+
var SYNKRO_DIR3;
|
|
9639
|
+
var init_setupToken = __esm({
|
|
9640
|
+
"cli/local-cc/setupToken.ts"() {
|
|
9641
|
+
"use strict";
|
|
9642
|
+
SYNKRO_DIR3 = join8(homedir8(), ".synkro");
|
|
9643
|
+
}
|
|
9644
|
+
});
|
|
9645
|
+
|
|
9250
9646
|
// cli/commands/setupGithub.ts
|
|
9251
9647
|
var setupGithub_exports = {};
|
|
9252
9648
|
__export(setupGithub_exports, {
|
|
@@ -9255,15 +9651,15 @@ __export(setupGithub_exports, {
|
|
|
9255
9651
|
});
|
|
9256
9652
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
9257
9653
|
import { stdin as input, stdout as output } from "process";
|
|
9258
|
-
import { execSync as execSync5
|
|
9259
|
-
import { existsSync as existsSync10, readFileSync as
|
|
9260
|
-
import { homedir as
|
|
9261
|
-
import { join as
|
|
9654
|
+
import { execSync as execSync5 } from "child_process";
|
|
9655
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
9656
|
+
import { homedir as homedir9, platform as platform4 } from "os";
|
|
9657
|
+
import { join as join9 } from "path";
|
|
9262
9658
|
import { execFile as execFile2 } from "child_process";
|
|
9263
9659
|
function readConfig() {
|
|
9264
9660
|
if (!existsSync10(CONFIG_PATH)) return {};
|
|
9265
9661
|
const out = {};
|
|
9266
|
-
for (const line of
|
|
9662
|
+
for (const line of readFileSync9(CONFIG_PATH, "utf-8").split("\n")) {
|
|
9267
9663
|
const t = line.trim();
|
|
9268
9664
|
if (!t || t.startsWith("#")) continue;
|
|
9269
9665
|
const eq = t.indexOf("=");
|
|
@@ -9300,7 +9696,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
9300
9696
|
return await rl.question(q);
|
|
9301
9697
|
}
|
|
9302
9698
|
function openBrowser3(url) {
|
|
9303
|
-
const os =
|
|
9699
|
+
const os = platform4();
|
|
9304
9700
|
let bin;
|
|
9305
9701
|
let args2;
|
|
9306
9702
|
switch (os) {
|
|
@@ -9323,42 +9719,6 @@ function openBrowser3(url) {
|
|
|
9323
9719
|
function sleep(ms) {
|
|
9324
9720
|
return new Promise((r) => setTimeout(r, ms));
|
|
9325
9721
|
}
|
|
9326
|
-
function captureClaudeSetupToken() {
|
|
9327
|
-
const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
9328
|
-
return new Promise((resolve4, reject) => {
|
|
9329
|
-
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
9330
|
-
stdio: "inherit"
|
|
9331
|
-
});
|
|
9332
|
-
proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
|
|
9333
|
-
proc.on("close", (code) => {
|
|
9334
|
-
let raw = "";
|
|
9335
|
-
try {
|
|
9336
|
-
raw = readFileSync8(tmpFile, "utf-8");
|
|
9337
|
-
} catch (e) {
|
|
9338
|
-
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
9339
|
-
return;
|
|
9340
|
-
}
|
|
9341
|
-
try {
|
|
9342
|
-
unlinkSync3(tmpFile);
|
|
9343
|
-
} catch {
|
|
9344
|
-
}
|
|
9345
|
-
if (code !== 0) {
|
|
9346
|
-
reject(new Error(`claude setup-token exited with code ${code}`));
|
|
9347
|
-
return;
|
|
9348
|
-
}
|
|
9349
|
-
const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
|
|
9350
|
-
let yellow = "";
|
|
9351
|
-
let m;
|
|
9352
|
-
while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
|
|
9353
|
-
const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
|
|
9354
|
-
if (!token) {
|
|
9355
|
-
reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
|
|
9356
|
-
return;
|
|
9357
|
-
}
|
|
9358
|
-
resolve4(token[0]);
|
|
9359
|
-
});
|
|
9360
|
-
});
|
|
9361
|
-
}
|
|
9362
9722
|
async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
|
|
9363
9723
|
const resp = await fetch(`${gatewayUrl}${path}`, {
|
|
9364
9724
|
...opts,
|
|
@@ -9599,14 +9959,15 @@ Will push secrets to ${selected.length} repo(s):`);
|
|
|
9599
9959
|
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
9600
9960
|
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
9601
9961
|
}
|
|
9602
|
-
var
|
|
9962
|
+
var SYNKRO_DIR4, CONFIG_PATH;
|
|
9603
9963
|
var init_setupGithub = __esm({
|
|
9604
9964
|
"cli/commands/setupGithub.ts"() {
|
|
9605
9965
|
"use strict";
|
|
9606
9966
|
init_githubSetup();
|
|
9607
9967
|
init_stub();
|
|
9608
|
-
|
|
9609
|
-
|
|
9968
|
+
init_setupToken();
|
|
9969
|
+
SYNKRO_DIR4 = join9(homedir9(), ".synkro");
|
|
9970
|
+
CONFIG_PATH = join9(SYNKRO_DIR4, "config.env");
|
|
9610
9971
|
}
|
|
9611
9972
|
});
|
|
9612
9973
|
|
|
@@ -9620,16 +9981,16 @@ __export(install_exports, {
|
|
|
9620
9981
|
syncSkillFiles: () => syncSkillFiles,
|
|
9621
9982
|
writeHookScripts: () => writeHookScripts
|
|
9622
9983
|
});
|
|
9623
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as
|
|
9624
|
-
import { homedir as
|
|
9625
|
-
import { join as
|
|
9984
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync } from "fs";
|
|
9985
|
+
import { homedir as homedir10 } from "os";
|
|
9986
|
+
import { join as join10 } from "path";
|
|
9626
9987
|
import { execSync as execSync6 } from "child_process";
|
|
9627
9988
|
import { createInterface as createInterface3 } from "readline";
|
|
9628
9989
|
function resolvePersistedHookMode() {
|
|
9629
9990
|
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
9630
9991
|
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
9631
9992
|
try {
|
|
9632
|
-
const env =
|
|
9993
|
+
const env = readFileSync10(CONFIG_PATH2, "utf-8");
|
|
9633
9994
|
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
9634
9995
|
} catch {
|
|
9635
9996
|
}
|
|
@@ -9649,6 +10010,7 @@ function parseArgs(argv) {
|
|
|
9649
10010
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
9650
10011
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
9651
10012
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
10013
|
+
else if (a === "--byok-gateway") opts.byokGateway = true;
|
|
9652
10014
|
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
9653
10015
|
else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
|
|
9654
10016
|
}
|
|
@@ -9721,25 +10083,12 @@ async function promptCursorApiKey(opts) {
|
|
|
9721
10083
|
console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
|
|
9722
10084
|
}
|
|
9723
10085
|
}
|
|
9724
|
-
async function
|
|
10086
|
+
async function promptDeployLocation() {
|
|
9725
10087
|
if (!process.stdin.isTTY) return "local";
|
|
9726
10088
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9727
10089
|
return new Promise((resolve4) => {
|
|
9728
10090
|
rl.question(
|
|
9729
|
-
"Where should
|
|
9730
|
-
(answer) => {
|
|
9731
|
-
rl.close();
|
|
9732
|
-
resolve4(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
|
|
9733
|
-
}
|
|
9734
|
-
);
|
|
9735
|
-
});
|
|
9736
|
-
}
|
|
9737
|
-
async function promptStorageMode() {
|
|
9738
|
-
if (!process.stdin.isTTY) return "local";
|
|
9739
|
-
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9740
|
-
return new Promise((resolve4) => {
|
|
9741
|
-
rl.question(
|
|
9742
|
-
"Where should telemetry be stored?\n local \u2014 on this machine only (default)\n cloud \u2014 sent to Synkro cloud\nChoose [local] / cloud: ",
|
|
10091
|
+
"Where should Synkro run?\n local \u2014 a grading container on this machine (Docker) (default)\n cloud \u2014 a private container hosted by Synkro (Cloudflare)\nBoth use your own Claude key. Choose [local] / cloud: ",
|
|
9743
10092
|
(answer) => {
|
|
9744
10093
|
rl.close();
|
|
9745
10094
|
resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
|
|
@@ -9778,36 +10127,36 @@ coding patterns and the dashboard shows full history. (Y/n) `
|
|
|
9778
10127
|
}
|
|
9779
10128
|
}
|
|
9780
10129
|
function ensureSynkroDir() {
|
|
9781
|
-
mkdirSync9(
|
|
10130
|
+
mkdirSync9(SYNKRO_DIR5, { recursive: true });
|
|
9782
10131
|
mkdirSync9(HOOKS_DIR, { recursive: true });
|
|
9783
10132
|
mkdirSync9(BIN_DIR, { recursive: true });
|
|
9784
10133
|
mkdirSync9(OFFSETS_DIR, { recursive: true });
|
|
9785
|
-
mkdirSync9(
|
|
10134
|
+
mkdirSync9(join10(SYNKRO_DIR5, "sessions"), { recursive: true });
|
|
9786
10135
|
}
|
|
9787
10136
|
function writeHookScripts(mode = "stub") {
|
|
9788
|
-
const installExtractCorePath =
|
|
9789
|
-
const bashScriptPath =
|
|
9790
|
-
const bashFollowupScriptPath =
|
|
9791
|
-
const editPrecheckScriptPath =
|
|
9792
|
-
const cwePrecheckScriptPath =
|
|
9793
|
-
const cvePrecheckScriptPath =
|
|
9794
|
-
const planJudgeScriptPath =
|
|
9795
|
-
const agentJudgeScriptPath =
|
|
9796
|
-
const stopSummaryScriptPath =
|
|
9797
|
-
const sessionStartScriptPath =
|
|
9798
|
-
const transcriptSyncScriptPath =
|
|
9799
|
-
const userPromptSubmitScriptPath =
|
|
9800
|
-
const commonScriptPath =
|
|
9801
|
-
const commonBashScriptPath =
|
|
9802
|
-
const installScanScriptPath =
|
|
9803
|
-
const cursorBashJudgePath =
|
|
9804
|
-
const cursorEditCapturePath =
|
|
9805
|
-
const cursorAgentCapturePath =
|
|
9806
|
-
const mcpStdioProxyPath =
|
|
9807
|
-
const taskActivateIntentScriptPath =
|
|
9808
|
-
const mcpGateScriptPath =
|
|
10137
|
+
const installExtractCorePath = join10(HOOKS_DIR, "installExtractCore.ts");
|
|
10138
|
+
const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.ts");
|
|
10139
|
+
const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.ts");
|
|
10140
|
+
const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
10141
|
+
const cwePrecheckScriptPath = join10(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
10142
|
+
const cvePrecheckScriptPath = join10(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
10143
|
+
const planJudgeScriptPath = join10(HOOKS_DIR, "cc-plan-judge.ts");
|
|
10144
|
+
const agentJudgeScriptPath = join10(HOOKS_DIR, "cc-agent-judge.ts");
|
|
10145
|
+
const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.ts");
|
|
10146
|
+
const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.ts");
|
|
10147
|
+
const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
10148
|
+
const userPromptSubmitScriptPath = join10(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
10149
|
+
const commonScriptPath = join10(HOOKS_DIR, "_synkro-common.ts");
|
|
10150
|
+
const commonBashScriptPath = join10(HOOKS_DIR, "_synkro-common.sh");
|
|
10151
|
+
const installScanScriptPath = join10(HOOKS_DIR, "cc-install-scan.ts");
|
|
10152
|
+
const cursorBashJudgePath = join10(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
10153
|
+
const cursorEditCapturePath = join10(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
10154
|
+
const cursorAgentCapturePath = join10(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
10155
|
+
const mcpStdioProxyPath = join10(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
10156
|
+
const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
10157
|
+
const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
|
|
9809
10158
|
if (mode === "stub") {
|
|
9810
|
-
const stubCommonPath =
|
|
10159
|
+
const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
9811
10160
|
const stubFiles = [
|
|
9812
10161
|
[stubCommonPath, STUB_COMMON_TS],
|
|
9813
10162
|
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
@@ -9836,7 +10185,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9836
10185
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
9837
10186
|
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
9838
10187
|
try {
|
|
9839
|
-
unlinkSync4(
|
|
10188
|
+
unlinkSync4(join10(HOOKS_DIR, stale));
|
|
9840
10189
|
} catch {
|
|
9841
10190
|
}
|
|
9842
10191
|
}
|
|
@@ -9903,7 +10252,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9903
10252
|
chmodSync2(taskActivateIntentScriptPath, 493);
|
|
9904
10253
|
chmodSync2(mcpGateScriptPath, 493);
|
|
9905
10254
|
try {
|
|
9906
|
-
unlinkSync4(
|
|
10255
|
+
unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
9907
10256
|
} catch {
|
|
9908
10257
|
}
|
|
9909
10258
|
return {
|
|
@@ -9939,7 +10288,7 @@ function resolveSynkroBundle() {
|
|
|
9939
10288
|
return null;
|
|
9940
10289
|
}
|
|
9941
10290
|
function writeConfigEnv(opts) {
|
|
9942
|
-
const credsPath =
|
|
10291
|
+
const credsPath = join10(SYNKRO_DIR5, "credentials.json");
|
|
9943
10292
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
9944
10293
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
9945
10294
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -9955,7 +10304,7 @@ function writeConfigEnv(opts) {
|
|
|
9955
10304
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
9956
10305
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
9957
10306
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
9958
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10307
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.59")}`
|
|
9959
10308
|
];
|
|
9960
10309
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
9961
10310
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -9969,17 +10318,66 @@ function writeConfigEnv(opts) {
|
|
|
9969
10318
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
9970
10319
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
9971
10320
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
10321
|
+
lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
|
|
9972
10322
|
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
9973
10323
|
lines.push("");
|
|
9974
10324
|
writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
9975
10325
|
chmodSync2(CONFIG_PATH2, 384);
|
|
9976
10326
|
}
|
|
10327
|
+
async function provisionCloudContainer(opts) {
|
|
10328
|
+
let setupToken;
|
|
10329
|
+
try {
|
|
10330
|
+
console.log(" Authorize your Claude key for the hosted Synkro worker \u2014");
|
|
10331
|
+
console.log(" a browser window will open for approval...\n");
|
|
10332
|
+
setupToken = await captureClaudeSetupToken();
|
|
10333
|
+
} catch (err) {
|
|
10334
|
+
console.error(` \u2717 Could not capture Claude setup-token: ${err.message}`);
|
|
10335
|
+
console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
|
|
10336
|
+
process.exit(1);
|
|
10337
|
+
}
|
|
10338
|
+
const repo = detectGitRepo2() || void 0;
|
|
10339
|
+
const harness = [];
|
|
10340
|
+
if (opts.hasClaudeCode) harness.push("claude-code");
|
|
10341
|
+
if (opts.hasCursor) harness.push("cursor");
|
|
10342
|
+
const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
10343
|
+
try {
|
|
10344
|
+
const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
|
|
10345
|
+
method: "POST",
|
|
10346
|
+
headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
|
|
10347
|
+
body: JSON.stringify({
|
|
10348
|
+
org_id: opts.orgId,
|
|
10349
|
+
user_id: opts.userId,
|
|
10350
|
+
email: opts.email,
|
|
10351
|
+
harness,
|
|
10352
|
+
claude_workers: total,
|
|
10353
|
+
cursor_workers: 0,
|
|
10354
|
+
connected_repo: repo,
|
|
10355
|
+
ruleset: readFullSynkroFile()?.ruleset || "default",
|
|
10356
|
+
setup_token: setupToken
|
|
10357
|
+
// never logged; gateway stores it as the org secret
|
|
10358
|
+
})
|
|
10359
|
+
});
|
|
10360
|
+
if (!resp.ok) {
|
|
10361
|
+
const t = await resp.text().catch(() => "");
|
|
10362
|
+
throw new Error(`cloud-provision failed (${resp.status}): ${t.slice(0, 200)}`);
|
|
10363
|
+
}
|
|
10364
|
+
const out = await resp.json().catch(() => ({}));
|
|
10365
|
+
console.log(` \u2713 cloud container provisioned${out.container_id ? ` (${out.container_id})` : ""}`);
|
|
10366
|
+
console.log();
|
|
10367
|
+
} catch (err) {
|
|
10368
|
+
console.error(` \u2717 Cloud provisioning failed: ${err.message}`);
|
|
10369
|
+
console.error(" Hooks + MCP are installed; re-run `synkro install` \u2192 cloud to retry.");
|
|
10370
|
+
console.log();
|
|
10371
|
+
} finally {
|
|
10372
|
+
setupToken = "";
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
9977
10375
|
function resolveDeploymentMode() {
|
|
9978
10376
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
9979
10377
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
9980
10378
|
try {
|
|
9981
10379
|
if (existsSync11(CONFIG_PATH2)) {
|
|
9982
|
-
const m =
|
|
10380
|
+
const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
9983
10381
|
const val = m?.[1]?.toLowerCase();
|
|
9984
10382
|
if (val === "bare-host" || val === "docker") return val;
|
|
9985
10383
|
}
|
|
@@ -10006,16 +10404,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
10006
10404
|
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
10007
10405
|
} catch {
|
|
10008
10406
|
}
|
|
10009
|
-
const claudeDir =
|
|
10407
|
+
const claudeDir = join10(homedir10(), ".claude");
|
|
10010
10408
|
try {
|
|
10011
|
-
const settings = JSON.parse(
|
|
10409
|
+
const settings = JSON.parse(readFileSync10(join10(claudeDir, "settings.json"), "utf-8"));
|
|
10012
10410
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
10013
10411
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
10014
10412
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
10015
10413
|
} catch {
|
|
10016
10414
|
}
|
|
10017
10415
|
try {
|
|
10018
|
-
const mcpCache = JSON.parse(
|
|
10416
|
+
const mcpCache = JSON.parse(readFileSync10(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
10019
10417
|
const mcpNames = Object.keys(mcpCache);
|
|
10020
10418
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
10021
10419
|
} catch {
|
|
@@ -10027,10 +10425,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
10027
10425
|
} catch {
|
|
10028
10426
|
}
|
|
10029
10427
|
try {
|
|
10030
|
-
const sessionsDir =
|
|
10428
|
+
const sessionsDir = join10(claudeDir, "sessions");
|
|
10031
10429
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
10032
10430
|
for (const f of files) {
|
|
10033
|
-
const s = JSON.parse(
|
|
10431
|
+
const s = JSON.parse(readFileSync10(join10(sessionsDir, f), "utf-8"));
|
|
10034
10432
|
if (s.version) {
|
|
10035
10433
|
meta.cc_version = meta.cc_version || s.version;
|
|
10036
10434
|
break;
|
|
@@ -10138,6 +10536,7 @@ async function installCommand(opts = {}) {
|
|
|
10138
10536
|
let agents;
|
|
10139
10537
|
let gradingMode;
|
|
10140
10538
|
let storageMode;
|
|
10539
|
+
let deployLocation = "local";
|
|
10141
10540
|
let transcriptCC = false;
|
|
10142
10541
|
let transcriptCursor = false;
|
|
10143
10542
|
if (existingSynkro) {
|
|
@@ -10147,8 +10546,9 @@ async function installCommand(opts = {}) {
|
|
|
10147
10546
|
(a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
|
|
10148
10547
|
);
|
|
10149
10548
|
if (agents.length === 0 && detected.length > 0) agents = detected;
|
|
10549
|
+
deployLocation = existingSynkro.grader.location === "cloud" ? "cloud" : "local";
|
|
10150
10550
|
gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
|
|
10151
|
-
storageMode = "local";
|
|
10551
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
10152
10552
|
console.log(`Using .synkro config:`);
|
|
10153
10553
|
for (const w of existingSynkro.warnings) console.warn(` \u26A0 ${w}`);
|
|
10154
10554
|
console.log(` harness: ${existingSynkro.harness.join(", ")}`);
|
|
@@ -10172,15 +10572,20 @@ async function installCommand(opts = {}) {
|
|
|
10172
10572
|
console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
|
|
10173
10573
|
`);
|
|
10174
10574
|
}
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10575
|
+
if (opts.byokGateway) {
|
|
10576
|
+
gradingMode = "byok";
|
|
10577
|
+
storageMode = "cloud";
|
|
10578
|
+
deployLocation = "local";
|
|
10579
|
+
console.log(" deploy: legacy BYOK-gateway grading (no container)\n");
|
|
10580
|
+
} else {
|
|
10581
|
+
deployLocation = await promptDeployLocation();
|
|
10582
|
+
gradingMode = "local";
|
|
10583
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
10584
|
+
console.log(` deploy: ${deployLocation}
|
|
10178
10585
|
`);
|
|
10179
|
-
if (gradingMode === "byok") {
|
|
10180
|
-
console.log(" BYOK grading uses your own provider key \u2014 register one in the");
|
|
10181
|
-
console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
|
|
10182
10586
|
}
|
|
10183
10587
|
}
|
|
10588
|
+
const target = deployLocation === "cloud" ? "cloud-container" : opts.byokGateway || gradingMode === "byok" ? "gateway-byok" : "local-docker";
|
|
10184
10589
|
{
|
|
10185
10590
|
const wantCC = agents.some((a) => a.kind === "claude_code");
|
|
10186
10591
|
const wantCursor = agents.some((a) => a.kind === "cursor");
|
|
@@ -10205,9 +10610,9 @@ async function installCommand(opts = {}) {
|
|
|
10205
10610
|
const scripts = writeHookScripts(hookMode);
|
|
10206
10611
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
10207
10612
|
for (const mode of ["edit", "bash"]) {
|
|
10208
|
-
const pidFile =
|
|
10613
|
+
const pidFile = join10(SYNKRO_DIR5, "daemon", mode, "daemon.pid");
|
|
10209
10614
|
try {
|
|
10210
|
-
const pid = parseInt(
|
|
10615
|
+
const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
|
|
10211
10616
|
if (pid > 0) {
|
|
10212
10617
|
process.kill(pid, "SIGTERM");
|
|
10213
10618
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -10272,10 +10677,11 @@ async function installCommand(opts = {}) {
|
|
|
10272
10677
|
} catch {
|
|
10273
10678
|
}
|
|
10274
10679
|
const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
|
|
10275
|
-
const
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10680
|
+
const useLocalMcp = target === "local-docker";
|
|
10681
|
+
if (target === "cloud-container") {
|
|
10682
|
+
console.log("Cloud setup \u2014 Synkro will provision a private hosted container for your org.\n");
|
|
10683
|
+
} else if (target === "gateway-byok") {
|
|
10684
|
+
console.log("Cloud-only setup (BYOK grading via gateway) \u2014 skipping the local container.\n");
|
|
10279
10685
|
}
|
|
10280
10686
|
if (hasClaudeCode && !opts.noMcp) {
|
|
10281
10687
|
if (useLocalMcp) {
|
|
@@ -10289,7 +10695,7 @@ async function installCommand(opts = {}) {
|
|
|
10289
10695
|
if (mintResp.ok) {
|
|
10290
10696
|
const minted = await mintResp.json();
|
|
10291
10697
|
mcpJwt = minted.token;
|
|
10292
|
-
writeFileSync8(
|
|
10698
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
10293
10699
|
} else {
|
|
10294
10700
|
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
10295
10701
|
}
|
|
@@ -10317,7 +10723,7 @@ async function installCommand(opts = {}) {
|
|
|
10317
10723
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10318
10724
|
}
|
|
10319
10725
|
const minted = await mintResp.json();
|
|
10320
|
-
writeFileSync8(
|
|
10726
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10321
10727
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10322
10728
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10323
10729
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10334,7 +10740,7 @@ async function installCommand(opts = {}) {
|
|
|
10334
10740
|
if (hasCursor && !opts.noMcp) {
|
|
10335
10741
|
try {
|
|
10336
10742
|
if (useLocalMcp) {
|
|
10337
|
-
const jwtPath =
|
|
10743
|
+
const jwtPath = join10(SYNKRO_DIR5, ".mcp-jwt");
|
|
10338
10744
|
if (!existsSync11(jwtPath)) {
|
|
10339
10745
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
10340
10746
|
method: "POST",
|
|
@@ -10363,7 +10769,7 @@ async function installCommand(opts = {}) {
|
|
|
10363
10769
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10364
10770
|
}
|
|
10365
10771
|
const minted = await mintResp.json();
|
|
10366
|
-
writeFileSync8(
|
|
10772
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10367
10773
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10368
10774
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10369
10775
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10376,7 +10782,7 @@ async function installCommand(opts = {}) {
|
|
|
10376
10782
|
}
|
|
10377
10783
|
const synkroBundle = resolveSynkroBundle();
|
|
10378
10784
|
const persistedMode = resolveDeploymentMode();
|
|
10379
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
|
|
10785
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
|
|
10380
10786
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
10381
10787
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
10382
10788
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -10388,7 +10794,7 @@ async function installCommand(opts = {}) {
|
|
|
10388
10794
|
} catch (err) {
|
|
10389
10795
|
console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
|
|
10390
10796
|
}
|
|
10391
|
-
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
|
|
10797
|
+
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
|
|
10392
10798
|
console.log();
|
|
10393
10799
|
if (useLocalMcp) {
|
|
10394
10800
|
const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
|
|
@@ -10439,7 +10845,7 @@ async function installCommand(opts = {}) {
|
|
|
10439
10845
|
const ready = await waitForContainerReady(6e4);
|
|
10440
10846
|
if (ready) {
|
|
10441
10847
|
console.log(" \u2713 container ready");
|
|
10442
|
-
const mcpJwt =
|
|
10848
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10443
10849
|
try {
|
|
10444
10850
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
10445
10851
|
method: "POST",
|
|
@@ -10469,6 +10875,8 @@ async function installCommand(opts = {}) {
|
|
|
10469
10875
|
process.exit(1);
|
|
10470
10876
|
}
|
|
10471
10877
|
console.log();
|
|
10878
|
+
} else if (target === "cloud-container") {
|
|
10879
|
+
await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
|
|
10472
10880
|
}
|
|
10473
10881
|
if (transcriptConsent) {
|
|
10474
10882
|
const repo = detectGitRepo2();
|
|
@@ -10477,7 +10885,7 @@ async function installCommand(opts = {}) {
|
|
|
10477
10885
|
try {
|
|
10478
10886
|
let mcpToken = "";
|
|
10479
10887
|
try {
|
|
10480
|
-
mcpToken =
|
|
10888
|
+
mcpToken = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10481
10889
|
} catch {
|
|
10482
10890
|
}
|
|
10483
10891
|
if (mcpToken) {
|
|
@@ -10605,11 +11013,11 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10605
11013
|
try {
|
|
10606
11014
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10607
11015
|
if (!root) return;
|
|
10608
|
-
if (root ===
|
|
10609
|
-
const fp =
|
|
11016
|
+
if (root === homedir10()) return;
|
|
11017
|
+
const fp = join10(root, "synkro.toml");
|
|
10610
11018
|
let hasFile = false;
|
|
10611
11019
|
try {
|
|
10612
|
-
hasFile =
|
|
11020
|
+
hasFile = statSync(fp).isFile();
|
|
10613
11021
|
} catch {
|
|
10614
11022
|
}
|
|
10615
11023
|
if (hasFile) {
|
|
@@ -10637,6 +11045,7 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10637
11045
|
"skills = []",
|
|
10638
11046
|
"",
|
|
10639
11047
|
"[grader]",
|
|
11048
|
+
`location = "${opts.deployLocation || "local"}"`,
|
|
10640
11049
|
`pool = "${pool}"`,
|
|
10641
11050
|
`mode = "${mode}"`,
|
|
10642
11051
|
"",
|
|
@@ -10654,9 +11063,9 @@ function readFullSynkroFile() {
|
|
|
10654
11063
|
try {
|
|
10655
11064
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10656
11065
|
if (!root) return null;
|
|
10657
|
-
const fp =
|
|
11066
|
+
const fp = join10(root, "synkro.toml");
|
|
10658
11067
|
if (!existsSync11(fp)) return null;
|
|
10659
|
-
const parsed = parseSynkroToml2(
|
|
11068
|
+
const parsed = parseSynkroToml2(readFileSync10(fp, "utf-8"));
|
|
10660
11069
|
const valid = ["claude-code", "cursor"];
|
|
10661
11070
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
10662
11071
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -10664,7 +11073,8 @@ function readFullSynkroFile() {
|
|
|
10664
11073
|
harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
|
|
10665
11074
|
grader: {
|
|
10666
11075
|
pool: resolved.pool,
|
|
10667
|
-
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
|
|
11076
|
+
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local",
|
|
11077
|
+
location: parsed.grader?.location === "cloud" ? "cloud" : "local"
|
|
10668
11078
|
},
|
|
10669
11079
|
workers: {
|
|
10670
11080
|
...resolved.claudeWorkers != null ? { claude: resolved.claudeWorkers } : {},
|
|
@@ -10692,7 +11102,7 @@ function reconcileHarness() {
|
|
|
10692
11102
|
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
10693
11103
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
10694
11104
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
10695
|
-
const ccSettings =
|
|
11105
|
+
const ccSettings = join10(homedir10(), ".claude", "settings.json");
|
|
10696
11106
|
if (wantCC) {
|
|
10697
11107
|
installCCHooks(ccSettings, {
|
|
10698
11108
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -10712,7 +11122,7 @@ function reconcileHarness() {
|
|
|
10712
11122
|
});
|
|
10713
11123
|
console.log(" \u2713 Claude Code hooks registered");
|
|
10714
11124
|
try {
|
|
10715
|
-
const mcpJwt =
|
|
11125
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10716
11126
|
if (mcpJwt) {
|
|
10717
11127
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
10718
11128
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -10722,8 +11132,9 @@ function reconcileHarness() {
|
|
|
10722
11132
|
} else {
|
|
10723
11133
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
10724
11134
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
11135
|
+
if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
|
|
10725
11136
|
}
|
|
10726
|
-
const cursorHooks =
|
|
11137
|
+
const cursorHooks = join10(homedir10(), ".cursor", "hooks.json");
|
|
10727
11138
|
if (wantCursor) {
|
|
10728
11139
|
installCursorHooks(cursorHooks, {
|
|
10729
11140
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -10777,7 +11188,7 @@ async function syncSkillFiles() {
|
|
|
10777
11188
|
if (resolved.length === 0) return;
|
|
10778
11189
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
10779
11190
|
const tasks = resolved.map((fp) => {
|
|
10780
|
-
const content =
|
|
11191
|
+
const content = readFileSync10(fp, "utf-8");
|
|
10781
11192
|
const source = `skill:${fp.split("/").pop()}`;
|
|
10782
11193
|
if (!content.trim()) {
|
|
10783
11194
|
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
@@ -10823,7 +11234,7 @@ function detectGitRepo2() {
|
|
|
10823
11234
|
function getClaudeProjectsFolder() {
|
|
10824
11235
|
const cwd = process.cwd();
|
|
10825
11236
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
10826
|
-
const projectsDir =
|
|
11237
|
+
const projectsDir = join10(homedir10(), ".claude", "projects", sanitized);
|
|
10827
11238
|
return existsSync11(projectsDir) ? projectsDir : null;
|
|
10828
11239
|
}
|
|
10829
11240
|
function extractSessionInsights(projectsDir) {
|
|
@@ -10831,9 +11242,9 @@ function extractSessionInsights(projectsDir) {
|
|
|
10831
11242
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
10832
11243
|
for (const file of files) {
|
|
10833
11244
|
const sessionId = file.replace(".jsonl", "");
|
|
10834
|
-
const filePath =
|
|
11245
|
+
const filePath = join10(projectsDir, file);
|
|
10835
11246
|
try {
|
|
10836
|
-
const content =
|
|
11247
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10837
11248
|
const lines = content.split("\n").filter(Boolean);
|
|
10838
11249
|
for (let i = 0; i < lines.length; i++) {
|
|
10839
11250
|
try {
|
|
@@ -10912,14 +11323,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
10912
11323
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10913
11324
|
}
|
|
10914
11325
|
function getCursorTranscriptsDir() {
|
|
10915
|
-
const dir =
|
|
11326
|
+
const dir = join10(homedir10(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10916
11327
|
return existsSync11(dir) ? dir : null;
|
|
10917
11328
|
}
|
|
10918
11329
|
function isSafeConvId(id) {
|
|
10919
11330
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
10920
11331
|
}
|
|
10921
11332
|
function parseCursorTranscriptFile(filePath) {
|
|
10922
|
-
const content =
|
|
11333
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10923
11334
|
const lines = content.split("\n").filter(Boolean);
|
|
10924
11335
|
const messages = [];
|
|
10925
11336
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -10951,7 +11362,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10951
11362
|
for (let i = 0; i < convDirs.length; i++) {
|
|
10952
11363
|
const convId = convDirs[i];
|
|
10953
11364
|
if (!isSafeConvId(convId)) continue;
|
|
10954
|
-
const filePath =
|
|
11365
|
+
const filePath = join10(dir, convId, `${convId}.jsonl`);
|
|
10955
11366
|
if (!existsSync11(filePath)) continue;
|
|
10956
11367
|
try {
|
|
10957
11368
|
const all = parseCursorTranscriptFile(filePath);
|
|
@@ -10974,8 +11385,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10974
11385
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
10975
11386
|
}
|
|
10976
11387
|
try {
|
|
10977
|
-
const lc =
|
|
10978
|
-
writeFileSync8(
|
|
11388
|
+
const lc = readFileSync10(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
11389
|
+
writeFileSync8(join10(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
10979
11390
|
} catch {
|
|
10980
11391
|
}
|
|
10981
11392
|
}
|
|
@@ -10983,7 +11394,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10983
11394
|
return { sessions: totalSessions, messages: totalMessages };
|
|
10984
11395
|
}
|
|
10985
11396
|
function parseTranscriptFile(filePath) {
|
|
10986
|
-
const content =
|
|
11397
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10987
11398
|
const lines = content.split("\n").filter(Boolean);
|
|
10988
11399
|
const messages = [];
|
|
10989
11400
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -11031,7 +11442,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
11031
11442
|
for (let i = 0; i < files.length; i++) {
|
|
11032
11443
|
const file = files[i];
|
|
11033
11444
|
const sessionId = file.replace(".jsonl", "");
|
|
11034
|
-
const filePath =
|
|
11445
|
+
const filePath = join10(projectsDir, file);
|
|
11035
11446
|
try {
|
|
11036
11447
|
const allMessages = parseTranscriptFile(filePath);
|
|
11037
11448
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -11053,9 +11464,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
11053
11464
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
11054
11465
|
}
|
|
11055
11466
|
try {
|
|
11056
|
-
const content =
|
|
11467
|
+
const content = readFileSync10(join10(projectsDir, file), "utf-8");
|
|
11057
11468
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
11058
|
-
writeFileSync8(
|
|
11469
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
11059
11470
|
} catch {
|
|
11060
11471
|
}
|
|
11061
11472
|
}
|
|
@@ -11076,7 +11487,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
11076
11487
|
const sessions = [];
|
|
11077
11488
|
for (const file of batch) {
|
|
11078
11489
|
const sessionId = file.replace(".jsonl", "");
|
|
11079
|
-
const filePath =
|
|
11490
|
+
const filePath = join10(projectsDir, file);
|
|
11080
11491
|
try {
|
|
11081
11492
|
const allMessages = parseTranscriptFile(filePath);
|
|
11082
11493
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -11105,18 +11516,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
11105
11516
|
}
|
|
11106
11517
|
for (const file of batch) {
|
|
11107
11518
|
const sessionId = file.replace(".jsonl", "");
|
|
11108
|
-
const filePath =
|
|
11519
|
+
const filePath = join10(projectsDir, file);
|
|
11109
11520
|
try {
|
|
11110
|
-
const content =
|
|
11521
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
11111
11522
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
11112
|
-
writeFileSync8(
|
|
11523
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
11113
11524
|
} catch {
|
|
11114
11525
|
}
|
|
11115
11526
|
}
|
|
11116
11527
|
}
|
|
11117
11528
|
return { sessions: totalSessions, messages: totalMessages };
|
|
11118
11529
|
}
|
|
11119
|
-
var
|
|
11530
|
+
var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
|
|
11120
11531
|
var init_install = __esm({
|
|
11121
11532
|
"cli/commands/install.ts"() {
|
|
11122
11533
|
"use strict";
|
|
@@ -11134,10 +11545,11 @@ var init_install = __esm({
|
|
|
11134
11545
|
init_promptFetcher();
|
|
11135
11546
|
init_claudeDesktopTap();
|
|
11136
11547
|
init_dockerInstall();
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11548
|
+
init_setupToken();
|
|
11549
|
+
SYNKRO_DIR5 = join10(homedir10(), ".synkro");
|
|
11550
|
+
HOOKS_DIR = join10(SYNKRO_DIR5, "hooks");
|
|
11551
|
+
BIN_DIR = join10(SYNKRO_DIR5, "bin");
|
|
11552
|
+
CONFIG_PATH2 = join10(SYNKRO_DIR5, "config.env");
|
|
11141
11553
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
11142
11554
|
import { readFileSync } from 'node:fs';
|
|
11143
11555
|
import { homedir } from 'node:os';
|
|
@@ -11248,15 +11660,15 @@ rl.on('line', async (line) => {
|
|
|
11248
11660
|
}
|
|
11249
11661
|
});
|
|
11250
11662
|
`;
|
|
11251
|
-
OFFSETS_DIR =
|
|
11663
|
+
OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
|
|
11252
11664
|
}
|
|
11253
11665
|
});
|
|
11254
11666
|
|
|
11255
11667
|
// cli/local-cc/install.ts
|
|
11256
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as
|
|
11257
|
-
import { join as
|
|
11258
|
-
import { homedir as
|
|
11259
|
-
import { spawnSync as
|
|
11668
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync11, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
11669
|
+
import { join as join11 } from "path";
|
|
11670
|
+
import { homedir as homedir11 } from "os";
|
|
11671
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
11260
11672
|
function writePluginFiles() {
|
|
11261
11673
|
for (const c of CHANNELS) {
|
|
11262
11674
|
mkdirSync10(c.sessionDir, { recursive: true });
|
|
@@ -11276,7 +11688,7 @@ function writePluginFiles() {
|
|
|
11276
11688
|
}
|
|
11277
11689
|
function runBunInstall() {
|
|
11278
11690
|
for (const c of CHANNELS) {
|
|
11279
|
-
const r =
|
|
11691
|
+
const r = spawnSync5("bun", ["install", "--silent"], {
|
|
11280
11692
|
cwd: c.sessionDir,
|
|
11281
11693
|
encoding: "utf-8",
|
|
11282
11694
|
timeout: 12e4
|
|
@@ -11292,7 +11704,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
11292
11704
|
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
11293
11705
|
return;
|
|
11294
11706
|
}
|
|
11295
|
-
const originalText =
|
|
11707
|
+
const originalText = readFileSync11(CLAUDE_JSON_PATH, "utf-8");
|
|
11296
11708
|
let parsed;
|
|
11297
11709
|
try {
|
|
11298
11710
|
parsed = JSON.parse(originalText);
|
|
@@ -11392,15 +11804,15 @@ function patchClaudeJson() {
|
|
|
11392
11804
|
});
|
|
11393
11805
|
}
|
|
11394
11806
|
function installLocalCC() {
|
|
11395
|
-
let bunCheck =
|
|
11807
|
+
let bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11396
11808
|
if (bunCheck.status !== 0) {
|
|
11397
11809
|
if (process.platform === "darwin") {
|
|
11398
11810
|
console.log(" Installing bun via brew...");
|
|
11399
|
-
const brewR =
|
|
11811
|
+
const brewR = spawnSync5("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
11400
11812
|
if (brewR.status !== 0) {
|
|
11401
11813
|
throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
11402
11814
|
}
|
|
11403
|
-
bunCheck =
|
|
11815
|
+
bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11404
11816
|
if (bunCheck.status !== 0) {
|
|
11405
11817
|
throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
|
|
11406
11818
|
}
|
|
@@ -11434,42 +11846,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
11434
11846
|
var init_install2 = __esm({
|
|
11435
11847
|
"cli/local-cc/install.ts"() {
|
|
11436
11848
|
"use strict";
|
|
11437
|
-
CLAUDE_JSON_BACKUP_PATH =
|
|
11438
|
-
SESSION_DIR =
|
|
11439
|
-
PLUGIN_PATH =
|
|
11440
|
-
PLUGIN_PKG_PATH =
|
|
11441
|
-
PLUGIN_SETTINGS_DIR =
|
|
11442
|
-
PLUGIN_SETTINGS_PATH =
|
|
11443
|
-
PROJECT_MCP_PATH =
|
|
11444
|
-
CLAUDE_JSON_PATH =
|
|
11445
|
-
RUN_SCRIPT_PATH =
|
|
11849
|
+
CLAUDE_JSON_BACKUP_PATH = join11(homedir11(), ".claude.json.synkro-bak");
|
|
11850
|
+
SESSION_DIR = join11(homedir11(), ".synkro", "cc_sessions");
|
|
11851
|
+
PLUGIN_PATH = join11(SESSION_DIR, "synkro-channel.ts");
|
|
11852
|
+
PLUGIN_PKG_PATH = join11(SESSION_DIR, "package.json");
|
|
11853
|
+
PLUGIN_SETTINGS_DIR = join11(SESSION_DIR, ".claude");
|
|
11854
|
+
PLUGIN_SETTINGS_PATH = join11(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
11855
|
+
PROJECT_MCP_PATH = join11(SESSION_DIR, ".mcp.json");
|
|
11856
|
+
CLAUDE_JSON_PATH = join11(homedir11(), ".claude.json");
|
|
11857
|
+
RUN_SCRIPT_PATH = join11(SESSION_DIR, "run-claude.sh");
|
|
11446
11858
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
11447
11859
|
CHANNEL_1_PORT = 8941;
|
|
11448
|
-
SESSION_DIR_2 =
|
|
11449
|
-
PLUGIN_PATH_2 =
|
|
11450
|
-
PLUGIN_PKG_PATH_2 =
|
|
11451
|
-
PLUGIN_SETTINGS_DIR_2 =
|
|
11452
|
-
PLUGIN_SETTINGS_PATH_2 =
|
|
11453
|
-
PROJECT_MCP_PATH_2 =
|
|
11454
|
-
RUN_SCRIPT_PATH_2 =
|
|
11860
|
+
SESSION_DIR_2 = join11(homedir11(), ".synkro", "cc_sessions_2");
|
|
11861
|
+
PLUGIN_PATH_2 = join11(SESSION_DIR_2, "synkro-channel.ts");
|
|
11862
|
+
PLUGIN_PKG_PATH_2 = join11(SESSION_DIR_2, "package.json");
|
|
11863
|
+
PLUGIN_SETTINGS_DIR_2 = join11(SESSION_DIR_2, ".claude");
|
|
11864
|
+
PLUGIN_SETTINGS_PATH_2 = join11(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
11865
|
+
PROJECT_MCP_PATH_2 = join11(SESSION_DIR_2, ".mcp.json");
|
|
11866
|
+
RUN_SCRIPT_PATH_2 = join11(SESSION_DIR_2, "run-claude.sh");
|
|
11455
11867
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
11456
11868
|
CHANNEL_2_PORT = 8951;
|
|
11457
|
-
SESSION_DIR_3 =
|
|
11458
|
-
PLUGIN_PATH_3 =
|
|
11459
|
-
PLUGIN_PKG_PATH_3 =
|
|
11460
|
-
PLUGIN_SETTINGS_DIR_3 =
|
|
11461
|
-
PLUGIN_SETTINGS_PATH_3 =
|
|
11462
|
-
PROJECT_MCP_PATH_3 =
|
|
11463
|
-
RUN_SCRIPT_PATH_3 =
|
|
11869
|
+
SESSION_DIR_3 = join11(homedir11(), ".synkro", "cc_sessions_3");
|
|
11870
|
+
PLUGIN_PATH_3 = join11(SESSION_DIR_3, "synkro-channel.ts");
|
|
11871
|
+
PLUGIN_PKG_PATH_3 = join11(SESSION_DIR_3, "package.json");
|
|
11872
|
+
PLUGIN_SETTINGS_DIR_3 = join11(SESSION_DIR_3, ".claude");
|
|
11873
|
+
PLUGIN_SETTINGS_PATH_3 = join11(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
11874
|
+
PROJECT_MCP_PATH_3 = join11(SESSION_DIR_3, ".mcp.json");
|
|
11875
|
+
RUN_SCRIPT_PATH_3 = join11(SESSION_DIR_3, "run-claude.sh");
|
|
11464
11876
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
11465
11877
|
CHANNEL_3_PORT = 8942;
|
|
11466
|
-
SESSION_DIR_4 =
|
|
11467
|
-
PLUGIN_PATH_4 =
|
|
11468
|
-
PLUGIN_PKG_PATH_4 =
|
|
11469
|
-
PLUGIN_SETTINGS_DIR_4 =
|
|
11470
|
-
PLUGIN_SETTINGS_PATH_4 =
|
|
11471
|
-
PROJECT_MCP_PATH_4 =
|
|
11472
|
-
RUN_SCRIPT_PATH_4 =
|
|
11878
|
+
SESSION_DIR_4 = join11(homedir11(), ".synkro", "cc_sessions_4");
|
|
11879
|
+
PLUGIN_PATH_4 = join11(SESSION_DIR_4, "synkro-channel.ts");
|
|
11880
|
+
PLUGIN_PKG_PATH_4 = join11(SESSION_DIR_4, "package.json");
|
|
11881
|
+
PLUGIN_SETTINGS_DIR_4 = join11(SESSION_DIR_4, ".claude");
|
|
11882
|
+
PLUGIN_SETTINGS_PATH_4 = join11(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
11883
|
+
PROJECT_MCP_PATH_4 = join11(SESSION_DIR_4, ".mcp.json");
|
|
11884
|
+
RUN_SCRIPT_PATH_4 = join11(SESSION_DIR_4, "run-claude.sh");
|
|
11473
11885
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
11474
11886
|
CHANNEL_4_PORT = 8952;
|
|
11475
11887
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -11744,9 +12156,9 @@ __export(disconnect_exports, {
|
|
|
11744
12156
|
disconnectCommand: () => disconnectCommand
|
|
11745
12157
|
});
|
|
11746
12158
|
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11747
|
-
import { homedir as
|
|
11748
|
-
import { join as
|
|
11749
|
-
import { spawnSync as
|
|
12159
|
+
import { homedir as homedir12 } from "os";
|
|
12160
|
+
import { join as join12 } from "path";
|
|
12161
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
11750
12162
|
import { createInterface as createInterface4 } from "readline";
|
|
11751
12163
|
async function tearDownLocalCC() {
|
|
11752
12164
|
const docker = dockerStatus();
|
|
@@ -11760,7 +12172,7 @@ async function tearDownLocalCC() {
|
|
|
11760
12172
|
console.log("\u2713 removed synkro-server container");
|
|
11761
12173
|
try {
|
|
11762
12174
|
const image = imageTag();
|
|
11763
|
-
const r =
|
|
12175
|
+
const r = spawnSync6("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
|
|
11764
12176
|
console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
|
|
11765
12177
|
} catch {
|
|
11766
12178
|
}
|
|
@@ -11819,15 +12231,19 @@ async function disconnectCommand(args2 = []) {
|
|
|
11819
12231
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
11820
12232
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
11821
12233
|
}
|
|
11822
|
-
|
|
12234
|
+
{
|
|
12235
|
+
const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
|
|
12236
|
+
console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
|
|
12237
|
+
}
|
|
12238
|
+
if (existsSync13(SYNKRO_DIR6)) {
|
|
11823
12239
|
if (purge) {
|
|
11824
|
-
rmSync(
|
|
11825
|
-
console.log(`\u2713 wiped ${
|
|
12240
|
+
rmSync(SYNKRO_DIR6, { recursive: true, force: true });
|
|
12241
|
+
console.log(`\u2713 wiped ${SYNKRO_DIR6} entirely \u2014 including all scan data and backups`);
|
|
11826
12242
|
} else {
|
|
11827
|
-
const keep = /* @__PURE__ */ new Set([
|
|
12243
|
+
const keep = /* @__PURE__ */ new Set([join12(SYNKRO_DIR6, "pgdata"), join12(SYNKRO_DIR6, "pgdata-backups")]);
|
|
11828
12244
|
const preserved = [];
|
|
11829
|
-
for (const entry of readdirSync4(
|
|
11830
|
-
const full =
|
|
12245
|
+
for (const entry of readdirSync4(SYNKRO_DIR6)) {
|
|
12246
|
+
const full = join12(SYNKRO_DIR6, entry);
|
|
11831
12247
|
if (keep.has(full)) {
|
|
11832
12248
|
preserved.push(entry);
|
|
11833
12249
|
continue;
|
|
@@ -11835,18 +12251,18 @@ async function disconnectCommand(args2 = []) {
|
|
|
11835
12251
|
rmSync(full, { recursive: true, force: true });
|
|
11836
12252
|
}
|
|
11837
12253
|
if (preserved.length > 0) {
|
|
11838
|
-
console.log(`\u2713 removed Synkro config from ${
|
|
12254
|
+
console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR6} (kept your scan data: ${preserved.join(", ")})`);
|
|
11839
12255
|
console.log(" run `synkro uninstall --purge` to delete that too");
|
|
11840
12256
|
} else {
|
|
11841
|
-
console.log(`\u2713 removed ${
|
|
12257
|
+
console.log(`\u2713 removed ${SYNKRO_DIR6}`);
|
|
11842
12258
|
}
|
|
11843
12259
|
}
|
|
11844
12260
|
} else {
|
|
11845
|
-
console.log(`\xB7 ${
|
|
12261
|
+
console.log(`\xB7 ${SYNKRO_DIR6} already gone`);
|
|
11846
12262
|
}
|
|
11847
12263
|
console.log(purge ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
|
|
11848
12264
|
}
|
|
11849
|
-
var
|
|
12265
|
+
var SYNKRO_DIR6;
|
|
11850
12266
|
var init_disconnect = __esm({
|
|
11851
12267
|
"cli/commands/disconnect.ts"() {
|
|
11852
12268
|
"use strict";
|
|
@@ -11857,14 +12273,14 @@ var init_disconnect = __esm({
|
|
|
11857
12273
|
init_install2();
|
|
11858
12274
|
init_dockerInstall();
|
|
11859
12275
|
init_macKeychain();
|
|
11860
|
-
|
|
12276
|
+
SYNKRO_DIR6 = join12(homedir12(), ".synkro");
|
|
11861
12277
|
}
|
|
11862
12278
|
});
|
|
11863
12279
|
|
|
11864
12280
|
// cli/local-cc/turnLog.ts
|
|
11865
|
-
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as
|
|
11866
|
-
import { dirname as dirname6, join as
|
|
11867
|
-
import { homedir as
|
|
12281
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as readFileSync12, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
12282
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
12283
|
+
import { homedir as homedir13 } from "os";
|
|
11868
12284
|
function truncate(s, max = PREVIEW_MAX) {
|
|
11869
12285
|
if (s.length <= max) return s;
|
|
11870
12286
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -11902,9 +12318,9 @@ function appendTurn(args2) {
|
|
|
11902
12318
|
function readRecentTurns(n = 20) {
|
|
11903
12319
|
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
11904
12320
|
try {
|
|
11905
|
-
const size =
|
|
12321
|
+
const size = statSync2(TURN_LOG_PATH).size;
|
|
11906
12322
|
if (size === 0) return [];
|
|
11907
|
-
const text =
|
|
12323
|
+
const text = readFileSync12(TURN_LOG_PATH, "utf-8");
|
|
11908
12324
|
const lines = text.split("\n").filter(Boolean);
|
|
11909
12325
|
const lastN = lines.slice(-n).reverse();
|
|
11910
12326
|
return lastN.map((line) => {
|
|
@@ -11928,7 +12344,7 @@ function followTurns(onEntry) {
|
|
|
11928
12344
|
}
|
|
11929
12345
|
let lastSize = (() => {
|
|
11930
12346
|
try {
|
|
11931
|
-
return
|
|
12347
|
+
return statSync2(TURN_LOG_PATH).size;
|
|
11932
12348
|
} catch {
|
|
11933
12349
|
return 0;
|
|
11934
12350
|
}
|
|
@@ -11983,7 +12399,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
11983
12399
|
var init_turnLog = __esm({
|
|
11984
12400
|
"cli/local-cc/turnLog.ts"() {
|
|
11985
12401
|
"use strict";
|
|
11986
|
-
TURN_LOG_PATH =
|
|
12402
|
+
TURN_LOG_PATH = join13(homedir13(), ".synkro", "cc_sessions", "turns.log");
|
|
11987
12403
|
PREVIEW_MAX = 400;
|
|
11988
12404
|
}
|
|
11989
12405
|
});
|
|
@@ -12132,19 +12548,19 @@ var init_grade = __esm({
|
|
|
12132
12548
|
});
|
|
12133
12549
|
|
|
12134
12550
|
// cli/local-cc/pueue.ts
|
|
12135
|
-
import { execFileSync as execFileSync3, spawnSync as
|
|
12136
|
-
import { homedir as
|
|
12137
|
-
import { join as
|
|
12551
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync7, spawn as spawn2 } from "child_process";
|
|
12552
|
+
import { homedir as homedir14 } from "os";
|
|
12553
|
+
import { join as join14 } from "path";
|
|
12138
12554
|
import { connect as connect2 } from "net";
|
|
12139
12555
|
function pueueAvailable() {
|
|
12140
|
-
const r =
|
|
12556
|
+
const r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12141
12557
|
if (r.status !== 0) {
|
|
12142
12558
|
throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
|
|
12143
12559
|
}
|
|
12144
12560
|
}
|
|
12145
12561
|
function statusJson() {
|
|
12146
12562
|
pueueAvailable();
|
|
12147
|
-
const r =
|
|
12563
|
+
const r = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8" });
|
|
12148
12564
|
if (r.status !== 0) {
|
|
12149
12565
|
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
12150
12566
|
}
|
|
@@ -12189,18 +12605,18 @@ function startTask(opts = {}) {
|
|
|
12189
12605
|
let existing = findTask(ch);
|
|
12190
12606
|
while (existing) {
|
|
12191
12607
|
if (existing.status === "Running" || existing.status === "Queued") {
|
|
12192
|
-
|
|
12193
|
-
|
|
12608
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
12609
|
+
spawnSync7("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
12194
12610
|
for (let i = 0; i < 10; i++) {
|
|
12195
12611
|
const check = findTask(ch);
|
|
12196
12612
|
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
12197
|
-
|
|
12613
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
12198
12614
|
}
|
|
12199
12615
|
}
|
|
12200
|
-
|
|
12616
|
+
spawnSync7("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
12201
12617
|
existing = findTask(ch);
|
|
12202
12618
|
}
|
|
12203
|
-
const runScript =
|
|
12619
|
+
const runScript = join14(cwd, "run-claude.sh");
|
|
12204
12620
|
const args2 = [
|
|
12205
12621
|
"add",
|
|
12206
12622
|
"--label",
|
|
@@ -12211,7 +12627,7 @@ function startTask(opts = {}) {
|
|
|
12211
12627
|
"bash",
|
|
12212
12628
|
runScript
|
|
12213
12629
|
];
|
|
12214
|
-
const r =
|
|
12630
|
+
const r = spawnSync7("pueue", args2, { encoding: "utf-8" });
|
|
12215
12631
|
if (r.status !== 0) {
|
|
12216
12632
|
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
12217
12633
|
}
|
|
@@ -12222,25 +12638,25 @@ function startTask(opts = {}) {
|
|
|
12222
12638
|
return created;
|
|
12223
12639
|
}
|
|
12224
12640
|
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
12225
|
-
|
|
12641
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
12226
12642
|
let t = findTask(channel);
|
|
12227
12643
|
while (t) {
|
|
12228
12644
|
if (t.status === "Running" || t.status === "Queued") {
|
|
12229
|
-
|
|
12645
|
+
spawnSync7("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
12230
12646
|
for (let i = 0; i < 10; i++) {
|
|
12231
12647
|
const check = findTask(channel);
|
|
12232
12648
|
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
12233
|
-
|
|
12649
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
12234
12650
|
}
|
|
12235
12651
|
}
|
|
12236
|
-
|
|
12652
|
+
spawnSync7("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
12237
12653
|
t = findTask(channel);
|
|
12238
12654
|
}
|
|
12239
12655
|
}
|
|
12240
12656
|
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
12241
12657
|
const t = findTask(channel);
|
|
12242
12658
|
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
12243
|
-
const r =
|
|
12659
|
+
const r = spawnSync7("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
12244
12660
|
return r.stdout || r.stderr || "(no output)";
|
|
12245
12661
|
}
|
|
12246
12662
|
function ensureRunning(opts = {}) {
|
|
@@ -12265,8 +12681,8 @@ function probePort(host, port, timeoutMs = 500) {
|
|
|
12265
12681
|
});
|
|
12266
12682
|
}
|
|
12267
12683
|
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
12268
|
-
|
|
12269
|
-
|
|
12684
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
12685
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
12270
12686
|
}
|
|
12271
12687
|
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
12272
12688
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -12278,46 +12694,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
|
|
|
12278
12694
|
return probePort(host, port);
|
|
12279
12695
|
}
|
|
12280
12696
|
function brewInstall(pkg) {
|
|
12281
|
-
const brew =
|
|
12697
|
+
const brew = spawnSync7("brew", ["--version"], { encoding: "utf-8" });
|
|
12282
12698
|
if (brew.status !== 0) return false;
|
|
12283
12699
|
console.log(` Installing ${pkg} via brew...`);
|
|
12284
|
-
const r =
|
|
12700
|
+
const r = spawnSync7("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
12285
12701
|
return r.status === 0;
|
|
12286
12702
|
}
|
|
12287
12703
|
function assertPueueInstalled() {
|
|
12288
|
-
let r =
|
|
12704
|
+
let r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12289
12705
|
if (r.status !== 0) {
|
|
12290
12706
|
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
12291
|
-
r =
|
|
12707
|
+
r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12292
12708
|
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
12293
12709
|
} else {
|
|
12294
12710
|
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
12295
12711
|
}
|
|
12296
12712
|
}
|
|
12297
|
-
const status =
|
|
12713
|
+
const status = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12298
12714
|
if (status.status !== 0) {
|
|
12299
12715
|
console.log(" Starting pueued daemon...");
|
|
12300
12716
|
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
12301
12717
|
child.unref();
|
|
12302
|
-
|
|
12303
|
-
const retry =
|
|
12718
|
+
spawnSync7("sleep", ["1"]);
|
|
12719
|
+
const retry = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12304
12720
|
if (retry.status !== 0) {
|
|
12305
12721
|
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
12306
12722
|
}
|
|
12307
12723
|
}
|
|
12308
|
-
|
|
12724
|
+
spawnSync7("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
12309
12725
|
}
|
|
12310
12726
|
function assertClaudeInstalled() {
|
|
12311
|
-
const r =
|
|
12727
|
+
const r = spawnSync7("claude", ["--version"], { encoding: "utf-8" });
|
|
12312
12728
|
if (r.status !== 0) {
|
|
12313
12729
|
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
12314
12730
|
}
|
|
12315
12731
|
}
|
|
12316
12732
|
function assertTmuxInstalled() {
|
|
12317
|
-
let r =
|
|
12733
|
+
let r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12318
12734
|
if (r.status !== 0) {
|
|
12319
12735
|
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
12320
|
-
r =
|
|
12736
|
+
r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12321
12737
|
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
12322
12738
|
} else {
|
|
12323
12739
|
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
@@ -12330,12 +12746,12 @@ var init_pueue = __esm({
|
|
|
12330
12746
|
"use strict";
|
|
12331
12747
|
TASK_LABEL = "synkro-local-cc";
|
|
12332
12748
|
TMUX_SESSION = "synkro-local-cc";
|
|
12333
|
-
SESSION_DIR2 =
|
|
12749
|
+
SESSION_DIR2 = join14(homedir14(), ".synkro", "cc_sessions");
|
|
12334
12750
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
12335
12751
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
12336
|
-
SESSION_DIR_22 =
|
|
12337
|
-
SESSION_DIR_32 =
|
|
12338
|
-
SESSION_DIR_42 =
|
|
12752
|
+
SESSION_DIR_22 = join14(homedir14(), ".synkro", "cc_sessions_2");
|
|
12753
|
+
SESSION_DIR_32 = join14(homedir14(), ".synkro", "cc_sessions_3");
|
|
12754
|
+
SESSION_DIR_42 = join14(homedir14(), ".synkro", "cc_sessions_4");
|
|
12339
12755
|
PueueError = class extends Error {
|
|
12340
12756
|
constructor(message, cause) {
|
|
12341
12757
|
super(message);
|
|
@@ -12350,13 +12766,13 @@ var init_pueue = __esm({
|
|
|
12350
12766
|
});
|
|
12351
12767
|
|
|
12352
12768
|
// cli/local-cc/settings.ts
|
|
12353
|
-
import { existsSync as existsSync15, readFileSync as
|
|
12354
|
-
import { homedir as
|
|
12355
|
-
import { join as
|
|
12769
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
12770
|
+
import { homedir as homedir15 } from "os";
|
|
12771
|
+
import { join as join15 } from "path";
|
|
12356
12772
|
function isLocalCCEnabled() {
|
|
12357
12773
|
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
12358
12774
|
try {
|
|
12359
|
-
const content =
|
|
12775
|
+
const content = readFileSync13(CONFIG_PATH3, "utf-8");
|
|
12360
12776
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
12361
12777
|
return match?.[1] === "yes";
|
|
12362
12778
|
} catch {
|
|
@@ -12367,7 +12783,7 @@ var CONFIG_PATH3;
|
|
|
12367
12783
|
var init_settings = __esm({
|
|
12368
12784
|
"cli/local-cc/settings.ts"() {
|
|
12369
12785
|
"use strict";
|
|
12370
|
-
CONFIG_PATH3 =
|
|
12786
|
+
CONFIG_PATH3 = join15(homedir15(), ".synkro", "config.env");
|
|
12371
12787
|
}
|
|
12372
12788
|
});
|
|
12373
12789
|
|
|
@@ -12376,11 +12792,11 @@ var localCc_exports = {};
|
|
|
12376
12792
|
__export(localCc_exports, {
|
|
12377
12793
|
localCcCommand: () => localCcCommand
|
|
12378
12794
|
});
|
|
12379
|
-
import { spawnSync as
|
|
12380
|
-
import { homedir as
|
|
12381
|
-
import { join as
|
|
12795
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
12796
|
+
import { homedir as homedir16 } from "os";
|
|
12797
|
+
import { join as join16 } from "path";
|
|
12382
12798
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
12383
|
-
import { existsSync as existsSync16, readFileSync as
|
|
12799
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
12384
12800
|
function deploymentMode() {
|
|
12385
12801
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
12386
12802
|
if (env === "docker") return "docker";
|
|
@@ -12487,14 +12903,14 @@ TROUBLESHOOTING
|
|
|
12487
12903
|
}
|
|
12488
12904
|
function readGatewayUrl() {
|
|
12489
12905
|
if (existsSync16(CONFIG_PATH4)) {
|
|
12490
|
-
const m =
|
|
12906
|
+
const m = readFileSync14(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
12491
12907
|
if (m) return m[1];
|
|
12492
12908
|
}
|
|
12493
12909
|
return "https://api.synkro.sh";
|
|
12494
12910
|
}
|
|
12495
12911
|
function updateLocalInferenceFlag(enabled) {
|
|
12496
12912
|
if (!existsSync16(CONFIG_PATH4)) return;
|
|
12497
|
-
let content =
|
|
12913
|
+
let content = readFileSync14(CONFIG_PATH4, "utf-8");
|
|
12498
12914
|
const flag = enabled ? "yes" : "no";
|
|
12499
12915
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
12500
12916
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -12557,7 +12973,7 @@ async function cmdStatus() {
|
|
|
12557
12973
|
}
|
|
12558
12974
|
const ch1Up = await isChannelAvailable();
|
|
12559
12975
|
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
12560
|
-
const tmux1 =
|
|
12976
|
+
const tmux1 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12561
12977
|
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
12562
12978
|
const t2 = findTask(CHANNEL_SECONDARY);
|
|
12563
12979
|
if (!t2) {
|
|
@@ -12567,7 +12983,7 @@ async function cmdStatus() {
|
|
|
12567
12983
|
}
|
|
12568
12984
|
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
12569
12985
|
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
12570
|
-
const tmux2 =
|
|
12986
|
+
const tmux2 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
|
|
12571
12987
|
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
12572
12988
|
}
|
|
12573
12989
|
async function cmdEnable() {
|
|
@@ -12773,7 +13189,7 @@ function cmdLogs(rest) {
|
|
|
12773
13189
|
}
|
|
12774
13190
|
return "200";
|
|
12775
13191
|
})();
|
|
12776
|
-
|
|
13192
|
+
spawnSync8("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
|
|
12777
13193
|
return;
|
|
12778
13194
|
}
|
|
12779
13195
|
for (const arg of rest) {
|
|
@@ -12821,7 +13237,7 @@ function cmdLogs(rest) {
|
|
|
12821
13237
|
function cmdAttach(rest) {
|
|
12822
13238
|
assertTmuxInstalled();
|
|
12823
13239
|
const readonly = rest.some((a) => a === "--readonly" || a === "-r");
|
|
12824
|
-
const has =
|
|
13240
|
+
const has = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12825
13241
|
if (has.status !== 0) {
|
|
12826
13242
|
console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
|
|
12827
13243
|
process.exit(1);
|
|
@@ -12834,7 +13250,7 @@ function cmdAttach(rest) {
|
|
|
12834
13250
|
console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
|
|
12835
13251
|
console.log();
|
|
12836
13252
|
const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
|
|
12837
|
-
const r =
|
|
13253
|
+
const r = spawnSync8("tmux", args2, { stdio: "inherit" });
|
|
12838
13254
|
process.exit(r.status ?? 0);
|
|
12839
13255
|
}
|
|
12840
13256
|
async function cmdTest() {
|
|
@@ -12933,8 +13349,8 @@ var init_localCc = __esm({
|
|
|
12933
13349
|
init_install();
|
|
12934
13350
|
init_client();
|
|
12935
13351
|
init_stub();
|
|
12936
|
-
SYNKRO_CONFIG_PATH =
|
|
12937
|
-
CONFIG_PATH4 =
|
|
13352
|
+
SYNKRO_CONFIG_PATH = join16(homedir16(), ".synkro", "config.env");
|
|
13353
|
+
CONFIG_PATH4 = join16(homedir16(), ".synkro", "config.env");
|
|
12938
13354
|
}
|
|
12939
13355
|
});
|
|
12940
13356
|
|
|
@@ -13045,13 +13461,13 @@ var config_exports = {};
|
|
|
13045
13461
|
__export(config_exports, {
|
|
13046
13462
|
configCommand: () => configCommand
|
|
13047
13463
|
});
|
|
13048
|
-
import { readFileSync as
|
|
13049
|
-
import { join as
|
|
13050
|
-
import { homedir as
|
|
13464
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
13465
|
+
import { join as join17 } from "path";
|
|
13466
|
+
import { homedir as homedir17 } from "os";
|
|
13051
13467
|
function readConfigEnv() {
|
|
13052
13468
|
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
13053
13469
|
const out = {};
|
|
13054
|
-
for (const line of
|
|
13470
|
+
for (const line of readFileSync15(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
13055
13471
|
const t = line.trim();
|
|
13056
13472
|
if (!t || t.startsWith("#")) continue;
|
|
13057
13473
|
const eq = t.indexOf("=");
|
|
@@ -13064,7 +13480,7 @@ function updateConfigValue(key, value) {
|
|
|
13064
13480
|
console.error("No config found. Run `synkro install` first.");
|
|
13065
13481
|
process.exit(1);
|
|
13066
13482
|
}
|
|
13067
|
-
const lines =
|
|
13483
|
+
const lines = readFileSync15(CONFIG_PATH5, "utf-8").split("\n");
|
|
13068
13484
|
const pattern = new RegExp(`^${key}=`);
|
|
13069
13485
|
let found = false;
|
|
13070
13486
|
const updated = lines.map((line) => {
|
|
@@ -13180,25 +13596,25 @@ To change:`);
|
|
|
13180
13596
|
updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
|
|
13181
13597
|
console.log(`\u2713 Inference set to '${inferenceValue}'.`);
|
|
13182
13598
|
}
|
|
13183
|
-
var
|
|
13599
|
+
var SYNKRO_DIR7, CONFIG_PATH5;
|
|
13184
13600
|
var init_config = __esm({
|
|
13185
13601
|
"cli/commands/config.ts"() {
|
|
13186
13602
|
"use strict";
|
|
13187
13603
|
init_stub();
|
|
13188
|
-
|
|
13189
|
-
CONFIG_PATH5 =
|
|
13604
|
+
SYNKRO_DIR7 = join17(homedir17(), ".synkro");
|
|
13605
|
+
CONFIG_PATH5 = join17(SYNKRO_DIR7, "config.env");
|
|
13190
13606
|
}
|
|
13191
13607
|
});
|
|
13192
13608
|
|
|
13193
13609
|
// cli/bootstrap.js
|
|
13194
|
-
import { readFileSync as
|
|
13610
|
+
import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
|
|
13195
13611
|
import { resolve as resolve3 } from "path";
|
|
13196
13612
|
var envCandidates = [
|
|
13197
13613
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
13198
13614
|
];
|
|
13199
13615
|
for (const envPath of envCandidates) {
|
|
13200
13616
|
if (!existsSync18(envPath)) continue;
|
|
13201
|
-
const envContent =
|
|
13617
|
+
const envContent = readFileSync16(envPath, "utf-8");
|
|
13202
13618
|
for (const line of envContent.split("\n")) {
|
|
13203
13619
|
const trimmed = line.trim();
|
|
13204
13620
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -13213,7 +13629,7 @@ var args = process.argv.slice(2);
|
|
|
13213
13629
|
var cmd = args[0] || "";
|
|
13214
13630
|
var subArgs = args.slice(1);
|
|
13215
13631
|
function printVersion() {
|
|
13216
|
-
console.log("1.6.
|
|
13632
|
+
console.log("1.6.59");
|
|
13217
13633
|
}
|
|
13218
13634
|
function printHelp2() {
|
|
13219
13635
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -13327,7 +13743,7 @@ async function main() {
|
|
|
13327
13743
|
}
|
|
13328
13744
|
case "claude-desktop": {
|
|
13329
13745
|
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
13330
|
-
await runClaudeDesktopTap2();
|
|
13746
|
+
await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
|
|
13331
13747
|
break;
|
|
13332
13748
|
}
|
|
13333
13749
|
default: {
|