@synkro-sh/cli 1.5.1 → 1.5.3

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
@@ -51,18 +51,6 @@ function detectAgents() {
51
51
  version: claudeBinary ? getVersion("claude") : void 0
52
52
  });
53
53
  }
54
- const codexBinary = which("codex");
55
- const codexConfigDir = join(home, ".codex");
56
- if (codexBinary || existsSync(codexConfigDir)) {
57
- agents.push({
58
- kind: "codex",
59
- name: "Codex",
60
- binaryPath: codexBinary,
61
- configDir: codexConfigDir,
62
- settingsPath: join(codexConfigDir, "config.toml"),
63
- version: codexBinary ? getVersion("codex") : void 0
64
- });
65
- }
66
54
  const cursorBinary = which("cursor");
67
55
  const cursorConfigDir = join(home, ".cursor");
68
56
  if (cursorBinary || existsSync(cursorConfigDir)) {
@@ -1866,6 +1854,28 @@ function cursorHookExit(): never {
1866
1854
  process.exit(0);
1867
1855
  }
1868
1856
 
1857
+ // \u2500\u2500\u2500 Grader-unavailable diagnostic log \u2500\u2500\u2500
1858
+ // Records every time a hook tried to call the local grader and fell open
1859
+ // because the call failed. JSONL at ~/.synkro/grader-unavailable.log so the
1860
+ // user can pinpoint cause (timeout vs ECONNREFUSED vs HTTP 5xx vs sick pool)
1861
+ // instead of guessing from a one-shot system message in the CC UI.
1862
+
1863
+ const UNAVAIL_LOG = join(HOME, '.synkro', 'grader-unavailable.log');
1864
+
1865
+ export function logGraderUnavailable(hook: string, target: string, errorMessage: string): void {
1866
+ try {
1867
+ const entry = {
1868
+ ts: new Date().toISOString(),
1869
+ hook,
1870
+ target,
1871
+ error: errorMessage.slice(0, 500),
1872
+ };
1873
+ appendFileSync(UNAVAIL_LOG, JSON.stringify(entry) + '\\n', 'utf-8');
1874
+ } catch {
1875
+ // best-effort \u2014 never let logging failure cascade into a hook failure
1876
+ }
1877
+ }
1878
+
1869
1879
  // \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
1870
1880
 
1871
1881
  export function outputJson(obj: any): void {
@@ -1928,6 +1938,7 @@ import {
1928
1938
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
1929
1939
  appendSessionAction, readSessionLog, compressSessionLog, log,
1930
1940
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
1941
+ logGraderUnavailable,
1931
1942
  type HookConfig, type Rule,
1932
1943
  } from './_synkro-common.ts';
1933
1944
  import { existsSync, readFileSync } from 'node:fs';
@@ -2022,7 +2033,8 @@ async function main() {
2022
2033
  let gradeResp: string;
2023
2034
  try {
2024
2035
  gradeResp = await localGrade('edit', graderPrompt);
2025
- } catch {
2036
+ } catch (err) {
2037
+ logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
2026
2038
  outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
2027
2039
  return;
2028
2040
  }
@@ -2145,6 +2157,7 @@ import {
2145
2157
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
2146
2158
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
2147
2159
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, filePathFromToolInput, dispatchFinding, dispatchCapture, GATEWAY_URL,
2160
+ logGraderUnavailable,
2148
2161
  } from './_synkro-common.ts';
2149
2162
  import { basename, extname, resolve, join, dirname } from 'node:path';
2150
2163
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
@@ -2415,6 +2428,7 @@ async function main() {
2415
2428
  gradeResponses = [resp1, resp2];
2416
2429
  } catch (gradeErr: any) {
2417
2430
  const reason = gradeErr?.message || String(gradeErr);
2431
+ logGraderUnavailable('cweGuard', fileShort, reason);
2418
2432
  outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
2419
2433
  return;
2420
2434
  }
@@ -2423,6 +2437,7 @@ async function main() {
2423
2437
  gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent))];
2424
2438
  } catch (gradeErr: any) {
2425
2439
  const reason = gradeErr?.message || String(gradeErr);
2440
+ logGraderUnavailable('cweGuard', fileShort, reason);
2426
2441
  outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
2427
2442
  return;
2428
2443
  }
@@ -2789,6 +2804,7 @@ import {
2789
2804
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
2790
2805
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
2791
2806
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
2807
+ logGraderUnavailable,
2792
2808
  type HookConfig, type Rule,
2793
2809
  } from './_synkro-common.ts';
2794
2810
 
