@synkro-sh/cli 1.6.78 → 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 +117 -55
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
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
|
|
|
@@ -3433,7 +3462,7 @@ export function emitUsageTick(params: {
|
|
|
3433
3462
|
if (isCursorHookFormat() && model && !model.startsWith('cursor/') && model !== 'cursor') {
|
|
3434
3463
|
model = 'cursor/' + model;
|
|
3435
3464
|
}
|
|
3436
|
-
|
|
3465
|
+
const body = {
|
|
3437
3466
|
capture_type: 'usage_tick',
|
|
3438
3467
|
event_id: mintEventId('usage'),
|
|
3439
3468
|
hook_type: hookType,
|
|
@@ -3449,7 +3478,14 @@ export function emitUsageTick(params: {
|
|
|
3449
3478
|
cache_read_input_tokens: usage.totals.cr,
|
|
3450
3479
|
},
|
|
3451
3480
|
...(gitRepo ? { repo: gitRepo } : {}),
|
|
3452
|
-
}
|
|
3481
|
+
};
|
|
3482
|
+
appendLocalTelemetry(body); // local spool \u2192 usage_ticks (no-op in cloud mode)
|
|
3483
|
+
// Cloud: ship the tick to /hook/capture \u2192 usage_ticks too. appendLocalTelemetry
|
|
3484
|
+
// is gated to local mode, so without this the cloud usage_ticks table stayed
|
|
3485
|
+
// EMPTY \u2014 no token/cost data for the Agents page. (/hook/capture is on the
|
|
3486
|
+
// long-lived MCP-token allowlist; loadJwt returns that token in cloud.)
|
|
3487
|
+
const jwt = loadJwt();
|
|
3488
|
+
if (jwt) shipCloud(jwt, '/api/v1/hook/capture', body);
|
|
3453
3489
|
}
|
|
3454
3490
|
|
|
3455
3491
|
export function cursorModelFromPayload(payload: Record<string, unknown>): string {
|
|
@@ -3930,13 +3966,13 @@ async function main() {
|
|
|
3930
3966
|
ruleFilterText(graderContent, transcript.userIntent || lastPrompt),
|
|
3931
3967
|
config.rules,
|
|
3932
3968
|
);
|
|
3933
|
-
const
|
|
3969
|
+
const buildGraderPrompt = (contentWindow: string) => [
|
|
3934
3970
|
'Working directory: ' + (cwd || '.'),
|
|
3935
3971
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
3936
3972
|
sessionLog,
|
|
3937
3973
|
'File: ' + filePath,
|
|
3938
|
-
'Proposed content
|
|
3939
|
-
|
|
3974
|
+
'Proposed content:',
|
|
3975
|
+
contentWindow,
|
|
3940
3976
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3941
3977
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3942
3978
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
@@ -3944,6 +3980,7 @@ async function main() {
|
|
|
3944
3980
|
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. An initial user instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
3945
3981
|
'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
|
|
3946
3982
|
].join('\\n');
|
|
3983
|
+
const graderPrompt = buildGraderPrompt(proposedShort);
|
|
3947
3984
|
|
|
3948
3985
|
// \u2500\u2500\u2500 Combined org-rules + CWE in ONE inference (SYNKRO_COMBINED_EDIT_GRADE) \u2500\u2500\u2500
|
|
3949
3986
|
// Self-contained early-return branch \u2014 the default two-grade path below is
|
|
@@ -3962,13 +3999,27 @@ async function main() {
|
|
|
3962
3999
|
if (cr.ok) cweRules = ((await cr.json() as any) || {}).rules || [];
|
|
3963
4000
|
} catch { /* CWE rules optional \u2014 rule grading still runs in the combined pass */ }
|
|
3964
4001
|
|
|
3965
|
-
const
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4002
|
+
const cweSuffix = '\\n\\nCWE rules to ALSO check the proposed content against (emit cwe-### violations with a <code_snippet>): ' + JSON.stringify(cweRules);
|
|
4003
|
+
const buildCombined = (win: string) => buildGraderPrompt(win) + cweSuffix;
|
|
4004
|
+
|
|
4005
|
+
// Large-file strategy \u2014 mirrors local cweScanner.ts: rather than one giant
|
|
4006
|
+
// single inference (which blows the 45s cloud budget AND truncated to the
|
|
4007
|
+
// first 4000 chars, missing changes deeper in the file), split content over
|
|
4008
|
+
// the threshold into TWO overlapping halves and grade them IN PARALLEL.
|
|
4009
|
+
// Each half is ~half the size \u2192 faster \u2192 fits the timeout; the whole file
|
|
4010
|
+
// is covered; findings are merged.
|
|
4011
|
+
const SPLIT_THRESHOLD = 4000, OVERLAP = 500;
|
|
4012
|
+
let cResponses: string[];
|
|
3970
4013
|
try {
|
|
3971
|
-
|
|
4014
|
+
if (proposed.length > SPLIT_THRESHOLD) {
|
|
4015
|
+
const mid = Math.floor(proposed.length / 2);
|
|
4016
|
+
cResponses = await Promise.all([
|
|
4017
|
+
localGrade('edit-cwe', buildCombined(proposed.slice(0, mid + OVERLAP)), undefined, graderPool),
|
|
4018
|
+
localGrade('edit-cwe', buildCombined(proposed.slice(mid - OVERLAP)), undefined, graderPool),
|
|
4019
|
+
]);
|
|
4020
|
+
} else {
|
|
4021
|
+
cResponses = [await localGrade('edit-cwe', buildCombined(proposed), undefined, graderPool)];
|
|
4022
|
+
}
|
|
3972
4023
|
} catch (err) {
|
|
3973
4024
|
const errMsg = (err as Error).message || String(err);
|
|
3974
4025
|
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
@@ -3976,7 +4027,18 @@ async function main() {
|
|
|
3976
4027
|
return;
|
|
3977
4028
|
}
|
|
3978
4029
|
|
|
3979
|
-
|
|
4030
|
+
// Merge across halves: a rule violation in EITHER half blocks (first one
|
|
4031
|
+
// wins, keeping its reason/severity); CWE findings are unioned by id.
|
|
4032
|
+
let ruleVerdict: any = null;
|
|
4033
|
+
const cweMap = new Map<string, any>();
|
|
4034
|
+
for (const r of cResponses) {
|
|
4035
|
+
const parsed = parseCombinedVerdict(r);
|
|
4036
|
+
if (!ruleVerdict) ruleVerdict = parsed.ruleVerdict;
|
|
4037
|
+
else if (!parsed.ruleVerdict.ok && ruleVerdict.ok) ruleVerdict = parsed.ruleVerdict;
|
|
4038
|
+
for (const f of parsed.cweFindings) if (f.id && !cweMap.has(f.id)) cweMap.set(f.id, f);
|
|
4039
|
+
}
|
|
4040
|
+
if (!ruleVerdict) ruleVerdict = { ok: true };
|
|
4041
|
+
const cweFindings = [...cweMap.values()];
|
|
3980
4042
|
const editContent = 'file=' + filePath + ' content=' + proposed.slice(0, 2000);
|
|
3981
4043
|
const violatedRules = ruleVerdict.ruleId ? [ruleVerdict.ruleId] : [];
|
|
3982
4044
|
const cweBlock = cweFindings.slice(0, 5)
|
|
@@ -10676,7 +10738,7 @@ function writeConfigEnv(opts) {
|
|
|
10676
10738
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10677
10739
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10678
10740
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10679
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10741
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.80")}`
|
|
10680
10742
|
];
|
|
10681
10743
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10682
10744
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -14351,7 +14413,7 @@ var args = process.argv.slice(2);
|
|
|
14351
14413
|
var cmd = args[0] || "";
|
|
14352
14414
|
var subArgs = args.slice(1);
|
|
14353
14415
|
function printVersion() {
|
|
14354
|
-
console.log("1.6.
|
|
14416
|
+
console.log("1.6.80");
|
|
14355
14417
|
}
|
|
14356
14418
|
function printHelp2() {
|
|
14357
14419
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|