@synkro-sh/cli 1.6.29 → 1.6.31

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
@@ -1906,6 +1906,21 @@ export function appendLocalTelemetry(body: Record<string, any>): void {
1906
1906
  try {
1907
1907
  appendFileSync(TELEMETRY_SPOOL, JSON.stringify(event) + '\\n');
1908
1908
  } catch {}
1909
+ // Realtime: fire-and-forget POST to the local server so events appear
1910
+ // in the dashboard immediately. Spool file remains the durable fallback.
1911
+ try {
1912
+ const port = process.env.SYNKRO_MCP_PORT || '18931';
1913
+ let token = '';
1914
+ try { token = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
1915
+ if (token) {
1916
+ fetch('http://127.0.0.1:' + port + '/api/ingest', {
1917
+ method: 'POST',
1918
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
1919
+ body: JSON.stringify(event),
1920
+ signal: AbortSignal.timeout(3000),
1921
+ }).catch(() => {});
1922
+ }
1923
+ } catch {}
1909
1924
  }
1910
1925
 
1911
1926
  function tryAcquireDrainLock(): boolean {
@@ -2336,12 +2351,10 @@ export async function pushConversationMessage(
2336
2351
  type: role,
2337
2352
  content: text,
2338
2353
  ts: new Date().toISOString(),
2354
+ message_index: seq,
2339
2355
  patch_redacted: opts.patchRedacted ?? true,
2340
2356
  }],
2341
2357
  };
2342
- if (!opts.patchRedacted) {
2343
- (body.messages as Record<string, unknown>[])[0].message_index = seq;
2344
- }
2345
2358
  const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/conversation-sync', {
2346
2359
  method: 'POST',
2347
2360
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
@@ -2478,10 +2491,13 @@ export async function syncConversationTranscript(
2478
2491
  } catch {}
2479
2492
  }
2480
2493
 
2481
- writeFileSync(offsetFile, String(totalLines), 'utf-8');
2482
- if (messages.length === 0) return { ingested: 0, messages: [] };
2494
+ if (messages.length === 0) {
2495
+ writeFileSync(offsetFile, String(totalLines), 'utf-8');
2496
+ return { ingested: 0, messages: [] };
2497
+ }
2483
2498
 
2484
- const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
2499
+ const rawPort = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
2500
+ const mcpPort = (rawPort > 0 && rawPort < 65536) ? rawPort : 18931;
2485
2501
  let mcpToken = '';
2486
2502
  try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
2487
2503
  if (!mcpToken) return { ingested: 0, messages };
@@ -2494,6 +2510,7 @@ export async function syncConversationTranscript(
2494
2510
  signal: AbortSignal.timeout(5000),
2495
2511
  });
2496
2512
  if (resp.ok) {
2513
+ writeFileSync(offsetFile, String(totalLines), 'utf-8');
2497
2514
  const data = await resp.json() as { ingested?: number };
2498
2515
  return { ingested: data.ingested ?? messages.length, messages };
2499
2516
  }
@@ -3027,7 +3044,17 @@ export function hookSessionId(payload: Record<string, unknown>): string {
3027
3044
  }
3028
3045
 
3029
3046
  export function isCursorHookFormat(): boolean {
3030
- return process.env.SYNKRO_HOOK_FORMAT === 'cursor';
3047
+ return process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor');
3048
+ }
3049
+
3050
+ // Cursor reads CC hooks from ~/.claude/settings.json and fires them alongside
3051
+ // its own ~/.cursor/hooks.json entries. When that happens, agentKind is
3052
+ // 'claude_code' but the payload model is non-Claude (e.g. gpt-5.5).
3053
+ // Return true so the CC hook can bail out and let Cursor's hooks handle it.
3054
+ export function isCursorInvokingCcHook(agentKind: string, model: string): boolean {
3055
+ if (agentKind === 'cursor') return false;
3056
+ if (!model || model === 'unknown' || model === '') return false;
3057
+ return !model.startsWith('claude-');
3031
3058
  }
3032
3059
 
3033
3060
  let cursorHookExited = false;
@@ -3127,13 +3154,13 @@ import {
3127
3154
  appendSessionAction, readSessionLog, compressSessionLog, log,
3128
3155
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
3129
3156
  logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
3130
- captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath,
3157
+ captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
3131
3158
  type HookConfig, type Rule,
3132
3159
  } from './_synkro-common.ts';
