@synkro-sh/cli 1.6.52 → 1.6.54
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 +1193 -483
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -193,6 +193,19 @@ function installCCHooks(settingsPath, config) {
|
|
|
193
193
|
],
|
|
194
194
|
[SYNKRO_MARKER]: true
|
|
195
195
|
});
|
|
196
|
+
if (config.taskActivateIntentScriptPath) {
|
|
197
|
+
settings.hooks.PreToolUse.push({
|
|
198
|
+
matcher: "mcp__synkro-guardrails__activate_standard",
|
|
199
|
+
hooks: [
|
|
200
|
+
{
|
|
201
|
+
type: "command",
|
|
202
|
+
command: config.taskActivateIntentScriptPath,
|
|
203
|
+
timeout: 5
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
[SYNKRO_MARKER]: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
196
209
|
settings.hooks.PostToolUse.push({
|
|
197
210
|
matcher: "Bash",
|
|
198
211
|
hooks: [
|
|
@@ -807,7 +820,7 @@ synkro_post_with_retry() {
|
|
|
807
820
|
function stubHook(surface, optsLiteral) {
|
|
808
821
|
return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
|
|
809
822
|
}
|
|
810
|
-
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
823
|
+
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_TASK_ACTIVATE_INTENT_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
811
824
|
var init_hookScriptsTs = __esm({
|
|
812
825
|
"cli/installer/hookScriptsTs.ts"() {
|
|
813
826
|
"use strict";
|
|
@@ -1167,7 +1180,7 @@ export function findGitRoot(cwd: string): string {
|
|
|
1167
1180
|
} catch { return ''; }
|
|
1168
1181
|
}
|
|
1169
1182
|
|
|
1170
|
-
// \u2500\u2500\u2500 .
|
|
1183
|
+
// \u2500\u2500\u2500 synkro.toml file \u2500\u2500\u2500
|
|
1171
1184
|
|
|
1172
1185
|
export interface SynkroFileConfig {
|
|
1173
1186
|
version: number;
|
|
@@ -1189,51 +1202,49 @@ const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
|
|
|
1189
1202
|
scanning: { cwe: true, cve: true },
|
|
1190
1203
|
};
|
|
1191
1204
|
|
|
1192
|
-
|
|
1205
|
+
// Coerce a single TOML value token to JS.
|
|
1206
|
+
function parseTomlValue(raw: string): any {
|
|
1207
|
+
const v = raw.trim();
|
|
1208
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
1209
|
+
const inner = v.slice(1, -1).trim();
|
|
1210
|
+
if (!inner) return [];
|
|
1211
|
+
return inner.split(',').map((s: string) => parseTomlValue(s)).filter((s: any) => s !== '');
|
|
1212
|
+
}
|
|
1213
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1);
|
|
1214
|
+
if (v === 'true') return true;
|
|
1215
|
+
if (v === 'false') return false;
|
|
1216
|
+
if (/^-?\\d+$/.test(v)) return parseInt(v, 10);
|
|
1217
|
+
return v;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Minimal TOML reader for synkro.toml's fixed schema (top-level scalars/arrays
|
|
1221
|
+
// + [grader]/[scanning] tables). Dependency-free \u2014 this runs inside the
|
|
1222
|
+
// materialized _synkro-common.ts which has no node_modules. Returns the same
|
|
1223
|
+
// nested-object shape the old YAML parser did.
|
|
1224
|
+
function parseSynkroToml(raw: string): Record<string, any> {
|
|
1193
1225
|
const result: Record<string, any> = {};
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
else if (val === 'false') result[key] = false;
|
|
1217
|
-
else if (/^\\d+$/.test(val)) result[key] = parseInt(val, 10);
|
|
1218
|
-
else result[key] = val;
|
|
1219
|
-
currentKey = '';
|
|
1220
|
-
}
|
|
1221
|
-
} else if (line.match(/^ - /)) {
|
|
1222
|
-
if (!currentArr) currentArr = [];
|
|
1223
|
-
currentArr.push(line.replace(/^ - /, '').trim());
|
|
1224
|
-
} else if (line.match(/^ \\S/) && line.includes(':')) {
|
|
1225
|
-
if (!currentObj) currentObj = {};
|
|
1226
|
-
const colonIdx = line.indexOf(':');
|
|
1227
|
-
const k = line.slice(0, colonIdx).trim();
|
|
1228
|
-
const v = line.slice(colonIdx + 1).trim();
|
|
1229
|
-
if (v === 'true') currentObj[k] = true;
|
|
1230
|
-
else if (v === 'false') currentObj[k] = false;
|
|
1231
|
-
else if (/^\\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
|
|
1232
|
-
else currentObj[k] = v;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (currentObj && currentKey) result[currentKey] = currentObj;
|
|
1236
|
-
if (currentArr && currentKey) result[currentKey] = currentArr;
|
|
1226
|
+
let section: string | null = null;
|
|
1227
|
+
for (const rawLine of raw.split('\\n')) {
|
|
1228
|
+
const line = rawLine.trim();
|
|
1229
|
+
if (!line || line.startsWith('#')) continue;
|
|
1230
|
+
const sec = line.match(/^\\[([^\\]]+)\\]$/);
|
|
1231
|
+
if (sec) {
|
|
1232
|
+
section = sec[1].trim();
|
|
1233
|
+
if (!result[section]) result[section] = {};
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const eq = line.indexOf('=');
|
|
1237
|
+
if (eq === -1) continue;
|
|
1238
|
+
const key = line.slice(0, eq).trim().replace(/^["']|["']$/g, '');
|
|
1239
|
+
let valRaw = line.slice(eq + 1).trim();
|
|
1240
|
+
if (!valRaw.startsWith('"') && !valRaw.startsWith("'") && !valRaw.startsWith('[')) {
|
|
1241
|
+
const h = valRaw.indexOf('#');
|
|
1242
|
+
if (h !== -1) valRaw = valRaw.slice(0, h).trim();
|
|
1243
|
+
}
|
|
1244
|
+
const value = parseTomlValue(valRaw);
|
|
1245
|
+
if (section) (result[section] as Record<string, any>)[key] = value;
|
|
1246
|
+
else result[key] = value;
|
|
1247
|
+
}
|
|
1237
1248
|
return result;
|
|
1238
1249
|
}
|
|
1239
1250
|
|
|
@@ -1266,11 +1277,11 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
|
1266
1277
|
if (_synkroFileCache) return _synkroFileCache;
|
|
1267
1278
|
const root = cwd ? findGitRoot(cwd) : '';
|
|
1268
1279
|
if (!root) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1269
|
-
const fp = root + '
|
|
1280
|
+
const fp = root + '/synkro.toml';
|
|
1270
1281
|
try {
|
|
1271
1282
|
if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1272
1283
|
const raw = readFileSync(fp, 'utf-8');
|
|
1273
|
-
const parsed =
|
|
1284
|
+
const parsed = parseSynkroToml(raw);
|
|
1274
1285
|
const validHarness = ['claude-code', 'cursor'] as const;
|
|
1275
1286
|
const harness = Array.isArray(parsed.harness)
|
|
1276
1287
|
? parsed.harness.filter((h: string) => validHarness.includes(h as any))
|
|
@@ -1309,13 +1320,13 @@ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind:
|
|
|
1309
1320
|
return 'cursor';
|
|
1310
1321
|
}
|
|
1311
1322
|
|
|
1312
|
-
// \u2500\u2500\u2500 .
|
|
1313
|
-
// A repo is "onboarded" to Synkro when it has a .
|
|
1314
|
-
// The hooks are installed globally, so they fire in every repo \u2014
|
|
1315
|
-
// the user never set up. Without a .
|
|
1316
|
-
// rather than attempting a grade that surfaces a confusing
|
|
1317
|
-
// error: the tool call passes through and we show a
|
|
1318
|
-
// "synkro install".
|
|
1323
|
+
// \u2500\u2500\u2500 synkro.toml presence (per-repo onboarding) \u2500\u2500\u2500
|
|
1324
|
+
// A repo is "onboarded" to Synkro when it has a synkro.toml file at its git
|
|
1325
|
+
// root. The hooks are installed globally, so they fire in every repo \u2014
|
|
1326
|
+
// including ones the user never set up. Without a synkro.toml there, grading is
|
|
1327
|
+
// SKIPPED gracefully rather than attempting a grade that surfaces a confusing
|
|
1328
|
+
// "grader unavailable" error: the tool call passes through and we show a
|
|
1329
|
+
// one-time hint pointing at "synkro install".
|
|
1319
1330
|
export function synkroFilePresent(cwd?: string): boolean {
|
|
1320
1331
|
try {
|
|
1321
1332
|
const root = cwd ? findGitRoot(cwd) : '';
|
|
@@ -1325,18 +1336,17 @@ export function synkroFilePresent(cwd?: string): boolean {
|
|
|
1325
1336
|
// under ~), never treat it as onboarded \u2014 otherwise grading fires in every
|
|
1326
1337
|
// directory under home.
|
|
1327
1338
|
if (root === HOME) return false;
|
|
1328
|
-
// A repo is onboarded ONLY via a regular .
|
|
1329
|
-
// isFile
|
|
1330
|
-
// .
|
|
1331
|
-
|
|
1332
|
-
return statSync(root + '/.synkro').isFile();
|
|
1339
|
+
// A repo is onboarded ONLY via a regular synkro.toml FILE at its root.
|
|
1340
|
+
// statSync + isFile guards against a directory of that name being mistaken
|
|
1341
|
+
// for config.
|
|
1342
|
+
return statSync(root + '/synkro.toml').isFile();
|
|
1333
1343
|
} catch { return false; }
|
|
1334
1344
|
}
|
|
1335
1345
|
|
|
1336
1346
|
const NO_SYNKRO_HINT_DIR = join(HOME, '.synkro', '.no-synkro-hint');
|
|
1337
1347
|
|
|
1338
1348
|
// Returns the onboarding hint the FIRST time a session touches a repo with no
|
|
1339
|
-
// .
|
|
1349
|
+
// synkro.toml file, then null on subsequent calls so it doesn't repeat on every
|
|
1340
1350
|
// tool call. Caller passes null straight through to outputEmpty().
|
|
1341
1351
|
export function noSynkroSkipMessage(sessionId: string): string | null {
|
|
1342
1352
|
try {
|
|
@@ -1348,7 +1358,7 @@ export function noSynkroSkipMessage(sessionId: string): string | null {
|
|
|
1348
1358
|
} catch {
|
|
1349
1359
|
// best-effort dedup \u2014 if the marker can't be written, still show the hint
|
|
1350
1360
|
}
|
|
1351
|
-
return '[synkro] No .
|
|
1361
|
+
return '[synkro] No synkro.toml config in this repo \u2014 grading skipped. Run \`synkro install\` here to enable Synkro.';
|
|
1352
1362
|
}
|
|
1353
1363
|
|
|
1354
1364
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
@@ -1660,6 +1670,13 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
|
1660
1670
|
'jq','yq','sort','uniq','cut','tr','xxd','hexdump','od','column',
|
|
1661
1671
|
'node','npm','pnpm','yarn','bun','python','python3','ruby','go','rustc','cargo',
|
|
1662
1672
|
'git',
|
|
1673
|
+
// Read-only system / inspection utilities (no mutation, no exec). Deliberately
|
|
1674
|
+
// EXCLUDES curl/wget (exfil + -o write), sed/awk (w/e write+exec), env (command
|
|
1675
|
+
// runner), tree (-o write) \u2014 those stay graded. Keep in sync with scanning/safeRead.ts.
|
|
1676
|
+
'lsof','ps','df','du','uname','hostname','uptime',
|
|
1677
|
+
'basename','dirname','realpath','readlink',
|
|
1678
|
+
'shasum','md5','md5sum','sha256sum','cksum',
|
|
1679
|
+
'docker',
|
|
1663
1680
|
]);
|
|
1664
1681
|
const tokens = seg.trim().split(' ').filter(t => t.length > 0);
|
|
1665
1682
|
const verb = tokens[0] || '';
|
|
@@ -1689,6 +1706,15 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
|
1689
1706
|
} else if (['node','python','python3','ruby','rustc'].includes(verb)) {
|
|
1690
1707
|
const sub = tokens[1] || '';
|
|
1691
1708
|
if (sub !== '--version' && sub !== '-v' && sub !== '-V') return false;
|
|
1709
|
+
} else if (verb === 'docker') {
|
|
1710
|
+
// Read-only docker subcommands only \u2014 same gating pattern as git. Excludes
|
|
1711
|
+
// run/exec/rm/rmi/kill/stop/start/build/buildx/push/pull/cp/commit/save/load/
|
|
1712
|
+
// compose and every mutating verb; those fall through to the judge.
|
|
1713
|
+
const SAFE_DOCKER = new Set([
|
|
1714
|
+
'ps','logs','images','inspect','version','info','top','port','history','diff',
|
|
1715
|
+
]);
|
|
1716
|
+
const sub = tokens[1] || '';
|
|
1717
|
+
if (!SAFE_DOCKER.has(sub)) return false;
|
|
1692
1718
|
}
|
|
1693
1719
|
if (!repoRoot) return false;
|
|
1694
1720
|
for (let i = 1; i < tokens.length; i++) {
|
|
@@ -2056,6 +2082,7 @@ export function dispatchCapture(
|
|
|
2056
2082
|
ccModel?: string;
|
|
2057
2083
|
linesAdded?: number;
|
|
2058
2084
|
linesRemoved?: number;
|
|
2085
|
+
codeChange?: string;
|
|
2059
2086
|
},
|
|
2060
2087
|
): void {
|
|
2061
2088
|
// Fire-and-forget
|
|
@@ -2091,6 +2118,7 @@ export function dispatchCapture(
|
|
|
2091
2118
|
if (opts.recentUserMessages) localBody.recent_user_messages = opts.recentUserMessages;
|
|
2092
2119
|
if (opts.linesAdded != null) localBody.lines_added = opts.linesAdded;
|
|
2093
2120
|
if (opts.linesRemoved != null) localBody.lines_removed = opts.linesRemoved;
|
|
2121
|
+
if (opts.codeChange) localBody.code_change = opts.codeChange;
|
|
2094
2122
|
}
|
|
2095
2123
|
appendLocalTelemetry(localBody);
|
|
2096
2124
|
|
|
@@ -2102,6 +2130,9 @@ export function dispatchCapture(
|
|
|
2102
2130
|
if (opts.rulesChecked) body.rules_checked = opts.rulesChecked;
|
|
2103
2131
|
if (opts.violatedRules) body.violated_rules = opts.violatedRules;
|
|
2104
2132
|
if (opts.recentUserMessages) body.recent_user_messages = opts.recentUserMessages;
|
|
2133
|
+
if (opts.linesAdded != null) body.lines_added = opts.linesAdded;
|
|
2134
|
+
if (opts.linesRemoved != null) body.lines_removed = opts.linesRemoved;
|
|
2135
|
+
if (opts.codeChange) body.code_change = opts.codeChange;
|
|
2105
2136
|
}
|
|
2106
2137
|
shipCloud(jwt, '/api/v1/hook/capture', body);
|
|
2107
2138
|
}
|
|
@@ -3142,6 +3173,8 @@ export type ScanFindingInput = {
|
|
|
3142
3173
|
aliases?: string[];
|
|
3143
3174
|
references?: Array<{ type: string; url: string }>;
|
|
3144
3175
|
cwe_name?: string;
|
|
3176
|
+
code_snippet?: string;
|
|
3177
|
+
offending_lines?: string;
|
|
3145
3178
|
};
|
|
3146
3179
|
|
|
3147
3180
|
/** Persist open scan_findings rows for a block and flush the telemetry spool. */
|
|
@@ -3159,12 +3192,39 @@ export function emitBlockScanFindings(
|
|
|
3159
3192
|
file_path: ctx.file_path,
|
|
3160
3193
|
repo: ctx.repo,
|
|
3161
3194
|
status: 'open',
|
|
3195
|
+
// These are BLOCK findings \u2014 Synkro denied the action, nothing landed.
|
|
3196
|
+
// prevented \u2192 recorded as auto-resolved on the server.
|
|
3197
|
+
prevented: true,
|
|
3162
3198
|
...f,
|
|
3163
3199
|
}, captureDepth);
|
|
3164
3200
|
}
|
|
3165
3201
|
if (rows.length > 0) drainSpool().catch(() => {});
|
|
3166
3202
|
}
|
|
3167
3203
|
|
|
3204
|
+
/**
|
|
3205
|
+
* Derive a 1-based file line range ("42" or "42-45") for a grader-reported
|
|
3206
|
+
* offending snippet by locating it verbatim in the reconstructed file. Mirrors
|
|
3207
|
+
* the container scanner's locateSnippetLines so cloud + local storage record
|
|
3208
|
+
* the same lines. Returns undefined when the snippet can't be located.
|
|
3209
|
+
*/
|
|
3210
|
+
export function cweSnippetLines(haystack: string, snippet?: string): string | undefined {
|
|
3211
|
+
if (!haystack || !snippet) return undefined;
|
|
3212
|
+
const trimmed = snippet.replace(/^\\n+|\\n+$/g, '');
|
|
3213
|
+
if (!trimmed) return undefined;
|
|
3214
|
+
let idx = haystack.indexOf(trimmed);
|
|
3215
|
+
let spanLines = trimmed.split('\\n').length;
|
|
3216
|
+
if (idx < 0) {
|
|
3217
|
+
const firstLine = trimmed.split('\\n').map((l) => l.trim()).find(Boolean);
|
|
3218
|
+
if (!firstLine) return undefined;
|
|
3219
|
+
idx = haystack.indexOf(firstLine);
|
|
3220
|
+
if (idx < 0) return undefined;
|
|
3221
|
+
spanLines = 1;
|
|
3222
|
+
}
|
|
3223
|
+
const startLine = haystack.slice(0, idx).split('\\n').length;
|
|
3224
|
+
const endLine = startLine + spanLines - 1;
|
|
3225
|
+
return endLine > startLine ? startLine + '-' + endLine : String(startLine);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3168
3228
|
export function dispatchFinding(
|
|
3169
3229
|
jwt: string,
|
|
3170
3230
|
finding: {
|
|
@@ -3183,6 +3243,9 @@ export function dispatchFinding(
|
|
|
3183
3243
|
aliases?: string[];
|
|
3184
3244
|
references?: Array<{ type: string; url: string }>;
|
|
3185
3245
|
cwe_name?: string;
|
|
3246
|
+
code_snippet?: string;
|
|
3247
|
+
offending_lines?: string;
|
|
3248
|
+
prevented?: boolean;
|
|
3186
3249
|
},
|
|
3187
3250
|
captureDepth: string,
|
|
3188
3251
|
): void {
|
|
@@ -3209,6 +3272,9 @@ export function dispatchFinding(
|
|
|
3209
3272
|
cwe_name: finding.cwe_name,
|
|
3210
3273
|
detail: finding.detail,
|
|
3211
3274
|
description: finding.description,
|
|
3275
|
+
code_snippet: finding.code_snippet,
|
|
3276
|
+
offending_lines: finding.offending_lines,
|
|
3277
|
+
prevented: finding.prevented,
|
|
3212
3278
|
};
|
|
3213
3279
|
shipCloud(jwt, '/api/v1/hook/finding', cloudBody);
|
|
3214
3280
|
}
|
|
@@ -3339,7 +3405,7 @@ export function graderUnavailableMessage(hook: string, target: string, errorMess
|
|
|
3339
3405
|
}
|
|
3340
3406
|
if (isGraderNotConfigured(errorMessage)) {
|
|
3341
3407
|
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3342
|
-
return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your .
|
|
3408
|
+
return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your synkro.toml file and run \`synkro install\`.';
|
|
3343
3409
|
}
|
|
3344
3410
|
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3345
3411
|
}
|
|
@@ -3689,7 +3755,7 @@ import {
|
|
|
3689
3755
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
3690
3756
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3691
3757
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3692
|
-
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3758
|
+
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, cweSnippetLines, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3693
3759
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3694
3760
|
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
3695
3761
|
} from './_synkro-common.ts';
|
|
@@ -3725,6 +3791,8 @@ interface CweScanTarget {
|
|
|
3725
3791
|
cweDiffSection: string;
|
|
3726
3792
|
toolName: string;
|
|
3727
3793
|
toolInput: any;
|
|
3794
|
+
/** Full reconstructed file (or written content) — used to locate snippet line ranges. */
|
|
3795
|
+
proposed: string;
|
|
3728
3796
|
}
|
|
3729
3797
|
|
|
3730
3798
|
const JS_DANGEROUS_MODULES = new Set([
|
|
@@ -3867,6 +3935,7 @@ async function main() {
|
|
|
3867
3935
|
cweDiffSection: '',
|
|
3868
3936
|
toolName: toolName || 'Shell',
|
|
3869
3937
|
toolInput: {},
|
|
3938
|
+
proposed: w.content,
|
|
3870
3939
|
});
|
|
3871
3940
|
}
|
|
3872
3941
|
if (targets.length === 0) { outputEmpty(); return; }
|
|
@@ -3901,7 +3970,7 @@ async function main() {
|
|
|
3901
3970
|
cweContent = proposed.slice(0, 4000);
|
|
3902
3971
|
}
|
|
3903
3972
|
|
|
3904
|
-
targets.push({ filePath, cweContent, cweDiffSection, toolName, toolInput });
|
|
3973
|
+
targets.push({ filePath, cweContent, cweDiffSection, toolName, toolInput, proposed });
|
|
3905
3974
|
} else {
|
|
3906
3975
|
outputEmpty();
|
|
3907
3976
|
return;
|
|
@@ -3927,6 +3996,7 @@ async function main() {
|
|
|
3927
3996
|
const cweDiffSection = scan.cweDiffSection;
|
|
3928
3997
|
const scanToolName = scan.toolName;
|
|
3929
3998
|
const scanToolInput = scan.toolInput;
|
|
3999
|
+
const proposedFull = scan.proposed || scan.cweContent;
|
|
3930
4000
|
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3931
4001
|
const fileShort = basename(filePath);
|
|
3932
4002
|
const fileExt = extname(filePath).toLowerCase();
|
|
@@ -4012,12 +4082,15 @@ async function main() {
|
|
|
4012
4082
|
{ session_id: sessionId, file_path: filePath, repo: gitRepo || undefined },
|
|
4013
4083
|
activeCweIds.map((cweId) => {
|
|
4014
4084
|
const f = findings.find((x: any) => x.cwe === cweId);
|
|
4085
|
+
const snippet = f?.code_snippet || undefined;
|
|
4015
4086
|
return {
|
|
4016
4087
|
finding_type: 'cwe' as const,
|
|
4017
4088
|
finding_id: cweId,
|
|
4018
4089
|
severity: f?.severity || 'high',
|
|
4019
4090
|
detail: f?.reason || 'code weakness detected',
|
|
4020
4091
|
cwe_name: f?.name || undefined,
|
|
4092
|
+
code_snippet: snippet,
|
|
4093
|
+
offending_lines: cweSnippetLines(proposedFull, snippet),
|
|
4021
4094
|
};
|
|
4022
4095
|
}),
|
|
4023
4096
|
{
|
|
@@ -4147,6 +4220,7 @@ async function main() {
|
|
|
4147
4220
|
|
|
4148
4221
|
const cweIds: string[] = [];
|
|
4149
4222
|
const fixes: Record<string, string> = {};
|
|
4223
|
+
const snippets: Record<string, string> = {};
|
|
4150
4224
|
let mergedReason = '';
|
|
4151
4225
|
let mergedSeverity = '';
|
|
4152
4226
|
let mergedCategory = '';
|
|
@@ -4169,6 +4243,19 @@ async function main() {
|
|
|
4169
4243
|
for (let i = 0; i < Math.min(respIds.length, fMatches.length); i++) {
|
|
4170
4244
|
if (!fixes[respIds[i]]) fixes[respIds[i]] = fMatches[i].replace(/<\/?suggested_fix>/g, '').trim();
|
|
4171
4245
|
}
|
|
4246
|
+
// Per-<violation> pass for the verbatim snippet (code may contain '<'),
|
|
4247
|
+
// paired with its rule_id so each finding carries its own offending code.
|
|
4248
|
+
const vBlocks = gradeResp.match(/<violation>[\s\S]*?<\/violation>/g) || [];
|
|
4249
|
+
for (const block of vBlocks.slice(0, 8)) {
|
|
4250
|
+
const bId = block.match(/<rule_id>([^<]+)<\/rule_id>/)?.[1];
|
|
4251
|
+
if (!bId) continue;
|
|
4252
|
+
const id = bId.trim().replace(/^cwe-/i, 'CWE-');
|
|
4253
|
+
const snM = block.match(/<code_snippet>([\s\S]*?)<\/code_snippet>/);
|
|
4254
|
+
if (snM && !snippets[id]) {
|
|
4255
|
+
const snip = snM[1].replace(/^\n+|\n+$/g, '');
|
|
4256
|
+
if (snip.trim()) snippets[id] = snip;
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4172
4259
|
}
|
|
4173
4260
|
}
|
|
4174
4261
|
|
|
@@ -4203,13 +4290,18 @@ async function main() {
|
|
|
4203
4290
|
jwt,
|
|
4204
4291
|
config.captureDepth,
|
|
4205
4292
|
{ session_id: sessionId, file_path: filePath, repo: gitRepo || undefined },
|
|
4206
|
-
activeCweIds.map((cweId) =>
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4293
|
+
activeCweIds.map((cweId) => {
|
|
4294
|
+
const snippet = snippets[cweId] || undefined;
|
|
4295
|
+
return {
|
|
4296
|
+
finding_type: 'cwe' as const,
|
|
4297
|
+
finding_id: cweId,
|
|
4298
|
+
severity: verdict.severity || 'high',
|
|
4299
|
+
detail: verdict.reason || 'code weakness detected',
|
|
4300
|
+
cwe_name: cweNameMap.get(cweId.toUpperCase()) || undefined,
|
|
4301
|
+
code_snippet: snippet,
|
|
4302
|
+
offending_lines: cweSnippetLines(proposedFull, snippet),
|
|
4303
|
+
};
|
|
4304
|
+
}),
|
|
4213
4305
|
{
|
|
4214
4306
|
finding_type: 'cwe',
|
|
4215
4307
|
finding_id: activeCweIds[0] || 'CWE-UNKNOWN',
|
|
@@ -5830,7 +5922,7 @@ async function main() {
|
|
|
5830
5922
|
const model = rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
5831
5923
|
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
5832
5924
|
|
|
5833
|
-
// No .
|
|
5925
|
+
// No synkro.toml at the resolved repo root \u2192 Synkro is dormant here; allow
|
|
5834
5926
|
// without grading. Keyed off the validated detectRepo() root, not raw cwd.
|
|
5835
5927
|
if (!repo || !synkroFilePresent(repo)) finishAllow();
|
|
5836
5928
|
|
|
@@ -6043,7 +6135,7 @@ async function main() {
|
|
|
6043
6135
|
const filePath = String(payload.file_path || payload.path || payload.target_file || '');
|
|
6044
6136
|
if (!filePath) finish();
|
|
6045
6137
|
|
|
6046
|
-
// No .
|
|
6138
|
+
// No synkro.toml at the resolved repo root \u2192 Synkro is dormant here; skip capture.
|
|
6047
6139
|
const _root = detectRepo((typeof payload.cwd === 'string' && payload.cwd) || dirname(filePath), '', filePath, []);
|
|
6048
6140
|
if (!_root || !isPathUnder(filePath, _root) || !synkroFilePresent(_root)) finish();
|
|
6049
6141
|
|
|
@@ -6222,7 +6314,7 @@ function gitRoot(cwd: string): string {
|
|
|
6222
6314
|
} catch { return ''; }
|
|
6223
6315
|
}
|
|
6224
6316
|
|
|
6225
|
-
// Once-per-session onboarding hint for repos with no .
|
|
6317
|
+
// Once-per-session onboarding hint for repos with no synkro.toml file.
|
|
6226
6318
|
function noSynkroHint(sessionId: string): string | null {
|
|
6227
6319
|
const dir = join(HOME, '.synkro', '.no-synkro-hint');
|
|
6228
6320
|
try {
|
|
@@ -6232,7 +6324,7 @@ function noSynkroHint(sessionId: string): string | null {
|
|
|
6232
6324
|
if (existsSync(marker)) return null;
|
|
6233
6325
|
writeFileSync(marker, '', { flag: 'w' });
|
|
6234
6326
|
} catch {}
|
|
6235
|
-
return '[synkro] No .
|
|
6327
|
+
return '[synkro] No synkro.toml config in this repo — grading skipped. Run synkro install here to enable Synkro.';
|
|
6236
6328
|
}
|
|
6237
6329
|
|
|
6238
6330
|
function filePathFromToolInput(ti: any): string {
|
|
@@ -6263,13 +6355,13 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
6263
6355
|
const sessionId = String(payload.session_id || payload.conversation_id || '');
|
|
6264
6356
|
const root = gitRoot(cwd);
|
|
6265
6357
|
|
|
6266
|
-
// Dormancy: a repo is onboarded only if it has a .
|
|
6358
|
+
// Dormancy: a repo is onboarded only if it has a synkro.toml FILE at its git
|
|
6267
6359
|
// root. Guard root !== HOME — ~/.synkro is the config DIRECTORY, so without
|
|
6268
|
-
// this a home-rooted cwd (dotfiles in git, non-git dir under home)
|
|
6269
|
-
//
|
|
6360
|
+
// this a home-rooted cwd (dotfiles in git, non-git dir under home) could
|
|
6361
|
+
// look onboarded.
|
|
6270
6362
|
let synkroFileText = '';
|
|
6271
|
-
if (root && root !== HOME && existsSync(join(root, '.
|
|
6272
|
-
try { synkroFileText = readFileSync(join(root, '.
|
|
6363
|
+
if (root && root !== HOME && existsSync(join(root, 'synkro.toml'))) {
|
|
6364
|
+
try { synkroFileText = readFileSync(join(root, 'synkro.toml'), 'utf-8'); } catch {}
|
|
6273
6365
|
}
|
|
6274
6366
|
if (!synkroFileText) {
|
|
6275
6367
|
if (opts.telemetry) { out(failOpen(harness)); return; }
|
|
@@ -6303,16 +6395,32 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
6303
6395
|
envelope.planText = String(ti.plan || ti.content || payload.plan || '');
|
|
6304
6396
|
}
|
|
6305
6397
|
|
|
6306
|
-
|
|
6398
|
+
// bash-followup is telemetry, but it can carry the INLINE "task complete ✓ N/N"
|
|
6399
|
+
// result (the server waits up to 30s for the completion grade), so it needs a
|
|
6400
|
+
// wait above that budget — not the 6s telemetry default — or the stub abandons
|
|
6401
|
+
// the request before the result lands and the completion shows up a hook late.
|
|
6402
|
+
const timeoutMs = surface === 'bash-followup' ? 32000 : (opts.telemetry ? 6000 : 28000);
|
|
6307
6403
|
const url = 'http://127.0.0.1:' + PORT + '/api/scan/' + surface + (harness === 'cursor' ? '?harness=cursor' : '');
|
|
6308
|
-
const
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6404
|
+
const body = JSON.stringify(envelope);
|
|
6405
|
+
const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() };
|
|
6406
|
+
// Telemetry hooks (bash-followup, transcript-sync, session telemetry) IGNORE the
|
|
6407
|
+
// response and the server processes them DETACHED, so delivery RELIABILITY beats
|
|
6408
|
+
// latency: retry a few times with backoff. Right after a server restart the
|
|
6409
|
+
// event loop can be briefly busy / not-yet-listening and a single POST silently
|
|
6410
|
+
// drops the event (this was the followup flakiness). Double-delivery is safe here
|
|
6411
|
+
// — the server side is idempotent-ish (completion in-flight guard, dup evidence
|
|
6412
|
+
// is harmless). Blocking hooks make ONE attempt: their response IS the verdict
|
|
6413
|
+
// and a retry would double-grade. Normal case: first attempt wins, no delay.
|
|
6414
|
+
const attempts = opts.telemetry ? 3 : 1;
|
|
6415
|
+
let text = '';
|
|
6416
|
+
for (let i = 0; i < attempts; i++) {
|
|
6417
|
+
try {
|
|
6418
|
+
const resp = await fetch(url, { method: 'POST', headers, body, signal: AbortSignal.timeout(timeoutMs) });
|
|
6419
|
+
if (resp.ok) { text = (await resp.text()).trim(); break; }
|
|
6420
|
+
} catch (e) { /* connection refused / timeout → retry below */ }
|
|
6421
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
|
6422
|
+
}
|
|
6423
|
+
out(text || failOpen(harness));
|
|
6316
6424
|
} catch {
|
|
6317
6425
|
out(failOpen(harness));
|
|
6318
6426
|
}
|
|
@@ -6325,11 +6433,30 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
|
|
|
6325
6433
|
STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
|
|
6326
6434
|
STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
|
|
6327
6435
|
STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
|
|
6328
|
-
STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true
|
|
6436
|
+
STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true }");
|
|
6329
6437
|
STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
|
|
6330
6438
|
STUB_TRANSCRIPT_SYNC_TS = stubHook("transcript-sync", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
|
|
6331
6439
|
STUB_USER_PROMPT_SUBMIT_TS = stubHook("prompt-submit", "{ telemetry: true }");
|
|
6332
6440
|
STUB_BASH_FOLLOWUP_TS = stubHook("bash-followup", "{ telemetry: true }");
|
|
6441
|
+
STUB_TASK_ACTIVATE_INTENT_TS = `#!/usr/bin/env bun
|
|
6442
|
+
import { readFileSync } from 'node:fs';
|
|
6443
|
+
import { homedir } from 'node:os';
|
|
6444
|
+
import { join } from 'node:path';
|
|
6445
|
+
const PORT = process.env.SYNKRO_MCP_PORT || '18931';
|
|
6446
|
+
function mcpJwt(): string { try { return readFileSync(join(homedir(), '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; } }
|
|
6447
|
+
const chunks: Buffer[] = [];
|
|
6448
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
6449
|
+
try {
|
|
6450
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString('utf-8') || '{}');
|
|
6451
|
+
await fetch('http://127.0.0.1:' + PORT + '/api/local/task-activate-intent', {
|
|
6452
|
+
method: 'POST',
|
|
6453
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpJwt() },
|
|
6454
|
+
body: JSON.stringify({ session_id: payload.session_id, tool_input: payload.tool_input }),
|
|
6455
|
+
signal: AbortSignal.timeout(5000),
|
|
6456
|
+
});
|
|
6457
|
+
} catch {}
|
|
6458
|
+
process.stdout.write('{}\\n');
|
|
6459
|
+
`;
|
|
6333
6460
|
STUB_CURSOR_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
6334
6461
|
STUB_CURSOR_EDIT_CAPTURE_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true, harness: 'cursor' }");
|
|
6335
6462
|
STUB_CURSOR_AGENT_CAPTURE_TS = stubHook("agent-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
@@ -7230,6 +7357,510 @@ var init_promptFetcher = __esm({
|
|
|
7230
7357
|
}
|
|
7231
7358
|
});
|
|
7232
7359
|
|
|
7360
|
+
// cli/installer/claudeDesktopTap.ts
|
|
7361
|
+
var claudeDesktopTap_exports = {};
|
|
7362
|
+
__export(claudeDesktopTap_exports, {
|
|
7363
|
+
claudeDesktopInstalled: () => claudeDesktopInstalled,
|
|
7364
|
+
runClaudeDesktopTap: () => runClaudeDesktopTap
|
|
7365
|
+
});
|
|
7366
|
+
import { spawn } from "child_process";
|
|
7367
|
+
import { writeFileSync as writeFileSync6, mkdtempSync, readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
|
|
7368
|
+
import { join as join5 } from "path";
|
|
7369
|
+
import { tmpdir, homedir as homedir5 } from "os";
|
|
7370
|
+
function claudeDesktopInstalled() {
|
|
7371
|
+
return process.platform === "darwin" && existsSync7("/Applications/Claude.app/Contents/MacOS/Claude");
|
|
7372
|
+
}
|
|
7373
|
+
function buildRunner(sessionDir) {
|
|
7374
|
+
return `#!/bin/bash
|
|
7375
|
+
set -e
|
|
7376
|
+
SESSION_DIR=${JSON.stringify(sessionDir)}
|
|
7377
|
+
PROXY_PORT=8080
|
|
7378
|
+
MITM_CA_DIR="$SESSION_DIR/ca"
|
|
7379
|
+
TAP_SCRIPT="$SESSION_DIR/tap.py"
|
|
7380
|
+
CA_FINGERPRINT=""
|
|
7381
|
+
MITM_PID=""
|
|
7382
|
+
SUDO_KEEPALIVE_PID=""
|
|
7383
|
+
MCP_PROXY="$SESSION_DIR/mcp_proxy.py"
|
|
7384
|
+
MCP_PATCH="$SESSION_DIR/mcp_patch.py"
|
|
7385
|
+
CLAUDE_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
7386
|
+
CLAUDE_CONFIG_BACKUP="$SESSION_DIR/claude_desktop_config.backup.json"
|
|
7387
|
+
MCP_PATCHED=false
|
|
7388
|
+
|
|
7389
|
+
cleanup() {
|
|
7390
|
+
echo ""
|
|
7391
|
+
echo "Stopping Claude Desktop observability..."
|
|
7392
|
+
[ -n "$MITM_PID" ] && kill "$MITM_PID" 2>/dev/null
|
|
7393
|
+
[ -n "$SUDO_KEEPALIVE_PID" ] && kill "$SUDO_KEEPALIVE_PID" 2>/dev/null
|
|
7394
|
+
# Restore the original MCP config (unwrap the proxy) on exit.
|
|
7395
|
+
if [ "$MCP_PATCHED" = "true" ] && [ -f "$CLAUDE_CONFIG_BACKUP" ]; then
|
|
7396
|
+
cp "$CLAUDE_CONFIG_BACKUP" "$CLAUDE_CONFIG" && echo " MCP config restored"
|
|
7397
|
+
fi
|
|
7398
|
+
if [ -n "$CA_FINGERPRINT" ]; then
|
|
7399
|
+
# Non-interactive (-n) so teardown can NEVER hang on a sudo password prompt
|
|
7400
|
+
# (which would swallow the user's Ctrl+C). The keepalive below keeps creds
|
|
7401
|
+
# warm so this normally succeeds without a prompt.
|
|
7402
|
+
if sudo -n security delete-certificate -Z "$CA_FINGERPRINT" -t /Library/Keychains/System.keychain 2>/dev/null; then
|
|
7403
|
+
echo " Session CA removed from keychain"
|
|
7404
|
+
else
|
|
7405
|
+
echo " Could not auto-remove the session CA (sudo timed out)."
|
|
7406
|
+
echo " Remove it manually: sudo security delete-certificate -c mitmproxy /Library/Keychains/System.keychain"
|
|
7407
|
+
fi
|
|
7408
|
+
fi
|
|
7409
|
+
echo "Done."
|
|
7410
|
+
exit 0
|
|
7411
|
+
}
|
|
7412
|
+
trap cleanup INT TERM EXIT
|
|
7413
|
+
|
|
7414
|
+
if ! command -v mitmdump &>/dev/null; then
|
|
7415
|
+
echo "Installing mitmproxy..."
|
|
7416
|
+
if command -v brew &>/dev/null; then brew install mitmproxy; else echo "mitmproxy required: brew install mitmproxy"; exit 1; fi
|
|
7417
|
+
fi
|
|
7418
|
+
|
|
7419
|
+
mkdir -p "$MITM_CA_DIR"
|
|
7420
|
+
echo "Generating session CA certificate..."
|
|
7421
|
+
mitmdump --set confdir="$MITM_CA_DIR" --listen-port 0 </dev/null &>/dev/null &
|
|
7422
|
+
TMPPID=$!; sleep 2; kill $TMPPID 2>/dev/null || true
|
|
7423
|
+
|
|
7424
|
+
echo "Trusting session CA (sudo \u2014 removed automatically on exit)..."
|
|
7425
|
+
sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" "$MITM_CA_DIR/mitmproxy-ca-cert.pem"
|
|
7426
|
+
CA_FINGERPRINT=$(openssl x509 -in "$MITM_CA_DIR/mitmproxy-ca-cert.pem" -noout -fingerprint -sha1 2>/dev/null | sed "s/.*=//;s/://g")
|
|
7427
|
+
|
|
7428
|
+
# Keep the sudo timestamp warm for the life of the session so teardown's CA
|
|
7429
|
+
# removal never blocks on a password prompt (a prompt would eat Ctrl+C). Exits
|
|
7430
|
+
# itself if creds are ever revoked. Killed in cleanup().
|
|
7431
|
+
( while true; do sudo -n true 2>/dev/null || exit 0; sleep 30; done ) &
|
|
7432
|
+
SUDO_KEEPALIVE_PID=$!
|
|
7433
|
+
|
|
7434
|
+
lsof -ti:$PROXY_PORT 2>/dev/null | xargs kill 2>/dev/null || true
|
|
7435
|
+
osascript -e 'tell app "Claude" to quit' 2>/dev/null || true
|
|
7436
|
+
sleep 1
|
|
7437
|
+
|
|
7438
|
+
# Patch Claude Desktop's MCP config to wrap each server with the Synkro stdio
|
|
7439
|
+
# proxy (restored on exit). Capture URL + token come from the inherited env.
|
|
7440
|
+
if [ -f "$CLAUDE_CONFIG" ]; then
|
|
7441
|
+
cp "$CLAUDE_CONFIG" "$CLAUDE_CONFIG_BACKUP"
|
|
7442
|
+
if python3 "$MCP_PATCH" "$CLAUDE_CONFIG" "$MCP_PROXY" "$SYNKRO_CAPTURE_URL" "$SYNKRO_TAP_TOKEN" 2>/dev/null; then
|
|
7443
|
+
MCP_PATCHED=true
|
|
7444
|
+
echo "MCP servers wrapped with Synkro proxy"
|
|
7445
|
+
else
|
|
7446
|
+
echo "No MCP servers configured \u2014 skipping MCP proxy"
|
|
7447
|
+
cp "$CLAUDE_CONFIG_BACKUP" "$CLAUDE_CONFIG" 2>/dev/null || true
|
|
7448
|
+
fi
|
|
7449
|
+
fi
|
|
7450
|
+
|
|
7451
|
+
echo "Starting proxy on :$PROXY_PORT..."
|
|
7452
|
+
# stdin from /dev/null so mitmdump can't grab the TTY in raw mode \u2014 otherwise it
|
|
7453
|
+
# captures keypresses (incl. Ctrl+C) instead of letting them become a signal.
|
|
7454
|
+
mitmdump --set confdir="$MITM_CA_DIR" --listen-port $PROXY_PORT --mode regular -s "$TAP_SCRIPT" </dev/null &>/tmp/synkro-mitm.log &
|
|
7455
|
+
MITM_PID=$!
|
|
7456
|
+
sleep 2
|
|
7457
|
+
|
|
7458
|
+
CLAUDE_BIN=""
|
|
7459
|
+
for p in "/Applications/Claude.app/Contents/MacOS/Claude" "/Volumes/Claude/Claude.app/Contents/MacOS/Claude"; do
|
|
7460
|
+
[ -f "$p" ] && CLAUDE_BIN="$p" && break
|
|
7461
|
+
done
|
|
7462
|
+
if [ -z "$CLAUDE_BIN" ]; then
|
|
7463
|
+
CLAUDE_BIN="$(mdfind 'kMDItemFSName == "Claude.app"' 2>/dev/null | head -1)/Contents/MacOS/Claude"
|
|
7464
|
+
fi
|
|
7465
|
+
if [ ! -f "$CLAUDE_BIN" ]; then echo "Claude Desktop not found."; exit 1; fi
|
|
7466
|
+
|
|
7467
|
+
echo "Launching Claude Desktop..."
|
|
7468
|
+
"$CLAUDE_BIN" --proxy-server="http://localhost:$PROXY_PORT" &>/dev/null &
|
|
7469
|
+
|
|
7470
|
+
echo ""
|
|
7471
|
+
echo " \u2713 Claude Desktop is being monitored by Synkro (local)."
|
|
7472
|
+
echo " Your conversations will appear in the dashboard Events page."
|
|
7473
|
+
echo " Press Ctrl+C to stop (this removes the session CA cleanly)."
|
|
7474
|
+
echo ""
|
|
7475
|
+
wait $MITM_PID
|
|
7476
|
+
`;
|
|
7477
|
+
}
|
|
7478
|
+
async function runClaudeDesktopTap() {
|
|
7479
|
+
if (process.platform !== "darwin") {
|
|
7480
|
+
console.log(" Claude Desktop observability is macOS-only \u2014 skipped.");
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
let token = "";
|
|
7484
|
+
try {
|
|
7485
|
+
token = readFileSync5(JWT_PATH, "utf-8").trim();
|
|
7486
|
+
} catch {
|
|
7487
|
+
}
|
|
7488
|
+
if (!token) {
|
|
7489
|
+
console.log(" Could not read local auth token (~/.synkro/.mcp-jwt). Run `synkro install` first.");
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
7492
|
+
const sessionDir = mkdtempSync(join5(tmpdir(), "synkro-cd-"));
|
|
7493
|
+
writeFileSync6(join5(sessionDir, "tap.py"), ADDON_PY, "utf-8");
|
|
7494
|
+
writeFileSync6(join5(sessionDir, "mcp_proxy.py"), MCP_PROXY_PY, { mode: 493 });
|
|
7495
|
+
writeFileSync6(join5(sessionDir, "mcp_patch.py"), MCP_PATCH_PY, "utf-8");
|
|
7496
|
+
const runnerPath = join5(sessionDir, "run.sh");
|
|
7497
|
+
writeFileSync6(runnerPath, buildRunner(sessionDir), { mode: 493 });
|
|
7498
|
+
await new Promise((resolve4) => {
|
|
7499
|
+
const child = spawn("bash", [runnerPath], {
|
|
7500
|
+
stdio: "inherit",
|
|
7501
|
+
env: { ...process.env, SYNKRO_CAPTURE_URL: CAPTURE_URL, SYNKRO_TAP_TOKEN: token }
|
|
7502
|
+
});
|
|
7503
|
+
const forward = (sig) => {
|
|
7504
|
+
try {
|
|
7505
|
+
child.kill(sig);
|
|
7506
|
+
} catch {
|
|
7507
|
+
}
|
|
7508
|
+
};
|
|
7509
|
+
process.on("SIGINT", forward);
|
|
7510
|
+
process.on("SIGTERM", forward);
|
|
7511
|
+
child.on("exit", () => {
|
|
7512
|
+
process.off("SIGINT", forward);
|
|
7513
|
+
process.off("SIGTERM", forward);
|
|
7514
|
+
resolve4();
|
|
7515
|
+
});
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
var CAPTURE_URL, JWT_PATH, ADDON_PY, MCP_PROXY_PY, MCP_PATCH_PY;
|
|
7519
|
+
var init_claudeDesktopTap = __esm({
|
|
7520
|
+
"cli/installer/claudeDesktopTap.ts"() {
|
|
7521
|
+
"use strict";
|
|
7522
|
+
CAPTURE_URL = "http://127.0.0.1:18931/api/local/capture";
|
|
7523
|
+
JWT_PATH = join5(homedir5(), ".synkro", ".mcp-jwt");
|
|
7524
|
+
ADDON_PY = `import os, json, threading, urllib.request, re
|
|
7525
|
+
from mitmproxy import http
|
|
7526
|
+
|
|
7527
|
+
CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
|
|
7528
|
+
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7529
|
+
|
|
7530
|
+
# Governance signals parsed from the completion SSE (usage window + web-search
|
|
7531
|
+
# sources), keyed by conversation id. The completion POST always precedes the
|
|
7532
|
+
# tree GET for the same turn, so the tree capture below reads + clears this.
|
|
7533
|
+
# Guarded by a lock \u2014 tap callbacks can run concurrently across flows (CWE-362).
|
|
7534
|
+
_sse_meta = {}
|
|
7535
|
+
_sse_lock = threading.Lock()
|
|
7536
|
+
# Session-global MCP server registry (uuid -> {name,url,connected,tools}), parsed
|
|
7537
|
+
# from the mcp/v2/bootstrap SSE. All access guarded by _mcp_lock (CWE-362).
|
|
7538
|
+
_mcp_registry = {}
|
|
7539
|
+
_mcp_lock = threading.Lock()
|
|
7540
|
+
|
|
7541
|
+
class SynkroTap:
|
|
7542
|
+
def responseheaders(self, flow):
|
|
7543
|
+
# Buffer the small conversation-tree GET (parsed for the dashboard).
|
|
7544
|
+
# For the completion POST, TAP the SSE chunk-by-chunk and pass each
|
|
7545
|
+
# through unchanged \u2014 Claude Desktop keeps streaming normally while we
|
|
7546
|
+
# accumulate a bounded copy for the raw diagnostic dump. Everything else
|
|
7547
|
+
# streams untouched.
|
|
7548
|
+
r = flow.request
|
|
7549
|
+
if "claude.ai" in (r.pretty_host or "") and "chat_conversations" in r.path:
|
|
7550
|
+
if r.method == "GET" and "tree" in r.pretty_url:
|
|
7551
|
+
return
|
|
7552
|
+
if r.method == "POST" and "completion" in r.path:
|
|
7553
|
+
cm = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7554
|
+
cid = cm.group(1) if cm else ""
|
|
7555
|
+
# Incremental parse: scan complete SSE lines for the small governance
|
|
7556
|
+
# signals AS THEY STREAM and keep only the extracted results \u2014 never
|
|
7557
|
+
# buffer the body. The carry holds at most one partial line (bounded),
|
|
7558
|
+
# so there's NO cap on total stream length and no source is dropped.
|
|
7559
|
+
st = {"carry": "", "usage": None, "sources": [], "seen": set(), "citations": [], "cseen": set()}
|
|
7560
|
+
def _scan(line, _st=st):
|
|
7561
|
+
if _st["usage"] is None:
|
|
7562
|
+
um = re.search(r'"5h":\\{"status":"([^"]*)","resets_at":(\\d+),"utilization":([0-9.]+)', line)
|
|
7563
|
+
if um:
|
|
7564
|
+
_st["usage"] = {"window": "5h", "status": um.group(1), "resets_at": int(um.group(2)), "utilization": float(um.group(3))}
|
|
7565
|
+
if len(_st["sources"]) < 50:
|
|
7566
|
+
for sm in re.finditer(r'"webpage_metadata","site_domain":"([^"]*)"(?:,"favicon_url":"[^"]*")?,"site_name":"([^"]*)"', line):
|
|
7567
|
+
d = sm.group(1)
|
|
7568
|
+
if d and d not in _st["seen"] and len(_st["sources"]) < 50:
|
|
7569
|
+
_st["seen"].add(d)
|
|
7570
|
+
_st["sources"].append({"domain": d, "name": sm.group(2)})
|
|
7571
|
+
# Citations: the specific pages the model cited (title + url +
|
|
7572
|
+
# domain) \u2014 the real "tool result" for web search.
|
|
7573
|
+
if "citation_start_delta" in line and line.startswith("data: ") and len(_st["citations"]) < 50:
|
|
7574
|
+
try:
|
|
7575
|
+
cit = json.loads(line[6:]).get("delta", {}).get("citation", {})
|
|
7576
|
+
url = cit.get("url", "")
|
|
7577
|
+
if url and url not in _st["cseen"]:
|
|
7578
|
+
_st["cseen"].add(url)
|
|
7579
|
+
md = cit.get("metadata") or {}
|
|
7580
|
+
_st["citations"].append({"title": cit.get("title", ""), "url": url, "domain": md.get("site_domain", "")})
|
|
7581
|
+
except Exception:
|
|
7582
|
+
pass
|
|
7583
|
+
def _tap(data, _st=st, _cid=cid):
|
|
7584
|
+
if data:
|
|
7585
|
+
try:
|
|
7586
|
+
txt = _st["carry"] + data.decode("utf-8", "replace")
|
|
7587
|
+
except Exception:
|
|
7588
|
+
txt = _st["carry"]
|
|
7589
|
+
lines = txt.split("\\n")
|
|
7590
|
+
_st["carry"] = lines[-1][-200000:] # bound the carried partial line (CWE-400)
|
|
7591
|
+
for ln in lines[:-1]:
|
|
7592
|
+
_scan(ln)
|
|
7593
|
+
else: # end of stream \u2014 flush carry + publish
|
|
7594
|
+
_scan(_st["carry"])
|
|
7595
|
+
if (_st["usage"] or _st["sources"] or _st["citations"]) and _cid:
|
|
7596
|
+
with _sse_lock:
|
|
7597
|
+
_sse_meta[_cid] = {"usage": _st["usage"], "sources": _st["sources"], "citations": _st["citations"]}
|
|
7598
|
+
return data
|
|
7599
|
+
flow.response.stream = _tap
|
|
7600
|
+
return
|
|
7601
|
+
if "claude.ai" in (r.pretty_host or "") and "mcp" in r.path and "bootstrap" in r.path:
|
|
7602
|
+
return # buffer the MCP registry bootstrap (parsed in response())
|
|
7603
|
+
flow.response.stream = True
|
|
7604
|
+
|
|
7605
|
+
def response(self, flow):
|
|
7606
|
+
r = flow.request
|
|
7607
|
+
if "claude.ai" not in (r.pretty_host or ""): return
|
|
7608
|
+
# MCP server registry from the bootstrap SSE stream.
|
|
7609
|
+
if "mcp" in r.path and "bootstrap" in r.path and flow.response:
|
|
7610
|
+
try:
|
|
7611
|
+
servers = {}
|
|
7612
|
+
for line in (flow.response.get_text() or "").split("\\n"):
|
|
7613
|
+
if not line.startswith("data: "): continue
|
|
7614
|
+
try: dd = json.loads(line[6:])
|
|
7615
|
+
except: continue
|
|
7616
|
+
if isinstance(dd.get("servers"), list):
|
|
7617
|
+
for s in dd["servers"]:
|
|
7618
|
+
sid = s.get("uuid", "")
|
|
7619
|
+
if sid: servers[sid] = {"name": s.get("name", ""), "url": s.get("url", ""), "connected": False, "tools": []}
|
|
7620
|
+
if "uuid" in dd and "connected" in dd and dd.get("uuid") in servers:
|
|
7621
|
+
servers[dd["uuid"]]["connected"] = dd.get("connected", False)
|
|
7622
|
+
if "server_uuid" in dd and isinstance(dd.get("tools"), list) and dd.get("server_uuid") in servers:
|
|
7623
|
+
for t in dd["tools"]:
|
|
7624
|
+
servers[dd["server_uuid"]]["tools"].append({"name": t.get("name", ""), "description": (t.get("description") or "")[:200]})
|
|
7625
|
+
if servers:
|
|
7626
|
+
with _mcp_lock:
|
|
7627
|
+
_mcp_registry.clear()
|
|
7628
|
+
_mcp_registry.update(servers)
|
|
7629
|
+
print("[synkro] MCP servers: " + str(len(servers)))
|
|
7630
|
+
except Exception:
|
|
7631
|
+
pass
|
|
7632
|
+
return
|
|
7633
|
+
if not ("chat_conversations" in r.path and r.method == "GET" and "tree" in r.pretty_url and flow.response): return
|
|
7634
|
+
if not CAPTURE_URL or not TOKEN: return
|
|
7635
|
+
try: convo = json.loads(flow.response.get_text())
|
|
7636
|
+
except: return
|
|
7637
|
+
m = re.search(r"chat_conversations/([0-9a-fA-F-]+)", r.path)
|
|
7638
|
+
convo_id = m.group(1) if m else ""
|
|
7639
|
+
model = convo.get("model", "unknown")
|
|
7640
|
+
settings = convo.get("settings") or {}
|
|
7641
|
+
emt = settings.get("enabled_mcp_tools")
|
|
7642
|
+
enabled_mcp = list(emt.keys()) if isinstance(emt, dict) else []
|
|
7643
|
+
web_search_on = bool(settings.get("enabled_web_search", False))
|
|
7644
|
+
platform = convo.get("platform", "")
|
|
7645
|
+
all_msgs = convo.get("chat_messages", [])
|
|
7646
|
+
if not all_msgs: return
|
|
7647
|
+
# Current turn only: last human message and everything after it.
|
|
7648
|
+
last_human = -1
|
|
7649
|
+
for i in range(len(all_msgs) - 1, -1, -1):
|
|
7650
|
+
if all_msgs[i].get("sender") == "human": last_human = i; break
|
|
7651
|
+
chat = all_msgs[last_human:] if last_human >= 0 else all_msgs[-2:]
|
|
7652
|
+
messages = []
|
|
7653
|
+
for msg in chat:
|
|
7654
|
+
sender = msg.get("sender", "")
|
|
7655
|
+
role = "user" if sender == "human" else "assistant"
|
|
7656
|
+
blocks = msg.get("content", [])
|
|
7657
|
+
text_parts = []
|
|
7658
|
+
tool_calls = []
|
|
7659
|
+
tool_results = []
|
|
7660
|
+
thinking = ""
|
|
7661
|
+
if isinstance(blocks, list):
|
|
7662
|
+
for b in blocks:
|
|
7663
|
+
if isinstance(b, str): text_parts.append(b); continue
|
|
7664
|
+
if not isinstance(b, dict): continue
|
|
7665
|
+
bt = b.get("type", "")
|
|
7666
|
+
if bt == "text":
|
|
7667
|
+
text_parts.append(b.get("text", ""))
|
|
7668
|
+
elif bt == "thinking":
|
|
7669
|
+
thinking += b.get("thinking", "") or b.get("text", "")
|
|
7670
|
+
elif bt == "tool_use" or bt == "server_tool_use":
|
|
7671
|
+
tool_calls.append({"name": b.get("name", ""), "id": b.get("id", ""), "arguments": json.dumps(b.get("input", {}))})
|
|
7672
|
+
elif bt.endswith("_tool_result") or bt == "tool_result":
|
|
7673
|
+
rc = b.get("content", "")
|
|
7674
|
+
if isinstance(rc, list):
|
|
7675
|
+
rparts = []
|
|
7676
|
+
for x in rc:
|
|
7677
|
+
if not isinstance(x, dict): continue
|
|
7678
|
+
if x.get("type") == "web_search_result":
|
|
7679
|
+
sn = x.get("page_content", "") or x.get("snippet", "") or x.get("content", "") or ""
|
|
7680
|
+
if isinstance(sn, dict): sn = sn.get("text", "")
|
|
7681
|
+
rparts.append((x.get("title", "") + " (" + x.get("url", "") + ")" + ((" - " + str(sn)[:200]) if sn else "")).strip())
|
|
7682
|
+
elif x.get("text"):
|
|
7683
|
+
rparts.append(x["text"])
|
|
7684
|
+
elif isinstance(x.get("content"), str):
|
|
7685
|
+
rparts.append(x["content"])
|
|
7686
|
+
rc = " | ".join(p for p in rparts if p and p.strip())
|
|
7687
|
+
dc = b.get("display_content")
|
|
7688
|
+
if isinstance(dc, dict):
|
|
7689
|
+
rc = rc or dc.get("json_block", "") or dc.get("text", "")
|
|
7690
|
+
# web_search results are server-side (content:[]); we surface the
|
|
7691
|
+
# sources via sse_meta instead. Only record a non-empty result.
|
|
7692
|
+
if rc and str(rc).strip():
|
|
7693
|
+
tool_results.append({"name": b.get("name", ""), "content": str(rc)})
|
|
7694
|
+
elif isinstance(blocks, str):
|
|
7695
|
+
text_parts.append(blocks)
|
|
7696
|
+
text = " ".join(p for p in text_parts if p).strip()
|
|
7697
|
+
if role == "assistant":
|
|
7698
|
+
am = {"role": "assistant", "content": text}
|
|
7699
|
+
if tool_calls: am["tool_calls"] = tool_calls
|
|
7700
|
+
if thinking: am["thinking"] = thinking
|
|
7701
|
+
if text or tool_calls or thinking: messages.append(am)
|
|
7702
|
+
for tr in tool_results:
|
|
7703
|
+
messages.append({"role": "tool", "content": tr["content"], "name": tr.get("name", "")})
|
|
7704
|
+
elif role == "user" and text:
|
|
7705
|
+
messages.append({"role": "user", "content": text})
|
|
7706
|
+
if not messages: return
|
|
7707
|
+
data = {"request": {"model": model, "messages": messages}, "conversation_id": convo_id}
|
|
7708
|
+
meta = {}
|
|
7709
|
+
if platform: meta["platform"] = platform
|
|
7710
|
+
if web_search_on: meta["web_search_enabled"] = True
|
|
7711
|
+
if enabled_mcp: meta["enabled_mcp_tools"] = enabled_mcp[:20]
|
|
7712
|
+
if meta: data["conversation_metadata"] = meta
|
|
7713
|
+
with _sse_lock:
|
|
7714
|
+
sm = _sse_meta.pop(convo_id, None)
|
|
7715
|
+
if sm: data["sse_meta"] = sm
|
|
7716
|
+
with _mcp_lock:
|
|
7717
|
+
reg = list(_mcp_registry.values())
|
|
7718
|
+
if reg: data["mcp_servers"] = reg
|
|
7719
|
+
ntools = 0
|
|
7720
|
+
for mm in messages:
|
|
7721
|
+
ntools += len(mm.get("tool_calls") or [])
|
|
7722
|
+
nsrc = len((sm or {}).get("sources") or [])
|
|
7723
|
+
print("[synkro] captured chat (msgs=" + str(len(messages)) + " tools=" + str(ntools) + " sources=" + str(nsrc) + " model=" + model + ")")
|
|
7724
|
+
def send(_d=data):
|
|
7725
|
+
try:
|
|
7726
|
+
req = urllib.request.Request(CAPTURE_URL, data=json.dumps(_d).encode(), headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN})
|
|
7727
|
+
urllib.request.urlopen(req, timeout=5)
|
|
7728
|
+
except Exception as e:
|
|
7729
|
+
print("[synkro] capture error: " + str(e))
|
|
7730
|
+
threading.Thread(target=send, daemon=True).start()
|
|
7731
|
+
|
|
7732
|
+
addons = [SynkroTap()]
|
|
7733
|
+
`;
|
|
7734
|
+
MCP_PROXY_PY = `#!/usr/bin/env python3
|
|
7735
|
+
"""Synkro MCP Proxy (local) \u2014 stdio passthrough that taps MCP tool calls."""
|
|
7736
|
+
import sys, os, json, subprocess, threading, urllib.request, time
|
|
7737
|
+
|
|
7738
|
+
CAPTURE_URL = os.environ.get("SYNKRO_CAPTURE_URL", "")
|
|
7739
|
+
TOKEN = os.environ.get("SYNKRO_TAP_TOKEN", "")
|
|
7740
|
+
SERVER_NAME = os.environ.get("SYNKRO_MCP_SERVER", "unknown")
|
|
7741
|
+
|
|
7742
|
+
pending = {}
|
|
7743
|
+
pending_lock = threading.Lock()
|
|
7744
|
+
|
|
7745
|
+
def send_capture(tool_name, tool_args, tool_result, latency_ms):
|
|
7746
|
+
if not CAPTURE_URL or not TOKEN: return
|
|
7747
|
+
try:
|
|
7748
|
+
payload = json.dumps({
|
|
7749
|
+
"request": {
|
|
7750
|
+
"model": "mcp:" + SERVER_NAME,
|
|
7751
|
+
"messages": [
|
|
7752
|
+
{"role": "assistant", "content": "", "tool_calls": [{"name": tool_name, "arguments": json.dumps(tool_args)}]},
|
|
7753
|
+
{"role": "tool", "content": json.dumps(tool_result), "name": tool_name},
|
|
7754
|
+
]
|
|
7755
|
+
},
|
|
7756
|
+
"mcp": {"server": SERVER_NAME, "tool": tool_name, "latency_ms": latency_ms}
|
|
7757
|
+
}).encode()
|
|
7758
|
+
req = urllib.request.Request(CAPTURE_URL, data=payload, headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN})
|
|
7759
|
+
urllib.request.urlopen(req, timeout=5)
|
|
7760
|
+
except Exception as e:
|
|
7761
|
+
print("[synkro-mcp] capture error: " + str(e), file=sys.stderr)
|
|
7762
|
+
|
|
7763
|
+
def tap_message(data, direction):
|
|
7764
|
+
if not isinstance(data, dict): return
|
|
7765
|
+
method = data.get("method")
|
|
7766
|
+
msg_id = data.get("id")
|
|
7767
|
+
if direction == "request" and method == "tools/call":
|
|
7768
|
+
params = data.get("params", {})
|
|
7769
|
+
tool_name = params.get("name", "unknown")
|
|
7770
|
+
tool_args = params.get("arguments", {})
|
|
7771
|
+
if msg_id is not None:
|
|
7772
|
+
with pending_lock:
|
|
7773
|
+
if len(pending) < 1000:
|
|
7774
|
+
pending[msg_id] = {"name": tool_name, "args": tool_args, "start": time.time()}
|
|
7775
|
+
print("[synkro-mcp] " + SERVER_NAME + "." + str(tool_name), file=sys.stderr)
|
|
7776
|
+
if direction == "response" and msg_id is not None:
|
|
7777
|
+
with pending_lock:
|
|
7778
|
+
call = pending.pop(msg_id, None)
|
|
7779
|
+
if call:
|
|
7780
|
+
latency_ms = int((time.time() - call["start"]) * 1000)
|
|
7781
|
+
result = data.get("result", data.get("error", {}))
|
|
7782
|
+
threading.Thread(target=send_capture, args=(call["name"], call["args"], result, latency_ms), daemon=True).start()
|
|
7783
|
+
|
|
7784
|
+
def read_jsonrpc(stream):
|
|
7785
|
+
buf = b""
|
|
7786
|
+
while True:
|
|
7787
|
+
chunk = stream.read(1)
|
|
7788
|
+
if not chunk: return None
|
|
7789
|
+
if len(buf) < 5000000: # bound the line buffer (CWE-400)
|
|
7790
|
+
buf += chunk
|
|
7791
|
+
if chunk == b"\\n":
|
|
7792
|
+
line = buf.strip()
|
|
7793
|
+
buf = b""
|
|
7794
|
+
if not line: continue
|
|
7795
|
+
try: return json.loads(line)
|
|
7796
|
+
except: continue
|
|
7797
|
+
|
|
7798
|
+
def forward_stdin(proc):
|
|
7799
|
+
while True:
|
|
7800
|
+
msg = read_jsonrpc(sys.stdin.buffer)
|
|
7801
|
+
if msg is None: break
|
|
7802
|
+
tap_message(msg, "request")
|
|
7803
|
+
line = json.dumps(msg) + "\\n"
|
|
7804
|
+
try:
|
|
7805
|
+
proc.stdin.write(line.encode())
|
|
7806
|
+
proc.stdin.flush()
|
|
7807
|
+
except: break
|
|
7808
|
+
|
|
7809
|
+
def main():
|
|
7810
|
+
args = sys.argv[1:]
|
|
7811
|
+
if "--" in args:
|
|
7812
|
+
sep = args.index("--")
|
|
7813
|
+
args = args[sep + 1:]
|
|
7814
|
+
if not args:
|
|
7815
|
+
print("Usage: mcp_proxy.py -- <command> [args...]", file=sys.stderr)
|
|
7816
|
+
sys.exit(1)
|
|
7817
|
+
# Spawn the user's own configured MCP server (from claude_desktop_config.json).
|
|
7818
|
+
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
|
|
7819
|
+
t = threading.Thread(target=forward_stdin, args=(proc,), daemon=True)
|
|
7820
|
+
t.start()
|
|
7821
|
+
while True:
|
|
7822
|
+
msg = read_jsonrpc(proc.stdout)
|
|
7823
|
+
if msg is None: break
|
|
7824
|
+
tap_message(msg, "response")
|
|
7825
|
+
line = json.dumps(msg) + "\\n"
|
|
7826
|
+
try:
|
|
7827
|
+
sys.stdout.buffer.write(line.encode())
|
|
7828
|
+
sys.stdout.buffer.flush()
|
|
7829
|
+
except: break
|
|
7830
|
+
proc.wait()
|
|
7831
|
+
sys.exit(proc.returncode)
|
|
7832
|
+
|
|
7833
|
+
if __name__ == "__main__":
|
|
7834
|
+
main()
|
|
7835
|
+
`;
|
|
7836
|
+
MCP_PATCH_PY = `import json, sys
|
|
7837
|
+
config_path, proxy_path, capture_url, token = sys.argv[1:5]
|
|
7838
|
+
with open(config_path) as f: config = json.load(f)
|
|
7839
|
+
servers = config.get("mcpServers", {})
|
|
7840
|
+
if not servers:
|
|
7841
|
+
print("No MCP servers configured")
|
|
7842
|
+
sys.exit(1)
|
|
7843
|
+
patched = 0
|
|
7844
|
+
for name, server in servers.items():
|
|
7845
|
+
if server.get("_synkro_original"): continue
|
|
7846
|
+
orig_cmd = server.get("command", "")
|
|
7847
|
+
orig_args = server.get("args", [])
|
|
7848
|
+
if not orig_cmd: continue
|
|
7849
|
+
server["_synkro_original"] = {"command": orig_cmd, "args": orig_args}
|
|
7850
|
+
server["command"] = "python3"
|
|
7851
|
+
server["args"] = [proxy_path, "--", orig_cmd] + orig_args
|
|
7852
|
+
env = server.get("env", {})
|
|
7853
|
+
env["SYNKRO_CAPTURE_URL"] = capture_url
|
|
7854
|
+
env["SYNKRO_TAP_TOKEN"] = token
|
|
7855
|
+
env["SYNKRO_MCP_SERVER"] = name
|
|
7856
|
+
server["env"] = env
|
|
7857
|
+
patched += 1
|
|
7858
|
+
with open(config_path, "w") as f: json.dump(config, f, indent=2)
|
|
7859
|
+
print("Patched " + str(patched) + " MCP servers")
|
|
7860
|
+
`;
|
|
7861
|
+
}
|
|
7862
|
+
});
|
|
7863
|
+
|
|
7233
7864
|
// cli/local-cc/macKeychain.ts
|
|
7234
7865
|
var macKeychain_exports = {};
|
|
7235
7866
|
__export(macKeychain_exports, {
|
|
@@ -7252,9 +7883,9 @@ __export(macKeychain_exports, {
|
|
|
7252
7883
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
7253
7884
|
writeRefreshAgent: () => writeRefreshAgent
|
|
7254
7885
|
});
|
|
7255
|
-
import { existsSync as
|
|
7256
|
-
import { homedir as
|
|
7257
|
-
import { join as
|
|
7886
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
|
|
7887
|
+
import { homedir as homedir6, platform as platform2 } from "os";
|
|
7888
|
+
import { join as join6 } from "path";
|
|
7258
7889
|
import { spawnSync } from "child_process";
|
|
7259
7890
|
function needsKeychainBridge() {
|
|
7260
7891
|
return platform2() === "darwin";
|
|
@@ -7274,13 +7905,13 @@ function exportKeychainCreds() {
|
|
|
7274
7905
|
if (!blob) return null;
|
|
7275
7906
|
mkdirSync6(CLAUDE_CREDS_DIR, { recursive: true });
|
|
7276
7907
|
chmodSync(CLAUDE_CREDS_DIR, 448);
|
|
7277
|
-
|
|
7908
|
+
writeFileSync7(CLAUDE_CREDS_FILE, blob, "utf-8");
|
|
7278
7909
|
chmodSync(CLAUDE_CREDS_FILE, 384);
|
|
7279
7910
|
return CLAUDE_CREDS_FILE;
|
|
7280
7911
|
}
|
|
7281
7912
|
function cursorApiKeyConfigured() {
|
|
7282
7913
|
try {
|
|
7283
|
-
return
|
|
7914
|
+
return existsSync8(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
|
|
7284
7915
|
} catch {
|
|
7285
7916
|
return false;
|
|
7286
7917
|
}
|
|
@@ -7290,13 +7921,13 @@ function writeCursorApiKey(key) {
|
|
|
7290
7921
|
if (!trimmed) return;
|
|
7291
7922
|
mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
|
|
7292
7923
|
chmodSync(CURSOR_CREDS_DIR, 448);
|
|
7293
|
-
|
|
7924
|
+
writeFileSync7(CURSOR_API_KEY_FILE, trimmed, "utf-8");
|
|
7294
7925
|
chmodSync(CURSOR_API_KEY_FILE, 384);
|
|
7295
7926
|
}
|
|
7296
7927
|
async function validateCursorApiKey() {
|
|
7297
7928
|
let key;
|
|
7298
7929
|
try {
|
|
7299
|
-
key =
|
|
7930
|
+
key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
|
|
7300
7931
|
} catch {
|
|
7301
7932
|
return null;
|
|
7302
7933
|
}
|
|
@@ -7315,7 +7946,7 @@ async function validateCursorApiKey() {
|
|
|
7315
7946
|
}
|
|
7316
7947
|
}
|
|
7317
7948
|
function credsAreStale() {
|
|
7318
|
-
if (!
|
|
7949
|
+
if (!existsSync8(CLAUDE_CREDS_FILE)) return true;
|
|
7319
7950
|
try {
|
|
7320
7951
|
const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
|
|
7321
7952
|
return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
|
|
@@ -7327,7 +7958,7 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
7327
7958
|
if (platform2() !== "darwin") {
|
|
7328
7959
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
7329
7960
|
}
|
|
7330
|
-
mkdirSync6(
|
|
7961
|
+
mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
7331
7962
|
const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
|
|
7332
7963
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7333
7964
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -7346,13 +7977,13 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
7346
7977
|
<key>RunAtLoad</key>
|
|
7347
7978
|
<true/>
|
|
7348
7979
|
<key>StandardErrorPath</key>
|
|
7349
|
-
<string>${
|
|
7980
|
+
<string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
|
|
7350
7981
|
<key>StandardOutPath</key>
|
|
7351
|
-
<string>${
|
|
7982
|
+
<string>${join6(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
|
|
7352
7983
|
</dict>
|
|
7353
7984
|
</plist>
|
|
7354
7985
|
`;
|
|
7355
|
-
|
|
7986
|
+
writeFileSync7(LAUNCHD_PLIST, plist, "utf-8");
|
|
7356
7987
|
return LAUNCHD_PLIST;
|
|
7357
7988
|
}
|
|
7358
7989
|
function loadRefreshAgent() {
|
|
@@ -7378,7 +8009,7 @@ function uninstallRefreshAgent() {
|
|
|
7378
8009
|
timeout: 5e3
|
|
7379
8010
|
});
|
|
7380
8011
|
try {
|
|
7381
|
-
if (
|
|
8012
|
+
if (existsSync8(LAUNCHD_PLIST)) {
|
|
7382
8013
|
__require("fs").unlinkSync(LAUNCHD_PLIST);
|
|
7383
8014
|
}
|
|
7384
8015
|
} catch {
|
|
@@ -7390,7 +8021,7 @@ function refreshCreds() {
|
|
|
7390
8021
|
}
|
|
7391
8022
|
function readExportedCreds() {
|
|
7392
8023
|
try {
|
|
7393
|
-
return
|
|
8024
|
+
return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
|
|
7394
8025
|
} catch {
|
|
7395
8026
|
return null;
|
|
7396
8027
|
}
|
|
@@ -7399,14 +8030,14 @@ var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_AP
|
|
|
7399
8030
|
var init_macKeychain = __esm({
|
|
7400
8031
|
"cli/local-cc/macKeychain.ts"() {
|
|
7401
8032
|
"use strict";
|
|
7402
|
-
SYNKRO_DIR =
|
|
7403
|
-
CLAUDE_CREDS_DIR =
|
|
7404
|
-
CLAUDE_CREDS_FILE =
|
|
7405
|
-
CURSOR_CREDS_DIR =
|
|
7406
|
-
CURSOR_API_KEY_FILE =
|
|
8033
|
+
SYNKRO_DIR = join6(homedir6(), ".synkro");
|
|
8034
|
+
CLAUDE_CREDS_DIR = join6(SYNKRO_DIR, "claude-creds");
|
|
8035
|
+
CLAUDE_CREDS_FILE = join6(CLAUDE_CREDS_DIR, ".credentials.json");
|
|
8036
|
+
CURSOR_CREDS_DIR = join6(SYNKRO_DIR, "cursor-creds");
|
|
8037
|
+
CURSOR_API_KEY_FILE = join6(CURSOR_CREDS_DIR, "api-key");
|
|
7407
8038
|
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
7408
8039
|
LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
|
|
7409
|
-
LAUNCHD_PLIST =
|
|
8040
|
+
LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
7410
8041
|
REFRESH_INTERVAL_SECONDS = 45 * 60;
|
|
7411
8042
|
KeychainExportError = class extends Error {
|
|
7412
8043
|
constructor(message, cause) {
|
|
@@ -7443,9 +8074,9 @@ __export(dockerInstall_exports, {
|
|
|
7443
8074
|
waitForContainerReady: () => waitForContainerReady,
|
|
7444
8075
|
waitForWorkersReady: () => waitForWorkersReady
|
|
7445
8076
|
});
|
|
7446
|
-
import { copyFileSync, existsSync as
|
|
7447
|
-
import { homedir as
|
|
7448
|
-
import { join as
|
|
8077
|
+
import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
8078
|
+
import { homedir as homedir7 } from "os";
|
|
8079
|
+
import { join as join7 } from "path";
|
|
7449
8080
|
import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
|
|
7450
8081
|
function splitWorkers(total, providers) {
|
|
7451
8082
|
const t = Math.max(0, Math.floor(total));
|
|
@@ -7476,7 +8107,7 @@ function resolveGraderPool(parsed) {
|
|
|
7476
8107
|
if (np === "claude_code") counts.claude_code = Math.max(0, Math.floor(v));
|
|
7477
8108
|
else if (np === "cursor") counts.cursor = Math.max(0, Math.floor(v));
|
|
7478
8109
|
else if (flagUnknown) {
|
|
7479
|
-
warnings.push(
|
|
8110
|
+
warnings.push(`synkro.toml: ${where}.${k} is not a known provider (use claude-code or cursor) \u2014 ignored`);
|
|
7480
8111
|
}
|
|
7481
8112
|
}
|
|
7482
8113
|
};
|
|
@@ -7487,7 +8118,7 @@ function resolveGraderPool(parsed) {
|
|
|
7487
8118
|
if (rawPool != null && rawPool !== "auto") {
|
|
7488
8119
|
const np = normalizeProvider(String(rawPool));
|
|
7489
8120
|
if (np) pool = np;
|
|
7490
|
-
else warnings.push(
|
|
8121
|
+
else warnings.push(`synkro.toml: grader.pool="${rawPool}" is not recognized (use auto | claude-code | cursor) \u2014 falling back to auto`);
|
|
7491
8122
|
}
|
|
7492
8123
|
if (counts.claude_code != null || counts.cursor != null) {
|
|
7493
8124
|
return { claudeWorkers: counts.claude_code, cursorWorkers: counts.cursor, pool, warnings };
|
|
@@ -7497,57 +8128,52 @@ function resolveGraderPool(parsed) {
|
|
|
7497
8128
|
function poolLabel(pool) {
|
|
7498
8129
|
return pool === "claude_code" ? "claude-code" : pool;
|
|
7499
8130
|
}
|
|
7500
|
-
function
|
|
8131
|
+
function parseTomlValue(raw) {
|
|
8132
|
+
const v = raw.trim();
|
|
8133
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
8134
|
+
const inner = v.slice(1, -1).trim();
|
|
8135
|
+
if (!inner) return [];
|
|
8136
|
+
return inner.split(",").map((s) => parseTomlValue(s)).filter((s) => s !== "");
|
|
8137
|
+
}
|
|
8138
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1);
|
|
8139
|
+
if (v === "true") return true;
|
|
8140
|
+
if (v === "false") return false;
|
|
8141
|
+
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
|
|
8142
|
+
return v;
|
|
8143
|
+
}
|
|
8144
|
+
function parseSynkroToml(raw) {
|
|
7501
8145
|
const result = {};
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
if (
|
|
7508
|
-
|
|
7509
|
-
if (
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
}
|
|
7525
|
-
} else if (/^ - /.test(line)) {
|
|
7526
|
-
if (!currentArr) currentArr = [];
|
|
7527
|
-
currentArr.push(line.replace(/^ - /, "").trim());
|
|
7528
|
-
} else if (/^ \S/.test(line) && line.includes(":")) {
|
|
7529
|
-
if (!currentObj) currentObj = {};
|
|
7530
|
-
const ci = line.indexOf(":");
|
|
7531
|
-
const k = line.slice(0, ci).trim();
|
|
7532
|
-
const v = line.slice(ci + 1).trim();
|
|
7533
|
-
if (v === "true") currentObj[k] = true;
|
|
7534
|
-
else if (v === "false") currentObj[k] = false;
|
|
7535
|
-
else if (/^\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
|
|
7536
|
-
else currentObj[k] = v;
|
|
7537
|
-
}
|
|
7538
|
-
}
|
|
7539
|
-
if (currentObj && currentKey) result[currentKey] = currentObj;
|
|
7540
|
-
if (currentArr && currentKey) result[currentKey] = currentArr;
|
|
8146
|
+
let section = null;
|
|
8147
|
+
for (const rawLine of raw.split("\n")) {
|
|
8148
|
+
const line = rawLine.trim();
|
|
8149
|
+
if (!line || line.startsWith("#")) continue;
|
|
8150
|
+
const sec = line.match(/^\[([^\]]+)\]$/);
|
|
8151
|
+
if (sec) {
|
|
8152
|
+
section = sec[1].trim();
|
|
8153
|
+
if (!result[section]) result[section] = {};
|
|
8154
|
+
continue;
|
|
8155
|
+
}
|
|
8156
|
+
const eq = line.indexOf("=");
|
|
8157
|
+
if (eq === -1) continue;
|
|
8158
|
+
const key = line.slice(0, eq).trim().replace(/^["']|["']$/g, "");
|
|
8159
|
+
let valRaw = line.slice(eq + 1).trim();
|
|
8160
|
+
if (!valRaw.startsWith('"') && !valRaw.startsWith("'") && !valRaw.startsWith("[")) {
|
|
8161
|
+
const h = valRaw.indexOf("#");
|
|
8162
|
+
if (h !== -1) valRaw = valRaw.slice(0, h).trim();
|
|
8163
|
+
}
|
|
8164
|
+
const value = parseTomlValue(valRaw);
|
|
8165
|
+
if (section) result[section][key] = value;
|
|
8166
|
+
else result[key] = value;
|
|
8167
|
+
}
|
|
7541
8168
|
return result;
|
|
7542
8169
|
}
|
|
7543
8170
|
function readSynkroFileConfig() {
|
|
7544
8171
|
try {
|
|
7545
8172
|
const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7546
8173
|
if (!root) return { pool: "auto", warnings: [] };
|
|
7547
|
-
const fp =
|
|
7548
|
-
if (!
|
|
7549
|
-
const
|
|
7550
|
-
const parsed = raw.trimStart().startsWith("{") ? JSON.parse(raw) : parseSynkroYaml(raw);
|
|
8174
|
+
const fp = join7(root, "synkro.toml");
|
|
8175
|
+
if (!existsSync9(fp)) return { pool: "auto", warnings: [] };
|
|
8176
|
+
const parsed = parseSynkroToml(readFileSync7(fp, "utf-8"));
|
|
7551
8177
|
return resolveGraderPool(parsed);
|
|
7552
8178
|
} catch {
|
|
7553
8179
|
}
|
|
@@ -7622,7 +8248,7 @@ function assertDockerAvailable() {
|
|
|
7622
8248
|
}
|
|
7623
8249
|
function claudeCredsHostDir() {
|
|
7624
8250
|
if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
|
|
7625
|
-
return
|
|
8251
|
+
return join7(homedir7(), ".claude");
|
|
7626
8252
|
}
|
|
7627
8253
|
function resolveSynkroBin() {
|
|
7628
8254
|
const which2 = spawnSync2("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -7678,11 +8304,11 @@ async function dockerInstall(opts = {}) {
|
|
|
7678
8304
|
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
7679
8305
|
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
7680
8306
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
7681
|
-
const hostClaudeJson =
|
|
7682
|
-
if (
|
|
8307
|
+
const hostClaudeJson = join7(homedir7(), ".claude.json");
|
|
8308
|
+
if (existsSync9(hostClaudeJson)) {
|
|
7683
8309
|
copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
|
|
7684
8310
|
}
|
|
7685
|
-
if (!
|
|
8311
|
+
if (!existsSync9(MCP_JWT_PATH)) {
|
|
7686
8312
|
throw new DockerInstallError(
|
|
7687
8313
|
`MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
|
|
7688
8314
|
);
|
|
@@ -7710,7 +8336,7 @@ async function dockerInstall(opts = {}) {
|
|
|
7710
8336
|
console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
|
|
7711
8337
|
}
|
|
7712
8338
|
} else {
|
|
7713
|
-
mkdirSync7(
|
|
8339
|
+
mkdirSync7(join7(homedir7(), ".claude"), { recursive: true });
|
|
7714
8340
|
}
|
|
7715
8341
|
const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
7716
8342
|
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
@@ -7763,7 +8389,7 @@ async function dockerInstall(opts = {}) {
|
|
|
7763
8389
|
"-v",
|
|
7764
8390
|
`${credsDir}:/home/synkro/.claude:rw`,
|
|
7765
8391
|
"-v",
|
|
7766
|
-
`${
|
|
8392
|
+
`${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
|
|
7767
8393
|
"-v",
|
|
7768
8394
|
`${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
|
|
7769
8395
|
// Cursor creds — mounted RW so the in-container refresher can rotate the
|
|
@@ -7943,7 +8569,7 @@ async function dockerSafeStart() {
|
|
|
7943
8569
|
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
7944
8570
|
}
|
|
7945
8571
|
const pgCheck = checkPgdata();
|
|
7946
|
-
if (
|
|
8572
|
+
if (existsSync9(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
|
|
7947
8573
|
if (pgCheck.healthy) {
|
|
7948
8574
|
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
7949
8575
|
} else {
|
|
@@ -7983,7 +8609,7 @@ async function dockerSafeRestart() {
|
|
|
7983
8609
|
return { ok: startResult.ok, stop: stopResult, start: startResult };
|
|
7984
8610
|
}
|
|
7985
8611
|
function checkPgdata() {
|
|
7986
|
-
if (!
|
|
8612
|
+
if (!existsSync9(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
7987
8613
|
const entries = readdirSync2(PGDATA_PATH);
|
|
7988
8614
|
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
7989
8615
|
const hasPidFile = entries.includes("postmaster.pid");
|
|
@@ -8000,11 +8626,11 @@ var init_dockerInstall = __esm({
|
|
|
8000
8626
|
"use strict";
|
|
8001
8627
|
init_agentDetect();
|
|
8002
8628
|
init_macKeychain();
|
|
8003
|
-
SYNKRO_DIR2 =
|
|
8004
|
-
MCP_JWT_PATH =
|
|
8005
|
-
PGDATA_PATH =
|
|
8006
|
-
CLAUDE_HOST_STATE_DIR =
|
|
8007
|
-
CLAUDE_HOST_STATE_FILE =
|
|
8629
|
+
SYNKRO_DIR2 = join7(homedir7(), ".synkro");
|
|
8630
|
+
MCP_JWT_PATH = join7(SYNKRO_DIR2, ".mcp-jwt");
|
|
8631
|
+
PGDATA_PATH = join7(SYNKRO_DIR2, "pgdata");
|
|
8632
|
+
CLAUDE_HOST_STATE_DIR = join7(SYNKRO_DIR2, "claude-host-state");
|
|
8633
|
+
CLAUDE_HOST_STATE_FILE = join7(CLAUDE_HOST_STATE_DIR, ".claude.json");
|
|
8008
8634
|
HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
|
|
8009
8635
|
HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
|
|
8010
8636
|
HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
|
|
@@ -8019,7 +8645,7 @@ var init_dockerInstall = __esm({
|
|
|
8019
8645
|
}
|
|
8020
8646
|
cause;
|
|
8021
8647
|
};
|
|
8022
|
-
BACKUP_DIR =
|
|
8648
|
+
BACKUP_DIR = join7(SYNKRO_DIR2, "pgdata-backups");
|
|
8023
8649
|
}
|
|
8024
8650
|
});
|
|
8025
8651
|
|
|
@@ -8032,14 +8658,14 @@ __export(setupGithub_exports, {
|
|
|
8032
8658
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8033
8659
|
import { stdin as input, stdout as output } from "process";
|
|
8034
8660
|
import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
|
|
8035
|
-
import { existsSync as
|
|
8036
|
-
import { homedir as
|
|
8037
|
-
import { join as
|
|
8661
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
8662
|
+
import { homedir as homedir8, platform as platform3 } from "os";
|
|
8663
|
+
import { join as join8 } from "path";
|
|
8038
8664
|
import { execFile as execFile2 } from "child_process";
|
|
8039
8665
|
function readConfig() {
|
|
8040
|
-
if (!
|
|
8666
|
+
if (!existsSync10(CONFIG_PATH)) return {};
|
|
8041
8667
|
const out = {};
|
|
8042
|
-
for (const line of
|
|
8668
|
+
for (const line of readFileSync8(CONFIG_PATH, "utf-8").split("\n")) {
|
|
8043
8669
|
const t = line.trim();
|
|
8044
8670
|
if (!t || t.startsWith("#")) continue;
|
|
8045
8671
|
const eq = t.indexOf("=");
|
|
@@ -8100,7 +8726,7 @@ function sleep(ms) {
|
|
|
8100
8726
|
return new Promise((r) => setTimeout(r, ms));
|
|
8101
8727
|
}
|
|
8102
8728
|
function captureClaudeSetupToken() {
|
|
8103
|
-
const tmpFile =
|
|
8729
|
+
const tmpFile = join8(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
8104
8730
|
return new Promise((resolve4, reject) => {
|
|
8105
8731
|
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
8106
8732
|
stdio: "inherit"
|
|
@@ -8109,7 +8735,7 @@ function captureClaudeSetupToken() {
|
|
|
8109
8735
|
proc.on("close", (code) => {
|
|
8110
8736
|
let raw = "";
|
|
8111
8737
|
try {
|
|
8112
|
-
raw =
|
|
8738
|
+
raw = readFileSync8(tmpFile, "utf-8");
|
|
8113
8739
|
} catch (e) {
|
|
8114
8740
|
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
8115
8741
|
return;
|
|
@@ -8381,8 +9007,8 @@ var init_setupGithub = __esm({
|
|
|
8381
9007
|
"use strict";
|
|
8382
9008
|
init_githubSetup();
|
|
8383
9009
|
init_stub();
|
|
8384
|
-
SYNKRO_DIR3 =
|
|
8385
|
-
CONFIG_PATH =
|
|
9010
|
+
SYNKRO_DIR3 = join8(homedir8(), ".synkro");
|
|
9011
|
+
CONFIG_PATH = join8(SYNKRO_DIR3, "config.env");
|
|
8386
9012
|
}
|
|
8387
9013
|
});
|
|
8388
9014
|
|
|
@@ -8396,16 +9022,16 @@ __export(install_exports, {
|
|
|
8396
9022
|
syncSkillFiles: () => syncSkillFiles,
|
|
8397
9023
|
writeHookScripts: () => writeHookScripts
|
|
8398
9024
|
});
|
|
8399
|
-
import { existsSync as
|
|
8400
|
-
import { homedir as
|
|
8401
|
-
import { join as
|
|
9025
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
|
|
9026
|
+
import { homedir as homedir9 } from "os";
|
|
9027
|
+
import { join as join9 } from "path";
|
|
8402
9028
|
import { execSync as execSync6 } from "child_process";
|
|
8403
9029
|
import { createInterface as createInterface3 } from "readline";
|
|
8404
9030
|
function resolvePersistedHookMode() {
|
|
8405
9031
|
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
8406
9032
|
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
8407
9033
|
try {
|
|
8408
|
-
const env =
|
|
9034
|
+
const env = readFileSync9(CONFIG_PATH2, "utf-8");
|
|
8409
9035
|
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
8410
9036
|
} catch {
|
|
8411
9037
|
}
|
|
@@ -8558,30 +9184,31 @@ function ensureSynkroDir() {
|
|
|
8558
9184
|
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
8559
9185
|
mkdirSync8(BIN_DIR, { recursive: true });
|
|
8560
9186
|
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
8561
|
-
mkdirSync8(
|
|
9187
|
+
mkdirSync8(join9(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
8562
9188
|
}
|
|
8563
9189
|
function writeHookScripts(mode = "stub") {
|
|
8564
|
-
const installExtractCorePath =
|
|
8565
|
-
const bashScriptPath =
|
|
8566
|
-
const bashFollowupScriptPath =
|
|
8567
|
-
const editPrecheckScriptPath =
|
|
8568
|
-
const cwePrecheckScriptPath =
|
|
8569
|
-
const cvePrecheckScriptPath =
|
|
8570
|
-
const planJudgeScriptPath =
|
|
8571
|
-
const agentJudgeScriptPath =
|
|
8572
|
-
const stopSummaryScriptPath =
|
|
8573
|
-
const sessionStartScriptPath =
|
|
8574
|
-
const transcriptSyncScriptPath =
|
|
8575
|
-
const userPromptSubmitScriptPath =
|
|
8576
|
-
const commonScriptPath =
|
|
8577
|
-
const commonBashScriptPath =
|
|
8578
|
-
const installScanScriptPath =
|
|
8579
|
-
const cursorBashJudgePath =
|
|
8580
|
-
const cursorEditCapturePath =
|
|
8581
|
-
const cursorAgentCapturePath =
|
|
8582
|
-
const mcpStdioProxyPath =
|
|
9190
|
+
const installExtractCorePath = join9(HOOKS_DIR, "installExtractCore.ts");
|
|
9191
|
+
const bashScriptPath = join9(HOOKS_DIR, "cc-bash-judge.ts");
|
|
9192
|
+
const bashFollowupScriptPath = join9(HOOKS_DIR, "cc-bash-followup.ts");
|
|
9193
|
+
const editPrecheckScriptPath = join9(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
9194
|
+
const cwePrecheckScriptPath = join9(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
9195
|
+
const cvePrecheckScriptPath = join9(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
9196
|
+
const planJudgeScriptPath = join9(HOOKS_DIR, "cc-plan-judge.ts");
|
|
9197
|
+
const agentJudgeScriptPath = join9(HOOKS_DIR, "cc-agent-judge.ts");
|
|
9198
|
+
const stopSummaryScriptPath = join9(HOOKS_DIR, "cc-stop-summary.ts");
|
|
9199
|
+
const sessionStartScriptPath = join9(HOOKS_DIR, "cc-session-start.ts");
|
|
9200
|
+
const transcriptSyncScriptPath = join9(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
9201
|
+
const userPromptSubmitScriptPath = join9(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
9202
|
+
const commonScriptPath = join9(HOOKS_DIR, "_synkro-common.ts");
|
|
9203
|
+
const commonBashScriptPath = join9(HOOKS_DIR, "_synkro-common.sh");
|
|
9204
|
+
const installScanScriptPath = join9(HOOKS_DIR, "cc-install-scan.ts");
|
|
9205
|
+
const cursorBashJudgePath = join9(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
9206
|
+
const cursorEditCapturePath = join9(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
9207
|
+
const cursorAgentCapturePath = join9(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
9208
|
+
const mcpStdioProxyPath = join9(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
9209
|
+
const taskActivateIntentScriptPath = join9(HOOKS_DIR, "cc-task-activate-intent.ts");
|
|
8583
9210
|
if (mode === "stub") {
|
|
8584
|
-
const stubCommonPath =
|
|
9211
|
+
const stubCommonPath = join9(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8585
9212
|
const stubFiles = [
|
|
8586
9213
|
[stubCommonPath, STUB_COMMON_TS],
|
|
8587
9214
|
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
@@ -8596,19 +9223,20 @@ function writeHookScripts(mode = "stub") {
|
|
|
8596
9223
|
[transcriptSyncScriptPath, STUB_TRANSCRIPT_SYNC_TS],
|
|
8597
9224
|
[userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
|
|
8598
9225
|
[installScanScriptPath, STUB_INSTALL_SCAN_TS],
|
|
9226
|
+
[taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS],
|
|
8599
9227
|
[cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
|
|
8600
9228
|
[cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
|
|
8601
9229
|
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
8602
9230
|
];
|
|
8603
9231
|
for (const [p, content] of stubFiles) {
|
|
8604
|
-
|
|
9232
|
+
writeFileSync8(p, content, "utf-8");
|
|
8605
9233
|
chmodSync2(p, 493);
|
|
8606
9234
|
}
|
|
8607
|
-
|
|
9235
|
+
writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8608
9236
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8609
9237
|
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
8610
9238
|
try {
|
|
8611
|
-
unlinkSync4(
|
|
9239
|
+
unlinkSync4(join9(HOOKS_DIR, stale));
|
|
8612
9240
|
} catch {
|
|
8613
9241
|
}
|
|
8614
9242
|
}
|
|
@@ -8627,28 +9255,30 @@ function writeHookScripts(mode = "stub") {
|
|
|
8627
9255
|
installScanScript: installScanScriptPath,
|
|
8628
9256
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
8629
9257
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8630
|
-
cursorAgentCaptureScript: cursorAgentCapturePath
|
|
9258
|
+
cursorAgentCaptureScript: cursorAgentCapturePath,
|
|
9259
|
+
taskActivateIntentScript: taskActivateIntentScriptPath
|
|
8631
9260
|
};
|
|
8632
9261
|
}
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
writeFileSync7(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
9262
|
+
writeFileSync8(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
9263
|
+
writeFileSync8(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
9264
|
+
writeFileSync8(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
9265
|
+
writeFileSync8(cwePrecheckScriptPath, CWE_PRECHECK_TS, "utf-8");
|
|
9266
|
+
writeFileSync8(cvePrecheckScriptPath, CVE_PRECHECK_TS, "utf-8");
|
|
9267
|
+
writeFileSync8(planJudgeScriptPath, PLAN_JUDGE_TS, "utf-8");
|
|
9268
|
+
writeFileSync8(agentJudgeScriptPath, AGENT_JUDGE_TS, "utf-8");
|
|
9269
|
+
writeFileSync8(stopSummaryScriptPath, STOP_SUMMARY_TS, "utf-8");
|
|
9270
|
+
writeFileSync8(sessionStartScriptPath, SESSION_START_TS, "utf-8");
|
|
9271
|
+
writeFileSync8(transcriptSyncScriptPath, TRANSCRIPT_SYNC_TS, "utf-8");
|
|
9272
|
+
writeFileSync8(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
|
|
9273
|
+
writeFileSync8(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
9274
|
+
writeFileSync8(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
9275
|
+
writeFileSync8(installScanScriptPath, INSTALL_SCAN_TS, "utf-8");
|
|
9276
|
+
writeFileSync8(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
9277
|
+
writeFileSync8(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
9278
|
+
writeFileSync8(cursorAgentCapturePath, CURSOR_AGENT_CAPTURE_TS, "utf-8");
|
|
9279
|
+
writeFileSync8(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
9280
|
+
writeFileSync8(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
9281
|
+
writeFileSync8(taskActivateIntentScriptPath, STUB_TASK_ACTIVATE_INTENT_TS, "utf-8");
|
|
8652
9282
|
chmodSync2(bashScriptPath, 493);
|
|
8653
9283
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
8654
9284
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -8668,8 +9298,9 @@ function writeHookScripts(mode = "stub") {
|
|
|
8668
9298
|
chmodSync2(cursorAgentCapturePath, 493);
|
|
8669
9299
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8670
9300
|
chmodSync2(installExtractCorePath, 493);
|
|
9301
|
+
chmodSync2(taskActivateIntentScriptPath, 493);
|
|
8671
9302
|
try {
|
|
8672
|
-
unlinkSync4(
|
|
9303
|
+
unlinkSync4(join9(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
8673
9304
|
} catch {
|
|
8674
9305
|
}
|
|
8675
9306
|
return {
|
|
@@ -8687,7 +9318,8 @@ function writeHookScripts(mode = "stub") {
|
|
|
8687
9318
|
installScanScript: installScanScriptPath,
|
|
8688
9319
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
8689
9320
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8690
|
-
cursorAgentCaptureScript: cursorAgentCapturePath
|
|
9321
|
+
cursorAgentCaptureScript: cursorAgentCapturePath,
|
|
9322
|
+
taskActivateIntentScript: taskActivateIntentScriptPath
|
|
8691
9323
|
};
|
|
8692
9324
|
}
|
|
8693
9325
|
function sanitizeConfigValue(raw, maxLen = 256) {
|
|
@@ -8699,11 +9331,11 @@ function shellQuoteSingle(value) {
|
|
|
8699
9331
|
}
|
|
8700
9332
|
function resolveSynkroBundle() {
|
|
8701
9333
|
const scriptPath = process.argv[1];
|
|
8702
|
-
if (scriptPath &&
|
|
9334
|
+
if (scriptPath && existsSync11(scriptPath)) return scriptPath;
|
|
8703
9335
|
return null;
|
|
8704
9336
|
}
|
|
8705
9337
|
function writeConfigEnv(opts) {
|
|
8706
|
-
const credsPath =
|
|
9338
|
+
const credsPath = join9(SYNKRO_DIR4, "credentials.json");
|
|
8707
9339
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
8708
9340
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
8709
9341
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -8719,7 +9351,7 @@ function writeConfigEnv(opts) {
|
|
|
8719
9351
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8720
9352
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8721
9353
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8722
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
9354
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.54")}`
|
|
8723
9355
|
];
|
|
8724
9356
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8725
9357
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8735,15 +9367,15 @@ function writeConfigEnv(opts) {
|
|
|
8735
9367
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
8736
9368
|
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
8737
9369
|
lines.push("");
|
|
8738
|
-
|
|
9370
|
+
writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
8739
9371
|
chmodSync2(CONFIG_PATH2, 384);
|
|
8740
9372
|
}
|
|
8741
9373
|
function resolveDeploymentMode() {
|
|
8742
9374
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
8743
9375
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
8744
9376
|
try {
|
|
8745
|
-
if (
|
|
8746
|
-
const m =
|
|
9377
|
+
if (existsSync11(CONFIG_PATH2)) {
|
|
9378
|
+
const m = readFileSync9(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
8747
9379
|
const val = m?.[1]?.toLowerCase();
|
|
8748
9380
|
if (val === "bare-host" || val === "docker") return val;
|
|
8749
9381
|
}
|
|
@@ -8770,16 +9402,16 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8770
9402
|
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
8771
9403
|
} catch {
|
|
8772
9404
|
}
|
|
8773
|
-
const claudeDir =
|
|
9405
|
+
const claudeDir = join9(homedir9(), ".claude");
|
|
8774
9406
|
try {
|
|
8775
|
-
const settings = JSON.parse(
|
|
9407
|
+
const settings = JSON.parse(readFileSync9(join9(claudeDir, "settings.json"), "utf-8"));
|
|
8776
9408
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
8777
9409
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
8778
9410
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
8779
9411
|
} catch {
|
|
8780
9412
|
}
|
|
8781
9413
|
try {
|
|
8782
|
-
const mcpCache = JSON.parse(
|
|
9414
|
+
const mcpCache = JSON.parse(readFileSync9(join9(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
8783
9415
|
const mcpNames = Object.keys(mcpCache);
|
|
8784
9416
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
8785
9417
|
} catch {
|
|
@@ -8791,10 +9423,10 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8791
9423
|
} catch {
|
|
8792
9424
|
}
|
|
8793
9425
|
try {
|
|
8794
|
-
const sessionsDir =
|
|
9426
|
+
const sessionsDir = join9(claudeDir, "sessions");
|
|
8795
9427
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
8796
9428
|
for (const f of files) {
|
|
8797
|
-
const s = JSON.parse(
|
|
9429
|
+
const s = JSON.parse(readFileSync9(join9(sessionsDir, f), "utf-8"));
|
|
8798
9430
|
if (s.version) {
|
|
8799
9431
|
meta.cc_version = meta.cc_version || s.version;
|
|
8800
9432
|
break;
|
|
@@ -8969,9 +9601,9 @@ async function installCommand(opts = {}) {
|
|
|
8969
9601
|
const scripts = writeHookScripts(hookMode);
|
|
8970
9602
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8971
9603
|
for (const mode of ["edit", "bash"]) {
|
|
8972
|
-
const pidFile =
|
|
9604
|
+
const pidFile = join9(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
8973
9605
|
try {
|
|
8974
|
-
const pid = parseInt(
|
|
9606
|
+
const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
|
|
8975
9607
|
if (pid > 0) {
|
|
8976
9608
|
process.kill(pid, "SIGTERM");
|
|
8977
9609
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -8997,6 +9629,7 @@ async function installCommand(opts = {}) {
|
|
|
8997
9629
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
8998
9630
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
8999
9631
|
installScanScriptPath: scripts.installScanScript,
|
|
9632
|
+
taskActivateIntentScriptPath: scripts.taskActivateIntentScript,
|
|
9000
9633
|
skipTranscriptSync: !transcriptConsent
|
|
9001
9634
|
});
|
|
9002
9635
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
@@ -9050,7 +9683,7 @@ async function installCommand(opts = {}) {
|
|
|
9050
9683
|
if (mintResp.ok) {
|
|
9051
9684
|
const minted = await mintResp.json();
|
|
9052
9685
|
mcpJwt = minted.token;
|
|
9053
|
-
|
|
9686
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
|
|
9054
9687
|
} else {
|
|
9055
9688
|
console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
|
|
9056
9689
|
}
|
|
@@ -9078,7 +9711,7 @@ async function installCommand(opts = {}) {
|
|
|
9078
9711
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
9079
9712
|
}
|
|
9080
9713
|
const minted = await mintResp.json();
|
|
9081
|
-
|
|
9714
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
9082
9715
|
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
9083
9716
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
9084
9717
|
console.log(` url: ${mcp.url}`);
|
|
@@ -9095,8 +9728,8 @@ async function installCommand(opts = {}) {
|
|
|
9095
9728
|
if (hasCursor && !opts.noMcp) {
|
|
9096
9729
|
try {
|
|
9097
9730
|
if (useLocalMcp) {
|
|
9098
|
-
const jwtPath =
|
|
9099
|
-
if (!
|
|
9731
|
+
const jwtPath = join9(SYNKRO_DIR4, ".mcp-jwt");
|
|
9732
|
+
if (!existsSync11(jwtPath)) {
|
|
9100
9733
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
9101
9734
|
method: "POST",
|
|
9102
9735
|
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -9104,7 +9737,7 @@ async function installCommand(opts = {}) {
|
|
|
9104
9737
|
});
|
|
9105
9738
|
if (mintResp.ok) {
|
|
9106
9739
|
const minted = await mintResp.json();
|
|
9107
|
-
|
|
9740
|
+
writeFileSync8(jwtPath, minted.token + "\n", { mode: 384 });
|
|
9108
9741
|
}
|
|
9109
9742
|
}
|
|
9110
9743
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
|
|
@@ -9124,7 +9757,7 @@ async function installCommand(opts = {}) {
|
|
|
9124
9757
|
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
9125
9758
|
}
|
|
9126
9759
|
const minted = await mintResp.json();
|
|
9127
|
-
|
|
9760
|
+
writeFileSync8(join9(SYNKRO_DIR4, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
|
|
9128
9761
|
const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
9129
9762
|
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
9130
9763
|
console.log(` url: ${mcp.url}`);
|
|
@@ -9175,7 +9808,7 @@ async function installCommand(opts = {}) {
|
|
|
9175
9808
|
claudeWorkers = 0;
|
|
9176
9809
|
cursorWorkers = 8;
|
|
9177
9810
|
}
|
|
9178
|
-
console.log(` .
|
|
9811
|
+
console.log(` synkro.toml: explicit workers \u2014 ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
9179
9812
|
} else {
|
|
9180
9813
|
const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
9181
9814
|
const synkroFilePool = sf?.grader?.pool || "auto";
|
|
@@ -9189,7 +9822,7 @@ async function installCommand(opts = {}) {
|
|
|
9189
9822
|
if (hasCursor) providers.push("cursor");
|
|
9190
9823
|
}
|
|
9191
9824
|
({ claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers));
|
|
9192
|
-
if (synkroFilePool !== "auto") console.log(` .
|
|
9825
|
+
if (synkroFilePool !== "auto") console.log(` synkro.toml: grader pool set to ${poolLabel(synkroFilePool)}`);
|
|
9193
9826
|
}
|
|
9194
9827
|
console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
9195
9828
|
const connectedRepo = detectGitRepo2() || void 0;
|
|
@@ -9200,7 +9833,7 @@ async function installCommand(opts = {}) {
|
|
|
9200
9833
|
const ready = await waitForContainerReady(6e4);
|
|
9201
9834
|
if (ready) {
|
|
9202
9835
|
console.log(" \u2713 container ready");
|
|
9203
|
-
const mcpJwt =
|
|
9836
|
+
const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9204
9837
|
try {
|
|
9205
9838
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
9206
9839
|
method: "POST",
|
|
@@ -9238,7 +9871,7 @@ async function installCommand(opts = {}) {
|
|
|
9238
9871
|
try {
|
|
9239
9872
|
let mcpToken = "";
|
|
9240
9873
|
try {
|
|
9241
|
-
mcpToken =
|
|
9874
|
+
mcpToken = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9242
9875
|
} catch {
|
|
9243
9876
|
}
|
|
9244
9877
|
if (mcpToken) {
|
|
@@ -9319,66 +9952,70 @@ async function installCommand(opts = {}) {
|
|
|
9319
9952
|
await setupGithubCommand2({ nonInteractive: true, githubToken: ghToken });
|
|
9320
9953
|
}
|
|
9321
9954
|
console.log("\u2713 Synkro installed.");
|
|
9955
|
+
if (process.stdin.isTTY && claudeDesktopInstalled()) {
|
|
9956
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9957
|
+
const answer = await new Promise((resolve4) => rl.question("\nWould you like Claude Desktop observability? This routes Claude Desktop through a local proxy so its conversations appear in your dashboard (you can stop it anytime with Ctrl+C). [y/N]: ", resolve4));
|
|
9958
|
+
rl.close();
|
|
9959
|
+
if (/^y(es)?$/i.test(answer.trim())) {
|
|
9960
|
+
console.log("\nStarting Claude Desktop observability \u2014 press Ctrl+C to stop.\n");
|
|
9961
|
+
await runClaudeDesktopTap();
|
|
9962
|
+
} else {
|
|
9963
|
+
console.log(" Skipped. You can enable it later by re-running `synkro install`.");
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9966
|
+
}
|
|
9967
|
+
function parseTomlValue2(raw) {
|
|
9968
|
+
const v = raw.trim();
|
|
9969
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
9970
|
+
const inner = v.slice(1, -1).trim();
|
|
9971
|
+
if (!inner) return [];
|
|
9972
|
+
return inner.split(",").map((s) => parseTomlValue2(s)).filter((s) => s !== "");
|
|
9973
|
+
}
|
|
9974
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1);
|
|
9975
|
+
if (v === "true") return true;
|
|
9976
|
+
if (v === "false") return false;
|
|
9977
|
+
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
|
|
9978
|
+
return v;
|
|
9322
9979
|
}
|
|
9323
|
-
function
|
|
9980
|
+
function parseSynkroToml2(raw) {
|
|
9324
9981
|
const result = {};
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
if (
|
|
9331
|
-
|
|
9332
|
-
if (
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
}
|
|
9348
|
-
} else if (/^ - /.test(line)) {
|
|
9349
|
-
if (!currentArr) currentArr = [];
|
|
9350
|
-
currentArr.push(line.replace(/^ - /, "").trim());
|
|
9351
|
-
} else if (/^ \S/.test(line) && line.includes(":")) {
|
|
9352
|
-
if (!currentObj) currentObj = {};
|
|
9353
|
-
const colonIdx = line.indexOf(":");
|
|
9354
|
-
const k = line.slice(0, colonIdx).trim();
|
|
9355
|
-
const v = line.slice(colonIdx + 1).trim();
|
|
9356
|
-
if (v === "true") currentObj[k] = true;
|
|
9357
|
-
else if (v === "false") currentObj[k] = false;
|
|
9358
|
-
else if (/^\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
|
|
9359
|
-
else currentObj[k] = v;
|
|
9360
|
-
}
|
|
9361
|
-
}
|
|
9362
|
-
if (currentObj && currentKey) result[currentKey] = currentObj;
|
|
9363
|
-
if (currentArr && currentKey) result[currentKey] = currentArr;
|
|
9982
|
+
let section = null;
|
|
9983
|
+
for (const rawLine of raw.split("\n")) {
|
|
9984
|
+
const line = rawLine.trim();
|
|
9985
|
+
if (!line || line.startsWith("#")) continue;
|
|
9986
|
+
const sec = line.match(/^\[([^\]]+)\]$/);
|
|
9987
|
+
if (sec) {
|
|
9988
|
+
section = sec[1].trim();
|
|
9989
|
+
if (!result[section]) result[section] = {};
|
|
9990
|
+
continue;
|
|
9991
|
+
}
|
|
9992
|
+
const eq = line.indexOf("=");
|
|
9993
|
+
if (eq === -1) continue;
|
|
9994
|
+
const key = line.slice(0, eq).trim().replace(/^["']|["']$/g, "");
|
|
9995
|
+
let valRaw = line.slice(eq + 1).trim();
|
|
9996
|
+
if (!valRaw.startsWith('"') && !valRaw.startsWith("'") && !valRaw.startsWith("[")) {
|
|
9997
|
+
const h = valRaw.indexOf("#");
|
|
9998
|
+
if (h !== -1) valRaw = valRaw.slice(0, h).trim();
|
|
9999
|
+
}
|
|
10000
|
+
const value = parseTomlValue2(valRaw);
|
|
10001
|
+
if (section) result[section][key] = value;
|
|
10002
|
+
else result[key] = value;
|
|
10003
|
+
}
|
|
9364
10004
|
return result;
|
|
9365
10005
|
}
|
|
9366
|
-
function parseSynkroFileRaw(content) {
|
|
9367
|
-
return content.trimStart().startsWith("{") ? JSON.parse(content) : parseSynkroYaml2(content);
|
|
9368
|
-
}
|
|
9369
10006
|
function writeSynkroFileIfMissing(opts) {
|
|
9370
10007
|
try {
|
|
9371
10008
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9372
10009
|
if (!root) return;
|
|
9373
|
-
if (root ===
|
|
9374
|
-
const fp =
|
|
10010
|
+
if (root === homedir9()) return;
|
|
10011
|
+
const fp = join9(root, "synkro.toml");
|
|
9375
10012
|
let hasFile = false;
|
|
9376
10013
|
try {
|
|
9377
10014
|
hasFile = statSync2(fp).isFile();
|
|
9378
10015
|
} catch {
|
|
9379
10016
|
}
|
|
9380
10017
|
if (hasFile) {
|
|
9381
|
-
console.log(` .
|
|
10018
|
+
console.log(` synkro.toml: ${fp} (existing, respected)`);
|
|
9382
10019
|
return;
|
|
9383
10020
|
}
|
|
9384
10021
|
let pool = "auto";
|
|
@@ -9392,27 +10029,26 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
9392
10029
|
if (!opts.hasClaudeCode) pool = "cursor";
|
|
9393
10030
|
}
|
|
9394
10031
|
const mode = opts.gradingMode === "byok" ? "byok" : "local";
|
|
9395
|
-
const
|
|
9396
|
-
"version
|
|
10032
|
+
const toml = [
|
|
10033
|
+
"version = 1",
|
|
9397
10034
|
"",
|
|
9398
|
-
|
|
9399
|
-
...harness.map((h) => ` - ${h}`),
|
|
10035
|
+
`harness = [${harness.map((h) => `"${h}"`).join(", ")}]`,
|
|
9400
10036
|
"",
|
|
9401
|
-
"
|
|
9402
|
-
` pool: ${pool}`,
|
|
9403
|
-
` mode: ${mode}`,
|
|
10037
|
+
'ruleset = "default"',
|
|
9404
10038
|
"",
|
|
9405
|
-
"
|
|
10039
|
+
"skills = []",
|
|
9406
10040
|
"",
|
|
9407
|
-
"
|
|
10041
|
+
"[grader]",
|
|
10042
|
+
`pool = "${pool}"`,
|
|
10043
|
+
`mode = "${mode}"`,
|
|
9408
10044
|
"",
|
|
9409
|
-
"scanning
|
|
9410
|
-
"
|
|
9411
|
-
"
|
|
10045
|
+
"[scanning]",
|
|
10046
|
+
"cwe = true",
|
|
10047
|
+
"cve = true",
|
|
9412
10048
|
""
|
|
9413
10049
|
].join("\n");
|
|
9414
|
-
|
|
9415
|
-
console.log(` .
|
|
10050
|
+
writeFileSync8(fp, toml, "utf-8");
|
|
10051
|
+
console.log(` synkro.toml: wrote ${fp} (pool=${pool}, mode=${mode})`);
|
|
9416
10052
|
} catch {
|
|
9417
10053
|
}
|
|
9418
10054
|
}
|
|
@@ -9420,9 +10056,9 @@ function readFullSynkroFile() {
|
|
|
9420
10056
|
try {
|
|
9421
10057
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9422
10058
|
if (!root) return null;
|
|
9423
|
-
const fp =
|
|
9424
|
-
if (!
|
|
9425
|
-
const parsed =
|
|
10059
|
+
const fp = join9(root, "synkro.toml");
|
|
10060
|
+
if (!existsSync11(fp)) return null;
|
|
10061
|
+
const parsed = parseSynkroToml2(readFileSync9(fp, "utf-8"));
|
|
9426
10062
|
const valid = ["claude-code", "cursor"];
|
|
9427
10063
|
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
9428
10064
|
const resolved = resolveGraderPool(parsed);
|
|
@@ -9455,10 +10091,10 @@ function reconcileHarness() {
|
|
|
9455
10091
|
const wantCC = sf.harness.includes("claude-code");
|
|
9456
10092
|
const wantCursor = sf.harness.includes("cursor");
|
|
9457
10093
|
for (const w of sf.warnings) console.warn(` \u26A0 ${w}`);
|
|
9458
|
-
console.log(
|
|
10094
|
+
console.log(`synkro.toml: harness=[${sf.harness.join(", ")}] pool=${poolLabel(sf.grader.pool)} mode=${sf.grader.mode}`);
|
|
9459
10095
|
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9460
10096
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9461
|
-
const ccSettings =
|
|
10097
|
+
const ccSettings = join9(homedir9(), ".claude", "settings.json");
|
|
9462
10098
|
if (wantCC) {
|
|
9463
10099
|
installCCHooks(ccSettings, {
|
|
9464
10100
|
bashJudgeScriptPath: scripts.bashScript,
|
|
@@ -9472,11 +10108,12 @@ function reconcileHarness() {
|
|
|
9472
10108
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
9473
10109
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
9474
10110
|
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
9475
|
-
installScanScriptPath: scripts.installScanScript
|
|
10111
|
+
installScanScriptPath: scripts.installScanScript,
|
|
10112
|
+
taskActivateIntentScriptPath: scripts.taskActivateIntentScript
|
|
9476
10113
|
});
|
|
9477
10114
|
console.log(" \u2713 Claude Code hooks registered");
|
|
9478
10115
|
try {
|
|
9479
|
-
const mcpJwt =
|
|
10116
|
+
const mcpJwt = readFileSync9(join9(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
9480
10117
|
if (mcpJwt) {
|
|
9481
10118
|
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
9482
10119
|
console.log(" \u2713 Claude Code MCP registered");
|
|
@@ -9487,7 +10124,7 @@ function reconcileHarness() {
|
|
|
9487
10124
|
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
9488
10125
|
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
9489
10126
|
}
|
|
9490
|
-
const cursorHooks =
|
|
10127
|
+
const cursorHooks = join9(homedir9(), ".cursor", "hooks.json");
|
|
9491
10128
|
if (wantCursor) {
|
|
9492
10129
|
installCursorHooks(cursorHooks, {
|
|
9493
10130
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
@@ -9540,29 +10177,31 @@ async function syncSkillFiles() {
|
|
|
9540
10177
|
if (resolved.length === 0) return;
|
|
9541
10178
|
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
9542
10179
|
const tasks = resolved.map((fp) => {
|
|
9543
|
-
const content =
|
|
10180
|
+
const content = readFileSync9(fp, "utf-8");
|
|
9544
10181
|
const source = `skill:${fp.split("/").pop()}`;
|
|
9545
|
-
if (!content.trim())
|
|
10182
|
+
if (!content.trim()) {
|
|
10183
|
+
console.log(` \u2298 skill ${source}: empty file, skipped`);
|
|
10184
|
+
return null;
|
|
10185
|
+
}
|
|
10186
|
+
const lines = content.split("\n").length;
|
|
10187
|
+
console.log(` \u2192 read ${source} (${lines} lines, ${(content.length / 1024).toFixed(1)} KB)`);
|
|
9546
10188
|
return { source, content };
|
|
9547
10189
|
}).filter(Boolean);
|
|
9548
10190
|
if (tasks.length === 0) return;
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
console.log(created > 0 ? ` \u2713 skill ${source}: ${created} new rules added` : ` \u2713 skill ${source}: ${message || "up to date"}`);
|
|
9564
|
-
} else {
|
|
9565
|
-
console.warn(` \u26A0 skill sync failed: ${r.reason}`);
|
|
10191
|
+
console.log(` indexing ${tasks.length} skill file${tasks.length === 1 ? "" : "s"} \u2014 comparing against existing ruleset (this may take a few seconds)...`);
|
|
10192
|
+
for (const { source, content } of tasks) {
|
|
10193
|
+
try {
|
|
10194
|
+
const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/skills/sync`, {
|
|
10195
|
+
method: "POST",
|
|
10196
|
+
headers: { "Content-Type": "application/json" },
|
|
10197
|
+
body: JSON.stringify({ source, content }),
|
|
10198
|
+
signal: AbortSignal.timeout(65e3)
|
|
10199
|
+
});
|
|
10200
|
+
if (!resp.ok) throw new Error(`${resp.status}`);
|
|
10201
|
+
const { created, message } = await resp.json();
|
|
10202
|
+
console.log(created > 0 ? ` \u2713 skill ${source}: ${created} new rule${created === 1 ? "" : "s"} added to index` : ` \u2713 skill ${source}: ${message || "up to date"} \u2014 index unchanged`);
|
|
10203
|
+
} catch (e) {
|
|
10204
|
+
console.warn(` \u26A0 skill ${source}: sync failed (${e instanceof Error ? e.message : String(e)})`);
|
|
9566
10205
|
}
|
|
9567
10206
|
}
|
|
9568
10207
|
}
|
|
@@ -9584,17 +10223,17 @@ function detectGitRepo2() {
|
|
|
9584
10223
|
function getClaudeProjectsFolder() {
|
|
9585
10224
|
const cwd = process.cwd();
|
|
9586
10225
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
9587
|
-
const projectsDir =
|
|
9588
|
-
return
|
|
10226
|
+
const projectsDir = join9(homedir9(), ".claude", "projects", sanitized);
|
|
10227
|
+
return existsSync11(projectsDir) ? projectsDir : null;
|
|
9589
10228
|
}
|
|
9590
10229
|
function extractSessionInsights(projectsDir) {
|
|
9591
10230
|
const insights = [];
|
|
9592
10231
|
const files = readdirSync3(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9593
10232
|
for (const file of files) {
|
|
9594
10233
|
const sessionId = file.replace(".jsonl", "");
|
|
9595
|
-
const filePath =
|
|
10234
|
+
const filePath = join9(projectsDir, file);
|
|
9596
10235
|
try {
|
|
9597
|
-
const content =
|
|
10236
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9598
10237
|
const lines = content.split("\n").filter(Boolean);
|
|
9599
10238
|
for (let i = 0; i < lines.length; i++) {
|
|
9600
10239
|
try {
|
|
@@ -9673,14 +10312,14 @@ function cursorProjectSlug(workspaceRoot) {
|
|
|
9673
10312
|
return workspaceRoot.replace(/^[/]+/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9674
10313
|
}
|
|
9675
10314
|
function getCursorTranscriptsDir() {
|
|
9676
|
-
const dir =
|
|
9677
|
-
return
|
|
10315
|
+
const dir = join9(homedir9(), ".cursor", "projects", cursorProjectSlug(process.cwd()), "agent-transcripts");
|
|
10316
|
+
return existsSync11(dir) ? dir : null;
|
|
9678
10317
|
}
|
|
9679
10318
|
function isSafeConvId(id) {
|
|
9680
10319
|
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
9681
10320
|
}
|
|
9682
10321
|
function parseCursorTranscriptFile(filePath) {
|
|
9683
|
-
const content =
|
|
10322
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9684
10323
|
const lines = content.split("\n").filter(Boolean);
|
|
9685
10324
|
const messages = [];
|
|
9686
10325
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9712,8 +10351,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9712
10351
|
for (let i = 0; i < convDirs.length; i++) {
|
|
9713
10352
|
const convId = convDirs[i];
|
|
9714
10353
|
if (!isSafeConvId(convId)) continue;
|
|
9715
|
-
const filePath =
|
|
9716
|
-
if (!
|
|
10354
|
+
const filePath = join9(dir, convId, `${convId}.jsonl`);
|
|
10355
|
+
if (!existsSync11(filePath)) continue;
|
|
9717
10356
|
try {
|
|
9718
10357
|
const all = parseCursorTranscriptFile(filePath);
|
|
9719
10358
|
const messages = all.length > 500 ? all.slice(-500) : all;
|
|
@@ -9735,8 +10374,8 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9735
10374
|
process.stdout.write(`\r Progress: ${i + 1}/${convDirs.length} sessions (${totalMessages} messages embedded) `);
|
|
9736
10375
|
}
|
|
9737
10376
|
try {
|
|
9738
|
-
const lc =
|
|
9739
|
-
|
|
10377
|
+
const lc = readFileSync9(filePath, "utf-8").split("\n").filter(Boolean).length;
|
|
10378
|
+
writeFileSync8(join9(OFFSETS_DIR, convId), String(lc), "utf-8");
|
|
9740
10379
|
} catch {
|
|
9741
10380
|
}
|
|
9742
10381
|
}
|
|
@@ -9744,7 +10383,7 @@ async function syncCursorTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9744
10383
|
return { sessions: totalSessions, messages: totalMessages };
|
|
9745
10384
|
}
|
|
9746
10385
|
function parseTranscriptFile(filePath) {
|
|
9747
|
-
const content =
|
|
10386
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9748
10387
|
const lines = content.split("\n").filter(Boolean);
|
|
9749
10388
|
const messages = [];
|
|
9750
10389
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9792,7 +10431,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9792
10431
|
for (let i = 0; i < files.length; i++) {
|
|
9793
10432
|
const file = files[i];
|
|
9794
10433
|
const sessionId = file.replace(".jsonl", "");
|
|
9795
|
-
const filePath =
|
|
10434
|
+
const filePath = join9(projectsDir, file);
|
|
9796
10435
|
try {
|
|
9797
10436
|
const allMessages = parseTranscriptFile(filePath);
|
|
9798
10437
|
const messages = allMessages.length > 500 ? allMessages.slice(-500) : allMessages;
|
|
@@ -9814,9 +10453,9 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
9814
10453
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
9815
10454
|
}
|
|
9816
10455
|
try {
|
|
9817
|
-
const content =
|
|
10456
|
+
const content = readFileSync9(join9(projectsDir, file), "utf-8");
|
|
9818
10457
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9819
|
-
|
|
10458
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9820
10459
|
} catch {
|
|
9821
10460
|
}
|
|
9822
10461
|
}
|
|
@@ -9837,7 +10476,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9837
10476
|
const sessions = [];
|
|
9838
10477
|
for (const file of batch) {
|
|
9839
10478
|
const sessionId = file.replace(".jsonl", "");
|
|
9840
|
-
const filePath =
|
|
10479
|
+
const filePath = join9(projectsDir, file);
|
|
9841
10480
|
try {
|
|
9842
10481
|
const allMessages = parseTranscriptFile(filePath);
|
|
9843
10482
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -9866,11 +10505,11 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
9866
10505
|
}
|
|
9867
10506
|
for (const file of batch) {
|
|
9868
10507
|
const sessionId = file.replace(".jsonl", "");
|
|
9869
|
-
const filePath =
|
|
10508
|
+
const filePath = join9(projectsDir, file);
|
|
9870
10509
|
try {
|
|
9871
|
-
const content =
|
|
10510
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
9872
10511
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
9873
|
-
|
|
10512
|
+
writeFileSync8(join9(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
9874
10513
|
} catch {
|
|
9875
10514
|
}
|
|
9876
10515
|
}
|
|
@@ -9893,11 +10532,12 @@ var init_install = __esm({
|
|
|
9893
10532
|
init_repoConnect();
|
|
9894
10533
|
init_projects();
|
|
9895
10534
|
init_promptFetcher();
|
|
10535
|
+
init_claudeDesktopTap();
|
|
9896
10536
|
init_dockerInstall();
|
|
9897
|
-
SYNKRO_DIR4 =
|
|
9898
|
-
HOOKS_DIR =
|
|
9899
|
-
BIN_DIR =
|
|
9900
|
-
CONFIG_PATH2 =
|
|
10537
|
+
SYNKRO_DIR4 = join9(homedir9(), ".synkro");
|
|
10538
|
+
HOOKS_DIR = join9(SYNKRO_DIR4, "hooks");
|
|
10539
|
+
BIN_DIR = join9(SYNKRO_DIR4, "bin");
|
|
10540
|
+
CONFIG_PATH2 = join9(SYNKRO_DIR4, "config.env");
|
|
9901
10541
|
MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
|
|
9902
10542
|
import { readFileSync } from 'node:fs';
|
|
9903
10543
|
import { homedir } from 'node:os';
|
|
@@ -9909,56 +10549,120 @@ const TOKEN_PATH = join(HOME, '.synkro', '.mcp-jwt');
|
|
|
9909
10549
|
const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
|
|
9910
10550
|
const URL = \`http://127.0.0.1:\${PORT}\`;
|
|
9911
10551
|
|
|
9912
|
-
|
|
9913
|
-
|
|
10552
|
+
// Re-read the JWT on EVERY request, not once at spawn: a \`synkro restart\` rotates
|
|
10553
|
+
// the token, and a proxy that cached the old one would 401 every call until it
|
|
10554
|
+
// was respawned (a manual /mcp).
|
|
10555
|
+
function loadToken() {
|
|
10556
|
+
try { return readFileSync(TOKEN_PATH, 'utf-8').trim(); } catch { return ''; }
|
|
10557
|
+
}
|
|
10558
|
+
function write(obj) { try { process.stdout.write(JSON.stringify(obj) + '\\n'); } catch {} }
|
|
10559
|
+
|
|
10560
|
+
// Answer \`initialize\` LOCALLY without touching the backend. Claude Code latches an
|
|
10561
|
+
// MCP server as failed \u2014 forcing a manual /mcp \u2014 only when the child process exits
|
|
10562
|
+
// or the initialize handshake fails. A \`synkro restart\` takes the backend down for
|
|
10563
|
+
// ~30-60s, so if CC (re)connects in that window a forwarded initialize would fail
|
|
10564
|
+
// and the server would be dead until /mcp. Replying here makes the handshake
|
|
10565
|
+
// succeed unconditionally; tools/list and tools/call still hit the backend.
|
|
10566
|
+
function localInitialize(msg) {
|
|
10567
|
+
return {
|
|
10568
|
+
jsonrpc: '2.0',
|
|
10569
|
+
id: msg.id,
|
|
10570
|
+
result: {
|
|
10571
|
+
protocolVersion: (msg && msg.params && msg.params.protocolVersion) || '2024-11-05',
|
|
10572
|
+
capabilities: { tools: {} },
|
|
10573
|
+
serverInfo: { name: 'synkro-guardrails-local', version: '1.0.0' },
|
|
10574
|
+
},
|
|
10575
|
+
};
|
|
10576
|
+
}
|
|
10577
|
+
|
|
10578
|
+
// Cap a single JSON-RPC line. This proxy only carries small tool-call envelopes;
|
|
10579
|
+
// refusing oversized input keeps a malicious or runaway client from buffering
|
|
10580
|
+
// unbounded memory (CWE-400).
|
|
10581
|
+
const MAX_LINE_BYTES = 4 * 1024 * 1024;
|
|
9914
10582
|
|
|
9915
10583
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
9916
10584
|
|
|
9917
10585
|
rl.on('line', async (line) => {
|
|
9918
10586
|
if (!line.trim()) return;
|
|
10587
|
+
if (line.length > MAX_LINE_BYTES) return;
|
|
9919
10588
|
let msg;
|
|
9920
10589
|
try { msg = JSON.parse(line); } catch { return; }
|
|
9921
|
-
|
|
10590
|
+
// Notifications carry no id and need no reply.
|
|
10591
|
+
if (msg.id == null && typeof msg.method === 'string' && msg.method.startsWith('notifications/')) return;
|
|
9922
10592
|
|
|
9923
|
-
try
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
'
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
10593
|
+
// initialize: try the backend briefly for the real instructions, but ALWAYS
|
|
10594
|
+
// fall back to a local reply so the handshake can never block or fail.
|
|
10595
|
+
if (msg.method === 'initialize') {
|
|
10596
|
+
try {
|
|
10597
|
+
const resp = await fetch(URL, {
|
|
10598
|
+
method: 'POST',
|
|
10599
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${loadToken()}\` },
|
|
10600
|
+
body: line,
|
|
10601
|
+
signal: AbortSignal.timeout(2500),
|
|
10602
|
+
});
|
|
10603
|
+
if (resp.ok) { process.stdout.write((await resp.text()) + '\\n'); return; }
|
|
10604
|
+
} catch {}
|
|
10605
|
+
write(localInitialize(msg));
|
|
10606
|
+
return;
|
|
10607
|
+
}
|
|
10608
|
+
|
|
10609
|
+
// tool calls: forward, allowing a generous per-attempt window for slow calls
|
|
10610
|
+
// (create + decompose). If the backend is UNREACHABLE (mid-restart), retry only
|
|
10611
|
+
// briefly, then return a CLEAN JSON-RPC error and move on. Never hold the request
|
|
10612
|
+
// open for tens of seconds \u2014 a stdio request held that long wedges CC's MCP
|
|
10613
|
+
// client and every later call queues behind it. A returned error keeps the
|
|
10614
|
+
// client responsive and the server connected; the agent just retries.
|
|
10615
|
+
const REFUSED_BUDGET_MS = 4000;
|
|
10616
|
+
const deadline = Date.now() + REFUSED_BUDGET_MS;
|
|
10617
|
+
let attempt = 0;
|
|
10618
|
+
while (true) {
|
|
10619
|
+
try {
|
|
10620
|
+
const resp = await fetch(URL, {
|
|
10621
|
+
method: 'POST',
|
|
10622
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${loadToken()}\` },
|
|
10623
|
+
body: line,
|
|
10624
|
+
signal: AbortSignal.timeout(30000),
|
|
10625
|
+
});
|
|
10626
|
+
if (resp.status === 204) return;
|
|
10627
|
+
process.stdout.write((await resp.text()) + '\\n');
|
|
10628
|
+
return;
|
|
10629
|
+
} catch (err) {
|
|
10630
|
+
// A 30s timeout means the backend was UP but slow \u2014 don't spin. ECONNREFUSED /
|
|
10631
|
+
// fetch failed means it's unreachable \u2192 retry briefly within the budget.
|
|
10632
|
+
const name = err && err.name;
|
|
10633
|
+
const isTimeout = name === 'TimeoutError' || name === 'AbortError';
|
|
10634
|
+
if (!isTimeout && Date.now() < deadline) {
|
|
10635
|
+
attempt++;
|
|
10636
|
+
await new Promise((r) => setTimeout(r, Math.min(500, 150 * attempt)));
|
|
10637
|
+
continue;
|
|
10638
|
+
}
|
|
10639
|
+
if (msg.id != null) {
|
|
10640
|
+
write({
|
|
10641
|
+
jsonrpc: '2.0',
|
|
10642
|
+
id: msg.id,
|
|
10643
|
+
error: { code: -32603, message: 'Synkro MCP backend unavailable (container may be restarting) \u2014 retry in a moment' },
|
|
10644
|
+
});
|
|
10645
|
+
}
|
|
10646
|
+
return;
|
|
9943
10647
|
}
|
|
9944
10648
|
}
|
|
9945
10649
|
});
|
|
9946
10650
|
`;
|
|
9947
|
-
OFFSETS_DIR =
|
|
10651
|
+
OFFSETS_DIR = join9(SYNKRO_DIR4, ".transcript-offsets");
|
|
9948
10652
|
}
|
|
9949
10653
|
});
|
|
9950
10654
|
|
|
9951
10655
|
// cli/local-cc/install.ts
|
|
9952
|
-
import { existsSync as
|
|
9953
|
-
import { join as
|
|
9954
|
-
import { homedir as
|
|
10656
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync10, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
10657
|
+
import { join as join10 } from "path";
|
|
10658
|
+
import { homedir as homedir10 } from "os";
|
|
9955
10659
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
9956
10660
|
function writePluginFiles() {
|
|
9957
10661
|
for (const c of CHANNELS) {
|
|
9958
10662
|
mkdirSync9(c.sessionDir, { recursive: true });
|
|
9959
10663
|
mkdirSync9(c.pluginSettingsDir, { recursive: true });
|
|
9960
|
-
|
|
9961
|
-
|
|
10664
|
+
writeFileSync9(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
10665
|
+
writeFileSync9(
|
|
9962
10666
|
c.pluginSettingsPath,
|
|
9963
10667
|
JSON.stringify({
|
|
9964
10668
|
fastMode: true,
|
|
@@ -9966,7 +10670,7 @@ function writePluginFiles() {
|
|
|
9966
10670
|
}, null, 2) + "\n",
|
|
9967
10671
|
"utf-8"
|
|
9968
10672
|
);
|
|
9969
|
-
|
|
10673
|
+
writeFileSync9(c.runScriptPath, c.runScriptSource, "utf-8");
|
|
9970
10674
|
chmodSync3(c.runScriptPath, 493);
|
|
9971
10675
|
}
|
|
9972
10676
|
}
|
|
@@ -9985,10 +10689,10 @@ function runBunInstall() {
|
|
|
9985
10689
|
}
|
|
9986
10690
|
}
|
|
9987
10691
|
function safelyMutateClaudeJson(mutator) {
|
|
9988
|
-
if (!
|
|
10692
|
+
if (!existsSync12(CLAUDE_JSON_PATH)) {
|
|
9989
10693
|
return;
|
|
9990
10694
|
}
|
|
9991
|
-
const originalText =
|
|
10695
|
+
const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
|
|
9992
10696
|
let parsed;
|
|
9993
10697
|
try {
|
|
9994
10698
|
parsed = JSON.parse(originalText);
|
|
@@ -10020,7 +10724,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
10020
10724
|
copyFileSync2(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
|
|
10021
10725
|
const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
|
|
10022
10726
|
try {
|
|
10023
|
-
|
|
10727
|
+
writeFileSync9(tmpPath, newText, "utf-8");
|
|
10024
10728
|
const fd = openSync(tmpPath, "r");
|
|
10025
10729
|
try {
|
|
10026
10730
|
fsyncSync(fd);
|
|
@@ -10053,7 +10757,7 @@ function writeProjectMcpJson() {
|
|
|
10053
10757
|
}
|
|
10054
10758
|
}
|
|
10055
10759
|
};
|
|
10056
|
-
|
|
10760
|
+
writeFileSync9(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
10057
10761
|
}
|
|
10058
10762
|
}
|
|
10059
10763
|
function patchClaudeJson() {
|
|
@@ -10130,42 +10834,42 @@ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_S
|
|
|
10130
10834
|
var init_install2 = __esm({
|
|
10131
10835
|
"cli/local-cc/install.ts"() {
|
|
10132
10836
|
"use strict";
|
|
10133
|
-
CLAUDE_JSON_BACKUP_PATH =
|
|
10134
|
-
SESSION_DIR =
|
|
10135
|
-
PLUGIN_PATH =
|
|
10136
|
-
PLUGIN_PKG_PATH =
|
|
10137
|
-
PLUGIN_SETTINGS_DIR =
|
|
10138
|
-
PLUGIN_SETTINGS_PATH =
|
|
10139
|
-
PROJECT_MCP_PATH =
|
|
10140
|
-
CLAUDE_JSON_PATH =
|
|
10141
|
-
RUN_SCRIPT_PATH =
|
|
10837
|
+
CLAUDE_JSON_BACKUP_PATH = join10(homedir10(), ".claude.json.synkro-bak");
|
|
10838
|
+
SESSION_DIR = join10(homedir10(), ".synkro", "cc_sessions");
|
|
10839
|
+
PLUGIN_PATH = join10(SESSION_DIR, "synkro-channel.ts");
|
|
10840
|
+
PLUGIN_PKG_PATH = join10(SESSION_DIR, "package.json");
|
|
10841
|
+
PLUGIN_SETTINGS_DIR = join10(SESSION_DIR, ".claude");
|
|
10842
|
+
PLUGIN_SETTINGS_PATH = join10(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
10843
|
+
PROJECT_MCP_PATH = join10(SESSION_DIR, ".mcp.json");
|
|
10844
|
+
CLAUDE_JSON_PATH = join10(homedir10(), ".claude.json");
|
|
10845
|
+
RUN_SCRIPT_PATH = join10(SESSION_DIR, "run-claude.sh");
|
|
10142
10846
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
10143
10847
|
CHANNEL_1_PORT = 8941;
|
|
10144
|
-
SESSION_DIR_2 =
|
|
10145
|
-
PLUGIN_PATH_2 =
|
|
10146
|
-
PLUGIN_PKG_PATH_2 =
|
|
10147
|
-
PLUGIN_SETTINGS_DIR_2 =
|
|
10148
|
-
PLUGIN_SETTINGS_PATH_2 =
|
|
10149
|
-
PROJECT_MCP_PATH_2 =
|
|
10150
|
-
RUN_SCRIPT_PATH_2 =
|
|
10848
|
+
SESSION_DIR_2 = join10(homedir10(), ".synkro", "cc_sessions_2");
|
|
10849
|
+
PLUGIN_PATH_2 = join10(SESSION_DIR_2, "synkro-channel.ts");
|
|
10850
|
+
PLUGIN_PKG_PATH_2 = join10(SESSION_DIR_2, "package.json");
|
|
10851
|
+
PLUGIN_SETTINGS_DIR_2 = join10(SESSION_DIR_2, ".claude");
|
|
10852
|
+
PLUGIN_SETTINGS_PATH_2 = join10(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
10853
|
+
PROJECT_MCP_PATH_2 = join10(SESSION_DIR_2, ".mcp.json");
|
|
10854
|
+
RUN_SCRIPT_PATH_2 = join10(SESSION_DIR_2, "run-claude.sh");
|
|
10151
10855
|
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
10152
10856
|
CHANNEL_2_PORT = 8951;
|
|
10153
|
-
SESSION_DIR_3 =
|
|
10154
|
-
PLUGIN_PATH_3 =
|
|
10155
|
-
PLUGIN_PKG_PATH_3 =
|
|
10156
|
-
PLUGIN_SETTINGS_DIR_3 =
|
|
10157
|
-
PLUGIN_SETTINGS_PATH_3 =
|
|
10158
|
-
PROJECT_MCP_PATH_3 =
|
|
10159
|
-
RUN_SCRIPT_PATH_3 =
|
|
10857
|
+
SESSION_DIR_3 = join10(homedir10(), ".synkro", "cc_sessions_3");
|
|
10858
|
+
PLUGIN_PATH_3 = join10(SESSION_DIR_3, "synkro-channel.ts");
|
|
10859
|
+
PLUGIN_PKG_PATH_3 = join10(SESSION_DIR_3, "package.json");
|
|
10860
|
+
PLUGIN_SETTINGS_DIR_3 = join10(SESSION_DIR_3, ".claude");
|
|
10861
|
+
PLUGIN_SETTINGS_PATH_3 = join10(PLUGIN_SETTINGS_DIR_3, "settings.json");
|
|
10862
|
+
PROJECT_MCP_PATH_3 = join10(SESSION_DIR_3, ".mcp.json");
|
|
10863
|
+
RUN_SCRIPT_PATH_3 = join10(SESSION_DIR_3, "run-claude.sh");
|
|
10160
10864
|
TMUX_SESSION_NAME_3 = "synkro-local-cc-3";
|
|
10161
10865
|
CHANNEL_3_PORT = 8942;
|
|
10162
|
-
SESSION_DIR_4 =
|
|
10163
|
-
PLUGIN_PATH_4 =
|
|
10164
|
-
PLUGIN_PKG_PATH_4 =
|
|
10165
|
-
PLUGIN_SETTINGS_DIR_4 =
|
|
10166
|
-
PLUGIN_SETTINGS_PATH_4 =
|
|
10167
|
-
PROJECT_MCP_PATH_4 =
|
|
10168
|
-
RUN_SCRIPT_PATH_4 =
|
|
10866
|
+
SESSION_DIR_4 = join10(homedir10(), ".synkro", "cc_sessions_4");
|
|
10867
|
+
PLUGIN_PATH_4 = join10(SESSION_DIR_4, "synkro-channel.ts");
|
|
10868
|
+
PLUGIN_PKG_PATH_4 = join10(SESSION_DIR_4, "package.json");
|
|
10869
|
+
PLUGIN_SETTINGS_DIR_4 = join10(SESSION_DIR_4, ".claude");
|
|
10870
|
+
PLUGIN_SETTINGS_PATH_4 = join10(PLUGIN_SETTINGS_DIR_4, "settings.json");
|
|
10871
|
+
PROJECT_MCP_PATH_4 = join10(SESSION_DIR_4, ".mcp.json");
|
|
10872
|
+
RUN_SCRIPT_PATH_4 = join10(SESSION_DIR_4, "run-claude.sh");
|
|
10169
10873
|
TMUX_SESSION_NAME_4 = "synkro-local-cc-4";
|
|
10170
10874
|
CHANNEL_4_PORT = 8952;
|
|
10171
10875
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
@@ -10439,9 +11143,9 @@ var disconnect_exports = {};
|
|
|
10439
11143
|
__export(disconnect_exports, {
|
|
10440
11144
|
disconnectCommand: () => disconnectCommand
|
|
10441
11145
|
});
|
|
10442
|
-
import { existsSync as
|
|
10443
|
-
import { homedir as
|
|
10444
|
-
import { join as
|
|
11146
|
+
import { existsSync as existsSync13, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
11147
|
+
import { homedir as homedir11 } from "os";
|
|
11148
|
+
import { join as join11 } from "path";
|
|
10445
11149
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
10446
11150
|
import { createInterface as createInterface4 } from "readline";
|
|
10447
11151
|
async function tearDownLocalCC() {
|
|
@@ -10515,15 +11219,15 @@ async function disconnectCommand(args2 = []) {
|
|
|
10515
11219
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
10516
11220
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
10517
11221
|
}
|
|
10518
|
-
if (
|
|
11222
|
+
if (existsSync13(SYNKRO_DIR5)) {
|
|
10519
11223
|
if (purge) {
|
|
10520
11224
|
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
10521
11225
|
console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
|
|
10522
11226
|
} else {
|
|
10523
|
-
const keep = /* @__PURE__ */ new Set([
|
|
11227
|
+
const keep = /* @__PURE__ */ new Set([join11(SYNKRO_DIR5, "pgdata"), join11(SYNKRO_DIR5, "pgdata-backups")]);
|
|
10524
11228
|
const preserved = [];
|
|
10525
11229
|
for (const entry of readdirSync4(SYNKRO_DIR5)) {
|
|
10526
|
-
const full =
|
|
11230
|
+
const full = join11(SYNKRO_DIR5, entry);
|
|
10527
11231
|
if (keep.has(full)) {
|
|
10528
11232
|
preserved.push(entry);
|
|
10529
11233
|
continue;
|
|
@@ -10553,14 +11257,14 @@ var init_disconnect = __esm({
|
|
|
10553
11257
|
init_install2();
|
|
10554
11258
|
init_dockerInstall();
|
|
10555
11259
|
init_macKeychain();
|
|
10556
|
-
SYNKRO_DIR5 =
|
|
11260
|
+
SYNKRO_DIR5 = join11(homedir11(), ".synkro");
|
|
10557
11261
|
}
|
|
10558
11262
|
});
|
|
10559
11263
|
|
|
10560
11264
|
// cli/local-cc/turnLog.ts
|
|
10561
|
-
import { appendFileSync, existsSync as
|
|
10562
|
-
import { dirname as dirname6, join as
|
|
10563
|
-
import { homedir as
|
|
11265
|
+
import { appendFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync as statSync3, watchFile, unwatchFile } from "fs";
|
|
11266
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
11267
|
+
import { homedir as homedir12 } from "os";
|
|
10564
11268
|
function truncate(s, max = PREVIEW_MAX) {
|
|
10565
11269
|
if (s.length <= max) return s;
|
|
10566
11270
|
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
@@ -10596,11 +11300,11 @@ function appendTurn(args2) {
|
|
|
10596
11300
|
}
|
|
10597
11301
|
}
|
|
10598
11302
|
function readRecentTurns(n = 20) {
|
|
10599
|
-
if (!
|
|
11303
|
+
if (!existsSync14(TURN_LOG_PATH)) return [];
|
|
10600
11304
|
try {
|
|
10601
11305
|
const size = statSync3(TURN_LOG_PATH).size;
|
|
10602
11306
|
if (size === 0) return [];
|
|
10603
|
-
const text =
|
|
11307
|
+
const text = readFileSync11(TURN_LOG_PATH, "utf-8");
|
|
10604
11308
|
const lines = text.split("\n").filter(Boolean);
|
|
10605
11309
|
const lastN = lines.slice(-n).reverse();
|
|
10606
11310
|
return lastN.map((line) => {
|
|
@@ -10617,7 +11321,7 @@ function readRecentTurns(n = 20) {
|
|
|
10617
11321
|
function followTurns(onEntry) {
|
|
10618
11322
|
try {
|
|
10619
11323
|
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
10620
|
-
if (!
|
|
11324
|
+
if (!existsSync14(TURN_LOG_PATH)) {
|
|
10621
11325
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
10622
11326
|
}
|
|
10623
11327
|
} catch {
|
|
@@ -10679,7 +11383,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
|
|
|
10679
11383
|
var init_turnLog = __esm({
|
|
10680
11384
|
"cli/local-cc/turnLog.ts"() {
|
|
10681
11385
|
"use strict";
|
|
10682
|
-
TURN_LOG_PATH =
|
|
11386
|
+
TURN_LOG_PATH = join12(homedir12(), ".synkro", "cc_sessions", "turns.log");
|
|
10683
11387
|
PREVIEW_MAX = 400;
|
|
10684
11388
|
}
|
|
10685
11389
|
});
|
|
@@ -10828,9 +11532,9 @@ var init_grade = __esm({
|
|
|
10828
11532
|
});
|
|
10829
11533
|
|
|
10830
11534
|
// cli/local-cc/pueue.ts
|
|
10831
|
-
import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn } from "child_process";
|
|
10832
|
-
import { homedir as
|
|
10833
|
-
import { join as
|
|
11535
|
+
import { execFileSync as execFileSync3, spawnSync as spawnSync6, spawn as spawn2 } from "child_process";
|
|
11536
|
+
import { homedir as homedir13 } from "os";
|
|
11537
|
+
import { join as join13 } from "path";
|
|
10834
11538
|
import { connect as connect2 } from "net";
|
|
10835
11539
|
function pueueAvailable() {
|
|
10836
11540
|
const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
@@ -10896,7 +11600,7 @@ function startTask(opts = {}) {
|
|
|
10896
11600
|
spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
10897
11601
|
existing = findTask(ch);
|
|
10898
11602
|
}
|
|
10899
|
-
const runScript =
|
|
11603
|
+
const runScript = join13(cwd, "run-claude.sh");
|
|
10900
11604
|
const args2 = [
|
|
10901
11605
|
"add",
|
|
10902
11606
|
"--label",
|
|
@@ -10993,7 +11697,7 @@ function assertPueueInstalled() {
|
|
|
10993
11697
|
const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
10994
11698
|
if (status.status !== 0) {
|
|
10995
11699
|
console.log(" Starting pueued daemon...");
|
|
10996
|
-
const child =
|
|
11700
|
+
const child = spawn2("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
10997
11701
|
child.unref();
|
|
10998
11702
|
spawnSync6("sleep", ["1"]);
|
|
10999
11703
|
const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -11026,12 +11730,12 @@ var init_pueue = __esm({
|
|
|
11026
11730
|
"use strict";
|
|
11027
11731
|
TASK_LABEL = "synkro-local-cc";
|
|
11028
11732
|
TMUX_SESSION = "synkro-local-cc";
|
|
11029
|
-
SESSION_DIR2 =
|
|
11733
|
+
SESSION_DIR2 = join13(homedir13(), ".synkro", "cc_sessions");
|
|
11030
11734
|
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
11031
11735
|
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
11032
|
-
SESSION_DIR_22 =
|
|
11033
|
-
SESSION_DIR_32 =
|
|
11034
|
-
SESSION_DIR_42 =
|
|
11736
|
+
SESSION_DIR_22 = join13(homedir13(), ".synkro", "cc_sessions_2");
|
|
11737
|
+
SESSION_DIR_32 = join13(homedir13(), ".synkro", "cc_sessions_3");
|
|
11738
|
+
SESSION_DIR_42 = join13(homedir13(), ".synkro", "cc_sessions_4");
|
|
11035
11739
|
PueueError = class extends Error {
|
|
11036
11740
|
constructor(message, cause) {
|
|
11037
11741
|
super(message);
|
|
@@ -11046,13 +11750,13 @@ var init_pueue = __esm({
|
|
|
11046
11750
|
});
|
|
11047
11751
|
|
|
11048
11752
|
// cli/local-cc/settings.ts
|
|
11049
|
-
import { existsSync as
|
|
11050
|
-
import { homedir as
|
|
11051
|
-
import { join as
|
|
11753
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
|
|
11754
|
+
import { homedir as homedir14 } from "os";
|
|
11755
|
+
import { join as join14 } from "path";
|
|
11052
11756
|
function isLocalCCEnabled() {
|
|
11053
|
-
if (!
|
|
11757
|
+
if (!existsSync15(CONFIG_PATH3)) return false;
|
|
11054
11758
|
try {
|
|
11055
|
-
const content =
|
|
11759
|
+
const content = readFileSync12(CONFIG_PATH3, "utf-8");
|
|
11056
11760
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
11057
11761
|
return match?.[1] === "yes";
|
|
11058
11762
|
} catch {
|
|
@@ -11063,7 +11767,7 @@ var CONFIG_PATH3;
|
|
|
11063
11767
|
var init_settings = __esm({
|
|
11064
11768
|
"cli/local-cc/settings.ts"() {
|
|
11065
11769
|
"use strict";
|
|
11066
|
-
CONFIG_PATH3 =
|
|
11770
|
+
CONFIG_PATH3 = join14(homedir14(), ".synkro", "config.env");
|
|
11067
11771
|
}
|
|
11068
11772
|
});
|
|
11069
11773
|
|
|
@@ -11073,10 +11777,10 @@ __export(localCc_exports, {
|
|
|
11073
11777
|
localCcCommand: () => localCcCommand
|
|
11074
11778
|
});
|
|
11075
11779
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
11076
|
-
import { homedir as
|
|
11077
|
-
import { join as
|
|
11780
|
+
import { homedir as homedir15 } from "os";
|
|
11781
|
+
import { join as join15 } from "path";
|
|
11078
11782
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
11079
|
-
import { existsSync as
|
|
11783
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
11080
11784
|
function deploymentMode() {
|
|
11081
11785
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
11082
11786
|
if (env === "docker") return "docker";
|
|
@@ -11182,15 +11886,15 @@ TROUBLESHOOTING
|
|
|
11182
11886
|
`);
|
|
11183
11887
|
}
|
|
11184
11888
|
function readGatewayUrl() {
|
|
11185
|
-
if (
|
|
11186
|
-
const m =
|
|
11889
|
+
if (existsSync16(CONFIG_PATH4)) {
|
|
11890
|
+
const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
11187
11891
|
if (m) return m[1];
|
|
11188
11892
|
}
|
|
11189
11893
|
return "https://api.synkro.sh";
|
|
11190
11894
|
}
|
|
11191
11895
|
function updateLocalInferenceFlag(enabled) {
|
|
11192
|
-
if (!
|
|
11193
|
-
let content =
|
|
11896
|
+
if (!existsSync16(CONFIG_PATH4)) return;
|
|
11897
|
+
let content = readFileSync13(CONFIG_PATH4, "utf-8");
|
|
11194
11898
|
const flag = enabled ? "yes" : "no";
|
|
11195
11899
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
11196
11900
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -11199,7 +11903,7 @@ function updateLocalInferenceFlag(enabled) {
|
|
|
11199
11903
|
SYNKRO_LOCAL_INFERENCE='${flag}'
|
|
11200
11904
|
`;
|
|
11201
11905
|
}
|
|
11202
|
-
|
|
11906
|
+
writeFileSync10(CONFIG_PATH4, content, "utf-8");
|
|
11203
11907
|
}
|
|
11204
11908
|
async function setServerGradingProvider(provider) {
|
|
11205
11909
|
await ensureValidToken();
|
|
@@ -11629,8 +12333,8 @@ var init_localCc = __esm({
|
|
|
11629
12333
|
init_install();
|
|
11630
12334
|
init_client();
|
|
11631
12335
|
init_stub();
|
|
11632
|
-
SYNKRO_CONFIG_PATH =
|
|
11633
|
-
CONFIG_PATH4 =
|
|
12336
|
+
SYNKRO_CONFIG_PATH = join15(homedir15(), ".synkro", "config.env");
|
|
12337
|
+
CONFIG_PATH4 = join15(homedir15(), ".synkro", "config.env");
|
|
11634
12338
|
}
|
|
11635
12339
|
});
|
|
11636
12340
|
|
|
@@ -11741,13 +12445,13 @@ var config_exports = {};
|
|
|
11741
12445
|
__export(config_exports, {
|
|
11742
12446
|
configCommand: () => configCommand
|
|
11743
12447
|
});
|
|
11744
|
-
import { readFileSync as
|
|
11745
|
-
import { join as
|
|
11746
|
-
import { homedir as
|
|
12448
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
12449
|
+
import { join as join16 } from "path";
|
|
12450
|
+
import { homedir as homedir16 } from "os";
|
|
11747
12451
|
function readConfigEnv() {
|
|
11748
|
-
if (!
|
|
12452
|
+
if (!existsSync17(CONFIG_PATH5)) return {};
|
|
11749
12453
|
const out = {};
|
|
11750
|
-
for (const line of
|
|
12454
|
+
for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
11751
12455
|
const t = line.trim();
|
|
11752
12456
|
if (!t || t.startsWith("#")) continue;
|
|
11753
12457
|
const eq = t.indexOf("=");
|
|
@@ -11756,11 +12460,11 @@ function readConfigEnv() {
|
|
|
11756
12460
|
return out;
|
|
11757
12461
|
}
|
|
11758
12462
|
function updateConfigValue(key, value) {
|
|
11759
|
-
if (!
|
|
12463
|
+
if (!existsSync17(CONFIG_PATH5)) {
|
|
11760
12464
|
console.error("No config found. Run `synkro install` first.");
|
|
11761
12465
|
process.exit(1);
|
|
11762
12466
|
}
|
|
11763
|
-
const lines =
|
|
12467
|
+
const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
|
|
11764
12468
|
const pattern = new RegExp(`^${key}=`);
|
|
11765
12469
|
let found = false;
|
|
11766
12470
|
const updated = lines.map((line) => {
|
|
@@ -11771,7 +12475,7 @@ function updateConfigValue(key, value) {
|
|
|
11771
12475
|
return line;
|
|
11772
12476
|
});
|
|
11773
12477
|
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
11774
|
-
|
|
12478
|
+
writeFileSync11(CONFIG_PATH5, updated.join("\n"), "utf-8");
|
|
11775
12479
|
}
|
|
11776
12480
|
async function reconcileContainer() {
|
|
11777
12481
|
const cfg = readConfigEnv();
|
|
@@ -11881,20 +12585,20 @@ var init_config = __esm({
|
|
|
11881
12585
|
"cli/commands/config.ts"() {
|
|
11882
12586
|
"use strict";
|
|
11883
12587
|
init_stub();
|
|
11884
|
-
SYNKRO_DIR6 =
|
|
11885
|
-
CONFIG_PATH5 =
|
|
12588
|
+
SYNKRO_DIR6 = join16(homedir16(), ".synkro");
|
|
12589
|
+
CONFIG_PATH5 = join16(SYNKRO_DIR6, "config.env");
|
|
11886
12590
|
}
|
|
11887
12591
|
});
|
|
11888
12592
|
|
|
11889
12593
|
// cli/bootstrap.js
|
|
11890
|
-
import { readFileSync as
|
|
12594
|
+
import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
|
|
11891
12595
|
import { resolve as resolve3 } from "path";
|
|
11892
12596
|
var envCandidates = [
|
|
11893
12597
|
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
11894
12598
|
];
|
|
11895
12599
|
for (const envPath of envCandidates) {
|
|
11896
|
-
if (!
|
|
11897
|
-
const envContent =
|
|
12600
|
+
if (!existsSync18(envPath)) continue;
|
|
12601
|
+
const envContent = readFileSync15(envPath, "utf-8");
|
|
11898
12602
|
for (const line of envContent.split("\n")) {
|
|
11899
12603
|
const trimmed = line.trim();
|
|
11900
12604
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -11909,7 +12613,7 @@ var args = process.argv.slice(2);
|
|
|
11909
12613
|
var cmd = args[0] || "";
|
|
11910
12614
|
var subArgs = args.slice(1);
|
|
11911
12615
|
function printVersion() {
|
|
11912
|
-
console.log("1.6.
|
|
12616
|
+
console.log("1.6.54");
|
|
11913
12617
|
}
|
|
11914
12618
|
function printHelp2() {
|
|
11915
12619
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -11925,6 +12629,7 @@ Commands:
|
|
|
11925
12629
|
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
11926
12630
|
update Pull the latest container image and safely restart
|
|
11927
12631
|
config Show or change grading + storage modes
|
|
12632
|
+
claude-desktop Monitor Claude Desktop conversations (local, macOS)
|
|
11928
12633
|
version Show version
|
|
11929
12634
|
|
|
11930
12635
|
config:
|
|
@@ -12020,6 +12725,11 @@ async function main() {
|
|
|
12020
12725
|
await configCommand2(args.slice(1));
|
|
12021
12726
|
break;
|
|
12022
12727
|
}
|
|
12728
|
+
case "claude-desktop": {
|
|
12729
|
+
const { runClaudeDesktopTap: runClaudeDesktopTap2 } = await Promise.resolve().then(() => (init_claudeDesktopTap(), claudeDesktopTap_exports));
|
|
12730
|
+
await runClaudeDesktopTap2();
|
|
12731
|
+
break;
|
|
12732
|
+
}
|
|
12023
12733
|
default: {
|
|
12024
12734
|
console.error(`Unknown command: ${cmd}`);
|
|
12025
12735
|
printHelp2();
|