@synkro-sh/cli 1.4.58 → 1.4.60
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 +73 -30
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -834,10 +834,11 @@ var init_hookScriptsTs = __esm({
|
|
|
834
834
|
"use strict";
|
|
835
835
|
SYNKRO_COMMON_TS = `
|
|
836
836
|
// Shared Synkro hook utilities \u2014 imported by all hook scripts.
|
|
837
|
-
import { readFileSync, writeFileSync,
|
|
838
|
-
import { join, dirname, basename, extname } from 'node:path';
|
|
837
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
838
|
+
import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
|
|
839
839
|
import { homedir } from 'node:os';
|
|
840
840
|
import { execSync, spawn } from 'node:child_process';
|
|
841
|
+
import { constants as FS_CONSTANTS } from 'node:fs';
|
|
841
842
|
|
|
842
843
|
// \u2500\u2500\u2500 Config \u2500\u2500\u2500
|
|
843
844
|
|
|
@@ -868,6 +869,15 @@ export const GATEWAY_URL = process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro
|
|
|
868
869
|
export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
|
|
869
870
|
const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
|
|
870
871
|
|
|
872
|
+
// \u2500\u2500\u2500 Path Validation \u2500\u2500\u2500
|
|
873
|
+
|
|
874
|
+
export function isPathUnder(filePath: string, cwd: string): boolean {
|
|
875
|
+
if (!filePath || !cwd) return false;
|
|
876
|
+
const resolved = resolvePath(filePath);
|
|
877
|
+
const base = resolvePath(cwd);
|
|
878
|
+
return resolved.startsWith(base + '/') || resolved === base;
|
|
879
|
+
}
|
|
880
|
+
|
|
871
881
|
// \u2500\u2500\u2500 Logging \u2500\u2500\u2500
|
|
872
882
|
|
|
873
883
|
export function log(msg: string): void {
|
|
@@ -936,24 +946,22 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
|
936
946
|
const now = Math.floor(Date.now() / 1000);
|
|
937
947
|
if (exp - now >= 60) return jwt;
|
|
938
948
|
|
|
939
|
-
const
|
|
940
|
-
let
|
|
949
|
+
const lockfile = CREDS_PATH + '.lock';
|
|
950
|
+
let fd = -1;
|
|
941
951
|
try {
|
|
942
|
-
|
|
943
|
-
acquired = true;
|
|
952
|
+
fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
|
|
944
953
|
} catch {
|
|
945
|
-
// Another process
|
|
946
|
-
for (let i = 0; i <
|
|
947
|
-
await new Promise(r => setTimeout(r,
|
|
948
|
-
if (!existsSync(
|
|
954
|
+
// Another process holds the lock \u2014 wait for it to finish and re-read
|
|
955
|
+
for (let i = 0; i < 10; i++) {
|
|
956
|
+
await new Promise(r => setTimeout(r, 300));
|
|
957
|
+
if (!existsSync(lockfile)) break;
|
|
949
958
|
}
|
|
950
|
-
// Re-read creds
|
|
951
959
|
const fresh = loadJwt();
|
|
952
960
|
return fresh || jwt;
|
|
953
961
|
}
|
|
954
962
|
|
|
955
963
|
try {
|
|
956
|
-
// Re-check \u2014 another hook may have just refreshed
|
|
964
|
+
// Re-check \u2014 another hook may have just refreshed while we waited
|
|
957
965
|
const freshJwt = loadJwt();
|
|
958
966
|
if (freshJwt) {
|
|
959
967
|
const freshExp = decodeJwtExp(freshJwt);
|
|
@@ -961,9 +969,8 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
|
961
969
|
}
|
|
962
970
|
return await refreshJwt(jwt);
|
|
963
971
|
} finally {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
972
|
+
try { closeSync(fd); } catch {}
|
|
973
|
+
try { unlinkSync(lockfile); } catch {}
|
|
967
974
|
}
|
|
968
975
|
}
|
|
969
976
|
|
|
@@ -1014,6 +1021,7 @@ export interface HookConfig {
|
|
|
1014
1021
|
silent: boolean;
|
|
1015
1022
|
policyName: string;
|
|
1016
1023
|
rules: Rule[];
|
|
1024
|
+
scanExemptions: Array<{ path: string; cwe_id: string }>;
|
|
1017
1025
|
}
|
|
1018
1026
|
|
|
1019
1027
|
export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
|
|
@@ -1023,6 +1031,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1023
1031
|
silent: false,
|
|
1024
1032
|
policyName: '',
|
|
1025
1033
|
rules: [],
|
|
1034
|
+
scanExemptions: [],
|
|
1026
1035
|
};
|
|
1027
1036
|
try {
|
|
1028
1037
|
const url = GATEWAY_URL + '/api/v1/hook/config' + (query ? '?' + query : '');
|
|
@@ -1035,6 +1044,11 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1035
1044
|
config.tier = data.tier || 'standard';
|
|
1036
1045
|
config.silent = data.silent_mode === true || data.silent_mode === 'true';
|
|
1037
1046
|
config.policyName = data.active_policy_name || '';
|
|
1047
|
+
if (Array.isArray(data.scan_exemptions)) {
|
|
1048
|
+
config.scanExemptions = data.scan_exemptions
|
|
1049
|
+
.filter((e: any) => e && typeof e.path === 'string')
|
|
1050
|
+
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
1051
|
+
}
|
|
1038
1052
|
if (Array.isArray(data.rules)) {
|
|
1039
1053
|
config.rules = data.rules
|
|
1040
1054
|
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
@@ -1250,6 +1264,8 @@ export function dispatchCapture(
|
|
|
1250
1264
|
if (opts.recentUserMessages) body.recent_user_messages = opts.recentUserMessages;
|
|
1251
1265
|
}
|
|
1252
1266
|
|
|
1267
|
+
appendLocalTelemetry(body);
|
|
1268
|
+
|
|
1253
1269
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
1254
1270
|
method: 'POST',
|
|
1255
1271
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -1258,6 +1274,13 @@ export function dispatchCapture(
|
|
|
1258
1274
|
}).catch(() => {});
|
|
1259
1275
|
}
|
|
1260
1276
|
|
|
1277
|
+
export function appendLocalTelemetry(body: Record<string, any>): void {
|
|
1278
|
+
try {
|
|
1279
|
+
const telPath = join(HOME, '.synkro', 'telemetry.jsonl');
|
|
1280
|
+
appendFileSync(telPath, JSON.stringify({ ...body, _ts: new Date().toISOString() }) + '\\n', 'utf-8');
|
|
1281
|
+
} catch {}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1261
1284
|
// \u2500\u2500\u2500 Rule Mode Lookup \u2500\u2500\u2500
|
|
1262
1285
|
|
|
1263
1286
|
export function ruleMode(ruleId: string, rules: Rule[]): 'blocking' | 'audit' {
|
|
@@ -1269,14 +1292,15 @@ export function ruleMode(ruleId: string, rules: Rule[]): 'blocking' | 'audit' {
|
|
|
1269
1292
|
|
|
1270
1293
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
1271
1294
|
|
|
1272
|
-
export function reconstructContent(toolName: string, toolInput: any, filePath: string): string {
|
|
1295
|
+
export function reconstructContent(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
|
|
1296
|
+
const canRead = filePath && (!cwd || isPathUnder(filePath, cwd));
|
|
1273
1297
|
switch (toolName) {
|
|
1274
1298
|
case 'Write':
|
|
1275
1299
|
return toolInput.content || '';
|
|
1276
1300
|
case 'Edit': {
|
|
1277
1301
|
let content = '';
|
|
1278
1302
|
try {
|
|
1279
|
-
if (
|
|
1303
|
+
if (canRead && existsSync(filePath)) {
|
|
1280
1304
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1281
1305
|
}
|
|
1282
1306
|
} catch {}
|
|
@@ -1290,7 +1314,7 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
|
|
|
1290
1314
|
case 'MultiEdit': {
|
|
1291
1315
|
let content = '';
|
|
1292
1316
|
try {
|
|
1293
|
-
if (
|
|
1317
|
+
if (canRead && existsSync(filePath)) {
|
|
1294
1318
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1295
1319
|
}
|
|
1296
1320
|
} catch {}
|
|
@@ -1575,7 +1599,7 @@ export function outputEmpty(): void {
|
|
|
1575
1599
|
EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
1576
1600
|
import {
|
|
1577
1601
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
1578
|
-
parseVerdict, dispatchCapture, ruleMode, reconstructContent, postWithRetry,
|
|
1602
|
+
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
1579
1603
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
|
|
1580
1604
|
outputJson, outputEmpty, GATEWAY_URL,
|
|
1581
1605
|
type HookConfig, type Rule,
|
|
@@ -1617,7 +1641,7 @@ async function main() {
|
|
|
1617
1641
|
jwt = await ensureFreshJwt(jwt);
|
|
1618
1642
|
|
|
1619
1643
|
// Reconstruct proposed content
|
|
1620
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1644
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1621
1645
|
if (!proposed) { outputEmpty(); return; }
|
|
1622
1646
|
|
|
1623
1647
|
// Build diff field
|
|
@@ -1631,7 +1655,7 @@ async function main() {
|
|
|
1631
1655
|
|
|
1632
1656
|
// Read file before edit for cloud payload
|
|
1633
1657
|
let fileBefore = '';
|
|
1634
|
-
if (toolName !== 'Write' && filePath && existsSync(filePath)) {
|
|
1658
|
+
if (toolName !== 'Write' && filePath && isPathUnder(filePath, cwd || '.') && existsSync(filePath)) {
|
|
1635
1659
|
try { fileBefore = readFileSync(filePath, 'utf-8').slice(0, 65536); } catch {}
|
|
1636
1660
|
}
|
|
1637
1661
|
|
|
@@ -1821,12 +1845,19 @@ async function main() {
|
|
|
1821
1845
|
jwt = await ensureFreshJwt(jwt);
|
|
1822
1846
|
|
|
1823
1847
|
// Reconstruct proposed content
|
|
1824
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1848
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1825
1849
|
if (!proposed) { outputEmpty(); return; }
|
|
1826
1850
|
|
|
1827
1851
|
const config = await loadConfig(jwt);
|
|
1828
1852
|
const rt = await cweRoute(config);
|
|
1829
1853
|
|
|
1854
|
+
// Build set of exempted CWE IDs for this file path
|
|
1855
|
+
const exemptedCwes = new Set<string>();
|
|
1856
|
+
for (const ex of config.scanExemptions) {
|
|
1857
|
+
if (ex.cwe_id && filePath.includes(ex.path)) {
|
|
1858
|
+
exemptedCwes.add(ex.cwe_id.toUpperCase());
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1830
1861
|
if (config.silent) {
|
|
1831
1862
|
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] ' + fileShort + ' \\u2192 skipped (silent mode)' });
|
|
1832
1863
|
return;
|
|
@@ -1872,15 +1903,23 @@ async function main() {
|
|
|
1872
1903
|
const verdict = parseVerdict(gradeResp);
|
|
1873
1904
|
|
|
1874
1905
|
if (!verdict.ok) {
|
|
1875
|
-
// Extract all CWE rule_ids from the raw response
|
|
1876
1906
|
const ruleIdMatches = gradeResp.match(/<rule_id>([^<]+)<\\/rule_id>/g) || [];
|
|
1877
1907
|
const cweIds: string[] = [];
|
|
1878
1908
|
for (const match of ruleIdMatches.slice(0, 5)) {
|
|
1879
1909
|
const id = match.replace(/<\\/?rule_id>/g, '').trim().replace(/^cwe-/, 'CWE-');
|
|
1880
1910
|
if (id && !cweIds.includes(id)) cweIds.push(id);
|
|
1881
1911
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1912
|
+
|
|
1913
|
+
// Filter out exempted CWEs for this file
|
|
1914
|
+
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
1915
|
+
|
|
1916
|
+
if (activeCweIds.length === 0) {
|
|
1917
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean (exempted: ' + cweIds.join(', ') + ')' });
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
1922
|
+
const count = activeCweIds.length;
|
|
1884
1923
|
const label = count === 1 ? 'match' : 'matches';
|
|
1885
1924
|
const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
1886
1925
|
const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
|
|
@@ -1966,7 +2005,7 @@ async function main() {
|
|
|
1966
2005
|
const cveTag = '[synkro:' + rt + ':cveScan]';
|
|
1967
2006
|
|
|
1968
2007
|
// Reconstruct proposed content
|
|
1969
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
2008
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1970
2009
|
if (!proposed) {
|
|
1971
2010
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \\u2192 skip (no content)' });
|
|
1972
2011
|
return;
|
|
@@ -2401,7 +2440,7 @@ main();
|
|
|
2401
2440
|
STOP_SUMMARY_TS = `#!/usr/bin/env bun
|
|
2402
2441
|
import {
|
|
2403
2442
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
|
|
2404
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2443
|
+
outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2405
2444
|
} from './_synkro-common.ts';
|
|
2406
2445
|
|
|
2407
2446
|
async function main() {
|
|
@@ -2440,6 +2479,7 @@ async function main() {
|
|
|
2440
2479
|
...(gitRepo ? { repo: gitRepo } : {}),
|
|
2441
2480
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
2442
2481
|
};
|
|
2482
|
+
appendLocalTelemetry(usageBody);
|
|
2443
2483
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2444
2484
|
method: 'POST',
|
|
2445
2485
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2551,7 +2591,7 @@ main();
|
|
|
2551
2591
|
BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
2552
2592
|
import {
|
|
2553
2593
|
loadJwt, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
2554
|
-
outputEmpty, GATEWAY_URL,
|
|
2594
|
+
outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2555
2595
|
} from './_synkro-common.ts';
|
|
2556
2596
|
|
|
2557
2597
|
async function main() {
|
|
@@ -2592,6 +2632,8 @@ async function main() {
|
|
|
2592
2632
|
command_hash: cmdHash,
|
|
2593
2633
|
};
|
|
2594
2634
|
|
|
2635
|
+
appendLocalTelemetry(body);
|
|
2636
|
+
|
|
2595
2637
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2596
2638
|
method: 'POST',
|
|
2597
2639
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2609,7 +2651,7 @@ main();
|
|
|
2609
2651
|
`;
|
|
2610
2652
|
TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
|
|
2611
2653
|
import {
|
|
2612
|
-
loadJwt, detectRepo, readStdin, aggregateUsage,
|
|
2654
|
+
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
2613
2655
|
outputEmpty, GATEWAY_URL,
|
|
2614
2656
|
} from './_synkro-common.ts';
|
|
2615
2657
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -2652,6 +2694,7 @@ async function main() {
|
|
|
2652
2694
|
},
|
|
2653
2695
|
session_id: sessionId,
|
|
2654
2696
|
};
|
|
2697
|
+
appendLocalTelemetry(usageBody);
|
|
2655
2698
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2656
2699
|
method: 'POST',
|
|
2657
2700
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -5094,7 +5137,7 @@ function writeConfigEnv(opts) {
|
|
|
5094
5137
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5095
5138
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5096
5139
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5097
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
5140
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.60")}`
|
|
5098
5141
|
];
|
|
5099
5142
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5100
5143
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|