@synkro-sh/cli 1.6.58 → 1.6.60
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 +861 -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
|
|
|
@@ -9616,20 +9977,21 @@ __export(install_exports, {
|
|
|
9616
9977
|
detectGitRepo: () => detectGitRepo2,
|
|
9617
9978
|
installCommand: () => installCommand,
|
|
9618
9979
|
parseArgs: () => parseArgs,
|
|
9980
|
+
reconcileDeployLocation: () => reconcileDeployLocation,
|
|
9619
9981
|
reconcileHarness: () => reconcileHarness,
|
|
9620
9982
|
syncSkillFiles: () => syncSkillFiles,
|
|
9621
9983
|
writeHookScripts: () => writeHookScripts
|
|
9622
9984
|
});
|
|
9623
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as
|
|
9624
|
-
import { homedir as
|
|
9625
|
-
import { join as
|
|
9985
|
+
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";
|
|
9986
|
+
import { homedir as homedir10 } from "os";
|
|
9987
|
+
import { join as join10 } from "path";
|
|
9626
9988
|
import { execSync as execSync6 } from "child_process";
|
|
9627
9989
|
import { createInterface as createInterface3 } from "readline";
|
|
9628
9990
|
function resolvePersistedHookMode() {
|
|
9629
9991
|
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
9630
9992
|
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
9631
9993
|
try {
|
|
9632
|
-
const env =
|
|
9994
|
+
const env = readFileSync10(CONFIG_PATH2, "utf-8");
|
|
9633
9995
|
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
9634
9996
|
} catch {
|
|
9635
9997
|
}
|
|
@@ -9649,6 +10011,7 @@ function parseArgs(argv) {
|
|
|
9649
10011
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
9650
10012
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
9651
10013
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
10014
|
+
else if (a === "--byok-gateway") opts.byokGateway = true;
|
|
9652
10015
|
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
9653
10016
|
else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
|
|
9654
10017
|
}
|
|
@@ -9721,25 +10084,12 @@ async function promptCursorApiKey(opts) {
|
|
|
9721
10084
|
console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
|
|
9722
10085
|
}
|
|
9723
10086
|
}
|
|
9724
|
-
async function
|
|
10087
|
+
async function promptDeployLocation() {
|
|
9725
10088
|
if (!process.stdin.isTTY) return "local";
|
|
9726
10089
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9727
10090
|
return new Promise((resolve4) => {
|
|
9728
10091
|
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: ",
|
|
10092
|
+
"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
10093
|
(answer) => {
|
|
9744
10094
|
rl.close();
|
|
9745
10095
|
resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
|
|
@@ -9778,36 +10128,36 @@ coding patterns and the dashboard shows full history. (Y/n) `
|
|
|
9778
10128
|
}
|
|
9779
10129
|
}
|
|
9780
10130
|
function ensureSynkroDir() {
|
|
9781
|
-
mkdirSync9(
|
|
10131
|
+
mkdirSync9(SYNKRO_DIR5, { recursive: true });
|
|
9782
10132
|
mkdirSync9(HOOKS_DIR, { recursive: true });
|
|
9783
10133
|
mkdirSync9(BIN_DIR, { recursive: true });
|
|
9784
10134
|
mkdirSync9(OFFSETS_DIR, { recursive: true });
|
|
9785
|
-
mkdirSync9(
|
|
10135
|
+
mkdirSync9(join10(SYNKRO_DIR5, "sessions"), { recursive: true });
|
|
9786
10136
|
}
|
|
9787
10137
|
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 =
|
|
10138
|
+
const installExtractCorePath = join10(HOOKS_DIR, "installExtractCore.ts");
|
|
10139
|
+
const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.ts");
|
|
10140
|
+
const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.ts");
|
|
10141
|
+
const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
10142
|
+
const cwePrecheckScriptPath = join10(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
10143
|
+
const cvePrecheckScriptPath = join10(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
10144
|
+
const planJudgeScriptPath = join10(HOOKS_DIR, "cc-plan-judge.ts");
|
|
10145
|
+
const agentJudgeScriptPath = join10(HOOKS_DIR, "cc-agent-judge.ts");
|
|
10146
|
+
const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.ts");
|
|
10147
|
+
const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.ts");
|
|
10148
|
+
const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
10149
|
+
const userPromptSubmitScriptPath = join10(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
10150
|
+
const commonScriptPath = join10(HOOKS_DIR, "_synkro-common.ts");
|
|
10151
|
+
const commonBashScriptPath = join10(HOOKS_DIR, "_synkro-common.sh");
|
|
10152
|
+
const installScanScriptPath = join10(HOOKS_DIR, "cc-install-scan.ts");
|
|
10153
|
+
const cursorBashJudgePath = join10(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
10154
|
+
const cursorEditCapturePath = join10(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
10155
|
+
const cursorAgentCapturePath = join10(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
10156
|
+
const mcpStdioProxyPath = join10(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
10157
|
+
const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
10158
|
+
const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
|
|
9809
10159
|
if (mode === "stub") {
|
|
9810
|
-
const stubCommonPath =
|
|
10160
|
+
const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
9811
10161
|
const stubFiles = [
|
|
9812
10162
|
[stubCommonPath, STUB_COMMON_TS],
|
|
9813
10163
|
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
@@ -9836,7 +10186,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9836
10186
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
9837
10187
|
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
9838
10188
|
try {
|
|
9839
|
-
unlinkSync4(
|
|
10189
|
+
unlinkSync4(join10(HOOKS_DIR, stale));
|
|
9840
10190
|
} catch {
|
|
9841
10191
|
}
|
|
9842
10192
|
}
|
|
@@ -9903,7 +10253,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9903
10253
|
chmodSync2(taskActivateIntentScriptPath, 493);
|
|
9904
10254
|
chmodSync2(mcpGateScriptPath, 493);
|
|
9905
10255
|
try {
|
|
9906
|
-
unlinkSync4(
|
|
10256
|
+
unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
9907
10257
|
} catch {
|
|
9908
10258
|
}
|
|
9909
10259
|
return {
|
|
@@ -9939,7 +10289,7 @@ function resolveSynkroBundle() {
|
|
|
9939
10289
|
return null;
|
|
9940
10290
|
}
|
|
9941
10291
|
function writeConfigEnv(opts) {
|
|
9942
|
-
const credsPath =
|
|
10292
|
+
const credsPath = join10(SYNKRO_DIR5, "credentials.json");
|
|
9943
10293
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
9944
10294
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
9945
10295
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -9955,7 +10305,7 @@ function writeConfigEnv(opts) {
|
|
|
9955
10305
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
9956
10306
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
9957
10307
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
9958
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10308
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.60")}`
|
|
9959
10309
|
];
|
|
9960
10310
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
9961
10311
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -9969,17 +10319,184 @@ function writeConfigEnv(opts) {
|
|
|
9969
10319
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
9970
10320
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
9971
10321
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
10322
|
+
lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
|
|
9972
10323
|
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
9973
10324
|
lines.push("");
|
|
9974
10325
|
writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
9975
10326
|
chmodSync2(CONFIG_PATH2, 384);
|
|
9976
10327
|
}
|
|
10328
|
+
async function provisionCloudContainer(opts) {
|
|
10329
|
+
let setupToken;
|
|
10330
|
+
try {
|
|
10331
|
+
console.log(" Authorize your Claude key for the hosted Synkro worker \u2014");
|
|
10332
|
+
console.log(" a browser window will open for approval...\n");
|
|
10333
|
+
setupToken = await captureClaudeSetupToken();
|
|
10334
|
+
} catch (err) {
|
|
10335
|
+
console.error(` \u2717 Could not capture Claude setup-token: ${err.message}`);
|
|
10336
|
+
console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
|
|
10337
|
+
process.exit(1);
|
|
10338
|
+
}
|
|
10339
|
+
const repo = detectGitRepo2() || void 0;
|
|
10340
|
+
const harness = [];
|
|
10341
|
+
if (opts.hasClaudeCode) harness.push("claude-code");
|
|
10342
|
+
if (opts.hasCursor) harness.push("cursor");
|
|
10343
|
+
const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
10344
|
+
try {
|
|
10345
|
+
const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
|
|
10346
|
+
method: "POST",
|
|
10347
|
+
headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
|
|
10348
|
+
body: JSON.stringify({
|
|
10349
|
+
org_id: opts.orgId,
|
|
10350
|
+
user_id: opts.userId,
|
|
10351
|
+
email: opts.email,
|
|
10352
|
+
harness,
|
|
10353
|
+
claude_workers: total,
|
|
10354
|
+
cursor_workers: 0,
|
|
10355
|
+
connected_repo: repo,
|
|
10356
|
+
ruleset: readFullSynkroFile()?.ruleset || "default",
|
|
10357
|
+
setup_token: setupToken
|
|
10358
|
+
// never logged; gateway stores it as the org secret
|
|
10359
|
+
})
|
|
10360
|
+
});
|
|
10361
|
+
if (!resp.ok) {
|
|
10362
|
+
const t = await resp.text().catch(() => "");
|
|
10363
|
+
throw new Error(`cloud-provision failed (${resp.status}): ${t.slice(0, 200)}`);
|
|
10364
|
+
}
|
|
10365
|
+
const out = await resp.json().catch(() => ({}));
|
|
10366
|
+
console.log(` \u2713 cloud container provisioned${out.container_id ? ` (${out.container_id})` : ""}`);
|
|
10367
|
+
console.log();
|
|
10368
|
+
} catch (err) {
|
|
10369
|
+
console.error(` \u2717 Cloud provisioning failed: ${err.message}`);
|
|
10370
|
+
console.error(" Hooks + MCP are installed; re-run `synkro install` \u2192 cloud to retry.");
|
|
10371
|
+
console.log();
|
|
10372
|
+
} finally {
|
|
10373
|
+
setupToken = "";
|
|
10374
|
+
}
|
|
10375
|
+
}
|
|
10376
|
+
function readPersistedDeployLocation() {
|
|
10377
|
+
try {
|
|
10378
|
+
if (existsSync11(CONFIG_PATH2)) {
|
|
10379
|
+
const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
|
|
10380
|
+
if (m?.[1] === "cloud") return "cloud";
|
|
10381
|
+
}
|
|
10382
|
+
} catch {
|
|
10383
|
+
}
|
|
10384
|
+
return "local";
|
|
10385
|
+
}
|
|
10386
|
+
function updateConfigEnvLocation(location) {
|
|
10387
|
+
if (!existsSync11(CONFIG_PATH2)) return;
|
|
10388
|
+
let env = readFileSync10(CONFIG_PATH2, "utf-8");
|
|
10389
|
+
const set = (k, v) => {
|
|
10390
|
+
const re = new RegExp(`^${k}=.*$`, "m");
|
|
10391
|
+
const line = `${k}='${v}'`;
|
|
10392
|
+
env = re.test(env) ? env.replace(re, line) : `${env.replace(/\n?$/, "\n")}${line}`;
|
|
10393
|
+
};
|
|
10394
|
+
set("SYNKRO_DEPLOY_LOCATION", location);
|
|
10395
|
+
set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
|
|
10396
|
+
writeFileSync8(CONFIG_PATH2, env, "utf-8");
|
|
10397
|
+
chmodSync2(CONFIG_PATH2, 384);
|
|
10398
|
+
}
|
|
10399
|
+
async function applyMcpConfig(opts) {
|
|
10400
|
+
if (!opts.hasClaudeCode && !opts.hasCursor) return;
|
|
10401
|
+
let mcpJwt = "";
|
|
10402
|
+
try {
|
|
10403
|
+
mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10404
|
+
} catch {
|
|
10405
|
+
}
|
|
10406
|
+
if (!mcpJwt) {
|
|
10407
|
+
try {
|
|
10408
|
+
const r = await fetch(`${opts.gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
10409
|
+
method: "POST",
|
|
10410
|
+
headers: { Authorization: `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
|
|
10411
|
+
body: "{}"
|
|
10412
|
+
});
|
|
10413
|
+
if (r.ok) mcpJwt = (await r.json()).token;
|
|
10414
|
+
else console.warn(` \u26A0 MCP token mint failed (${r.status}) \u2014 run \`synkro install\` to set MCP up.`);
|
|
10415
|
+
} catch {
|
|
10416
|
+
console.warn(" \u26A0 MCP token mint failed (network) \u2014 run `synkro install` to set MCP up.");
|
|
10417
|
+
}
|
|
10418
|
+
}
|
|
10419
|
+
if (opts.hasClaudeCode) {
|
|
10420
|
+
try {
|
|
10421
|
+
const mcp = installMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: mcpJwt, local: opts.local });
|
|
10422
|
+
console.log(` \u2713 Claude Code MCP \u2192 ${mcp.url}`);
|
|
10423
|
+
} catch (err) {
|
|
10424
|
+
console.warn(` \u26A0 Claude Code MCP config failed: ${err.message}`);
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
if (opts.hasCursor) {
|
|
10428
|
+
try {
|
|
10429
|
+
const mcp = installCursorMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: opts.local ? "" : mcpJwt, local: opts.local });
|
|
10430
|
+
console.log(` \u2713 Cursor MCP \u2192 ${mcp.url}`);
|
|
10431
|
+
} catch (err) {
|
|
10432
|
+
console.warn(` \u26A0 Cursor MCP config failed: ${err.message}`);
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
async function reconcileDeployLocation() {
|
|
10437
|
+
const sf = readFullSynkroFile();
|
|
10438
|
+
const desired = sf?.grader.location === "cloud" ? "cloud" : "local";
|
|
10439
|
+
const current = readPersistedDeployLocation();
|
|
10440
|
+
if (desired === current) return desired;
|
|
10441
|
+
const hasClaudeCode = sf ? sf.harness.includes("claude-code") : true;
|
|
10442
|
+
const hasCursor = sf ? sf.harness.includes("cursor") : false;
|
|
10443
|
+
console.log(`
|
|
10444
|
+
synkro.toml: deploy location changed ${current} \u2192 ${desired}
|
|
10445
|
+
`);
|
|
10446
|
+
if (!isAuthenticated()) {
|
|
10447
|
+
console.log(" Opening browser for Synkro auth...");
|
|
10448
|
+
const ok = await authenticate(() => {
|
|
10449
|
+
});
|
|
10450
|
+
if (!ok) {
|
|
10451
|
+
console.error(" \u2717 authentication failed");
|
|
10452
|
+
process.exit(1);
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10455
|
+
await ensureValidToken();
|
|
10456
|
+
const token = getAccessToken();
|
|
10457
|
+
if (!token) {
|
|
10458
|
+
console.error(" \u2717 no access token \u2014 run `synkro login`");
|
|
10459
|
+
process.exit(1);
|
|
10460
|
+
}
|
|
10461
|
+
const gatewayUrl = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL) || "https://api.synkro.sh";
|
|
10462
|
+
try {
|
|
10463
|
+
assertGatewayAllowed(gatewayUrl);
|
|
10464
|
+
} catch (err) {
|
|
10465
|
+
console.error(err.message);
|
|
10466
|
+
process.exit(1);
|
|
10467
|
+
}
|
|
10468
|
+
if (desired === "cloud") {
|
|
10469
|
+
if (dockerStatus().running) {
|
|
10470
|
+
console.log(" Stopping local grader container (snapshotting state first)...");
|
|
10471
|
+
await dockerSafeStop();
|
|
10472
|
+
}
|
|
10473
|
+
dockerRemove();
|
|
10474
|
+
console.log(" \u2713 local container stopped + removed");
|
|
10475
|
+
let orgId, userId, email;
|
|
10476
|
+
try {
|
|
10477
|
+
const info = getUserInfo();
|
|
10478
|
+
userId = info.id;
|
|
10479
|
+
orgId = info.org_id;
|
|
10480
|
+
email = info.email;
|
|
10481
|
+
} catch {
|
|
10482
|
+
}
|
|
10483
|
+
await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
|
|
10484
|
+
updateConfigEnvLocation("cloud");
|
|
10485
|
+
await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: false });
|
|
10486
|
+
console.log(" \u2713 grading + rules/MCP now served from Synkro Cloud\n");
|
|
10487
|
+
} else {
|
|
10488
|
+
updateConfigEnvLocation("local");
|
|
10489
|
+
await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: true });
|
|
10490
|
+
console.log(" \u2713 switched to local \u2014 bringing the local grader + MCP up\n");
|
|
10491
|
+
}
|
|
10492
|
+
return desired;
|
|
10493
|
+
}
|
|
9977
10494
|
function resolveDeploymentMode() {
|
|
9978
10495
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
9979
10496
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
9980
10497
|
try {
|
|
9981
10498
|
if (existsSync11(CONFIG_PATH2)) {
|
|
9982
|
-
const m =
|
|
10499
|
+
const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
9983
10500
|
const val = m?.[1]?.toLowerCase();
|
|
9984
10501
|
if (val === "bare-host" || val === "docker") return val;
|
|
9985
10502
|
}
|
|
@@ -10006,16 +10523,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
10006
10523
|
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
10007
10524
|
} catch {
|
|
10008
10525
|
}
|
|
10009
|
-
const claudeDir =
|
|
10526
|
+
const claudeDir = join10(homedir10(), ".claude");
|
|
10010
10527
|
try {
|
|
10011
|
-
const settings = JSON.parse(
|
|
10528
|
+
const settings = JSON.parse(readFileSync10(join10(claudeDir, "settings.json"), "utf-8"));
|
|
10012
10529
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
10013
10530
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
10014
10531
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
10015
10532
|
} catch {
|
|
10016
10533
|
}
|
|
10017
10534
|
try {
|
|
10018
|
-
const mcpCache = JSON.parse(
|
|
10535
|
+
const mcpCache = JSON.parse(readFileSync10(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
10019
10536
|
const mcpNames = Object.keys(mcpCache);
|
|
10020
10537
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
10021
10538
|
} catch {
|
|
@@ -10027,10 +10544,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
10027
10544
|
} catch {
|
|
10028
10545
|
}
|
|
10029
10546
|
try {
|
|
10030
|
-
const sessionsDir =
|
|
10547
|
+
const sessionsDir = join10(claudeDir, "sessions");
|
|
10031
10548
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
10032
10549
|
for (const f of files) {
|
|
10033
|
-
const s = JSON.parse(
|
|
10550
|
+
const s = JSON.parse(readFileSync10(join10(sessionsDir, f), "utf-8"));
|
|
10034
10551
|
if (s.version) {
|
|
10035
10552
|
meta.cc_version = meta.cc_version || s.version;
|
|
10036
10553
|
break;
|
|
@@ -10138,6 +10655,7 @@ async function installCommand(opts = {}) {
|
|
|
10138
10655
|
let agents;
|
|
10139
10656
|
let gradingMode;
|
|
10140
10657
|
let storageMode;
|
|
10658
|
+
let deployLocation = "local";
|
|
10141
10659
|
let transcriptCC = false;
|
|
10142
10660
|
let transcriptCursor = false;
|
|
10143
10661
|
if (existingSynkro) {
|
|
@@ -10147,8 +10665,9 @@ async function installCommand(opts = {}) {
|
|
|
10147
10665
|
(a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
|
|
10148
10666
|
);
|
|
10149
10667
|
if (agents.length === 0 && detected.length > 0) agents = detected;
|
|
10668
|
+
deployLocation = existingSynkro.grader.location === "cloud" ? "cloud" : "local";
|
|
10150
10669
|
gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
|
|
10151
|
-
storageMode = "local";
|
|
10670
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
10152
10671
|
console.log(`Using .synkro config:`);
|
|
10153
10672
|
for (const w of existingSynkro.warnings) console.warn(` \u26A0 ${w}`);
|
|
10154
10673
|
console.log(` harness: ${existingSynkro.harness.join(", ")}`);
|
|
@@ -10172,15 +10691,20 @@ async function installCommand(opts = {}) {
|
|
|
10172
10691
|
console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
|
|
10173
10692
|
`);
|
|
10174
10693
|
}
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10694
|
+
if (opts.byokGateway) {
|
|
10695
|
+
gradingMode = "byok";
|
|
10696
|
+
storageMode = "cloud";
|
|
10697
|
+
deployLocation = "local";
|
|
10698
|
+
console.log(" deploy: legacy BYOK-gateway grading (no container)\n");
|
|
10699
|
+
} else {
|
|
10700
|
+
deployLocation = await promptDeployLocation();
|
|
10701
|
+
gradingMode = "local";
|
|
10702
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
10703
|
+
console.log(` deploy: ${deployLocation}
|
|
10178
10704
|
`);
|
|
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
10705
|
}
|
|
10183
10706
|
}
|
|
10707
|
+
const target = deployLocation === "cloud" ? "cloud-container" : opts.byokGateway || gradingMode === "byok" ? "gateway-byok" : "local-docker";
|
|
10184
10708
|
{
|
|
10185
10709
|
const wantCC = agents.some((a) => a.kind === "claude_code");
|
|
10186
10710
|
const wantCursor = agents.some((a) => a.kind === "cursor");
|
|
@@ -10205,9 +10729,9 @@ async function installCommand(opts = {}) {
|
|
|
10205
10729
|
const scripts = writeHookScripts(hookMode);
|
|
10206
10730
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
10207
10731
|
for (const mode of ["edit", "bash"]) {
|
|
10208
|
-
const pidFile =
|
|
10732
|
+
const pidFile = join10(SYNKRO_DIR5, "daemon", mode, "daemon.pid");
|
|
10209
10733
|
try {
|
|
10210
|
-
const pid = parseInt(
|
|
10734
|
+
const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
|
|
10211
10735
|
if (pid > 0) {
|
|
10212
10736
|
process.kill(pid, "SIGTERM");
|
|
10213
10737
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -10272,10 +10796,11 @@ async function installCommand(opts = {}) {
|
|
|
10272
10796
|
} catch {
|
|
10273
10797
|
}
|
|
10274
10798
|
const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
|
|
10275
|
-
const
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10799
|
+
const useLocalMcp = target === "local-docker";
|
|
10800
|
+
if (target === "cloud-container") {
|
|
10801
|
+
console.log("Cloud setup \u2014 Synkro will provision a private hosted container for your org.\n");
|
|
10802
|
+
} else if (target === "gateway-byok") {
|
|
10803
|
+
console.log("Cloud-only setup (BYOK grading via gateway) \u2014 skipping the local container.\n");
|
|
10279
10804
|
}
|
|
10280
10805
|
if (hasClaudeCode && !opts.noMcp) {
|
|
10281
10806
|
if (useLocalMcp) {
|
|
@@ -10289,7 +10814,7 @@ async function installCommand(opts = {}) {
|
|
|
10289
10814
|
if (mintResp.ok) {
|
|
10290
10815
|
const minted = await mintResp.json();
|
|
10291
10816
|
mcpJwt = minted.token;
|
|
10292
|
-
writeFileSync8(
|
|
10817
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
10293
10818
|
} else {
|
|
10294
10819
|
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
10295
10820
|
}
|
|
@@ -10317,7 +10842,7 @@ async function installCommand(opts = {}) {
|
|
|
10317
10842
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10318
10843
|
}
|
|
10319
10844
|
const minted = await mintResp.json();
|
|
10320
|
-
writeFileSync8(
|
|
10845
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10321
10846
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10322
10847
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10323
10848
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10334,7 +10859,7 @@ async function installCommand(opts = {}) {
|
|
|
10334
10859
|
if (hasCursor && !opts.noMcp) {
|
|
10335
10860
|
try {
|
|
10336
10861
|
if (useLocalMcp) {
|
|
10337
|
-
const jwtPath =
|
|
10862
|
+
const jwtPath = join10(SYNKRO_DIR5, ".mcp-jwt");
|
|
10338
10863
|
if (!existsSync11(jwtPath)) {
|
|
10339
10864
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
10340
10865
|
method: "POST",
|
|
@@ -10363,7 +10888,7 @@ async function installCommand(opts = {}) {
|
|
|
10363
10888
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10364
10889
|
}
|
|
10365
10890
|
const minted = await mintResp.json();
|
|
10366
|
-
writeFileSync8(
|
|
10891
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10367
10892
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10368
10893
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10369
10894
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10376,7 +10901,7 @@ async function installCommand(opts = {}) {
|
|
|
10376
10901
|
}
|
|
10377
10902
|
const synkroBundle = resolveSynkroBundle();
|
|
10378
10903
|
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 });
|
|
10904
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
|
|
10380
10905
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
10381
10906
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
10382
10907
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -10388,7 +10913,7 @@ async function installCommand(opts = {}) {
|
|
|
10388
10913
|
} catch (err) {
|
|
10389
10914
|
console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
|
|
10390
10915
|
}
|
|
10391
|
-
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
|
|
10916
|
+
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
|
|
10392
10917
|
console.log();
|
|
10393
10918
|
if (useLocalMcp) {
|
|
10394
10919
|
const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
|
|
@@ -10439,7 +10964,7 @@ async function installCommand(opts = {}) {
|
|
|
10439
10964
|
const ready = await waitForContainerReady(6e4);
|
|
10440
10965
|
if (ready) {
|
|
10441
10966
|
console.log(" \u2713 container ready");
|
|
10442
|
-
const mcpJwt =
|
|
10967
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10443
10968
|
try {
|
|
10444
10969
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
10445
10970
|
method: "POST",
|
|
@@ -10469,6 +10994,8 @@ async function installCommand(opts = {}) {
|
|
|
10469
10994
|
process.exit(1);
|
|
10470
10995
|
}
|
|
10471
10996
|
console.log();
|
|
10997
|
+
} else if (target === "cloud-container") {
|
|
10998
|
+
await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
|
|
10472
10999
|
}
|
|
10473
11000
|
if (transcriptConsent) {
|
|
10474
11001
|
const repo = detectGitRepo2();
|
|
@@ -10477,7 +11004,7 @@ async function installCommand(opts = {}) {
|
|
|
10477
11004
|
try {
|
|
10478
11005
|
let mcpToken = "";
|
|
10479
11006
|
try {
|
|
10480
|
-
mcpToken =
|
|
11007
|
+
mcpToken = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10481
11008
|
} catch {
|
|
10482
11009
|
}
|
|
10483
11010
|
if (mcpToken) {
|
|
@@ -10605,11 +11132,11 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10605
11132
|
try {
|
|
10606
11133
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10607
11134
|
if (!root) return;
|
|
10608
|
-
if (root ===
|
|
10609
|
-
const fp =
|
|
11135
|
+
if (root === homedir10()) return;
|
|
11136
|
+
const fp = join10(root, "synkro.toml");
|
|
10610
11137
|
let hasFile = false;
|
|
10611
11138
|
try {
|
|
10612
|
-
hasFile =
|
|
11139
|
+
hasFile = statSync(fp).isFile();
|
|
10613
11140
|
} catch {
|
|
10614
11141
|
}
|
|
10615
11142
|
if (hasFile) {
|
|
@@ -10637,6 +11164,7 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10637
11164
|
"skills = []",
|
|
10638
11165
|
"",
|
|
10639
11166
|
"[grader]",
|
|
11167
|
+
`location = "${opts.deployLocation || "local"}"`,
|
|
10640
11168
|
`pool = "${pool}"`,
|
|
10641
11169
|
`mode = "${mode}"`,
|
|
10642
11170
|
"",
|
|
@@ -10654,9 +11182,9 @@ function readFullSynkroFile() {
|
|
|
10654
11182
|
try {
|
|
10655
11183
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10656
11184
|
if (!root) return null;
|
|
10657
|
-
const fp =
|
|
11185
|
+
const fp = join10(root, "synkro.toml");
|
|
10658
11186
|
if (!existsSync11(fp)) return null;
|
|
10659
|
-
const parsed = parseSynkroToml2(
|
|
11187
|
+
const parsed = parseSynkroToml2(readFileSync10(fp, "utf-8"));
|
|
10660
11188
|
const valid = ["claude-code", "cursor"];
|
|
10661
11189
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
10662
11190
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -10664,7 +11192,8 @@ function readFullSynkroFile() {
|
|
|
10664
11192
|
harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
|
|
10665
11193
|
grader: {
|
|
10666
11194
|
pool: resolved.pool,
|
|
10667
|
-
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
|
|
11195
|
+
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local",
|
|
11196
|
+
location: parsed.grader?.location === "cloud" ? "cloud" : "local"
|
|
10668
11197
|
},
|
|
10669
11198
|
workers: {
|
|
10670
11199
|
...resolved.claudeWorkers != null ? { claude: resolved.claudeWorkers } : {},
|
|
@@ -10692,7 +11221,7 @@ function reconcileHarness() {
|
|
|
10692
11221
|
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
10693
11222
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
10694
11223
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
10695
|
-
const ccSettings =
|
|
11224
|
+
const ccSettings = join10(homedir10(), ".claude", "settings.json");
|
|
10696
11225
|
if (wantCC) {
|
|
10697
11226
|
installCCHooks(ccSettings, {
|
|
10698
11227
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -10712,7 +11241,7 @@ function reconcileHarness() {
|
|
|
10712
11241
|
});
|
|
10713
11242
|
console.log(" \u2713 Claude Code hooks registered");
|
|
10714
11243
|
try {
|
|
10715
|
-
const mcpJwt =
|
|
11244
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10716
11245
|
if (mcpJwt) {
|
|
10717
11246
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
10718
11247
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -10722,8 +11251,9 @@ function reconcileHarness() {
|
|
|
10722
11251
|
} else {
|
|
10723
11252
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
10724
11253
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
11254
|
+
if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
|
|
10725
11255
|
}
|
|
10726
|
-
const cursorHooks =
|
|
11256
|
+
const cursorHooks = join10(homedir10(), ".cursor", "hooks.json");
|
|
10727
11257
|
if (wantCursor) {
|
|
10728
11258
|
installCursorHooks(cursorHooks, {
|
|
10729
11259
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -10777,7 +11307,7 @@ async function syncSkillFiles() {
|
|
|
10777
11307
|
if (resolved.length === 0) return;
|
|
10778
11308
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
10779
11309
|
const tasks = resolved.map((fp) => {
|
|
10780
|
-
const content =
|
|
11310
|
+
const content = readFileSync10(fp, "utf-8");
|
|
10781
11311
|
const source = `skill:${fp.split("/").pop()}`;
|
|
10782
11312
|
if (!content.trim()) {
|
|
10783
11313
|
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
@@ -10823,7 +11353,7 @@ function detectGitRepo2() {
|
|
|
10823
11353
|
function getClaudeProjectsFolder() {
|
|
10824
11354
|
const cwd = process.cwd();
|
|
10825
11355
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
10826
|
-
const projectsDir =
|
|
11356
|
+
const projectsDir = join10(homedir10(), ".claude", "projects", sanitized);
|
|
10827
11357
|
return existsSync11(projectsDir) ? projectsDir : null;
|
|
10828
11358
|
}
|
|
10829
11359
|
function extractSessionInsights(projectsDir) {
|
|
@@ -10831,9 +11361,9 @@ function extractSessionInsights(projectsDir) {
|
|
|
10831
11361
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
10832
11362
|
for (const file of files) {
|
|
10833
11363
|
const sessionId = file.replace(".jsonl", "");
|
|
10834
|
-
const filePath =
|
|
11364
|
+
const filePath = join10(projectsDir, file);
|
|
10835
11365
|
try {
|
|
10836
|
-
const content =
|
|
11366
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10837
11367
|
const lines = content.split("\n").filter(Boolean);
|
|
10838
11368
|
for (let i = 0; i < lines.length; i++) {
|
|
10839
11369
|
try {
|
|
@@ -10912,14 +11442,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
10912
11442
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10913
11443
|
}
|
|
10914
11444
|
function getCursorTranscriptsDir() {
|
|
10915
|
-
const dir =
|
|
11445
|
+
const dir = join10(homedir10(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10916
11446
|
return existsSync11(dir) ? dir : null;
|
|
10917
11447
|
}
|
|
10918
11448
|
function isSafeConvId(id) {
|
|
10919
11449
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
10920
11450
|
}
|
|
10921
11451
|
function parseCursorTranscriptFile(filePath) {
|
|
10922
|
-
const content =
|
|
11452
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10923
11453
|
const lines = content.split("\n").filter(Boolean);
|
|
10924
11454
|
const messages = [];
|
|
10925
11455
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -10951,7 +11481,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10951
11481
|
for (let i = 0; i < convDirs.length; i++) {
|
|
10952
11482
|
const convId = convDirs[i];
|
|
10953
11483
|
if (!isSafeConvId(convId)) continue;
|
|
10954
|
-
const filePath =
|
|
11484
|
+
const filePath = join10(dir, convId, `${convId}.jsonl`);
|
|
10955
11485
|
if (!existsSync11(filePath)) continue;
|
|
10956
11486
|
try {
|
|
10957
11487
|
const all = parseCursorTranscriptFile(filePath);
|
|
@@ -10974,8 +11504,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10974
11504
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
10975
11505
|
}
|
|
10976
11506
|
try {
|
|
10977
|
-
const lc =
|
|
10978
|
-
writeFileSync8(
|
|
11507
|
+
const lc = readFileSync10(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
11508
|
+
writeFileSync8(join10(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
10979
11509
|
} catch {
|
|
10980
11510
|
}
|
|
10981
11511
|
}
|
|
@@ -10983,7 +11513,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10983
11513
|
return { sessions: totalSessions, messages: totalMessages };
|
|
10984
11514
|
}
|
|
10985
11515
|
function parseTranscriptFile(filePath) {
|
|
10986
|
-
const content =
|
|
11516
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10987
11517
|
const lines = content.split("\n").filter(Boolean);
|
|
10988
11518
|
const messages = [];
|
|
10989
11519
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -11031,7 +11561,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
11031
11561
|
for (let i = 0; i < files.length; i++) {
|
|
11032
11562
|
const file = files[i];
|
|
11033
11563
|
const sessionId = file.replace(".jsonl", "");
|
|
11034
|
-
const filePath =
|
|
11564
|
+
const filePath = join10(projectsDir, file);
|
|
11035
11565
|
try {
|
|
11036
11566
|
const allMessages = parseTranscriptFile(filePath);
|
|
11037
11567
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -11053,9 +11583,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
11053
11583
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
11054
11584
|
}
|
|
11055
11585
|
try {
|
|
11056
|
-
const content =
|
|
11586
|
+
const content = readFileSync10(join10(projectsDir, file), "utf-8");
|
|
11057
11587
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
11058
|
-
writeFileSync8(
|
|
11588
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
11059
11589
|
} catch {
|
|
11060
11590
|
}
|
|
11061
11591
|
}
|
|
@@ -11076,7 +11606,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
11076
11606
|
const sessions = [];
|
|
11077
11607
|
for (const file of batch) {
|
|
11078
11608
|
const sessionId = file.replace(".jsonl", "");
|
|
11079
|
-
const filePath =
|
|
11609
|
+
const filePath = join10(projectsDir, file);
|
|
11080
11610
|
try {
|
|
11081
11611
|
const allMessages = parseTranscriptFile(filePath);
|
|
11082
11612
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -11105,18 +11635,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
11105
11635
|
}
|
|
11106
11636
|
for (const file of batch) {
|
|
11107
11637
|
const sessionId = file.replace(".jsonl", "");
|
|
11108
|
-
const filePath =
|
|
11638
|
+
const filePath = join10(projectsDir, file);
|
|
11109
11639
|
try {
|
|
11110
|
-
const content =
|
|
11640
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
11111
11641
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
11112
|
-
writeFileSync8(
|
|
11642
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
11113
11643
|
} catch {
|
|
11114
11644
|
}
|
|
11115
11645
|
}
|
|
11116
11646
|
}
|
|
11117
11647
|
return { sessions: totalSessions, messages: totalMessages };
|
|
11118
11648
|
}
|
|
11119
|
-
var
|
|
11649
|
+
var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
|
|
11120
11650
|
var init_install = __esm({
|
|
11121
11651
|
"cli/commands/install.ts"() {
|
|
11122
11652
|
"use strict";
|
|
@@ -11134,10 +11664,11 @@ var init_install = __esm({
|
|
|
11134
11664
|
init_promptFetcher();
|
|
11135
11665
|
init_claudeDesktopTap();
|
|
11136
11666
|
init_dockerInstall();
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11667
|
+
init_setupToken();
|
|
11668
|
+
SYNKRO_DIR5 = join10(homedir10(), ".synkro");
|
|
11669
|
+
HOOKS_DIR = join10(SYNKRO_DIR5, "hooks");
|
|
11670
|
+
BIN_DIR = join10(SYNKRO_DIR5, "bin");
|
|
11671
|
+
CONFIG_PATH2 = join10(SYNKRO_DIR5, "config.env");
|
|
11141
11672
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
11142
11673
|
import { readFileSync } from 'node:fs';
|
|
11143
11674
|
import { homedir } from 'node:os';
|
|
@@ -11248,15 +11779,15 @@ rl.on('line', async (line) => {
|
|
|
11248
11779
|
}
|
|
11249
11780
|
});
|
|
11250
11781
|
`;
|
|
11251
|
-
OFFSETS_DIR =
|
|
11782
|
+
OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
|
|
11252
11783
|
}
|
|
11253
11784
|
});
|
|
11254
11785
|
|
|
11255
11786
|
// 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
|
|
11787
|
+
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";
|
|
11788
|
+
import { join as join11 } from "path";
|
|
11789
|
+
import { homedir as homedir11 } from "os";
|
|
11790
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
11260
11791
|
function writePluginFiles() {
|
|
11261
11792
|
for (const c of CHANNELS) {
|
|
11262
11793
|
mkdirSync10(c.sessionDir, { recursive: true });
|
|
@@ -11276,7 +11807,7 @@ function writePluginFiles() {
|
|
|
11276
11807
|
}
|
|
11277
11808
|
function runBunInstall() {
|
|
11278
11809
|
for (const c of CHANNELS) {
|
|
11279
|
-
const r =
|
|
11810
|
+
const r = spawnSync5("bun", ["install", "--silent"], {
|
|
11280
11811
|
cwd: c.sessionDir,
|
|
11281
11812
|
encoding: "utf-8",
|
|
11282
11813
|
timeout: 12e4
|
|
@@ -11292,7 +11823,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
11292
11823
|
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
11293
11824
|
return;
|
|
11294
11825
|
}
|
|
11295
|
-
const originalText =
|
|
11826
|
+
const originalText = readFileSync11(CLAUDE_JSON_PATH, "utf-8");
|
|
11296
11827
|
let parsed;
|
|
11297
11828
|
try {
|
|
11298
11829
|
parsed = JSON.parse(originalText);
|
|
@@ -11392,15 +11923,15 @@ function patchClaudeJson() {
|
|
|
11392
11923
|
});
|
|
11393
11924
|
}
|
|
11394
11925
|
function installLocalCC() {
|
|
11395
|
-
let bunCheck =
|
|
11926
|
+
let bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11396
11927
|
if (bunCheck.status !== 0) {
|
|
11397
11928
|
if (process.platform === "darwin") {
|
|
11398
11929
|
console.log(" Installing bun via brew...");
|
|
11399
|
-
const brewR =
|
|
11930
|
+
const brewR = spawnSync5("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
11400
11931
|
if (brewR.status !== 0) {
|
|
11401
11932
|
throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
11402
11933
|
}
|
|
11403
|
-
bunCheck =
|
|
11934
|
+
bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11404
11935
|
if (bunCheck.status !== 0) {
|
|
11405
11936
|
throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
|
|
11406
11937
|
}
|
|
@@ -11434,42 +11965,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
11434
11965
|
var init_install2 = __esm({
|
|
11435
11966
|
"cli/local-cc/install.ts"() {
|
|
11436
11967
|
"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 =
|
|
11968
|
+
CLAUDE_JSON_BACKUP_PATH = join11(homedir11(), ".claude.json.synkro-bak");
|
|
11969
|
+
SESSION_DIR = join11(homedir11(), ".synkro", "cc_sessions");
|
|
11970
|
+
PLUGIN_PATH = join11(SESSION_DIR, "synkro-channel.ts");
|
|
11971
|
+
PLUGIN_PKG_PATH = join11(SESSION_DIR, "package.json");
|
|
11972
|
+
PLUGIN_SETTINGS_DIR = join11(SESSION_DIR, ".claude");
|
|
11973
|
+
PLUGIN_SETTINGS_PATH = join11(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
11974
|
+
PROJECT_MCP_PATH = join11(SESSION_DIR, ".mcp.json");
|
|
11975
|
+
CLAUDE_JSON_PATH = join11(homedir11(), ".claude.json");
|
|
11976
|
+
RUN_SCRIPT_PATH = join11(SESSION_DIR, "run-claude.sh");
|
|
11446
11977
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
11447
11978
|
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 =
|
|
11979
|
+
SESSION_DIR_2 = join11(homedir11(), ".synkro", "cc_sessions_2");
|
|
11980
|
+
PLUGIN_PATH_2 = join11(SESSION_DIR_2, "synkro-channel.ts");
|
|
11981
|
+
PLUGIN_PKG_PATH_2 = join11(SESSION_DIR_2, "package.json");
|
|
11982
|
+
PLUGIN_SETTINGS_DIR_2 = join11(SESSION_DIR_2, ".claude");
|
|
11983
|
+
PLUGIN_SETTINGS_PATH_2 = join11(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
11984
|
+
PROJECT_MCP_PATH_2 = join11(SESSION_DIR_2, ".mcp.json");
|
|
11985
|
+
RUN_SCRIPT_PATH_2 = join11(SESSION_DIR_2, "run-claude.sh");
|
|
11455
11986
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
11456
11987
|
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 =
|
|
11988
|
+
SESSION_DIR_3 = join11(homedir11(), ".synkro", "cc_sessions_3");
|
|
11989
|
+
PLUGIN_PATH_3 = join11(SESSION_DIR_3, "synkro-channel.ts");
|
|
11990
|
+
PLUGIN_PKG_PATH_3 = join11(SESSION_DIR_3, "package.json");
|
|
11991
|
+
PLUGIN_SETTINGS_DIR_3 = join11(SESSION_DIR_3, ".claude");
|
|
11992
|
+
PLUGIN_SETTINGS_PATH_3 = join11(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
11993
|
+
PROJECT_MCP_PATH_3 = join11(SESSION_DIR_3, ".mcp.json");
|
|
11994
|
+
RUN_SCRIPT_PATH_3 = join11(SESSION_DIR_3, "run-claude.sh");
|
|
11464
11995
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
11465
11996
|
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 =
|
|
11997
|
+
SESSION_DIR_4 = join11(homedir11(), ".synkro", "cc_sessions_4");
|
|
11998
|
+
PLUGIN_PATH_4 = join11(SESSION_DIR_4, "synkro-channel.ts");
|
|
11999
|
+
PLUGIN_PKG_PATH_4 = join11(SESSION_DIR_4, "package.json");
|
|
12000
|
+
PLUGIN_SETTINGS_DIR_4 = join11(SESSION_DIR_4, ".claude");
|
|
12001
|
+
PLUGIN_SETTINGS_PATH_4 = join11(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
12002
|
+
PROJECT_MCP_PATH_4 = join11(SESSION_DIR_4, ".mcp.json");
|
|
12003
|
+
RUN_SCRIPT_PATH_4 = join11(SESSION_DIR_4, "run-claude.sh");
|
|
11473
12004
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
11474
12005
|
CHANNEL_4_PORT = 8952;
|
|
11475
12006
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -11744,9 +12275,9 @@ __export(disconnect_exports, {
|
|
|
11744
12275
|
disconnectCommand: () => disconnectCommand
|
|
11745
12276
|
});
|
|
11746
12277
|
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11747
|
-
import { homedir as
|
|
11748
|
-
import { join as
|
|
11749
|
-
import { spawnSync as
|
|
12278
|
+
import { homedir as homedir12 } from "os";
|
|
12279
|
+
import { join as join12 } from "path";
|
|
12280
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
11750
12281
|
import { createInterface as createInterface4 } from "readline";
|
|
11751
12282
|
async function tearDownLocalCC() {
|
|
11752
12283
|
const docker = dockerStatus();
|
|
@@ -11760,7 +12291,7 @@ async function tearDownLocalCC() {
|
|
|
11760
12291
|
console.log("\u2713 removed synkro-server container");
|
|
11761
12292
|
try {
|
|
11762
12293
|
const image = imageTag();
|
|
11763
|
-
const r =
|
|
12294
|
+
const r = spawnSync6("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
|
|
11764
12295
|
console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
|
|
11765
12296
|
} catch {
|
|
11766
12297
|
}
|
|
@@ -11819,15 +12350,19 @@ async function disconnectCommand(args2 = []) {
|
|
|
11819
12350
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
11820
12351
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
11821
12352
|
}
|
|
11822
|
-
|
|
12353
|
+
{
|
|
12354
|
+
const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
|
|
12355
|
+
console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
|
|
12356
|
+
}
|
|
12357
|
+
if (existsSync13(SYNKRO_DIR6)) {
|
|
11823
12358
|
if (purge) {
|
|
11824
|
-
rmSync(
|
|
11825
|
-
console.log(`\u2713 wiped ${
|
|
12359
|
+
rmSync(SYNKRO_DIR6, { recursive: true, force: true });
|
|
12360
|
+
console.log(`\u2713 wiped ${SYNKRO_DIR6} entirely \u2014 including all scan data and backups`);
|
|
11826
12361
|
} else {
|
|
11827
|
-
const keep = /* @__PURE__ */ new Set([
|
|
12362
|
+
const keep = /* @__PURE__ */ new Set([join12(SYNKRO_DIR6, "pgdata"), join12(SYNKRO_DIR6, "pgdata-backups")]);
|
|
11828
12363
|
const preserved = [];
|
|
11829
|
-
for (const entry of readdirSync4(
|
|
11830
|
-
const full =
|
|
12364
|
+
for (const entry of readdirSync4(SYNKRO_DIR6)) {
|
|
12365
|
+
const full = join12(SYNKRO_DIR6, entry);
|
|
11831
12366
|
if (keep.has(full)) {
|
|
11832
12367
|
preserved.push(entry);
|
|
11833
12368
|
continue;
|
|
@@ -11835,18 +12370,18 @@ async function disconnectCommand(args2 = []) {
|
|
|
11835
12370
|
rmSync(full, { recursive: true, force: true });
|
|
11836
12371
|
}
|
|
11837
12372
|
if (preserved.length > 0) {
|
|
11838
|
-
console.log(`\u2713 removed Synkro config from ${
|
|
12373
|
+
console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR6} (kept your scan data: ${preserved.join(", ")})`);
|
|
11839
12374
|
console.log(" run `synkro uninstall --purge` to delete that too");
|
|
11840
12375
|
} else {
|
|
11841
|
-
console.log(`\u2713 removed ${
|
|
12376
|
+
console.log(`\u2713 removed ${SYNKRO_DIR6}`);
|
|
11842
12377
|
}
|
|
11843
12378
|
}
|
|
11844
12379
|
} else {
|
|
11845
|
-
console.log(`\xB7 ${
|
|
12380
|
+
console.log(`\xB7 ${SYNKRO_DIR6} already gone`);
|
|
11846
12381
|
}
|
|
11847
12382
|
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
12383
|
}
|
|
11849
|
-
var
|
|
12384
|
+
var SYNKRO_DIR6;
|
|
11850
12385
|
var init_disconnect = __esm({
|
|
11851
12386
|
"cli/commands/disconnect.ts"() {
|
|
11852
12387
|
"use strict";
|
|
@@ -11857,14 +12392,14 @@ var init_disconnect = __esm({
|
|
|
11857
12392
|
init_install2();
|
|
11858
12393
|
init_dockerInstall();
|
|
11859
12394
|
init_macKeychain();
|
|
11860
|
-
|
|
12395
|
+
SYNKRO_DIR6 = join12(homedir12(), ".synkro");
|
|
11861
12396
|
}
|
|
11862
12397
|
});
|
|
11863
12398
|
|
|
11864
12399
|
// 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
|
|
12400
|
+
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";
|
|
12401
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
12402
|
+
import { homedir as homedir13 } from "os";
|
|
11868
12403
|
function truncate(s, max = PREVIEW_MAX) {
|
|
11869
12404
|
if (s.length <= max) return s;
|
|
11870
12405
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -11902,9 +12437,9 @@ function appendTurn(args2) {
|
|
|
11902
12437
|
function readRecentTurns(n = 20) {
|
|
11903
12438
|
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
11904
12439
|
try {
|
|
11905
|
-
const size =
|
|
12440
|
+
const size = statSync2(TURN_LOG_PATH).size;
|
|
11906
12441
|
if (size === 0) return [];
|
|
11907
|
-
const text =
|
|
12442
|
+
const text = readFileSync12(TURN_LOG_PATH, "utf-8");
|
|
11908
12443
|
const lines = text.split("\n").filter(Boolean);
|
|
11909
12444
|
const lastN = lines.slice(-n).reverse();
|
|
11910
12445
|
return lastN.map((line) => {
|
|
@@ -11928,7 +12463,7 @@ function followTurns(onEntry) {
|
|
|
11928
12463
|
}
|
|
11929
12464
|
let lastSize = (() => {
|
|
11930
12465
|
try {
|
|
11931
|
-
return
|
|
12466
|
+
return statSync2(TURN_LOG_PATH).size;
|
|
11932
12467
|
} catch {
|
|
11933
12468
|
return 0;
|
|
11934
12469
|
}
|
|
@@ -11983,7 +12518,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
11983
12518
|
var init_turnLog = __esm({
|
|
11984
12519
|
"cli/local-cc/turnLog.ts"() {
|
|
11985
12520
|
"use strict";
|
|
11986
|
-
TURN_LOG_PATH =
|
|
12521
|
+
TURN_LOG_PATH = join13(homedir13(), ".synkro", "cc_sessions", "turns.log");
|
|
11987
12522
|
PREVIEW_MAX = 400;
|
|
11988
12523
|
}
|
|
11989
12524
|
});
|
|
@@ -12132,19 +12667,19 @@ var init_grade = __esm({
|
|
|
12132
12667
|
});
|
|
12133
12668
|
|
|
12134
12669
|
// cli/local-cc/pueue.ts
|
|
12135
|
-
import { execFileSync as execFileSync3, spawnSync as
|
|
12136
|
-
import { homedir as
|
|
12137
|
-
import { join as
|
|
12670
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync7, spawn as spawn2 } from "child_process";
|
|
12671
|
+
import { homedir as homedir14 } from "os";
|
|
12672
|
+
import { join as join14 } from "path";
|
|
12138
12673
|
import { connect as connect2 } from "net";
|
|
12139
12674
|
function pueueAvailable() {
|
|
12140
|
-
const r =
|
|
12675
|
+
const r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12141
12676
|
if (r.status !== 0) {
|
|
12142
12677
|
throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
|
|
12143
12678
|
}
|
|
12144
12679
|
}
|
|
12145
12680
|
function statusJson() {
|
|
12146
12681
|
pueueAvailable();
|
|
12147
|
-
const r =
|
|
12682
|
+
const r = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8" });
|
|
12148
12683
|
if (r.status !== 0) {
|
|
12149
12684
|
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
12150
12685
|
}
|
|
@@ -12189,18 +12724,18 @@ function startTask(opts = {}) {
|
|
|
12189
12724
|
let existing = findTask(ch);
|
|
12190
12725
|
while (existing) {
|
|
12191
12726
|
if (existing.status === "Running" || existing.status === "Queued") {
|
|
12192
|
-
|
|
12193
|
-
|
|
12727
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
12728
|
+
spawnSync7("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
12194
12729
|
for (let i = 0; i < 10; i++) {
|
|
12195
12730
|
const check = findTask(ch);
|
|
12196
12731
|
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
12197
|
-
|
|
12732
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
12198
12733
|
}
|
|
12199
12734
|
}
|
|
12200
|
-
|
|
12735
|
+
spawnSync7("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
12201
12736
|
existing = findTask(ch);
|
|
12202
12737
|
}
|
|
12203
|
-
const runScript =
|
|
12738
|
+
const runScript = join14(cwd, "run-claude.sh");
|
|
12204
12739
|
const args2 = [
|
|
12205
12740
|
"add",
|
|
12206
12741
|
"--label",
|
|
@@ -12211,7 +12746,7 @@ function startTask(opts = {}) {
|
|
|
12211
12746
|
"bash",
|
|
12212
12747
|
runScript
|
|
12213
12748
|
];
|
|
12214
|
-
const r =
|
|
12749
|
+
const r = spawnSync7("pueue", args2, { encoding: "utf-8" });
|
|
12215
12750
|
if (r.status !== 0) {
|
|
12216
12751
|
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
12217
12752
|
}
|
|
@@ -12222,25 +12757,25 @@ function startTask(opts = {}) {
|
|
|
12222
12757
|
return created;
|
|
12223
12758
|
}
|
|
12224
12759
|
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
12225
|
-
|
|
12760
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
12226
12761
|
let t = findTask(channel);
|
|
12227
12762
|
while (t) {
|
|
12228
12763
|
if (t.status === "Running" || t.status === "Queued") {
|
|
12229
|
-
|
|
12764
|
+
spawnSync7("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
12230
12765
|
for (let i = 0; i < 10; i++) {
|
|
12231
12766
|
const check = findTask(channel);
|
|
12232
12767
|
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
12233
|
-
|
|
12768
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
12234
12769
|
}
|
|
12235
12770
|
}
|
|
12236
|
-
|
|
12771
|
+
spawnSync7("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
12237
12772
|
t = findTask(channel);
|
|
12238
12773
|
}
|
|
12239
12774
|
}
|
|
12240
12775
|
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
12241
12776
|
const t = findTask(channel);
|
|
12242
12777
|
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
12243
|
-
const r =
|
|
12778
|
+
const r = spawnSync7("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
12244
12779
|
return r.stdout || r.stderr || "(no output)";
|
|
12245
12780
|
}
|
|
12246
12781
|
function ensureRunning(opts = {}) {
|
|
@@ -12265,8 +12800,8 @@ function probePort(host, port, timeoutMs = 500) {
|
|
|
12265
12800
|
});
|
|
12266
12801
|
}
|
|
12267
12802
|
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
12268
|
-
|
|
12269
|
-
|
|
12803
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
12804
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
12270
12805
|
}
|
|
12271
12806
|
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
12272
12807
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -12278,46 +12813,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
|
|
|
12278
12813
|
return probePort(host, port);
|
|
12279
12814
|
}
|
|
12280
12815
|
function brewInstall(pkg) {
|
|
12281
|
-
const brew =
|
|
12816
|
+
const brew = spawnSync7("brew", ["--version"], { encoding: "utf-8" });
|
|
12282
12817
|
if (brew.status !== 0) return false;
|
|
12283
12818
|
console.log(` Installing ${pkg} via brew...`);
|
|
12284
|
-
const r =
|
|
12819
|
+
const r = spawnSync7("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
12285
12820
|
return r.status === 0;
|
|
12286
12821
|
}
|
|
12287
12822
|
function assertPueueInstalled() {
|
|
12288
|
-
let r =
|
|
12823
|
+
let r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12289
12824
|
if (r.status !== 0) {
|
|
12290
12825
|
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
12291
|
-
r =
|
|
12826
|
+
r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12292
12827
|
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
12293
12828
|
} else {
|
|
12294
12829
|
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
12295
12830
|
}
|
|
12296
12831
|
}
|
|
12297
|
-
const status =
|
|
12832
|
+
const status = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12298
12833
|
if (status.status !== 0) {
|
|
12299
12834
|
console.log(" Starting pueued daemon...");
|
|
12300
12835
|
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
12301
12836
|
child.unref();
|
|
12302
|
-
|
|
12303
|
-
const retry =
|
|
12837
|
+
spawnSync7("sleep", ["1"]);
|
|
12838
|
+
const retry = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12304
12839
|
if (retry.status !== 0) {
|
|
12305
12840
|
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
12306
12841
|
}
|
|
12307
12842
|
}
|
|
12308
|
-
|
|
12843
|
+
spawnSync7("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
12309
12844
|
}
|
|
12310
12845
|
function assertClaudeInstalled() {
|
|
12311
|
-
const r =
|
|
12846
|
+
const r = spawnSync7("claude", ["--version"], { encoding: "utf-8" });
|
|
12312
12847
|
if (r.status !== 0) {
|
|
12313
12848
|
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
12314
12849
|
}
|
|
12315
12850
|
}
|
|
12316
12851
|
function assertTmuxInstalled() {
|
|
12317
|
-
let r =
|
|
12852
|
+
let r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12318
12853
|
if (r.status !== 0) {
|
|
12319
12854
|
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
12320
|
-
r =
|
|
12855
|
+
r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12321
12856
|
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
12322
12857
|
} else {
|
|
12323
12858
|
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
@@ -12330,12 +12865,12 @@ var init_pueue = __esm({
|
|
|
12330
12865
|
"use strict";
|
|
12331
12866
|
TASK_LABEL = "synkro-local-cc";
|
|
12332
12867
|
TMUX_SESSION = "synkro-local-cc";
|
|
12333
|
-
SESSION_DIR2 =
|
|
12868
|
+
SESSION_DIR2 = join14(homedir14(), ".synkro", "cc_sessions");
|
|
12334
12869
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
12335
12870
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
12336
|
-
SESSION_DIR_22 =
|
|
12337
|
-
SESSION_DIR_32 =
|
|
12338
|
-
SESSION_DIR_42 =
|
|
12871
|
+
SESSION_DIR_22 = join14(homedir14(), ".synkro", "cc_sessions_2");
|
|
12872
|
+
SESSION_DIR_32 = join14(homedir14(), ".synkro", "cc_sessions_3");
|
|
12873
|
+
SESSION_DIR_42 = join14(homedir14(), ".synkro", "cc_sessions_4");
|
|
12339
12874
|
PueueError = class extends Error {
|
|
12340
12875
|
constructor(message, cause) {
|
|
12341
12876
|
super(message);
|
|
@@ -12350,13 +12885,13 @@ var init_pueue = __esm({
|
|
|
12350
12885
|
});
|
|
12351
12886
|
|
|
12352
12887
|
// cli/local-cc/settings.ts
|
|
12353
|
-
import { existsSync as existsSync15, readFileSync as
|
|
12354
|
-
import { homedir as
|
|
12355
|
-
import { join as
|
|
12888
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
12889
|
+
import { homedir as homedir15 } from "os";
|
|
12890
|
+
import { join as join15 } from "path";
|
|
12356
12891
|
function isLocalCCEnabled() {
|
|
12357
12892
|
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
12358
12893
|
try {
|
|
12359
|
-
const content =
|
|
12894
|
+
const content = readFileSync13(CONFIG_PATH3, "utf-8");
|
|
12360
12895
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
12361
12896
|
return match?.[1] === "yes";
|
|
12362
12897
|
} catch {
|
|
@@ -12367,7 +12902,7 @@ var CONFIG_PATH3;
|
|
|
12367
12902
|
var init_settings = __esm({
|
|
12368
12903
|
"cli/local-cc/settings.ts"() {
|
|
12369
12904
|
"use strict";
|
|
12370
|
-
CONFIG_PATH3 =
|
|
12905
|
+
CONFIG_PATH3 = join15(homedir15(), ".synkro", "config.env");
|
|
12371
12906
|
}
|
|
12372
12907
|
});
|
|
12373
12908
|
|
|
@@ -12376,11 +12911,11 @@ var localCc_exports = {};
|
|
|
12376
12911
|
__export(localCc_exports, {
|
|
12377
12912
|
localCcCommand: () => localCcCommand
|
|
12378
12913
|
});
|
|
12379
|
-
import { spawnSync as
|
|
12380
|
-
import { homedir as
|
|
12381
|
-
import { join as
|
|
12914
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
12915
|
+
import { homedir as homedir16 } from "os";
|
|
12916
|
+
import { join as join16 } from "path";
|
|
12382
12917
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
12383
|
-
import { existsSync as existsSync16, readFileSync as
|
|
12918
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
12384
12919
|
function deploymentMode() {
|
|
12385
12920
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
12386
12921
|
if (env === "docker") return "docker";
|
|
@@ -12487,14 +13022,14 @@ TROUBLESHOOTING
|
|
|
12487
13022
|
}
|
|
12488
13023
|
function readGatewayUrl() {
|
|
12489
13024
|
if (existsSync16(CONFIG_PATH4)) {
|
|
12490
|
-
const m =
|
|
13025
|
+
const m = readFileSync14(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
12491
13026
|
if (m) return m[1];
|
|
12492
13027
|
}
|
|
12493
13028
|
return "https://api.synkro.sh";
|
|
12494
13029
|
}
|
|
12495
13030
|
function updateLocalInferenceFlag(enabled) {
|
|
12496
13031
|
if (!existsSync16(CONFIG_PATH4)) return;
|
|
12497
|
-
let content =
|
|
13032
|
+
let content = readFileSync14(CONFIG_PATH4, "utf-8");
|
|
12498
13033
|
const flag = enabled ? "yes" : "no";
|
|
12499
13034
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
12500
13035
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -12557,7 +13092,7 @@ async function cmdStatus() {
|
|
|
12557
13092
|
}
|
|
12558
13093
|
const ch1Up = await isChannelAvailable();
|
|
12559
13094
|
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
12560
|
-
const tmux1 =
|
|
13095
|
+
const tmux1 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12561
13096
|
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
12562
13097
|
const t2 = findTask(CHANNEL_SECONDARY);
|
|
12563
13098
|
if (!t2) {
|
|
@@ -12567,7 +13102,7 @@ async function cmdStatus() {
|
|
|
12567
13102
|
}
|
|
12568
13103
|
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
12569
13104
|
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
12570
|
-
const tmux2 =
|
|
13105
|
+
const tmux2 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
|
|
12571
13106
|
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
12572
13107
|
}
|
|
12573
13108
|
async function cmdEnable() {
|
|
@@ -12773,7 +13308,7 @@ function cmdLogs(rest) {
|
|
|
12773
13308
|
}
|
|
12774
13309
|
return "200";
|
|
12775
13310
|
})();
|
|
12776
|
-
|
|
13311
|
+
spawnSync8("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
|
|
12777
13312
|
return;
|
|
12778
13313
|
}
|
|
12779
13314
|
for (const arg of rest) {
|
|
@@ -12821,7 +13356,7 @@ function cmdLogs(rest) {
|
|
|
12821
13356
|
function cmdAttach(rest) {
|
|
12822
13357
|
assertTmuxInstalled();
|
|
12823
13358
|
const readonly = rest.some((a) => a === "--readonly" || a === "-r");
|
|
12824
|
-
const has =
|
|
13359
|
+
const has = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12825
13360
|
if (has.status !== 0) {
|
|
12826
13361
|
console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
|
|
12827
13362
|
process.exit(1);
|
|
@@ -12834,7 +13369,7 @@ function cmdAttach(rest) {
|
|
|
12834
13369
|
console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
|
|
12835
13370
|
console.log();
|
|
12836
13371
|
const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
|
|
12837
|
-
const r =
|
|
13372
|
+
const r = spawnSync8("tmux", args2, { stdio: "inherit" });
|
|
12838
13373
|
process.exit(r.status ?? 0);
|
|
12839
13374
|
}
|
|
12840
13375
|
async function cmdTest() {
|
|
@@ -12933,8 +13468,8 @@ var init_localCc = __esm({
|
|
|
12933
13468
|
init_install();
|
|
12934
13469
|
init_client();
|
|
12935
13470
|
init_stub();
|
|
12936
|
-
SYNKRO_CONFIG_PATH =
|
|
12937
|
-
CONFIG_PATH4 =
|
|
13471
|
+
SYNKRO_CONFIG_PATH = join16(homedir16(), ".synkro", "config.env");
|
|
13472
|
+
CONFIG_PATH4 = join16(homedir16(), ".synkro", "config.env");
|
|
12938
13473
|
}
|
|
12939
13474
|
});
|
|
12940
13475
|
|
|
@@ -13004,6 +13539,11 @@ async function updateCommand() {
|
|
|
13004
13539
|
console.log("\nSynkro updated \u2014 now running the latest version.");
|
|
13005
13540
|
}
|
|
13006
13541
|
async function restartCommand(rest = []) {
|
|
13542
|
+
const location = await reconcileDeployLocation();
|
|
13543
|
+
if (location === "cloud") {
|
|
13544
|
+
console.log("Synkro: grading runs on Synkro Cloud \u2014 no local container to restart.");
|
|
13545
|
+
return;
|
|
13546
|
+
}
|
|
13007
13547
|
assertDockerAvailable();
|
|
13008
13548
|
const cfg = resolveWorkerConfig(rest);
|
|
13009
13549
|
let claudeWorkers = cfg.claudeWorkers;
|
|
@@ -13045,13 +13585,13 @@ var config_exports = {};
|
|
|
13045
13585
|
__export(config_exports, {
|
|
13046
13586
|
configCommand: () => configCommand
|
|
13047
13587
|
});
|
|
13048
|
-
import { readFileSync as
|
|
13049
|
-
import { join as
|
|
13050
|
-
import { homedir as
|
|
13588
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
13589
|
+
import { join as join17 } from "path";
|
|
13590
|
+
import { homedir as homedir17 } from "os";
|
|
13051
13591
|
function readConfigEnv() {
|
|
13052
13592
|
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
13053
13593
|
const out = {};
|
|
13054
|
-
for (const line of
|
|
13594
|
+
for (const line of readFileSync15(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
13055
13595
|
const t = line.trim();
|
|
13056
13596
|
if (!t || t.startsWith("#")) continue;
|
|
13057
13597
|
const eq = t.indexOf("=");
|
|
@@ -13064,7 +13604,7 @@ function updateConfigValue(key, value) {
|
|
|
13064
13604
|
console.error("No config found. Run `synkro install` first.");
|
|
13065
13605
|
process.exit(1);
|
|
13066
13606
|
}
|
|
13067
|
-
const lines =
|
|
13607
|
+
const lines = readFileSync15(CONFIG_PATH5, "utf-8").split("\n");
|
|
13068
13608
|
const pattern = new RegExp(`^${key}=`);
|
|
13069
13609
|
let found = false;
|
|
13070
13610
|
const updated = lines.map((line) => {
|
|
@@ -13180,25 +13720,25 @@ To change:`);
|
|
|
13180
13720
|
updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
|
|
13181
13721
|
console.log(`\u2713 Inference set to '${inferenceValue}'.`);
|
|
13182
13722
|
}
|
|
13183
|
-
var
|
|
13723
|
+
var SYNKRO_DIR7, CONFIG_PATH5;
|
|
13184
13724
|
var init_config = __esm({
|
|
13185
13725
|
"cli/commands/config.ts"() {
|
|
13186
13726
|
"use strict";
|
|
13187
13727
|
init_stub();
|
|
13188
|
-
|
|
13189
|
-
CONFIG_PATH5 =
|
|
13728
|
+
SYNKRO_DIR7 = join17(homedir17(), ".synkro");
|
|
13729
|
+
CONFIG_PATH5 = join17(SYNKRO_DIR7, "config.env");
|
|
13190
13730
|
}
|
|
13191
13731
|
});
|
|
13192
13732
|
|
|
13193
13733
|
// cli/bootstrap.js
|
|
13194
|
-
import { readFileSync as
|
|
13734
|
+
import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
|
|
13195
13735
|
import { resolve as resolve3 } from "path";
|
|
13196
13736
|
var envCandidates = [
|
|
13197
13737
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
13198
13738
|
];
|
|
13199
13739
|
for (const envPath of envCandidates) {
|
|
13200
13740
|
if (!existsSync18(envPath)) continue;
|
|
13201
|
-
const envContent =
|
|
13741
|
+
const envContent = readFileSync16(envPath, "utf-8");
|
|
13202
13742
|
for (const line of envContent.split("\n")) {
|
|
13203
13743
|
const trimmed = line.trim();
|
|
13204
13744
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -13213,7 +13753,7 @@ var args = process.argv.slice(2);
|
|
|
13213
13753
|
var cmd = args[0] || "";
|
|
13214
13754
|
var subArgs = args.slice(1);
|
|
13215
13755
|
function printVersion() {
|
|
13216
|
-
console.log("1.6.
|
|
13756
|
+
console.log("1.6.60");
|
|
13217
13757
|
}
|
|
13218
13758
|
function printHelp2() {
|
|
13219
13759
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -13327,7 +13867,7 @@ async function main() {
|
|
|
13327
13867
|
}
|
|
13328
13868
|
case "claude-desktop": {
|
|
13329
13869
|
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
13330
|
-
await runClaudeDesktopTap2();
|
|
13870
|
+
await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
|
|
13331
13871
|
break;
|
|
13332
13872
|
}
|
|
13333
13873
|
default: {
|