@synkro-sh/cli 1.6.28 → 1.6.29
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 +798 -337
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -336,6 +336,12 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
336
336
|
failClosed: false,
|
|
337
337
|
[SYNKRO_MARKER2]: true
|
|
338
338
|
});
|
|
339
|
+
h.beforeShellExecution.push({
|
|
340
|
+
command: cursorCcCmd(config.cwePrecheckScriptPath),
|
|
341
|
+
timeout: 60,
|
|
342
|
+
failClosed: false,
|
|
343
|
+
[SYNKRO_MARKER2]: true
|
|
344
|
+
});
|
|
339
345
|
h.beforeShellExecution.push({
|
|
340
346
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
341
347
|
timeout: 15,
|
|
@@ -351,6 +357,13 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
351
357
|
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command",
|
|
352
358
|
[SYNKRO_MARKER2]: true
|
|
353
359
|
});
|
|
360
|
+
h.preToolUse.push({
|
|
361
|
+
command: cursorCcCmd(config.cwePrecheckScriptPath),
|
|
362
|
+
timeout: 60,
|
|
363
|
+
failClosed: false,
|
|
364
|
+
matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command",
|
|
365
|
+
[SYNKRO_MARKER2]: true
|
|
366
|
+
});
|
|
354
367
|
h.preToolUse.push({
|
|
355
368
|
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
356
369
|
timeout: 15,
|
|
@@ -773,7 +786,8 @@ var init_hookScriptsTs = __esm({
|
|
|
773
786
|
"use strict";
|
|
774
787
|
SYNKRO_COMMON_TS = `
|
|
775
788
|
// Shared Synkro hook utilities \u2014 imported by all hook scripts.
|
|
776
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync, readdirSync, statSync } from 'node:fs';
|
|
789
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync, readdirSync, statSync, createReadStream } from 'node:fs';
|
|
790
|
+
import { createInterface } from 'node:readline';
|
|
777
791
|
import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
|
|
778
792
|
import { homedir } from 'node:os';
|
|
779
793
|
import { execSync } from 'node:child_process';
|
|
@@ -827,6 +841,51 @@ export function isPathUnder(filePath: string, cwd: string): boolean {
|
|
|
827
841
|
return resolved.startsWith(base + '/') || resolved === base;
|
|
828
842
|
}
|
|
829
843
|
|
|
844
|
+
const SHELL_CODE_FILE_EXT = /.(ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|php|rs|vue|svelte)$/i;
|
|
845
|
+
|
|
846
|
+
export interface ShellCodeWrite {
|
|
847
|
+
filePath: string;
|
|
848
|
+
content: string;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Detect shell commands that write/rewrite source files (closes Edit-tool CWE bypass). */
|
|
852
|
+
export function extractShellCodeWrites(command: string, cwd: string): ShellCodeWrite[] {
|
|
853
|
+
if (!command.trim()) return [];
|
|
854
|
+
const writes: ShellCodeWrite[] = [];
|
|
855
|
+
const seen = new Set<string>();
|
|
856
|
+
|
|
857
|
+
function add(rawPath: string, content: string) {
|
|
858
|
+
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, '');
|
|
859
|
+
if (!trimmed) return;
|
|
860
|
+
const resolved = trimmed.startsWith('/') ? resolvePath(trimmed) : resolvePath(cwd || '.', trimmed);
|
|
861
|
+
if (!SHELL_CODE_FILE_EXT.test(resolved)) return;
|
|
862
|
+
const key = resolved;
|
|
863
|
+
if (seen.has(key)) return;
|
|
864
|
+
seen.add(key);
|
|
865
|
+
writes.push({ filePath: resolved, content: content.slice(0, 6000) });
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const heredocBodies: string[] = [];
|
|
869
|
+
const heredocRe = /<<-?\\s*['"]?(\\w+)['"]?\\s*\\n([\\s\\S]*?)\\n\\1\\b/g;
|
|
870
|
+
let hm: RegExpExecArray | null;
|
|
871
|
+
while ((hm = heredocRe.exec(command)) !== null) {
|
|
872
|
+
heredocBodies.push(hm[2]);
|
|
873
|
+
}
|
|
874
|
+
const body = heredocBodies.length > 0 ? heredocBodies.join('\\n\\n') : command;
|
|
875
|
+
|
|
876
|
+
for (const m of command.matchAll(/Path\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)/g)) add(m[1], body);
|
|
877
|
+
for (const m of command.matchAll(/writeFileSync\\s*\\(\\s*['"]([^'"]+)['"]/g)) add(m[1], body);
|
|
878
|
+
for (const m of command.matchAll(/writeFile\\s*\\(\\s*['"]([^'"]+)['"]/g)) add(m[1], body);
|
|
879
|
+
for (const m of command.matchAll(/(?:^|[\\s;|])(?:>>?)\\s*([^\\s;&|]+\\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|php|rs|vue|svelte))\\b/gim)) {
|
|
880
|
+
add(m[1], body);
|
|
881
|
+
}
|
|
882
|
+
for (const m of command.matchAll(/\\btee(?:\\s+-a)?\\s+([^\\s;&|]+\\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|java|rb|php|rs|vue|svelte))\\b/gi)) {
|
|
883
|
+
add(m[1], body);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return writes;
|
|
887
|
+
}
|
|
888
|
+
|
|
830
889
|
// \u2500\u2500\u2500 Logging \u2500\u2500\u2500
|
|
831
890
|
|
|
832
891
|
// Hooks must keep stderr quiet for non-error paths. Claude Code's PreToolUse
|
|
@@ -1220,10 +1279,11 @@ export async function route(config: HookConfig): Promise<'local' | 'cloud'> {
|
|
|
1220
1279
|
return 'cloud';
|
|
1221
1280
|
}
|
|
1222
1281
|
|
|
1223
|
-
export async function cweRoute(config: HookConfig): Promise<'local' | '
|
|
1224
|
-
|
|
1282
|
+
export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | 'skip'> {
|
|
1283
|
+
const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1284
|
+
if (gradingMode === 'byok') return 'byok';
|
|
1225
1285
|
if (await cweChannelUp()) return 'local';
|
|
1226
|
-
return '
|
|
1286
|
+
return 'skip';
|
|
1227
1287
|
}
|
|
1228
1288
|
|
|
1229
1289
|
// \u2500\u2500\u2500 Tag Building \u2500\u2500\u2500
|
|
@@ -1299,10 +1359,10 @@ export async function localGrade(surface: string, prompt: string, timeoutMs = 30
|
|
|
1299
1359
|
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
|
|
1300
1360
|
}
|
|
1301
1361
|
|
|
1302
|
-
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1362
|
+
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code', timeoutMs = 45000): Promise<string> {
|
|
1303
1363
|
const jwt = loadJwt();
|
|
1304
1364
|
if (!jwt) throw new Error('NO_JWT');
|
|
1305
|
-
return channelGrade('grade-cwe', prompt, jwt, 18930,
|
|
1365
|
+
return channelGrade('grade-cwe', prompt, jwt, 18930, timeoutMs, agentKind);
|
|
1306
1366
|
}
|
|
1307
1367
|
|
|
1308
1368
|
// \u2500\u2500\u2500 Rule Pre-Filter (embedding-based) \u2500\u2500\u2500
|
|
@@ -1768,7 +1828,7 @@ export function dispatchCapture(
|
|
|
1768
1828
|
},
|
|
1769
1829
|
): void {
|
|
1770
1830
|
// Fire-and-forget
|
|
1771
|
-
const eventId = '
|
|
1831
|
+
const eventId = mintEventId('evt');
|
|
1772
1832
|
const model = normalizeCaptureModel(opts?.ccModel || 'unknown');
|
|
1773
1833
|
|
|
1774
1834
|
const body: Record<string, any> = {
|
|
@@ -1817,105 +1877,194 @@ export function dispatchCapture(
|
|
|
1817
1877
|
|
|
1818
1878
|
// \u2500\u2500\u2500 Durable Telemetry Spool \u2500\u2500\u2500
|
|
1819
1879
|
// Telemetry must survive process death, container restarts, and ingest-server
|
|
1820
|
-
// backpressure.
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1824
|
-
// it overlaps the multi-second grade) batch-ships the spool to the ingest
|
|
1825
|
-
// server and only deletes events after a confirmed write. Ingest is idempotent
|
|
1826
|
-
// (ON CONFLICT DO NOTHING on event_id), so a retried or double-drained event
|
|
1827
|
-
// is harmless.
|
|
1880
|
+
// backpressure. Every event is appended synchronously to a local JSONL spool.
|
|
1881
|
+
// drainSpool streams claim files line-by-line (never loads multi-GB into RAM),
|
|
1882
|
+
// ships in batches, and on partial failure only re-spools the batches that
|
|
1883
|
+
// did not land \u2014 never the whole claim file (that caused 2GB amplification).
|
|
1828
1884
|
|
|
1829
1885
|
const TELEMETRY_SPOOL = join(HOME, '.synkro', 'telemetry-spool.jsonl');
|
|
1830
1886
|
const SPOOL_DRAIN_PREFIX = 'telemetry-spool.jsonl.draining.';
|
|
1887
|
+
const SPOOL_DRAIN_LOCK = join(HOME, '.synkro', 'telemetry-spool.drain.lock');
|
|
1888
|
+
const SPOOL_MAX_CLAIM_BYTES = 50 * 1024 * 1024;
|
|
1889
|
+
const SPOOL_BATCH_SIZE = 200;
|
|
1890
|
+
const SPOOL_DRAIN_LOCK_STALE_MS = 120_000;
|
|
1891
|
+
|
|
1892
|
+
/** Stable id for spool rows \u2014 required for idempotent ingest (ON CONFLICT). */
|
|
1893
|
+
export function mintEventId(prefix = 'evt'): string {
|
|
1894
|
+
return prefix + '_' + Date.now() + '_' + process.pid;
|
|
1895
|
+
}
|
|
1831
1896
|
|
|
1832
|
-
// appendLocalTelemetry \u2014 durably records one telemetry event. The synchronous
|
|
1833
|
-
// append IS the durability guarantee; nothing here can drop the event.
|
|
1834
1897
|
export function appendLocalTelemetry(body: Record<string, any>): void {
|
|
1835
|
-
// Cloud storage mode: the dashboard reads Timescale and a cloud-only setup
|
|
1836
|
-
// has no container to drain a local spool \u2014 so skip it. Cloud telemetry goes
|
|
1837
|
-
// via shipCloud; the local spool is only meaningful in local storage mode.
|
|
1838
1898
|
if ((process.env.SYNKRO_STORAGE_MODE || 'local') !== 'local') return;
|
|
1839
|
-
const event = { ...body
|
|
1899
|
+
const event = { ...body };
|
|
1900
|
+
if (!event.event_id) {
|
|
1901
|
+
const ct = String(event.capture_type || '');
|
|
1902
|
+
const prefix = ct === 'usage_tick' ? 'usage' : ct === 'edit_scan' ? 'edit' : 'evt';
|
|
1903
|
+
event.event_id = mintEventId(prefix);
|
|
1904
|
+
}
|
|
1905
|
+
if (!event._ts) event._ts = new Date().toISOString();
|
|
1840
1906
|
try {
|
|
1841
1907
|
appendFileSync(TELEMETRY_SPOOL, JSON.stringify(event) + '\\n');
|
|
1842
1908
|
} catch {}
|
|
1843
1909
|
}
|
|
1844
1910
|
|
|
1845
|
-
|
|
1846
|
-
// ingest server, deletes on success, re-spools on failure. Fire-and-forget:
|
|
1847
|
-
// callers kick it and let it run concurrently with the grade.
|
|
1848
|
-
export async function drainSpool(): Promise<void> {
|
|
1849
|
-
const dir = join(HOME, '.synkro');
|
|
1850
|
-
const claimed: string[] = [];
|
|
1851
|
-
|
|
1852
|
-
// 1. Claim the live spool by atomic rename \u2014 a fresh spool takes new writes.
|
|
1911
|
+
function tryAcquireDrainLock(): boolean {
|
|
1853
1912
|
try {
|
|
1854
|
-
if (existsSync(
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
claimed.push(claim);
|
|
1913
|
+
if (existsSync(SPOOL_DRAIN_LOCK)) {
|
|
1914
|
+
if (Date.now() - statSync(SPOOL_DRAIN_LOCK).mtimeMs < SPOOL_DRAIN_LOCK_STALE_MS) return false;
|
|
1915
|
+
unlinkSync(SPOOL_DRAIN_LOCK);
|
|
1858
1916
|
}
|
|
1859
|
-
|
|
1917
|
+
mkdirSync(dirname(SPOOL_DRAIN_LOCK), { recursive: true });
|
|
1918
|
+
writeFileSync(SPOOL_DRAIN_LOCK, String(process.pid) + '\\n');
|
|
1919
|
+
return true;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1860
1924
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1925
|
+
function releaseDrainLock(): void {
|
|
1926
|
+
try { unlinkSync(SPOOL_DRAIN_LOCK); } catch {}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
async function postSpoolBatch(mcpPort: string, mcpToken: string, events: any[]): Promise<boolean> {
|
|
1930
|
+
if (!events.length) return true;
|
|
1863
1931
|
try {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1932
|
+
const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/ingest/batch', {
|
|
1933
|
+
method: 'POST',
|
|
1934
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
|
|
1935
|
+
body: JSON.stringify({ events }),
|
|
1936
|
+
signal: AbortSignal.timeout(30000),
|
|
1937
|
+
});
|
|
1938
|
+
if (!resp.ok) {
|
|
1939
|
+
const errBody = await resp.text().catch(() => '');
|
|
1940
|
+
log('drainSpool batch HTTP ' + resp.status + ': ' + errBody.slice(0, 120));
|
|
1871
1941
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1942
|
+
return resp.ok;
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
log('drainSpool batch error: ' + ((e as Error).message || String(e)).slice(0, 120));
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1874
1948
|
|
|
1875
|
-
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
try { events.push(JSON.parse(t)); } catch {}
|
|
1883
|
-
}
|
|
1884
|
-
} catch {}
|
|
1949
|
+
function quarantineOversizedClaim(claimPath: string): void {
|
|
1950
|
+
const stuck = claimPath + '.STUCK-OVERSIZED.bak';
|
|
1951
|
+
try {
|
|
1952
|
+
renameSync(claimPath, stuck);
|
|
1953
|
+
log('drainSpool quarantined oversized claim \u2192 ' + basename(stuck));
|
|
1954
|
+
} catch (e) {
|
|
1955
|
+
log('drainSpool quarantine failed: ' + String(e));
|
|
1885
1956
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
async function drainClaimFile(claimPath: string, mcpPort: string, mcpToken: string): Promise<void> {
|
|
1960
|
+
let sz = 0;
|
|
1961
|
+
try { sz = statSync(claimPath).size; } catch { return; }
|
|
1962
|
+
if (sz === 0) {
|
|
1963
|
+
try { unlinkSync(claimPath); } catch {}
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (sz > SPOOL_MAX_CLAIM_BYTES) {
|
|
1967
|
+
quarantineOversizedClaim(claimPath);
|
|
1888
1968
|
return;
|
|
1889
1969
|
}
|
|
1890
1970
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
let
|
|
1894
|
-
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
1895
|
-
if (!mcpToken) return; // leave claim files; a later drain retries them
|
|
1971
|
+
const pending: any[] = [];
|
|
1972
|
+
let batch: any[] = [];
|
|
1973
|
+
let failed = false;
|
|
1896
1974
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1975
|
+
const rl = createInterface({
|
|
1976
|
+
input: createReadStream(claimPath, { encoding: 'utf8' }),
|
|
1977
|
+
crlfDelay: Infinity,
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
for await (const line of rl) {
|
|
1981
|
+
if (failed) {
|
|
1982
|
+
const t = line.trim();
|
|
1983
|
+
if (!t) continue;
|
|
1984
|
+
try { pending.push(JSON.parse(t)); } catch {}
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
const t = line.trim();
|
|
1988
|
+
if (!t) continue;
|
|
1899
1989
|
try {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1990
|
+
batch.push(JSON.parse(t));
|
|
1991
|
+
} catch {
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
if (batch.length < SPOOL_BATCH_SIZE) continue;
|
|
1995
|
+
const chunk = batch.splice(0, SPOOL_BATCH_SIZE);
|
|
1996
|
+
if (!(await postSpoolBatch(mcpPort, mcpToken, chunk))) {
|
|
1997
|
+
pending.push(...chunk);
|
|
1998
|
+
failed = true;
|
|
1999
|
+
}
|
|
1908
2000
|
}
|
|
1909
2001
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2002
|
+
if (!failed && batch.length > 0) {
|
|
2003
|
+
if (!(await postSpoolBatch(mcpPort, mcpToken, batch))) {
|
|
2004
|
+
pending.push(...batch);
|
|
2005
|
+
failed = true;
|
|
2006
|
+
} else {
|
|
2007
|
+
batch = [];
|
|
2008
|
+
}
|
|
2009
|
+
} else if (failed && batch.length > 0) {
|
|
2010
|
+
pending.push(...batch);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (pending.length === 0) {
|
|
2014
|
+
try { unlinkSync(claimPath); } catch {}
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
try {
|
|
2019
|
+
for (const evt of pending) {
|
|
2020
|
+
appendFileSync(TELEMETRY_SPOOL, JSON.stringify(evt) + '\\n');
|
|
2021
|
+
}
|
|
2022
|
+
try { unlinkSync(claimPath); } catch {}
|
|
2023
|
+
log('drainSpool re-spooled ' + pending.length + ' events from ' + basename(claimPath));
|
|
2024
|
+
} catch (e) {
|
|
2025
|
+
log('drainSpool re-spool failed: ' + String(e));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
export async function drainSpool(): Promise<void> {
|
|
2030
|
+
if (!tryAcquireDrainLock()) return;
|
|
2031
|
+
|
|
2032
|
+
const dir = join(HOME, '.synkro');
|
|
2033
|
+
const claimed: string[] = [];
|
|
2034
|
+
|
|
2035
|
+
try {
|
|
1914
2036
|
try {
|
|
1915
|
-
|
|
1916
|
-
|
|
2037
|
+
if (existsSync(TELEMETRY_SPOOL) && statSync(TELEMETRY_SPOOL).size > 0) {
|
|
2038
|
+
const claim = join(dir, SPOOL_DRAIN_PREFIX + process.pid + '.' + Date.now());
|
|
2039
|
+
renameSync(TELEMETRY_SPOOL, claim);
|
|
2040
|
+
claimed.push(claim);
|
|
2041
|
+
}
|
|
1917
2042
|
} catch {}
|
|
1918
|
-
|
|
2043
|
+
|
|
2044
|
+
try {
|
|
2045
|
+
for (const f of readdirSync(dir)) {
|
|
2046
|
+
if (!f.startsWith(SPOOL_DRAIN_PREFIX)) continue;
|
|
2047
|
+
if (f.includes('.STUCK-') || f.endsWith('.bak')) continue;
|
|
2048
|
+
const full = join(dir, f);
|
|
2049
|
+
if (claimed.indexOf(full) !== -1) continue;
|
|
2050
|
+
try {
|
|
2051
|
+
if (Date.now() - statSync(full).mtimeMs > 30000) claimed.push(full);
|
|
2052
|
+
} catch {}
|
|
2053
|
+
}
|
|
2054
|
+
} catch {}
|
|
2055
|
+
|
|
2056
|
+
if (claimed.length === 0) return;
|
|
2057
|
+
|
|
2058
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
2059
|
+
let mcpToken = '';
|
|
2060
|
+
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
2061
|
+
if (!mcpToken) return;
|
|
2062
|
+
|
|
2063
|
+
for (const f of claimed) {
|
|
2064
|
+
await drainClaimFile(f, mcpPort, mcpToken);
|
|
2065
|
+
}
|
|
2066
|
+
} finally {
|
|
2067
|
+
releaseDrainLock();
|
|
1919
2068
|
}
|
|
1920
2069
|
}
|
|
1921
2070
|
|
|
@@ -2708,7 +2857,7 @@ export function emitUsageTick(params: {
|
|
|
2708
2857
|
}
|
|
2709
2858
|
appendLocalTelemetry({
|
|
2710
2859
|
capture_type: 'usage_tick',
|
|
2711
|
-
event_id: '
|
|
2860
|
+
event_id: mintEventId('usage'),
|
|
2712
2861
|
hook_type: hookType,
|
|
2713
2862
|
verdict: 'allow',
|
|
2714
2863
|
severity: 'none',
|
|
@@ -2816,6 +2965,38 @@ export function dispatchFinding(
|
|
|
2816
2965
|
shipCloud(jwt, '/api/v1/hook/finding', cloudBody);
|
|
2817
2966
|
}
|
|
2818
2967
|
|
|
2968
|
+
export function dispatchScanResult(
|
|
2969
|
+
jwt: string,
|
|
2970
|
+
scan: {
|
|
2971
|
+
session_id: string;
|
|
2972
|
+
file_path: string;
|
|
2973
|
+
scan_type: 'cve' | 'cwe' | 'pkg';
|
|
2974
|
+
result: 'pass' | 'block' | 'error';
|
|
2975
|
+
finding_count: number;
|
|
2976
|
+
finding_ids?: string[];
|
|
2977
|
+
severity?: string;
|
|
2978
|
+
repo?: string;
|
|
2979
|
+
},
|
|
2980
|
+
): void {
|
|
2981
|
+
const localEntry: Record<string, any> = {
|
|
2982
|
+
capture_type: 'scan_result',
|
|
2983
|
+
event_id: 'scan_' + Date.now() + '_' + process.pid,
|
|
2984
|
+
_ts: new Date().toISOString(),
|
|
2985
|
+
...scan,
|
|
2986
|
+
};
|
|
2987
|
+
appendLocalTelemetry(localEntry);
|
|
2988
|
+
shipCloud(jwt, '/api/v1/hook/scan-result', {
|
|
2989
|
+
scan_type: scan.scan_type,
|
|
2990
|
+
result: scan.result,
|
|
2991
|
+
finding_count: scan.finding_count,
|
|
2992
|
+
finding_ids: scan.finding_ids,
|
|
2993
|
+
severity: scan.severity,
|
|
2994
|
+
session_id: scan.session_id,
|
|
2995
|
+
file_path: scan.file_path,
|
|
2996
|
+
repo: scan.repo,
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
|
|
2819
3000
|
// \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
|
|
2820
3001
|
|
|
2821
3002
|
export const EDIT_TOOL_NAMES = new Set([
|
|
@@ -3194,7 +3375,8 @@ main();
|
|
|
3194
3375
|
import {
|
|
3195
3376
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
3196
3377
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3197
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool,
|
|
3378
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3379
|
+
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3198
3380
|
logGraderUnavailable, resolveTranscriptPath,
|
|
3199
3381
|
} from './_synkro-common.ts';
|
|
3200
3382
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
@@ -3215,6 +3397,22 @@ interface PackageCapability {
|
|
|
3215
3397
|
sourceExcerpt: string;
|
|
3216
3398
|
}
|
|
3217
3399
|
|
|
3400
|
+
const NON_CODE_EXTS = new Set([
|
|
3401
|
+
'.md', '.mdx', '.txt', '.rst', '.adoc', '.org',
|
|
3402
|
+
'.log', '.csv', '.tsv', '.html', '.htm',
|
|
3403
|
+
'.lock', '.gitignore', '.dockerignore', '.npmignore',
|
|
3404
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.pdf',
|
|
3405
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
3406
|
+
]);
|
|
3407
|
+
|
|
3408
|
+
interface CweScanTarget {
|
|
3409
|
+
filePath: string;
|
|
3410
|
+
cweContent: string;
|
|
3411
|
+
cweDiffSection: string;
|
|
3412
|
+
toolName: string;
|
|
3413
|
+
toolInput: any;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3218
3416
|
const JS_DANGEROUS_MODULES = new Set([
|
|
3219
3417
|
'child_process', 'net', 'dgram', 'http', 'https', 'fs', 'vm',
|
|
3220
3418
|
'worker_threads', 'cluster', 'dns', 'tls', 'crypto',
|
|
@@ -3322,69 +3520,90 @@ async function main() {
|
|
|
3322
3520
|
|
|
3323
3521
|
const payload = JSON.parse(input);
|
|
3324
3522
|
const toolName = payload.tool_name || '';
|
|
3325
|
-
if (!isEditTool(toolName)) {
|
|
3326
|
-
outputEmpty();
|
|
3327
|
-
return;
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
3523
|
const toolInput = payload.tool_input || {};
|
|
3331
3524
|
const sessionId = hookSessionId(payload);
|
|
3332
3525
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3333
3526
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3334
3527
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
3335
|
-
|
|
3336
|
-
const filePath = filePathFromToolInput(toolInput);
|
|
3337
|
-
if (!filePath) { outputEmpty(); return; }
|
|
3338
|
-
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3528
|
+
const shellCommand = typeof payload.command === 'string' ? payload.command.trim() : '';
|
|
3339
3529
|
const ccModel = detectModel(payload);
|
|
3340
3530
|
|
|
3341
|
-
|
|
3531
|
+
const targets: CweScanTarget[] = [];
|
|
3532
|
+
|
|
3533
|
+
if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
|
|
3534
|
+
const cmd = shellCommand || String(toolInput.command || '');
|
|
3535
|
+
if (!cmd) { outputEmpty(); return; }
|
|
3536
|
+
for (const w of extractShellCodeWrites(cmd, cwd)) {
|
|
3537
|
+
if (w.filePath.includes('/.synkro/hooks/')) continue;
|
|
3538
|
+
const ext = extname(w.filePath).toLowerCase();
|
|
3539
|
+
if (NON_CODE_EXTS.has(ext)) continue;
|
|
3540
|
+
targets.push({
|
|
3541
|
+
filePath: w.filePath,
|
|
3542
|
+
cweContent: w.content,
|
|
3543
|
+
cweDiffSection: '',
|
|
3544
|
+
toolName: toolName || 'Shell',
|
|
3545
|
+
toolInput: {},
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
if (targets.length === 0) { outputEmpty(); return; }
|
|
3549
|
+
} else if (isEditTool(toolName)) {
|
|
3550
|
+
const filePath = filePathFromToolInput(toolInput);
|
|
3551
|
+
if (!filePath) { outputEmpty(); return; }
|
|
3552
|
+
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
3553
|
+
const fileExt = extname(filePath).toLowerCase();
|
|
3554
|
+
if (NON_CODE_EXTS.has(fileExt)) { outputEmpty(); return; }
|
|
3555
|
+
|
|
3556
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
3557
|
+
if (!proposed) { outputEmpty(); return; }
|
|
3558
|
+
|
|
3559
|
+
let cweContent: string;
|
|
3560
|
+
let cweDiffSection = '';
|
|
3561
|
+
if (toolName === 'Edit' || toolName === 'MultiEdit' || toolName === 'edit_file' || toolName === 'reapply' || toolName === 'ApplyPatch' || toolName === 'apply_patch') {
|
|
3562
|
+
const newStr = toolName === 'Edit' || toolName === 'edit_file' || toolName === 'reapply'
|
|
3563
|
+
? (toolInput.new_string || '')
|
|
3564
|
+
: toolName === 'ApplyPatch' || toolName === 'apply_patch'
|
|
3565
|
+
? (toolInput.patch || toolInput.content || toolInput.code_edit || '')
|
|
3566
|
+
: (Array.isArray(toolInput.edits) ? toolInput.edits.map((e: any) => e?.new_string || '').join('\n') : '');
|
|
3567
|
+
cweDiffSection = newStr.slice(0, 4000);
|
|
3568
|
+
const changeIdx = proposed.indexOf(newStr);
|
|
3569
|
+
if (changeIdx >= 0 && proposed.length > 6000) {
|
|
3570
|
+
const start = Math.max(0, changeIdx - 2000);
|
|
3571
|
+
const end = Math.min(proposed.length, changeIdx + newStr.length + 2000);
|
|
3572
|
+
cweContent = proposed.slice(start, end);
|
|
3573
|
+
} else {
|
|
3574
|
+
cweContent = proposed.slice(0, 6000);
|
|
3575
|
+
}
|
|
3576
|
+
} else {
|
|
3577
|
+
cweContent = proposed.slice(0, 4000);
|
|
3578
|
+
}
|
|
3342
3579
|
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
// can trigger a false positive on the literal string.
|
|
3349
|
-
const NON_CODE_EXTS = new Set([
|
|
3350
|
-
'.md', '.mdx', '.txt', '.rst', '.adoc', '.org',
|
|
3351
|
-
'.log', '.csv', '.tsv', '.html', '.htm',
|
|
3352
|
-
'.lock', '.gitignore', '.dockerignore', '.npmignore',
|
|
3353
|
-
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.pdf',
|
|
3354
|
-
'.woff', '.woff2', '.ttf', '.otf',
|
|
3355
|
-
]);
|
|
3356
|
-
if (NON_CODE_EXTS.has(fileExt)) { outputEmpty(); return; }
|
|
3580
|
+
targets.push({ filePath, cweContent, cweDiffSection, toolName, toolInput });
|
|
3581
|
+
} else {
|
|
3582
|
+
outputEmpty();
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3357
3585
|
|
|
3358
3586
|
let jwt = loadJwt();
|
|
3359
3587
|
if (!jwt) { outputEmpty(); return; }
|
|
3360
3588
|
jwt = await ensureFreshJwt(jwt);
|
|
3361
3589
|
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3590
|
+
const config = await loadConfig(jwt);
|
|
3591
|
+
const rt = await cweRoute(config);
|
|
3364
3592
|
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
const newStr = toolName === 'Edit' || toolName === 'edit_file' || toolName === 'reapply'
|
|
3369
|
-
? (toolInput.new_string || '')
|
|
3370
|
-
: toolName === 'ApplyPatch' || toolName === 'apply_patch'
|
|
3371
|
-
? (toolInput.patch || toolInput.content || toolInput.code_edit || '')
|
|
3372
|
-
: (Array.isArray(toolInput.edits) ? toolInput.edits.map((e: any) => e?.new_string || '').join('\n') : '');
|
|
3373
|
-
cweDiffSection = newStr.slice(0, 4000);
|
|
3374
|
-
const changeIdx = proposed.indexOf(newStr);
|
|
3375
|
-
if (changeIdx >= 0 && proposed.length > 6000) {
|
|
3376
|
-
const start = Math.max(0, changeIdx - 2000);
|
|
3377
|
-
const end = Math.min(proposed.length, changeIdx + newStr.length + 2000);
|
|
3378
|
-
cweContent = proposed.slice(start, end);
|
|
3379
|
-
} else {
|
|
3380
|
-
cweContent = proposed.slice(0, 6000);
|
|
3381
|
-
}
|
|
3382
|
-
} else {
|
|
3383
|
-
cweContent = proposed.slice(0, 4000);
|
|
3593
|
+
if (config.silent) {
|
|
3594
|
+
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] skipped (silent mode)' });
|
|
3595
|
+
return;
|
|
3384
3596
|
}
|
|
3385
3597
|
|
|
3386
|
-
const
|
|
3387
|
-
|
|
3598
|
+
for (const scan of targets) {
|
|
3599
|
+
const filePath = scan.filePath;
|
|
3600
|
+
const cweContent = scan.cweContent;
|
|
3601
|
+
const cweDiffSection = scan.cweDiffSection;
|
|
3602
|
+
const scanToolName = scan.toolName;
|
|
3603
|
+
const scanToolInput = scan.toolInput;
|
|
3604
|
+
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3605
|
+
const fileShort = basename(filePath);
|
|
3606
|
+
const fileExt = extname(filePath).toLowerCase();
|
|
3388
3607
|
|
|
3389
3608
|
const exemptedCwes = new Set<string>();
|
|
3390
3609
|
for (const ex of config.scanExemptions) {
|
|
@@ -3392,12 +3611,120 @@ async function main() {
|
|
|
3392
3611
|
exemptedCwes.add(ex.cwe_id.toUpperCase());
|
|
3393
3612
|
}
|
|
3394
3613
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3614
|
+
|
|
3615
|
+
const cweTag = '[synkro:' + rt + ':cweScan]';
|
|
3616
|
+
|
|
3617
|
+
if (rt === 'skip') {
|
|
3618
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 local CWE grader unavailable, skipped' });
|
|
3397
3619
|
return;
|
|
3398
3620
|
}
|
|
3399
3621
|
|
|
3400
|
-
|
|
3622
|
+
if (rt === 'byok') {
|
|
3623
|
+
let packageContext: PackageCapability[] | undefined;
|
|
3624
|
+
if (cwd) {
|
|
3625
|
+
const newImports = detectNewImports(scanToolName, scanToolInput, fileExt);
|
|
3626
|
+
if (newImports.length > 0) {
|
|
3627
|
+
const caps = newImports
|
|
3628
|
+
.slice(0, 5)
|
|
3629
|
+
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
3630
|
+
.filter((c): c is PackageCapability => c !== null);
|
|
3631
|
+
if (caps.length > 0) packageContext = caps;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
const scanBody: any = { file_path: filePath, content: cweContent };
|
|
3635
|
+
if (packageContext) {
|
|
3636
|
+
scanBody.package_context = packageContext.map(c => ({
|
|
3637
|
+
name: c.name, description: c.description, capabilities: c.capabilities, source_excerpt: c.sourceExcerpt,
|
|
3638
|
+
}));
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
let cweResp: any;
|
|
3642
|
+
try {
|
|
3643
|
+
const resp = await fetch(GATEWAY_URL + '/api/v1/cwe-scan', {
|
|
3644
|
+
method: 'POST',
|
|
3645
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3646
|
+
body: JSON.stringify(scanBody),
|
|
3647
|
+
signal: AbortSignal.timeout(12000),
|
|
3648
|
+
});
|
|
3649
|
+
if (!resp.ok) {
|
|
3650
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud CWE scan failed (HTTP ' + resp.status + '), skipped' });
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
cweResp = await resp.json();
|
|
3654
|
+
} catch {
|
|
3655
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud CWE scan timeout, skipped' });
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
3660
|
+
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
3661
|
+
const activeCweIds = findings
|
|
3662
|
+
.filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
|
|
3663
|
+
.map((f: any) => f.cwe)
|
|
3664
|
+
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
3665
|
+
|
|
3666
|
+
if (activeCweIds.length === 0) {
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
3671
|
+
const count = activeCweIds.length;
|
|
3672
|
+
const label = count === 1 ? 'match' : 'matches';
|
|
3673
|
+
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
3674
|
+
|
|
3675
|
+
const fixLines = findings
|
|
3676
|
+
.filter((f: any) => activeCweIds.includes(f.cwe) && f.suggested_fix)
|
|
3677
|
+
.map((f: any) => '[' + f.cwe + '] Fix: ' + f.suggested_fix);
|
|
3678
|
+
const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
|
|
3679
|
+
const denyDetail = '[' + displayIds + '] ' + (findings[0]?.reason || 'code weakness detected');
|
|
3680
|
+
const ctx = 'CWE: ' + denyDetail + fixHint + '\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
3681
|
+
|
|
3682
|
+
emitBlockScanFindings(
|
|
3683
|
+
jwt,
|
|
3684
|
+
config.captureDepth,
|
|
3685
|
+
{ session_id: sessionId, file_path: filePath, repo: gitRepo || undefined },
|
|
3686
|
+
activeCweIds.map((cweId) => {
|
|
3687
|
+
const f = findings.find((x: any) => x.cwe === cweId);
|
|
3688
|
+
return {
|
|
3689
|
+
finding_type: 'cwe' as const,
|
|
3690
|
+
finding_id: cweId,
|
|
3691
|
+
severity: f?.severity || 'high',
|
|
3692
|
+
detail: f?.reason || 'code weakness detected',
|
|
3693
|
+
cwe_name: f?.name || undefined,
|
|
3694
|
+
};
|
|
3695
|
+
}),
|
|
3696
|
+
{
|
|
3697
|
+
finding_type: 'cwe',
|
|
3698
|
+
finding_id: activeCweIds[0] || 'CWE-UNKNOWN',
|
|
3699
|
+
severity: findings[0]?.severity || 'high',
|
|
3700
|
+
detail: denyDetail,
|
|
3701
|
+
},
|
|
3702
|
+
);
|
|
3703
|
+
|
|
3704
|
+
dispatchCapture(jwt, 'cwe', 'block', findings[0]?.severity || 'high', 'security',
|
|
3705
|
+
scanToolName, gitRepo, sessionId, config.captureDepth, {
|
|
3706
|
+
command: (isShellTool(scanToolName) ? 'shell write ' : 'edit ') + filePath,
|
|
3707
|
+
reasoning: denyDetail,
|
|
3708
|
+
violatedRules: activeCweIds,
|
|
3709
|
+
ccModel,
|
|
3710
|
+
});
|
|
3711
|
+
|
|
3712
|
+
outputJson({
|
|
3713
|
+
systemMessage: cweMsg,
|
|
3714
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3715
|
+
});
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
dispatchFinding(jwt, {
|
|
3720
|
+
session_id: sessionId,
|
|
3721
|
+
file_path: filePath,
|
|
3722
|
+
finding_type: 'cwe',
|
|
3723
|
+
finding_id: 'pass',
|
|
3724
|
+
status: 'resolved',
|
|
3725
|
+
}, config.captureDepth);
|
|
3726
|
+
continue;
|
|
3727
|
+
}
|
|
3401
3728
|
|
|
3402
3729
|
if (rt === 'local') {
|
|
3403
3730
|
let cweRules: any[] = [];
|
|
@@ -3431,7 +3758,7 @@ async function main() {
|
|
|
3431
3758
|
|
|
3432
3759
|
let localPkgContext = '';
|
|
3433
3760
|
if (cwd) {
|
|
3434
|
-
const newImports = detectNewImports(
|
|
3761
|
+
const newImports = detectNewImports(scanToolName, scanToolInput, fileExt);
|
|
3435
3762
|
const caps = newImports.slice(0, 5)
|
|
3436
3763
|
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
3437
3764
|
.filter((c): c is PackageCapability => c !== null);
|
|
@@ -3526,8 +3853,7 @@ async function main() {
|
|
|
3526
3853
|
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
3527
3854
|
|
|
3528
3855
|
if (activeCweIds.length === 0) {
|
|
3529
|
-
|
|
3530
|
-
return;
|
|
3856
|
+
continue;
|
|
3531
3857
|
}
|
|
3532
3858
|
|
|
3533
3859
|
const cweNameMap = new Map<string, string>();
|
|
@@ -3566,8 +3892,8 @@ async function main() {
|
|
|
3566
3892
|
);
|
|
3567
3893
|
|
|
3568
3894
|
dispatchCapture(jwt, 'cwe', 'block', verdict.severity || 'high', verdict.category || 'security',
|
|
3569
|
-
|
|
3570
|
-
command: 'edit ' + filePath,
|
|
3895
|
+
scanToolName, gitRepo, sessionId, config.captureDepth, {
|
|
3896
|
+
command: (isShellTool(scanToolName) ? 'shell write ' : 'edit ') + filePath,
|
|
3571
3897
|
reasoning: denyDetail,
|
|
3572
3898
|
violatedRules: activeCweIds,
|
|
3573
3899
|
ccModel,
|
|
@@ -3587,114 +3913,11 @@ async function main() {
|
|
|
3587
3913
|
finding_id: 'pass',
|
|
3588
3914
|
status: 'resolved',
|
|
3589
3915
|
}, config.captureDepth);
|
|
3590
|
-
|
|
3591
|
-
const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (verdict.reason ? ' (' + verdict.reason + ')' : '');
|
|
3592
|
-
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
3593
|
-
return;
|
|
3916
|
+
continue;
|
|
3594
3917
|
}
|
|
3595
|
-
|
|
3596
|
-
// Cloud path \u2014 thin client, all grading logic server-side
|
|
3597
|
-
// Detect imports and scan capabilities for cloud grading
|
|
3598
|
-
let packageContext: PackageCapability[] | undefined;
|
|
3599
|
-
if (cwd) {
|
|
3600
|
-
const newImports = detectNewImports(toolName, toolInput, fileExt);
|
|
3601
|
-
if (newImports.length > 0) {
|
|
3602
|
-
const caps = newImports
|
|
3603
|
-
.slice(0, 5)
|
|
3604
|
-
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
3605
|
-
.filter((c): c is PackageCapability => c !== null);
|
|
3606
|
-
if (caps.length > 0) packageContext = caps;
|
|
3607
|
-
}
|
|
3608
|
-
}
|
|
3609
|
-
const scanBody: any = { file_path: filePath, content: cweContent };
|
|
3610
|
-
if (packageContext) scanBody.package_context = packageContext.map(c => ({
|
|
3611
|
-
name: c.name, description: c.description, capabilities: c.capabilities, source_excerpt: c.sourceExcerpt,
|
|
3612
|
-
}));
|
|
3613
|
-
let cweResp: any;
|
|
3614
|
-
try {
|
|
3615
|
-
const resp = await fetch(GATEWAY_URL + '/api/v1/cwe-scan', {
|
|
3616
|
-
method: 'POST',
|
|
3617
|
-
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3618
|
-
body: JSON.stringify(scanBody),
|
|
3619
|
-
signal: AbortSignal.timeout(12000),
|
|
3620
|
-
});
|
|
3621
|
-
cweResp = await resp.json();
|
|
3622
|
-
} catch {
|
|
3623
|
-
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud grader timeout, skipped' });
|
|
3624
|
-
return;
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
3628
|
-
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
3629
|
-
const activeCweIds = findings
|
|
3630
|
-
.filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
|
|
3631
|
-
.map((f: any) => f.cwe)
|
|
3632
|
-
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
3633
|
-
|
|
3634
|
-
if (activeCweIds.length === 0) {
|
|
3635
|
-
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 clean (exempted)' });
|
|
3636
|
-
return;
|
|
3637
|
-
}
|
|
3638
|
-
|
|
3639
|
-
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
3640
|
-
const count = activeCweIds.length;
|
|
3641
|
-
const label = count === 1 ? 'match' : 'matches';
|
|
3642
|
-
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
3643
|
-
|
|
3644
|
-
const fixLines = findings
|
|
3645
|
-
.filter((f: any) => activeCweIds.includes(f.cwe) && f.suggested_fix)
|
|
3646
|
-
.map((f: any) => '[' + f.cwe + '] Fix: ' + f.suggested_fix);
|
|
3647
|
-
const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
|
|
3648
|
-
const denyDetail = '[' + displayIds + '] ' + (findings[0]?.reason || 'code weakness detected');
|
|
3649
|
-
const ctx = 'CWE: ' + denyDetail + fixHint + '\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
|
|
3650
|
-
|
|
3651
|
-
emitBlockScanFindings(
|
|
3652
|
-
jwt,
|
|
3653
|
-
config.captureDepth,
|
|
3654
|
-
{ session_id: sessionId, file_path: filePath, repo: gitRepo || undefined },
|
|
3655
|
-
activeCweIds.map((cweId) => {
|
|
3656
|
-
const f = findings.find((x: any) => x.cwe === cweId);
|
|
3657
|
-
return {
|
|
3658
|
-
finding_type: 'cwe' as const,
|
|
3659
|
-
finding_id: cweId,
|
|
3660
|
-
severity: f?.severity || 'high',
|
|
3661
|
-
detail: f?.reason || 'code weakness detected',
|
|
3662
|
-
cwe_name: f?.name || undefined,
|
|
3663
|
-
};
|
|
3664
|
-
}),
|
|
3665
|
-
{
|
|
3666
|
-
finding_type: 'cwe',
|
|
3667
|
-
finding_id: activeCweIds[0] || 'CWE-UNKNOWN',
|
|
3668
|
-
severity: findings[0]?.severity || 'high',
|
|
3669
|
-
detail: denyDetail,
|
|
3670
|
-
},
|
|
3671
|
-
);
|
|
3672
|
-
|
|
3673
|
-
dispatchCapture(jwt, 'cwe', 'block', findings[0]?.severity || 'high', 'security',
|
|
3674
|
-
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3675
|
-
command: 'edit ' + filePath,
|
|
3676
|
-
reasoning: denyDetail,
|
|
3677
|
-
violatedRules: activeCweIds,
|
|
3678
|
-
ccModel,
|
|
3679
|
-
});
|
|
3680
|
-
|
|
3681
|
-
outputJson({
|
|
3682
|
-
systemMessage: cweMsg,
|
|
3683
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3684
|
-
});
|
|
3685
|
-
return;
|
|
3686
3918
|
}
|
|
3687
3919
|
|
|
3688
|
-
|
|
3689
|
-
session_id: sessionId,
|
|
3690
|
-
file_path: filePath,
|
|
3691
|
-
finding_type: 'cwe',
|
|
3692
|
-
finding_id: 'pass',
|
|
3693
|
-
status: 'resolved',
|
|
3694
|
-
}, config.captureDepth);
|
|
3695
|
-
|
|
3696
|
-
const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (cweResp?.summary ? ' (cloud)' : '');
|
|
3697
|
-
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
3920
|
+
outputEmpty();
|
|
3698
3921
|
} catch (err) {
|
|
3699
3922
|
log('cweGuard error: ' + String(err));
|
|
3700
3923
|
outputEmpty();
|
|
@@ -3707,7 +3930,7 @@ main();
|
|
|
3707
3930
|
import {
|
|
3708
3931
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3709
3932
|
reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
|
|
3710
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
3933
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
3711
3934
|
} from './_synkro-common.ts';
|
|
3712
3935
|
import { basename } from 'node:path';
|
|
3713
3936
|
import { readFileSync } from 'node:fs';
|
|
@@ -3858,6 +4081,12 @@ async function main() {
|
|
|
3858
4081
|
violatedRules: violatedIds,
|
|
3859
4082
|
ccModel,
|
|
3860
4083
|
});
|
|
4084
|
+
dispatchScanResult(jwt, {
|
|
4085
|
+
session_id: sessionId, file_path: filePath, scan_type: 'pkg',
|
|
4086
|
+
result: 'block', finding_count: violatedIds.length,
|
|
4087
|
+
finding_ids: violatedIds, severity: 'critical',
|
|
4088
|
+
repo: gitRepo || undefined,
|
|
4089
|
+
});
|
|
3861
4090
|
const tagStr = '[synkro:' + rt + ':pkgScan]';
|
|
3862
4091
|
const denyReason = tagStr + ' BLOCKED: ' + summary + '\\nDo not write this version. Pick a fixed/safe version instead.';
|
|
3863
4092
|
outputJson({
|
|
@@ -3942,6 +4171,12 @@ async function main() {
|
|
|
3942
4171
|
reasoning: top3,
|
|
3943
4172
|
violatedRules: cveIds,
|
|
3944
4173
|
});
|
|
4174
|
+
dispatchScanResult(jwt, {
|
|
4175
|
+
session_id: sessionId, file_path: filePath, scan_type: 'cve',
|
|
4176
|
+
result: 'block', finding_count: findings.length,
|
|
4177
|
+
finding_ids: cveIds, severity: 'critical',
|
|
4178
|
+
repo: gitRepo || undefined,
|
|
4179
|
+
});
|
|
3945
4180
|
|
|
3946
4181
|
outputJson({
|
|
3947
4182
|
systemMessage: cveMsg,
|
|
@@ -3950,6 +4185,11 @@ async function main() {
|
|
|
3950
4185
|
return;
|
|
3951
4186
|
}
|
|
3952
4187
|
|
|
4188
|
+
dispatchScanResult(jwt, {
|
|
4189
|
+
session_id: sessionId, file_path: filePath, scan_type: 'cve',
|
|
4190
|
+
result: 'pass', finding_count: 0,
|
|
4191
|
+
repo: gitRepo || undefined,
|
|
4192
|
+
});
|
|
3953
4193
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \u2192 clean' });
|
|
3954
4194
|
} catch (err) {
|
|
3955
4195
|
log('cveGuard error: ' + String(err));
|
|
@@ -3962,7 +4202,7 @@ main();
|
|
|
3962
4202
|
INSTALL_SCAN_TS = `#!/usr/bin/env bun
|
|
3963
4203
|
import {
|
|
3964
4204
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3965
|
-
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, hashCommand,
|
|
4205
|
+
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
3966
4206
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
3967
4207
|
resolveTranscriptPath, isCursorHookFormat,
|
|
3968
4208
|
} from './_synkro-common.ts';
|
|
@@ -4045,6 +4285,12 @@ async function main() {
|
|
|
4045
4285
|
violatedRules: scan.violatedIds,
|
|
4046
4286
|
ccModel: model || undefined,
|
|
4047
4287
|
});
|
|
4288
|
+
dispatchScanResult(jwt, {
|
|
4289
|
+
session_id: sessionId, file_path: command, scan_type: 'pkg',
|
|
4290
|
+
result: 'block', finding_count: scan.violatedIds?.length || 1,
|
|
4291
|
+
finding_ids: scan.violatedIds, severity: 'critical',
|
|
4292
|
+
repo: repo || undefined,
|
|
4293
|
+
});
|
|
4048
4294
|
const denyReason = '[synkro:installScan] BLOCKED: ' + scan.summary + '\\nDo not retry this install. Suggest a safe version to the user instead.';
|
|
4049
4295
|
outputJson({
|
|
4050
4296
|
systemMessage: denyReason,
|
|
@@ -5339,10 +5585,10 @@ import {
|
|
|
5339
5585
|
loadJwt, ensureFreshJwt, detectRepo, readStdin, resolveTranscriptPath,
|
|
5340
5586
|
appendSessionAction, appendLocalTelemetry, shipCloud, log, GATEWAY_URL,
|
|
5341
5587
|
countEditLineDelta, dispatchCapture, hookSessionId, cursorModelFromPayload,
|
|
5588
|
+
isLocalStorageMode,
|
|
5342
5589
|
} from './_synkro-common.ts';
|
|
5343
5590
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5344
5591
|
import { basename, dirname, join } from 'node:path';
|
|
5345
|
-
import { homedir } from 'node:os';
|
|
5346
5592
|
|
|
5347
5593
|
let hookDone = false;
|
|
5348
5594
|
|
|
@@ -5391,16 +5637,8 @@ async function main() {
|
|
|
5391
5637
|
if (!jwt) finish();
|
|
5392
5638
|
jwt = await ensureFreshJwt(jwt);
|
|
5393
5639
|
|
|
5394
|
-
let fileContent = '';
|
|
5395
|
-
const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
|
|
5396
|
-
try {
|
|
5397
|
-
if (existsSync(fullPath)) {
|
|
5398
|
-
const buf = readFileSync(fullPath);
|
|
5399
|
-
fileContent = buf.slice(0, 50000).toString('utf-8');
|
|
5400
|
-
}
|
|
5401
|
-
} catch {}
|
|
5402
|
-
|
|
5403
5640
|
let dependencies: Record<string, string> = {};
|
|
5641
|
+
const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
|
|
5404
5642
|
let pkgDir = cwd || dirname(fullPath);
|
|
5405
5643
|
while (pkgDir !== '/' && pkgDir !== '.') {
|
|
5406
5644
|
const pkgPath = join(pkgDir, 'package.json');
|
|
@@ -5416,20 +5654,39 @@ async function main() {
|
|
|
5416
5654
|
pkgDir = parent;
|
|
5417
5655
|
}
|
|
5418
5656
|
|
|
5419
|
-
const
|
|
5657
|
+
const localSpoolBody: Record<string, any> = {
|
|
5420
5658
|
capture_type: 'edit_scan',
|
|
5421
|
-
tool_input: { file_path: filePath,
|
|
5659
|
+
tool_input: { file_path: filePath, lines_added: linesAdded, lines_removed: linesRemoved },
|
|
5422
5660
|
edit_verdict: { ok: true },
|
|
5423
5661
|
dependencies,
|
|
5424
5662
|
cc_model: model,
|
|
5425
5663
|
model,
|
|
5426
5664
|
};
|
|
5427
|
-
if (sessionId)
|
|
5428
|
-
if (cwd)
|
|
5429
|
-
if (repo)
|
|
5665
|
+
if (sessionId) localSpoolBody.session_id = sessionId;
|
|
5666
|
+
if (cwd) localSpoolBody.cwd = cwd;
|
|
5667
|
+
if (repo) localSpoolBody.repo = repo;
|
|
5668
|
+
appendLocalTelemetry(localSpoolBody);
|
|
5430
5669
|
|
|
5431
|
-
|
|
5432
|
-
|
|
5670
|
+
if (!isLocalStorageMode()) {
|
|
5671
|
+
let fileContent = '';
|
|
5672
|
+
try {
|
|
5673
|
+
if (existsSync(fullPath)) {
|
|
5674
|
+
fileContent = readFileSync(fullPath).slice(0, 50000).toString('utf-8');
|
|
5675
|
+
}
|
|
5676
|
+
} catch {}
|
|
5677
|
+
const cloudBody: Record<string, any> = {
|
|
5678
|
+
capture_type: 'edit_scan',
|
|
5679
|
+
tool_input: { file_path: filePath, content: fileContent },
|
|
5680
|
+
edit_verdict: { ok: true },
|
|
5681
|
+
dependencies,
|
|
5682
|
+
cc_model: model,
|
|
5683
|
+
model,
|
|
5684
|
+
};
|
|
5685
|
+
if (sessionId) cloudBody.session_id = sessionId;
|
|
5686
|
+
if (cwd) cloudBody.cwd = cwd;
|
|
5687
|
+
if (repo) cloudBody.repo = repo;
|
|
5688
|
+
shipCloud(jwt, '/api/v1/hook/capture', cloudBody, 10000);
|
|
5689
|
+
}
|
|
5433
5690
|
|
|
5434
5691
|
if (sessionId) {
|
|
5435
5692
|
dispatchCapture(jwt, 'edit', 'pass', 'clean', 'trivial_edit', 'Edit', repo, sessionId, 'full', {
|
|
@@ -6032,12 +6289,13 @@ var init_githubSetup = __esm({
|
|
|
6032
6289
|
});
|
|
6033
6290
|
|
|
6034
6291
|
// cli/commands/repoConnect.ts
|
|
6035
|
-
import { execSync as execSync3 } from "child_process";
|
|
6292
|
+
import { execSync as execSync3, execFileSync } from "child_process";
|
|
6293
|
+
import { readdirSync } from "fs";
|
|
6036
6294
|
import { createServer as createServer2 } from "http";
|
|
6037
6295
|
import { createInterface } from "readline";
|
|
6038
6296
|
function detectGitRepo() {
|
|
6039
6297
|
try {
|
|
6040
|
-
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
6298
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
6041
6299
|
const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
6042
6300
|
const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
6043
6301
|
const match = sshMatch || httpMatch;
|
|
@@ -6048,6 +6306,33 @@ function detectGitRepo() {
|
|
|
6048
6306
|
return null;
|
|
6049
6307
|
}
|
|
6050
6308
|
}
|
|
6309
|
+
function detectSubdirRepos() {
|
|
6310
|
+
try {
|
|
6311
|
+
const entries = readdirSync(".", { withFileTypes: true });
|
|
6312
|
+
const repos = [];
|
|
6313
|
+
for (const entry of entries) {
|
|
6314
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
6315
|
+
try {
|
|
6316
|
+
const remoteUrl = execFileSync("git", ["-C", entry.name, "remote", "get-url", "origin"], {
|
|
6317
|
+
encoding: "utf-8",
|
|
6318
|
+
timeout: 5e3,
|
|
6319
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6320
|
+
}).trim();
|
|
6321
|
+
const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
6322
|
+
const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
6323
|
+
const match = sshMatch || httpMatch;
|
|
6324
|
+
if (match) {
|
|
6325
|
+
const fullName = match[1];
|
|
6326
|
+
repos.push({ fullName, shortName: fullName.split("/").pop() || fullName });
|
|
6327
|
+
}
|
|
6328
|
+
} catch {
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
return repos;
|
|
6332
|
+
} catch {
|
|
6333
|
+
return [];
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6051
6336
|
function ask(rl, question) {
|
|
6052
6337
|
return new Promise((resolve3) => rl.question(question, resolve3));
|
|
6053
6338
|
}
|
|
@@ -6149,6 +6434,7 @@ async function connectGithubAndSelectRepos() {
|
|
|
6149
6434
|
}
|
|
6150
6435
|
async function promptRepoConnection(opts) {
|
|
6151
6436
|
const localRepo = detectGitRepo();
|
|
6437
|
+
const subdirRepos = !localRepo ? detectSubdirRepos() : [];
|
|
6152
6438
|
if (opts?.linkRepo && localRepo) {
|
|
6153
6439
|
console.log("Connect repos to Synkro:\n");
|
|
6154
6440
|
try {
|
|
@@ -6171,46 +6457,35 @@ async function promptRepoConnection(opts) {
|
|
|
6171
6457
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6172
6458
|
try {
|
|
6173
6459
|
console.log("Connect repos to Synkro:\n");
|
|
6174
|
-
|
|
6175
|
-
if (
|
|
6176
|
-
options.push(`Link this repo (${localRepo.fullName})`);
|
|
6177
|
-
}
|
|
6178
|
-
options.push("Connect GitHub to select repos");
|
|
6179
|
-
options.push("Skip for now");
|
|
6180
|
-
options.forEach((opt, i) => {
|
|
6181
|
-
console.log(` ${i + 1}. ${opt}`);
|
|
6182
|
-
});
|
|
6183
|
-
console.log();
|
|
6184
|
-
const choice = await ask(rl, " Choose (number): ");
|
|
6185
|
-
const choiceNum = parseInt(choice.trim(), 10);
|
|
6186
|
-
console.log();
|
|
6187
|
-
rl.close();
|
|
6188
|
-
const localIdx = localRepo ? 1 : -1;
|
|
6189
|
-
const githubIdx = localRepo ? 2 : 1;
|
|
6190
|
-
const skipIdx = localRepo ? 3 : 2;
|
|
6191
|
-
if (choiceNum === localIdx && localRepo) {
|
|
6460
|
+
let existingFullNames = /* @__PURE__ */ new Set();
|
|
6461
|
+
if (subdirRepos.length > 0) {
|
|
6192
6462
|
try {
|
|
6193
6463
|
const existing = await listProjects();
|
|
6194
|
-
|
|
6195
|
-
(p) => p.repos
|
|
6464
|
+
existingFullNames = new Set(
|
|
6465
|
+
existing.flatMap((p) => (p.repos || []).map((r) => r.full_name))
|
|
6196
6466
|
);
|
|
6197
|
-
|
|
6198
|
-
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
6199
|
-
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
6200
|
-
} else {
|
|
6201
|
-
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
6202
|
-
}
|
|
6203
|
-
} catch (err) {
|
|
6204
|
-
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
6467
|
+
} catch {
|
|
6205
6468
|
}
|
|
6206
|
-
}
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6469
|
+
}
|
|
6470
|
+
if (subdirRepos.length > 0) {
|
|
6471
|
+
console.log(` Found ${subdirRepos.length} repo(s) in this directory:
|
|
6472
|
+
`);
|
|
6473
|
+
subdirRepos.forEach((r, i) => {
|
|
6474
|
+
const linked = existingFullNames.has(r.fullName);
|
|
6475
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.fullName}${linked ? " \u2713 linked" : ""}`);
|
|
6476
|
+
});
|
|
6477
|
+
const ghIdx = subdirRepos.length + 1;
|
|
6478
|
+
const skipIdx = subdirRepos.length + 2;
|
|
6479
|
+
console.log(` ${String(ghIdx).padStart(3)}. Connect GitHub to select repos`);
|
|
6480
|
+
console.log(` ${String(skipIdx).padStart(3)}. Skip for now`);
|
|
6481
|
+
console.log();
|
|
6482
|
+
const selection = await ask(rl, " Select repos to link (comma-separated numbers, e.g. 1,3): ");
|
|
6483
|
+
const nums = selection.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
6484
|
+
console.log();
|
|
6485
|
+
rl.close();
|
|
6486
|
+
if (nums.includes(ghIdx)) {
|
|
6487
|
+
const selectedRepos = await connectGithubAndSelectRepos();
|
|
6488
|
+
if (selectedRepos.length > 0) {
|
|
6214
6489
|
const newRepos = selectedRepos.filter((r) => !existingFullNames.has(r.full_name));
|
|
6215
6490
|
if (newRepos.length === 0) {
|
|
6216
6491
|
console.log(" \u2713 All selected repos are already linked.");
|
|
@@ -6219,14 +6494,83 @@ async function promptRepoConnection(opts) {
|
|
|
6219
6494
|
await createProject(projectName, newRepos);
|
|
6220
6495
|
console.log(` \u2713 Linked ${newRepos.length} repo(s) to project "${projectName}"`);
|
|
6221
6496
|
}
|
|
6222
|
-
}
|
|
6223
|
-
|
|
6497
|
+
}
|
|
6498
|
+
} else if (nums.includes(skipIdx) || nums.length === 0) {
|
|
6499
|
+
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6500
|
+
} else {
|
|
6501
|
+
const repoIndices = nums.map((n) => n - 1).filter((n) => n >= 0 && n < subdirRepos.length);
|
|
6502
|
+
const toLink = repoIndices.map((i) => subdirRepos[i]).filter((r) => !existingFullNames.has(r.fullName));
|
|
6503
|
+
if (toLink.length === 0) {
|
|
6504
|
+
console.log(" \u2713 All selected repos are already linked.");
|
|
6505
|
+
} else {
|
|
6506
|
+
for (const repo of toLink) {
|
|
6507
|
+
try {
|
|
6508
|
+
await createProject(repo.shortName, [{ full_name: repo.fullName }]);
|
|
6509
|
+
console.log(` \u2713 Created project "${repo.shortName}" linked to ${repo.fullName}`);
|
|
6510
|
+
} catch (err) {
|
|
6511
|
+
console.warn(` \u26A0 Could not link ${repo.fullName}: ${err.message}`);
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6224
6514
|
}
|
|
6225
6515
|
}
|
|
6226
|
-
} else if (choiceNum === skipIdx) {
|
|
6227
|
-
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6228
6516
|
} else {
|
|
6229
|
-
|
|
6517
|
+
const options = [];
|
|
6518
|
+
if (localRepo) {
|
|
6519
|
+
options.push(`Link this repo (${localRepo.fullName})`);
|
|
6520
|
+
}
|
|
6521
|
+
options.push("Connect GitHub to select repos");
|
|
6522
|
+
options.push("Skip for now");
|
|
6523
|
+
options.forEach((opt, i) => {
|
|
6524
|
+
console.log(` ${i + 1}. ${opt}`);
|
|
6525
|
+
});
|
|
6526
|
+
console.log();
|
|
6527
|
+
const choice = await ask(rl, " Choose (number): ");
|
|
6528
|
+
const choiceNum = parseInt(choice.trim(), 10);
|
|
6529
|
+
console.log();
|
|
6530
|
+
rl.close();
|
|
6531
|
+
const localIdx = localRepo ? 1 : -1;
|
|
6532
|
+
const githubIdx = localRepo ? 2 : 1;
|
|
6533
|
+
const skipIdx = localRepo ? 3 : 2;
|
|
6534
|
+
if (choiceNum === localIdx && localRepo) {
|
|
6535
|
+
try {
|
|
6536
|
+
const existing = await listProjects();
|
|
6537
|
+
const alreadyLinked = existing.some(
|
|
6538
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
6539
|
+
);
|
|
6540
|
+
if (!alreadyLinked) {
|
|
6541
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
6542
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
6543
|
+
} else {
|
|
6544
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
6545
|
+
}
|
|
6546
|
+
} catch (err) {
|
|
6547
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
6548
|
+
}
|
|
6549
|
+
} else if (choiceNum === githubIdx) {
|
|
6550
|
+
const selectedRepos = await connectGithubAndSelectRepos();
|
|
6551
|
+
if (selectedRepos.length > 0) {
|
|
6552
|
+
try {
|
|
6553
|
+
const existing = await listProjects();
|
|
6554
|
+
const linkedNames = new Set(
|
|
6555
|
+
existing.flatMap((p) => (p.repos || []).map((r) => r.full_name))
|
|
6556
|
+
);
|
|
6557
|
+
const newRepos = selectedRepos.filter((r) => !linkedNames.has(r.full_name));
|
|
6558
|
+
if (newRepos.length === 0) {
|
|
6559
|
+
console.log(" \u2713 All selected repos are already linked.");
|
|
6560
|
+
} else {
|
|
6561
|
+
const projectName = newRepos.length === 1 ? newRepos[0].full_name.split("/").pop() || "Project" : "Multi-Repo Project";
|
|
6562
|
+
await createProject(projectName, newRepos);
|
|
6563
|
+
console.log(` \u2713 Linked ${newRepos.length} repo(s) to project "${projectName}"`);
|
|
6564
|
+
}
|
|
6565
|
+
} catch (err) {
|
|
6566
|
+
console.warn(` \u26A0 Could not link repos: ${err.message}`);
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
} else if (choiceNum === skipIdx) {
|
|
6570
|
+
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6571
|
+
} else {
|
|
6572
|
+
console.log(" Invalid choice. Skipping repo connection.");
|
|
6573
|
+
}
|
|
6230
6574
|
}
|
|
6231
6575
|
} catch {
|
|
6232
6576
|
rl.close();
|
|
@@ -6477,7 +6821,7 @@ __export(dockerInstall_exports, {
|
|
|
6477
6821
|
splitWorkers: () => splitWorkers,
|
|
6478
6822
|
waitForContainerReady: () => waitForContainerReady
|
|
6479
6823
|
});
|
|
6480
|
-
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync } from "fs";
|
|
6824
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2 } from "fs";
|
|
6481
6825
|
import { homedir as homedir6 } from "os";
|
|
6482
6826
|
import { join as join6 } from "path";
|
|
6483
6827
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -6849,7 +7193,7 @@ async function dockerSafeStart() {
|
|
|
6849
7193
|
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
6850
7194
|
}
|
|
6851
7195
|
const pgCheck = checkPgdata();
|
|
6852
|
-
if (existsSync7(PGDATA_PATH) &&
|
|
7196
|
+
if (existsSync7(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
|
|
6853
7197
|
if (pgCheck.healthy) {
|
|
6854
7198
|
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
6855
7199
|
} else {
|
|
@@ -6890,7 +7234,7 @@ async function dockerSafeRestart() {
|
|
|
6890
7234
|
}
|
|
6891
7235
|
function checkPgdata() {
|
|
6892
7236
|
if (!existsSync7(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
6893
|
-
const entries =
|
|
7237
|
+
const entries = readdirSync2(PGDATA_PATH);
|
|
6894
7238
|
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
6895
7239
|
const hasPidFile = entries.includes("postmaster.pid");
|
|
6896
7240
|
const hasWalDir = entries.includes("pg_wal");
|
|
@@ -7299,7 +7643,7 @@ __export(install_exports, {
|
|
|
7299
7643
|
installCommand: () => installCommand,
|
|
7300
7644
|
parseArgs: () => parseArgs
|
|
7301
7645
|
});
|
|
7302
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as
|
|
7646
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
7303
7647
|
import { homedir as homedir8 } from "os";
|
|
7304
7648
|
import { join as join8 } from "path";
|
|
7305
7649
|
import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
|
|
@@ -7409,6 +7753,19 @@ async function promptStorageMode() {
|
|
|
7409
7753
|
);
|
|
7410
7754
|
});
|
|
7411
7755
|
}
|
|
7756
|
+
async function promptTranscriptConsent() {
|
|
7757
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7758
|
+
return new Promise((resolve3) => {
|
|
7759
|
+
rl.question(
|
|
7760
|
+
"Import and embed your Claude Code session history?\nThis indexes past sessions so Ask Synkro can answer questions\nabout your coding patterns and the dashboard shows full history. (Y/n) ",
|
|
7761
|
+
(answer) => {
|
|
7762
|
+
rl.close();
|
|
7763
|
+
const trimmed = answer.trim().toLowerCase();
|
|
7764
|
+
resolve3(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
7765
|
+
}
|
|
7766
|
+
);
|
|
7767
|
+
});
|
|
7768
|
+
}
|
|
7412
7769
|
function ensureSynkroDir() {
|
|
7413
7770
|
mkdirSync8(SYNKRO_DIR4, { recursive: true });
|
|
7414
7771
|
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
@@ -7532,7 +7889,7 @@ function writeConfigEnv(opts) {
|
|
|
7532
7889
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
7533
7890
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
7534
7891
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
7535
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
7892
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.29")}`
|
|
7536
7893
|
];
|
|
7537
7894
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
7538
7895
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7603,7 +7960,7 @@ function collectLocalMetadata() {
|
|
|
7603
7960
|
}
|
|
7604
7961
|
try {
|
|
7605
7962
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
7606
|
-
const files =
|
|
7963
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
7607
7964
|
for (const f of files) {
|
|
7608
7965
|
const s = JSON.parse(readFileSync7(join8(sessionsDir, f), "utf-8"));
|
|
7609
7966
|
if (s.version) {
|
|
@@ -7756,7 +8113,15 @@ async function installCommand(opts = {}) {
|
|
|
7756
8113
|
} catch {
|
|
7757
8114
|
}
|
|
7758
8115
|
}
|
|
7759
|
-
|
|
8116
|
+
let transcriptConsent = true;
|
|
8117
|
+
if (process.stdin.isTTY) {
|
|
8118
|
+
transcriptConsent = await promptTranscriptConsent();
|
|
8119
|
+
if (transcriptConsent) {
|
|
8120
|
+
console.log(" \u2713 Session import enabled\n");
|
|
8121
|
+
} else {
|
|
8122
|
+
console.log(" \u2717 Session import skipped\n");
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
7760
8125
|
let hasClaudeCode = false;
|
|
7761
8126
|
let hasCursor = false;
|
|
7762
8127
|
for (const agent of agents) {
|
|
@@ -7955,6 +8320,23 @@ async function installCommand(opts = {}) {
|
|
|
7955
8320
|
const ready = await waitForContainerReady(6e4);
|
|
7956
8321
|
if (ready) {
|
|
7957
8322
|
console.log(" \u2713 container ready");
|
|
8323
|
+
const mcpJwt = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8324
|
+
try {
|
|
8325
|
+
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
8326
|
+
method: "POST",
|
|
8327
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpJwt}` },
|
|
8328
|
+
body: JSON.stringify({ capture_type: "local_verdict", event_id: `healthcheck_${Date.now()}`, verdict: "pass", severity: "clean", hook_type: "install", tool_name: "healthcheck" }),
|
|
8329
|
+
signal: AbortSignal.timeout(5e3)
|
|
8330
|
+
});
|
|
8331
|
+
if (ingestResp.ok) {
|
|
8332
|
+
console.log(" \u2713 ingest endpoint verified");
|
|
8333
|
+
} else {
|
|
8334
|
+
console.warn(` \u26A0 ingest endpoint returned ${ingestResp.status} \u2014 telemetry spool may not drain.`);
|
|
8335
|
+
console.warn(" The .mcp-jwt token may not match the container. Try: synkro uninstall && synkro install");
|
|
8336
|
+
}
|
|
8337
|
+
} catch {
|
|
8338
|
+
console.warn(" \u26A0 ingest endpoint unreachable \u2014 telemetry spool may not drain.");
|
|
8339
|
+
}
|
|
7958
8340
|
} else {
|
|
7959
8341
|
console.error(" \u2717 container did not become healthy within 60s");
|
|
7960
8342
|
console.error(" Run `docker logs synkro-server` to debug.");
|
|
@@ -7963,31 +8345,68 @@ async function installCommand(opts = {}) {
|
|
|
7963
8345
|
console.log();
|
|
7964
8346
|
}
|
|
7965
8347
|
if (transcriptConsent) {
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
if (
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
8348
|
+
const repo = detectGitRepo2();
|
|
8349
|
+
if (repo) {
|
|
8350
|
+
if (storageMode === "local") {
|
|
8351
|
+
try {
|
|
8352
|
+
let mcpToken = "";
|
|
8353
|
+
try {
|
|
8354
|
+
mcpToken = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8355
|
+
} catch {
|
|
8356
|
+
}
|
|
8357
|
+
if (mcpToken) {
|
|
8358
|
+
const mcpPort = parseInt(process.env.SYNKRO_MCP_PORT || "18931", 10);
|
|
8359
|
+
const result = await syncTranscriptsLocal(mcpPort, mcpToken, repo);
|
|
8360
|
+
if (result.messages > 0) {
|
|
8361
|
+
console.log(` \u2713 Imported ${result.sessions} sessions (${result.messages} messages) into local store.`);
|
|
8362
|
+
console.log(" Embeddings generated. Analyzing for rule suggestions...");
|
|
8363
|
+
try {
|
|
8364
|
+
const suggestResp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/suggest-rules`, {
|
|
8365
|
+
method: "POST",
|
|
8366
|
+
headers: { "Content-Type": "application/json" },
|
|
8367
|
+
signal: AbortSignal.timeout(9e4)
|
|
8368
|
+
});
|
|
8369
|
+
if (suggestResp.ok) {
|
|
8370
|
+
const suggestResult = await suggestResp.json();
|
|
8371
|
+
if (suggestResult.suggested && suggestResult.suggested > 0) {
|
|
8372
|
+
console.log(` \u2713 Generated ${suggestResult.suggested} rule suggestions from your session history.`);
|
|
8373
|
+
console.log(' Ask Claude: "show me suggested rules" to review them.\n');
|
|
8374
|
+
} else {
|
|
8375
|
+
console.log(" No rule suggestions generated yet \u2014 more session data needed.\n");
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
} catch {
|
|
8379
|
+
console.log(" Rule analysis will run automatically as more sessions are recorded.\n");
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
} else {
|
|
8383
|
+
console.warn(" \u26A0 Session import skipped \u2014 container auth token not found.\n");
|
|
8384
|
+
}
|
|
8385
|
+
} catch (err) {
|
|
8386
|
+
console.warn(` \u26A0 Local session import failed: ${err.message}
|
|
8387
|
+
`);
|
|
7973
8388
|
}
|
|
7974
|
-
}
|
|
7975
|
-
|
|
7976
|
-
|
|
8389
|
+
} else {
|
|
8390
|
+
try {
|
|
8391
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
8392
|
+
if (ingested > 0) {
|
|
8393
|
+
console.log(` \u2713 Indexed ${ingested} session insights from Claude Code history.`);
|
|
8394
|
+
}
|
|
8395
|
+
} catch (err) {
|
|
8396
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
7977
8397
|
`);
|
|
7978
|
-
}
|
|
7979
|
-
try {
|
|
7980
|
-
const repo = detectGitRepo2();
|
|
7981
|
-
if (repo) {
|
|
7982
|
-
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
7983
|
-
if (result.messages > 0) {
|
|
7984
|
-
console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
|
|
7985
|
-
console.log(" This data will be used to suggest guardrail rules.\n");
|
|
7986
8398
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
8399
|
+
try {
|
|
8400
|
+
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
8401
|
+
if (result.messages > 0) {
|
|
8402
|
+
console.log(` \u2713 Synced ${result.sessions} sessions (${result.messages} messages) to cloud.`);
|
|
8403
|
+
console.log(" Embeddings use your configured inference provider.\n");
|
|
8404
|
+
}
|
|
8405
|
+
} catch (err) {
|
|
8406
|
+
console.warn(` \u26A0 Transcript sync skipped: ${err.message}
|
|
7990
8407
|
`);
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
7991
8410
|
}
|
|
7992
8411
|
}
|
|
7993
8412
|
if (ghToken) {
|
|
@@ -8019,7 +8438,7 @@ function getClaudeProjectsFolder() {
|
|
|
8019
8438
|
}
|
|
8020
8439
|
function extractSessionInsights(projectsDir) {
|
|
8021
8440
|
const insights = [];
|
|
8022
|
-
const files =
|
|
8441
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8023
8442
|
for (const file of files) {
|
|
8024
8443
|
const sessionId = file.replace(".jsonl", "");
|
|
8025
8444
|
const filePath = join8(projectsDir, file);
|
|
@@ -8137,10 +8556,52 @@ function parseTranscriptFile(filePath) {
|
|
|
8137
8556
|
}
|
|
8138
8557
|
return messages;
|
|
8139
8558
|
}
|
|
8559
|
+
async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
8560
|
+
const projectsDir = getClaudeProjectsFolder();
|
|
8561
|
+
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
8562
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8563
|
+
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
8564
|
+
console.log(` Found ${files.length} CC session transcripts, importing + embedding...`);
|
|
8565
|
+
let totalSessions = 0;
|
|
8566
|
+
let totalMessages = 0;
|
|
8567
|
+
for (let i = 0; i < files.length; i++) {
|
|
8568
|
+
const file = files[i];
|
|
8569
|
+
const sessionId = file.replace(".jsonl", "");
|
|
8570
|
+
const filePath = join8(projectsDir, file);
|
|
8571
|
+
try {
|
|
8572
|
+
const allMessages = parseTranscriptFile(filePath);
|
|
8573
|
+
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
8574
|
+
if (messages.length === 0) continue;
|
|
8575
|
+
const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/conversation-sync`, {
|
|
8576
|
+
method: "POST",
|
|
8577
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpToken}` },
|
|
8578
|
+
body: JSON.stringify({ session_id: sessionId, repo, messages }),
|
|
8579
|
+
signal: AbortSignal.timeout(15e3)
|
|
8580
|
+
});
|
|
8581
|
+
if (resp.ok) {
|
|
8582
|
+
const result = await resp.json();
|
|
8583
|
+
totalSessions++;
|
|
8584
|
+
totalMessages += result.ingested ?? messages.length;
|
|
8585
|
+
}
|
|
8586
|
+
} catch {
|
|
8587
|
+
}
|
|
8588
|
+
if ((i + 1) % 10 === 0 || i === files.length - 1) {
|
|
8589
|
+
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
8590
|
+
}
|
|
8591
|
+
try {
|
|
8592
|
+
const content = readFileSync7(join8(projectsDir, file), "utf-8");
|
|
8593
|
+
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8594
|
+
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8595
|
+
} catch {
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
if (totalSessions > 0) process.stdout.write("\n");
|
|
8599
|
+
return { sessions: totalSessions, messages: totalMessages };
|
|
8600
|
+
}
|
|
8140
8601
|
async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
8141
8602
|
const projectsDir = getClaudeProjectsFolder();
|
|
8142
8603
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
8143
|
-
const files =
|
|
8604
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8144
8605
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
8145
8606
|
console.log(`Found ${files.length} CC session transcripts, syncing...`);
|
|
8146
8607
|
const maxMessagesPerSession = 500;
|
|
@@ -8751,7 +9212,7 @@ var disconnect_exports = {};
|
|
|
8751
9212
|
__export(disconnect_exports, {
|
|
8752
9213
|
disconnectCommand: () => disconnectCommand
|
|
8753
9214
|
});
|
|
8754
|
-
import { existsSync as existsSync11, rmSync, readdirSync as
|
|
9215
|
+
import { existsSync as existsSync11, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
8755
9216
|
import { homedir as homedir10 } from "os";
|
|
8756
9217
|
import { join as join10 } from "path";
|
|
8757
9218
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -8834,7 +9295,7 @@ async function disconnectCommand(args2 = []) {
|
|
|
8834
9295
|
} else {
|
|
8835
9296
|
const keep = /* @__PURE__ */ new Set([join10(SYNKRO_DIR5, "pgdata"), join10(SYNKRO_DIR5, "pgdata-backups")]);
|
|
8836
9297
|
const preserved = [];
|
|
8837
|
-
for (const entry of
|
|
9298
|
+
for (const entry of readdirSync4(SYNKRO_DIR5)) {
|
|
8838
9299
|
const full = join10(SYNKRO_DIR5, entry);
|
|
8839
9300
|
if (keep.has(full)) {
|
|
8840
9301
|
preserved.push(entry);
|
|
@@ -9140,7 +9601,7 @@ var init_grade = __esm({
|
|
|
9140
9601
|
});
|
|
9141
9602
|
|
|
9142
9603
|
// cli/local-cc/pueue.ts
|
|
9143
|
-
import { execFileSync, spawnSync as spawnSync6, spawn } from "child_process";
|
|
9604
|
+
import { execFileSync as execFileSync2, spawnSync as spawnSync6, spawn } from "child_process";
|
|
9144
9605
|
import { homedir as homedir12 } from "os";
|
|
9145
9606
|
import { join as join12 } from "path";
|
|
9146
9607
|
import { connect as connect2 } from "net";
|
|
@@ -10200,7 +10661,7 @@ var args = process.argv.slice(2);
|
|
|
10200
10661
|
var cmd = args[0] || "";
|
|
10201
10662
|
var subArgs = args.slice(1);
|
|
10202
10663
|
function printVersion() {
|
|
10203
|
-
console.log("1.6.
|
|
10664
|
+
console.log("1.6.29");
|
|
10204
10665
|
}
|
|
10205
10666
|
function printHelp2() {
|
|
10206
10667
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|