@synkro-sh/cli 1.3.35 → 1.3.37

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
@@ -790,6 +790,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
790
790
  --arg category "$CATEGORY" \\
791
791
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
792
792
  --arg tool_name "$TOOL_NAME" \\
793
+ --arg repo "\${GIT_REPO:-}" \\
793
794
  '{
794
795
  event_id: $event_id,
795
796
  timestamp: $timestamp,
@@ -800,7 +801,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
800
801
  category: $category,
801
802
  model: $model,
802
803
  tool_name: $tool_name
803
- }')
804
+ } + (if $repo != "" then {repo: $repo} else {} end)')
804
805
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
805
806
  -H "Content-Type: application/json" \\
806
807
  -H "Authorization: Bearer $JWT" \\
@@ -1267,6 +1268,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1267
1268
  --arg category "$LOCAL_CATEGORY" \\
1268
1269
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1269
1270
  --arg tool_name "$TOOL_NAME" \\
1271
+ --arg repo "\${GIT_REPO:-}" \\
1270
1272
  '{
1271
1273
  event_id: $event_id,
1272
1274
  timestamp: $timestamp,
@@ -1276,7 +1278,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1276
1278
  category: $category,
1277
1279
  model: $model,
1278
1280
  tool_name: $tool_name
1279
- }')
1281
+ } + (if $repo != "" then {repo: $repo} else {} end)')
1280
1282
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1281
1283
  -H "Content-Type: application/json" \\
1282
1284
  -H "Authorization: Bearer $JWT" \\
@@ -1573,11 +1575,12 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1573
1575
  --arg category "$CATEGORY" \\
1574
1576
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1575
1577
  --arg tool_name "$TOOL_NAME" \\
1578
+ --arg repo "\${GIT_REPO:-}" \\
1576
1579
  '{
1577
1580
  event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1578
1581
  verdict: $verdict, severity: $severity, risk_level: $risk_level,
1579
1582
  category: $category, model: $model, tool_name: $tool_name
1580
- }')
1583
+ } + (if $repo != "" then {repo: $repo} else {} end)')
1581
1584
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1582
1585
  -H "Content-Type: application/json" \\
1583
1586
  -H "Authorization: Bearer $JWT" \\
@@ -1986,25 +1989,73 @@ def _read_jwt():
1986
1989
  except Exception:
1987
1990
  return ""
1988
1991
 
1992
+ def _refresh_jwt():
1993
+ """Refresh the access token using the saved refresh_token. Writes new
1994
+ tokens back to credentials.json. Returns the new access_token or ''."""
1995
+ try:
1996
+ with open(CREDS_PATH) as f:
1997
+ creds = json.load(f)
1998
+ rt = creds.get("refresh_token", "")
1999
+ if not rt:
2000
+ return ""
2001
+ req = urllib.request.Request(
2002
+ f"{GATEWAY_URL}/api/auth/refresh",
2003
+ data=json.dumps({"refresh_token": rt}).encode("utf-8"),
2004
+ headers={
2005
+ "Content-Type": "application/json",
2006
+ "User-Agent": "synkro-cli-grader-daemon/1",
2007
+ },
2008
+ )
2009
+ with urllib.request.urlopen(req, timeout=5) as resp:
2010
+ data = json.loads(resp.read().decode("utf-8"))
2011
+ new_at = data.get("access_token", "")
2012
+ new_rt = data.get("refresh_token", "") or rt
2013
+ if not new_at:
2014
+ return ""
2015
+ creds["access_token"] = new_at
2016
+ creds["refresh_token"] = new_rt
2017
+ tmp = str(CREDS_PATH) + ".synkro.tmp"
2018
+ with open(tmp, "w") as f:
2019
+ json.dump(creds, f)
2020
+ os.replace(tmp, str(CREDS_PATH))
2021
+ return new_at
2022
+ except Exception:
2023
+ return ""
2024
+
1989
2025
  def fetch_primer(mode):
1990
- """Fetch primer text for {bash,edit} from /cli/judge-prompts. In-memory only."""
2026
+ """Fetch primer text for {bash,edit} from /cli/judge-prompts. In-memory only.
2027
+ Auto-refreshes JWT on 401 and retries once."""
1991
2028
  _load_gateway_url()