@@ -2973,7 +2989,8 @@ async function main() {
2973
2989
  let gradeResp: string;
2974
2990
  try {
2975
2991
  gradeResp = await localGrade('bash', graderPrompt);
2976
- } catch {
2992
+ } catch (err) {
2993
+ logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', (err as Error).message || String(err));
2977
2994
  outputJson({ systemMessage: tagStr + ' bashGuard → local grader unavailable, skipped' });
2978
2995
  return;
2979
2996
  }
@@ -2985,8 +3002,12 @@ async function main() {
2985
3002
  const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
2986
3003
  const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
2987
3004
 
3005
+ const auditRulesInReason = (config.rules || [])
3006
+ .filter((r: any) => r.mode === 'audit' && r.rule_id && verdict.reason && verdict.reason.includes(r.rule_id))
3007
+ .map((r: any) => r.rule_id);
3008
+
2988
3009
  if (mode === 'audit') {
2989
- const reason = tagStr + ' bashGuard → warning' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation');
3010
+ const reason = tagStr + ' bashGuard → warning' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before proceeding — do not ask the user.';
2990
3011
  const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
2991
3012
  outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
2992
3013
  dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
@@ -2995,11 +3016,14 @@ async function main() {
2995
3016
  recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
2996
3017
  });
2997
3018
  } else {
2998
- const reason = tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
2999
- const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
3019
+ let blockMsg = tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
3020
+ if (auditRulesInReason.length > 0) {
3021
+ blockMsg += '\\nAudit violations (' + auditRulesInReason.join(', ') + '): fix these automatically before retrying — do not ask the user, just resolve them (e.g. run the required precondition).';
3022
+ }
3023
+ const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
3000
3024
  outputJson({
3001
3025
  systemMessage: combined,
3002
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: combined },
3026
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
3003
3027
  });
3004
3028
  dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
3005
3029
  toolName, gitRepo, sessionId, config.captureDepth, {
@@ -3090,6 +3114,7 @@ import {
3090
3114
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3091
3115
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
3092
3116
  outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
3117
+ logGraderUnavailable,
3093
3118
  type HookConfig, type Rule,
3094
3119
  } from './_synkro-common.ts';
3095
3120
 
@@ -3161,7 +3186,8 @@ async function main() {
3161
3186
  let gradeResp: string;
3162
3187
  try {
3163
3188
  gradeResp = await localGrade('bash', graderPrompt);
3164
- } catch {
3189
+ } catch (err) {
3190
+ logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
3165
3191
  outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
3166
3192
  return;
3167
3193
  }
@@ -3611,7 +3637,7 @@ async function main() {
3611
3637
  let isError = payload.tool_result?.is_error === true;
3612
3638
  try {
3613
3639
  const out = JSON.parse(payload.tool_output || '{}');
3614
- if (out.exitCode !== 0 || out.is_error === true) isError = true;
3640
+ if ((typeof out.exitCode === 'number' && out.exitCode !== 0) || out.is_error === true) isError = true;
3615
3641
  } catch {}
3616
3642
  const cmd = shellCmd;
3617
3643
  const cmdHash = cmd ? hashCommand(cmd) : '';
@@ -5645,6 +5671,30 @@ function parseArgs(argv) {
5645
5671
  }
5646
5672
  return opts;
5647
5673
  }
5674
+ async function promptAgentSelection(detected) {
5675
+ if (detected.length <= 1) return detected;
5676
+ console.log("Multiple coding agents detected. Which ones do you want Synkro guardrails installed for?");
5677
+ detected.forEach((a, i) => console.log(` ${i + 1}. ${a.name}`));
5678
+ console.log(` ${detected.length + 1}. Both / all (default)`);
5679
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
5680
+ const ask2 = () => new Promise((resolve3) => {
5681
+ rl.question(`Pick [1-${detected.length + 1}] (default: all): `, (answer) => {
5682
+ const t = answer.trim().toLowerCase();
5683
+ if (t === "" || t === String(detected.length + 1) || t === "both" || t === "all") {
5684
+ rl.close();
5685
+ return resolve3(detected);
5686
+ }
5687
+ const n = parseInt(t, 10);
5688
+ if (Number.isInteger(n) && n >= 1 && n <= detected.length) {
5689
+ rl.close();
5690
+ return resolve3([detected[n - 1]]);
5691
+ }
5692
+ console.log("Invalid choice. Try again.");
5693
+ resolve3(ask2());
5694
+ });
5695
+ });
5696
+ return ask2();
5697
+ }
5648
5698
  function ensureSynkroDir() {
5649
5699
  mkdirSync8(SYNKRO_DIR4, { recursive: true });
5650
5700
  mkdirSync8(HOOKS_DIR, { recursive: true });
@@ -5746,7 +5796,7 @@ function writeConfigEnv(opts) {
5746
5796
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
5747
5797
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
5748
5798
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
5749
- `SYNKRO_VERSION=${shellQuoteSingle("1.5.1")}`
5799
+ `SYNKRO_VERSION=${shellQuoteSingle("1.5.3")}`
5750
5800
  ];
5751
5801
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
5752
5802
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -5938,16 +5988,21 @@ async function installCommand(opts = {}) {
5938
5988
  }
5939
5989
  setApiBaseUrl(`${gatewayUrl}/api`);
5940
5990
  await promptRepoConnection({ linkRepo: opts.linkRepo });
5941
- const agents = detectAgents();
5942
- if (agents.length === 0) {
5943
- console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
5991
+ const detected = detectAgents();
5992
+ if (detected.length === 0) {
5993
+ console.error("No supported coding agents detected. Install Claude Code or Cursor first.");
5944
5994
  process.exit(1);
5945
5995
  }
5946
5996
  console.log("Detected agents:");
5947
- for (const a of agents) {
5997
+ for (const a of detected) {
5948
5998
  console.log(` \u2713 ${a.name}${a.version ? ` (${a.version})` : ""}`);
5949
5999
  }
5950
6000
  console.log();
6001
+ const agents = await promptAgentSelection(detected);
6002
+ if (agents.length < detected.length) {
6003
+ console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
6004
+ `);
6005
+ }
5951
6006
  ensureSynkroDir();
5952
6007
  const scripts = writeHookScripts();
5953
6008
  console.log("Wrote hook scripts:");
@@ -7131,7 +7186,7 @@ var args = process.argv.slice(2);
7131
7186
  var cmd = args[0] || "";
7132
7187
  var subArgs = args.slice(1);
7133
7188
  function printVersion() {
7134
- console.log("1.5.1");
7189
+ console.log("1.5.3");
7135
7190
  }
7136
7191
  function printHelp() {
7137
7192
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents