@synkro-sh/cli 1.7.93 → 1.7.95

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
@@ -147,7 +147,7 @@ function getIdentity() {
147
147
  if (cached2) return cached2;
148
148
  let cliVersion = "0.0.0";
149
149
  try {
150
- cliVersion = "1.7.93";
150
+ cliVersion = "1.7.95";
151
151
  } catch {
152
152
  }
153
153
  const creds = loadCredentialsIdentity();
@@ -2147,7 +2147,7 @@ var init_codexHookConfig = __esm({
2147
2147
 
2148
2148
  // cli/installer/codexHookTrust.ts
2149
2149
  import { spawn as spawn2 } from "child_process";
2150
- function parseCodexHookTrustOutput(stdout) {
2150
+ function parseCodexHookTrustRecords(stdout) {
2151
2151
  for (const line of stdout.split("\n")) {
2152
2152
  let message;
2153
2153
  try {
@@ -2159,20 +2159,41 @@ function parseCodexHookTrustOutput(stdout) {
2159
2159
  if (!Array.isArray(data)) continue;
2160
2160
  const hooks = data.flatMap((entry) => Array.isArray(entry?.hooks) ? entry.hooks : []).filter((hook) => typeof hook.command === "string" && isSynkroHookCommand(hook.command));
2161
2161
  if (!hooks.length) return null;
2162
- return {
2163
- total: hooks.length,
2164
- trusted: hooks.filter((hook) => hook.trustStatus === "trusted" || hook.trustStatus === "managed").length,
2165
- needsReview: hooks.filter((hook) => hook.trustStatus === "modified" || hook.trustStatus === "untrusted").length,
2166
- disabled: hooks.filter((hook) => hook.enabled === false).length
2167
- };
2162
+ return hooks;
2168
2163
  }
2169
2164
  return null;
2170
2165
  }
2171
- function inspectCodexHookTrust(codexBinary = "codex", cwd = process.cwd()) {
2166
+ function summarizeCodexHookTrust(hooks) {
2167
+ return {
2168
+ total: hooks.length,
2169
+ trusted: hooks.filter((hook) => hook.trustStatus === "trusted" || hook.trustStatus === "managed").length,
2170
+ needsReview: hooks.filter((hook) => hook.trustStatus === "modified" || hook.trustStatus === "untrusted").length,
2171
+ disabled: hooks.filter((hook) => hook.enabled === false).length
2172
+ };
2173
+ }
2174
+ function parseCodexHookTrustOutput(stdout) {
2175
+ const hooks = parseCodexHookTrustRecords(stdout);
2176
+ return hooks ? summarizeCodexHookTrust(hooks) : null;
2177
+ }
2178
+ function buildCodexHookTrustEdits(hooks) {
2179
+ const edits = /* @__PURE__ */ new Map();
2180
+ for (const hook of hooks) {
2181
+ if (typeof hook.command !== "string" || !isSynkroHookCommand(hook.command) || typeof hook.key !== "string" || !hook.key || typeof hook.currentHash !== "string" || !/^sha256:[0-9a-f]{64}$/i.test(hook.currentHash)) continue;
2182
+ const edit = {
2183
+ keyPath: `hooks.state.${JSON.stringify(hook.key)}.trusted_hash`,
2184
+ value: hook.currentHash,
2185
+ mergeStrategy: "upsert"
2186
+ };
2187
+ edits.set(edit.keyPath, edit);
2188
+ }
2189
+ return [...edits.values()];
2190
+ }
2191
+ function queryCodexHookTrust(codexBinary = "codex", cwd = process.cwd(), autoTrust = false) {
2172
2192
  return new Promise((resolve6) => {
2173
2193
  let settled = false;
2174
2194
  let stdout = "";
2175
2195
  let pending = "";
2196
+ let hooks = null;
2176
2197
  let child;
2177
2198
  const finish = (summary) => {
2178
2199
  if (settled) return;
@@ -2217,7 +2238,32 @@ function inspectCodexHookTrust(codexBinary = "codex", cwd = process.cwd()) {
2217
2238
  params: { cwds: [cwd] }
2218
2239
  }) + "\n");
2219
2240
  } else if (message?.id === 2) {
2220
- finish(parseCodexHookTrustOutput(line));
2241
+ hooks = parseCodexHookTrustRecords(line);
2242
+ if (!hooks || !autoTrust) {
2243
+ finish(hooks ? summarizeCodexHookTrust(hooks) : null);
2244
+ continue;
2245
+ }
2246
+ const edits = buildCodexHookTrustEdits(hooks);
2247
+ if (!edits.length) {
2248
+ finish(summarizeCodexHookTrust(hooks));
2249
+ continue;
2250
+ }
2251
+ child.stdin.write(JSON.stringify({
2252
+ id: 3,
2253
+ method: "config/batchWrite",
2254
+ params: { edits, reloadUserConfig: true }
2255
+ }) + "\n");
2256
+ } else if (message?.id === 3) {
2257
+ if (message?.result?.status !== "ok" || !hooks) {
2258
+ finish(hooks ? summarizeCodexHookTrust(hooks) : null);
2259
+ continue;
2260
+ }
2261
+ finish({
2262
+ total: hooks.length,
2263
+ trusted: hooks.length,
2264
+ needsReview: 0,
2265
+ disabled: hooks.filter((hook) => hook.enabled === false).length
2266
+ });
2221
2267
  }
2222
2268
  }
2223
2269
  });
@@ -2234,6 +2280,9 @@ function inspectCodexHookTrust(codexBinary = "codex", cwd = process.cwd()) {
2234
2280
  }
2235
2281
  });
2236
2282
  }
2283
+ function trustCodexHooks(codexBinary = "codex", cwd = process.cwd()) {
2284
+ return queryCodexHookTrust(codexBinary, cwd, true);
2285
+ }
2237
2286
  function codexHookTrustLines(summary) {
2238
2287
  if (!summary) {
2239
2288
  return [
@@ -2254,7 +2303,7 @@ function codexHookTrustLines(summary) {
2254
2303
  ];
2255
2304
  }
2256
2305
  async function reportCodexHookTrust(codexBinary, cwd) {
2257
- const summary = await inspectCodexHookTrust(codexBinary || "codex", cwd);
2306
+ const summary = await trustCodexHooks(codexBinary || "codex", cwd);
2258
2307
  for (const line of codexHookTrustLines(summary)) console.log(line);
2259
2308
  return summary;
2260
2309
  }
@@ -2428,9 +2477,54 @@ function removeCodexManagedBlock(content, path) {
2428
2477
  if (inside) throw new Error(`Incomplete Synkro MCP marker block in ${path}`);
2429
2478
  return { content: out.join("\n").replace(/\n{3,}$/g, "\n\n"), removed };
2430
2479
  }
2431
- function hasUnmanagedCodexEntry(content) {
2432
- const section = /^\s*\[\s*mcp_servers\s*\.\s*(?:"synkro-guardrails"|'synkro-guardrails'|synkro-guardrails)\s*\]\s*(?:#.*)?$/m;
2433
- return section.test(content);
2480
+ function findCodexMcpSection(content) {
2481
+ const section = /^\s*\[\s*mcp_servers\s*\.\s*(?:"synkro-guardrails"|'synkro-guardrails'|synkro-guardrails)\s*\]\s*(?:#.*)?$/gm;
2482
+ const match = section.exec(content);
2483
+ if (!match) return null;
2484
+ const nextSection = /^\s*\[{1,2}[^\r\n]+?\]{1,2}\s*(?:#.*)?$/gm;
2485
+ nextSection.lastIndex = match.index + match[0].length;
2486
+ const next = nextSection.exec(content);
2487
+ return {
2488
+ start: match.index,
2489
+ end: next?.index ?? content.length,
2490
+ body: content.slice(match.index, next?.index ?? content.length)
2491
+ };
2492
+ }
2493
+ function readTomlString(block, key) {
2494
+ const match = block.match(new RegExp(`^\\s*${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")\\s*$`, "m"));
2495
+ if (!match) return null;
2496
+ try {
2497
+ return JSON.parse(match[1]);
2498
+ } catch {
2499
+ return null;
2500
+ }
2501
+ }
2502
+ function isRecognizedCodexEntry(block, expectedUrl) {
2503
+ const command = readTomlString(block, "command");
2504
+ const argsLine = block.match(/^\s*args\s*=\s*(\[[^\r\n]*\])\s*$/m);
2505
+ if (command && argsLine) {
2506
+ try {
2507
+ const args2 = JSON.parse(argsLine[1]);
2508
+ if (/(?:^|[\\/])bun(?:\.exe)?$/i.test(command) && Array.isArray(args2) && args2.length === 2 && args2[0] === "run" && args2[1] === MCP_STDIO_PROXY_PATH) return true;
2509
+ } catch {
2510
+ }
2511
+ }
2512
+ const url = readTomlString(block, "url");
2513
+ return Boolean(expectedUrl && url === expectedUrl);
2514
+ }
2515
+ function stripDetachedCodexMarkers(content) {
2516
+ return content.split("\n").filter((line) => line.trim() !== CODEX_MCP_BEGIN && line.trim() !== CODEX_MCP_END).join("\n");
2517
+ }
2518
+ function removeRecognizedCodexEntry(content, path, expectedUrl) {
2519
+ const range = findCodexMcpSection(content);
2520
+ if (!range) return { content, removed: false };
2521
+ if (!isRecognizedCodexEntry(range.body, expectedUrl)) {
2522
+ throw new Error(
2523
+ `${path} already defines ${CODEX_MCP_SECTION}; remove or rename that unmanaged entry before installing Synkro`
2524
+ );
2525
+ }
2526
+ const next = `${content.slice(0, range.start)}${content.slice(range.end)}`;
2527
+ return { content: next.replace(/\n{3,}$/g, "\n\n"), removed: true };
2434
2528
  }
2435
2529
  function tomlString(value) {
2436
2530
  return JSON.stringify(value);
@@ -2438,16 +2532,17 @@ function tomlString(value) {
2438
2532
  function installCodexMcpConfig(opts) {
2439
2533
  const path = codexConfigPath(opts.configPath);
2440
2534
  const current = readCodexToml(path);
2441
- const withoutManaged = removeCodexManagedBlock(current, path).content;
2442
- if (hasUnmanagedCodexEntry(withoutManaged)) {
2443
- throw new Error(
2444
- `${path} already defines ${CODEX_MCP_SECTION}; remove or rename that unmanaged entry before installing Synkro`
2445
- );
2446
- }
2535
+ const targetUrl = opts.local ? `stdio://${MCP_STDIO_PROXY_PATH}` : `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
2536
+ const withoutManaged = stripDetachedCodexMarkers(removeCodexManagedBlock(current, path).content);
2537
+ const withoutExisting = removeRecognizedCodexEntry(
2538
+ withoutManaged,
2539
+ path,
2540
+ opts.local ? void 0 : targetUrl
2541
+ ).content;
2447
2542
  let url;
2448
2543
  let body;
2449
2544
  if (opts.local) {
2450
- url = `stdio://${MCP_STDIO_PROXY_PATH}`;
2545
+ url = targetUrl;
2451
2546
  body = [
2452
2547
  `[${CODEX_MCP_SECTION}]`,
2453
2548
  `command = ${tomlString(opts.bunBin || resolveBunBin2())}`,
@@ -2456,7 +2551,7 @@ function installCodexMcpConfig(opts) {
2456
2551
  ];
2457
2552
  } else {
2458
2553
  if (!opts.bearerToken) throw new Error("Codex cloud MCP registration requires a bearer token");
2459
- url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
2554
+ url = targetUrl;
2460
2555
  body = [
2461
2556
  `[${CODEX_MCP_SECTION}]`,
2462
2557
  `url = ${tomlString(url)}`,
@@ -2464,7 +2559,7 @@ function installCodexMcpConfig(opts) {
2464
2559
  "enabled = true"
2465
2560
  ];
2466
2561
  }
2467
- const prefix = withoutManaged.trimEnd();
2562
+ const prefix = withoutExisting.trimEnd();
2468
2563
  const managed = [CODEX_MCP_BEGIN, ...body, CODEX_MCP_END].join("\n");
2469
2564
  writeCodexTomlAtomic(path, `${prefix}${prefix ? "\n\n" : ""}${managed}
2470
2565
  `);
@@ -2475,8 +2570,19 @@ function uninstallCodexMcpConfig(configPath) {
2475
2570
  if (!existsSync13(path)) return false;
2476
2571
  const current = readCodexToml(path);
2477
2572
  const next = removeCodexManagedBlock(current, path);
2478
- if (!next.removed) return false;
2479
- writeCodexTomlAtomic(path, next.content.trimEnd() ? `${next.content.trimEnd()}
2573
+ let content = stripDetachedCodexMarkers(next.content);
2574
+ let removed = next.removed;
2575
+ if (!removed) {
2576
+ try {
2577
+ const recognized = removeRecognizedCodexEntry(content, path);
2578
+ content = recognized.content;
2579
+ removed = recognized.removed;
2580
+ } catch {
2581
+ return false;
2582
+ }
2583
+ }
2584
+ if (!removed) return false;
2585
+ writeCodexTomlAtomic(path, content.trimEnd() ? `${content.trimEnd()}
2480
2586
  ` : "");
2481
2587
  return true;
2482
2588
  }
@@ -6506,8 +6612,16 @@ async function dockerInstall(opts = {}) {
6506
6612
  ...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
6507
6613
  // Full verifier prompt/response tracing is explicit opt-in because it contains source code.
6508
6614
  ...process.env.SYNKRO_VERIFY_TRACE === "1" ? ["-e", "SYNKRO_VERIFY_TRACE=1"] : [],
6509
- // Cursor grading model tunable like SYNKRO_MAX_BATCH_SIZE.
6510
- ...process.env.SYNKRO_CURSOR_MODEL ? ["-e", `SYNKRO_CURSOR_MODEL=${process.env.SYNKRO_CURSOR_MODEL}`] : [],
6615
+ // Explicit model overrides are preserved across local install/update for
6616
+ // every provider and isolated lane. The server reports the resolved values
6617
+ // in /healthz so operators can audit exactly what is spending tokens.
6618
+ ...[
6619
+ "SYNKRO_CLAUDE_MODEL",
6620
+ "SYNKRO_CURSOR_MODEL",
6621
+ "SYNKRO_CODEX_MODEL",
6622
+ "SYNKRO_CONDUCTOR_MODEL",
6623
+ "SYNKRO_ROUTE_MODEL"
6624
+ ].flatMap((key) => process.env[key] ? ["-e", `${key}=${process.env[key]}`] : []),
6511
6625
  // Fix-poll kill switch. Default ON in the image; a benchmark/headless run
6512
6626
  // (e.g. sec-code-bench) sets SYNKRO_FIX_POLL=0 so ask-mode violations skip the
6513
6627
  // interactive AskUserQuestion poll and fall through to generate-the-fix. Only
@@ -6765,7 +6879,7 @@ var init_dockerInstall = __esm({
6765
6879
  HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
6766
6880
  CONTAINER_NAME = resolveContainerName();
6767
6881
  defaultImageVersion = () => {
6768
- if (true) return "1.7.93";
6882
+ if (true) return "1.7.95";
6769
6883
  try {
6770
6884
  const pkg = JSON.parse(readFileSync17(new URL("../../package.json", import.meta.url), "utf8"));
6771
6885
  if (pkg.version) return pkg.version;
@@ -7458,6 +7572,94 @@ var init_codexTranscriptMessages = __esm({
7458
7572
  }
7459
7573
  });
7460
7574
 
7575
+ // cli/scanning/claudeTranscriptUsage.ts
7576
+ function tokenCount(value) {
7577
+ const parsed = Number(value);
7578
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
7579
+ }
7580
+ function isoDay(value, fallbackDay) {
7581
+ if (typeof value === "string") {
7582
+ const parsed = new Date(value);
7583
+ if (Number.isFinite(parsed.getTime())) return parsed.toISOString().slice(0, 10);
7584
+ }
7585
+ return fallbackDay;
7586
+ }
7587
+ function parseClaudeTranscriptUsage(transcript, fallbackDay = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), options = {}) {
7588
+ const seen = options.seenStableIds ?? /* @__PURE__ */ new Set();
7589
+ const rollups = /* @__PURE__ */ new Map();
7590
+ const usage = {
7591
+ input_tokens: 0,
7592
+ output_tokens: 0,
7593
+ cache_creation_input_tokens: 0,
7594
+ cache_read_input_tokens: 0
7595
+ };
7596
+ let model = "";
7597
+ let turns = 0;
7598
+ let lineIndex = 0;
7599
+ for (const line of transcript.split("\n")) {
7600
+ lineIndex += 1;
7601
+ const text = line.trim();
7602
+ if (!text) continue;
7603
+ try {
7604
+ const entry = JSON.parse(text);
7605
+ const message = entry?.message;
7606
+ if (message?.role !== "assistant" || !message.usage || typeof message.usage !== "object") {
7607
+ continue;
7608
+ }
7609
+ const stableId = typeof entry.uuid === "string" && entry.uuid ? `uuid:${entry.uuid}` : typeof message.id === "string" && message.id ? `message:${message.id}:${String(entry.timestamp || "")}` : `${options.sourceId || "transcript"}:line:${lineIndex}`;
7610
+ if (seen.has(stableId)) continue;
7611
+ seen.add(stableId);
7612
+ const counts = {
7613
+ input_tokens: tokenCount(message.usage.input_tokens),
7614
+ output_tokens: tokenCount(message.usage.output_tokens),
7615
+ cache_creation_input_tokens: tokenCount(message.usage.cache_creation_input_tokens),
7616
+ cache_read_input_tokens: tokenCount(message.usage.cache_read_input_tokens)
7617
+ };
7618
+ const countTotal = counts.input_tokens + counts.output_tokens + counts.cache_creation_input_tokens + counts.cache_read_input_tokens;
7619
+ if (countTotal === 0) continue;
7620
+ const entryModel = typeof message.model === "string" && message.model ? message.model : "unknown";
7621
+ if (entryModel !== "<synthetic>") model = entryModel;
7622
+ const day = isoDay(entry.timestamp, fallbackDay);
7623
+ const key = `${day}\0${entryModel}`;
7624
+ const row = rollups.get(key) ?? {
7625
+ day,
7626
+ model: entryModel,
7627
+ turns: 0,
7628
+ input_tokens: 0,
7629
+ output_tokens: 0,
7630
+ cache_creation_input_tokens: 0,
7631
+ cache_read_input_tokens: 0
7632
+ };
7633
+ row.turns += 1;
7634
+ row.input_tokens += counts.input_tokens;
7635
+ row.output_tokens += counts.output_tokens;
7636
+ row.cache_creation_input_tokens += counts.cache_creation_input_tokens;
7637
+ row.cache_read_input_tokens += counts.cache_read_input_tokens;
7638
+ rollups.set(key, row);
7639
+ turns += 1;
7640
+ usage.input_tokens += counts.input_tokens;
7641
+ usage.output_tokens += counts.output_tokens;
7642
+ usage.cache_creation_input_tokens += counts.cache_creation_input_tokens;
7643
+ usage.cache_read_input_tokens += counts.cache_read_input_tokens;
7644
+ } catch {
7645
+ }
7646
+ }
7647
+ if (turns === 0) return null;
7648
+ return {
7649
+ usage,
7650
+ model: model || "unknown",
7651
+ rollups: [...rollups.values()].sort(
7652
+ (a, b) => a.day.localeCompare(b.day) || a.model.localeCompare(b.model)
7653
+ ),
7654
+ turns
7655
+ };
7656
+ }
7657
+ var init_claudeTranscriptUsage = __esm({
7658
+ "cli/scanning/claudeTranscriptUsage.ts"() {
7659
+ "use strict";
7660
+ }
7661
+ });
7662
+
7461
7663
  // cli/commands/install.ts
7462
7664
  var install_exports = {};
7463
7665
  __export(install_exports, {
@@ -7475,7 +7677,7 @@ __export(install_exports, {
7475
7677
  });
7476
7678
  import { existsSync as existsSync22, mkdirSync as mkdirSync15, writeFileSync as writeFileSync17, chmodSync as chmodSync5, readFileSync as readFileSync21, readdirSync as readdirSync4, unlinkSync as unlinkSync7, statSync as statSync2 } from "fs";
7477
7679
  import { homedir as homedir21 } from "os";
7478
- import { join as join19, isAbsolute, resolve as resolve4 } from "path";
7680
+ import { basename, join as join19, isAbsolute, resolve as resolve4, sep } from "path";
7479
7681
  import { execSync as execSync4, spawn as spawn5 } from "child_process";
7480
7682
  import { createInterface as createInterface2 } from "readline";
7481
7683
  import { createHash as createHash4 } from "crypto";
@@ -7734,7 +7936,7 @@ function writeConfigEnv(opts) {
7734
7936
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle2(credsPath)}`,
7735
7937
  `SYNKRO_TIER=${shellQuoteSingle2(safeTier)}`,
7736
7938
  `SYNKRO_INFERENCE=${shellQuoteSingle2(safeInference)}`,
7737
- `SYNKRO_VERSION=${shellQuoteSingle2("1.7.93")}`
7939
+ `SYNKRO_VERSION=${shellQuoteSingle2("1.7.95")}`
7738
7940
  ];
7739
7941
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle2(safeSynkroBin)}`);
7740
7942
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle2(safeUserId)}`);
@@ -7912,6 +8114,11 @@ async function provisionCloudContainer(opts) {
7912
8114
  cursor_workers: cursorWorkers,
7913
8115
  codex_workers: codexWorkers,
7914
8116
  conductor_provider: selectedKind,
8117
+ claude_model: process.env.SYNKRO_CLAUDE_MODEL || "",
8118
+ cursor_model: process.env.SYNKRO_CURSOR_MODEL || "",
8119
+ codex_model: process.env.SYNKRO_CODEX_MODEL || "",
8120
+ conductor_model: process.env.SYNKRO_CONDUCTOR_MODEL || "",
8121
+ route_model: process.env.SYNKRO_ROUTE_MODEL || "",
7915
8122
  cursor_api_key: cursorApiKey,
7916
8123
  // never logged; gateway stores it as the org secret
7917
8124
  connected_repo: repo,
@@ -8476,7 +8683,7 @@ async function installCommand(opts = {}) {
8476
8683
  await setTelemetryState({ enabled: true, remoteFlushEnabled: telemetryConsent });
8477
8684
  emit("install", {
8478
8685
  phase: "started",
8479
- cli_version_to: "1.7.93",
8686
+ cli_version_to: "1.7.95",
8480
8687
  agents_detected: agents.map((a) => a.kind),
8481
8688
  with_github: false,
8482
8689
  with_local_cc: false,
@@ -9543,8 +9750,8 @@ function ensureReachabilityGitHook() {
9543
9750
  }
9544
9751
  return "updated";
9545
9752
  }
9546
- const sep = cur.endsWith("\n") ? "" : "\n";
9547
- writeFileSync17(hookPath, cur + sep + "\n" + block + "\n");
9753
+ const sep3 = cur.endsWith("\n") ? "" : "\n";
9754
+ writeFileSync17(hookPath, cur + sep3 + "\n" + block + "\n");
9548
9755
  try {
9549
9756
  chmodSync5(hookPath, 493);
9550
9757
  } catch {
@@ -9571,10 +9778,33 @@ function detectGitRepo2() {
9571
9778
  }
9572
9779
  function getClaudeProjectsFolder() {
9573
9780
  const cwd = process.cwd();
9574
- const sanitized = "-" + cwd.replace(/\//g, "-");
9781
+ const sanitized = cwd.replace(/\//g, "-");
9575
9782
  const projectsDir = join19(homedir21(), ".claude", "projects", sanitized);
9576
9783
  return existsSync22(projectsDir) ? projectsDir : null;
9577
9784
  }
9785
+ function getClaudeTranscriptFileEntries(projectsDir) {
9786
+ let relativeFiles = [];
9787
+ try {
9788
+ relativeFiles = readdirSync4(projectsDir, { recursive: true, encoding: "utf-8" });
9789
+ } catch {
9790
+ return [];
9791
+ }
9792
+ return relativeFiles.filter((file) => file.endsWith(".jsonl")).map((file) => {
9793
+ const parts = file.split(sep);
9794
+ const subagentsIndex = parts.lastIndexOf("subagents");
9795
+ if (subagentsIndex > 0) {
9796
+ return {
9797
+ filePath: join19(projectsDir, file),
9798
+ sessionId: basename(file, ".jsonl"),
9799
+ parentSessionId: parts[subagentsIndex - 1]
9800
+ };
9801
+ }
9802
+ return {
9803
+ filePath: join19(projectsDir, file),
9804
+ sessionId: basename(file, ".jsonl")
9805
+ };
9806
+ });
9807
+ }
9578
9808
  function extractSessionInsights(projectsDir) {
9579
9809
  const insights = [];
9580
9810
  const files = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl"));
@@ -9874,23 +10104,40 @@ function parseTranscriptFile(filePath) {
9874
10104
  async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
9875
10105
  const projectsDir = getClaudeProjectsFolder();
9876
10106
  if (!projectsDir) return { sessions: 0, messages: 0 };
9877
- const files = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl"));
10107
+ const files = getClaudeTranscriptFileEntries(projectsDir);
9878
10108
  if (files.length === 0) return { sessions: 0, messages: 0 };
9879
10109
  console.log(` Found ${files.length} CC session transcripts, importing + embedding...`);
9880
10110
  let totalSessions = 0;
9881
10111
  let totalMessages = 0;
10112
+ const seenStableIds = /* @__PURE__ */ new Set();
9882
10113
  for (let i = 0; i < files.length; i++) {
9883
10114
  const file = files[i];
9884
- const sessionId = file.replace(".jsonl", "");
9885
- const filePath = join19(projectsDir, file);
10115
+ const sessionId = file.sessionId;
10116
+ const filePath = file.filePath;
9886
10117
  try {
10118
+ const transcript = readFileSync21(filePath, "utf-8");
10119
+ const transcriptUsage = parseClaudeTranscriptUsage(
10120
+ transcript,
10121
+ statSync2(filePath).mtime.toISOString().slice(0, 10),
10122
+ { seenStableIds, sourceId: file.parentSessionId ? `${file.parentSessionId}:subagent:${sessionId}` : sessionId }
10123
+ );
9887
10124
  const allMessages = parseTranscriptFile(filePath);
9888
10125
  const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
9889
10126
  if (messages.length === 0) continue;
9890
10127
  const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/conversation-sync`, {
9891
10128
  method: "POST",
9892
10129
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpToken}` },
9893
- body: JSON.stringify({ session_id: sessionId, repo, messages }),
10130
+ body: JSON.stringify({
10131
+ session_id: sessionId,
10132
+ parent_session_id: file.parentSessionId,
10133
+ repo,
10134
+ messages,
10135
+ session_usage: transcriptUsage?.usage,
10136
+ usage_rollups: transcriptUsage?.rollups ?? [],
10137
+ model: transcriptUsage?.model,
10138
+ harness: "claude-code",
10139
+ usage_cumulative: true
10140
+ }),
9894
10141
  signal: AbortSignal.timeout(15e3)
9895
10142
  });
9896
10143
  if (resp.ok) {
@@ -9904,9 +10151,10 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
9904
10151
  process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
9905
10152
  }
9906
10153
  try {
9907
- const content = readFileSync21(join19(projectsDir, file), "utf-8");
10154
+ const content = readFileSync21(filePath, "utf-8");
9908
10155
  const lineCount = content.split("\n").filter(Boolean).length;
9909
- writeFileSync17(join19(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
10156
+ const offsetId = file.parentSessionId ? `${file.parentSessionId}_${sessionId}` : sessionId;
10157
+ writeFileSync17(join19(OFFSETS_DIR, offsetId), String(lineCount), "utf-8");
9910
10158
  } catch {
9911
10159
  }
9912
10160
  }
@@ -9916,23 +10164,39 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
9916
10164
  async function syncTranscriptsBulk(gatewayUrl, token, repo) {
9917
10165
  const projectsDir = getClaudeProjectsFolder();
9918
10166
  if (!projectsDir) return { sessions: 0, messages: 0 };
9919
- const files = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl"));
10167
+ const files = getClaudeTranscriptFileEntries(projectsDir);
9920
10168
  if (files.length === 0) return { sessions: 0, messages: 0 };
9921
10169
  console.log(`Found ${files.length} CC session transcripts, syncing...`);
9922
10170
  const maxMessagesPerSession = 500;
9923
10171
  let totalSessions = 0;
9924
10172
  let totalMessages = 0;
10173
+ const seenStableIds = /* @__PURE__ */ new Set();
9925
10174
  for (let i = 0; i < files.length; i += 5) {
9926
10175
  const batch = files.slice(i, i + 5);
9927
10176
  const sessions = [];
9928
10177
  for (const file of batch) {
9929
- const sessionId = file.replace(".jsonl", "");
9930
- const filePath = join19(projectsDir, file);
10178
+ const sessionId = file.sessionId;
10179
+ const filePath = file.filePath;
9931
10180
  try {
10181
+ const transcript = readFileSync21(filePath, "utf-8");
10182
+ const transcriptUsage = parseClaudeTranscriptUsage(
10183
+ transcript,
10184
+ statSync2(filePath).mtime.toISOString().slice(0, 10),
10185
+ { seenStableIds, sourceId: file.parentSessionId ? `${file.parentSessionId}:subagent:${sessionId}` : sessionId }
10186
+ );
9932
10187
  const allMessages = parseTranscriptFile(filePath);
9933
10188
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
9934
10189
  if (messages.length > 0) {
9935
- sessions.push({ cc_session_id: sessionId, messages });
10190
+ sessions.push({
10191
+ cc_session_id: sessionId,
10192
+ parent_session_id: file.parentSessionId,
10193
+ messages,
10194
+ model: transcriptUsage?.model,
10195
+ session_usage: transcriptUsage?.usage,
10196
+ usage_rollups: transcriptUsage?.rollups ?? [],
10197
+ harness: "claude-code",
10198
+ usage_cumulative: true
10199
+ });
9936
10200
  }
9937
10201
  } catch {
9938
10202
  }
@@ -9955,12 +10219,13 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
9955
10219
  } catch {
9956
10220
  }
9957
10221
  for (const file of batch) {
9958
- const sessionId = file.replace(".jsonl", "");
9959
- const filePath = join19(projectsDir, file);
10222
+ const sessionId = file.sessionId;
10223
+ const filePath = file.filePath;
9960
10224
  try {
9961
10225
  const content = readFileSync21(filePath, "utf-8");
9962
10226
  const lineCount = content.split("\n").filter(Boolean).length;
9963
- writeFileSync17(join19(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
10227
+ const offsetId = file.parentSessionId ? `${file.parentSessionId}_${sessionId}` : sessionId;
10228
+ writeFileSync17(join19(OFFSETS_DIR, offsetId), String(lineCount), "utf-8");
9964
10229
  } catch {
9965
10230
  }
9966
10231
  }
@@ -10043,6 +10308,7 @@ var init_install = __esm({
10043
10308
  init_graderSmoke();
10044
10309
  init_codexTranscriptUsage();
10045
10310
  init_codexTranscriptMessages();
10311
+ init_claudeTranscriptUsage();
10046
10312
  SYNKRO_DIR11 = join19(homedir21(), ".synkro");
10047
10313
  HOOKS_DIR = join19(SYNKRO_DIR11, "hooks");
10048
10314
  CONFIG_PATH4 = join19(SYNKRO_DIR11, "config.env");
@@ -12639,9 +12905,9 @@ var import_exports = {};
12639
12905
  __export(import_exports, {
12640
12906
  importCommand: () => importCommand
12641
12907
  });
12642
- import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync6 } from "fs";
12908
+ import { existsSync as existsSync29, readFileSync as readFileSync27, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
12643
12909
  import { homedir as homedir28 } from "os";
12644
- import { join as join27 } from "path";
12910
+ import { basename as basename2, join as join27, sep as sep2 } from "path";
12645
12911
  import { execSync as execSync6 } from "child_process";
12646
12912
  import { createInterface as createInterface4 } from "readline";
12647
12913
  function readMcpJwt() {
@@ -12669,6 +12935,23 @@ function projectsFolder() {
12669
12935
  const dir = join27(homedir28(), ".claude", "projects", sanitized);
12670
12936
  return existsSync29(dir) ? dir : null;
12671
12937
  }
12938
+ function transcriptFiles(projectsDir) {
12939
+ let relativeFiles = [];
12940
+ try {
12941
+ relativeFiles = readdirSync6(projectsDir, { recursive: true, encoding: "utf-8" });
12942
+ } catch {
12943
+ return [];
12944
+ }
12945
+ return relativeFiles.filter((file) => file.endsWith(".jsonl")).map((file) => {
12946
+ const parts = file.split(sep2);
12947
+ const subagentsIndex = parts.lastIndexOf("subagents");
12948
+ return {
12949
+ filePath: join27(projectsDir, file),
12950
+ sessionId: basename2(file, ".jsonl"),
12951
+ parentSessionId: subagentsIndex > 0 ? parts[subagentsIndex - 1] : void 0
12952
+ };
12953
+ });
12954
+ }
12672
12955
  function repoName() {
12673
12956
  try {
12674
12957
  const url = execSync6("git config --get remote.origin.url", { encoding: "utf-8" }).trim();
@@ -12705,8 +12988,18 @@ function extractToolResultText(content, e) {
12705
12988
  }
12706
12989
  return t;
12707
12990
  }
12708
- function parseSession(filePath, sessionId) {
12709
- const lines = readFileSync27(filePath, "utf-8").split("\n").filter(Boolean);
12991
+ function parseSession(file, seenStableIds) {
12992
+ const { filePath, sessionId, parentSessionId } = file;
12993
+ const transcript = readFileSync27(filePath, "utf-8");
12994
+ const lines = transcript.split("\n").filter(Boolean);
12995
+ const transcriptUsage = parseClaudeTranscriptUsage(
12996
+ transcript,
12997
+ statSync4(filePath).mtime.toISOString().slice(0, 10),
12998
+ {
12999
+ seenStableIds,
13000
+ sourceId: parentSessionId ? `${parentSessionId}:subagent:${sessionId}` : sessionId
13001
+ }
13002
+ );
12710
13003
  const messages = [];
12711
13004
  const actions = [];
12712
13005
  let step = 0;
@@ -12755,7 +13048,17 @@ function parseSession(filePath, sessionId) {
12755
13048
  }
12756
13049
  messages.push(msg);
12757
13050
  }
12758
- return { cc_session_id: sessionId, messages, actions };
13051
+ return {
13052
+ cc_session_id: sessionId,
13053
+ parent_session_id: parentSessionId,
13054
+ messages,
13055
+ actions,
13056
+ model: transcriptUsage?.model,
13057
+ session_usage: transcriptUsage?.usage,
13058
+ usage_rollups: transcriptUsage?.rollups ?? [],
13059
+ harness: "claude-code",
13060
+ usage_cumulative: true
13061
+ };
12759
13062
  }
12760
13063
  function ask2(q) {
12761
13064
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
@@ -12773,7 +13076,7 @@ async function importCommand() {
12773
13076
  console.log("No Claude Code transcripts found for this repo (~/.claude/projects).");
12774
13077
  return;
12775
13078
  }
12776
- const files = readdirSync6(dir).filter((f) => f.endsWith(".jsonl"));
13079
+ const files = transcriptFiles(dir);
12777
13080
  if (!files.length) {
12778
13081
  console.log("No sessions to import.");
12779
13082
  return;
@@ -12786,7 +13089,8 @@ async function importCommand() {
12786
13089
  return;
12787
13090
  }
12788
13091
  }
12789
- const sessions = files.map((f) => parseSession(join27(dir, f), f.replace(".jsonl", ""))).filter((s) => s.messages.length > 0);
13092
+ const seenStableIds = /* @__PURE__ */ new Set();
13093
+ const sessions = files.map((file) => parseSession(file, seenStableIds)).filter((s) => s.messages.length > 0);
12790
13094
  const totalMsgs = sessions.reduce((n, s) => n + s.messages.length, 0);
12791
13095
  let ok = 0, fail = 0;
12792
13096
  if (isCloud) {
@@ -12832,7 +13136,19 @@ async function importCommand() {
12832
13136
  const r = await fetch(`http://127.0.0.1:${port}/api/ingest`, {
12833
13137
  method: "POST",
12834
13138
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpJwt2}` },
12835
- body: JSON.stringify({ capture_type: "transcript_sync", session_id: s.cc_session_id, repo, messages: s.messages, actions: s.actions }),
13139
+ body: JSON.stringify({
13140
+ capture_type: "transcript_sync",
13141
+ session_id: s.cc_session_id,
13142
+ parent_session_id: s.parent_session_id,
13143
+ repo,
13144
+ messages: s.messages,
13145
+ actions: s.actions,
13146
+ model: s.model,
13147
+ session_usage: s.session_usage,
13148
+ usage_rollups: s.usage_rollups,
13149
+ harness: s.harness,
13150
+ usage_cumulative: true
13151
+ }),
12836
13152
  // A full session can carry thousands of turns — 15s timed out mid-import.
12837
13153
  signal: AbortSignal.timeout(12e4)
12838
13154
  });
@@ -12861,6 +13177,7 @@ var init_import = __esm({
12861
13177
  "cli/commands/import.ts"() {
12862
13178
  "use strict";
12863
13179
  init_stub();
13180
+ init_claudeTranscriptUsage();
12864
13181
  CONFIG_PATH7 = join27(homedir28(), ".synkro", "config.env");
12865
13182
  }
12866
13183
  });
@@ -14233,7 +14550,7 @@ var subArgs = args.slice(1);
14233
14550
  var isDetachedChild = process.env.SYNKRO_TELEMETRY_DETACHED === "1";
14234
14551
  var FLUSH_SKIP = /* @__PURE__ */ new Set(["grade", "version", "--version", "-v", "help", "--help", "-h", ""]);
14235
14552
  function printVersion() {
14236
- console.log("1.7.93");
14553
+ console.log("1.7.95");
14237
14554
  }
14238
14555
  function printHelp2() {
14239
14556
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents