@synkro-sh/cli 1.6.57 → 1.6.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +1086 -410
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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_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,23 +7649,39 @@ async function runClaudeDesktopTap() {
|
|
|
7533
7649
|
});
|
|
7534
7650
|
});
|
|
7535
7651
|
}
|
|
7536
|
-
var CAPTURE_URL, SCAN_URL, SCAN_TURN_URL, DLP_POLICY_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";
|
|
7661
|
+
TURN_VERDICTS_URL = "http://127.0.0.1:18931/api/local/turn-verdicts";
|
|
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";
|
|
7544
7664
|
JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
|
|
7545
|
-
ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64
|
|
7665
|
+
ADDON_PY = `import os, json, threading, urllib.request, urllib.error, re, base64, time, hashlib
|
|
7546
7666
|
from mitmproxy import http
|
|
7547
7667
|
|
|
7548
|
-
|
|
7668
|
+
def _safe_url(u):
|
|
7669
|
+
# CWE-918: these URLs come from the environment the installer sets \u2014 accept ONLY
|
|
7670
|
+
# the loopback container endpoints, never an arbitrary host from a tampered env.
|
|
7671
|
+
return u if (u.startswith("http://127.0.0.1:") or u.startswith("http://localhost:")) else ""
|
|
7672
|
+
|
|
7673
|
+
CAPTURE_URL = _safe_url(os.environ.get("SYNKRO_CAPTURE_URL", ""))
|
|
7549
7674
|
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7550
|
-
SCAN_URL = os.environ.get("SYNKRO_SCAN_URL", "")
|
|
7551
|
-
SCAN_TURN_URL = os.environ.get("SYNKRO_SCAN_TURN_URL", "")
|
|
7552
|
-
POLICY_URL = os.environ.get("SYNKRO_DLP_POLICY_URL", "")
|
|
7675
|
+
SCAN_URL = _safe_url(os.environ.get("SYNKRO_SCAN_URL", ""))
|
|
7676
|
+
SCAN_TURN_URL = _safe_url(os.environ.get("SYNKRO_SCAN_TURN_URL", ""))
|
|
7677
|
+
POLICY_URL = _safe_url(os.environ.get("SYNKRO_DLP_POLICY_URL", ""))
|
|
7678
|
+
TURN_VERDICTS_URL = _safe_url(os.environ.get("SYNKRO_TURN_VERDICTS_URL", ""))
|
|
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"
|
|
7553
7685
|
# Token file path \u2014 the SERVER_TOKEN rotates whenever the container is recreated,
|
|
7554
7686
|
# so we re-read it from disk on a 401 instead of needing a tap restart.
|
|
7555
7687
|
TOKEN_FILE = os.environ.get("SYNKRO_TAP_TOKEN_FILE", "")
|
|
@@ -7615,6 +7747,151 @@ def _fmt_tool_name(raw):
|
|
|
7615
7747
|
pass
|
|
7616
7748
|
return raw
|
|
7617
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
|
+
|
|
7618
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
|
|
7619
7896
|
# Whether to HOLD connector turns (buffer the live completion until graded).
|
|
7620
7897
|
# Fetched once from the server: True only when a BLOCK-mode rule exists (+ not
|
|
@@ -7705,10 +7982,12 @@ def _extract_sse_text(body):
|
|
|
7705
7982
|
if isinstance(x, dict) and isinstance(x.get("text"), str): parts.append(x["text"]); total += len(x["text"])
|
|
7706
7983
|
return ("".join(parts)[:2000000], tool)
|
|
7707
7984
|
|
|
7708
|
-
def
|
|
7709
|
-
#
|
|
7710
|
-
#
|
|
7711
|
-
#
|
|
7985
|
+
def _neutralize_body(body, notice):
|
|
7986
|
+
# Buffered BLOCK render of a COMPLETE response: replace the answer with the notice
|
|
7987
|
+
# (first text delta, rest blanked), blank text/content content_blocks (incl. the
|
|
7988
|
+
# retrieved doc), leave tool_use + thinking + every structural event intact (the
|
|
7989
|
+
# tree GET scrubs thinking durably). Rewriting a WHOLE buffered body \u2014 not the
|
|
7990
|
+
# live stream \u2014 is what avoids the "couldn't finish" fragility.
|
|
7712
7991
|
out = []
|
|
7713
7992
|
injected = [False]
|
|
7714
7993
|
for line in body.split("\\n"):
|
|
@@ -7719,7 +7998,7 @@ def _neutralize_sse(body, notice):
|
|
|
7719
7998
|
out.append(line); continue
|
|
7720
7999
|
changed = False
|
|
7721
8000
|
d = obj.get("delta")
|
|
7722
|
-
if isinstance(d, dict) and isinstance(d.get("text"), str):
|
|
8001
|
+
if isinstance(d, dict) and d.get("type") == "text_delta" and isinstance(d.get("text"), str):
|
|
7723
8002
|
d["text"] = (notice if not injected[0] else "")
|
|
7724
8003
|
injected[0] = True; changed = True
|
|
7725
8004
|
cb = obj.get("content_block")
|
|
@@ -7731,6 +8010,304 @@ def _neutralize_sse(body, notice):
|
|
|
7731
8010
|
out.append(("data: " + json.dumps(obj)) if changed else line)
|
|
7732
8011
|
return "\\n".join(out)
|
|
7733
8012
|
|
|
8013
|
+
# Per-conversation DLP verdict cached by the held completion so the tree GET reuses
|
|
8014
|
+
# it (durable scrub) instead of re-grading. Bounded (CWE-400), lock-guarded (CWE-362).
|
|
8015
|
+
_turn_verdicts = {}
|
|
8016
|
+
_tv_lock = threading.Lock()
|
|
8017
|
+
|
|
8018
|
+
def _cache_turn_verdict(cid, verdict):
|
|
8019
|
+
if not cid: return
|
|
8020
|
+
with _tv_lock:
|
|
8021
|
+
if len(_turn_verdicts) > 200: _turn_verdicts.clear()
|
|
8022
|
+
_turn_verdicts[cid] = verdict
|
|
8023
|
+
|
|
8024
|
+
def _pop_turn_verdict(cid):
|
|
8025
|
+
with _tv_lock:
|
|
8026
|
+
return _turn_verdicts.pop(cid, None)
|
|
8027
|
+
|
|
8028
|
+
# \u2500\u2500 Per-turn durable verdict helpers (PGLite-backed via local endpoints) \u2500\u2500\u2500\u2500\u2500\u2500
|
|
8029
|
+
def _msg_blocks(msg):
|
|
8030
|
+
b = msg.get("content", [])
|
|
8031
|
+
return b if isinstance(b, list) else []
|
|
8032
|
+
|
|
8033
|
+
def _msg_key(msg):
|
|
8034
|
+
# Stable per-turn id. Prefer the message uuid; fall back to a content hash so the
|
|
8035
|
+
# verdict still keys consistently across reopens even if uuid is ever absent.
|
|
8036
|
+
u = msg.get("uuid") or msg.get("id")
|
|
8037
|
+
if isinstance(u, str) and u:
|
|
8038
|
+
return u
|
|
8039
|
+
try:
|
|
8040
|
+
raw = json.dumps(msg.get("content", ""), sort_keys=True).encode("utf-8", "replace")
|
|
8041
|
+
return "h:" + hashlib.sha256(raw).hexdigest()[:40]
|
|
8042
|
+
except Exception:
|
|
8043
|
+
return ""
|
|
8044
|
+
|
|
8045
|
+
def _msg_is_connector(msg):
|
|
8046
|
+
# A turn that pulled content via a connector \u2192 has a tool_result block (the doc).
|
|
8047
|
+
for b in _msg_blocks(msg):
|
|
8048
|
+
if isinstance(b, dict) and (b.get("type", "") == "tool_result" or b.get("type", "").endswith("_tool_result")):
|
|
8049
|
+
return True
|
|
8050
|
+
return False
|
|
8051
|
+
|
|
8052
|
+
def _msg_grade_payload(msg):
|
|
8053
|
+
# Extract (assistant_text, tool_result_text, connector) from a message's blocks.
|
|
8054
|
+
texts = []; tools = []; connector = ""
|
|
8055
|
+
for b in _msg_blocks(msg):
|
|
8056
|
+
if isinstance(b, str):
|
|
8057
|
+
texts.append(b); continue
|
|
8058
|
+
if not isinstance(b, dict):
|
|
8059
|
+
continue
|
|
8060
|
+
bt = b.get("type", "")
|
|
8061
|
+
if bt == "text":
|
|
8062
|
+
texts.append(b.get("text", "") or "")
|
|
8063
|
+
elif bt == "tool_use" or bt == "server_tool_use":
|
|
8064
|
+
nm = b.get("name", "") or ""
|
|
8065
|
+
if nm and not connector:
|
|
8066
|
+
connector = nm.split(":")[0] if ":" in nm else nm
|
|
8067
|
+
elif bt == "tool_result" or bt.endswith("_tool_result"):
|
|
8068
|
+
rc = b.get("content", "")
|
|
8069
|
+
if isinstance(rc, list):
|
|
8070
|
+
for x in rc:
|
|
8071
|
+
if isinstance(x, dict):
|
|
8072
|
+
if isinstance(x.get("text"), str): tools.append(x["text"])
|
|
8073
|
+
elif x.get("content"): tools.append(str(x.get("content")))
|
|
8074
|
+
elif isinstance(rc, str):
|
|
8075
|
+
tools.append(rc)
|
|
8076
|
+
ic = b.get("integration_name") or b.get("name") or ""
|
|
8077
|
+
if ic and not connector:
|
|
8078
|
+
connector = ic
|
|
8079
|
+
return ("\\n".join(texts), "\\n".join(tools), connector)
|
|
8080
|
+
|
|
8081
|
+
def _get_turn_verdicts(uuids):
|
|
8082
|
+
# Batch lookup of durable verdicts from PGLite (one round-trip). {uuid: {blocked,reason}}.
|
|
8083
|
+
if not (TURN_VERDICTS_URL and TOKEN and uuids):
|
|
8084
|
+
return {}
|
|
8085
|
+
try:
|
|
8086
|
+
resp = _post_json(TURN_VERDICTS_URL, json.dumps({"uuids": uuids[:500]}).encode("utf-8"), 8)
|
|
8087
|
+
if resp:
|
|
8088
|
+
o = json.loads(resp.decode("utf-8", "replace"))
|
|
8089
|
+
v = o.get("verdicts")
|
|
8090
|
+
return v if isinstance(v, dict) else {}
|
|
8091
|
+
except Exception as e:
|
|
8092
|
+
print("[synkro] verdict lookup error: " + str(e))
|
|
8093
|
+
return {}
|
|
8094
|
+
|
|
8095
|
+
def _put_turn_verdict(uuid, convo_id, blocked, reason):
|
|
8096
|
+
# Persist one graded turn (fire-and-forget, off the tree-GET critical path).
|
|
8097
|
+
if not (TURN_VERDICT_URL and TOKEN and uuid):
|
|
8098
|
+
return
|
|
8099
|
+
def _send():
|
|
8100
|
+
try:
|
|
8101
|
+
_post_json(TURN_VERDICT_URL, json.dumps({"message_uuid": uuid, "convo_id": convo_id, "blocked": bool(blocked), "reason": reason or ""}).encode("utf-8"), 8)
|
|
8102
|
+
except Exception as e:
|
|
8103
|
+
print("[synkro] verdict persist error: " + str(e))
|
|
8104
|
+
threading.Thread(target=_send, daemon=True).start()
|
|
8105
|
+
|
|
8106
|
+
def _intent_before(all_msgs, idx):
|
|
8107
|
+
# Most recent human message text before idx \u2014 the turn's user intent for grading.
|
|
8108
|
+
for j in range(idx - 1, -1, -1):
|
|
8109
|
+
if all_msgs[j].get("sender") == "human":
|
|
8110
|
+
for b in _msg_blocks(all_msgs[j]):
|
|
8111
|
+
if isinstance(b, dict) and b.get("type") == "text": return (b.get("text") or "")[:2000]
|
|
8112
|
+
if isinstance(b, str): return b[:2000]
|
|
8113
|
+
return ""
|
|
8114
|
+
return ""
|
|
8115
|
+
|
|
8116
|
+
def _apply_turn_verdicts(all_msgs, convo_id):
|
|
8117
|
+
# WHOLE-CONVERSATION per-turn DLP scrub. Mutates all_msgs in place: replaces every
|
|
8118
|
+
# BLOCKED assistant turn's content with the notice. Returns True if anything was
|
|
8119
|
+
# blocked. Verdicts are durable (PGLite via the local endpoints); each connector
|
|
8120
|
+
# turn (a tool_result is present) is graded once on a miss then persisted; pure-chat
|
|
8121
|
+
# turns are skipped. Scrubs EVERY blocked turn \u2014 not just the latest \u2014 so a follow-up
|
|
8122
|
+
# can never resurrect an earlier blocked turn. Fail-OPEN.
|
|
8123
|
+
asst_idx = [i for i in range(len(all_msgs)) if all_msgs[i].get("sender") != "human"]
|
|
8124
|
+
keys = {i: _msg_key(all_msgs[i]) for i in asst_idx}
|
|
8125
|
+
ids = [k for k in keys.values() if k]
|
|
8126
|
+
verdicts = _get_turn_verdicts(ids) if ids else {}
|
|
8127
|
+
do_grade = _should_hold() and SCAN_TURN_URL and TOKEN
|
|
8128
|
+
any_blocked = False
|
|
8129
|
+
for i in asst_idx:
|
|
8130
|
+
key = keys.get(i, "")
|
|
8131
|
+
v = verdicts.get(key) if key else None
|
|
8132
|
+
if v is None and do_grade and key and _msg_is_connector(all_msgs[i]):
|
|
8133
|
+
text, tools, connector = _msg_grade_payload(all_msgs[i])
|
|
8134
|
+
if tools.strip() or text.strip():
|
|
8135
|
+
scan_msgs = [
|
|
8136
|
+
{"role": "user", "content": _intent_before(all_msgs, i), "name": ""},
|
|
8137
|
+
{"role": "tool", "content": tools[:500000], "name": connector[:200]},
|
|
8138
|
+
{"role": "assistant", "content": text[:8000], "name": ""},
|
|
8139
|
+
]
|
|
8140
|
+
reason = _scan_turn_call(convo_id, scan_msgs, connector)
|
|
8141
|
+
v = {"blocked": reason is not None, "reason": reason or ""}
|
|
8142
|
+
_put_turn_verdict(key, convo_id, v["blocked"], v["reason"])
|
|
8143
|
+
if v and v.get("blocked"):
|
|
8144
|
+
all_msgs[i]["content"] = [{"type": "text", "text": "[Blocked by Synkro] " + (v.get("reason") or "restricted data")}]
|
|
8145
|
+
any_blocked = True
|
|
8146
|
+
return any_blocked
|
|
8147
|
+
|
|
8148
|
+
def _feed_answer(chunk, st):
|
|
8149
|
+
# Accumulate the assistant's ANSWER text (text_delta) as the held stream flows,
|
|
8150
|
+
# using a carry for SSE lines split across chunks. The answer reveals the doc's
|
|
8151
|
+
# classification, so once enough has arrived we grade it for an early verdict.
|
|
8152
|
+
txt = st["carry"] + chunk
|
|
8153
|
+
lines = txt.split("\\n")
|
|
8154
|
+
st["carry"] = lines[-1][-100000:] # bound carried partial line (CWE-400)
|
|
8155
|
+
for ln in lines[:-1]:
|
|
8156
|
+
if not ln.startswith("data: "):
|
|
8157
|
+
continue
|
|
8158
|
+
try:
|
|
8159
|
+
obj = json.loads(ln[6:])
|
|
8160
|
+
except Exception:
|
|
8161
|
+
continue
|
|
8162
|
+
d = obj.get("delta")
|
|
8163
|
+
if isinstance(d, dict) and d.get("type") == "text_delta" and isinstance(d.get("text"), str):
|
|
8164
|
+
if len(st["answer"]) < 200000: # bound (CWE-400)
|
|
8165
|
+
st["answer"] += d["text"]
|
|
8166
|
+
|
|
8167
|
+
def _grade_worker(st, ans):
|
|
8168
|
+
# Runs in a BACKGROUND THREAD (off mitmproxy's event loop). The urllib grade
|
|
8169
|
+
# releases the GIL during I/O, so the loop keeps flushing keep-alive pings.
|
|
8170
|
+
# CWE-362: ans is a snapshot PASSED BY the loop thread (the sole writer of
|
|
8171
|
+
# st["answer"]) at thread-creation \u2014 the worker never reads shared mutable
|
|
8172
|
+
# st["answer"], so there is no data race. It writes only verdict/graded, which the
|
|
8173
|
+
# loop reads AFTER graded.is_set() provides the happens-before barrier.
|
|
8174
|
+
try:
|
|
8175
|
+
ans = ans[:200000]
|
|
8176
|
+
reason = _scan_turn_call(st["cid"], [{"role": "assistant", "content": ans, "name": ""}], "") if ans.strip() else None
|
|
8177
|
+
except Exception:
|
|
8178
|
+
reason = None
|
|
8179
|
+
st["verdict"] = {"block": reason is not None, "reason": reason or ""}
|
|
8180
|
+
_cache_turn_verdict(st["cid"], st["verdict"])
|
|
8181
|
+
st["graded"].set()
|
|
8182
|
+
|
|
8183
|
+
def _classify_frame(fr):
|
|
8184
|
+
# Inspect one CRLF SSE frame's data line \u2192 'tool_use_start' | 'text_start' | 'other'
|
|
8185
|
+
# based on the content_block_start type. Used to find where the FINAL answer begins.
|
|
8186
|
+
for ln in fr.split("\\r\\n"):
|
|
8187
|
+
if ln.startswith("data: "):
|
|
8188
|
+
try:
|
|
8189
|
+
o = json.loads(ln[6:])
|
|
8190
|
+
except Exception:
|
|
8191
|
+
return "other"
|
|
8192
|
+
if o.get("type") == "content_block_start":
|
|
8193
|
+
cb = o.get("content_block")
|
|
8194
|
+
if isinstance(cb, dict):
|
|
8195
|
+
t = cb.get("type")
|
|
8196
|
+
if t == "tool_use":
|
|
8197
|
+
return "tool_use_start"
|
|
8198
|
+
if t == "text":
|
|
8199
|
+
return "text_start"
|
|
8200
|
+
return "other"
|
|
8201
|
+
return "other"
|
|
8202
|
+
|
|
8203
|
+
def _frame_answer_text(fr, st):
|
|
8204
|
+
# Accumulate text_delta text from a HELD frame into st["answer"] (for the grade).
|
|
8205
|
+
for ln in fr.split("\\r\\n"):
|
|
8206
|
+
if ln.startswith("data: "):
|
|
8207
|
+
try:
|
|
8208
|
+
d = json.loads(ln[6:]).get("delta")
|
|
8209
|
+
except Exception:
|
|
8210
|
+
continue
|
|
8211
|
+
if isinstance(d, dict) and d.get("type") == "text_delta" and isinstance(d.get("text"), str):
|
|
8212
|
+
if len(st["answer"]) < 200000: # CWE-400 bound
|
|
8213
|
+
st["answer"] += d["text"]
|
|
8214
|
+
|
|
8215
|
+
def _block_held(frames, notice):
|
|
8216
|
+
# Frame-aware, CRLF-PRESERVING block render of the held final-answer frames: replace
|
|
8217
|
+
# the answer's text_delta(s) (first\u2192notice, rest\u2192"") and leave every structural frame
|
|
8218
|
+
# (content_block_stop / message_delta / message_stop) intact so the message still
|
|
8219
|
+
# finalizes cleanly. Operates per-frame on \\r\\n lines \u2192 no \\n/\\r\\n mixing, so the
|
|
8220
|
+
# framing stays byte-clean (the thing _neutralize_body's \\n split would have broken).
|
|
8221
|
+
inj = [False]
|
|
8222
|
+
out = []
|
|
8223
|
+
for fr in frames:
|
|
8224
|
+
nl = []
|
|
8225
|
+
for ln in fr.split("\\r\\n"):
|
|
8226
|
+
if ln.startswith("data: "):
|
|
8227
|
+
try:
|
|
8228
|
+
o = json.loads(ln[6:])
|
|
8229
|
+
d = o.get("delta")
|
|
8230
|
+
if isinstance(d, dict) and d.get("type") == "text_delta" and isinstance(d.get("text"), str):
|
|
8231
|
+
o["delta"]["text"] = notice if not inj[0] else ""
|
|
8232
|
+
inj[0] = True
|
|
8233
|
+
ln = "data: " + json.dumps(o)
|
|
8234
|
+
except Exception:
|
|
8235
|
+
pass
|
|
8236
|
+
nl.append(ln)
|
|
8237
|
+
out.append("\\r\\n".join(nl))
|
|
8238
|
+
return ("\\r\\n\\r\\n".join(out) + "\\r\\n\\r\\n") if out else ""
|
|
8239
|
+
|
|
8240
|
+
def _make_hold_stream(cid):
|
|
8241
|
+
# Pass-through-until-FINAL-ANSWER hold for the claude.ai completion (CRLF SSE; frames
|
|
8242
|
+
# split by \\r\\n\\r\\n). A connector turn is: thinking \u2192 text(preamble) \u2192 tool_use \u2192
|
|
8243
|
+
# tool_result \u2192 \u2026 \u2192 text(ANSWER). Only the FINAL answer is in the user's face, so we
|
|
8244
|
+
# STREAM every earlier frame VERBATIM (natural thinking/search activity + tool chips,
|
|
8245
|
+
# zero mutation) and HOLD only the final answer \u2014 the text block that opens AFTER a
|
|
8246
|
+
# tool_use \u2014 through message_stop. Buffer it (no leak), grade off-thread, heartbeat to
|
|
8247
|
+
# keep Desktop alive, then emit the real answer (clean) or a frame-aware block rewrite
|
|
8248
|
+
# (restricted). CWE-362: the grade thread gets an immutable answer snapshot; the
|
|
8249
|
+
# verdict returns via the Event barrier; the loop is the sole writer of held/answer.
|
|
8250
|
+
st = {"cid": cid, "carry": "", "holding": False, "seen_tool": False,
|
|
8251
|
+
"held": [], "held_size": 0, "answer": "", "started": False, "meta": _new_scan_state(),
|
|
8252
|
+
"graded": threading.Event(), "verdict": {"block": False, "reason": ""}}
|
|
8253
|
+
def _hold(data, _st=st):
|
|
8254
|
+
if data:
|
|
8255
|
+
txt = _st["carry"] + data.decode("utf-8", "replace")
|
|
8256
|
+
frames = txt.split("\\r\\n\\r\\n")
|
|
8257
|
+
_st["carry"] = frames[-1][-200000:] # CWE-400 bounded carry (last partial frame)
|
|
8258
|
+
out = []
|
|
8259
|
+
for fr in frames[:-1]:
|
|
8260
|
+
for ln in fr.split("\\r\\n"):
|
|
8261
|
+
_scan_sse_line(ln, _st["meta"]) # governance metadata, all frames
|
|
8262
|
+
if not _st["holding"]:
|
|
8263
|
+
k = _classify_frame(fr)
|
|
8264
|
+
if k == "tool_use_start":
|
|
8265
|
+
_st["seen_tool"] = True
|
|
8266
|
+
if k == "text_start" and _st["seen_tool"]:
|
|
8267
|
+
_st["holding"] = True # the FINAL answer begins here \u2192 hold from now
|
|
8268
|
+
else:
|
|
8269
|
+
out.append(fr) # thinking / preamble / tool_use / tool_result \u2192 verbatim
|
|
8270
|
+
continue
|
|
8271
|
+
if _st["held_size"] < 8000000: # CWE-400 bound before append
|
|
8272
|
+
_st["held"].append(fr); _st["held_size"] += len(fr)
|
|
8273
|
+
_frame_answer_text(fr, _st)
|
|
8274
|
+
if _st["holding"] and not _st["started"] and len(_st["answer"]) >= 1200:
|
|
8275
|
+
_st["started"] = True
|
|
8276
|
+
threading.Thread(target=_grade_worker, args=(_st, _st["answer"][:200000]), daemon=True).start()
|
|
8277
|
+
if out:
|
|
8278
|
+
return ("\\r\\n\\r\\n".join(out) + "\\r\\n\\r\\n").encode("utf-8") # stream activity verbatim
|
|
8279
|
+
return b": synkro\\n\\n" # answer held \u2192 heartbeat (keeps Desktop alive, PROVEN)
|
|
8280
|
+
# upstream ended
|
|
8281
|
+
m = _st["meta"]
|
|
8282
|
+
if (m["usage"] or m["sources"] or m["citations"]) and _st["cid"]:
|
|
8283
|
+
with _sse_lock:
|
|
8284
|
+
_sse_meta[_st["cid"]] = {"usage": m["usage"], "sources": m["sources"], "citations": m["citations"]}
|
|
8285
|
+
if not _st["holding"]: # no final-answer block detected \u2014 everything already streamed
|
|
8286
|
+
return _st["carry"].encode("utf-8") if _st["carry"] else b""
|
|
8287
|
+
if _st["carry"]:
|
|
8288
|
+
_st["held"].append(_st["carry"]); _frame_answer_text(_st["carry"], _st); _st["carry"] = ""
|
|
8289
|
+
if not _st["started"] and _st["answer"].strip(): # short answer never hit the 1200 trigger
|
|
8290
|
+
_st["started"] = True
|
|
8291
|
+
threading.Thread(target=_grade_worker, args=(_st, _st["answer"][:200000]), daemon=True).start()
|
|
8292
|
+
held_frames = list(_st["held"])
|
|
8293
|
+
def _grade_window(_st2=_st, _frames=held_frames):
|
|
8294
|
+
waited = 0.0
|
|
8295
|
+
while not _st2["graded"].is_set() and waited < 60.0:
|
|
8296
|
+
yield b": synkro\\n\\n" # heartbeat WHILE grading \u2014 keeps Desktop alive (PROVEN)
|
|
8297
|
+
time.sleep(0.5)
|
|
8298
|
+
waited += 0.5
|
|
8299
|
+
v = _st2["verdict"]
|
|
8300
|
+
if _st2["graded"].is_set() and v["block"]:
|
|
8301
|
+
print("[synkro] HELD+BLOCKED live (reason=" + v["reason"][:60] + ")")
|
|
8302
|
+
yield _block_held(_frames, "[Blocked by Synkro] " + v["reason"]).encode("utf-8")
|
|
8303
|
+
elif _st2["graded"].is_set():
|
|
8304
|
+
yield ("\\r\\n\\r\\n".join(_frames) + "\\r\\n\\r\\n").encode("utf-8") # clean \u2192 real answer
|
|
8305
|
+
else: # grade never landed in 60s \u2014 never leak; tree GET backstop finalizes
|
|
8306
|
+
print("[synkro] hold: grade timeout \u2192 placeholder + tree-GET backstop")
|
|
8307
|
+
yield _block_held(_frames, "[Synkro is reviewing this response\u2026]").encode("utf-8")
|
|
8308
|
+
return _grade_window()
|
|
8309
|
+
return _hold
|
|
8310
|
+
|
|
7734
8311
|
class SynkroTap:
|
|
7735
8312
|
def request(self, flow):
|
|
7736
8313
|
# \u2500\u2500 Upload DLP gate \u2500\u2500 Scan file uploads BEFORE they reach claude.ai and
|
|
@@ -7785,9 +8362,16 @@ class SynkroTap:
|
|
|
7785
8362
|
return
|
|
7786
8363
|
if verdict.get("block"):
|
|
7787
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.
|
|
7788
8372
|
flow.response = http.Response.make(
|
|
7789
|
-
|
|
7790
|
-
json.dumps({"error": {"type": "
|
|
8373
|
+
400,
|
|
8374
|
+
json.dumps({"type": "error", "error": {"type": "invalid_request_error", "message": msg}}).encode(),
|
|
7791
8375
|
{"Content-Type": "application/json"},
|
|
7792
8376
|
)
|
|
7793
8377
|
print("[synkro] BLOCKED upload (size=" + str(payload.get("size", 0)) + " rule=" + str(verdict.get("ruleId", "")) + ")")
|
|
@@ -7795,13 +8379,14 @@ class SynkroTap:
|
|
|
7795
8379
|
print("[synkro] upload scanned -> allow (size=" + str(payload.get("size", 0)) + ")")
|
|
7796
8380
|
|
|
7797
8381
|
def responseheaders(self, flow):
|
|
7798
|
-
#
|
|
7799
|
-
#
|
|
7800
|
-
#
|
|
7801
|
-
#
|
|
7802
|
-
#
|
|
8382
|
+
# The answer the user watches TYPE OUT comes from claude.ai's completion \u2014 THAT
|
|
8383
|
+
# is the stream we HOLD (buffer + heartbeat \u2192 no leak, no disconnect; see
|
|
8384
|
+
# _make_hold_stream). a-api.anthropic.com is a background call that doesn't carry
|
|
8385
|
+
# the answer, so it streams through untouched (default below). The tree GET is
|
|
8386
|
+
# buffered for the dashboard + the durable block scrub.
|
|
7803
8387
|
r = flow.request
|
|
7804
|
-
|
|
8388
|
+
host = r.pretty_host or ""
|
|
8389
|
+
if "claude.ai" in host and "chat_conversations" in r.path:
|
|
7805
8390
|
if r.method == "GET" and "tree" in r.pretty_url:
|
|
7806
8391
|
return
|
|
7807
8392
|
if r.method == "POST" and "completion" in r.path:
|
|
@@ -7809,16 +8394,12 @@ class SynkroTap:
|
|
|
7809
8394
|
cid = cm.group(1) if cm else ""
|
|
7810
8395
|
with _mcp_lock:
|
|
7811
8396
|
has_servers = len(_mcp_registry) > 0
|
|
7812
|
-
#
|
|
7813
|
-
#
|
|
7814
|
-
# response() can grade it and neutralize restricted content BEFORE the
|
|
7815
|
-
# user sees a single token. Audit-only / no-connector turns skip this
|
|
7816
|
-
# entirely and stream as before \u2014 zero added latency.
|
|
8397
|
+
# HOLD when a BLOCK-mode rule exists AND connectors are connected \u2014 buffer
|
|
8398
|
+
# the answer, grade off-thread, heartbeat through the grade, emit the verdict.
|
|
7817
8399
|
if has_servers and _should_hold():
|
|
7818
|
-
flow.
|
|
8400
|
+
flow.response.stream = _make_hold_stream(cid)
|
|
7819
8401
|
return
|
|
7820
|
-
#
|
|
7821
|
-
# governance metadata only, never buffer the body. Audit stays async.
|
|
8402
|
+
# Otherwise just TAP for governance metadata (no hold; audit stays async).
|
|
7822
8403
|
st = _new_scan_state()
|
|
7823
8404
|
def _tap(data, _st=st, _cid=cid):
|
|
7824
8405
|
if data:
|
|
@@ -7845,36 +8426,6 @@ class SynkroTap:
|
|
|
7845
8426
|
def response(self, flow):
|
|
7846
8427
|
r = flow.request
|
|
7847
8428
|
if "claude.ai" not in (r.pretty_host or ""): return
|
|
7848
|
-
# \u2500\u2500 M3: a HELD completion (buffered for inline grading) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7849
|
-
# We have the full SSE body now. Re-derive governance metadata (same as the
|
|
7850
|
-
# streaming path), then \u2014 if the turn used a tool/connector \u2014 grade it
|
|
7851
|
-
# SYNCHRONOUSLY and, on a block, NEUTRALIZE the response so the user never
|
|
7852
|
-
# sees the restricted content. Verdict is cached for the tree GET to reuse.
|
|
7853
|
-
hold_cid = (flow.metadata.get("synkro_hold_cid") if hasattr(flow, "metadata") else None)
|
|
7854
|
-
if hold_cid is not None and "completion" in r.path and flow.response:
|
|
7855
|
-
try:
|
|
7856
|
-
body = flow.response.get_text() or ""
|
|
7857
|
-
except Exception:
|
|
7858
|
-
body = ""
|
|
7859
|
-
st = _new_scan_state()
|
|
7860
|
-
for ln in body.split("\\n"):
|
|
7861
|
-
_scan_sse_line(ln, st)
|
|
7862
|
-
if (st["usage"] or st["sources"] or st["citations"]) and hold_cid:
|
|
7863
|
-
with _sse_lock:
|
|
7864
|
-
_sse_meta[hold_cid] = {"usage": st["usage"], "sources": st["sources"], "citations": st["citations"]}
|
|
7865
|
-
text, has_tool = _extract_sse_text(body)
|
|
7866
|
-
if has_tool:
|
|
7867
|
-
# The turn used a connector \u2014 HOLD the live view: replace the streamed
|
|
7868
|
-
# content with a "reviewing" placeholder so the user never sees the raw
|
|
7869
|
-
# response. The tree GET is the SINGLE grader and renders the final
|
|
7870
|
-
# state (real content if clean, block notice if restricted) \u2014 so there
|
|
7871
|
-
# is exactly one full-document grade per turn (no double-grade race).
|
|
7872
|
-
try:
|
|
7873
|
-
flow.response.text = _neutralize_sse(body, REVIEWING_NOTICE)
|
|
7874
|
-
print("[synkro] HELD completion -> reviewing placeholder")
|
|
7875
|
-
except Exception as e:
|
|
7876
|
-
print("[synkro] hold placeholder failed (fail-open): " + str(e))
|
|
7877
|
-
return
|
|
7878
8429
|
# MCP server registry from the bootstrap SSE stream.
|
|
7879
8430
|
if "mcp" in r.path and "bootstrap" in r.path and flow.response:
|
|
7880
8431
|
try:
|
|
@@ -7897,6 +8448,12 @@ class SynkroTap:
|
|
|
7897
8448
|
_mcp_registry.clear()
|
|
7898
8449
|
_mcp_registry.update(servers)
|
|
7899
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))
|
|
7900
8457
|
except Exception:
|
|
7901
8458
|
pass
|
|
7902
8459
|
return
|
|
@@ -7906,6 +8463,12 @@ class SynkroTap:
|
|
|
7906
8463
|
except: return
|
|
7907
8464
|
m = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7908
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
|
|
7909
8472
|
model = convo.get("model", "unknown")
|
|
7910
8473
|
settings = convo.get("settings") or {}
|
|
7911
8474
|
emt = settings.get("enabled_mcp_tools")
|
|
@@ -7938,7 +8501,14 @@ class SynkroTap:
|
|
|
7938
8501
|
elif bt == "thinking":
|
|
7939
8502
|
thinking += b.get("thinking", "") or b.get("text", "")
|
|
7940
8503
|
elif bt == "tool_use" or bt == "server_tool_use":
|
|
7941
|
-
|
|
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)
|
|
7942
8512
|
elif bt.endswith("_tool_result") or bt == "tool_result":
|
|
7943
8513
|
rc = b.get("content", "")
|
|
7944
8514
|
if isinstance(rc, list):
|
|
@@ -7975,6 +8545,16 @@ class SynkroTap:
|
|
|
7975
8545
|
messages.append({"role": "user", "content": text})
|
|
7976
8546
|
if not messages: return
|
|
7977
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)
|
|
8550
|
+
# PER-TURN key: the current turn's assistant message id (same key used for
|
|
8551
|
+
# dlp_turn_verdicts). Lets the server store each turn as its OWN event so a
|
|
8552
|
+
# blocked turn isn't overwritten/deleted by a later passing turn.
|
|
8553
|
+
turn_id = ""
|
|
8554
|
+
for _m in reversed(chat):
|
|
8555
|
+
if _m.get("sender") != "human":
|
|
8556
|
+
turn_id = _msg_key(_m); break
|
|
8557
|
+
if turn_id: data["turn_id"] = turn_id
|
|
7978
8558
|
meta = {}
|
|
7979
8559
|
if platform: meta["platform"] = platform
|
|
7980
8560
|
if web_search_on: meta["web_search_enabled"] = True
|
|
@@ -7992,32 +8572,14 @@ class SynkroTap:
|
|
|
7992
8572
|
nsrc = len((sm or {}).get("sources") or [])
|
|
7993
8573
|
print("[synkro] captured chat (msgs=" + str(len(messages)) + " tools=" + str(ntools) + " sources=" + str(nsrc) + " model=" + model + ")")
|
|
7994
8574
|
|
|
7995
|
-
# \u2500\u2500 Tier-2 connector DLP gate \u2500\u2500
|
|
7996
|
-
#
|
|
7997
|
-
#
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
# SINGLE grader: the held completion only shows a "reviewing" placeholder, so
|
|
8004
|
-
# the tree GET is the one and only place a turn is graded \u2014 exactly one
|
|
8005
|
-
# full-document grade per turn (no double-grade). Gated on a BLOCK-mode rule
|
|
8006
|
-
# existing; audit-only orgs (_should_hold False) skip this and stay async.
|
|
8007
|
-
if _should_hold() and SCAN_TURN_URL and TOKEN and has_connector:
|
|
8008
|
-
# CWE-20/CWE-400: bounded, shape-validated payload (<=200 msgs, <=2MB).
|
|
8009
|
-
scan_msgs = []
|
|
8010
|
-
budget = 2000000
|
|
8011
|
-
for m in messages[:200]:
|
|
8012
|
-
if budget <= 0: break
|
|
8013
|
-
c = str(m.get("content", ""))[:min(500000, budget)]
|
|
8014
|
-
budget -= len(c)
|
|
8015
|
-
scan_msgs.append({"role": str(m.get("role", "user"))[:20], "content": c, "name": str(m.get("name", ""))[:200]})
|
|
8016
|
-
connector = ""
|
|
8017
|
-
for m in scan_msgs:
|
|
8018
|
-
if m["role"] == "tool" and m["name"]:
|
|
8019
|
-
nm = m["name"]; connector = (nm.split(":")[0] if ":" in nm else nm); break
|
|
8020
|
-
blocked_reason = _scan_turn_call(convo_id, scan_msgs, connector)
|
|
8575
|
+
# \u2500\u2500 Tier-2 connector DLP gate (WHOLE-CONVERSATION, per-turn, PERSISTENT) \u2500\u2500
|
|
8576
|
+
# Scrub every blocked assistant turn (durable PGLite verdicts; connector turns
|
|
8577
|
+
# graded once on miss). See _apply_turn_verdicts. Fail-OPEN on any error.
|
|
8578
|
+
try:
|
|
8579
|
+
any_blocked = _apply_turn_verdicts(all_msgs, convo_id)
|
|
8580
|
+
except Exception as e:
|
|
8581
|
+
any_blocked = False
|
|
8582
|
+
print("[synkro] turn-verdict scrub error (fail-open): " + str(e))
|
|
8021
8583
|
|
|
8022
8584
|
# Capture the REAL content for audit regardless of the block decision.
|
|
8023
8585
|
def send(_d=data):
|
|
@@ -8027,17 +8589,11 @@ class SynkroTap:
|
|
|
8027
8589
|
print("[synkro] capture error: " + str(e))
|
|
8028
8590
|
threading.Thread(target=send, daemon=True).start()
|
|
8029
8591
|
|
|
8030
|
-
#
|
|
8031
|
-
|
|
8032
|
-
# the restricted content + any model output derived from it is never shown.
|
|
8033
|
-
if blocked_reason is not None:
|
|
8034
|
-
notice = "[Blocked by Synkro] " + blocked_reason
|
|
8035
|
-
for i in range(last_human + 1, len(all_msgs)):
|
|
8036
|
-
if all_msgs[i].get("sender") != "human":
|
|
8037
|
-
all_msgs[i]["content"] = [{"type": "text", "text": notice}]
|
|
8592
|
+
# Re-serialize the tree so the renderer never sees any blocked turn's content.
|
|
8593
|
+
if any_blocked:
|
|
8038
8594
|
try:
|
|
8039
8595
|
flow.response.text = json.dumps(convo)
|
|
8040
|
-
print("[synkro] BLOCKED turn view (
|
|
8596
|
+
print("[synkro] BLOCKED turn view (whole-convo per-turn, durable)")
|
|
8041
8597
|
except Exception as e:
|
|
8042
8598
|
print("[synkro] block scrub failed (fail-open): " + str(e))
|
|
8043
8599
|
|
|
@@ -8221,16 +8777,16 @@ __export(macKeychain_exports, {
|
|
|
8221
8777
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
8222
8778
|
writeRefreshAgent: () => writeRefreshAgent
|
|
8223
8779
|
});
|
|
8224
|
-
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";
|
|
8225
8781
|
import { homedir as homedir6, platform as platform2 } from "os";
|
|
8226
8782
|
import { join as join6 } from "path";
|
|
8227
|
-
import { spawnSync } from "child_process";
|
|
8783
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
8228
8784
|
function needsKeychainBridge() {
|
|
8229
8785
|
return platform2() === "darwin";
|
|
8230
8786
|
}
|
|
8231
8787
|
function readKeychainCreds() {
|
|
8232
8788
|
if (platform2() !== "darwin") return null;
|
|
8233
|
-
const r =
|
|
8789
|
+
const r = spawnSync2("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
|
|
8234
8790
|
encoding: "utf-8",
|
|
8235
8791
|
timeout: 5e3
|
|
8236
8792
|
});
|
|
@@ -8243,7 +8799,14 @@ function exportKeychainCreds() {
|
|
|
8243
8799
|
if (!blob) return null;
|
|
8244
8800
|
mkdirSync7(CLAUDE_CREDS_DIR, { recursive: true });
|
|
8245
8801
|
chmodSync(CLAUDE_CREDS_DIR, 448);
|
|
8246
|
-
|
|
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");
|
|
8247
8810
|
chmodSync(CLAUDE_CREDS_FILE, 384);
|
|
8248
8811
|
return CLAUDE_CREDS_FILE;
|
|
8249
8812
|
}
|
|
@@ -8286,18 +8849,49 @@ async function validateCursorApiKey() {
|
|
|
8286
8849
|
function credsAreStale() {
|
|
8287
8850
|
if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
|
|
8288
8851
|
try {
|
|
8289
|
-
const
|
|
8290
|
-
|
|
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;
|
|
8291
8856
|
} catch {
|
|
8292
8857
|
return true;
|
|
8293
8858
|
}
|
|
8294
8859
|
}
|
|
8860
|
+
function xmlEscape(s) {
|
|
8861
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8862
|
+
}
|
|
8295
8863
|
function writeRefreshAgent(synkroBinPath) {
|
|
8296
8864
|
if (platform2() !== "darwin") {
|
|
8297
8865
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
8298
8866
|
}
|
|
8299
8867
|
mkdirSync7(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
8300
|
-
|
|
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);
|
|
8301
8895
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
8302
8896
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
8303
8897
|
<plist version="1.0">
|
|
@@ -8307,17 +8901,25 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
8307
8901
|
<key>ProgramArguments</key>
|
|
8308
8902
|
<array>
|
|
8309
8903
|
<string>/bin/bash</string>
|
|
8310
|
-
<string
|
|
8311
|
-
<string>${shellCmd}</string>
|
|
8904
|
+
<string>${xmlEscape(REFRESH_SCRIPT)}</string>
|
|
8312
8905
|
</array>
|
|
8313
|
-
<key>
|
|
8314
|
-
<
|
|
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/>
|
|
8315
8915
|
<key>RunAtLoad</key>
|
|
8316
8916
|
<true/>
|
|
8917
|
+
<key>ThrottleInterval</key>
|
|
8918
|
+
<integer>${MIN_REFRESH_INTERVAL_SECONDS}</integer>
|
|
8317
8919
|
<key>StandardErrorPath</key>
|
|
8318
|
-
<string>${
|
|
8920
|
+
<string>${xmlEscape(REFRESH_LOG)}</string>
|
|
8319
8921
|
<key>StandardOutPath</key>
|
|
8320
|
-
<string>${
|
|
8922
|
+
<string>${xmlEscape(REFRESH_LOG)}</string>
|
|
8321
8923
|
</dict>
|
|
8322
8924
|
</plist>
|
|
8323
8925
|
`;
|
|
@@ -8326,11 +8928,11 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
8326
8928
|
}
|
|
8327
8929
|
function loadRefreshAgent() {
|
|
8328
8930
|
if (platform2() !== "darwin") return;
|
|
8329
|
-
|
|
8931
|
+
spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8330
8932
|
encoding: "utf-8",
|
|
8331
8933
|
timeout: 5e3
|
|
8332
8934
|
});
|
|
8333
|
-
const r =
|
|
8935
|
+
const r = spawnSync2("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8334
8936
|
encoding: "utf-8",
|
|
8335
8937
|
timeout: 5e3
|
|
8336
8938
|
});
|
|
@@ -8342,14 +8944,14 @@ function loadRefreshAgent() {
|
|
|
8342
8944
|
}
|
|
8343
8945
|
function uninstallRefreshAgent() {
|
|
8344
8946
|
if (platform2() !== "darwin") return;
|
|
8345
|
-
|
|
8947
|
+
spawnSync2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
|
|
8346
8948
|
encoding: "utf-8",
|
|
8347
8949
|
timeout: 5e3
|
|
8348
8950
|
});
|
|
8349
8951
|
try {
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8952
|
+
const fs = __require("fs");
|
|
8953
|
+
if (existsSync8(LAUNCHD_PLIST)) fs.unlinkSync(LAUNCHD_PLIST);
|
|
8954
|
+
if (existsSync8(REFRESH_SCRIPT)) fs.unlinkSync(REFRESH_SCRIPT);
|
|
8353
8955
|
} catch {
|
|
8354
8956
|
}
|
|
8355
8957
|
}
|
|
@@ -8364,7 +8966,7 @@ function readExportedCreds() {
|
|
|
8364
8966
|
return null;
|
|
8365
8967
|
}
|
|
8366
8968
|
}
|
|
8367
|
-
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;
|
|
8368
8970
|
var init_macKeychain = __esm({
|
|
8369
8971
|
"cli/local-cc/macKeychain.ts"() {
|
|
8370
8972
|
"use strict";
|
|
@@ -8376,7 +8978,11 @@ var init_macKeychain = __esm({
|
|
|
8376
8978
|
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
8377
8979
|
LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
|
|
8378
8980
|
LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
8379
|
-
|
|
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;
|
|
8380
8986
|
KeychainExportError = class extends Error {
|
|
8381
8987
|
constructor(message, cause) {
|
|
8382
8988
|
super(message);
|
|
@@ -8415,7 +9021,7 @@ __export(dockerInstall_exports, {
|
|
|
8415
9021
|
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
8416
9022
|
import { homedir as homedir7 } from "os";
|
|
8417
9023
|
import { join as join7 } from "path";
|
|
8418
|
-
import { execSync as execSync4, spawnSync as
|
|
9024
|
+
import { execSync as execSync4, spawnSync as spawnSync3 } from "child_process";
|
|
8419
9025
|
function splitWorkers(total, providers) {
|
|
8420
9026
|
const t = Math.max(0, Math.floor(total));
|
|
8421
9027
|
const hasClaude = providers.includes("claude_code");
|
|
@@ -8574,7 +9180,7 @@ function imageTag() {
|
|
|
8574
9180
|
return registry ? `${registry.replace(/\/+$/, "")}/${tag.replace(/^.*\//, "")}` : tag;
|
|
8575
9181
|
}
|
|
8576
9182
|
function assertDockerAvailable() {
|
|
8577
|
-
const v =
|
|
9183
|
+
const v = spawnSync3("docker", ["version", "--format", "{{.Server.Version}}"], {
|
|
8578
9184
|
encoding: "utf-8",
|
|
8579
9185
|
timeout: 5e3
|
|
8580
9186
|
});
|
|
@@ -8589,12 +9195,12 @@ function claudeCredsHostDir() {
|
|
|
8589
9195
|
return join7(homedir7(), ".claude");
|
|
8590
9196
|
}
|
|
8591
9197
|
function resolveSynkroBin() {
|
|
8592
|
-
const which2 =
|
|
9198
|
+
const which2 = spawnSync3("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
|
|
8593
9199
|
const resolved = (which2.stdout || "").split("\n")[0].trim();
|
|
8594
9200
|
return resolved || "synkro";
|
|
8595
9201
|
}
|
|
8596
9202
|
function sweepHostPglite() {
|
|
8597
|
-
const ps =
|
|
9203
|
+
const ps = spawnSync3("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
|
|
8598
9204
|
if (ps.status !== 0) return;
|
|
8599
9205
|
const targets = [];
|
|
8600
9206
|
for (const line of (ps.stdout || "").split("\n")) {
|
|
@@ -8624,7 +9230,7 @@ function sweepHostPglite() {
|
|
|
8624
9230
|
}
|
|
8625
9231
|
});
|
|
8626
9232
|
if (alive.length === 0) return;
|
|
8627
|
-
|
|
9233
|
+
spawnSync3("sleep", ["0.2"], { timeout: 1e3 });
|
|
8628
9234
|
}
|
|
8629
9235
|
for (const pid of targets) {
|
|
8630
9236
|
try {
|
|
@@ -8676,13 +9282,13 @@ async function dockerInstall(opts = {}) {
|
|
|
8676
9282
|
} else {
|
|
8677
9283
|
mkdirSync8(join7(homedir7(), ".claude"), { recursive: true });
|
|
8678
9284
|
}
|
|
8679
|
-
const imageExistsLocally = () =>
|
|
9285
|
+
const imageExistsLocally = () => spawnSync3("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
8680
9286
|
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
8681
9287
|
if (skipPull && imageExistsLocally()) {
|
|
8682
9288
|
console.log(` Using local image ${image} (SYNKRO_SKIP_PULL set \u2014 pull skipped).`);
|
|
8683
9289
|
} else {
|
|
8684
9290
|
console.log(` Pulling ${image}...`);
|
|
8685
|
-
const pull =
|
|
9291
|
+
const pull = spawnSync3("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
|
|
8686
9292
|
if (pull.status !== 0) {
|
|
8687
9293
|
if (imageExistsLocally()) {
|
|
8688
9294
|
console.warn(` \u26A0 docker pull ${image} failed \u2014 falling back to the existing local image.`);
|
|
@@ -8696,7 +9302,7 @@ async function dockerInstall(opts = {}) {
|
|
|
8696
9302
|
console.log(" Stopping existing container gracefully...");
|
|
8697
9303
|
await dockerSafeStop();
|
|
8698
9304
|
}
|
|
8699
|
-
|
|
9305
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
8700
9306
|
sweepHostPglite();
|
|
8701
9307
|
const credsDir = claudeCredsHostDir();
|
|
8702
9308
|
const args2 = [
|
|
@@ -8754,7 +9360,7 @@ async function dockerInstall(opts = {}) {
|
|
|
8754
9360
|
...process.env.SYNKRO_EMAIL ? ["-e", `SYNKRO_EMAIL=${process.env.SYNKRO_EMAIL}`] : [],
|
|
8755
9361
|
image
|
|
8756
9362
|
];
|
|
8757
|
-
const run =
|
|
9363
|
+
const run = spawnSync3("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
8758
9364
|
if (run.status !== 0) {
|
|
8759
9365
|
throw new DockerInstallError(`docker run failed (image ${image})`);
|
|
8760
9366
|
}
|
|
@@ -8790,11 +9396,11 @@ async function waitForWorkersReady(timeoutMs = 6e4) {
|
|
|
8790
9396
|
return false;
|
|
8791
9397
|
}
|
|
8792
9398
|
function dockerRemove() {
|
|
8793
|
-
|
|
9399
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
8794
9400
|
}
|
|
8795
9401
|
function dockerStop() {
|
|
8796
|
-
|
|
8797
|
-
|
|
9402
|
+
spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
|
|
9403
|
+
spawnSync3("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
8798
9404
|
}
|
|
8799
9405
|
async function dockerUpdate(opts = {}) {
|
|
8800
9406
|
if (dockerStatus().running) {
|
|
@@ -8804,7 +9410,7 @@ async function dockerUpdate(opts = {}) {
|
|
|
8804
9410
|
await dockerInstall(opts);
|
|
8805
9411
|
}
|
|
8806
9412
|
function dockerStatus() {
|
|
8807
|
-
const r =
|
|
9413
|
+
const r = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
8808
9414
|
encoding: "utf-8",
|
|
8809
9415
|
timeout: 5e3
|
|
8810
9416
|
});
|
|
@@ -8817,7 +9423,7 @@ function dockerStatus() {
|
|
|
8817
9423
|
};
|
|
8818
9424
|
}
|
|
8819
9425
|
function readContainerConfig() {
|
|
8820
|
-
const r =
|
|
9426
|
+
const r = spawnSync3("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
|
|
8821
9427
|
encoding: "utf-8",
|
|
8822
9428
|
timeout: 5e3
|
|
8823
9429
|
});
|
|
@@ -8871,11 +9477,11 @@ async function dockerSafeStop() {
|
|
|
8871
9477
|
console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
|
|
8872
9478
|
}
|
|
8873
9479
|
console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
|
|
8874
|
-
const stop =
|
|
9480
|
+
const stop = spawnSync3("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
|
|
8875
9481
|
encoding: "utf-8",
|
|
8876
9482
|
timeout: 45e3
|
|
8877
9483
|
});
|
|
8878
|
-
const inspect =
|
|
9484
|
+
const inspect = spawnSync3("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
|
|
8879
9485
|
encoding: "utf-8",
|
|
8880
9486
|
timeout: 5e3
|
|
8881
9487
|
});
|
|
@@ -8899,7 +9505,7 @@ async function dockerSafeStart() {
|
|
|
8899
9505
|
console.log(" Container is already running.");
|
|
8900
9506
|
return { ok: true, pgdataState: "running" };
|
|
8901
9507
|
}
|
|
8902
|
-
const exists =
|
|
9508
|
+
const exists = spawnSync3("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
8903
9509
|
encoding: "utf-8",
|
|
8904
9510
|
timeout: 5e3
|
|
8905
9511
|
});
|
|
@@ -8919,7 +9525,7 @@ async function dockerSafeStart() {
|
|
|
8919
9525
|
mkdirSync8(PGDATA_PATH, { recursive: true });
|
|
8920
9526
|
}
|
|
8921
9527
|
console.log(" Starting container...");
|
|
8922
|
-
const start =
|
|
9528
|
+
const start = spawnSync3("docker", ["start", CONTAINER_NAME], {
|
|
8923
9529
|
encoding: "utf-8",
|
|
8924
9530
|
timeout: 3e4
|
|
8925
9531
|
});
|
|
@@ -8987,6 +9593,56 @@ var init_dockerInstall = __esm({
|
|
|
8987
9593
|
}
|
|
8988
9594
|
});
|
|
8989
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
|
+
|
|
8990
9646
|
// cli/commands/setupGithub.ts
|
|
8991
9647
|
var setupGithub_exports = {};
|
|
8992
9648
|
__export(setupGithub_exports, {
|
|
@@ -8995,15 +9651,15 @@ __export(setupGithub_exports, {
|
|
|
8995
9651
|
});
|
|
8996
9652
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8997
9653
|
import { stdin as input, stdout as output } from "process";
|
|
8998
|
-
import { execSync as execSync5
|
|
8999
|
-
import { existsSync as existsSync10, readFileSync as
|
|
9000
|
-
import { homedir as
|
|
9001
|
-
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";
|
|
9002
9658
|
import { execFile as execFile2 } from "child_process";
|
|
9003
9659
|
function readConfig() {
|
|
9004
9660
|
if (!existsSync10(CONFIG_PATH)) return {};
|
|
9005
9661
|
const out = {};
|
|
9006
|
-
for (const line of
|
|
9662
|
+
for (const line of readFileSync9(CONFIG_PATH, "utf-8").split("\n")) {
|
|
9007
9663
|
const t = line.trim();
|
|
9008
9664
|
if (!t || t.startsWith("#")) continue;
|
|
9009
9665
|
const eq = t.indexOf("=");
|
|
@@ -9040,7 +9696,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
9040
9696
|
return await rl.question(q);
|
|
9041
9697
|
}
|
|
9042
9698
|
function openBrowser3(url) {
|
|
9043
|
-
const os =
|
|
9699
|
+
const os = platform4();
|
|
9044
9700
|
let bin;
|
|
9045
9701
|
let args2;
|
|
9046
9702
|
switch (os) {
|
|
@@ -9063,42 +9719,6 @@ function openBrowser3(url) {
|
|
|
9063
9719
|
function sleep(ms) {
|
|
9064
9720
|
return new Promise((r) => setTimeout(r, ms));
|
|
9065
9721
|
}
|
|
9066
|
-
function captureClaudeSetupToken() {
|
|
9067
|
-
const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
9068
|
-
return new Promise((resolve4, reject) => {
|
|
9069
|
-
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
9070
|
-
stdio: "inherit"
|
|
9071
|
-
});
|
|
9072
|
-
proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
|
|
9073
|
-
proc.on("close", (code) => {
|
|
9074
|
-
let raw = "";
|
|
9075
|
-
try {
|
|
9076
|
-
raw = readFileSync8(tmpFile, "utf-8");
|
|
9077
|
-
} catch (e) {
|
|
9078
|
-
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
9079
|
-
return;
|
|
9080
|
-
}
|
|
9081
|
-
try {
|
|
9082
|
-
unlinkSync3(tmpFile);
|
|
9083
|
-
} catch {
|
|
9084
|
-
}
|
|
9085
|
-
if (code !== 0) {
|
|
9086
|
-
reject(new Error(`claude setup-token exited with code ${code}`));
|
|
9087
|
-
return;
|
|
9088
|
-
}
|
|
9089
|
-
const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
|
|
9090
|
-
let yellow = "";
|
|
9091
|
-
let m;
|
|
9092
|
-
while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
|
|
9093
|
-
const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
|
|
9094
|
-
if (!token) {
|
|
9095
|
-
reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
|
|
9096
|
-
return;
|
|
9097
|
-
}
|
|
9098
|
-
resolve4(token[0]);
|
|
9099
|
-
});
|
|
9100
|
-
});
|
|
9101
|
-
}
|
|
9102
9722
|
async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
|
|
9103
9723
|
const resp = await fetch(`${gatewayUrl}${path}`, {
|
|
9104
9724
|
...opts,
|
|
@@ -9339,14 +9959,15 @@ Will push secrets to ${selected.length} repo(s):`);
|
|
|
9339
9959
|
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
9340
9960
|
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
9341
9961
|
}
|
|
9342
|
-
var
|
|
9962
|
+
var SYNKRO_DIR4, CONFIG_PATH;
|
|
9343
9963
|
var init_setupGithub = __esm({
|
|
9344
9964
|
"cli/commands/setupGithub.ts"() {
|
|
9345
9965
|
"use strict";
|
|
9346
9966
|
init_githubSetup();
|
|
9347
9967
|
init_stub();
|
|
9348
|
-
|
|
9349
|
-
|
|
9968
|
+
init_setupToken();
|
|
9969
|
+
SYNKRO_DIR4 = join9(homedir9(), ".synkro");
|
|
9970
|
+
CONFIG_PATH = join9(SYNKRO_DIR4, "config.env");
|
|
9350
9971
|
}
|
|
9351
9972
|
});
|
|
9352
9973
|
|
|
@@ -9360,16 +9981,16 @@ __export(install_exports, {
|
|
|
9360
9981
|
syncSkillFiles: () => syncSkillFiles,
|
|
9361
9982
|
writeHookScripts: () => writeHookScripts
|
|
9362
9983
|
});
|
|
9363
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as
|
|
9364
|
-
import { homedir as
|
|
9365
|
-
import { join as
|
|
9984
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync } from "fs";
|
|
9985
|
+
import { homedir as homedir10 } from "os";
|
|
9986
|
+
import { join as join10 } from "path";
|
|
9366
9987
|
import { execSync as execSync6 } from "child_process";
|
|
9367
9988
|
import { createInterface as createInterface3 } from "readline";
|
|
9368
9989
|
function resolvePersistedHookMode() {
|
|
9369
9990
|
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
9370
9991
|
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
9371
9992
|
try {
|
|
9372
|
-
const env =
|
|
9993
|
+
const env = readFileSync10(CONFIG_PATH2, "utf-8");
|
|
9373
9994
|
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
9374
9995
|
} catch {
|
|
9375
9996
|
}
|
|
@@ -9389,6 +10010,7 @@ function parseArgs(argv) {
|
|
|
9389
10010
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
9390
10011
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
9391
10012
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
10013
|
+
else if (a === "--byok-gateway") opts.byokGateway = true;
|
|
9392
10014
|
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
9393
10015
|
else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
|
|
9394
10016
|
}
|
|
@@ -9461,25 +10083,12 @@ async function promptCursorApiKey(opts) {
|
|
|
9461
10083
|
console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
|
|
9462
10084
|
}
|
|
9463
10085
|
}
|
|
9464
|
-
async function
|
|
9465
|
-
if (!process.stdin.isTTY) return "local";
|
|
9466
|
-
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9467
|
-
return new Promise((resolve4) => {
|
|
9468
|
-
rl.question(
|
|
9469
|
-
"Where should grading run?\n local \u2014 on this machine, via the Synkro container (default)\n byok \u2014 via an LLM API using your own provider key\nChoose [local] / byok: ",
|
|
9470
|
-
(answer) => {
|
|
9471
|
-
rl.close();
|
|
9472
|
-
resolve4(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
|
|
9473
|
-
}
|
|
9474
|
-
);
|
|
9475
|
-
});
|
|
9476
|
-
}
|
|
9477
|
-
async function promptStorageMode() {
|
|
10086
|
+
async function promptDeployLocation() {
|
|
9478
10087
|
if (!process.stdin.isTTY) return "local";
|
|
9479
10088
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9480
10089
|
return new Promise((resolve4) => {
|
|
9481
10090
|
rl.question(
|
|
9482
|
-
"Where should
|
|
10091
|
+
"Where should Synkro run?\n local \u2014 a grading container on this machine (Docker) (default)\n cloud \u2014 a private container hosted by Synkro (Cloudflare)\nBoth use your own Claude key. Choose [local] / cloud: ",
|
|
9483
10092
|
(answer) => {
|
|
9484
10093
|
rl.close();
|
|
9485
10094
|
resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
|
|
@@ -9518,36 +10127,36 @@ coding patterns and the dashboard shows full history. (Y/n) `
|
|
|
9518
10127
|
}
|
|
9519
10128
|
}
|
|
9520
10129
|
function ensureSynkroDir() {
|
|
9521
|
-
mkdirSync9(
|
|
10130
|
+
mkdirSync9(SYNKRO_DIR5, { recursive: true });
|
|
9522
10131
|
mkdirSync9(HOOKS_DIR, { recursive: true });
|
|
9523
10132
|
mkdirSync9(BIN_DIR, { recursive: true });
|
|
9524
10133
|
mkdirSync9(OFFSETS_DIR, { recursive: true });
|
|
9525
|
-
mkdirSync9(
|
|
10134
|
+
mkdirSync9(join10(SYNKRO_DIR5, "sessions"), { recursive: true });
|
|
9526
10135
|
}
|
|
9527
10136
|
function writeHookScripts(mode = "stub") {
|
|
9528
|
-
const installExtractCorePath =
|
|
9529
|
-
const bashScriptPath =
|
|
9530
|
-
const bashFollowupScriptPath =
|
|
9531
|
-
const editPrecheckScriptPath =
|
|
9532
|
-
const cwePrecheckScriptPath =
|
|
9533
|
-
const cvePrecheckScriptPath =
|
|
9534
|
-
const planJudgeScriptPath =
|
|
9535
|
-
const agentJudgeScriptPath =
|
|
9536
|
-
const stopSummaryScriptPath =
|
|
9537
|
-
const sessionStartScriptPath =
|
|
9538
|
-
const transcriptSyncScriptPath =
|
|
9539
|
-
const userPromptSubmitScriptPath =
|
|
9540
|
-
const commonScriptPath =
|
|
9541
|
-
const commonBashScriptPath =
|
|
9542
|
-
const installScanScriptPath =
|
|
9543
|
-
const cursorBashJudgePath =
|
|
9544
|
-
const cursorEditCapturePath =
|
|
9545
|
-
const cursorAgentCapturePath =
|
|
9546
|
-
const mcpStdioProxyPath =
|
|
9547
|
-
const taskActivateIntentScriptPath =
|
|
9548
|
-
const mcpGateScriptPath =
|
|
10137
|
+
const installExtractCorePath = join10(HOOKS_DIR, "installExtractCore.ts");
|
|
10138
|
+
const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.ts");
|
|
10139
|
+
const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.ts");
|
|
10140
|
+
const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
10141
|
+
const cwePrecheckScriptPath = join10(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
10142
|
+
const cvePrecheckScriptPath = join10(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
10143
|
+
const planJudgeScriptPath = join10(HOOKS_DIR, "cc-plan-judge.ts");
|
|
10144
|
+
const agentJudgeScriptPath = join10(HOOKS_DIR, "cc-agent-judge.ts");
|
|
10145
|
+
const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.ts");
|
|
10146
|
+
const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.ts");
|
|
10147
|
+
const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
10148
|
+
const userPromptSubmitScriptPath = join10(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
10149
|
+
const commonScriptPath = join10(HOOKS_DIR, "_synkro-common.ts");
|
|
10150
|
+
const commonBashScriptPath = join10(HOOKS_DIR, "_synkro-common.sh");
|
|
10151
|
+
const installScanScriptPath = join10(HOOKS_DIR, "cc-install-scan.ts");
|
|
10152
|
+
const cursorBashJudgePath = join10(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
10153
|
+
const cursorEditCapturePath = join10(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
10154
|
+
const cursorAgentCapturePath = join10(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
10155
|
+
const mcpStdioProxyPath = join10(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
10156
|
+
const taskActivateIntentScriptPath = join10(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
10157
|
+
const mcpGateScriptPath = join10(HOOKS_DIR, "cc-mcp-gate.ts");
|
|
9549
10158
|
if (mode === "stub") {
|
|
9550
|
-
const stubCommonPath =
|
|
10159
|
+
const stubCommonPath = join10(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
9551
10160
|
const stubFiles = [
|
|
9552
10161
|
[stubCommonPath, STUB_COMMON_TS],
|
|
9553
10162
|
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
@@ -9576,7 +10185,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9576
10185
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
9577
10186
|
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
9578
10187
|
try {
|
|
9579
|
-
unlinkSync4(
|
|
10188
|
+
unlinkSync4(join10(HOOKS_DIR, stale));
|
|
9580
10189
|
} catch {
|
|
9581
10190
|
}
|
|
9582
10191
|
}
|
|
@@ -9643,7 +10252,7 @@ function writeHookScripts(mode = "stub") {
|
|
|
9643
10252
|
chmodSync2(taskActivateIntentScriptPath, 493);
|
|
9644
10253
|
chmodSync2(mcpGateScriptPath, 493);
|
|
9645
10254
|
try {
|
|
9646
|
-
unlinkSync4(
|
|
10255
|
+
unlinkSync4(join10(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
9647
10256
|
} catch {
|
|
9648
10257
|
}
|
|
9649
10258
|
return {
|
|
@@ -9679,7 +10288,7 @@ function resolveSynkroBundle() {
|
|
|
9679
10288
|
return null;
|
|
9680
10289
|
}
|
|
9681
10290
|
function writeConfigEnv(opts) {
|
|
9682
|
-
const credsPath =
|
|
10291
|
+
const credsPath = join10(SYNKRO_DIR5, "credentials.json");
|
|
9683
10292
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
9684
10293
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
9685
10294
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -9695,7 +10304,7 @@ function writeConfigEnv(opts) {
|
|
|
9695
10304
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
9696
10305
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
9697
10306
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
9698
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10307
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.59")}`
|
|
9699
10308
|
];
|
|
9700
10309
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
9701
10310
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -9709,17 +10318,66 @@ function writeConfigEnv(opts) {
|
|
|
9709
10318
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
9710
10319
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
9711
10320
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
10321
|
+
lines.push(`SYNKRO_DEPLOY_LOCATION=${shellQuoteSingle(sanitizeConfigValue(opts.deployLocation ?? "local", 16))}`);
|
|
9712
10322
|
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
9713
10323
|
lines.push("");
|
|
9714
10324
|
writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
9715
10325
|
chmodSync2(CONFIG_PATH2, 384);
|
|
9716
10326
|
}
|
|
10327
|
+
async function provisionCloudContainer(opts) {
|
|
10328
|
+
let setupToken;
|
|
10329
|
+
try {
|
|
10330
|
+
console.log(" Authorize your Claude key for the hosted Synkro worker \u2014");
|
|
10331
|
+
console.log(" a browser window will open for approval...\n");
|
|
10332
|
+
setupToken = await captureClaudeSetupToken();
|
|
10333
|
+
} catch (err) {
|
|
10334
|
+
console.error(` \u2717 Could not capture Claude setup-token: ${err.message}`);
|
|
10335
|
+
console.error(" Cloud needs a Claude setup-token. Run `claude setup-token` manually, then re-run install.");
|
|
10336
|
+
process.exit(1);
|
|
10337
|
+
}
|
|
10338
|
+
const repo = detectGitRepo2() || void 0;
|
|
10339
|
+
const harness = [];
|
|
10340
|
+
if (opts.hasClaudeCode) harness.push("claude-code");
|
|
10341
|
+
if (opts.hasCursor) harness.push("cursor");
|
|
10342
|
+
const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
10343
|
+
try {
|
|
10344
|
+
const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
|
|
10345
|
+
method: "POST",
|
|
10346
|
+
headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
|
|
10347
|
+
body: JSON.stringify({
|
|
10348
|
+
org_id: opts.orgId,
|
|
10349
|
+
user_id: opts.userId,
|
|
10350
|
+
email: opts.email,
|
|
10351
|
+
harness,
|
|
10352
|
+
claude_workers: total,
|
|
10353
|
+
cursor_workers: 0,
|
|
10354
|
+
connected_repo: repo,
|
|
10355
|
+
ruleset: readFullSynkroFile()?.ruleset || "default",
|
|
10356
|
+
setup_token: setupToken
|
|
10357
|
+
// never logged; gateway stores it as the org secret
|
|
10358
|
+
})
|
|
10359
|
+
});
|
|
10360
|
+
if (!resp.ok) {
|
|
10361
|
+
const t = await resp.text().catch(() => "");
|
|
10362
|
+
throw new Error(`cloud-provision failed (${resp.status}): ${t.slice(0, 200)}`);
|
|
10363
|
+
}
|
|
10364
|
+
const out = await resp.json().catch(() => ({}));
|
|
10365
|
+
console.log(` \u2713 cloud container provisioned${out.container_id ? ` (${out.container_id})` : ""}`);
|
|
10366
|
+
console.log();
|
|
10367
|
+
} catch (err) {
|
|
10368
|
+
console.error(` \u2717 Cloud provisioning failed: ${err.message}`);
|
|
10369
|
+
console.error(" Hooks + MCP are installed; re-run `synkro install` \u2192 cloud to retry.");
|
|
10370
|
+
console.log();
|
|
10371
|
+
} finally {
|
|
10372
|
+
setupToken = "";
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
9717
10375
|
function resolveDeploymentMode() {
|
|
9718
10376
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
9719
10377
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
9720
10378
|
try {
|
|
9721
10379
|
if (existsSync11(CONFIG_PATH2)) {
|
|
9722
|
-
const m =
|
|
10380
|
+
const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
9723
10381
|
const val = m?.[1]?.toLowerCase();
|
|
9724
10382
|
if (val === "bare-host" || val === "docker") return val;
|
|
9725
10383
|
}
|
|
@@ -9746,16 +10404,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
9746
10404
|
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
9747
10405
|
} catch {
|
|
9748
10406
|
}
|
|
9749
|
-
const claudeDir =
|
|
10407
|
+
const claudeDir = join10(homedir10(), ".claude");
|
|
9750
10408
|
try {
|
|
9751
|
-
const settings = JSON.parse(
|
|
10409
|
+
const settings = JSON.parse(readFileSync10(join10(claudeDir, "settings.json"), "utf-8"));
|
|
9752
10410
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
9753
10411
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
9754
10412
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
9755
10413
|
} catch {
|
|
9756
10414
|
}
|
|
9757
10415
|
try {
|
|
9758
|
-
const mcpCache = JSON.parse(
|
|
10416
|
+
const mcpCache = JSON.parse(readFileSync10(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
9759
10417
|
const mcpNames = Object.keys(mcpCache);
|
|
9760
10418
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
9761
10419
|
} catch {
|
|
@@ -9767,10 +10425,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
9767
10425
|
} catch {
|
|
9768
10426
|
}
|
|
9769
10427
|
try {
|
|
9770
|
-
const sessionsDir =
|
|
10428
|
+
const sessionsDir = join10(claudeDir, "sessions");
|
|
9771
10429
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
9772
10430
|
for (const f of files) {
|
|
9773
|
-
const s = JSON.parse(
|
|
10431
|
+
const s = JSON.parse(readFileSync10(join10(sessionsDir, f), "utf-8"));
|
|
9774
10432
|
if (s.version) {
|
|
9775
10433
|
meta.cc_version = meta.cc_version || s.version;
|
|
9776
10434
|
break;
|
|
@@ -9878,6 +10536,7 @@ async function installCommand(opts = {}) {
|
|
|
9878
10536
|
let agents;
|
|
9879
10537
|
let gradingMode;
|
|
9880
10538
|
let storageMode;
|
|
10539
|
+
let deployLocation = "local";
|
|
9881
10540
|
let transcriptCC = false;
|
|
9882
10541
|
let transcriptCursor = false;
|
|
9883
10542
|
if (existingSynkro) {
|
|
@@ -9887,8 +10546,9 @@ async function installCommand(opts = {}) {
|
|
|
9887
10546
|
(a) => a.kind === "claude_code" && wantCC || a.kind === "cursor" && wantCursor
|
|
9888
10547
|
);
|
|
9889
10548
|
if (agents.length === 0 && detected.length > 0) agents = detected;
|
|
10549
|
+
deployLocation = existingSynkro.grader.location === "cloud" ? "cloud" : "local";
|
|
9890
10550
|
gradingMode = existingSynkro.grader.mode === "byok" ? "byok" : "local";
|
|
9891
|
-
storageMode = "local";
|
|
10551
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
9892
10552
|
console.log(`Using .synkro config:`);
|
|
9893
10553
|
for (const w of existingSynkro.warnings) console.warn(` \u26A0 ${w}`);
|
|
9894
10554
|
console.log(` harness: ${existingSynkro.harness.join(", ")}`);
|
|
@@ -9912,15 +10572,20 @@ async function installCommand(opts = {}) {
|
|
|
9912
10572
|
console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
|
|
9913
10573
|
`);
|
|
9914
10574
|
}
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
10575
|
+
if (opts.byokGateway) {
|
|
10576
|
+
gradingMode = "byok";
|
|
10577
|
+
storageMode = "cloud";
|
|
10578
|
+
deployLocation = "local";
|
|
10579
|
+
console.log(" deploy: legacy BYOK-gateway grading (no container)\n");
|
|
10580
|
+
} else {
|
|
10581
|
+
deployLocation = await promptDeployLocation();
|
|
10582
|
+
gradingMode = "local";
|
|
10583
|
+
storageMode = deployLocation === "cloud" ? "cloud" : "local";
|
|
10584
|
+
console.log(` deploy: ${deployLocation}
|
|
9918
10585
|
`);
|
|
9919
|
-
if (gradingMode === "byok") {
|
|
9920
|
-
console.log(" BYOK grading uses your own provider key \u2014 register one in the");
|
|
9921
|
-
console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
|
|
9922
10586
|
}
|
|
9923
10587
|
}
|
|
10588
|
+
const target = deployLocation === "cloud" ? "cloud-container" : opts.byokGateway || gradingMode === "byok" ? "gateway-byok" : "local-docker";
|
|
9924
10589
|
{
|
|
9925
10590
|
const wantCC = agents.some((a) => a.kind === "claude_code");
|
|
9926
10591
|
const wantCursor = agents.some((a) => a.kind === "cursor");
|
|
@@ -9945,9 +10610,9 @@ async function installCommand(opts = {}) {
|
|
|
9945
10610
|
const scripts = writeHookScripts(hookMode);
|
|
9946
10611
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
9947
10612
|
for (const mode of ["edit", "bash"]) {
|
|
9948
|
-
const pidFile =
|
|
10613
|
+
const pidFile = join10(SYNKRO_DIR5, "daemon", mode, "daemon.pid");
|
|
9949
10614
|
try {
|
|
9950
|
-
const pid = parseInt(
|
|
10615
|
+
const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
|
|
9951
10616
|
if (pid > 0) {
|
|
9952
10617
|
process.kill(pid, "SIGTERM");
|
|
9953
10618
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -10012,10 +10677,11 @@ async function installCommand(opts = {}) {
|
|
|
10012
10677
|
} catch {
|
|
10013
10678
|
}
|
|
10014
10679
|
const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
|
|
10015
|
-
const
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10680
|
+
const useLocalMcp = target === "local-docker";
|
|
10681
|
+
if (target === "cloud-container") {
|
|
10682
|
+
console.log("Cloud setup \u2014 Synkro will provision a private hosted container for your org.\n");
|
|
10683
|
+
} else if (target === "gateway-byok") {
|
|
10684
|
+
console.log("Cloud-only setup (BYOK grading via gateway) \u2014 skipping the local container.\n");
|
|
10019
10685
|
}
|
|
10020
10686
|
if (hasClaudeCode && !opts.noMcp) {
|
|
10021
10687
|
if (useLocalMcp) {
|
|
@@ -10029,7 +10695,7 @@ async function installCommand(opts = {}) {
|
|
|
10029
10695
|
if (mintResp.ok) {
|
|
10030
10696
|
const minted = await mintResp.json();
|
|
10031
10697
|
mcpJwt = minted.token;
|
|
10032
|
-
writeFileSync8(
|
|
10698
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
10033
10699
|
} else {
|
|
10034
10700
|
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
10035
10701
|
}
|
|
@@ -10057,7 +10723,7 @@ async function installCommand(opts = {}) {
|
|
|
10057
10723
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10058
10724
|
}
|
|
10059
10725
|
const minted = await mintResp.json();
|
|
10060
|
-
writeFileSync8(
|
|
10726
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10061
10727
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10062
10728
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10063
10729
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10074,7 +10740,7 @@ async function installCommand(opts = {}) {
|
|
|
10074
10740
|
if (hasCursor && !opts.noMcp) {
|
|
10075
10741
|
try {
|
|
10076
10742
|
if (useLocalMcp) {
|
|
10077
|
-
const jwtPath =
|
|
10743
|
+
const jwtPath = join10(SYNKRO_DIR5, ".mcp-jwt");
|
|
10078
10744
|
if (!existsSync11(jwtPath)) {
|
|
10079
10745
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
10080
10746
|
method: "POST",
|
|
@@ -10103,7 +10769,7 @@ async function installCommand(opts = {}) {
|
|
|
10103
10769
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
10104
10770
|
}
|
|
10105
10771
|
const minted = await mintResp.json();
|
|
10106
|
-
writeFileSync8(
|
|
10772
|
+
writeFileSync8(join10(SYNKRO_DIR5, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
10107
10773
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
10108
10774
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
10109
10775
|
console.log(` url: ${mcp.url}`);
|
|
@@ -10116,7 +10782,7 @@ async function installCommand(opts = {}) {
|
|
|
10116
10782
|
}
|
|
10117
10783
|
const synkroBundle = resolveSynkroBundle();
|
|
10118
10784
|
const persistedMode = resolveDeploymentMode();
|
|
10119
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
|
|
10785
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, deployLocation, hookMode });
|
|
10120
10786
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
10121
10787
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
10122
10788
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -10128,7 +10794,7 @@ async function installCommand(opts = {}) {
|
|
|
10128
10794
|
} catch (err) {
|
|
10129
10795
|
console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
|
|
10130
10796
|
}
|
|
10131
|
-
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
|
|
10797
|
+
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode, deployLocation });
|
|
10132
10798
|
console.log();
|
|
10133
10799
|
if (useLocalMcp) {
|
|
10134
10800
|
const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
|
|
@@ -10179,7 +10845,7 @@ async function installCommand(opts = {}) {
|
|
|
10179
10845
|
const ready = await waitForContainerReady(6e4);
|
|
10180
10846
|
if (ready) {
|
|
10181
10847
|
console.log(" \u2713 container ready");
|
|
10182
|
-
const mcpJwt =
|
|
10848
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10183
10849
|
try {
|
|
10184
10850
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
10185
10851
|
method: "POST",
|
|
@@ -10209,6 +10875,8 @@ async function installCommand(opts = {}) {
|
|
|
10209
10875
|
process.exit(1);
|
|
10210
10876
|
}
|
|
10211
10877
|
console.log();
|
|
10878
|
+
} else if (target === "cloud-container") {
|
|
10879
|
+
await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
|
|
10212
10880
|
}
|
|
10213
10881
|
if (transcriptConsent) {
|
|
10214
10882
|
const repo = detectGitRepo2();
|
|
@@ -10217,7 +10885,7 @@ async function installCommand(opts = {}) {
|
|
|
10217
10885
|
try {
|
|
10218
10886
|
let mcpToken = "";
|
|
10219
10887
|
try {
|
|
10220
|
-
mcpToken =
|
|
10888
|
+
mcpToken = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10221
10889
|
} catch {
|
|
10222
10890
|
}
|
|
10223
10891
|
if (mcpToken) {
|
|
@@ -10345,11 +11013,11 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10345
11013
|
try {
|
|
10346
11014
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10347
11015
|
if (!root) return;
|
|
10348
|
-
if (root ===
|
|
10349
|
-
const fp =
|
|
11016
|
+
if (root === homedir10()) return;
|
|
11017
|
+
const fp = join10(root, "synkro.toml");
|
|
10350
11018
|
let hasFile = false;
|
|
10351
11019
|
try {
|
|
10352
|
-
hasFile =
|
|
11020
|
+
hasFile = statSync(fp).isFile();
|
|
10353
11021
|
} catch {
|
|
10354
11022
|
}
|
|
10355
11023
|
if (hasFile) {
|
|
@@ -10377,6 +11045,7 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
10377
11045
|
"skills = []",
|
|
10378
11046
|
"",
|
|
10379
11047
|
"[grader]",
|
|
11048
|
+
`location = "${opts.deployLocation || "local"}"`,
|
|
10380
11049
|
`pool = "${pool}"`,
|
|
10381
11050
|
`mode = "${mode}"`,
|
|
10382
11051
|
"",
|
|
@@ -10394,9 +11063,9 @@ function readFullSynkroFile() {
|
|
|
10394
11063
|
try {
|
|
10395
11064
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10396
11065
|
if (!root) return null;
|
|
10397
|
-
const fp =
|
|
11066
|
+
const fp = join10(root, "synkro.toml");
|
|
10398
11067
|
if (!existsSync11(fp)) return null;
|
|
10399
|
-
const parsed = parseSynkroToml2(
|
|
11068
|
+
const parsed = parseSynkroToml2(readFileSync10(fp, "utf-8"));
|
|
10400
11069
|
const valid = ["claude-code", "cursor"];
|
|
10401
11070
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
10402
11071
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -10404,7 +11073,8 @@ function readFullSynkroFile() {
|
|
|
10404
11073
|
harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
|
|
10405
11074
|
grader: {
|
|
10406
11075
|
pool: resolved.pool,
|
|
10407
|
-
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
|
|
11076
|
+
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local",
|
|
11077
|
+
location: parsed.grader?.location === "cloud" ? "cloud" : "local"
|
|
10408
11078
|
},
|
|
10409
11079
|
workers: {
|
|
10410
11080
|
...resolved.claudeWorkers != null ? { claude: resolved.claudeWorkers } : {},
|
|
@@ -10432,7 +11102,7 @@ function reconcileHarness() {
|
|
|
10432
11102
|
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
10433
11103
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
10434
11104
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
10435
|
-
const ccSettings =
|
|
11105
|
+
const ccSettings = join10(homedir10(), ".claude", "settings.json");
|
|
10436
11106
|
if (wantCC) {
|
|
10437
11107
|
installCCHooks(ccSettings, {
|
|
10438
11108
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -10452,7 +11122,7 @@ function reconcileHarness() {
|
|
|
10452
11122
|
});
|
|
10453
11123
|
console.log(" \u2713 Claude Code hooks registered");
|
|
10454
11124
|
try {
|
|
10455
|
-
const mcpJwt =
|
|
11125
|
+
const mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
|
|
10456
11126
|
if (mcpJwt) {
|
|
10457
11127
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
10458
11128
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -10462,8 +11132,9 @@ function reconcileHarness() {
|
|
|
10462
11132
|
} else {
|
|
10463
11133
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
10464
11134
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
11135
|
+
if (uninstallClaudeDesktopMcpConfig()) console.log(" \u2717 Claude Desktop MCP removed");
|
|
10465
11136
|
}
|
|
10466
|
-
const cursorHooks =
|
|
11137
|
+
const cursorHooks = join10(homedir10(), ".cursor", "hooks.json");
|
|
10467
11138
|
if (wantCursor) {
|
|
10468
11139
|
installCursorHooks(cursorHooks, {
|
|
10469
11140
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -10517,7 +11188,7 @@ async function syncSkillFiles() {
|
|
|
10517
11188
|
if (resolved.length === 0) return;
|
|
10518
11189
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
10519
11190
|
const tasks = resolved.map((fp) => {
|
|
10520
|
-
const content =
|
|
11191
|
+
const content = readFileSync10(fp, "utf-8");
|
|
10521
11192
|
const source = `skill:${fp.split("/").pop()}`;
|
|
10522
11193
|
if (!content.trim()) {
|
|
10523
11194
|
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
@@ -10563,7 +11234,7 @@ function detectGitRepo2() {
|
|
|
10563
11234
|
function getClaudeProjectsFolder() {
|
|
10564
11235
|
const cwd = process.cwd();
|
|
10565
11236
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
10566
|
-
const projectsDir =
|
|
11237
|
+
const projectsDir = join10(homedir10(), ".claude", "projects", sanitized);
|
|
10567
11238
|
return existsSync11(projectsDir) ? projectsDir : null;
|
|
10568
11239
|
}
|
|
10569
11240
|
function extractSessionInsights(projectsDir) {
|
|
@@ -10571,9 +11242,9 @@ function extractSessionInsights(projectsDir) {
|
|
|
10571
11242
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
10572
11243
|
for (const file of files) {
|
|
10573
11244
|
const sessionId = file.replace(".jsonl", "");
|
|
10574
|
-
const filePath =
|
|
11245
|
+
const filePath = join10(projectsDir, file);
|
|
10575
11246
|
try {
|
|
10576
|
-
const content =
|
|
11247
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10577
11248
|
const lines = content.split("\n").filter(Boolean);
|
|
10578
11249
|
for (let i = 0; i < lines.length; i++) {
|
|
10579
11250
|
try {
|
|
@@ -10652,14 +11323,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
10652
11323
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10653
11324
|
}
|
|
10654
11325
|
function getCursorTranscriptsDir() {
|
|
10655
|
-
const dir =
|
|
11326
|
+
const dir = join10(homedir10(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10656
11327
|
return existsSync11(dir) ? dir : null;
|
|
10657
11328
|
}
|
|
10658
11329
|
function isSafeConvId(id) {
|
|
10659
11330
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
10660
11331
|
}
|
|
10661
11332
|
function parseCursorTranscriptFile(filePath) {
|
|
10662
|
-
const content =
|
|
11333
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10663
11334
|
const lines = content.split("\n").filter(Boolean);
|
|
10664
11335
|
const messages = [];
|
|
10665
11336
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -10691,7 +11362,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10691
11362
|
for (let i = 0; i < convDirs.length; i++) {
|
|
10692
11363
|
const convId = convDirs[i];
|
|
10693
11364
|
if (!isSafeConvId(convId)) continue;
|
|
10694
|
-
const filePath =
|
|
11365
|
+
const filePath = join10(dir, convId, `${convId}.jsonl`);
|
|
10695
11366
|
if (!existsSync11(filePath)) continue;
|
|
10696
11367
|
try {
|
|
10697
11368
|
const all = parseCursorTranscriptFile(filePath);
|
|
@@ -10714,8 +11385,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10714
11385
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
10715
11386
|
}
|
|
10716
11387
|
try {
|
|
10717
|
-
const lc =
|
|
10718
|
-
writeFileSync8(
|
|
11388
|
+
const lc = readFileSync10(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
11389
|
+
writeFileSync8(join10(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
10719
11390
|
} catch {
|
|
10720
11391
|
}
|
|
10721
11392
|
}
|
|
@@ -10723,7 +11394,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10723
11394
|
return { sessions: totalSessions, messages: totalMessages };
|
|
10724
11395
|
}
|
|
10725
11396
|
function parseTranscriptFile(filePath) {
|
|
10726
|
-
const content =
|
|
11397
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10727
11398
|
const lines = content.split("\n").filter(Boolean);
|
|
10728
11399
|
const messages = [];
|
|
10729
11400
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -10771,7 +11442,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10771
11442
|
for (let i = 0; i < files.length; i++) {
|
|
10772
11443
|
const file = files[i];
|
|
10773
11444
|
const sessionId = file.replace(".jsonl", "");
|
|
10774
|
-
const filePath =
|
|
11445
|
+
const filePath = join10(projectsDir, file);
|
|
10775
11446
|
try {
|
|
10776
11447
|
const allMessages = parseTranscriptFile(filePath);
|
|
10777
11448
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -10793,9 +11464,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
10793
11464
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
10794
11465
|
}
|
|
10795
11466
|
try {
|
|
10796
|
-
const content =
|
|
11467
|
+
const content = readFileSync10(join10(projectsDir, file), "utf-8");
|
|
10797
11468
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
10798
|
-
writeFileSync8(
|
|
11469
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
10799
11470
|
} catch {
|
|
10800
11471
|
}
|
|
10801
11472
|
}
|
|
@@ -10816,7 +11487,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
10816
11487
|
const sessions = [];
|
|
10817
11488
|
for (const file of batch) {
|
|
10818
11489
|
const sessionId = file.replace(".jsonl", "");
|
|
10819
|
-
const filePath =
|
|
11490
|
+
const filePath = join10(projectsDir, file);
|
|
10820
11491
|
try {
|
|
10821
11492
|
const allMessages = parseTranscriptFile(filePath);
|
|
10822
11493
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -10845,18 +11516,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
10845
11516
|
}
|
|
10846
11517
|
for (const file of batch) {
|
|
10847
11518
|
const sessionId = file.replace(".jsonl", "");
|
|
10848
|
-
const filePath =
|
|
11519
|
+
const filePath = join10(projectsDir, file);
|
|
10849
11520
|
try {
|
|
10850
|
-
const content =
|
|
11521
|
+
const content = readFileSync10(filePath, "utf-8");
|
|
10851
11522
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
10852
|
-
writeFileSync8(
|
|
11523
|
+
writeFileSync8(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
10853
11524
|
} catch {
|
|
10854
11525
|
}
|
|
10855
11526
|
}
|
|
10856
11527
|
}
|
|
10857
11528
|
return { sessions: totalSessions, messages: totalMessages };
|
|
10858
11529
|
}
|
|
10859
|
-
var
|
|
11530
|
+
var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
|
|
10860
11531
|
var init_install = __esm({
|
|
10861
11532
|
"cli/commands/install.ts"() {
|
|
10862
11533
|
"use strict";
|
|
@@ -10874,10 +11545,11 @@ var init_install = __esm({
|
|
|
10874
11545
|
init_promptFetcher();
|
|
10875
11546
|
init_claudeDesktopTap();
|
|
10876
11547
|
init_dockerInstall();
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
11548
|
+
init_setupToken();
|
|
11549
|
+
SYNKRO_DIR5 = join10(homedir10(), ".synkro");
|
|
11550
|
+
HOOKS_DIR = join10(SYNKRO_DIR5, "hooks");
|
|
11551
|
+
BIN_DIR = join10(SYNKRO_DIR5, "bin");
|
|
11552
|
+
CONFIG_PATH2 = join10(SYNKRO_DIR5, "config.env");
|
|
10881
11553
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
10882
11554
|
import { readFileSync } from 'node:fs';
|
|
10883
11555
|
import { homedir } from 'node:os';
|
|
@@ -10988,15 +11660,15 @@ rl.on('line', async (line) => {
|
|
|
10988
11660
|
}
|
|
10989
11661
|
});
|
|
10990
11662
|
`;
|
|
10991
|
-
OFFSETS_DIR =
|
|
11663
|
+
OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
|
|
10992
11664
|
}
|
|
10993
11665
|
});
|
|
10994
11666
|
|
|
10995
11667
|
// cli/local-cc/install.ts
|
|
10996
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as
|
|
10997
|
-
import { join as
|
|
10998
|
-
import { homedir as
|
|
10999
|
-
import { spawnSync as
|
|
11668
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync11, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
11669
|
+
import { join as join11 } from "path";
|
|
11670
|
+
import { homedir as homedir11 } from "os";
|
|
11671
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
11000
11672
|
function writePluginFiles() {
|
|
11001
11673
|
for (const c of CHANNELS) {
|
|
11002
11674
|
mkdirSync10(c.sessionDir, { recursive: true });
|
|
@@ -11016,7 +11688,7 @@ function writePluginFiles() {
|
|
|
11016
11688
|
}
|
|
11017
11689
|
function runBunInstall() {
|
|
11018
11690
|
for (const c of CHANNELS) {
|
|
11019
|
-
const r =
|
|
11691
|
+
const r = spawnSync5("bun", ["install", "--silent"], {
|
|
11020
11692
|
cwd: c.sessionDir,
|
|
11021
11693
|
encoding: "utf-8",
|
|
11022
11694
|
timeout: 12e4
|
|
@@ -11032,7 +11704,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
11032
11704
|
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
11033
11705
|
return;
|
|
11034
11706
|
}
|
|
11035
|
-
const originalText =
|
|
11707
|
+
const originalText = readFileSync11(CLAUDE_JSON_PATH, "utf-8");
|
|
11036
11708
|
let parsed;
|
|
11037
11709
|
try {
|
|
11038
11710
|
parsed = JSON.parse(originalText);
|
|
@@ -11132,15 +11804,15 @@ function patchClaudeJson() {
|
|
|
11132
11804
|
});
|
|
11133
11805
|
}
|
|
11134
11806
|
function installLocalCC() {
|
|
11135
|
-
let bunCheck =
|
|
11807
|
+
let bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11136
11808
|
if (bunCheck.status !== 0) {
|
|
11137
11809
|
if (process.platform === "darwin") {
|
|
11138
11810
|
console.log(" Installing bun via brew...");
|
|
11139
|
-
const brewR =
|
|
11811
|
+
const brewR = spawnSync5("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
11140
11812
|
if (brewR.status !== 0) {
|
|
11141
11813
|
throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
11142
11814
|
}
|
|
11143
|
-
bunCheck =
|
|
11815
|
+
bunCheck = spawnSync5("bun", ["--version"], { encoding: "utf-8" });
|
|
11144
11816
|
if (bunCheck.status !== 0) {
|
|
11145
11817
|
throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
|
|
11146
11818
|
}
|
|
@@ -11174,42 +11846,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
11174
11846
|
var init_install2 = __esm({
|
|
11175
11847
|
"cli/local-cc/install.ts"() {
|
|
11176
11848
|
"use strict";
|
|
11177
|
-
CLAUDE_JSON_BACKUP_PATH =
|
|
11178
|
-
SESSION_DIR =
|
|
11179
|
-
PLUGIN_PATH =
|
|
11180
|
-
PLUGIN_PKG_PATH =
|
|
11181
|
-
PLUGIN_SETTINGS_DIR =
|
|
11182
|
-
PLUGIN_SETTINGS_PATH =
|
|
11183
|
-
PROJECT_MCP_PATH =
|
|
11184
|
-
CLAUDE_JSON_PATH =
|
|
11185
|
-
RUN_SCRIPT_PATH =
|
|
11849
|
+
CLAUDE_JSON_BACKUP_PATH = join11(homedir11(), ".claude.json.synkro-bak");
|
|
11850
|
+
SESSION_DIR = join11(homedir11(), ".synkro", "cc_sessions");
|
|
11851
|
+
PLUGIN_PATH = join11(SESSION_DIR, "synkro-channel.ts");
|
|
11852
|
+
PLUGIN_PKG_PATH = join11(SESSION_DIR, "package.json");
|
|
11853
|
+
PLUGIN_SETTINGS_DIR = join11(SESSION_DIR, ".claude");
|
|
11854
|
+
PLUGIN_SETTINGS_PATH = join11(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
11855
|
+
PROJECT_MCP_PATH = join11(SESSION_DIR, ".mcp.json");
|
|
11856
|
+
CLAUDE_JSON_PATH = join11(homedir11(), ".claude.json");
|
|
11857
|
+
RUN_SCRIPT_PATH = join11(SESSION_DIR, "run-claude.sh");
|
|
11186
11858
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
11187
11859
|
CHANNEL_1_PORT = 8941;
|
|
11188
|
-
SESSION_DIR_2 =
|
|
11189
|
-
PLUGIN_PATH_2 =
|
|
11190
|
-
PLUGIN_PKG_PATH_2 =
|
|
11191
|
-
PLUGIN_SETTINGS_DIR_2 =
|
|
11192
|
-
PLUGIN_SETTINGS_PATH_2 =
|
|
11193
|
-
PROJECT_MCP_PATH_2 =
|
|
11194
|
-
RUN_SCRIPT_PATH_2 =
|
|
11860
|
+
SESSION_DIR_2 = join11(homedir11(), ".synkro", "cc_sessions_2");
|
|
11861
|
+
PLUGIN_PATH_2 = join11(SESSION_DIR_2, "synkro-channel.ts");
|
|
11862
|
+
PLUGIN_PKG_PATH_2 = join11(SESSION_DIR_2, "package.json");
|
|
11863
|
+
PLUGIN_SETTINGS_DIR_2 = join11(SESSION_DIR_2, ".claude");
|
|
11864
|
+
PLUGIN_SETTINGS_PATH_2 = join11(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
11865
|
+
PROJECT_MCP_PATH_2 = join11(SESSION_DIR_2, ".mcp.json");
|
|
11866
|
+
RUN_SCRIPT_PATH_2 = join11(SESSION_DIR_2, "run-claude.sh");
|
|
11195
11867
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
11196
11868
|
CHANNEL_2_PORT = 8951;
|
|
11197
|
-
SESSION_DIR_3 =
|
|
11198
|
-
PLUGIN_PATH_3 =
|
|
11199
|
-
PLUGIN_PKG_PATH_3 =
|
|
11200
|
-
PLUGIN_SETTINGS_DIR_3 =
|
|
11201
|
-
PLUGIN_SETTINGS_PATH_3 =
|
|
11202
|
-
PROJECT_MCP_PATH_3 =
|
|
11203
|
-
RUN_SCRIPT_PATH_3 =
|
|
11869
|
+
SESSION_DIR_3 = join11(homedir11(), ".synkro", "cc_sessions_3");
|
|
11870
|
+
PLUGIN_PATH_3 = join11(SESSION_DIR_3, "synkro-channel.ts");
|
|
11871
|
+
PLUGIN_PKG_PATH_3 = join11(SESSION_DIR_3, "package.json");
|
|
11872
|
+
PLUGIN_SETTINGS_DIR_3 = join11(SESSION_DIR_3, ".claude");
|
|
11873
|
+
PLUGIN_SETTINGS_PATH_3 = join11(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
11874
|
+
PROJECT_MCP_PATH_3 = join11(SESSION_DIR_3, ".mcp.json");
|
|
11875
|
+
RUN_SCRIPT_PATH_3 = join11(SESSION_DIR_3, "run-claude.sh");
|
|
11204
11876
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
11205
11877
|
CHANNEL_3_PORT = 8942;
|
|
11206
|
-
SESSION_DIR_4 =
|
|
11207
|
-
PLUGIN_PATH_4 =
|
|
11208
|
-
PLUGIN_PKG_PATH_4 =
|
|
11209
|
-
PLUGIN_SETTINGS_DIR_4 =
|
|
11210
|
-
PLUGIN_SETTINGS_PATH_4 =
|
|
11211
|
-
PROJECT_MCP_PATH_4 =
|
|
11212
|
-
RUN_SCRIPT_PATH_4 =
|
|
11878
|
+
SESSION_DIR_4 = join11(homedir11(), ".synkro", "cc_sessions_4");
|
|
11879
|
+
PLUGIN_PATH_4 = join11(SESSION_DIR_4, "synkro-channel.ts");
|
|
11880
|
+
PLUGIN_PKG_PATH_4 = join11(SESSION_DIR_4, "package.json");
|
|
11881
|
+
PLUGIN_SETTINGS_DIR_4 = join11(SESSION_DIR_4, ".claude");
|
|
11882
|
+
PLUGIN_SETTINGS_PATH_4 = join11(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
11883
|
+
PROJECT_MCP_PATH_4 = join11(SESSION_DIR_4, ".mcp.json");
|
|
11884
|
+
RUN_SCRIPT_PATH_4 = join11(SESSION_DIR_4, "run-claude.sh");
|
|
11213
11885
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
11214
11886
|
CHANNEL_4_PORT = 8952;
|
|
11215
11887
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -11484,9 +12156,9 @@ __export(disconnect_exports, {
|
|
|
11484
12156
|
disconnectCommand: () => disconnectCommand
|
|
11485
12157
|
});
|
|
11486
12158
|
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11487
|
-
import { homedir as
|
|
11488
|
-
import { join as
|
|
11489
|
-
import { spawnSync as
|
|
12159
|
+
import { homedir as homedir12 } from "os";
|
|
12160
|
+
import { join as join12 } from "path";
|
|
12161
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
11490
12162
|
import { createInterface as createInterface4 } from "readline";
|
|
11491
12163
|
async function tearDownLocalCC() {
|
|
11492
12164
|
const docker = dockerStatus();
|
|
@@ -11500,7 +12172,7 @@ async function tearDownLocalCC() {
|
|
|
11500
12172
|
console.log("\u2713 removed synkro-server container");
|
|
11501
12173
|
try {
|
|
11502
12174
|
const image = imageTag();
|
|
11503
|
-
const r =
|
|
12175
|
+
const r = spawnSync6("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
|
|
11504
12176
|
console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
|
|
11505
12177
|
} catch {
|
|
11506
12178
|
}
|
|
@@ -11559,15 +12231,19 @@ async function disconnectCommand(args2 = []) {
|
|
|
11559
12231
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
11560
12232
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
11561
12233
|
}
|
|
11562
|
-
|
|
12234
|
+
{
|
|
12235
|
+
const desktopMcpRemoved = uninstallClaudeDesktopMcpConfig();
|
|
12236
|
+
console.log(`${desktopMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Claude Desktop): ${desktopMcpRemoved ? "removed from claude_desktop_config.json" : "no entry found"}`);
|
|
12237
|
+
}
|
|
12238
|
+
if (existsSync13(SYNKRO_DIR6)) {
|
|
11563
12239
|
if (purge) {
|
|
11564
|
-
rmSync(
|
|
11565
|
-
console.log(`\u2713 wiped ${
|
|
12240
|
+
rmSync(SYNKRO_DIR6, { recursive: true, force: true });
|
|
12241
|
+
console.log(`\u2713 wiped ${SYNKRO_DIR6} entirely \u2014 including all scan data and backups`);
|
|
11566
12242
|
} else {
|
|
11567
|
-
const keep = /* @__PURE__ */ new Set([
|
|
12243
|
+
const keep = /* @__PURE__ */ new Set([join12(SYNKRO_DIR6, "pgdata"), join12(SYNKRO_DIR6, "pgdata-backups")]);
|
|
11568
12244
|
const preserved = [];
|
|
11569
|
-
for (const entry of readdirSync4(
|
|
11570
|
-
const full =
|
|
12245
|
+
for (const entry of readdirSync4(SYNKRO_DIR6)) {
|
|
12246
|
+
const full = join12(SYNKRO_DIR6, entry);
|
|
11571
12247
|
if (keep.has(full)) {
|
|
11572
12248
|
preserved.push(entry);
|
|
11573
12249
|
continue;
|
|
@@ -11575,18 +12251,18 @@ async function disconnectCommand(args2 = []) {
|
|
|
11575
12251
|
rmSync(full, { recursive: true, force: true });
|
|
11576
12252
|
}
|
|
11577
12253
|
if (preserved.length > 0) {
|
|
11578
|
-
console.log(`\u2713 removed Synkro config from ${
|
|
12254
|
+
console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR6} (kept your scan data: ${preserved.join(", ")})`);
|
|
11579
12255
|
console.log(" run `synkro uninstall --purge` to delete that too");
|
|
11580
12256
|
} else {
|
|
11581
|
-
console.log(`\u2713 removed ${
|
|
12257
|
+
console.log(`\u2713 removed ${SYNKRO_DIR6}`);
|
|
11582
12258
|
}
|
|
11583
12259
|
}
|
|
11584
12260
|
} else {
|
|
11585
|
-
console.log(`\xB7 ${
|
|
12261
|
+
console.log(`\xB7 ${SYNKRO_DIR6} already gone`);
|
|
11586
12262
|
}
|
|
11587
12263
|
console.log(purge ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
|
|
11588
12264
|
}
|
|
11589
|
-
var
|
|
12265
|
+
var SYNKRO_DIR6;
|
|
11590
12266
|
var init_disconnect = __esm({
|
|
11591
12267
|
"cli/commands/disconnect.ts"() {
|
|
11592
12268
|
"use strict";
|
|
@@ -11597,14 +12273,14 @@ var init_disconnect = __esm({
|
|
|
11597
12273
|
init_install2();
|
|
11598
12274
|
init_dockerInstall();
|
|
11599
12275
|
init_macKeychain();
|
|
11600
|
-
|
|
12276
|
+
SYNKRO_DIR6 = join12(homedir12(), ".synkro");
|
|
11601
12277
|
}
|
|
11602
12278
|
});
|
|
11603
12279
|
|
|
11604
12280
|
// cli/local-cc/turnLog.ts
|
|
11605
|
-
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as
|
|
11606
|
-
import { dirname as dirname6, join as
|
|
11607
|
-
import { homedir as
|
|
12281
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync11, openSync as openSync2, readFileSync as readFileSync12, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
12282
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
12283
|
+
import { homedir as homedir13 } from "os";
|
|
11608
12284
|
function truncate(s, max = PREVIEW_MAX) {
|
|
11609
12285
|
if (s.length <= max) return s;
|
|
11610
12286
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -11642,9 +12318,9 @@ function appendTurn(args2) {
|
|
|
11642
12318
|
function readRecentTurns(n = 20) {
|
|
11643
12319
|
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
11644
12320
|
try {
|
|
11645
|
-
const size =
|
|
12321
|
+
const size = statSync2(TURN_LOG_PATH).size;
|
|
11646
12322
|
if (size === 0) return [];
|
|
11647
|
-
const text =
|
|
12323
|
+
const text = readFileSync12(TURN_LOG_PATH, "utf-8");
|
|
11648
12324
|
const lines = text.split("\n").filter(Boolean);
|
|
11649
12325
|
const lastN = lines.slice(-n).reverse();
|
|
11650
12326
|
return lastN.map((line) => {
|
|
@@ -11668,7 +12344,7 @@ function followTurns(onEntry) {
|
|
|
11668
12344
|
}
|
|
11669
12345
|
let lastSize = (() => {
|
|
11670
12346
|
try {
|
|
11671
|
-
return
|
|
12347
|
+
return statSync2(TURN_LOG_PATH).size;
|
|
11672
12348
|
} catch {
|
|
11673
12349
|
return 0;
|
|
11674
12350
|
}
|
|
@@ -11723,7 +12399,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
11723
12399
|
var init_turnLog = __esm({
|
|
11724
12400
|
"cli/local-cc/turnLog.ts"() {
|
|
11725
12401
|
"use strict";
|
|
11726
|
-
TURN_LOG_PATH =
|
|
12402
|
+
TURN_LOG_PATH = join13(homedir13(), ".synkro", "cc_sessions", "turns.log");
|
|
11727
12403
|
PREVIEW_MAX = 400;
|
|
11728
12404
|
}
|
|
11729
12405
|
});
|
|
@@ -11872,19 +12548,19 @@ var init_grade = __esm({
|
|
|
11872
12548
|
});
|
|
11873
12549
|
|
|
11874
12550
|
// cli/local-cc/pueue.ts
|
|
11875
|
-
import { execFileSync as execFileSync3, spawnSync as
|
|
11876
|
-
import { homedir as
|
|
11877
|
-
import { join as
|
|
12551
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync7, spawn as spawn2 } from "child_process";
|
|
12552
|
+
import { homedir as homedir14 } from "os";
|
|
12553
|
+
import { join as join14 } from "path";
|
|
11878
12554
|
import { connect as connect2 } from "net";
|
|
11879
12555
|
function pueueAvailable() {
|
|
11880
|
-
const r =
|
|
12556
|
+
const r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
11881
12557
|
if (r.status !== 0) {
|
|
11882
12558
|
throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
|
|
11883
12559
|
}
|
|
11884
12560
|
}
|
|
11885
12561
|
function statusJson() {
|
|
11886
12562
|
pueueAvailable();
|
|
11887
|
-
const r =
|
|
12563
|
+
const r = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8" });
|
|
11888
12564
|
if (r.status !== 0) {
|
|
11889
12565
|
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
11890
12566
|
}
|
|
@@ -11929,18 +12605,18 @@ function startTask(opts = {}) {
|
|
|
11929
12605
|
let existing = findTask(ch);
|
|
11930
12606
|
while (existing) {
|
|
11931
12607
|
if (existing.status === "Running" || existing.status === "Queued") {
|
|
11932
|
-
|
|
11933
|
-
|
|
12608
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
12609
|
+
spawnSync7("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
11934
12610
|
for (let i = 0; i < 10; i++) {
|
|
11935
12611
|
const check = findTask(ch);
|
|
11936
12612
|
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
11937
|
-
|
|
12613
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
11938
12614
|
}
|
|
11939
12615
|
}
|
|
11940
|
-
|
|
12616
|
+
spawnSync7("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
11941
12617
|
existing = findTask(ch);
|
|
11942
12618
|
}
|
|
11943
|
-
const runScript =
|
|
12619
|
+
const runScript = join14(cwd, "run-claude.sh");
|
|
11944
12620
|
const args2 = [
|
|
11945
12621
|
"add",
|
|
11946
12622
|
"--label",
|
|
@@ -11951,7 +12627,7 @@ function startTask(opts = {}) {
|
|
|
11951
12627
|
"bash",
|
|
11952
12628
|
runScript
|
|
11953
12629
|
];
|
|
11954
|
-
const r =
|
|
12630
|
+
const r = spawnSync7("pueue", args2, { encoding: "utf-8" });
|
|
11955
12631
|
if (r.status !== 0) {
|
|
11956
12632
|
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
11957
12633
|
}
|
|
@@ -11962,25 +12638,25 @@ function startTask(opts = {}) {
|
|
|
11962
12638
|
return created;
|
|
11963
12639
|
}
|
|
11964
12640
|
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
11965
|
-
|
|
12641
|
+
spawnSync7("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
11966
12642
|
let t = findTask(channel);
|
|
11967
12643
|
while (t) {
|
|
11968
12644
|
if (t.status === "Running" || t.status === "Queued") {
|
|
11969
|
-
|
|
12645
|
+
spawnSync7("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
11970
12646
|
for (let i = 0; i < 10; i++) {
|
|
11971
12647
|
const check = findTask(channel);
|
|
11972
12648
|
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
11973
|
-
|
|
12649
|
+
spawnSync7("sleep", ["0.5"], { encoding: "utf-8" });
|
|
11974
12650
|
}
|
|
11975
12651
|
}
|
|
11976
|
-
|
|
12652
|
+
spawnSync7("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
11977
12653
|
t = findTask(channel);
|
|
11978
12654
|
}
|
|
11979
12655
|
}
|
|
11980
12656
|
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
11981
12657
|
const t = findTask(channel);
|
|
11982
12658
|
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
11983
|
-
const r =
|
|
12659
|
+
const r = spawnSync7("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
11984
12660
|
return r.stdout || r.stderr || "(no output)";
|
|
11985
12661
|
}
|
|
11986
12662
|
function ensureRunning(opts = {}) {
|
|
@@ -12005,8 +12681,8 @@ function probePort(host, port, timeoutMs = 500) {
|
|
|
12005
12681
|
});
|
|
12006
12682
|
}
|
|
12007
12683
|
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
12008
|
-
|
|
12009
|
-
|
|
12684
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
12685
|
+
spawnSync7("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
12010
12686
|
}
|
|
12011
12687
|
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
12012
12688
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -12018,46 +12694,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
|
|
|
12018
12694
|
return probePort(host, port);
|
|
12019
12695
|
}
|
|
12020
12696
|
function brewInstall(pkg) {
|
|
12021
|
-
const brew =
|
|
12697
|
+
const brew = spawnSync7("brew", ["--version"], { encoding: "utf-8" });
|
|
12022
12698
|
if (brew.status !== 0) return false;
|
|
12023
12699
|
console.log(` Installing ${pkg} via brew...`);
|
|
12024
|
-
const r =
|
|
12700
|
+
const r = spawnSync7("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
12025
12701
|
return r.status === 0;
|
|
12026
12702
|
}
|
|
12027
12703
|
function assertPueueInstalled() {
|
|
12028
|
-
let r =
|
|
12704
|
+
let r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12029
12705
|
if (r.status !== 0) {
|
|
12030
12706
|
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
12031
|
-
r =
|
|
12707
|
+
r = spawnSync7("pueue", ["--version"], { encoding: "utf-8" });
|
|
12032
12708
|
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
12033
12709
|
} else {
|
|
12034
12710
|
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
12035
12711
|
}
|
|
12036
12712
|
}
|
|
12037
|
-
const status =
|
|
12713
|
+
const status = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12038
12714
|
if (status.status !== 0) {
|
|
12039
12715
|
console.log(" Starting pueued daemon...");
|
|
12040
12716
|
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
12041
12717
|
child.unref();
|
|
12042
|
-
|
|
12043
|
-
const retry =
|
|
12718
|
+
spawnSync7("sleep", ["1"]);
|
|
12719
|
+
const retry = spawnSync7("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
12044
12720
|
if (retry.status !== 0) {
|
|
12045
12721
|
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
12046
12722
|
}
|
|
12047
12723
|
}
|
|
12048
|
-
|
|
12724
|
+
spawnSync7("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
12049
12725
|
}
|
|
12050
12726
|
function assertClaudeInstalled() {
|
|
12051
|
-
const r =
|
|
12727
|
+
const r = spawnSync7("claude", ["--version"], { encoding: "utf-8" });
|
|
12052
12728
|
if (r.status !== 0) {
|
|
12053
12729
|
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
12054
12730
|
}
|
|
12055
12731
|
}
|
|
12056
12732
|
function assertTmuxInstalled() {
|
|
12057
|
-
let r =
|
|
12733
|
+
let r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12058
12734
|
if (r.status !== 0) {
|
|
12059
12735
|
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
12060
|
-
r =
|
|
12736
|
+
r = spawnSync7("tmux", ["-V"], { encoding: "utf-8" });
|
|
12061
12737
|
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
12062
12738
|
} else {
|
|
12063
12739
|
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
@@ -12070,12 +12746,12 @@ var init_pueue = __esm({
|
|
|
12070
12746
|
"use strict";
|
|
12071
12747
|
TASK_LABEL = "synkro-local-cc";
|
|
12072
12748
|
TMUX_SESSION = "synkro-local-cc";
|
|
12073
|
-
SESSION_DIR2 =
|
|
12749
|
+
SESSION_DIR2 = join14(homedir14(), ".synkro", "cc_sessions");
|
|
12074
12750
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
12075
12751
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
12076
|
-
SESSION_DIR_22 =
|
|
12077
|
-
SESSION_DIR_32 =
|
|
12078
|
-
SESSION_DIR_42 =
|
|
12752
|
+
SESSION_DIR_22 = join14(homedir14(), ".synkro", "cc_sessions_2");
|
|
12753
|
+
SESSION_DIR_32 = join14(homedir14(), ".synkro", "cc_sessions_3");
|
|
12754
|
+
SESSION_DIR_42 = join14(homedir14(), ".synkro", "cc_sessions_4");
|
|
12079
12755
|
PueueError = class extends Error {
|
|
12080
12756
|
constructor(message, cause) {
|
|
12081
12757
|
super(message);
|
|
@@ -12090,13 +12766,13 @@ var init_pueue = __esm({
|
|
|
12090
12766
|
});
|
|
12091
12767
|
|
|
12092
12768
|
// cli/local-cc/settings.ts
|
|
12093
|
-
import { existsSync as existsSync15, readFileSync as
|
|
12094
|
-
import { homedir as
|
|
12095
|
-
import { join as
|
|
12769
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
12770
|
+
import { homedir as homedir15 } from "os";
|
|
12771
|
+
import { join as join15 } from "path";
|
|
12096
12772
|
function isLocalCCEnabled() {
|
|
12097
12773
|
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
12098
12774
|
try {
|
|
12099
|
-
const content =
|
|
12775
|
+
const content = readFileSync13(CONFIG_PATH3, "utf-8");
|
|
12100
12776
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
12101
12777
|
return match?.[1] === "yes";
|
|
12102
12778
|
} catch {
|
|
@@ -12107,7 +12783,7 @@ var CONFIG_PATH3;
|
|
|
12107
12783
|
var init_settings = __esm({
|
|
12108
12784
|
"cli/local-cc/settings.ts"() {
|
|
12109
12785
|
"use strict";
|
|
12110
|
-
CONFIG_PATH3 =
|
|
12786
|
+
CONFIG_PATH3 = join15(homedir15(), ".synkro", "config.env");
|
|
12111
12787
|
}
|
|
12112
12788
|
});
|
|
12113
12789
|
|
|
@@ -12116,11 +12792,11 @@ var localCc_exports = {};
|
|
|
12116
12792
|
__export(localCc_exports, {
|
|
12117
12793
|
localCcCommand: () => localCcCommand
|
|
12118
12794
|
});
|
|
12119
|
-
import { spawnSync as
|
|
12120
|
-
import { homedir as
|
|
12121
|
-
import { join as
|
|
12795
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
12796
|
+
import { homedir as homedir16 } from "os";
|
|
12797
|
+
import { join as join16 } from "path";
|
|
12122
12798
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
12123
|
-
import { existsSync as existsSync16, readFileSync as
|
|
12799
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
12124
12800
|
function deploymentMode() {
|
|
12125
12801
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
12126
12802
|
if (env === "docker") return "docker";
|
|
@@ -12227,14 +12903,14 @@ TROUBLESHOOTING
|
|
|
12227
12903
|
}
|
|
12228
12904
|
function readGatewayUrl() {
|
|
12229
12905
|
if (existsSync16(CONFIG_PATH4)) {
|
|
12230
|
-
const m =
|
|
12906
|
+
const m = readFileSync14(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
12231
12907
|
if (m) return m[1];
|
|
12232
12908
|
}
|
|
12233
12909
|
return "https://api.synkro.sh";
|
|
12234
12910
|
}
|
|
12235
12911
|
function updateLocalInferenceFlag(enabled) {
|
|
12236
12912
|
if (!existsSync16(CONFIG_PATH4)) return;
|
|
12237
|
-
let content =
|
|
12913
|
+
let content = readFileSync14(CONFIG_PATH4, "utf-8");
|
|
12238
12914
|
const flag = enabled ? "yes" : "no";
|
|
12239
12915
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
12240
12916
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -12297,7 +12973,7 @@ async function cmdStatus() {
|
|
|
12297
12973
|
}
|
|
12298
12974
|
const ch1Up = await isChannelAvailable();
|
|
12299
12975
|
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
12300
|
-
const tmux1 =
|
|
12976
|
+
const tmux1 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12301
12977
|
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
12302
12978
|
const t2 = findTask(CHANNEL_SECONDARY);
|
|
12303
12979
|
if (!t2) {
|
|
@@ -12307,7 +12983,7 @@ async function cmdStatus() {
|
|
|
12307
12983
|
}
|
|
12308
12984
|
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
12309
12985
|
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
12310
|
-
const tmux2 =
|
|
12986
|
+
const tmux2 = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
|
|
12311
12987
|
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
12312
12988
|
}
|
|
12313
12989
|
async function cmdEnable() {
|
|
@@ -12513,7 +13189,7 @@ function cmdLogs(rest) {
|
|
|
12513
13189
|
}
|
|
12514
13190
|
return "200";
|
|
12515
13191
|
})();
|
|
12516
|
-
|
|
13192
|
+
spawnSync8("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
|
|
12517
13193
|
return;
|
|
12518
13194
|
}
|
|
12519
13195
|
for (const arg of rest) {
|
|
@@ -12561,7 +13237,7 @@ function cmdLogs(rest) {
|
|
|
12561
13237
|
function cmdAttach(rest) {
|
|
12562
13238
|
assertTmuxInstalled();
|
|
12563
13239
|
const readonly = rest.some((a) => a === "--readonly" || a === "-r");
|
|
12564
|
-
const has =
|
|
13240
|
+
const has = spawnSync8("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
12565
13241
|
if (has.status !== 0) {
|
|
12566
13242
|
console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
|
|
12567
13243
|
process.exit(1);
|
|
@@ -12574,7 +13250,7 @@ function cmdAttach(rest) {
|
|
|
12574
13250
|
console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
|
|
12575
13251
|
console.log();
|
|
12576
13252
|
const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
|
|
12577
|
-
const r =
|
|
13253
|
+
const r = spawnSync8("tmux", args2, { stdio: "inherit" });
|
|
12578
13254
|
process.exit(r.status ?? 0);
|
|
12579
13255
|
}
|
|
12580
13256
|
async function cmdTest() {
|
|
@@ -12673,8 +13349,8 @@ var init_localCc = __esm({
|
|
|
12673
13349
|
init_install();
|
|
12674
13350
|
init_client();
|
|
12675
13351
|
init_stub();
|
|
12676
|
-
SYNKRO_CONFIG_PATH =
|
|
12677
|
-
CONFIG_PATH4 =
|
|
13352
|
+
SYNKRO_CONFIG_PATH = join16(homedir16(), ".synkro", "config.env");
|
|
13353
|
+
CONFIG_PATH4 = join16(homedir16(), ".synkro", "config.env");
|
|
12678
13354
|
}
|
|
12679
13355
|
});
|
|
12680
13356
|
|
|
@@ -12785,13 +13461,13 @@ var config_exports = {};
|
|
|
12785
13461
|
__export(config_exports, {
|
|
12786
13462
|
configCommand: () => configCommand
|
|
12787
13463
|
});
|
|
12788
|
-
import { readFileSync as
|
|
12789
|
-
import { join as
|
|
12790
|
-
import { homedir as
|
|
13464
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
13465
|
+
import { join as join17 } from "path";
|
|
13466
|
+
import { homedir as homedir17 } from "os";
|
|
12791
13467
|
function readConfigEnv() {
|
|
12792
13468
|
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
12793
13469
|
const out = {};
|
|
12794
|
-
for (const line of
|
|
13470
|
+
for (const line of readFileSync15(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
12795
13471
|
const t = line.trim();
|
|
12796
13472
|
if (!t || t.startsWith("#")) continue;
|
|
12797
13473
|
const eq = t.indexOf("=");
|
|
@@ -12804,7 +13480,7 @@ function updateConfigValue(key, value) {
|
|
|
12804
13480
|
console.error("No config found. Run `synkro install` first.");
|
|
12805
13481
|
process.exit(1);
|
|
12806
13482
|
}
|
|
12807
|
-
const lines =
|
|
13483
|
+
const lines = readFileSync15(CONFIG_PATH5, "utf-8").split("\n");
|
|
12808
13484
|
const pattern = new RegExp(`^${key}=`);
|
|
12809
13485
|
let found = false;
|
|
12810
13486
|
const updated = lines.map((line) => {
|
|
@@ -12920,25 +13596,25 @@ To change:`);
|
|
|
12920
13596
|
updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
|
|
12921
13597
|
console.log(`\u2713 Inference set to '${inferenceValue}'.`);
|
|
12922
13598
|
}
|
|
12923
|
-
var
|
|
13599
|
+
var SYNKRO_DIR7, CONFIG_PATH5;
|
|
12924
13600
|
var init_config = __esm({
|
|
12925
13601
|
"cli/commands/config.ts"() {
|
|
12926
13602
|
"use strict";
|
|
12927
13603
|
init_stub();
|
|
12928
|
-
|
|
12929
|
-
CONFIG_PATH5 =
|
|
13604
|
+
SYNKRO_DIR7 = join17(homedir17(), ".synkro");
|
|
13605
|
+
CONFIG_PATH5 = join17(SYNKRO_DIR7, "config.env");
|
|
12930
13606
|
}
|
|
12931
13607
|
});
|
|
12932
13608
|
|
|
12933
13609
|
// cli/bootstrap.js
|
|
12934
|
-
import { readFileSync as
|
|
13610
|
+
import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
|
|
12935
13611
|
import { resolve as resolve3 } from "path";
|
|
12936
13612
|
var envCandidates = [
|
|
12937
13613
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
12938
13614
|
];
|
|
12939
13615
|
for (const envPath of envCandidates) {
|
|
12940
13616
|
if (!existsSync18(envPath)) continue;
|
|
12941
|
-
const envContent =
|
|
13617
|
+
const envContent = readFileSync16(envPath, "utf-8");
|
|
12942
13618
|
for (const line of envContent.split("\n")) {
|
|
12943
13619
|
const trimmed = line.trim();
|
|
12944
13620
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -12953,7 +13629,7 @@ var args = process.argv.slice(2);
|
|
|
12953
13629
|
var cmd = args[0] || "";
|
|
12954
13630
|
var subArgs = args.slice(1);
|
|
12955
13631
|
function printVersion() {
|
|
12956
|
-
console.log("1.6.
|
|
13632
|
+
console.log("1.6.59");
|
|
12957
13633
|
}
|
|
12958
13634
|
function printHelp2() {
|
|
12959
13635
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -13067,7 +13743,7 @@ async function main() {
|
|
|
13067
13743
|
}
|
|
13068
13744
|
case "claude-desktop": {
|
|
13069
13745
|
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
13070
|
-
await runClaudeDesktopTap2();
|
|
13746
|
+
await runClaudeDesktopTap2({ backfill: subArgs.includes("--backfill") });
|
|
13071
13747
|
break;
|
|
13072
13748
|
}
|
|
13073
13749
|
default: {
|