1992
- jwt = _read_jwt()
1993
- if not jwt:
1994
- return ""
1995
2029
  field = "grader_primer_bash" if mode == "bash" else "grader_primer_edit"
1996
- try:
2030
+
2031
+ def _do_fetch(jwt):
1997
2032
  req = urllib.request.Request(
1998
2033
  f"{GATEWAY_URL}/api/v1/cli/judge-prompts",
1999
2034
  headers={
2000
2035
  "Authorization": f"Bearer {jwt}",
2001
- # CF rejects requests without UA \u2014 without this we get 403.
2002
2036
  "User-Agent": "synkro-cli-grader-daemon/1",
2003
2037
  },
2004
2038
  )
2005
2039
  with urllib.request.urlopen(req, timeout=5) as resp:
2006
2040
  data = json.loads(resp.read().decode("utf-8"))
2007
2041
  return data.get(field, "") or ""
2042
+
2043
+ jwt = _read_jwt()
2044
+ if not jwt:
2045
+ return ""
2046
+ try:
2047
+ return _do_fetch(jwt)
2048
+ except urllib.error.HTTPError as e:
2049
+ if e.code != 401:
2050
+ return ""
2051
+ # Token expired \u2014 refresh and retry once.
2052
+ new_jwt = _refresh_jwt()
2053
+ if not new_jwt:
2054
+ return ""
2055
+ try:
2056
+ return _do_fetch(new_jwt)
2057
+ except Exception:
2058
+ return ""
2008
2059
  except Exception:
2009
2060
  return ""
2010
2061
 
@@ -2111,6 +2162,7 @@ class WarmGrader:
2111
2162
  def __init__(self, primer):
2112
2163
  self.primer = primer or ""
2113
2164
  self._warm_proc = None
2165
+ self._warm_ready_at = 0.0
2114
2166
  self._warm_thread = None
2115
2167
  self._lock = threading.Lock()
2116
2168
  self._total_grades = 0
@@ -2149,6 +2201,7 @@ class WarmGrader:
2149
2201
  with self._lock:
2150
2202
  old = self._warm_proc
2151
2203
  self._warm_proc = proc
2204
+ self._warm_ready_at = time.time()
2152
2205
  self._prewarm_ok = True
2153
2206
  if old:
2154
2207
  self._kill_proc(old)
@@ -2173,8 +2226,16 @@ class WarmGrader:
2173
2226
 
2174
2227
  with self._lock:
2175
2228
  proc = self._warm_proc
2229
+ ready_at = self._warm_ready_at
2176
2230
  self._warm_proc = None
2177
-
2231
+ self._warm_ready_at = 0.0
2232
+
2233
+ # Warm-process TTL: claude --print holds an HTTP/2 conn to Anthropic
2234
+ # that goes stale after ~20s idle. The subprocess doesn't notice the
2235
+ # broken pipe and just hangs forever when given a prompt. Force cold
2236
+ # if the warm has been sitting idle too long \u2014 still faster than
2237
+ # eating a 10s stall timeout + cold fallback.
2238
+ WARM_TTL_SEC = int(os.environ.get("SYNKRO_DAEMON_WARM_TTL", "15"))
2178
2239
  warm = True
2179
2240
  if not proc or proc.poll() is not None:
2180
2241
  log("no warm process, cold fallback")
@@ -2185,6 +2246,12 @@ class WarmGrader:
2185
2246
  self._kill_proc(proc)
2186
2247
  proc = self._make_proc()
2187
2248
  warm = False
2249
+ elif ready_at and (time.time() - ready_at) > WARM_TTL_SEC:
2250
+ age = time.time() - ready_at
2251
+ log(f"warm process stale ({age:.0f}s > {WARM_TTL_SEC}s), cold fallback")
2252
+ self._kill_proc(proc)
2253
+ proc = self._make_proc()
2254
+ warm = False
2188
2255
 
2189
2256
  wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
2190
2257
  t0 = time.time()
@@ -3651,7 +3718,7 @@ function writeConfigEnv(opts) {
3651
3718
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3652
3719
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3653
3720
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3654
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.35")}`
3721
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.37")}`
3655
3722
  ];
3656
3723
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3657
3724
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);