@synkro-sh/cli 1.6.28 → 1.6.30
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 +870 -338
- package/dist/bootstrap.js.map +1 -1
- package/package.json +10 -11
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,209 @@ 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 {}
|
|
1909
|
+
// Realtime: fire-and-forget POST to the local server so events appear
|
|
1910
|
+
// in the dashboard immediately. Spool file remains the durable fallback.
|
|
1911
|
+
try {
|
|
1912
|
+
const port = process.env.SYNKRO_MCP_PORT || '18931';
|
|
1913
|
+
let token = '';
|
|
1914
|
+
try { token = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
1915
|
+
if (token) {
|
|
1916
|
+
fetch('http://127.0.0.1:' + port + '/api/ingest', {
|
|
1917
|
+
method: 'POST',
|
|
1918
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
|
|
1919
|
+
body: JSON.stringify(event),
|
|
1920
|
+
signal: AbortSignal.timeout(3000),
|
|
1921
|
+
}).catch(() => {});
|
|
1922
|
+
}
|
|
1923
|
+
} catch {}
|
|
1843
1924
|
}
|
|
1844
1925
|
|
|
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.
|
|
1926
|
+
function tryAcquireDrainLock(): boolean {
|
|
1853
1927
|
try {
|
|
1854
|
-
if (existsSync(
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
claimed.push(claim);
|
|
1928
|
+
if (existsSync(SPOOL_DRAIN_LOCK)) {
|
|
1929
|
+
if (Date.now() - statSync(SPOOL_DRAIN_LOCK).mtimeMs < SPOOL_DRAIN_LOCK_STALE_MS) return false;
|
|
1930
|
+
unlinkSync(SPOOL_DRAIN_LOCK);
|
|
1858
1931
|
}
|
|
1859
|
-
|
|
1932
|
+
mkdirSync(dirname(SPOOL_DRAIN_LOCK), { recursive: true });
|
|
1933
|
+
writeFileSync(SPOOL_DRAIN_LOCK, String(process.pid) + '\\n');
|
|
1934
|
+
return true;
|
|
1935
|
+
} catch {
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1860
1939
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1940
|
+
function releaseDrainLock(): void {
|
|
1941
|
+
try { unlinkSync(SPOOL_DRAIN_LOCK); } catch {}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function postSpoolBatch(mcpPort: string, mcpToken: string, events: any[]): Promise<boolean> {
|
|
1945
|
+
if (!events.length) return true;
|
|
1863
1946
|
try {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1947
|
+
const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/ingest/batch', {
|
|
1948
|
+
method: 'POST',
|
|
1949
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
|
|
1950
|
+
body: JSON.stringify({ events }),
|
|
1951
|
+
signal: AbortSignal.timeout(30000),
|
|
1952
|
+
});
|
|
1953
|
+
if (!resp.ok) {
|
|
1954
|
+
const errBody = await resp.text().catch(() => '');
|
|
1955
|
+
log('drainSpool batch HTTP ' + resp.status + ': ' + errBody.slice(0, 120));
|
|
1871
1956
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1957
|
+
return resp.ok;
|
|
1958
|
+
} catch (e) {
|
|
1959
|
+
log('drainSpool batch error: ' + ((e as Error).message || String(e)).slice(0, 120));
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1874
1963
|
|
|
1875
|
-
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
try { events.push(JSON.parse(t)); } catch {}
|
|
1883
|
-
}
|
|
1884
|
-
} catch {}
|
|
1964
|
+
function quarantineOversizedClaim(claimPath: string): void {
|
|
1965
|
+
const stuck = claimPath + '.STUCK-OVERSIZED.bak';
|
|
1966
|
+
try {
|
|
1967
|
+
renameSync(claimPath, stuck);
|
|
1968
|
+
log('drainSpool quarantined oversized claim \u2192 ' + basename(stuck));
|
|
1969
|
+
} catch (e) {
|
|
1970
|
+
log('drainSpool quarantine failed: ' + String(e));
|
|
1885
1971
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
async function drainClaimFile(claimPath: string, mcpPort: string, mcpToken: string): Promise<void> {
|
|
1975
|
+
let sz = 0;
|
|
1976
|
+
try { sz = statSync(claimPath).size; } catch { return; }
|
|
1977
|
+
if (sz === 0) {
|
|
1978
|
+
try { unlinkSync(claimPath); } catch {}
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (sz > SPOOL_MAX_CLAIM_BYTES) {
|
|
1982
|
+
quarantineOversizedClaim(claimPath);
|
|
1888
1983
|
return;
|
|
1889
1984
|
}
|
|
1890
1985
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
let
|
|
1894
|
-
|
|
1895
|
-
|
|
1986
|
+
const pending: any[] = [];
|
|
1987
|
+
let batch: any[] = [];
|
|
1988
|
+
let failed = false;
|
|
1989
|
+
|
|
1990
|
+
const rl = createInterface({
|
|
1991
|
+
input: createReadStream(claimPath, { encoding: 'utf8' }),
|
|
1992
|
+
crlfDelay: Infinity,
|
|
1993
|
+
});
|
|
1896
1994
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1995
|
+
for await (const line of rl) {
|
|
1996
|
+
if (failed) {
|
|
1997
|
+
const t = line.trim();
|
|
1998
|
+
if (!t) continue;
|
|
1999
|
+
try { pending.push(JSON.parse(t)); } catch {}
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const t = line.trim();
|
|
2003
|
+
if (!t) continue;
|
|
1899
2004
|
try {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2005
|
+
batch.push(JSON.parse(t));
|
|
2006
|
+
} catch {
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
if (batch.length < SPOOL_BATCH_SIZE) continue;
|
|
2010
|
+
const chunk = batch.splice(0, SPOOL_BATCH_SIZE);
|
|
2011
|
+
if (!(await postSpoolBatch(mcpPort, mcpToken, chunk))) {
|
|
2012
|
+
pending.push(...chunk);
|
|
2013
|
+
failed = true;
|
|
2014
|
+
}
|
|
1908
2015
|
}
|
|
1909
2016
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2017
|
+
if (!failed && batch.length > 0) {
|
|
2018
|
+
if (!(await postSpoolBatch(mcpPort, mcpToken, batch))) {
|
|
2019
|
+
pending.push(...batch);
|
|
2020
|
+
failed = true;
|
|
2021
|
+
} else {
|
|
2022
|
+
batch = [];
|
|
2023
|
+
}
|
|
2024
|
+
} else if (failed && batch.length > 0) {
|
|
2025
|
+
pending.push(...batch);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
if (pending.length === 0) {
|
|
2029
|
+
try { unlinkSync(claimPath); } catch {}
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
try {
|
|
2034
|
+
for (const evt of pending) {
|
|
2035
|
+
appendFileSync(TELEMETRY_SPOOL, JSON.stringify(evt) + '\\n');
|
|
2036
|
+
}
|
|
2037
|
+
try { unlinkSync(claimPath); } catch {}
|
|
2038
|
+
log('drainSpool re-spooled ' + pending.length + ' events from ' + basename(claimPath));
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
log('drainSpool re-spool failed: ' + String(e));
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
export async function drainSpool(): Promise<void> {
|
|
2045
|
+
if (!tryAcquireDrainLock()) return;
|
|
2046
|
+
|
|
2047
|
+
const dir = join(HOME, '.synkro');
|
|
2048
|
+
const claimed: string[] = [];
|
|
2049
|
+
|
|
2050
|
+
try {
|
|
2051
|
+
try {
|
|
2052
|
+
if (existsSync(TELEMETRY_SPOOL) && statSync(TELEMETRY_SPOOL).size > 0) {
|
|
2053
|
+
const claim = join(dir, SPOOL_DRAIN_PREFIX + process.pid + '.' + Date.now());
|
|
2054
|
+
renameSync(TELEMETRY_SPOOL, claim);
|
|
2055
|
+
claimed.push(claim);
|
|
2056
|
+
}
|
|
2057
|
+
} catch {}
|
|
2058
|
+
|
|
1914
2059
|
try {
|
|
1915
|
-
|
|
1916
|
-
|
|
2060
|
+
for (const f of readdirSync(dir)) {
|
|
2061
|
+
if (!f.startsWith(SPOOL_DRAIN_PREFIX)) continue;
|
|
2062
|
+
if (f.includes('.STUCK-') || f.endsWith('.bak')) continue;
|
|
2063
|
+
const full = join(dir, f);
|
|
2064
|
+
if (claimed.indexOf(full) !== -1) continue;
|
|
2065
|
+
try {
|
|
2066
|
+
if (Date.now() - statSync(full).mtimeMs > 30000) claimed.push(full);
|
|
2067
|
+
} catch {}
|
|
2068
|
+
}
|
|
1917
2069
|
} catch {}
|
|
1918
|
-
|
|
2070
|
+
|
|
2071
|
+
if (claimed.length === 0) return;
|
|
2072
|
+
|
|
2073
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
2074
|
+
let mcpToken = '';
|
|
2075
|
+
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
2076
|
+
if (!mcpToken) return;
|
|
2077
|
+
|
|
2078
|
+
for (const f of claimed) {
|
|
2079
|
+
await drainClaimFile(f, mcpPort, mcpToken);
|
|
2080
|
+
}
|
|
2081
|
+
} finally {
|
|
2082
|
+
releaseDrainLock();
|
|
1919
2083
|
}
|
|
1920
2084
|
}
|
|
1921
2085
|
|
|
@@ -2708,7 +2872,7 @@ export function emitUsageTick(params: {
|
|
|
2708
2872
|
}
|
|
2709
2873
|
appendLocalTelemetry({
|
|
2710
2874
|
capture_type: 'usage_tick',
|
|
2711
|
-
event_id: '
|
|
2875
|
+
event_id: mintEventId('usage'),
|
|
2712
2876
|
hook_type: hookType,
|
|
2713
2877
|
verdict: 'allow',
|
|
2714
2878
|
severity: 'none',
|
|
@@ -2816,6 +2980,38 @@ export function dispatchFinding(
|
|
|
2816
2980
|
shipCloud(jwt, '/api/v1/hook/finding', cloudBody);
|
|
2817
2981
|
}
|
|
2818
2982
|
|
|
2983
|
+
export function dispatchScanResult(
|
|
2984
|
+
jwt: string,
|
|
2985
|
+
scan: {
|
|
2986
|
+
session_id: string;
|
|
2987
|
+
file_path: string;
|
|
2988
|
+
scan_type: 'cve' | 'cwe' | 'pkg';
|
|
2989
|
+
result: 'pass' | 'block' | 'error';
|
|
2990
|
+
finding_count: number;
|
|
2991
|
+
finding_ids?: string[];
|
|
2992
|
+
severity?: string;
|
|
2993
|
+
repo?: string;
|
|
2994
|
+
},
|
|
2995
|
+
): void {
|
|
2996
|
+
const localEntry: Record<string, any> = {
|
|
2997
|
+
capture_type: 'scan_result',
|
|
2998
|
+
event_id: 'scan_' + Date.now() + '_' + process.pid,
|
|
2999
|
+
_ts: new Date().toISOString(),
|
|
3000
|
+
...scan,
|
|
3001
|
+
};
|
|
3002
|
+
appendLocalTelemetry(localEntry);
|
|
3003
|
+
shipCloud(jwt, '/api/v1/hook/scan-result', {
|
|
3004
|
+
scan_type: scan.scan_type,
|
|
3005
|
+
result: scan.result,
|
|
3006
|
+
finding_count: scan.finding_count,
|
|
3007
|
+
finding_ids: scan.finding_ids,
|
|
3008
|
+
severity: scan.severity,
|
|
3009
|
+
session_id: scan.session_id,
|
|
3010
|
+
file_path: scan.file_path,
|
|
3011
|
+
repo: scan.repo,
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2819
3015
|
// \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
|
|
2820
3016
|
|
|
2821
3017
|
export const EDIT_TOOL_NAMES = new Set([
|
|
@@ -3194,7 +3390,8 @@ main();
|
|
|
3194
3390
|
import {
|
|
3195
3391
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
3196
3392
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3197
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool,
|
|
3393
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3394
|
+
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3198
3395
|
logGraderUnavailable, resolveTranscriptPath,
|
|
3199
3396
|
} from './_synkro-common.ts';
|
|
3200
3397
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
@@ -3215,6 +3412,22 @@ interface PackageCapability {
|
|
|
3215
3412
|
sourceExcerpt: string;
|
|
3216
3413
|
}
|
|
3217
3414
|
|
|
3415
|
+
const NON_CODE_EXTS = new Set([
|
|
3416
|
+
'.md', '.mdx', '.txt', '.rst', '.adoc', '.org',
|
|
3417
|
+
'.log', '.csv', '.tsv', '.html', '.htm',
|
|
3418
|
+
'.lock', '.gitignore', '.dockerignore', '.npmignore',
|
|
3419
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.pdf',
|
|
3420
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
3421
|
+
]);
|
|
3422
|
+
|
|
3423
|
+
interface CweScanTarget {
|
|
3424
|
+
filePath: string;
|
|
3425
|
+
cweContent: string;
|
|
3426
|
+
cweDiffSection: string;
|
|
3427
|
+
toolName: string;
|
|
3428
|
+
toolInput: any;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3218
3431
|
const JS_DANGEROUS_MODULES = new Set([
|
|
3219
3432
|
'child_process', 'net', 'dgram', 'http', 'https', 'fs', 'vm',
|
|
3220
3433
|
'worker_threads', 'cluster', 'dns', 'tls', 'crypto',
|
|
@@ -3322,69 +3535,90 @@ async function main() {
|
|
|
3322
3535
|
|
|
3323
3536
|
const payload = JSON.parse(input);
|
|
3324
3537
|
const toolName = payload.tool_name || '';
|
|
3325
|
-
if (!isEditTool(toolName)) {
|
|
3326
|
-
outputEmpty();
|
|
3327
|
-
return;
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
3538
|
const toolInput = payload.tool_input || {};
|
|
3331
3539
|
const sessionId = hookSessionId(payload);
|
|
3332
3540
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3333
3541
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3334
3542
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
3335
|
-
|
|
3336
|
-
const filePath = filePathFromToolInput(toolInput);
|
|
3337
|
-
if (!filePath) { outputEmpty(); return; }
|
|
3338
|
-
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3543
|
+
const shellCommand = typeof payload.command === 'string' ? payload.command.trim() : '';
|
|
3339
3544
|
const ccModel = detectModel(payload);
|
|
3340
3545
|
|
|
3341
|
-
|
|
3546
|
+
const targets: CweScanTarget[] = [];
|
|
3547
|
+
|
|
3548
|
+
if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
|
|
3549
|
+
const cmd = shellCommand || String(toolInput.command || '');
|
|
3550
|
+
if (!cmd) { outputEmpty(); return; }
|
|
3551
|
+
for (const w of extractShellCodeWrites(cmd, cwd)) {
|
|
3552
|
+
if (w.filePath.includes('/.synkro/hooks/')) continue;
|
|
3553
|
+
const ext = extname(w.filePath).toLowerCase();
|
|
3554
|
+
if (NON_CODE_EXTS.has(ext)) continue;
|
|
3555
|
+
targets.push({
|
|
3556
|
+
filePath: w.filePath,
|
|
3557
|
+
cweContent: w.content,
|
|
3558
|
+
cweDiffSection: '',
|
|
3559
|
+
toolName: toolName || 'Shell',
|
|
3560
|
+
toolInput: {},
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
if (targets.length === 0) { outputEmpty(); return; }
|
|
3564
|
+
} else if (isEditTool(toolName)) {
|
|
3565
|
+
const filePath = filePathFromToolInput(toolInput);
|
|
3566
|
+
if (!filePath) { outputEmpty(); return; }
|
|
3567
|
+
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
3568
|
+
const fileExt = extname(filePath).toLowerCase();
|
|
3569
|
+
if (NON_CODE_EXTS.has(fileExt)) { outputEmpty(); return; }
|
|
3570
|
+
|
|
3571
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
3572
|
+
if (!proposed) { outputEmpty(); return; }
|
|
3573
|
+
|
|
3574
|
+
let cweContent: string;
|
|
3575
|
+
let cweDiffSection = '';
|
|
3576
|
+
if (toolName === 'Edit' || toolName === 'MultiEdit' || toolName === 'edit_file' || toolName === 'reapply' || toolName === 'ApplyPatch' || toolName === 'apply_patch') {
|
|
3577
|
+
const newStr = toolName === 'Edit' || toolName === 'edit_file' || toolName === 'reapply'
|
|
3578
|
+
? (toolInput.new_string || '')
|
|
3579
|
+
: toolName === 'ApplyPatch' || toolName === 'apply_patch'
|
|
3580
|
+
? (toolInput.patch || toolInput.content || toolInput.code_edit || '')
|
|
3581
|
+
: (Array.isArray(toolInput.edits) ? toolInput.edits.map((e: any) => e?.new_string || '').join('\n') : '');
|
|
3582
|
+
cweDiffSection = newStr.slice(0, 4000);
|
|
3583
|
+
const changeIdx = proposed.indexOf(newStr);
|
|
3584
|
+
if (changeIdx >= 0 && proposed.length > 6000) {
|
|
3585
|
+
const start = Math.max(0, changeIdx - 2000);
|
|
3586
|
+
const end = Math.min(proposed.length, changeIdx + newStr.length + 2000);
|
|
3587
|
+
cweContent = proposed.slice(start, end);
|
|
3588
|
+
} else {
|
|
3589
|
+
cweContent = proposed.slice(0, 6000);
|
|
3590
|
+
}
|
|
3591
|
+
} else {
|
|
3592
|
+
cweContent = proposed.slice(0, 4000);
|
|
3593
|
+
}
|
|
3342
3594
|
|
|
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; }
|
|
3595
|
+
targets.push({ filePath, cweContent, cweDiffSection, toolName, toolInput });
|
|
3596
|
+
} else {
|
|
3597
|
+
outputEmpty();
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3357
3600
|
|
|
3358
3601
|
let jwt = loadJwt();
|
|
3359
3602
|
if (!jwt) { outputEmpty(); return; }
|
|
3360
3603
|
jwt = await ensureFreshJwt(jwt);
|
|
3361
3604
|
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3605
|
+
const config = await loadConfig(jwt);
|
|
3606
|
+
const rt = await cweRoute(config);
|
|
3364
3607
|
|
|
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);
|
|
3608
|
+
if (config.silent) {
|
|
3609
|
+
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] skipped (silent mode)' });
|
|
3610
|
+
return;
|
|
3384
3611
|
}
|
|
3385
3612
|
|
|
3386
|
-
const
|
|
3387
|
-
|
|
3613
|
+
for (const scan of targets) {
|
|
3614
|
+
const filePath = scan.filePath;
|
|
3615
|
+
const cweContent = scan.cweContent;
|
|
3616
|
+
const cweDiffSection = scan.cweDiffSection;
|
|
3617
|
+
const scanToolName = scan.toolName;
|
|
3618
|
+
const scanToolInput = scan.toolInput;
|
|
3619
|
+
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3620
|
+
const fileShort = basename(filePath);
|
|
3621
|
+
const fileExt = extname(filePath).toLowerCase();
|
|
3388
3622
|
|
|
3389
3623
|
const exemptedCwes = new Set<string>();
|
|
3390
3624
|
for (const ex of config.scanExemptions) {
|
|
@@ -3392,12 +3626,120 @@ async function main() {
|
|
|
3392
3626
|
exemptedCwes.add(ex.cwe_id.toUpperCase());
|
|
3393
3627
|
}
|
|
3394
3628
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3629
|
+
|
|
3630
|
+
const cweTag = '[synkro:' + rt + ':cweScan]';
|
|
3631
|
+
|
|
3632
|
+
if (rt === 'skip') {
|
|
3633
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 local CWE grader unavailable, skipped' });
|
|
3397
3634
|
return;
|
|
3398
3635
|
}
|
|
3399
3636
|
|
|
3400
|
-
|
|
3637
|
+
if (rt === 'byok') {
|
|
3638
|
+
let packageContext: PackageCapability[] | undefined;
|
|
3639
|
+
if (cwd) {
|
|
3640
|
+
const newImports = detectNewImports(scanToolName, scanToolInput, fileExt);
|
|
3641
|
+
if (newImports.length > 0) {
|
|
3642
|
+
const caps = newImports
|
|
3643
|
+
.slice(0, 5)
|
|
3644
|
+
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
3645
|
+
.filter((c): c is PackageCapability => c !== null);
|
|
3646
|
+
if (caps.length > 0) packageContext = caps;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
const scanBody: any = { file_path: filePath, content: cweContent };
|
|
3650
|
+
if (packageContext) {
|
|
3651
|
+
scanBody.package_context = packageContext.map(c => ({
|
|
3652
|
+
name: c.name, description: c.description, capabilities: c.capabilities, source_excerpt: c.sourceExcerpt,
|
|
3653
|
+
}));
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
let cweResp: any;
|
|
3657
|
+
try {
|
|
3658
|
+
const resp = await fetch(GATEWAY_URL + '/api/v1/cwe-scan', {
|
|
3659
|
+
method: 'POST',
|
|
3660
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3661
|
+
body: JSON.stringify(scanBody),
|
|
3662
|
+
signal: AbortSignal.timeout(12000),
|
|
3663
|
+
});
|
|
3664
|
+
if (!resp.ok) {
|
|
3665
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud CWE scan failed (HTTP ' + resp.status + '), skipped' });
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
cweResp = await resp.json();
|
|
3669
|
+
} catch {
|
|
3670
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \u2192 cloud CWE scan timeout, skipped' });
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
3675
|
+
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
3676
|
+
const activeCweIds = findings
|
|
3677
|
+
.filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
|
|
3678
|
+
.map((f: any) => f.cwe)
|
|
3679
|
+
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
3680
|
+
|
|
3681
|
+
if (activeCweIds.length === 0) {
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
3686
|
+
const count = activeCweIds.length;
|
|
3687
|
+
const label = count === 1 ? 'match' : 'matches';
|
|
3688
|
+
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
3689
|
+
|
|
3690
|
+
const fixLines = findings
|
|
3691
|
+
.filter((f: any) => activeCweIds.includes(f.cwe) && f.suggested_fix)
|
|
3692
|
+
.map((f: any) => '[' + f.cwe + '] Fix: ' + f.suggested_fix);
|
|
3693
|
+
const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
|
|
3694
|
+
const denyDetail = '[' + displayIds + '] ' + (findings[0]?.reason || 'code weakness detected');
|
|
3695
|
+
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.';
|
|
3696
|
+
|
|
3697
|
+
emitBlockScanFindings(
|
|
3698
|
+
jwt,
|
|
3699
|
+
config.captureDepth,
|
|
3700
|
+
{ session_id: sessionId, file_path: filePath, repo: gitRepo || undefined },
|
|
3701
|
+
activeCweIds.map((cweId) => {
|
|
3702
|
+
const f = findings.find((x: any) => x.cwe === cweId);
|
|
3703
|
+
return {
|
|
3704
|
+
finding_type: 'cwe' as const,
|
|
3705
|
+
finding_id: cweId,
|
|
3706
|
+
severity: f?.severity || 'high',
|
|
3707
|
+
detail: f?.reason || 'code weakness detected',
|
|
3708
|
+
cwe_name: f?.name || undefined,
|
|
3709
|
+
};
|
|
3710
|
+
}),
|
|
3711
|
+
{
|
|
3712
|
+
finding_type: 'cwe',
|
|
3713
|
+
finding_id: activeCweIds[0] || 'CWE-UNKNOWN',
|
|
3714
|
+
severity: findings[0]?.severity || 'high',
|
|
3715
|
+
detail: denyDetail,
|
|
3716
|
+
},
|
|
3717
|
+
);
|
|
3718
|
+
|
|
3719
|
+
dispatchCapture(jwt, 'cwe', 'block', findings[0]?.severity || 'high', 'security',
|
|
3720
|
+
scanToolName, gitRepo, sessionId, config.captureDepth, {
|
|
3721
|
+
command: (isShellTool(scanToolName) ? 'shell write ' : 'edit ') + filePath,
|
|
3722
|
+
reasoning: denyDetail,
|
|
3723
|
+
violatedRules: activeCweIds,
|
|
3724
|
+
ccModel,
|
|
3725
|
+
});
|
|
3726
|
+
|
|
3727
|
+
outputJson({
|
|
3728
|
+
systemMessage: cweMsg,
|
|
3729
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3730
|
+
});
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
dispatchFinding(jwt, {
|
|
3735
|
+
session_id: sessionId,
|
|
3736
|
+
file_path: filePath,
|
|
3737
|
+
finding_type: 'cwe',
|
|
3738
|
+
finding_id: 'pass',
|
|
3739
|
+
status: 'resolved',
|
|
3740
|
+
}, config.captureDepth);
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3401
3743
|
|
|
3402
3744
|
if (rt === 'local') {
|
|
3403
3745
|
let cweRules: any[] = [];
|
|
@@ -3431,7 +3773,7 @@ async function main() {
|
|
|
3431
3773
|
|
|
3432
3774
|
let localPkgContext = '';
|
|
3433
3775
|
if (cwd) {
|
|
3434
|
-
const newImports = detectNewImports(
|
|
3776
|
+
const newImports = detectNewImports(scanToolName, scanToolInput, fileExt);
|
|
3435
3777
|
const caps = newImports.slice(0, 5)
|
|
3436
3778
|
.map(pkg => scanPackageCapabilities(pkg, cwd))
|
|
3437
3779
|
.filter((c): c is PackageCapability => c !== null);
|
|
@@ -3526,8 +3868,7 @@ async function main() {
|
|
|
3526
3868
|
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
3527
3869
|
|
|
3528
3870
|
if (activeCweIds.length === 0) {
|
|
3529
|
-
|
|
3530
|
-
return;
|
|
3871
|
+
continue;
|
|
3531
3872
|
}
|
|
3532
3873
|
|
|
3533
3874
|
const cweNameMap = new Map<string, string>();
|
|
@@ -3566,8 +3907,8 @@ async function main() {
|
|
|
3566
3907
|
);
|
|
3567
3908
|
|
|
3568
3909
|
dispatchCapture(jwt, 'cwe', 'block', verdict.severity || 'high', verdict.category || 'security',
|
|
3569
|
-
|
|
3570
|
-
command: 'edit ' + filePath,
|
|
3910
|
+
scanToolName, gitRepo, sessionId, config.captureDepth, {
|
|
3911
|
+
command: (isShellTool(scanToolName) ? 'shell write ' : 'edit ') + filePath,
|
|
3571
3912
|
reasoning: denyDetail,
|
|
3572
3913
|
violatedRules: activeCweIds,
|
|
3573
3914
|
ccModel,
|
|
@@ -3587,114 +3928,11 @@ async function main() {
|
|
|
3587
3928
|
finding_id: 'pass',
|
|
3588
3929
|
status: 'resolved',
|
|
3589
3930
|
}, 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;
|
|
3594
|
-
}
|
|
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;
|
|
3931
|
+
continue;
|
|
3625
3932
|
}
|
|
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
3933
|
}
|
|
3687
3934
|
|
|
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 } });
|
|
3935
|
+
outputEmpty();
|
|
3698
3936
|
} catch (err) {
|
|
3699
3937
|
log('cweGuard error: ' + String(err));
|
|
3700
3938
|
outputEmpty();
|
|
@@ -3707,7 +3945,7 @@ main();
|
|
|
3707
3945
|
import {
|
|
3708
3946
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3709
3947
|
reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
|
|
3710
|
-
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
3948
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
3711
3949
|
} from './_synkro-common.ts';
|
|
3712
3950
|
import { basename } from 'node:path';
|
|
3713
3951
|
import { readFileSync } from 'node:fs';
|
|
@@ -3858,6 +4096,12 @@ async function main() {
|
|
|
3858
4096
|
violatedRules: violatedIds,
|
|
3859
4097
|
ccModel,
|
|
3860
4098
|
});
|
|
4099
|
+
dispatchScanResult(jwt, {
|
|
4100
|
+
session_id: sessionId, file_path: filePath, scan_type: 'pkg',
|
|
4101
|
+
result: 'block', finding_count: violatedIds.length,
|
|
4102
|
+
finding_ids: violatedIds, severity: 'critical',
|
|
4103
|
+
repo: gitRepo || undefined,
|
|
4104
|
+
});
|
|
3861
4105
|
const tagStr = '[synkro:' + rt + ':pkgScan]';
|
|
3862
4106
|
const denyReason = tagStr + ' BLOCKED: ' + summary + '\\nDo not write this version. Pick a fixed/safe version instead.';
|
|
3863
4107
|
outputJson({
|
|
@@ -3936,12 +4180,23 @@ async function main() {
|
|
|
3936
4180
|
const cveIds = findings.slice(0, 10).map((f: any) =>
|
|
3937
4181
|
(f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown'
|
|
3938
4182
|
);
|
|
4183
|
+
let cveCcModel: string | undefined = String(payload.model ?? payload.model_id ?? '') || undefined;
|
|
4184
|
+
if (!cveCcModel && transcriptPath) {
|
|
4185
|
+
try { cveCcModel = extractTranscript(transcriptPath).ccModel || undefined; } catch {}
|
|
4186
|
+
}
|
|
3939
4187
|
dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
|
|
3940
4188
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3941
4189
|
command: 'edit ' + filePath,
|
|
3942
4190
|
reasoning: top3,
|
|
3943
4191
|
violatedRules: cveIds,
|
|
4192
|
+
ccModel: cveCcModel,
|
|
3944
4193
|
});
|
|
4194
|
+
dispatchScanResult(jwt, {
|
|
4195
|
+
session_id: sessionId, file_path: filePath, scan_type: 'cve',
|
|
4196
|
+
result: 'block', finding_count: findings.length,
|
|
4197
|
+
finding_ids: cveIds, severity: 'critical',
|
|
4198
|
+
repo: gitRepo || undefined,
|
|
4199
|
+
});
|
|
3945
4200
|
|
|
3946
4201
|
outputJson({
|
|
3947
4202
|
systemMessage: cveMsg,
|
|
@@ -3950,6 +4205,11 @@ async function main() {
|
|
|
3950
4205
|
return;
|
|
3951
4206
|
}
|
|
3952
4207
|
|
|
4208
|
+
dispatchScanResult(jwt, {
|
|
4209
|
+
session_id: sessionId, file_path: filePath, scan_type: 'cve',
|
|
4210
|
+
result: 'pass', finding_count: 0,
|
|
4211
|
+
repo: gitRepo || undefined,
|
|
4212
|
+
});
|
|
3953
4213
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \u2192 clean' });
|
|
3954
4214
|
} catch (err) {
|
|
3955
4215
|
log('cveGuard error: ' + String(err));
|
|
@@ -3962,7 +4222,7 @@ main();
|
|
|
3962
4222
|
INSTALL_SCAN_TS = `#!/usr/bin/env bun
|
|
3963
4223
|
import {
|
|
3964
4224
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3965
|
-
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, hashCommand,
|
|
4225
|
+
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
3966
4226
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
3967
4227
|
resolveTranscriptPath, isCursorHookFormat,
|
|
3968
4228
|
} from './_synkro-common.ts';
|
|
@@ -4045,6 +4305,12 @@ async function main() {
|
|
|
4045
4305
|
violatedRules: scan.violatedIds,
|
|
4046
4306
|
ccModel: model || undefined,
|
|
4047
4307
|
});
|
|
4308
|
+
dispatchScanResult(jwt, {
|
|
4309
|
+
session_id: sessionId, file_path: command, scan_type: 'pkg',
|
|
4310
|
+
result: 'block', finding_count: scan.violatedIds?.length || 1,
|
|
4311
|
+
finding_ids: scan.violatedIds, severity: 'critical',
|
|
4312
|
+
repo: repo || undefined,
|
|
4313
|
+
});
|
|
4048
4314
|
const denyReason = '[synkro:installScan] BLOCKED: ' + scan.summary + '\\nDo not retry this install. Suggest a safe version to the user instead.';
|
|
4049
4315
|
outputJson({
|
|
4050
4316
|
systemMessage: denyReason,
|
|
@@ -5339,10 +5605,10 @@ import {
|
|
|
5339
5605
|
loadJwt, ensureFreshJwt, detectRepo, readStdin, resolveTranscriptPath,
|
|
5340
5606
|
appendSessionAction, appendLocalTelemetry, shipCloud, log, GATEWAY_URL,
|
|
5341
5607
|
countEditLineDelta, dispatchCapture, hookSessionId, cursorModelFromPayload,
|
|
5608
|
+
isLocalStorageMode,
|
|
5342
5609
|
} from './_synkro-common.ts';
|
|
5343
5610
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5344
5611
|
import { basename, dirname, join } from 'node:path';
|
|
5345
|
-
import { homedir } from 'node:os';
|
|
5346
5612
|
|
|
5347
5613
|
let hookDone = false;
|
|
5348
5614
|
|
|
@@ -5391,16 +5657,8 @@ async function main() {
|
|
|
5391
5657
|
if (!jwt) finish();
|
|
5392
5658
|
jwt = await ensureFreshJwt(jwt);
|
|
5393
5659
|
|
|
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
5660
|
let dependencies: Record<string, string> = {};
|
|
5661
|
+
const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
|
|
5404
5662
|
let pkgDir = cwd || dirname(fullPath);
|
|
5405
5663
|
while (pkgDir !== '/' && pkgDir !== '.') {
|
|
5406
5664
|
const pkgPath = join(pkgDir, 'package.json');
|
|
@@ -5416,20 +5674,39 @@ async function main() {
|
|
|
5416
5674
|
pkgDir = parent;
|
|
5417
5675
|
}
|
|
5418
5676
|
|
|
5419
|
-
const
|
|
5677
|
+
const localSpoolBody: Record<string, any> = {
|
|
5420
5678
|
capture_type: 'edit_scan',
|
|
5421
|
-
tool_input: { file_path: filePath,
|
|
5679
|
+
tool_input: { file_path: filePath, lines_added: linesAdded, lines_removed: linesRemoved },
|
|
5422
5680
|
edit_verdict: { ok: true },
|
|
5423
5681
|
dependencies,
|
|
5424
5682
|
cc_model: model,
|
|
5425
5683
|
model,
|
|
5426
5684
|
};
|
|
5427
|
-
if (sessionId)
|
|
5428
|
-
if (cwd)
|
|
5429
|
-
if (repo)
|
|
5685
|
+
if (sessionId) localSpoolBody.session_id = sessionId;
|
|
5686
|
+
if (cwd) localSpoolBody.cwd = cwd;
|
|
5687
|
+
if (repo) localSpoolBody.repo = repo;
|
|
5688
|
+
appendLocalTelemetry(localSpoolBody);
|
|
5430
5689
|
|
|
5431
|
-
|
|
5432
|
-
|
|
5690
|
+
if (!isLocalStorageMode()) {
|
|
5691
|
+
let fileContent = '';
|
|
5692
|
+
try {
|
|
5693
|
+
if (existsSync(fullPath)) {
|
|
5694
|
+
fileContent = readFileSync(fullPath).slice(0, 50000).toString('utf-8');
|
|
5695
|
+
}
|
|
5696
|
+
} catch {}
|
|
5697
|
+
const cloudBody: Record<string, any> = {
|
|
5698
|
+
capture_type: 'edit_scan',
|
|
5699
|
+
tool_input: { file_path: filePath, content: fileContent },
|
|
5700
|
+
edit_verdict: { ok: true },
|
|
5701
|
+
dependencies,
|
|
5702
|
+
cc_model: model,
|
|
5703
|
+
model,
|
|
5704
|
+
};
|
|
5705
|
+
if (sessionId) cloudBody.session_id = sessionId;
|
|
5706
|
+
if (cwd) cloudBody.cwd = cwd;
|
|
5707
|
+
if (repo) cloudBody.repo = repo;
|
|
5708
|
+
shipCloud(jwt, '/api/v1/hook/capture', cloudBody, 10000);
|
|
5709
|
+
}
|
|
5433
5710
|
|
|
5434
5711
|
if (sessionId) {
|
|
5435
5712
|
dispatchCapture(jwt, 'edit', 'pass', 'clean', 'trivial_edit', 'Edit', repo, sessionId, 'full', {
|
|
@@ -5497,6 +5774,21 @@ main();
|
|
|
5497
5774
|
});
|
|
5498
5775
|
|
|
5499
5776
|
// cli/auth/stub.ts
|
|
5777
|
+
var stub_exports = {};
|
|
5778
|
+
__export(stub_exports, {
|
|
5779
|
+
authenticate: () => authenticate,
|
|
5780
|
+
clearCredentials: () => clearCredentials,
|
|
5781
|
+
ensureValidToken: () => ensureValidToken,
|
|
5782
|
+
getAccessToken: () => getAccessToken,
|
|
5783
|
+
getCurrentUserId: () => getCurrentUserId,
|
|
5784
|
+
getSecrets: () => getSecrets,
|
|
5785
|
+
getUserInfo: () => getUserInfo,
|
|
5786
|
+
isAuthenticated: () => isAuthenticated,
|
|
5787
|
+
isTokenExpired: () => isTokenExpired,
|
|
5788
|
+
loadCredentials: () => loadCredentials,
|
|
5789
|
+
refreshToken: () => refreshToken,
|
|
5790
|
+
saveCredentials: () => saveCredentials
|
|
5791
|
+
});
|
|
5500
5792
|
import { createServer } from "http";
|
|
5501
5793
|
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
5502
5794
|
import { homedir as homedir4, platform } from "os";
|
|
@@ -5703,6 +5995,17 @@ function isAuthenticated() {
|
|
|
5703
5995
|
return true;
|
|
5704
5996
|
}
|
|
5705
5997
|
}
|
|
5998
|
+
function getCurrentUserId() {
|
|
5999
|
+
const creds = loadCredentials();
|
|
6000
|
+
if (!creds) {
|
|
6001
|
+
throw new Error("Not authenticated");
|
|
6002
|
+
}
|
|
6003
|
+
const decoded = jwt.decode(creds.access_token);
|
|
6004
|
+
if (!decoded?.sub) {
|
|
6005
|
+
throw new Error("Invalid token");
|
|
6006
|
+
}
|
|
6007
|
+
return decoded.sub;
|
|
6008
|
+
}
|
|
5706
6009
|
function getUserInfo() {
|
|
5707
6010
|
const creds = loadCredentials();
|
|
5708
6011
|
if (!creds) {
|
|
@@ -5787,6 +6090,15 @@ function clearCredentials() {
|
|
|
5787
6090
|
unlinkSync2(AUTH_FILE);
|
|
5788
6091
|
}
|
|
5789
6092
|
}
|
|
6093
|
+
async function getSecrets(userId, integrationId) {
|
|
6094
|
+
return {
|
|
6095
|
+
AWS_ACCESS_KEY_ID: process.env.USER_AWS_KEY || "",
|
|
6096
|
+
AWS_SECRET_ACCESS_KEY: process.env.USER_AWS_SECRET || "",
|
|
6097
|
+
AWS_REGION: process.env.USER_AWS_REGION || "us-east-1",
|
|
6098
|
+
HF_TOKEN: process.env.USER_HF_TOKEN || "",
|
|
6099
|
+
LANGSMITH_API_KEY: process.env.USER_LANGSMITH_KEY || ""
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
5790
6102
|
var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, RAW_API_URL, SYNKRO_API_URL, ERROR_HTML, refreshPromise;
|
|
5791
6103
|
var init_stub = __esm({
|
|
5792
6104
|
"cli/auth/stub.ts"() {
|
|
@@ -6032,12 +6344,13 @@ var init_githubSetup = __esm({
|
|
|
6032
6344
|
});
|
|
6033
6345
|
|
|
6034
6346
|
// cli/commands/repoConnect.ts
|
|
6035
|
-
import { execSync as execSync3 } from "child_process";
|
|
6347
|
+
import { execSync as execSync3, execFileSync } from "child_process";
|
|
6348
|
+
import { readdirSync } from "fs";
|
|
6036
6349
|
import { createServer as createServer2 } from "http";
|
|
6037
6350
|
import { createInterface } from "readline";
|
|
6038
6351
|
function detectGitRepo() {
|
|
6039
6352
|
try {
|
|
6040
|
-
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
6353
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
6041
6354
|
const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
6042
6355
|
const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
6043
6356
|
const match = sshMatch || httpMatch;
|
|
@@ -6048,6 +6361,33 @@ function detectGitRepo() {
|
|
|
6048
6361
|
return null;
|
|
6049
6362
|
}
|
|
6050
6363
|
}
|
|
6364
|
+
function detectSubdirRepos() {
|
|
6365
|
+
try {
|
|
6366
|
+
const entries = readdirSync(".", { withFileTypes: true });
|
|
6367
|
+
const repos = [];
|
|
6368
|
+
for (const entry of entries) {
|
|
6369
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
6370
|
+
try {
|
|
6371
|
+
const remoteUrl = execFileSync("git", ["-C", entry.name, "remote", "get-url", "origin"], {
|
|
6372
|
+
encoding: "utf-8",
|
|
6373
|
+
timeout: 5e3,
|
|
6374
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6375
|
+
}).trim();
|
|
6376
|
+
const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
6377
|
+
const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
6378
|
+
const match = sshMatch || httpMatch;
|
|
6379
|
+
if (match) {
|
|
6380
|
+
const fullName = match[1];
|
|
6381
|
+
repos.push({ fullName, shortName: fullName.split("/").pop() || fullName });
|
|
6382
|
+
}
|
|
6383
|
+
} catch {
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
return repos;
|
|
6387
|
+
} catch {
|
|
6388
|
+
return [];
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6051
6391
|
function ask(rl, question) {
|
|
6052
6392
|
return new Promise((resolve3) => rl.question(question, resolve3));
|
|
6053
6393
|
}
|
|
@@ -6149,6 +6489,7 @@ async function connectGithubAndSelectRepos() {
|
|
|
6149
6489
|
}
|
|
6150
6490
|
async function promptRepoConnection(opts) {
|
|
6151
6491
|
const localRepo = detectGitRepo();
|
|
6492
|
+
const subdirRepos = !localRepo ? detectSubdirRepos() : [];
|
|
6152
6493
|
if (opts?.linkRepo && localRepo) {
|
|
6153
6494
|
console.log("Connect repos to Synkro:\n");
|
|
6154
6495
|
try {
|
|
@@ -6171,46 +6512,35 @@ async function promptRepoConnection(opts) {
|
|
|
6171
6512
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6172
6513
|
try {
|
|
6173
6514
|
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) {
|
|
6515
|
+
let existingFullNames = /* @__PURE__ */ new Set();
|
|
6516
|
+
if (subdirRepos.length > 0) {
|
|
6192
6517
|
try {
|
|
6193
6518
|
const existing = await listProjects();
|
|
6194
|
-
|
|
6195
|
-
(p) => p.repos
|
|
6519
|
+
existingFullNames = new Set(
|
|
6520
|
+
existing.flatMap((p) => (p.repos || []).map((r) => r.full_name))
|
|
6196
6521
|
);
|
|
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}`);
|
|
6522
|
+
} catch {
|
|
6205
6523
|
}
|
|
6206
|
-
}
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6524
|
+
}
|
|
6525
|
+
if (subdirRepos.length > 0) {
|
|
6526
|
+
console.log(` Found ${subdirRepos.length} repo(s) in this directory:
|
|
6527
|
+
`);
|
|
6528
|
+
subdirRepos.forEach((r, i) => {
|
|
6529
|
+
const linked = existingFullNames.has(r.fullName);
|
|
6530
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.fullName}${linked ? " \u2713 linked" : ""}`);
|
|
6531
|
+
});
|
|
6532
|
+
const ghIdx = subdirRepos.length + 1;
|
|
6533
|
+
const skipIdx = subdirRepos.length + 2;
|
|
6534
|
+
console.log(` ${String(ghIdx).padStart(3)}. Connect GitHub to select repos`);
|
|
6535
|
+
console.log(` ${String(skipIdx).padStart(3)}. Skip for now`);
|
|
6536
|
+
console.log();
|
|
6537
|
+
const selection = await ask(rl, " Select repos to link (comma-separated numbers, e.g. 1,3): ");
|
|
6538
|
+
const nums = selection.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
|
6539
|
+
console.log();
|
|
6540
|
+
rl.close();
|
|
6541
|
+
if (nums.includes(ghIdx)) {
|
|
6542
|
+
const selectedRepos = await connectGithubAndSelectRepos();
|
|
6543
|
+
if (selectedRepos.length > 0) {
|
|
6214
6544
|
const newRepos = selectedRepos.filter((r) => !existingFullNames.has(r.full_name));
|
|
6215
6545
|
if (newRepos.length === 0) {
|
|
6216
6546
|
console.log(" \u2713 All selected repos are already linked.");
|
|
@@ -6219,14 +6549,83 @@ async function promptRepoConnection(opts) {
|
|
|
6219
6549
|
await createProject(projectName, newRepos);
|
|
6220
6550
|
console.log(` \u2713 Linked ${newRepos.length} repo(s) to project "${projectName}"`);
|
|
6221
6551
|
}
|
|
6222
|
-
}
|
|
6223
|
-
|
|
6552
|
+
}
|
|
6553
|
+
} else if (nums.includes(skipIdx) || nums.length === 0) {
|
|
6554
|
+
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6555
|
+
} else {
|
|
6556
|
+
const repoIndices = nums.map((n) => n - 1).filter((n) => n >= 0 && n < subdirRepos.length);
|
|
6557
|
+
const toLink = repoIndices.map((i) => subdirRepos[i]).filter((r) => !existingFullNames.has(r.fullName));
|
|
6558
|
+
if (toLink.length === 0) {
|
|
6559
|
+
console.log(" \u2713 All selected repos are already linked.");
|
|
6560
|
+
} else {
|
|
6561
|
+
for (const repo of toLink) {
|
|
6562
|
+
try {
|
|
6563
|
+
await createProject(repo.shortName, [{ full_name: repo.fullName }]);
|
|
6564
|
+
console.log(` \u2713 Created project "${repo.shortName}" linked to ${repo.fullName}`);
|
|
6565
|
+
} catch (err) {
|
|
6566
|
+
console.warn(` \u26A0 Could not link ${repo.fullName}: ${err.message}`);
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6224
6569
|
}
|
|
6225
6570
|
}
|
|
6226
|
-
} else if (choiceNum === skipIdx) {
|
|
6227
|
-
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6228
6571
|
} else {
|
|
6229
|
-
|
|
6572
|
+
const options = [];
|
|
6573
|
+
if (localRepo) {
|
|
6574
|
+
options.push(`Link this repo (${localRepo.fullName})`);
|
|
6575
|
+
}
|
|
6576
|
+
options.push("Connect GitHub to select repos");
|
|
6577
|
+
options.push("Skip for now");
|
|
6578
|
+
options.forEach((opt, i) => {
|
|
6579
|
+
console.log(` ${i + 1}. ${opt}`);
|
|
6580
|
+
});
|
|
6581
|
+
console.log();
|
|
6582
|
+
const choice = await ask(rl, " Choose (number): ");
|
|
6583
|
+
const choiceNum = parseInt(choice.trim(), 10);
|
|
6584
|
+
console.log();
|
|
6585
|
+
rl.close();
|
|
6586
|
+
const localIdx = localRepo ? 1 : -1;
|
|
6587
|
+
const githubIdx = localRepo ? 2 : 1;
|
|
6588
|
+
const skipIdx = localRepo ? 3 : 2;
|
|
6589
|
+
if (choiceNum === localIdx && localRepo) {
|
|
6590
|
+
try {
|
|
6591
|
+
const existing = await listProjects();
|
|
6592
|
+
const alreadyLinked = existing.some(
|
|
6593
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
6594
|
+
);
|
|
6595
|
+
if (!alreadyLinked) {
|
|
6596
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
6597
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
6598
|
+
} else {
|
|
6599
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
6600
|
+
}
|
|
6601
|
+
} catch (err) {
|
|
6602
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
6603
|
+
}
|
|
6604
|
+
} else if (choiceNum === githubIdx) {
|
|
6605
|
+
const selectedRepos = await connectGithubAndSelectRepos();
|
|
6606
|
+
if (selectedRepos.length > 0) {
|
|
6607
|
+
try {
|
|
6608
|
+
const existing = await listProjects();
|
|
6609
|
+
const linkedNames = new Set(
|
|
6610
|
+
existing.flatMap((p) => (p.repos || []).map((r) => r.full_name))
|
|
6611
|
+
);
|
|
6612
|
+
const newRepos = selectedRepos.filter((r) => !linkedNames.has(r.full_name));
|
|
6613
|
+
if (newRepos.length === 0) {
|
|
6614
|
+
console.log(" \u2713 All selected repos are already linked.");
|
|
6615
|
+
} else {
|
|
6616
|
+
const projectName = newRepos.length === 1 ? newRepos[0].full_name.split("/").pop() || "Project" : "Multi-Repo Project";
|
|
6617
|
+
await createProject(projectName, newRepos);
|
|
6618
|
+
console.log(` \u2713 Linked ${newRepos.length} repo(s) to project "${projectName}"`);
|
|
6619
|
+
}
|
|
6620
|
+
} catch (err) {
|
|
6621
|
+
console.warn(` \u26A0 Could not link repos: ${err.message}`);
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
} else if (choiceNum === skipIdx) {
|
|
6625
|
+
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
6626
|
+
} else {
|
|
6627
|
+
console.log(" Invalid choice. Skipping repo connection.");
|
|
6628
|
+
}
|
|
6230
6629
|
}
|
|
6231
6630
|
} catch {
|
|
6232
6631
|
rl.close();
|
|
@@ -6477,7 +6876,7 @@ __export(dockerInstall_exports, {
|
|
|
6477
6876
|
splitWorkers: () => splitWorkers,
|
|
6478
6877
|
waitForContainerReady: () => waitForContainerReady
|
|
6479
6878
|
});
|
|
6480
|
-
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync } from "fs";
|
|
6879
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2 } from "fs";
|
|
6481
6880
|
import { homedir as homedir6 } from "os";
|
|
6482
6881
|
import { join as join6 } from "path";
|
|
6483
6882
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -6849,7 +7248,7 @@ async function dockerSafeStart() {
|
|
|
6849
7248
|
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
6850
7249
|
}
|
|
6851
7250
|
const pgCheck = checkPgdata();
|
|
6852
|
-
if (existsSync7(PGDATA_PATH) &&
|
|
7251
|
+
if (existsSync7(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
|
|
6853
7252
|
if (pgCheck.healthy) {
|
|
6854
7253
|
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
6855
7254
|
} else {
|
|
@@ -6890,7 +7289,7 @@ async function dockerSafeRestart() {
|
|
|
6890
7289
|
}
|
|
6891
7290
|
function checkPgdata() {
|
|
6892
7291
|
if (!existsSync7(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
6893
|
-
const entries =
|
|
7292
|
+
const entries = readdirSync2(PGDATA_PATH);
|
|
6894
7293
|
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
6895
7294
|
const hasPidFile = entries.includes("postmaster.pid");
|
|
6896
7295
|
const hasWalDir = entries.includes("pg_wal");
|
|
@@ -7299,7 +7698,7 @@ __export(install_exports, {
|
|
|
7299
7698
|
installCommand: () => installCommand,
|
|
7300
7699
|
parseArgs: () => parseArgs
|
|
7301
7700
|
});
|
|
7302
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as
|
|
7701
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
7303
7702
|
import { homedir as homedir8 } from "os";
|
|
7304
7703
|
import { join as join8 } from "path";
|
|
7305
7704
|
import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
|
|
@@ -7409,6 +7808,19 @@ async function promptStorageMode() {
|
|
|
7409
7808
|
);
|
|
7410
7809
|
});
|
|
7411
7810
|
}
|
|
7811
|
+
async function promptTranscriptConsent() {
|
|
7812
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7813
|
+
return new Promise((resolve3) => {
|
|
7814
|
+
rl.question(
|
|
7815
|
+
"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) ",
|
|
7816
|
+
(answer) => {
|
|
7817
|
+
rl.close();
|
|
7818
|
+
const trimmed = answer.trim().toLowerCase();
|
|
7819
|
+
resolve3(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
7820
|
+
}
|
|
7821
|
+
);
|
|
7822
|
+
});
|
|
7823
|
+
}
|
|
7412
7824
|
function ensureSynkroDir() {
|
|
7413
7825
|
mkdirSync8(SYNKRO_DIR4, { recursive: true });
|
|
7414
7826
|
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
@@ -7532,7 +7944,7 @@ function writeConfigEnv(opts) {
|
|
|
7532
7944
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
7533
7945
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
7534
7946
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
7535
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
7947
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.30")}`
|
|
7536
7948
|
];
|
|
7537
7949
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
7538
7950
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7603,7 +8015,7 @@ function collectLocalMetadata() {
|
|
|
7603
8015
|
}
|
|
7604
8016
|
try {
|
|
7605
8017
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
7606
|
-
const files =
|
|
8018
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
7607
8019
|
for (const f of files) {
|
|
7608
8020
|
const s = JSON.parse(readFileSync7(join8(sessionsDir, f), "utf-8"));
|
|
7609
8021
|
if (s.version) {
|
|
@@ -7756,7 +8168,15 @@ async function installCommand(opts = {}) {
|
|
|
7756
8168
|
} catch {
|
|
7757
8169
|
}
|
|
7758
8170
|
}
|
|
7759
|
-
|
|
8171
|
+
let transcriptConsent = true;
|
|
8172
|
+
if (process.stdin.isTTY) {
|
|
8173
|
+
transcriptConsent = await promptTranscriptConsent();
|
|
8174
|
+
if (transcriptConsent) {
|
|
8175
|
+
console.log(" \u2713 Session import enabled\n");
|
|
8176
|
+
} else {
|
|
8177
|
+
console.log(" \u2717 Session import skipped\n");
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
7760
8180
|
let hasClaudeCode = false;
|
|
7761
8181
|
let hasCursor = false;
|
|
7762
8182
|
for (const agent of agents) {
|
|
@@ -7955,6 +8375,23 @@ async function installCommand(opts = {}) {
|
|
|
7955
8375
|
const ready = await waitForContainerReady(6e4);
|
|
7956
8376
|
if (ready) {
|
|
7957
8377
|
console.log(" \u2713 container ready");
|
|
8378
|
+
const mcpJwt = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8379
|
+
try {
|
|
8380
|
+
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
8381
|
+
method: "POST",
|
|
8382
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpJwt}` },
|
|
8383
|
+
body: JSON.stringify({ capture_type: "healthcheck", event_id: `healthcheck_${Date.now()}` }),
|
|
8384
|
+
signal: AbortSignal.timeout(5e3)
|
|
8385
|
+
});
|
|
8386
|
+
if (ingestResp.ok) {
|
|
8387
|
+
console.log(" \u2713 ingest endpoint verified");
|
|
8388
|
+
} else {
|
|
8389
|
+
console.warn(` \u26A0 ingest endpoint returned ${ingestResp.status} \u2014 telemetry spool may not drain.`);
|
|
8390
|
+
console.warn(" The .mcp-jwt token may not match the container. Try: synkro uninstall && synkro install");
|
|
8391
|
+
}
|
|
8392
|
+
} catch {
|
|
8393
|
+
console.warn(" \u26A0 ingest endpoint unreachable \u2014 telemetry spool may not drain.");
|
|
8394
|
+
}
|
|
7958
8395
|
} else {
|
|
7959
8396
|
console.error(" \u2717 container did not become healthy within 60s");
|
|
7960
8397
|
console.error(" Run `docker logs synkro-server` to debug.");
|
|
@@ -7963,31 +8400,68 @@ async function installCommand(opts = {}) {
|
|
|
7963
8400
|
console.log();
|
|
7964
8401
|
}
|
|
7965
8402
|
if (transcriptConsent) {
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
if (
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
8403
|
+
const repo = detectGitRepo2();
|
|
8404
|
+
if (repo) {
|
|
8405
|
+
if (storageMode === "local") {
|
|
8406
|
+
try {
|
|
8407
|
+
let mcpToken = "";
|
|
8408
|
+
try {
|
|
8409
|
+
mcpToken = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8410
|
+
} catch {
|
|
8411
|
+
}
|
|
8412
|
+
if (mcpToken) {
|
|
8413
|
+
const mcpPort = parseInt(process.env.SYNKRO_MCP_PORT || "18931", 10);
|
|
8414
|
+
const result = await syncTranscriptsLocal(mcpPort, mcpToken, repo);
|
|
8415
|
+
if (result.messages > 0) {
|
|
8416
|
+
console.log(` \u2713 Imported ${result.sessions} sessions (${result.messages} messages) into local store.`);
|
|
8417
|
+
console.log(" Embeddings generated. Analyzing for rule suggestions...");
|
|
8418
|
+
try {
|
|
8419
|
+
const suggestResp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/suggest-rules`, {
|
|
8420
|
+
method: "POST",
|
|
8421
|
+
headers: { "Content-Type": "application/json" },
|
|
8422
|
+
signal: AbortSignal.timeout(9e4)
|
|
8423
|
+
});
|
|
8424
|
+
if (suggestResp.ok) {
|
|
8425
|
+
const suggestResult = await suggestResp.json();
|
|
8426
|
+
if (suggestResult.suggested && suggestResult.suggested > 0) {
|
|
8427
|
+
console.log(` \u2713 Generated ${suggestResult.suggested} rule suggestions from your session history.`);
|
|
8428
|
+
console.log(' Ask Claude: "show me suggested rules" to review them.\n');
|
|
8429
|
+
} else {
|
|
8430
|
+
console.log(" No rule suggestions generated yet \u2014 more session data needed.\n");
|
|
8431
|
+
}
|
|
8432
|
+
}
|
|
8433
|
+
} catch {
|
|
8434
|
+
console.log(" Rule analysis will run automatically as more sessions are recorded.\n");
|
|
8435
|
+
}
|
|
8436
|
+
}
|
|
8437
|
+
} else {
|
|
8438
|
+
console.warn(" \u26A0 Session import skipped \u2014 container auth token not found.\n");
|
|
8439
|
+
}
|
|
8440
|
+
} catch (err) {
|
|
8441
|
+
console.warn(` \u26A0 Local session import failed: ${err.message}
|
|
8442
|
+
`);
|
|
7973
8443
|
}
|
|
7974
|
-
}
|
|
7975
|
-
|
|
7976
|
-
|
|
8444
|
+
} else {
|
|
8445
|
+
try {
|
|
8446
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
8447
|
+
if (ingested > 0) {
|
|
8448
|
+
console.log(` \u2713 Indexed ${ingested} session insights from Claude Code history.`);
|
|
8449
|
+
}
|
|
8450
|
+
} catch (err) {
|
|
8451
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
7977
8452
|
`);
|
|
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
8453
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
8454
|
+
try {
|
|
8455
|
+
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
8456
|
+
if (result.messages > 0) {
|
|
8457
|
+
console.log(` \u2713 Synced ${result.sessions} sessions (${result.messages} messages) to cloud.`);
|
|
8458
|
+
console.log(" Embeddings use your configured inference provider.\n");
|
|
8459
|
+
}
|
|
8460
|
+
} catch (err) {
|
|
8461
|
+
console.warn(` \u26A0 Transcript sync skipped: ${err.message}
|
|
7990
8462
|
`);
|
|
8463
|
+
}
|
|
8464
|
+
}
|
|
7991
8465
|
}
|
|
7992
8466
|
}
|
|
7993
8467
|
if (ghToken) {
|
|
@@ -8019,7 +8493,7 @@ function getClaudeProjectsFolder() {
|
|
|
8019
8493
|
}
|
|
8020
8494
|
function extractSessionInsights(projectsDir) {
|
|
8021
8495
|
const insights = [];
|
|
8022
|
-
const files =
|
|
8496
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8023
8497
|
for (const file of files) {
|
|
8024
8498
|
const sessionId = file.replace(".jsonl", "");
|
|
8025
8499
|
const filePath = join8(projectsDir, file);
|
|
@@ -8137,10 +8611,52 @@ function parseTranscriptFile(filePath) {
|
|
|
8137
8611
|
}
|
|
8138
8612
|
return messages;
|
|
8139
8613
|
}
|
|
8614
|
+
async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
8615
|
+
const projectsDir = getClaudeProjectsFolder();
|
|
8616
|
+
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
8617
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8618
|
+
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
8619
|
+
console.log(` Found ${files.length} CC session transcripts, importing + embedding...`);
|
|
8620
|
+
let totalSessions = 0;
|
|
8621
|
+
let totalMessages = 0;
|
|
8622
|
+
for (let i = 0; i < files.length; i++) {
|
|
8623
|
+
const file = files[i];
|
|
8624
|
+
const sessionId = file.replace(".jsonl", "");
|
|
8625
|
+
const filePath = join8(projectsDir, file);
|
|
8626
|
+
try {
|
|
8627
|
+
const allMessages = parseTranscriptFile(filePath);
|
|
8628
|
+
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
8629
|
+
if (messages.length === 0) continue;
|
|
8630
|
+
const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/conversation-sync`, {
|
|
8631
|
+
method: "POST",
|
|
8632
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${mcpToken}` },
|
|
8633
|
+
body: JSON.stringify({ session_id: sessionId, repo, messages }),
|
|
8634
|
+
signal: AbortSignal.timeout(15e3)
|
|
8635
|
+
});
|
|
8636
|
+
if (resp.ok) {
|
|
8637
|
+
const result = await resp.json();
|
|
8638
|
+
totalSessions++;
|
|
8639
|
+
totalMessages += result.ingested ?? messages.length;
|
|
8640
|
+
}
|
|
8641
|
+
} catch {
|
|
8642
|
+
}
|
|
8643
|
+
if ((i + 1) % 10 === 0 || i === files.length - 1) {
|
|
8644
|
+
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
8645
|
+
}
|
|
8646
|
+
try {
|
|
8647
|
+
const content = readFileSync7(join8(projectsDir, file), "utf-8");
|
|
8648
|
+
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8649
|
+
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8650
|
+
} catch {
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
if (totalSessions > 0) process.stdout.write("\n");
|
|
8654
|
+
return { sessions: totalSessions, messages: totalMessages };
|
|
8655
|
+
}
|
|
8140
8656
|
async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
8141
8657
|
const projectsDir = getClaudeProjectsFolder();
|
|
8142
8658
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
8143
|
-
const files =
|
|
8659
|
+
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
8144
8660
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
8145
8661
|
console.log(`Found ${files.length} CC session transcripts, syncing...`);
|
|
8146
8662
|
const maxMessagesPerSession = 500;
|
|
@@ -8751,7 +9267,7 @@ var disconnect_exports = {};
|
|
|
8751
9267
|
__export(disconnect_exports, {
|
|
8752
9268
|
disconnectCommand: () => disconnectCommand
|
|
8753
9269
|
});
|
|
8754
|
-
import { existsSync as existsSync11, rmSync, readdirSync as
|
|
9270
|
+
import { existsSync as existsSync11, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
8755
9271
|
import { homedir as homedir10 } from "os";
|
|
8756
9272
|
import { join as join10 } from "path";
|
|
8757
9273
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -8834,7 +9350,7 @@ async function disconnectCommand(args2 = []) {
|
|
|
8834
9350
|
} else {
|
|
8835
9351
|
const keep = /* @__PURE__ */ new Set([join10(SYNKRO_DIR5, "pgdata"), join10(SYNKRO_DIR5, "pgdata-backups")]);
|
|
8836
9352
|
const preserved = [];
|
|
8837
|
-
for (const entry of
|
|
9353
|
+
for (const entry of readdirSync4(SYNKRO_DIR5)) {
|
|
8838
9354
|
const full = join10(SYNKRO_DIR5, entry);
|
|
8839
9355
|
if (keep.has(full)) {
|
|
8840
9356
|
preserved.push(entry);
|
|
@@ -9140,7 +9656,7 @@ var init_grade = __esm({
|
|
|
9140
9656
|
});
|
|
9141
9657
|
|
|
9142
9658
|
// cli/local-cc/pueue.ts
|
|
9143
|
-
import { execFileSync, spawnSync as spawnSync6, spawn } from "child_process";
|
|
9659
|
+
import { execFileSync as execFileSync2, spawnSync as spawnSync6, spawn } from "child_process";
|
|
9144
9660
|
import { homedir as homedir12 } from "os";
|
|
9145
9661
|
import { join as join12 } from "path";
|
|
9146
9662
|
import { connect as connect2 } from "net";
|
|
@@ -10180,7 +10696,6 @@ var init_config = __esm({
|
|
|
10180
10696
|
import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
|
|
10181
10697
|
import { resolve as resolve2 } from "path";
|
|
10182
10698
|
var envCandidates = [
|
|
10183
|
-
resolve2(process.cwd(), ".env"),
|
|
10184
10699
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
10185
10700
|
];
|
|
10186
10701
|
for (const envPath of envCandidates) {
|
|
@@ -10200,7 +10715,7 @@ var args = process.argv.slice(2);
|
|
|
10200
10715
|
var cmd = args[0] || "";
|
|
10201
10716
|
var subArgs = args.slice(1);
|
|
10202
10717
|
function printVersion() {
|
|
10203
|
-
console.log("1.6.
|
|
10718
|
+
console.log("1.6.30");
|
|
10204
10719
|
}
|
|
10205
10720
|
function printHelp2() {
|
|
10206
10721
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -10246,6 +10761,23 @@ async function main() {
|
|
|
10246
10761
|
disconnectCommand2(subArgs);
|
|
10247
10762
|
break;
|
|
10248
10763
|
}
|
|
10764
|
+
case "login": {
|
|
10765
|
+
const { authenticate: authenticate2, isAuthenticated: isAuthenticated2 } = await Promise.resolve().then(() => (init_stub(), stub_exports));
|
|
10766
|
+
if (isAuthenticated2()) {
|
|
10767
|
+
console.log("Already authenticated.");
|
|
10768
|
+
} else {
|
|
10769
|
+
console.log("Opening browser for Synkro auth...");
|
|
10770
|
+
const result = await authenticate2((status) => {
|
|
10771
|
+
if (status.phase === "success") console.log(" \u2713 Authenticated");
|
|
10772
|
+
else if (status.phase === "error") console.error(" \u2717 " + status.message);
|
|
10773
|
+
});
|
|
10774
|
+
if (!result) {
|
|
10775
|
+
console.error("Authentication failed.");
|
|
10776
|
+
process.exit(1);
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
break;
|
|
10780
|
+
}
|
|
10249
10781
|
case "grade": {
|
|
10250
10782
|
const { gradeCommand: gradeCommand2 } = await Promise.resolve().then(() => (init_grade(), grade_exports));
|
|
10251
10783
|
await gradeCommand2(subArgs);
|