@synkro-sh/cli 1.3.39 → 1.3.41

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 CHANGED
@@ -388,12 +388,7 @@ fi
388
388
  CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
389
389
  synkro_log "bashGuard checking: $CMD_SHORT"
390
390
 
391
- # NO hard regex gate \u2014 server-side bashShapes runs the universal pattern set
392
- # (including HTTP-payload destructive shapes like graphql_destructive_mutation
393
- # and http_method_delete) and returns a "trivial" fast-path verdict for boring
394
- # read-only commands (ls, pwd, git status, --version, etc.). Anything ambiguous
395
- # goes to Cerebras for grading. This closes the gap that verb-only filters miss
396
- # (e.g. a curl POST whose JSON body contains a destructive mutation).
391
+ # All commands are graded; no client-side regex gate.
397
392
 
398
393
  # Extract context from the transcript file
399
394
  TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
@@ -455,7 +450,7 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
455
450
  ] | .[-10:]' 2>/dev/null || echo "[]")
456
451
  # Recent agent actions (last 5 tool_use blocks paired with results)
457
452
  RECENT_ACTIONS=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
458
- # tool_result blocks live in USER messages (Anthropic API format)
453
+ # tool_result blocks live in USER messages
459
454
  ([ .[]
460
455
  | select(.type == "user")
461
456
  | .message.content[]?
@@ -571,6 +566,24 @@ refresh_jwt() {
571
566
  return 0
572
567
  }
573
568
 
569
+ ensure_fresh_jwt() {
570
+ [ -z "$JWT" ] && return 1
571
+ local payload exp now remaining
572
+ payload=$(printf '%s' "$JWT" | cut -d. -f2)
573
+ case $((\${#payload} % 4)) in
574
+ 2) payload="\${payload}==" ;;
575
+ 3) payload="\${payload}=" ;;
576
+ esac
577
+ exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
578
+ now=$(date -u +%s)
579
+ remaining=$((exp - now))
580
+ if [ "$remaining" -lt 60 ]; then
581
+ refresh_jwt
582
+ fi
583
+ }
584
+
585
+ ensure_fresh_jwt
586
+
574
587
  # Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
575
588
  TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
576
589
  CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
@@ -656,7 +669,7 @@ if [ "$USE_LOCAL" = "true" ]; then
656
669
  ALTERNATIVE=$(xtag "$V_INNER" alternative)
657
670
 
658
671
  # Fail-open on no verdict \u2014 daemon handles cold fallback internally; if it
659
- # still came back empty (network/Anthropic outage), don't block the user.
672
+ # still came back empty (grader unavailable), don't block the user.
660
673
  if [ -z "$VERDICT_KIND" ] || [ -z "$SEVERITY" ]; then
661
674
  synkro_log "bashGuard $CMD_SHORT \u2192 pass (grader unavailable)"
662
675
  jq -n --arg m "[synkro] bashGuard \u2192 pass (grader unavailable)" '{systemMessage: $m}'
@@ -666,7 +679,7 @@ if [ "$USE_LOCAL" = "true" ]; then
666
679
  # already populated above. Server-side path (else branch) keeps using JSON.
667
680
  VERDICT="__LOCAL_XML_PARSED__"
668
681
  else
669
- # \u2500\u2500\u2500 FAST TIER: server-side Cerebras grading. \u2500\u2500\u2500
682
+ # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
670
683
 
671
684
  VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
672
685
  -H "Content-Type: application/json" \\
@@ -814,18 +827,7 @@ exit 0
814
827
  `;
815
828
  CC_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
816
829
  # Synkro PreToolUse Edit/Write/MultiEdit/NotebookEdit pre-check hook.
817
- #
818
- # Steering-at-violation: thin shim that POSTs the proposed file content to
819
- # /api/v1/precheck-edit. The server cosines it against the org's active
820
- # agent_runtime rules in Timescale and returns the CC hook JSON shape
821
- # directly (permissionDecision: "deny" + retry guidance, or empty {} to allow).
822
- #
823
- # No LLM in this path \u2014 just embedding + cosine. Customer's CC pays for the
824
- # retry generation when the agent reads the denial reason and rewrites.
825
- # Synkro's COGS = ~$0.00001 per edit (one Gemini embedding call).
826
- #
827
830
  # Always exits 0 with valid JSON. Fails open on any error.
828
- # No set -e: hook must ALWAYS produce JSON output. Silent death = CC timeout.
829
831
 
830
832
  synkro_log() { echo "[synkro] $1" >&2; }
831
833
 
@@ -1036,6 +1038,24 @@ refresh_jwt() {
1036
1038
  return 0
1037
1039
  }
1038
1040
 
1041
+ ensure_fresh_jwt() {
1042
+ [ -z "$JWT" ] && return 1
1043
+ local payload exp now remaining
1044
+ payload=$(printf '%s' "$JWT" | cut -d. -f2)
1045
+ case $((\${#payload} % 4)) in
1046
+ 2) payload="\${payload}==" ;;
1047
+ 3) payload="\${payload}=" ;;
1048
+ esac
1049
+ exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
1050
+ now=$(date -u +%s)
1051
+ remaining=$((exp - now))
1052
+ if [ "$remaining" -lt 60 ]; then
1053
+ refresh_jwt
1054
+ fi
1055
+ }
1056
+
1057
+ ensure_fresh_jwt
1058
+
1039
1059
 
1040
1060
  # Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
1041
1061
  TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
@@ -1201,7 +1221,7 @@ if [ "$USE_LOCAL" = "true" ]; then
1201
1221
  fi
1202
1222
  fi
1203
1223
  else
1204
- # \u2500\u2500\u2500 FAST TIER: server-side Cerebras grading (~500ms) \u2500\u2500\u2500
1224
+ # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
1205
1225
  RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit" \\
1206
1226
  -H "Content-Type: application/json" \\
1207
1227
  -H "Authorization: Bearer $JWT" \\
@@ -1291,19 +1311,8 @@ exit 0
1291
1311
  `;
1292
1312
  CC_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
1293
1313
  # Synkro PostToolUse Edit/Write afterFileEdit hook.
1294
- #
1295
- # Reads the file from disk after the edit lands, sends to /api/v1/judge-edit
1296
- # for Cerebras grading against the org's rules. On ok=false, emits a system
1297
- # message that surfaces inline in CC ("[synkro] afterFileEdit \u2192 Finding: \u2026").
1298
- # The agent reads the finding via transcript and re-edits on its own
1299
- # inference \u2014 same retry pattern as PreToolUse precheck, looser/asynchronous.
1300
- #
1301
- # Complementary to the PreToolUse precheck: precheck blocks unsafe writes
1302
- # before disk; this hook catches issues that only become visible AFTER the
1303
- # edit lands (cross-file shapes, real disk state, post-edit semantic effects).
1304
- #
1314
+ # On ok=false, emits a system message that surfaces inline in CC.
1305
1315
  # Always exits 0 with valid JSON \u2014 never breaks CC's flow.
1306
- # No set -e: hook must ALWAYS produce JSON output. Silent death = CC timeout.
1307
1316
 
1308
1317
  synkro_log() { echo "[synkro] $1" >&2; }
1309
1318
 
@@ -1422,6 +1431,24 @@ refresh_jwt() {
1422
1431
  return 0
1423
1432
  }
1424
1433
 
1434
+ ensure_fresh_jwt() {
1435
+ [ -z "$JWT" ] && return 1
1436
+ local payload exp now remaining
1437
+ payload=$(printf '%s' "$JWT" | cut -d. -f2)
1438
+ case $((\${#payload} % 4)) in
1439
+ 2) payload="\${payload}==" ;;
1440
+ 3) payload="\${payload}=" ;;
1441
+ esac
1442
+ exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
1443
+ now=$(date -u +%s)
1444
+ remaining=$((exp - now))
1445
+ if [ "$remaining" -lt 60 ]; then
1446
+ refresh_jwt
1447
+ fi
1448
+ }
1449
+
1450
+ ensure_fresh_jwt
1451
+
1425
1452
  # Fire-and-forget correction-followup (backgrounded \u2014 must not block grading).
1426
1453
  if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
1427
1454
  (
@@ -1533,7 +1560,7 @@ if [ "$USE_LOCAL" = "true" ]; then
1533
1560
  RESP=""
1534
1561
  fi
1535
1562
  else
1536
- # \u2500\u2500\u2500 FAST TIER: server-side Cerebras (~500ms). \u2500\u2500\u2500
1563
+ # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
1537
1564
  RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge-edit" \\
1538
1565
  -H "Content-Type: application/json" \\
1539
1566
  -H "Authorization: Bearer $JWT" \\
@@ -2235,11 +2262,7 @@ class WarmGrader:
2235
2262
  self._warm_proc = None
2236
2263
  self._warm_ready_at = 0.0
2237
2264
 
2238
- # Warm-process TTL: claude --print holds an HTTP/2 conn to Anthropic
2239
- # that goes stale after ~20s idle. The subprocess doesn't notice the
2240
- # broken pipe and just hangs forever when given a prompt. Force cold
2241
- # if the warm has been sitting idle too long \u2014 still faster than
2242
- # eating a 10s stall timeout + cold fallback.
2265
+ # Discard warm processes that have been idle too long.
2243
2266
  WARM_TTL_SEC = int(os.environ.get("SYNKRO_DAEMON_WARM_TTL", "15"))
2244
2267
  warm = True
2245
2268
  if not proc or proc.poll() is not None:
@@ -3723,7 +3746,7 @@ function writeConfigEnv(opts) {
3723
3746
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3724
3747
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3725
3748
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3726
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.39")}`
3749
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.41")}`
3727
3750
  ];
3728
3751
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3729
3752
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4770,54 +4793,9 @@ async function ensureOpenPr(repo, prNumber, sha) {
4770
4793
  try {
4771
4794
  pr = ghJson(["api", `/repos/${repo}/pulls/${prNumber}`]);
4772
4795
  } catch {
4773
- return { prNumber, sha, branched: false };
4774
- }
4775
- if (pr.state === "open") {
4776
- return { prNumber, sha, branched: false };
4777
- }
4778
- if (pr.merged) {
4779
- console.log(`PR #${prNumber} is merged. Scanning original diff and posting findings there.
4780
- `);
4781
- return { prNumber, sha: pr.head.sha, branched: false };
4782
- }
4783
- console.log(`PR #${prNumber} is closed. Creating a scan branch...
4784
- `);
4785
- const scanBranch = `synkro/scan-${pr.head.ref}-${Date.now()}`;
4786
- try {
4787
- execSync5(`gh api -X POST /repos/${repo}/git/refs --input -`, {
4788
- encoding: "utf-8",
4789
- input: JSON.stringify({ ref: `refs/heads/${scanBranch}`, sha: pr.head.sha }),
4790
- stdio: ["pipe", "pipe", "pipe"]
4791
- });
4792
- } catch (err) {
4793
- console.warn(`Failed to create scan branch: ${err.message}`);
4794
- return { prNumber, sha, branched: false };
4795
- }
4796
- try {
4797
- const newPrBody = `Security scan of closed PR #${prNumber} (\`${pr.head.ref}\`).
4798
-
4799
- This PR contains no code changes \u2014 it exists so Synkro can post review findings on an active PR.`;
4800
- const result = ghJson([
4801
- "api",
4802
- "-X",
4803
- "POST",
4804
- `/repos/${repo}/pulls`,
4805
- "-f",
4806
- `title=Synkro Scan: ${pr.title}`,
4807
- "-f",
4808
- `body=${newPrBody}`,
4809
- "-f",
4810
- `head=${scanBranch}`,
4811
- "-f",
4812
- `base=${pr.base.ref}`
4813
- ]);
4814
- console.log(`Opened PR #${result.number} for scan findings.
4815
- `);
4816
- return { prNumber: result.number, sha: result.head.sha, branched: true };
4817
- } catch (err) {
4818
- console.warn(`Failed to open scan PR: ${err.message}`);
4819
- return { prNumber, sha, branched: false };
4796
+ return { prNumber, sha, branched: false, prState: "unknown", merged: false };
4820
4797
  }
4798
+ return { prNumber, sha: pr.head.sha, branched: false, prState: pr.state, merged: pr.merged };
4821
4799
  }
4822
4800
  function getPrFiles(repo, prNumber) {
4823
4801
  const data = ghJson([
@@ -5094,7 +5072,28 @@ ${linesStr}: ${first.description}
5094
5072
  severity: maxSeverity
5095
5073
  };
5096
5074
  }
5097
- function postPrReview(repo, prNumber, sha, review) {
5075
+ function postPrReview(repo, prNumber, sha, review, skipLineReview = false) {
5076
+ function postIssueComment() {
5077
+ try {
5078
+ const body = `## \u{1F512} Synkro Security Review
5079
+
5080
+ ${review.summary}
5081
+
5082
+ ` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
5083
+ execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
5084
+ encoding: "utf-8",
5085
+ input: JSON.stringify({ body }),
5086
+ stdio: ["pipe", "ignore", "pipe"]
5087
+ });
5088
+ console.log(" \u2713 Posted issue comment with findings.");
5089
+ } catch (err) {
5090
+ console.warn("Failed to post issue comment:", err.message);
5091
+ }
5092
+ }
5093
+ if (skipLineReview) {
5094
+ postIssueComment();
5095
+ return;
5096
+ }
5098
5097
  const preferredEvent = review.severity === "critical" || review.severity === "high" ? "REQUEST_CHANGES" : "COMMENT";
5099
5098
  function tryPost(event) {
5100
5099
  const body = JSON.stringify({
@@ -5120,27 +5119,12 @@ ${review.summary}`,
5120
5119
  if (combined.includes("own pull request") && event === "REQUEST_CHANGES") {
5121
5120
  return false;
5122
5121
  }
5123
- console.warn(`Failed to post review: ${(stderr || stdout || err.message).slice(0, 200)}`);
5124
5122
  return false;
5125
5123
  }
5126
5124
  }
5127
5125
  if (tryPost(preferredEvent)) return;
5128
5126
  if (preferredEvent === "REQUEST_CHANGES" && tryPost("COMMENT")) return;
5129
- try {
5130
- const fallbackBody = `## \u{1F512} Synkro Security Review
5131
-
5132
- ${review.summary}
5133
-
5134
- ` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
5135
- execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
5136
- encoding: "utf-8",
5137
- input: JSON.stringify({ body: fallbackBody }),
5138
- stdio: ["pipe", "ignore", "pipe"]
5139
- });
5140
- console.log(" \u2713 Posted fallback issue comment.");
5141
- } catch (err2) {
5142
- console.warn("Failed to post fallback comment:", err2.message);
5143
- }
5127
+ postIssueComment();
5144
5128
  }
5145
5129
  function postCheckRun(repo, sha, conclusion, findings) {
5146
5130
  const summary = findings.length === 0 ? "No security findings." : `${findings.length} finding(s):
@@ -5295,7 +5279,8 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
5295
5279
  const review = await spawnOpusConsolidator(allFindings, claudeToken);
5296
5280
  console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
5297
5281
  if (review.comments.length > 0) {
5298
- postPrReview(repo, activePrNumber, activeSha, review);
5282
+ const skipLineReview = prTarget.prState === "closed" && !prTarget.merged;
5283
+ postPrReview(repo, activePrNumber, activeSha, review, skipLineReview);
5299
5284
  }
5300
5285
  }
5301
5286
  const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
@@ -5474,7 +5459,7 @@ Usage:
5474
5459
 
5475
5460
  Commands:
5476
5461
  install [--force] Install Synkro hooks for detected agents (Claude Code, etc.)
5477
- login Authenticate with Synkro (browser OAuth via WorkOS)
5462
+ login Authenticate with Synkro (browser sign-in)
5478
5463
  logout Clear local credentials
5479
5464
  status Show current setup state
5480
5465
  link Link repos to a Synkro project (local git or GitHub OAuth)