3133
3160
  import { existsSync, readFileSync } from 'node:fs';
3134
3161
  import { basename, join } from 'node:path';
3135
3162
 
3136
- const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
3163
+ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
3137
3164
 
3138
3165
  async function main() {
3139
3166
  setupCursorHookSignals();
@@ -3220,6 +3247,8 @@ async function main() {
3220
3247
  ? cursorModelFromPayload(payload)
3221
3248
  : (transcript.ccModel || String(payload.model ?? payload.model_id ?? ''));
3222
3249
 
3250
+ if (isCursorInvokingCcHook(agentKind, captureModel)) { outputEmpty(); return; }
3251
+
3223
3252
  // Model detection: prefer transcript (CC), fall back to payload (Cursor)
3224
3253
  if (!transcript.ccModel) {
3225
3254
  transcript.ccModel = captureModel;
@@ -3377,12 +3406,12 @@ import {
3377
3406
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
3378
3407
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
3379
3408
  extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
3380
- logGraderUnavailable, resolveTranscriptPath,
3409
+ logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
3381
3410
  } from './_synkro-common.ts';
3382
3411
  import { basename, extname, resolve, join, dirname } from 'node:path';
3383
3412
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
3384
3413
 
3385
- const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
3414
+ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
3386
3415
 
3387
3416
  function detectModel(payload: Record<string, unknown>): string {
3388
3417
  const raw = String(payload.model ?? payload.model_id ?? '');
@@ -3528,6 +3557,8 @@ async function main() {
3528
3557
  const shellCommand = typeof payload.command === 'string' ? payload.command.trim() : '';
3529
3558
  const ccModel = detectModel(payload);
3530
3559
 
3560
+ if (isCursorInvokingCcHook(agentKind, ccModel)) { outputEmpty(); return; }
3561
+
3531
3562
  const targets: CweScanTarget[] = [];
3532
3563
 
3533
3564
  if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
@@ -3931,6 +3962,7 @@ import {
3931
3962
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
3932
3963
  reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
3933
3964
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
3965
+ isCursorHookFormat,
3934
3966
  } from './_synkro-common.ts';
3935
3967
  import { basename } from 'node:path';
3936
3968
  import { readFileSync } from 'node:fs';
@@ -3961,6 +3993,9 @@ async function main() {
3961
3993
  return;
3962
3994
  }
3963
3995
 
3996
+ const _m = String(payload.model ?? payload.model_id ?? '');
3997
+ if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
3998
+
3964
3999
  const toolInput = payload.tool_input || {};
3965
4000
  const sessionId = hookSessionId(payload);
3966
4001
  const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
@@ -4165,11 +4200,16 @@ async function main() {
4165
4200
  const cveIds = findings.slice(0, 10).map((f: any) =>
4166
4201
  (f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown'
4167
4202
  );
4203
+ let cveCcModel: string | undefined = String(payload.model ?? payload.model_id ?? '') || undefined;
4204
+ if (!cveCcModel && transcriptPath) {
4205
+ try { cveCcModel = extractTranscript(transcriptPath).ccModel || undefined; } catch {}
4206
+ }
4168
4207
  dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
4169
4208
  toolName, gitRepo, sessionId, config.captureDepth, {
4170
4209
  command: 'edit ' + filePath,
4171
4210
  reasoning: top3,
4172
4211
  violatedRules: cveIds,
4212
+ ccModel: cveCcModel,
4173
4213
  });
4174
4214
  dispatchScanResult(jwt, {
4175
4215
  session_id: sessionId, file_path: filePath, scan_type: 'cve',
@@ -4218,6 +4258,9 @@ async function main() {
4218
4258
  if (!input.trim()) { outputEmpty(); return; }
4219
4259
 
4220
4260
  const payload = JSON.parse(input);
4261
+ const _m = String(payload.model ?? payload.model_id ?? '');
4262
+ if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
4263
+
4221
4264
  const toolInput = payload.tool_input || {};
4222
4265
  const command = typeof payload.command === 'string' ? payload.command : (toolInput.command || '');
4223
4266
  if (!command) { outputEmpty(); return; }
@@ -4320,7 +4363,7 @@ import {
4320
4363
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
4321
4364
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
4322
4365
  logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
4323
- hashCommand, resolveTranscriptPath,
4366
+ hashCommand, resolveTranscriptPath, isCursorHookFormat,
4324
4367
  type HookConfig, type Rule,
4325
4368
  } from './_synkro-common.ts';
4326
4369
  import { createHash } from 'node:crypto';
@@ -4371,6 +4414,9 @@ async function main() {
4371
4414
  return;
4372
4415
  }
4373
4416
 
4417
+ const _m = String(payload.model ?? payload.model_id ?? '');
4418
+ if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
4419
+
4374
4420
  const toolInput = payload.tool_input || {};
4375
4421
  const sessionId = hookSessionId(payload);
4376
4422
  const toolUseId = payload.tool_use_id || '';
@@ -4610,11 +4656,11 @@ import {
4610
4656
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
4611
4657
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
4612
4658
  outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
4613
- logGraderUnavailable, filterRules, normalizeMode, resolveTranscriptPath,
4659
+ logGraderUnavailable, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
4614
4660
  type HookConfig, type Rule,
4615
4661
  } from './_synkro-common.ts';
4616
4662
 
4617
- const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
4663
+ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
4618
4664
 
4619
4665
  async function main() {
4620
4666
  setupCursorHookSignals();
@@ -4629,6 +4675,9 @@ async function main() {
4629
4675
  return;
4630
4676
  }
4631
4677
 
4678
+ const _m = String(payload.model ?? payload.model_id ?? '');
4679
+ if (isCursorInvokingCcHook(agentKind, _m)) { outputEmpty(); return; }
4680
+
4632
4681
  const toolInput = payload.tool_input || {};
4633
4682
  const sessionId = hookSessionId(payload);
4634
4683
  const toolUseId = payload.tool_use_id || '';
@@ -4789,13 +4838,13 @@ import {
4789
4838
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4790
4839
  parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
4791
4840
  outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
4792
- filterRules, resolveTranscriptPath,
4841
+ filterRules, resolveTranscriptPath, isCursorInvokingCcHook,
4793
4842
  } from './_synkro-common.ts';
4794
4843
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
4795
4844
  import { join } from 'node:path';
4796
4845
  import { homedir } from 'node:os';
4797
4846
 
4798
- const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
4847
+ const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
4799
4848
 
4800
4849
  function findLatestPlanInDir(plansDir: string): string | null {
4801
4850
  if (!existsSync(plansDir)) return null;
@@ -4847,6 +4896,9 @@ async function main() {
4847
4896
  const toolName = payload.tool_name || '';
4848
4897
  if (!isPlanTool(toolName)) { outputEmpty(); return; }
4849
4898
 
4899
+ const _m = String(payload.model ?? payload.model_id ?? '');
4900
+ if (isCursorInvokingCcHook(agentKind, _m)) { outputEmpty(); return; }
4901
+
4850
4902
  const planFile = findLatestPlan();
4851
4903
  if (!planFile) { outputEmpty(); return; }
4852
4904
  const plan = readFileSync(planFile, 'utf-8');
@@ -5754,6 +5806,21 @@ main();
5754
5806
  });
5755
5807
 
5756
5808
  // cli/auth/stub.ts
5809
+ var stub_exports = {};
5810
+ __export(stub_exports, {
5811
+ authenticate: () => authenticate,
5812
+ clearCredentials: () => clearCredentials,
5813
+ ensureValidToken: () => ensureValidToken,
5814
+ getAccessToken: () => getAccessToken,
5815
+ getCurrentUserId: () => getCurrentUserId,
5816
+ getSecrets: () => getSecrets,
5817
+ getUserInfo: () => getUserInfo,
5818
+ isAuthenticated: () => isAuthenticated,
5819
+ isTokenExpired: () => isTokenExpired,
5820
+ loadCredentials: () => loadCredentials,
5821
+ refreshToken: () => refreshToken,
5822
+ saveCredentials: () => saveCredentials
5823
+ });
5757
5824
  import { createServer } from "http";
5758
5825
  import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
5759
5826
  import { homedir as homedir4, platform } from "os";
@@ -5960,6 +6027,17 @@ function isAuthenticated() {
5960
6027
  return true;
5961
6028
  }
5962
6029
  }
6030
+ function getCurrentUserId() {
6031
+ const creds = loadCredentials();
6032
+ if (!creds) {
6033
+ throw new Error("Not authenticated");
6034
+ }
6035
+ const decoded = jwt.decode(creds.access_token);
6036
+ if (!decoded?.sub) {
6037
+ throw new Error("Invalid token");
6038
+ }
6039
+ return decoded.sub;
6040
+ }
5963
6041
  function getUserInfo() {
5964
6042
  const creds = loadCredentials();
5965
6043
  if (!creds) {
@@ -6044,6 +6122,15 @@ function clearCredentials() {
6044
6122
  unlinkSync2(AUTH_FILE);
6045
6123
  }
6046
6124
  }
6125
+ async function getSecrets(userId, integrationId) {
6126
+ return {
6127
+ AWS_ACCESS_KEY_ID: process.env.USER_AWS_KEY || "",
6128
+ AWS_SECRET_ACCESS_KEY: process.env.USER_AWS_SECRET || "",
6129
+ AWS_REGION: process.env.USER_AWS_REGION || "us-east-1",
6130
+ HF_TOKEN: process.env.USER_HF_TOKEN || "",
6131
+ LANGSMITH_API_KEY: process.env.USER_LANGSMITH_KEY || ""
6132
+ };
6133
+ }
6047
6134
  var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, RAW_API_URL, SYNKRO_API_URL, ERROR_HTML, refreshPromise;
6048
6135
  var init_stub = __esm({
6049
6136
  "cli/auth/stub.ts"() {
@@ -7889,7 +7976,7 @@ function writeConfigEnv(opts) {
7889
7976
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
7890
7977
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
7891
7978
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
7892
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.29")}`
7979
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.31")}`
7893
7980
  ];
7894
7981
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
7895
7982
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -8325,7 +8412,7 @@ async function installCommand(opts = {}) {
8325
8412
  const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
8326
8413
  method: "POST",
8327
8414
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpJwt}` },
8328
- body: JSON.stringify({ capture_type: "local_verdict", event_id: `healthcheck_${Date.now()}`, verdict: "pass", severity: "clean", hook_type: "install", tool_name: "healthcheck" }),
8415
+ body: JSON.stringify({ capture_type: "healthcheck", event_id: `healthcheck_${Date.now()}` }),
8329
8416
  signal: AbortSignal.timeout(5e3)
8330
8417
  });
8331
8418
  if (ingestResp.ok) {
@@ -10641,7 +10728,6 @@ var init_config = __esm({
10641
10728
  import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
10642
10729
  import { resolve as resolve2 } from "path";
10643
10730
  var envCandidates = [
10644
- resolve2(process.cwd(), ".env"),
10645
10731
  resolve2(process.env.HOME ?? "", ".synkro", "config.env")
10646
10732
  ];
10647
10733
  for (const envPath of envCandidates) {
@@ -10661,7 +10747,7 @@ var args = process.argv.slice(2);
10661
10747
  var cmd = args[0] || "";
10662
10748
  var subArgs = args.slice(1);
10663
10749
  function printVersion() {
10664
- console.log("1.6.29");
10750
+ console.log("1.6.31");
10665
10751
  }
10666
10752
  function printHelp2() {
10667
10753
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -10707,6 +10793,23 @@ async function main() {
10707
10793
  disconnectCommand2(subArgs);
10708
10794
  break;
10709
10795
  }
10796
+ case "login": {
10797
+ const { authenticate: authenticate2, isAuthenticated: isAuthenticated2 } = await Promise.resolve().then(() => (init_stub(), stub_exports));
10798
+ if (isAuthenticated2()) {
10799
+ console.log("Already authenticated.");
10800
+ } else {
10801
+ console.log("Opening browser for Synkro auth...");
10802
+ const result = await authenticate2((status) => {
10803
+ if (status.phase === "success") console.log(" \u2713 Authenticated");
10804
+ else if (status.phase === "error") console.error(" \u2717 " + status.message);
10805
+ });
10806
+ if (!result) {
10807
+ console.error("Authentication failed.");
10808
+ process.exit(1);
10809
+ }
10810
+ }
10811
+ break;
10812
+ }
10710
10813
  case "grade": {
10711
10814
  const { gradeCommand: gradeCommand2 } = await Promise.resolve().then(() => (init_grade(), grade_exports));
10712
10815
  await gradeCommand2(subArgs);