@synkro-sh/cli 1.6.79 → 1.6.80

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
@@ -913,7 +913,7 @@ var init_hookScriptsTs = __esm({
913
913
  "use strict";
914
914
  SYNKRO_COMMON_TS = `
915
915
  // Shared Synkro hook utilities \u2014 imported by all hook scripts.
916
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync, readdirSync, statSync, createReadStream } from 'node:fs';
916
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, readSync, unlinkSync, readdirSync, statSync, createReadStream } from 'node:fs';
917
917
  import { createInterface } from 'node:readline';
918
918
  import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
919
919
  import { homedir } from 'node:os';
@@ -3374,47 +3374,76 @@ export function aggregateUsage(
3374
3374
  ): { model: string; totals: Record<string, number> } {
3375
3375
  const result = { model: opts?.modelFallback || '', totals: { in: 0, out: 0, cw: 0, cr: 0 } };
3376
3376
  if (!transcriptPath || !existsSync(transcriptPath)) return result;
3377
- try {
3378
- const raw = readFileSync(transcriptPath, 'utf-8');
3379
- const entries: Record<string, unknown>[] = [];
3380
- for (const line of raw.split('\\n')) {
3381
- if (!line.trim()) continue;
3382
- try { entries.push(JSON.parse(line) as Record<string, unknown>); } catch {}
3383
- }
3384
-
3385
- let sawExplicit = false;
3386
- for (const entry of entries) {
3387
- const tokenCount = (entry.tokenCount ?? entry.token_count) as Record<string, unknown> | undefined;
3388
- const msg = entry.message as Record<string, unknown> | undefined;
3389
- if (addUsageBlock({ model: '', totals: { in: 0, out: 0, cw: 0, cr: 0 } }, tokenCount)) sawExplicit = true;
3390
- if (addUsageBlock({ model: '', totals: { in: 0, out: 0, cw: 0, cr: 0 } }, msg?.usage as Record<string, unknown>)) sawExplicit = true;
3391
- if (addUsageBlock({ model: '', totals: { in: 0, out: 0, cw: 0, cr: 0 } }, entry.usage as Record<string, unknown>)) sawExplicit = true;
3392
- }
3393
-
3394
- for (const entry of entries) {
3395
- const role = String(entry.role || entry.type || '');
3396
- const modelInfo = entry.modelInfo as Record<string, unknown> | undefined;
3397
- const model = (typeof modelInfo?.modelName === 'string' && modelInfo.modelName)
3398
- || (typeof modelInfo?.model === 'string' && modelInfo.model)
3399
- || (typeof entry.model === 'string' && entry.model)
3400
- || msgModel(entry);
3401
- if (model) result.model = model;
3402
-
3403
- const tokenCount = (entry.tokenCount ?? entry.token_count) as Record<string, unknown> | undefined;
3404
- const msg = entry.message as Record<string, unknown> | undefined;
3405
- const hadLine = addUsageBlock(result, tokenCount)
3406
- || addUsageBlock(result, msg?.usage as Record<string, unknown>)
3407
- || addUsageBlock(result, entry.usage as Record<string, unknown>);
3408
-
3409
- if (hadLine || sawExplicit) continue;
3410
-
3411
- const text = usageTextFromTranscriptEntry(entry);
3412
- if (!text) continue;
3413
- const est = Math.ceil(text.length / 4);
3414
- if (role === 'user' || entry.type === 'user') result.totals.in += est;
3415
- else if (role === 'assistant' || entry.type === 'assistant') result.totals.out += est;
3416
- }
3377
+ // CWE-22: only ever read the agent's own transcript, which lives under HOME
3378
+ // (~/.claude, ~/.cursor). Never follow an arbitrary external path.
3379
+ if (!isPathUnder(transcriptPath, HOME)) return result;
3380
+
3381
+ // INCREMENTAL DELTA: returns ONLY the usage in transcript bytes appended since
3382
+ // the last call (a byte offset is tracked per transcript in a sidecar). All three
3383
+ // usage hooks (Claude UserPromptSubmit + Stop, Cursor transcript-sync) share that
3384
+ // offset, so their deltas never overlap; the DB then INCREMENTS by each delta, so
3385
+ // usage climbs per message in both harnesses. We never re-read the whole
3386
+ // (100s-of-MB) transcript \u2014 the old full read OOM'd bun and dropped the tick.
3387
+ const key = (transcriptPath.split('/').pop() || 'session').replace(/[^A-Za-z0-9._-]/g, '_');
3388
+ const stateFile = join(SESSIONS_DIR, key + '.usage.json');
3389
+ let offset = 0, sawExplicit = false, model = '';
3390
+ try {
3391
+ const s = JSON.parse(readFileSync(stateFile, 'utf-8'));
3392
+ if (s && typeof s.offset === 'number') { offset = s.offset; sawExplicit = !!s.sawExplicit; model = s.model || ''; }
3417
3393
  } catch {}
3394
+
3395
+ let fd = -1;
3396
+ try {
3397
+ const size = statSync(transcriptPath).size;
3398
+ if (size < offset) { offset = 0; sawExplicit = false; } // rotated/truncated
3399
+ // Never read more than 16MB in one shot; first sight of a giant existing
3400
+ // transcript jumps to the tail rather than OOM on the whole history.
3401
+ const MAX_READ = 16 * 1024 * 1024;
3402
+ let start = offset, capped = false;
3403
+ if (size - start > MAX_READ) { start = size - MAX_READ; capped = true; }
3404
+ const len = size - start;
3405
+ if (len > 0) {
3406
+ fd = openSync(transcriptPath, 'r');
3407
+ const buf = Buffer.alloc(len);
3408
+ readSync(fd, buf, 0, len, start);
3409
+ const lines = buf.toString('utf-8').split('\\n');
3410
+ if (capped && lines.length) lines.shift(); // drop the partial first line when we jumped mid-file
3411
+ for (const line of lines) {
3412
+ if (!line.trim()) continue;
3413
+ let entry: Record<string, unknown>;
3414
+ try { entry = JSON.parse(line) as Record<string, unknown>; } catch { continue; }
3415
+ const msg = entry.message as Record<string, unknown> | undefined;
3416
+ const modelInfo = entry.modelInfo as Record<string, unknown> | undefined;
3417
+ const m = (typeof modelInfo?.modelName === 'string' && modelInfo.modelName)
3418
+ || (typeof modelInfo?.model === 'string' && modelInfo.model)
3419
+ || (typeof entry.model === 'string' && entry.model)
3420
+ || msgModel(entry);
3421
+ if (m) { model = m; result.model = m; }
3422
+
3423
+ const tokenCount = (entry.tokenCount ?? entry.token_count) as Record<string, unknown> | undefined;
3424
+ // result.totals is the DELTA for THIS call only (it never carries prior totals).
3425
+ const had = addUsageBlock(result, tokenCount)
3426
+ || addUsageBlock(result, msg?.usage as Record<string, unknown>)
3427
+ || addUsageBlock(result, entry.usage as Record<string, unknown>);
3428
+ if (had) { sawExplicit = true; continue; }
3429
+ if (sawExplicit) continue;
3430
+
3431
+ // Fallback (Cursor / transcripts without usage blocks): estimate from text.
3432
+ const text = usageTextFromTranscriptEntry(entry);
3433
+ if (!text) continue;
3434
+ const est = Math.ceil(text.length / 4);
3435
+ const role = String(entry.role || entry.type || '');
3436
+ if (role === 'user') result.totals.in += est;
3437
+ else if (role === 'assistant') result.totals.out += est;
3438
+ }
3439
+ }
3440
+ // Advance the offset (delta consumed). Ship is best-effort like the other
3441
+ // hooks \u2014 a rare dropped tick loses only that one delta, not the running total.
3442
+ try { mkdirSync(SESSIONS_DIR, { recursive: true }); writeFileSync(stateFile, JSON.stringify({ offset: size, model, sawExplicit })); } catch {}
3443
+ } catch {} finally {
3444
+ if (fd >= 0) { try { closeSync(fd); } catch {} }
3445
+ }
3446
+ if (!result.model) result.model = model || opts?.modelFallback || '';
3418
3447
  return result;
3419
3448
  }
3420
3449
 
@@ -10709,7 +10738,7 @@ function writeConfigEnv(opts) {
10709
10738
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10710
10739
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10711
10740
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10712
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.79")}`
10741
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.80")}`
10713
10742
  ];
10714
10743
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10715
10744
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -14384,7 +14413,7 @@ var args = process.argv.slice(2);
14384
14413
  var cmd = args[0] || "";
14385
14414
  var subArgs = args.slice(1);
14386
14415
  function printVersion() {
14387
- console.log("1.6.79");
14416
+ console.log("1.6.80");
14388
14417
  }
14389
14418
  function printHelp2() {
14390
14419
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents