@synkro-sh/cli 1.4.61 → 1.4.63
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 +275 -62
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -2
package/dist/bootstrap.js
CHANGED
|
@@ -837,7 +837,7 @@ var init_hookScriptsTs = __esm({
|
|
|
837
837
|
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
838
838
|
import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
|
|
839
839
|
import { homedir } from 'node:os';
|
|
840
|
-
import { execSync
|
|
840
|
+
import { execSync } from 'node:child_process';
|
|
841
841
|
import { constants as FS_CONSTANTS } from 'node:fs';
|
|
842
842
|
|
|
843
843
|
// \u2500\u2500\u2500 Config \u2500\u2500\u2500
|
|
@@ -1116,56 +1116,79 @@ export function tag(rt: string, config: HookConfig): string {
|
|
|
1116
1116
|
return '[synkro:' + rt + ':' + rs + ']';
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
|
-
// \u2500\u2500\u2500 Local Grading \u2500\u2500\u2500
|
|
1119
|
+
// \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
|
|
1120
1120
|
|
|
1121
|
-
|
|
1122
|
-
return new Promise((resolve, reject) => {
|
|
1123
|
-
const cliBin = process.env.SYNKRO_CLI_BIN;
|
|
1124
|
-
let cmd: string;
|
|
1125
|
-
let args: string[];
|
|
1121
|
+
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe';
|
|
1126
1122
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
args = [cliBin, 'grade', surface];
|
|
1131
|
-
} else {
|
|
1132
|
-
cmd = 'synkro';
|
|
1133
|
-
args = ['grade', surface];
|
|
1134
|
-
}
|
|
1123
|
+
const ROLE_MAP: Record<string, GradeRole> = {
|
|
1124
|
+
edit: 'grade-edit', bash: 'grade-bash', plan: 'grade-plan', cwe: 'grade-cwe',
|
|
1125
|
+
};
|
|
1135
1126
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1127
|
+
const PRIMER_KEY: Record<GradeRole, string> = {
|
|
1128
|
+
'grade-edit': 'grader_primer_edit',
|
|
1129
|
+
'grade-bash': 'grader_primer_bash',
|
|
1130
|
+
'grade-plan': 'grader_primer_plan',
|
|
1131
|
+
'grade-cwe': 'grader_primer_cwe',
|
|
1132
|
+
};
|
|
1140
1133
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}, timeoutMs);
|
|
1152
|
-
|
|
1153
|
-
child.on('close', (code: number | null) => {
|
|
1154
|
-
clearTimeout(timer);
|
|
1155
|
-
if (code === 0) resolve(stdout);
|
|
1156
|
-
else reject(new Error(stderr || 'exit ' + code));
|
|
1157
|
-
});
|
|
1158
|
-
child.on('error', (err: Error) => { clearTimeout(timer); reject(err); });
|
|
1134
|
+
let primerCache: { data: Record<string, string>; ts: number } | null = null;
|
|
1135
|
+
|
|
1136
|
+
async function fetchPrimer(role: GradeRole, jwt: string): Promise<string> {
|
|
1137
|
+
if (primerCache && Date.now() - primerCache.ts < 60_000) {
|
|
1138
|
+
const cached = primerCache.data[PRIMER_KEY[role]];
|
|
1139
|
+
if (cached) return cached;
|
|
1140
|
+
}
|
|
1141
|
+
const resp = await fetch(GATEWAY_URL + '/api/v1/cli/judge-prompts', {
|
|
1142
|
+
headers: { Authorization: 'Bearer ' + jwt },
|
|
1143
|
+
signal: AbortSignal.timeout(4000),
|
|
1159
1144
|
});
|
|
1145
|
+
if (!resp.ok) throw new Error('primer fetch failed: ' + resp.status);
|
|
1146
|
+
const data = await resp.json() as Record<string, string>;
|
|
1147
|
+
primerCache = { data, ts: Date.now() };
|
|
1148
|
+
return data[PRIMER_KEY[role]] || '';
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const CHANNEL_REPLY_INSTRUCTIONS = \`
|
|
1152
|
+
DELIVERY METHOD \u2014 MANDATORY, OVERRIDES ALL OTHER OUTPUT RULES:
|
|
1153
|
+
You are running inside a Synkro MCP channel. Do NOT output your verdict as text.
|
|
1154
|
+
Instead, after generating your verdict, call the reply tool EXACTLY ONCE with:
|
|
1155
|
+
- req_id: the req_id from this channel event's meta
|
|
1156
|
+
- result: your complete verdict block as a string (the <synkro-verdict>\u2026</synkro-verdict> XML)
|
|
1157
|
+
Any text output is silently discarded. Only the reply tool call is captured.\`;
|
|
1158
|
+
|
|
1159
|
+
async function channelGrade(role: GradeRole, prompt: string, jwt: string, port: number, timeoutMs = 20000): Promise<string> {
|
|
1160
|
+
const primer = await fetchPrimer(role, jwt);
|
|
1161
|
+
const content = primer + '\\n\\n' + CHANNEL_REPLY_INSTRUCTIONS + '\\n\\n---\\nPAYLOAD (the input to evaluate):\\n\\n' + prompt;
|
|
1162
|
+
const body = JSON.stringify({ role, content });
|
|
1163
|
+
|
|
1164
|
+
const resp = await fetch('http://127.0.0.1:' + port + '/submit', {
|
|
1165
|
+
method: 'POST',
|
|
1166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1167
|
+
body,
|
|
1168
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
if (!resp.ok) {
|
|
1172
|
+
const text = await resp.text().catch(() => '');
|
|
1173
|
+
throw new Error('channel ' + resp.status + ': ' + text.slice(0, 200));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const data = await resp.json() as { result?: string; error?: string };
|
|
1177
|
+
if (data.error) throw new Error(data.error);
|
|
1178
|
+
return String(data.result || '');
|
|
1160
1179
|
}
|
|
1161
1180
|
|
|
1162
1181
|
export async function localGrade(surface: string, prompt: string): Promise<string> {
|
|
1163
1182
|
if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
|
|
1164
|
-
|
|
1183
|
+
const jwt = loadJwt();
|
|
1184
|
+
if (!jwt) throw new Error('NO_JWT');
|
|
1185
|
+
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929);
|
|
1165
1186
|
}
|
|
1166
1187
|
|
|
1167
1188
|
export async function localGradeCwe(prompt: string): Promise<string> {
|
|
1168
|
-
|
|
1189
|
+
const jwt = loadJwt();
|
|
1190
|
+
if (!jwt) throw new Error('NO_JWT');
|
|
1191
|
+
return channelGrade('grade-cwe', prompt, jwt, 8930, 20000);
|
|
1169
1192
|
}
|
|
1170
1193
|
|
|
1171
1194
|
// \u2500\u2500\u2500 Verdict Parsing \u2500\u2500\u2500
|
|
@@ -1285,6 +1308,20 @@ export function dispatchCapture(
|
|
|
1285
1308
|
if (repo) body.repo = repo;
|
|
1286
1309
|
if (sessionId) body.session_id = sessionId;
|
|
1287
1310
|
|
|
1311
|
+
// Local telemetry always gets full content \u2014 data never leaves the machine
|
|
1312
|
+
const localBody = { ...body };
|
|
1313
|
+
if (opts) {
|
|
1314
|
+
if (opts.command) localBody.command = opts.command;
|
|
1315
|
+
if (opts.reasoning) localBody.reasoning = opts.reasoning;
|
|
1316
|
+
if (opts.rulesChecked) localBody.rules_checked = opts.rulesChecked;
|
|
1317
|
+
if (opts.violatedRules) localBody.violated_rules = opts.violatedRules;
|
|
1318
|
+
if (opts.recentUserMessages) localBody.recent_user_messages = opts.recentUserMessages;
|
|
1319
|
+
}
|
|
1320
|
+
appendLocalTelemetry(localBody);
|
|
1321
|
+
|
|
1322
|
+
// local_only: no data leaves the machine
|
|
1323
|
+
if (captureDepth === 'local_only') return;
|
|
1324
|
+
|
|
1288
1325
|
if (sendFull && opts) {
|
|
1289
1326
|
body.capture_depth = captureDepth;
|
|
1290
1327
|
if (opts.command) body.command = opts.command;
|
|
@@ -1294,8 +1331,6 @@ export function dispatchCapture(
|
|
|
1294
1331
|
if (opts.recentUserMessages) body.recent_user_messages = opts.recentUserMessages;
|
|
1295
1332
|
}
|
|
1296
1333
|
|
|
1297
|
-
appendLocalTelemetry(body);
|
|
1298
|
-
|
|
1299
1334
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
1300
1335
|
method: 'POST',
|
|
1301
1336
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -1323,7 +1358,7 @@ export function ruleMode(ruleId: string, rules: Rule[]): 'blocking' | 'audit' {
|
|
|
1323
1358
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
1324
1359
|
|
|
1325
1360
|
export function reconstructContent(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
|
|
1326
|
-
const canRead = filePath &&
|
|
1361
|
+
const canRead = filePath && cwd && isPathUnder(filePath, cwd);
|
|
1327
1362
|
switch (toolName) {
|
|
1328
1363
|
case 'Write':
|
|
1329
1364
|
return toolInput.content || '';
|
|
@@ -1616,6 +1651,66 @@ export function aggregateUsage(transcriptPath: string): { model: string; totals:
|
|
|
1616
1651
|
return result;
|
|
1617
1652
|
}
|
|
1618
1653
|
|
|
1654
|
+
// \u2500\u2500\u2500 Scan Finding Dispatch \u2500\u2500\u2500
|
|
1655
|
+
|
|
1656
|
+
export function dispatchFinding(
|
|
1657
|
+
jwt: string,
|
|
1658
|
+
finding: {
|
|
1659
|
+
session_id: string;
|
|
1660
|
+
file_path: string;
|
|
1661
|
+
finding_type: 'cwe' | 'cve';
|
|
1662
|
+
finding_id: string;
|
|
1663
|
+
severity?: string;
|
|
1664
|
+
status: 'open' | 'resolved' | 'exempted';
|
|
1665
|
+
detail?: string;
|
|
1666
|
+
description?: string;
|
|
1667
|
+
package_name?: string;
|
|
1668
|
+
package_version?: string;
|
|
1669
|
+
fixed_version?: string;
|
|
1670
|
+
aliases?: string[];
|
|
1671
|
+
references?: Array<{ type: string; url: string }>;
|
|
1672
|
+
cwe_name?: string;
|
|
1673
|
+
},
|
|
1674
|
+
captureDepth: string,
|
|
1675
|
+
): void {
|
|
1676
|
+
const localEntry: Record<string, any> = {
|
|
1677
|
+
capture_type: 'scan_finding',
|
|
1678
|
+
...finding,
|
|
1679
|
+
};
|
|
1680
|
+
appendLocalTelemetry(localEntry);
|
|
1681
|
+
|
|
1682
|
+
if (captureDepth === 'local_only') return;
|
|
1683
|
+
|
|
1684
|
+
const cloudBody: Record<string, any> = {
|
|
1685
|
+
finding_type: finding.finding_type,
|
|
1686
|
+
finding_id: finding.finding_id,
|
|
1687
|
+
severity: finding.severity,
|
|
1688
|
+
status: finding.status,
|
|
1689
|
+
session_id: finding.session_id,
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
if (captureDepth === 'evidence_on_violation' || captureDepth === 'full') {
|
|
1693
|
+
cloudBody.file_path = finding.file_path;
|
|
1694
|
+
cloudBody.package_name = finding.package_name;
|
|
1695
|
+
cloudBody.package_version = finding.package_version;
|
|
1696
|
+
cloudBody.fixed_version = finding.fixed_version;
|
|
1697
|
+
cloudBody.aliases = finding.aliases;
|
|
1698
|
+
cloudBody.references = finding.references;
|
|
1699
|
+
cloudBody.cwe_name = finding.cwe_name;
|
|
1700
|
+
}
|
|
1701
|
+
if (captureDepth === 'full') {
|
|
1702
|
+
cloudBody.detail = finding.detail;
|
|
1703
|
+
cloudBody.description = finding.description;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
fetch(GATEWAY_URL + '/api/v1/hook/finding', {
|
|
1707
|
+
method: 'POST',
|
|
1708
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
1709
|
+
body: JSON.stringify(cloudBody),
|
|
1710
|
+
signal: AbortSignal.timeout(3000),
|
|
1711
|
+
}).catch(() => {});
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1619
1714
|
// \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
|
|
1620
1715
|
|
|
1621
1716
|
export function outputJson(obj: any): void {
|
|
@@ -1841,7 +1936,7 @@ main();
|
|
|
1841
1936
|
import {
|
|
1842
1937
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
1843
1938
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
1844
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
1939
|
+
outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
|
|
1845
1940
|
} from './_synkro-common.ts';
|
|
1846
1941
|
import { basename, extname } from 'node:path';
|
|
1847
1942
|
|
|
@@ -1878,6 +1973,25 @@ async function main() {
|
|
|
1878
1973
|
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1879
1974
|
if (!proposed) { outputEmpty(); return; }
|
|
1880
1975
|
|
|
1976
|
+
// Change-anchored window: for Edit/MultiEdit send context around the diff,
|
|
1977
|
+
// for Write send first 4000 chars (new files have patterns at the top).
|
|
1978
|
+
let cweContent: string;
|
|
1979
|
+
if (toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
1980
|
+
const newStr = toolName === 'Edit'
|
|
1981
|
+
? (toolInput.new_string || '')
|
|
1982
|
+
: (Array.isArray(toolInput.edits) ? toolInput.edits.map((e: any) => e?.new_string || '').join('\\n') : '');
|
|
1983
|
+
const changeIdx = proposed.indexOf(newStr);
|
|
1984
|
+
if (changeIdx >= 0 && proposed.length > 6000) {
|
|
1985
|
+
const start = Math.max(0, changeIdx - 2000);
|
|
1986
|
+
const end = Math.min(proposed.length, changeIdx + newStr.length + 2000);
|
|
1987
|
+
cweContent = proposed.slice(start, end);
|
|
1988
|
+
} else {
|
|
1989
|
+
cweContent = proposed.slice(0, 6000);
|
|
1990
|
+
}
|
|
1991
|
+
} else {
|
|
1992
|
+
cweContent = proposed.slice(0, 4000);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1881
1995
|
const config = await loadConfig(jwt);
|
|
1882
1996
|
const rt = await cweRoute(config);
|
|
1883
1997
|
|
|
@@ -1915,7 +2029,7 @@ async function main() {
|
|
|
1915
2029
|
const graderPrompt = [
|
|
1916
2030
|
'File: ' + filePath,
|
|
1917
2031
|
'Content:',
|
|
1918
|
-
|
|
2032
|
+
cweContent,
|
|
1919
2033
|
'',
|
|
1920
2034
|
'CWE rules to check against:',
|
|
1921
2035
|
JSON.stringify(cweRules),
|
|
@@ -1948,6 +2062,11 @@ async function main() {
|
|
|
1948
2062
|
return;
|
|
1949
2063
|
}
|
|
1950
2064
|
|
|
2065
|
+
const cweNameMap = new Map<string, string>();
|
|
2066
|
+
for (const r of cweRules) {
|
|
2067
|
+
if (r.cwe && r.name) cweNameMap.set(r.cwe.toUpperCase(), r.name);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1951
2070
|
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
1952
2071
|
const count = activeCweIds.length;
|
|
1953
2072
|
const label = count === 1 ? 'match' : 'matches';
|
|
@@ -1955,6 +2074,19 @@ async function main() {
|
|
|
1955
2074
|
const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
|
|
1956
2075
|
const ctx = 'CWE: ' + denyDetail + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
1957
2076
|
|
|
2077
|
+
for (const cweId of activeCweIds) {
|
|
2078
|
+
dispatchFinding(jwt, {
|
|
2079
|
+
session_id: sessionId,
|
|
2080
|
+
file_path: filePath,
|
|
2081
|
+
finding_type: 'cwe',
|
|
2082
|
+
finding_id: cweId,
|
|
2083
|
+
severity: verdict.severity || 'high',
|
|
2084
|
+
status: 'open',
|
|
2085
|
+
detail: verdict.reason || 'code weakness detected',
|
|
2086
|
+
cwe_name: cweNameMap.get(cweId.toUpperCase()) || undefined,
|
|
2087
|
+
}, config.captureDepth);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1958
2090
|
outputJson({
|
|
1959
2091
|
systemMessage: cweMsg,
|
|
1960
2092
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
@@ -1962,6 +2094,14 @@ async function main() {
|
|
|
1962
2094
|
return;
|
|
1963
2095
|
}
|
|
1964
2096
|
|
|
2097
|
+
dispatchFinding(jwt, {
|
|
2098
|
+
session_id: sessionId,
|
|
2099
|
+
file_path: filePath,
|
|
2100
|
+
finding_type: 'cwe',
|
|
2101
|
+
finding_id: 'pass',
|
|
2102
|
+
status: 'resolved',
|
|
2103
|
+
}, config.captureDepth);
|
|
2104
|
+
|
|
1965
2105
|
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean' });
|
|
1966
2106
|
return;
|
|
1967
2107
|
}
|
|
@@ -1981,7 +2121,7 @@ main();
|
|
|
1981
2121
|
import {
|
|
1982
2122
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
1983
2123
|
reconstructContent, readStdin, findNearestDeps, log,
|
|
1984
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2124
|
+
outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
|
|
1985
2125
|
} from './_synkro-common.ts';
|
|
1986
2126
|
import { basename } from 'node:path';
|
|
1987
2127
|
|
|
@@ -2011,6 +2151,7 @@ async function main() {
|
|
|
2011
2151
|
}
|
|
2012
2152
|
|
|
2013
2153
|
const toolInput = payload.tool_input || {};
|
|
2154
|
+
const sessionId = payload.session_id || '';
|
|
2014
2155
|
const cwd = payload.cwd || '';
|
|
2015
2156
|
|
|
2016
2157
|
const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
|
|
@@ -2072,15 +2213,35 @@ async function main() {
|
|
|
2072
2213
|
|
|
2073
2214
|
const findings = Array.isArray(cveResp?.findings) ? cveResp.findings : [];
|
|
2074
2215
|
if (findings.length > 0) {
|
|
2075
|
-
const
|
|
2076
|
-
const
|
|
2216
|
+
for (const f of findings.slice(0, 10)) {
|
|
2217
|
+
const cveId = (f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown';
|
|
2218
|
+
dispatchFinding(jwt, {
|
|
2219
|
+
session_id: sessionId,
|
|
2220
|
+
file_path: filePath,
|
|
2221
|
+
finding_type: 'cve',
|
|
2222
|
+
finding_id: cveId,
|
|
2223
|
+
severity: f.severity || 'high',
|
|
2224
|
+
status: 'open',
|
|
2225
|
+
detail: f.summary || f.title || 'vulnerable dependency',
|
|
2226
|
+
description: f.details || undefined,
|
|
2227
|
+
package_name: f.package || undefined,
|
|
2228
|
+
package_version: f.version || undefined,
|
|
2229
|
+
fixed_version: f.fixed || undefined,
|
|
2230
|
+
aliases: f.aliases || undefined,
|
|
2231
|
+
references: f.references || undefined,
|
|
2232
|
+
}, config.captureDepth);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
const formatFinding = (f: any): string => {
|
|
2236
|
+
const id = (f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || '?';
|
|
2077
2237
|
const pkg = f.package || '?';
|
|
2078
2238
|
const ver = f.version || '?';
|
|
2079
2239
|
const title = f.title || f.summary || 'vulnerable';
|
|
2080
|
-
const fix = f.fixed ? ' (fix: >=' + f.fixed + ')' : ' (no safe version
|
|
2240
|
+
const fix = f.fixed ? ' (fix: >=' + f.fixed + ')' : ' (no safe version)';
|
|
2081
2241
|
return '[' + id + '] ' + pkg + '@' + ver + ': ' + title + fix;
|
|
2082
|
-
}
|
|
2242
|
+
};
|
|
2083
2243
|
|
|
2244
|
+
const top3 = findings.slice(0, 3).map(formatFinding).join('; ');
|
|
2084
2245
|
const count = findings.length;
|
|
2085
2246
|
const label = count === 1 ? 'advisory' : 'advisories';
|
|
2086
2247
|
const cveMsg = cveTag + ' ' + fileShort + ' \\u2192 ' + count + ' ' + label;
|
|
@@ -2150,14 +2311,32 @@ async function main() {
|
|
|
2150
2311
|
// \u2500\u2500\u2500 CVE scan for package install commands \u2500\u2500\u2500
|
|
2151
2312
|
if (toolName === 'Bash') {
|
|
2152
2313
|
const pkgInstallMatch = command.match(
|
|
2153
|
-
/(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|
|
|
2314
|
+
/(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+(.+)/
|
|
2154
2315
|
);
|
|
2316
|
+
const isPip = /(?:uv\\s+)?pip3?\\s+install/.test(command);
|
|
2317
|
+
const isGo = command.match(/^go\\s+get/);
|
|
2318
|
+
const isCargo = command.match(/^cargo\\s+add/);
|
|
2319
|
+
const isGem = command.match(/^gem\\s+install/);
|
|
2320
|
+
const isComposer = command.match(/^composer\\s+require/);
|
|
2155
2321
|
if (pkgInstallMatch) {
|
|
2156
2322
|
const rawArgs = pkgInstallMatch[1];
|
|
2157
2323
|
const deps: Record<string, string> = {};
|
|
2158
2324
|
const tokens = rawArgs.split(/\\s+/);
|
|
2325
|
+
let skipNext = false;
|
|
2159
2326
|
for (const token of tokens) {
|
|
2160
|
-
if (
|
|
2327
|
+
if (skipNext) { skipNext = false; continue; }
|
|
2328
|
+
if (token.startsWith('-')) {
|
|
2329
|
+
if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir)$/.test(token)) skipNext = true;
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (isPip) {
|
|
2333
|
+
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
2334
|
+
if (pipMatch) {
|
|
2335
|
+
deps[pipMatch[1]] = pipMatch[2]?.replace(/^=/, '') || '*';
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
// npm/yarn: pkg@1.0
|
|
2161
2340
|
const atIdx = token.lastIndexOf('@');
|
|
2162
2341
|
if (atIdx > 0) {
|
|
2163
2342
|
deps[token.slice(0, atIdx)] = token.slice(atIdx + 1);
|
|
@@ -2165,9 +2344,18 @@ async function main() {
|
|
|
2165
2344
|
deps[token] = '*';
|
|
2166
2345
|
}
|
|
2167
2346
|
}
|
|
2347
|
+
const manifestFile = isPip ? 'requirements.txt'
|
|
2348
|
+
: isGo ? 'go.mod'
|
|
2349
|
+
: isCargo ? 'Cargo.toml'
|
|
2350
|
+
: isGem ? 'Gemfile'
|
|
2351
|
+
: isComposer ? 'composer.json'
|
|
2352
|
+
: 'package.json';
|
|
2353
|
+
const manifestContent = isPip
|
|
2354
|
+
? Object.entries(deps).map(([k, v]) => v === '*' ? k : k + '==' + v).join('\\n')
|
|
2355
|
+
: JSON.stringify({ dependencies: deps });
|
|
2168
2356
|
if (Object.keys(deps).length > 0) {
|
|
2169
2357
|
try {
|
|
2170
|
-
const cveBody = { file_path:
|
|
2358
|
+
const cveBody = { file_path: manifestFile, content: manifestContent, dependencies: deps };
|
|
2171
2359
|
const cveResp = await fetch(GATEWAY_URL + '/api/v1/cve-scan', {
|
|
2172
2360
|
method: 'POST',
|
|
2173
2361
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2493,7 +2681,7 @@ async function main() {
|
|
|
2493
2681
|
const usage = aggregateUsage(transcriptPath);
|
|
2494
2682
|
if (usage.totals.in + usage.totals.out > 0) {
|
|
2495
2683
|
const usageBody = {
|
|
2496
|
-
capture_type: '
|
|
2684
|
+
capture_type: 'usage_tick',
|
|
2497
2685
|
event_id: 'usage_' + Date.now() + '_' + process.pid,
|
|
2498
2686
|
hook_type: 'stop',
|
|
2499
2687
|
verdict: 'allow',
|
|
@@ -2620,7 +2808,7 @@ main();
|
|
|
2620
2808
|
`;
|
|
2621
2809
|
BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
2622
2810
|
import {
|
|
2623
|
-
loadJwt, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
2811
|
+
loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
2624
2812
|
outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2625
2813
|
} from './_synkro-common.ts';
|
|
2626
2814
|
|
|
@@ -2664,12 +2852,15 @@ async function main() {
|
|
|
2664
2852
|
|
|
2665
2853
|
appendLocalTelemetry(body);
|
|
2666
2854
|
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2855
|
+
const config = await loadConfig(jwt);
|
|
2856
|
+
if (config.captureDepth !== 'local_only') {
|
|
2857
|
+
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2858
|
+
method: 'POST',
|
|
2859
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
2860
|
+
body: JSON.stringify(body),
|
|
2861
|
+
signal: AbortSignal.timeout(3000),
|
|
2862
|
+
}).catch(() => {});
|
|
2863
|
+
}
|
|
2673
2864
|
|
|
2674
2865
|
outputEmpty();
|
|
2675
2866
|
} catch {
|
|
@@ -2822,7 +3013,7 @@ async function main() {
|
|
|
2822
3013
|
main();
|
|
2823
3014
|
`;
|
|
2824
3015
|
USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
|
|
2825
|
-
import { readStdin } from './_synkro-common.ts';
|
|
3016
|
+
import { readStdin, appendLocalTelemetry, aggregateUsage } from './_synkro-common.ts';
|
|
2826
3017
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2827
3018
|
import { join, dirname } from 'node:path';
|
|
2828
3019
|
import { homedir } from 'node:os';
|
|
@@ -2838,6 +3029,28 @@ async function main() {
|
|
|
2838
3029
|
mkdirSync(dirname(promptFile), { recursive: true });
|
|
2839
3030
|
writeFileSync(promptFile, msg, 'utf-8');
|
|
2840
3031
|
}
|
|
3032
|
+
|
|
3033
|
+
const sessionId = payload.session_id || '';
|
|
3034
|
+
const transcriptPath = payload.transcript_path || '';
|
|
3035
|
+
if (sessionId && transcriptPath) {
|
|
3036
|
+
const usage = aggregateUsage(transcriptPath);
|
|
3037
|
+
if (usage.totals.in + usage.totals.out > 0) {
|
|
3038
|
+
appendLocalTelemetry({
|
|
3039
|
+
capture_type: 'usage_tick',
|
|
3040
|
+
event_id: 'usage_' + Date.now() + '_' + process.pid,
|
|
3041
|
+
hook_type: 'prompt_submit',
|
|
3042
|
+
session_id: sessionId,
|
|
3043
|
+
model: usage.model || 'unknown',
|
|
3044
|
+
cc_model: usage.model || '',
|
|
3045
|
+
cc_usage: {
|
|
3046
|
+
input_tokens: usage.totals.in,
|
|
3047
|
+
output_tokens: usage.totals.out,
|
|
3048
|
+
cache_creation_input_tokens: usage.totals.cw,
|
|
3049
|
+
cache_read_input_tokens: usage.totals.cr,
|
|
3050
|
+
},
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
2841
3054
|
} catch {}
|
|
2842
3055
|
}
|
|
2843
3056
|
|
|
@@ -5167,7 +5380,7 @@ function writeConfigEnv(opts) {
|
|
|
5167
5380
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5168
5381
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5169
5382
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5170
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
5383
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.63")}`
|
|
5171
5384
|
];
|
|
5172
5385
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5173
5386
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|