@synkro-sh/cli 1.4.65 → 1.4.67
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 +2430 -989
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -2
package/dist/bootstrap.js
CHANGED
|
@@ -283,22 +283,32 @@ var init_ccHookConfig = __esm({
|
|
|
283
283
|
});
|
|
284
284
|
|
|
285
285
|
// cli/installer/cursorHookConfig.ts
|
|
286
|
-
import {
|
|
287
|
-
import { dirname as dirname2 } from "path";
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
287
|
+
import { dirname as dirname2, resolve, normalize } from "path";
|
|
288
|
+
import { homedir as homedir2 } from "os";
|
|
289
|
+
function validateHooksPath(path) {
|
|
290
|
+
const resolved = resolve(normalize(path));
|
|
291
|
+
if (!ALLOWED_PARENT_DIRS.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) {
|
|
292
|
+
throw new Error(`Hooks path must be under ~/.cursor or ~/.config/cursor, got: ${resolved}`);
|
|
293
|
+
}
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
function readHooksFile(rawPath) {
|
|
297
|
+
const safePath = validateHooksPath(rawPath);
|
|
290
298
|
try {
|
|
291
|
-
const raw = readFileSync2(
|
|
299
|
+
const raw = readFileSync2(safePath, "utf-8");
|
|
292
300
|
return JSON.parse(raw);
|
|
293
301
|
} catch (err) {
|
|
294
|
-
|
|
302
|
+
if (err?.code === "ENOENT") return { version: 1, hooks: {} };
|
|
303
|
+
throw new Error(`Failed to parse ${safePath}: ${err.message}`);
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
|
-
function writeHooksFileAtomic(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
function writeHooksFileAtomic(rawPath, data) {
|
|
307
|
+
const safePath = validateHooksPath(rawPath);
|
|
308
|
+
mkdirSync2(dirname2(safePath), { recursive: true });
|
|
309
|
+
const tmpPath = `${safePath}.synkro.tmp`;
|
|
310
|
+
writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
311
|
+
renameSync2(tmpPath, safePath);
|
|
302
312
|
}
|
|
303
313
|
function isSynkroEntry2(entry) {
|
|
304
314
|
if (entry?.[SYNKRO_MARKER2]) return true;
|
|
@@ -314,10 +324,15 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
314
324
|
const file = readHooksFile(hooksJsonPath);
|
|
315
325
|
file.version = file.version ?? 1;
|
|
316
326
|
file.hooks = file.hooks ?? {};
|
|
317
|
-
const
|
|
318
|
-
for (const evt of events) {
|
|
327
|
+
for (const evt of ALL_EVENTS) {
|
|
319
328
|
removeSynkroEntries2(file.hooks, evt);
|
|
320
329
|
}
|
|
330
|
+
file.hooks.sessionStart = file.hooks.sessionStart ?? [];
|
|
331
|
+
file.hooks.sessionStart.push({
|
|
332
|
+
command: config.sessionStartScriptPath,
|
|
333
|
+
timeout: 5,
|
|
334
|
+
[SYNKRO_MARKER2]: true
|
|
335
|
+
});
|
|
321
336
|
file.hooks.beforeShellExecution = file.hooks.beforeShellExecution ?? [];
|
|
322
337
|
file.hooks.beforeShellExecution.push({
|
|
323
338
|
command: config.bashJudgeScriptPath,
|
|
@@ -346,14 +361,17 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
346
361
|
writeHooksFileAtomic(hooksJsonPath, file);
|
|
347
362
|
}
|
|
348
363
|
function uninstallCursorHooks(hooksJsonPath) {
|
|
349
|
-
|
|
350
|
-
|
|
364
|
+
let file;
|
|
365
|
+
try {
|
|
366
|
+
file = readHooksFile(hooksJsonPath);
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
351
370
|
if (!file.hooks) return false;
|
|
352
|
-
const
|
|
353
|
-
for (const evt of events) {
|
|
371
|
+
for (const evt of ALL_EVENTS) {
|
|
354
372
|
removeSynkroEntries2(file.hooks, evt);
|
|
355
373
|
}
|
|
356
|
-
for (const evt of
|
|
374
|
+
for (const evt of ALL_EVENTS) {
|
|
357
375
|
if (Array.isArray(file.hooks[evt]) && file.hooks[evt].length === 0) {
|
|
358
376
|
delete file.hooks[evt];
|
|
359
377
|
}
|
|
@@ -365,37 +383,46 @@ function uninstallCursorHooks(hooksJsonPath) {
|
|
|
365
383
|
return true;
|
|
366
384
|
}
|
|
367
385
|
function inspectCursorHooks(hooksJsonPath) {
|
|
368
|
-
|
|
369
|
-
|
|
386
|
+
let file;
|
|
387
|
+
try {
|
|
388
|
+
file = readHooksFile(hooksJsonPath);
|
|
389
|
+
} catch {
|
|
390
|
+
return { installed: false, sessionStart: false, beforeShellExecution: false, preToolUse: false, afterFileEdit: false, postToolUse: false };
|
|
370
391
|
}
|
|
371
|
-
const file = readHooksFile(hooksJsonPath);
|
|
372
392
|
const h = file.hooks ?? {};
|
|
393
|
+
const sessionStart = (h.sessionStart ?? []).some((e) => isSynkroEntry2(e));
|
|
373
394
|
const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
|
|
374
395
|
const preToolUse = (h.preToolUse ?? []).some((e) => isSynkroEntry2(e));
|
|
375
396
|
const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
|
|
376
397
|
const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
|
|
377
398
|
return {
|
|
378
|
-
installed: beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
|
|
399
|
+
installed: sessionStart || beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
|
|
400
|
+
sessionStart,
|
|
379
401
|
beforeShellExecution,
|
|
380
402
|
preToolUse,
|
|
381
403
|
afterFileEdit,
|
|
382
404
|
postToolUse
|
|
383
405
|
};
|
|
384
406
|
}
|
|
385
|
-
var SYNKRO_MARKER2;
|
|
407
|
+
var SYNKRO_MARKER2, ALLOWED_PARENT_DIRS, ALL_EVENTS;
|
|
386
408
|
var init_cursorHookConfig = __esm({
|
|
387
409
|
"cli/installer/cursorHookConfig.ts"() {
|
|
388
410
|
"use strict";
|
|
389
411
|
SYNKRO_MARKER2 = "__synkro_managed__";
|
|
412
|
+
ALLOWED_PARENT_DIRS = [
|
|
413
|
+
resolve(homedir2(), ".cursor"),
|
|
414
|
+
resolve(homedir2(), ".config", "cursor")
|
|
415
|
+
];
|
|
416
|
+
ALL_EVENTS = ["sessionStart", "beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
|
|
390
417
|
}
|
|
391
418
|
});
|
|
392
419
|
|
|
393
420
|
// cli/installer/mcpConfig.ts
|
|
394
|
-
import { existsSync as
|
|
395
|
-
import { homedir as
|
|
421
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
422
|
+
import { homedir as homedir3 } from "os";
|
|
396
423
|
import { dirname as dirname3, join as join2 } from "path";
|
|
397
424
|
function readClaudeJson() {
|
|
398
|
-
if (!
|
|
425
|
+
if (!existsSync3(CC_CONFIG_PATH)) return {};
|
|
399
426
|
try {
|
|
400
427
|
const raw = readFileSync3(CC_CONFIG_PATH, "utf-8");
|
|
401
428
|
return JSON.parse(raw);
|
|
@@ -415,6 +442,23 @@ function installMcpConfig(opts) {
|
|
|
415
442
|
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
416
443
|
if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
|
|
417
444
|
}
|
|
445
|
+
if (opts.local) {
|
|
446
|
+
const url2 = "http://127.0.0.1:8931/";
|
|
447
|
+
const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
|
|
448
|
+
let localToken = "";
|
|
449
|
+
try {
|
|
450
|
+
localToken = readFileSync3(tokenPath, "utf-8").trim();
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
454
|
+
type: "http",
|
|
455
|
+
url: url2,
|
|
456
|
+
...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
|
|
457
|
+
[SYNKRO_MARKER3]: true
|
|
458
|
+
};
|
|
459
|
+
writeClaudeJsonAtomic(config);
|
|
460
|
+
return { path: CC_CONFIG_PATH, url: url2 };
|
|
461
|
+
}
|
|
418
462
|
const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
|
|
419
463
|
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
420
464
|
type: "http",
|
|
@@ -426,7 +470,7 @@ function installMcpConfig(opts) {
|
|
|
426
470
|
return { path: CC_CONFIG_PATH, url };
|
|
427
471
|
}
|
|
428
472
|
function uninstallMcpConfig() {
|
|
429
|
-
if (!
|
|
473
|
+
if (!existsSync3(CC_CONFIG_PATH)) return false;
|
|
430
474
|
const config = readClaudeJson();
|
|
431
475
|
if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
|
|
432
476
|
let removed = false;
|
|
@@ -442,7 +486,7 @@ function uninstallMcpConfig() {
|
|
|
442
486
|
return true;
|
|
443
487
|
}
|
|
444
488
|
function inspectMcpConfig() {
|
|
445
|
-
if (!
|
|
489
|
+
if (!existsSync3(CC_CONFIG_PATH)) {
|
|
446
490
|
return { installed: false, configPath: CC_CONFIG_PATH };
|
|
447
491
|
}
|
|
448
492
|
const config = readClaudeJson();
|
|
@@ -458,12 +502,12 @@ var init_mcpConfig = __esm({
|
|
|
458
502
|
"use strict";
|
|
459
503
|
SYNKRO_MARKER3 = "__synkro_managed__";
|
|
460
504
|
SYNKRO_SERVER_NAME = "synkro-guardrails";
|
|
461
|
-
CC_CONFIG_PATH = join2(
|
|
505
|
+
CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
|
|
462
506
|
}
|
|
463
507
|
});
|
|
464
508
|
|
|
465
509
|
// cli/installer/hookScripts.ts
|
|
466
|
-
var SYNKRO_COMMON_SCRIPT
|
|
510
|
+
var SYNKRO_COMMON_SCRIPT;
|
|
467
511
|
var init_hookScripts = __esm({
|
|
468
512
|
"cli/installer/hookScripts.ts"() {
|
|
469
513
|
"use strict";
|
|
@@ -555,7 +599,32 @@ synkro_channel_up() {
|
|
|
555
599
|
}
|
|
556
600
|
|
|
557
601
|
# Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_SILENT, SYNKRO_POLICY_NAME.
|
|
602
|
+
_SYNKRO_RULES_FILE="$HOME/.synkro/rules.json"
|
|
603
|
+
_SYNKRO_TELEMETRY_FILE="$HOME/.synkro/telemetry.jsonl"
|
|
604
|
+
|
|
558
605
|
synkro_load_config() {
|
|
606
|
+
# Local-first: read from ~/.synkro/rules.json if it exists (zero latency, no network)
|
|
607
|
+
if [ -f "$_SYNKRO_RULES_FILE" ]; then
|
|
608
|
+
local rdata
|
|
609
|
+
rdata=$(cat "$_SYNKRO_RULES_FILE" 2>/dev/null)
|
|
610
|
+
if [ -n "$rdata" ]; then
|
|
611
|
+
SYNKRO_CAPTURE_DEPTH="local_only"
|
|
612
|
+
SYNKRO_TIER="standard"
|
|
613
|
+
SYNKRO_SILENT=$(echo "$rdata" | jq -r '.config.silent // false' 2>/dev/null)
|
|
614
|
+
local active_id
|
|
615
|
+
active_id=$(echo "$rdata" | jq -r '.config.activePolicyId // empty' 2>/dev/null)
|
|
616
|
+
if [ -n "$active_id" ]; then
|
|
617
|
+
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r --arg id "$active_id" '.policies[]? | select(.id == $id) | .name // empty' 2>/dev/null)
|
|
618
|
+
SYNKRO_RULES=$(echo "$rdata" | jq -c --arg id "$active_id" '[.policies[]? | select(.id == $id) | .rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
619
|
+
else
|
|
620
|
+
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r '.policies[0]?.name // empty' 2>/dev/null)
|
|
621
|
+
SYNKRO_RULES=$(echo "$rdata" | jq -c '[.policies[0]?.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
622
|
+
fi
|
|
623
|
+
return
|
|
624
|
+
fi
|
|
625
|
+
fi
|
|
626
|
+
|
|
627
|
+
# Fallback: fetch from cloud API
|
|
559
628
|
local resp
|
|
560
629
|
resp=$(curl -sS "\${GATEWAY_URL}/api/v1/hook/config\${1:+?$1}" -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
|
|
561
630
|
if [ -z "$resp" ]; then return; fi
|
|
@@ -566,6 +635,15 @@ synkro_load_config() {
|
|
|
566
635
|
SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
567
636
|
}
|
|
568
637
|
|
|
638
|
+
synkro_local_capture() {
|
|
639
|
+
local event_json="$1"
|
|
640
|
+
local ts
|
|
641
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
642
|
+
local line
|
|
643
|
+
line=$(echo "$event_json" | jq -c --arg ts "$ts" '. + {_ts: $ts}' 2>/dev/null)
|
|
644
|
+
[ -n "$line" ] && printf '%s\\n' "$line" >> "$_SYNKRO_TELEMETRY_FILE" 2>/dev/null
|
|
645
|
+
}
|
|
646
|
+
|
|
569
647
|
synkro_tag() {
|
|
570
648
|
if [ "$SYNKRO_SILENT" = "true" ]; then echo "[synkro:silent]"; return; fi
|
|
571
649
|
local route="\${1:-$(synkro_route)}"
|
|
@@ -619,227 +697,12 @@ synkro_post_with_retry() {
|
|
|
619
697
|
fi
|
|
620
698
|
echo "$resp"
|
|
621
699
|
}
|
|
622
|
-
`;
|
|
623
|
-
CURSOR_BASH_JUDGE_SCRIPT = `#!/bin/bash
|
|
624
|
-
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
625
|
-
. "$SCRIPT_DIR/_synkro-common.sh"
|
|
626
|
-
|
|
627
|
-
JWT=$(synkro_load_jwt)
|
|
628
|
-
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
629
|
-
synkro_ensure_fresh_jwt
|
|
630
|
-
|
|
631
|
-
PAYLOAD=$(cat)
|
|
632
|
-
if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
|
|
633
|
-
|
|
634
|
-
COMMAND=$(echo "$PAYLOAD" | jq -r '.command // empty' 2>/dev/null)
|
|
635
|
-
if [ -z "$COMMAND" ]; then echo '{}'; exit 0; fi
|
|
636
|
-
|
|
637
|
-
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
638
|
-
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
|
|
639
|
-
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
640
|
-
|
|
641
|
-
CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
|
|
642
|
-
synkro_log "bashGuard checking: $CMD_SHORT"
|
|
643
|
-
|
|
644
|
-
synkro_load_config
|
|
645
|
-
if [ "$SYNKRO_SILENT" = "true" ]; then
|
|
646
|
-
echo '{}'; exit 0
|
|
647
|
-
fi
|
|
648
|
-
|
|
649
|
-
BODY=$(jq -n \\
|
|
650
|
-
--arg cmd "$COMMAND" \\
|
|
651
|
-
--arg session_id "$SESSION_ID" \\
|
|
652
|
-
--arg cwd "$CWD" \\
|
|
653
|
-
--arg repo "$GIT_REPO" \\
|
|
654
|
-
'{
|
|
655
|
-
hook_event: "PreToolUse",
|
|
656
|
-
tool_name: "Bash",
|
|
657
|
-
tool_input: {command: $cmd},
|
|
658
|
-
response_format: "cursor",
|
|
659
|
-
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
660
|
-
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
661
|
-
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
662
|
-
}')
|
|
663
|
-
|
|
664
|
-
RESP=$(synkro_post_with_retry "\${GATEWAY_URL}/api/v1/hook/judge" "$BODY" 6)
|
|
665
|
-
|
|
666
|
-
if [ -z "$RESP" ]; then
|
|
667
|
-
synkro_log "bashGuard $CMD_SHORT \u2192 error (timeout)"
|
|
668
|
-
echo '{}'; exit 0
|
|
669
|
-
fi
|
|
670
|
-
|
|
671
|
-
# Server returns cursor-format directly in hook_response
|
|
672
|
-
if echo "$RESP" | jq -e '.hook_response' >/dev/null 2>&1; then
|
|
673
|
-
echo "$RESP" | jq -c '.hook_response'
|
|
674
|
-
else
|
|
675
|
-
echo '{}'
|
|
676
|
-
fi
|
|
677
|
-
exit 0
|
|
678
|
-
`;
|
|
679
|
-
CURSOR_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
|
|
680
|
-
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
681
|
-
. "$SCRIPT_DIR/_synkro-common.sh"
|
|
682
|
-
|
|
683
|
-
JWT=$(synkro_load_jwt)
|
|
684
|
-
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
685
|
-
synkro_ensure_fresh_jwt
|
|
686
|
-
|
|
687
|
-
PAYLOAD=$(cat)
|
|
688
|
-
if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
|
|
689
|
-
|
|
690
|
-
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
691
|
-
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
692
|
-
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
|
|
693
|
-
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
694
|
-
|
|
695
|
-
FILE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.target_file // empty' 2>/dev/null)
|
|
696
|
-
CONTENT=$(echo "$PAYLOAD" | jq -r '.tool_input.content // .tool_input.new_string // .tool_input.code_edit // empty' 2>/dev/null)
|
|
697
|
-
if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
|
|
698
|
-
|
|
699
|
-
BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
700
|
-
synkro_log "editGuard checking: $BASENAME"
|
|
701
|
-
|
|
702
|
-
synkro_load_config
|
|
703
|
-
if [ "$SYNKRO_SILENT" = "true" ]; then
|
|
704
|
-
echo '{}'; exit 0
|
|
705
|
-
fi
|
|
706
|
-
|
|
707
|
-
BODY=$(jq -n \\
|
|
708
|
-
--arg file_path "$FILE_PATH" \\
|
|
709
|
-
--arg content "$CONTENT" \\
|
|
710
|
-
--arg session_id "$SESSION_ID" \\
|
|
711
|
-
--arg cwd "$CWD" \\
|
|
712
|
-
--arg repo "$GIT_REPO" \\
|
|
713
|
-
'{
|
|
714
|
-
hook_event: "PreToolUse",
|
|
715
|
-
tool_name: "Edit",
|
|
716
|
-
tool_input: {file_path: $file_path, content: $content},
|
|
717
|
-
file_path: $file_path,
|
|
718
|
-
content: $content,
|
|
719
|
-
response_format: "cursor",
|
|
720
|
-
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
721
|
-
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
722
|
-
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
723
|
-
}')
|
|
724
|
-
|
|
725
|
-
RESP=$(synkro_post_with_retry "\${GATEWAY_URL}/api/v1/hook/judge" "$BODY" 8)
|
|
726
|
-
|
|
727
|
-
if [ -z "$RESP" ]; then
|
|
728
|
-
synkro_log "editGuard $BASENAME \u2192 error (timeout)"
|
|
729
|
-
echo '{}'; exit 0
|
|
730
|
-
fi
|
|
731
|
-
|
|
732
|
-
if echo "$RESP" | jq -e '.hook_response' >/dev/null 2>&1; then
|
|
733
|
-
echo "$RESP" | jq -c '.hook_response'
|
|
734
|
-
else
|
|
735
|
-
echo '{}'
|
|
736
|
-
fi
|
|
737
|
-
exit 0
|
|
738
|
-
`;
|
|
739
|
-
CURSOR_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
|
|
740
|
-
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
741
|
-
. "$SCRIPT_DIR/_synkro-common.sh"
|
|
742
|
-
|
|
743
|
-
JWT=$(synkro_load_jwt)
|
|
744
|
-
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
745
|
-
|
|
746
|
-
PAYLOAD=$(cat)
|
|
747
|
-
if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
|
|
748
|
-
|
|
749
|
-
FILE_PATH=$(echo "$PAYLOAD" | jq -r '.file_path // empty' 2>/dev/null)
|
|
750
|
-
if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
|
|
751
|
-
|
|
752
|
-
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // .workspace_roots[0] // empty' 2>/dev/null)
|
|
753
|
-
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
|
|
754
|
-
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
755
|
-
BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
756
|
-
|
|
757
|
-
FULL_PATH="$FILE_PATH"
|
|
758
|
-
[ -n "$CWD" ] && FULL_PATH="$CWD/$FILE_PATH"
|
|
759
|
-
FULL_CONTENT=""
|
|
760
|
-
[ -f "$FULL_PATH" ] && FULL_CONTENT=$(head -c 50000 "$FULL_PATH" 2>/dev/null || true)
|
|
761
|
-
|
|
762
|
-
DEPS_JSON="{}"
|
|
763
|
-
_PKG_DIR="\${CWD:-.}"
|
|
764
|
-
while [ "$_PKG_DIR" != "/" ]; do
|
|
765
|
-
if [ -f "$_PKG_DIR/package.json" ]; then
|
|
766
|
-
DEPS_JSON=$(jq -c '(.dependencies // {}) + (.devDependencies // {})' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}")
|
|
767
|
-
break
|
|
768
|
-
fi
|
|
769
|
-
_PKG_DIR=$(dirname "$_PKG_DIR")
|
|
770
|
-
done
|
|
771
|
-
|
|
772
|
-
synkro_log "editScan $BASENAME"
|
|
773
|
-
|
|
774
|
-
(
|
|
775
|
-
BODY=$(jq -n \\
|
|
776
|
-
--arg file_path "$FILE_PATH" --arg content "$FULL_CONTENT" \\
|
|
777
|
-
--arg session_id "$SESSION_ID" --arg cwd "$CWD" --arg repo "$GIT_REPO" \\
|
|
778
|
-
--argjson deps "$DEPS_JSON" \\
|
|
779
|
-
'{capture_type:"edit_scan",tool_input:{file_path:$file_path,content:$content},edit_verdict:{ok:true},dependencies:$deps}
|
|
780
|
-
+ (if ($session_id | length) > 0 then {session_id:$session_id} else {} end)
|
|
781
|
-
+ (if ($cwd | length) > 0 then {cwd:$cwd} else {} end)
|
|
782
|
-
+ (if ($repo | length) > 0 then {repo:$repo} else {} end)')
|
|
783
|
-
curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
|
|
784
|
-
-H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
|
|
785
|
-
-d "$BODY" --max-time 10 >/dev/null 2>&1 || true
|
|
786
|
-
) &
|
|
787
|
-
disown 2>/dev/null || true
|
|
788
|
-
|
|
789
|
-
echo '{}'
|
|
790
|
-
exit 0
|
|
791
|
-
`;
|
|
792
|
-
CURSOR_BASH_FOLLOWUP_SCRIPT = `#!/bin/bash
|
|
793
|
-
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
794
|
-
. "$SCRIPT_DIR/_synkro-common.sh"
|
|
795
|
-
|
|
796
|
-
JWT=$(synkro_load_jwt)
|
|
797
|
-
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
798
|
-
|
|
799
|
-
PAYLOAD=$(cat)
|
|
800
|
-
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
801
|
-
case "$TOOL_NAME" in Shell|Bash|terminal|run_terminal_cmd|execute_command) ;; *) echo '{}'; exit 0 ;; esac
|
|
802
|
-
|
|
803
|
-
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
|
|
804
|
-
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
805
|
-
|
|
806
|
-
IS_ERROR=$(echo "$PAYLOAD" | jq -r '.tool_result.is_error // false' 2>/dev/null)
|
|
807
|
-
CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
808
|
-
CMD_HASH=""
|
|
809
|
-
if [ -n "$CMD" ]; then
|
|
810
|
-
CMD_HASH=$(printf '%s' "$CMD" | shasum -a 256 | cut -c1-16)
|
|
811
|
-
fi
|
|
812
|
-
|
|
813
|
-
if [ -n "$CMD_HASH" ] && [ -n "$SESSION_ID" ]; then
|
|
814
|
-
if [ "$IS_ERROR" = "false" ]; then
|
|
815
|
-
synkro_consent_consume "$SESSION_ID" "$CMD_HASH"
|
|
816
|
-
else
|
|
817
|
-
if ! synkro_consent_has_active "$SESSION_ID" "$CMD_HASH"; then
|
|
818
|
-
synkro_consent_grant "$SESSION_ID" "$CMD_HASH"
|
|
819
|
-
fi
|
|
820
|
-
fi
|
|
821
|
-
fi
|
|
822
|
-
|
|
823
|
-
if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
|
|
824
|
-
(
|
|
825
|
-
BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
|
|
826
|
-
--argjson err "$IS_ERROR" --arg ch "$CMD_HASH" \\
|
|
827
|
-
'{capture_type:"bash_followup",session_id:$sid,tool_use_id:$tid,is_error:$err,command_hash:$ch}')
|
|
828
|
-
curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
|
|
829
|
-
-H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
|
|
830
|
-
-d "$BODY" --max-time 3 >/dev/null 2>&1 || true
|
|
831
|
-
) &
|
|
832
|
-
disown 2>/dev/null || true
|
|
833
|
-
fi
|
|
834
|
-
|
|
835
|
-
echo '{}'
|
|
836
|
-
exit 0
|
|
837
700
|
`;
|
|
838
701
|
}
|
|
839
702
|
});
|
|
840
703
|
|
|
841
704
|
// cli/installer/hookScriptsTs.ts
|
|
842
|
-
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_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;
|
|
705
|
+
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_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_PRECHECK_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_BASH_FOLLOWUP_TS, CURSOR_SESSION_START_TS;
|
|
843
706
|
var init_hookScriptsTs = __esm({
|
|
844
707
|
"cli/installer/hookScriptsTs.ts"() {
|
|
845
708
|
"use strict";
|
|
@@ -1019,7 +882,7 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
|
1019
882
|
|
|
1020
883
|
export function detectRepo(cwd: string): string {
|
|
1021
884
|
try {
|
|
1022
|
-
const url = execSync('git remote get-url origin', { cwd, timeout: 3000, encoding: 'utf-8' }).trim();
|
|
885
|
+
const url = execSync('git remote get-url origin 2>/dev/null', { cwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
1023
886
|
if (!url) return '';
|
|
1024
887
|
return url
|
|
1025
888
|
.replace(/^git@[^:]+:/, '')
|
|
@@ -1074,6 +937,37 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1074
937
|
rules: [],
|
|
1075
938
|
scanExemptions: [],
|
|
1076
939
|
};
|
|
940
|
+
|
|
941
|
+
// Local-first: read from ~/.synkro/rules.json if it exists (zero latency, no network)
|
|
942
|
+
const localRulesPath = join(HOME, '.synkro', 'rules.json');
|
|
943
|
+
try {
|
|
944
|
+
if (existsSync(localRulesPath)) {
|
|
945
|
+
const raw = JSON.parse(readFileSync(localRulesPath, 'utf-8'));
|
|
946
|
+
const activePolicyId = raw.config?.activePolicyId || 'local-policy';
|
|
947
|
+
const policy = (raw.policies || []).find((p: any) => p.id === activePolicyId) || raw.policies?.[0];
|
|
948
|
+
if (policy) {
|
|
949
|
+
config.policyName = policy.name || '';
|
|
950
|
+
config.rules = (policy.rules || [])
|
|
951
|
+
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
952
|
+
.map((r: any) => ({
|
|
953
|
+
rule_id: r.rule_id || '',
|
|
954
|
+
text: r.text || '',
|
|
955
|
+
severity: r.severity || '',
|
|
956
|
+
category: r.category || '',
|
|
957
|
+
mode: r.mode || 'blocking',
|
|
958
|
+
}));
|
|
959
|
+
}
|
|
960
|
+
config.silent = raw.config?.silent === true;
|
|
961
|
+
if (Array.isArray(raw.scanExemptions)) {
|
|
962
|
+
config.scanExemptions = raw.scanExemptions
|
|
963
|
+
.filter((e: any) => e && typeof e.path === 'string')
|
|
964
|
+
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
965
|
+
}
|
|
966
|
+
return config;
|
|
967
|
+
}
|
|
968
|
+
} catch {}
|
|
969
|
+
|
|
970
|
+
// Fallback: fetch from cloud API
|
|
1077
971
|
try {
|
|
1078
972
|
const url = GATEWAY_URL + '/api/v1/hook/config' + (query ? '?' + query : '');
|
|
1079
973
|
const resp = await fetch(url, {
|
|
@@ -2283,6 +2177,77 @@ import {
|
|
|
2283
2177
|
type HookConfig, type Rule,
|
|
2284
2178
|
} from './_synkro-common.ts';
|
|
2285
2179
|
|
|
2180
|
+
const TOP_NPM_PKGS = new Set([
|
|
2181
|
+
'express','react','lodash','axios','chalk','commander','debug','dotenv','webpack',
|
|
2182
|
+
'typescript','moment','uuid','cors','body-parser','mongoose','jsonwebtoken','bcrypt',
|
|
2183
|
+
'nodemon','eslint','prettier','jest','mocha','chai','sinon','supertest','request',
|
|
2184
|
+
'async','bluebird','underscore','ramda','rxjs','socket.io','redis','pg','mysql',
|
|
2185
|
+
'sequelize','knex','prisma','next','nuxt','vue','svelte','angular','ember',
|
|
2186
|
+
'react-dom','react-router','react-redux','redux','mobx','formik','yup','zod',
|
|
2187
|
+
'ajv','joi','helmet','morgan','passport','cookie-parser','express-session',
|
|
2188
|
+
'multer','sharp','jimp','puppeteer','playwright','cheerio','got','node-fetch',
|
|
2189
|
+
'superagent','inquirer','ora','yargs','minimist','glob','rimraf','mkdirp',
|
|
2190
|
+
'fs-extra','chokidar','ws','graphql','apollo-server','fastify','koa','hapi',
|
|
2191
|
+
'nest','drizzle-orm','typeorm','mikro-orm','bull','bullmq','ioredis','kafkajs',
|
|
2192
|
+
'amqplib','nodemailer','handlebars','ejs','pug','marked','highlight.js',
|
|
2193
|
+
'dayjs','date-fns','luxon','nanoid','cuid','short-uuid','colors','picocolors',
|
|
2194
|
+
'winston','pino','bunyan','semver','tar','archiver','unzipper','crypto-js',
|
|
2195
|
+
'bcryptjs','argon2','jose','openai','anthropic','langchain','tensorflow',
|
|
2196
|
+
'onnxruntime-node','sharp','canvas','three','d3','chart.js','echarts',
|
|
2197
|
+
'tailwindcss','postcss','autoprefixer','sass','less','styled-components',
|
|
2198
|
+
'emotion','framer-motion','gsap','lottie-web','swiper','i18next',
|
|
2199
|
+
]);
|
|
2200
|
+
|
|
2201
|
+
const TOP_PYPI_PKGS = new Set([
|
|
2202
|
+
'requests','flask','django','numpy','pandas','scipy','matplotlib','scikit-learn',
|
|
2203
|
+
'tensorflow','torch','pytorch','keras','fastapi','uvicorn','gunicorn','celery',
|
|
2204
|
+
'redis','sqlalchemy','alembic','pydantic','httpx','aiohttp','beautifulsoup4',
|
|
2205
|
+
'scrapy','selenium','playwright','pillow','opencv-python','boto3','awscli',
|
|
2206
|
+
'google-cloud-storage','azure-storage-blob','psycopg2','pymongo','motor',
|
|
2207
|
+
'pytest','unittest2','mock','coverage','tox','black','flake8','mypy','ruff',
|
|
2208
|
+
'isort','pylint','bandit','cryptography','paramiko','fabric','click','typer',
|
|
2209
|
+
'rich','colorama','tqdm','loguru','python-dotenv','pyyaml','toml','orjson',
|
|
2210
|
+
'ujson','marshmallow','attrs','dataclasses-json','jinja2','mako','arrow',
|
|
2211
|
+
'pendulum','dateutil','pytz','regex','chardet','charset-normalizer',
|
|
2212
|
+
'langchain','openai','anthropic','transformers','huggingface-hub','tokenizers',
|
|
2213
|
+
'gradio','streamlit','dash','plotly','seaborn','bokeh','altair',
|
|
2214
|
+
]);
|
|
2215
|
+
|
|
2216
|
+
function levenshtein(a: string, b: string): number {
|
|
2217
|
+
const m = a.length, n = b.length;
|
|
2218
|
+
if (Math.abs(m - n) > 2) return 3;
|
|
2219
|
+
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) => {
|
|
2220
|
+
const row = new Array(n + 1).fill(0);
|
|
2221
|
+
row[0] = i;
|
|
2222
|
+
return row;
|
|
2223
|
+
});
|
|
2224
|
+
for (let j = 1; j <= n; j++) dp[0][j] = j;
|
|
2225
|
+
for (let i = 1; i <= m; i++) {
|
|
2226
|
+
for (let j = 1; j <= n; j++) {
|
|
2227
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
2228
|
+
? dp[i - 1][j - 1]
|
|
2229
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return dp[m][n];
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function checkTyposquat(pkg: string, isPip: boolean): string | null {
|
|
2236
|
+
const topPkgs = isPip ? TOP_PYPI_PKGS : TOP_NPM_PKGS;
|
|
2237
|
+
if (topPkgs.has(pkg)) return null;
|
|
2238
|
+
const pkgLower = pkg.toLowerCase();
|
|
2239
|
+
for (const known of topPkgs) {
|
|
2240
|
+
const dist = levenshtein(pkgLower, known);
|
|
2241
|
+
if (dist > 0 && dist <= 2) return known;
|
|
2242
|
+
}
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
interface PkgMeta {
|
|
2247
|
+
deprecated?: string;
|
|
2248
|
+
weeklyDownloads?: number;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2286
2251
|
async function main() {
|
|
2287
2252
|
try {
|
|
2288
2253
|
const input = await readStdin();
|
|
@@ -2319,11 +2284,11 @@ async function main() {
|
|
|
2319
2284
|
if (!jwt) { outputEmpty(); return; }
|
|
2320
2285
|
jwt = await ensureFreshJwt(jwt);
|
|
2321
2286
|
|
|
2322
|
-
// \u2500\u2500\u2500 CVE
|
|
2323
|
-
let
|
|
2287
|
+
// \u2500\u2500\u2500 Install protection: CVE + typosquat + deprecated + popularity \u2500\u2500\u2500
|
|
2288
|
+
let installScanMsg = '';
|
|
2324
2289
|
if (toolName === 'Bash') {
|
|
2325
2290
|
const pkgInstallMatch = command.match(
|
|
2326
|
-
/(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+(
|
|
2291
|
+
/(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+([^|;&><]+)/
|
|
2327
2292
|
);
|
|
2328
2293
|
const isPip = /(?:uv\\s+)?pip3?\\s+install/.test(command);
|
|
2329
2294
|
const isGo = command.match(/^go\\s+get/);
|
|
@@ -2337,6 +2302,7 @@ async function main() {
|
|
|
2337
2302
|
let skipNext = false;
|
|
2338
2303
|
for (const token of tokens) {
|
|
2339
2304
|
if (skipNext) { skipNext = false; continue; }
|
|
2305
|
+
if (!token || !/^[@a-zA-Z]/.test(token)) continue;
|
|
2340
2306
|
if (token.startsWith('-')) {
|
|
2341
2307
|
if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir|filter|workspace)$/.test(token)) skipNext = true;
|
|
2342
2308
|
continue;
|
|
@@ -2348,7 +2314,6 @@ async function main() {
|
|
|
2348
2314
|
continue;
|
|
2349
2315
|
}
|
|
2350
2316
|
}
|
|
2351
|
-
// npm/yarn: pkg@1.0
|
|
2352
2317
|
const atIdx = token.lastIndexOf('@');
|
|
2353
2318
|
if (atIdx > 0) {
|
|
2354
2319
|
deps[token.slice(0, atIdx)] = token.slice(atIdx + 1);
|
|
@@ -2356,33 +2321,81 @@ async function main() {
|
|
|
2356
2321
|
deps[token] = '*';
|
|
2357
2322
|
}
|
|
2358
2323
|
}
|
|
2359
|
-
|
|
2360
|
-
if (
|
|
2361
|
-
const
|
|
2324
|
+
|
|
2325
|
+
if (Object.keys(deps).length > 0) {
|
|
2326
|
+
const warnings: string[] = [];
|
|
2327
|
+
const pkgMeta: Record<string, PkgMeta> = {};
|
|
2328
|
+
|
|
2329
|
+
const metaLookups = Object.keys(deps).map(async (pkg) => {
|
|
2362
2330
|
try {
|
|
2363
|
-
let ver: string | null = null;
|
|
2364
2331
|
if (isPip) {
|
|
2365
2332
|
const r = await fetch('https://pypi.org/pypi/' + encodeURIComponent(pkg) + '/json', { signal: AbortSignal.timeout(4000) });
|
|
2366
|
-
if (r.ok) {
|
|
2333
|
+
if (r.ok) {
|
|
2334
|
+
const d = await r.json() as any;
|
|
2335
|
+
if (deps[pkg] === '*' && d?.info?.version) deps[pkg] = d.info.version;
|
|
2336
|
+
const classifiers: string[] = d?.info?.classifiers || [];
|
|
2337
|
+
const isInactive = classifiers.some((c: string) => /Development Status :: [67]/.test(c));
|
|
2338
|
+
if (isInactive) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: 'package marked as inactive/obsolete' };
|
|
2339
|
+
if (d?.info?.yanked) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: d.info.yanked_reason || 'yanked from PyPI' };
|
|
2340
|
+
} else if (r.status === 404) {
|
|
2341
|
+
warnings.push('\\u26a0 ' + pkg + ': package not found on PyPI \\u2014 may not exist');
|
|
2342
|
+
}
|
|
2367
2343
|
} else {
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2344
|
+
const verSlug = deps[pkg] !== '*' ? deps[pkg] : 'latest';
|
|
2345
|
+
const [metaResp, dlResp] = await Promise.all([
|
|
2346
|
+
fetch('https://registry.npmjs.org/' + encodeURIComponent(pkg) + '/' + verSlug, { signal: AbortSignal.timeout(4000) }),
|
|
2347
|
+
fetch('https://api.npmjs.org/downloads/point/last-week/' + encodeURIComponent(pkg), { signal: AbortSignal.timeout(4000) }),
|
|
2348
|
+
]);
|
|
2349
|
+
if (metaResp.ok) {
|
|
2350
|
+
const d = await metaResp.json() as any;
|
|
2351
|
+
if (deps[pkg] === '*' && d?.version) deps[pkg] = d.version;
|
|
2352
|
+
if (d?.deprecated) pkgMeta[pkg] = { ...pkgMeta[pkg], deprecated: d.deprecated };
|
|
2353
|
+
} else if (metaResp.status === 404) {
|
|
2354
|
+
warnings.push('\\u26a0 ' + pkg + ': package not found on npm \\u2014 may not exist');
|
|
2355
|
+
}
|
|
2356
|
+
if (dlResp.ok) {
|
|
2357
|
+
const d = await dlResp.json() as any;
|
|
2358
|
+
if (typeof d?.downloads === 'number') pkgMeta[pkg] = { ...pkgMeta[pkg], weeklyDownloads: d.downloads };
|
|
2359
|
+
}
|
|
2370
2360
|
}
|
|
2371
|
-
if (ver) deps[pkg] = ver;
|
|
2372
2361
|
} catch {}
|
|
2373
2362
|
});
|
|
2374
|
-
await Promise.all(
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2363
|
+
await Promise.all(metaLookups);
|
|
2364
|
+
|
|
2365
|
+
for (const pkg of Object.keys(deps)) {
|
|
2366
|
+
const similar = checkTyposquat(pkg, isPip);
|
|
2367
|
+
if (similar) {
|
|
2368
|
+
const dl = pkgMeta[pkg]?.weeklyDownloads;
|
|
2369
|
+
if (dl === undefined || dl < 1000) {
|
|
2370
|
+
warnings.push('\\u26a0 ' + pkg + ': possible typosquat of "' + similar + '"' + (dl !== undefined ? ' (' + dl + ' weekly downloads)' : '') + ' \\u2014 verify package name');
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
for (const [pkg, meta] of Object.entries(pkgMeta)) {
|
|
2376
|
+
if (meta.deprecated) {
|
|
2377
|
+
warnings.push('\\u26a0 ' + pkg + ': deprecated \\u2014 ' + meta.deprecated);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (!isPip) {
|
|
2382
|
+
for (const [pkg, meta] of Object.entries(pkgMeta)) {
|
|
2383
|
+
if (meta.weeklyDownloads !== undefined && meta.weeklyDownloads < 50 && !warnings.some(w => w.includes(pkg))) {
|
|
2384
|
+
warnings.push('\\u26a0 ' + pkg + ': very low adoption (' + meta.weeklyDownloads + ' weekly downloads) \\u2014 consider a more established alternative');
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const manifestFile = isPip ? 'requirements.txt'
|
|
2390
|
+
: isGo ? 'go.mod'
|
|
2391
|
+
: isCargo ? 'Cargo.toml'
|
|
2392
|
+
: isGem ? 'Gemfile'
|
|
2393
|
+
: isComposer ? 'composer.json'
|
|
2394
|
+
: 'package.json';
|
|
2395
|
+
const manifestContent = isPip
|
|
2396
|
+
? Object.entries(deps).map(([k, v]) => v === '*' ? k : k + '==' + v).join('\\n')
|
|
2397
|
+
: JSON.stringify({ dependencies: deps });
|
|
2398
|
+
|
|
2386
2399
|
try {
|
|
2387
2400
|
const cveBody = { file_path: manifestFile, content: manifestContent, dependencies: deps };
|
|
2388
2401
|
const cveResp = await fetch(GATEWAY_URL + '/api/v1/cve-scan', {
|
|
@@ -2394,10 +2407,8 @@ async function main() {
|
|
|
2394
2407
|
|
|
2395
2408
|
const findings = Array.isArray(cveResp?.findings) ? cveResp.findings : [];
|
|
2396
2409
|
const scannedPkgs = Object.entries(deps).map(([k, v]) => k + '@' + v).join(', ');
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
}
|
|
2400
|
-
else if (findings.length > 0) {
|
|
2410
|
+
|
|
2411
|
+
if (findings.length > 0) {
|
|
2401
2412
|
const top3 = findings.slice(0, 3).map((f: any) => {
|
|
2402
2413
|
const id = f.cve || f.id || '?';
|
|
2403
2414
|
const pkg = f.package || '?';
|
|
@@ -2407,8 +2418,9 @@ async function main() {
|
|
|
2407
2418
|
}).join('; ');
|
|
2408
2419
|
const count = findings.length;
|
|
2409
2420
|
const label = count === 1 ? 'advisory' : 'advisories';
|
|
2410
|
-
const cveMsg = '[synkro:
|
|
2411
|
-
const ctx = 'CVE: ' + top3 + '\\nDo NOT install packages with known vulnerabilities. Use a patched version or a different package.'
|
|
2421
|
+
const cveMsg = '[synkro:installScan] ' + cmdShort + ' \\u2192 ' + count + ' ' + label;
|
|
2422
|
+
const ctx = 'CVE: ' + top3 + '\\nDo NOT install packages with known vulnerabilities. Use a patched version or a different package.'
|
|
2423
|
+
+ (warnings.length > 0 ? '\\n' + warnings.join('\\n') : '');
|
|
2412
2424
|
|
|
2413
2425
|
const config = await loadConfig(jwt);
|
|
2414
2426
|
for (const f of findings) {
|
|
@@ -2435,8 +2447,15 @@ async function main() {
|
|
|
2435
2447
|
});
|
|
2436
2448
|
return;
|
|
2437
2449
|
}
|
|
2450
|
+
|
|
2451
|
+
const parts: string[] = ['[synkro:installScan] ' + scannedPkgs + ' \\u2192 clean, no known vulnerabilities'];
|
|
2452
|
+
if (warnings.length > 0) parts.push(...warnings);
|
|
2453
|
+
installScanMsg = parts.join('\\n');
|
|
2438
2454
|
} catch (e) {
|
|
2439
|
-
log('bashGuard
|
|
2455
|
+
log('bashGuard install scan failed: ' + String(e));
|
|
2456
|
+
if (warnings.length > 0) {
|
|
2457
|
+
installScanMsg = '[synkro:installScan] ' + warnings.join('\\n');
|
|
2458
|
+
}
|
|
2440
2459
|
}
|
|
2441
2460
|
}
|
|
2442
2461
|
}
|
|
@@ -2450,7 +2469,7 @@ async function main() {
|
|
|
2450
2469
|
const tagStr = tag(rt, config);
|
|
2451
2470
|
|
|
2452
2471
|
if (config.silent) {
|
|
2453
|
-
const msg = (
|
|
2472
|
+
const msg = (installScanMsg ? installScanMsg + '\\n' : '') + tagStr + ' bashGuard \\u2192 skipped (silent mode)';
|
|
2454
2473
|
outputJson({ systemMessage: msg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
|
|
2455
2474
|
return;
|
|
2456
2475
|
}
|
|
@@ -2482,7 +2501,7 @@ async function main() {
|
|
|
2482
2501
|
|
|
2483
2502
|
if (mode === 'audit') {
|
|
2484
2503
|
const reason = tagStr + ' bashGuard \\u2192 warning' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation');
|
|
2485
|
-
const combined = (
|
|
2504
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
2486
2505
|
outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
|
|
2487
2506
|
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
2488
2507
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
@@ -2491,7 +2510,7 @@ async function main() {
|
|
|
2491
2510
|
});
|
|
2492
2511
|
} else {
|
|
2493
2512
|
const reason = tagStr + ' bashGuard \\u2192 blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
2494
|
-
const combined = (
|
|
2513
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
2495
2514
|
outputJson({
|
|
2496
2515
|
systemMessage: combined,
|
|
2497
2516
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: combined },
|
|
@@ -2504,7 +2523,7 @@ async function main() {
|
|
|
2504
2523
|
}
|
|
2505
2524
|
} else {
|
|
2506
2525
|
const reason = tagStr + ' bashGuard \\u2192 pass: ' + (verdict.reason || 'no policy violations detected');
|
|
2507
|
-
const combined = (
|
|
2526
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
2508
2527
|
outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
|
|
2509
2528
|
dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'trivial_utility',
|
|
2510
2529
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
@@ -2544,26 +2563,26 @@ async function main() {
|
|
|
2544
2563
|
|
|
2545
2564
|
if (!resp) {
|
|
2546
2565
|
log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
|
|
2547
|
-
if (
|
|
2548
|
-
outputJson({ systemMessage:
|
|
2566
|
+
if (installScanMsg) {
|
|
2567
|
+
outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
|
|
2549
2568
|
} else { outputEmpty(); }
|
|
2550
2569
|
return;
|
|
2551
2570
|
}
|
|
2552
2571
|
|
|
2553
2572
|
if (!resp.hook_response || typeof resp.hook_response !== 'object') {
|
|
2554
2573
|
log('bashGuard ' + cmdShort + ' \\u2192 pass (no hook_response)');
|
|
2555
|
-
if (
|
|
2556
|
-
outputJson({ systemMessage:
|
|
2574
|
+
if (installScanMsg) {
|
|
2575
|
+
outputJson({ systemMessage: installScanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: installScanMsg } });
|
|
2557
2576
|
} else { outputEmpty(); }
|
|
2558
2577
|
return;
|
|
2559
2578
|
}
|
|
2560
2579
|
|
|
2561
|
-
if (
|
|
2580
|
+
if (installScanMsg) {
|
|
2562
2581
|
const existing = resp.hook_response.systemMessage || '';
|
|
2563
|
-
resp.hook_response.systemMessage =
|
|
2582
|
+
resp.hook_response.systemMessage = installScanMsg + (existing ? '\\n' + existing : '');
|
|
2564
2583
|
if (resp.hook_response.hookSpecificOutput) {
|
|
2565
2584
|
const existingCtx = resp.hook_response.hookSpecificOutput.additionalContext || '';
|
|
2566
|
-
resp.hook_response.hookSpecificOutput.additionalContext =
|
|
2585
|
+
resp.hook_response.hookSpecificOutput.additionalContext = installScanMsg + (existingCtx ? '\\n' + existingCtx : '');
|
|
2567
2586
|
} else {
|
|
2568
2587
|
resp.hook_response.hookSpecificOutput = { hookEventName: 'PreToolUse', additionalContext: resp.hook_response.systemMessage };
|
|
2569
2588
|
}
|
|
@@ -3292,102 +3311,595 @@ async function main() {
|
|
|
3292
3311
|
|
|
3293
3312
|
main();
|
|
3294
3313
|
`;
|
|
3295
|
-
|
|
3296
|
-
|
|
3314
|
+
CURSOR_BASH_JUDGE_TS = `#!/usr/bin/env bun
|
|
3315
|
+
import {
|
|
3316
|
+
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3317
|
+
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3318
|
+
appendLocalTelemetry, log, GATEWAY_URL,
|
|
3319
|
+
type HookConfig, type Rule,
|
|
3320
|
+
} from './_synkro-common.ts';
|
|
3297
3321
|
|
|
3298
|
-
|
|
3299
|
-
import { createServer } from "http";
|
|
3300
|
-
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
3301
|
-
import { homedir as homedir3, platform } from "os";
|
|
3302
|
-
import { join as join3, dirname as dirname4 } from "path";
|
|
3303
|
-
import { execFile } from "child_process";
|
|
3304
|
-
import jwt from "jsonwebtoken";
|
|
3305
|
-
function openBrowser(url) {
|
|
3306
|
-
const os = platform();
|
|
3307
|
-
let bin;
|
|
3308
|
-
let args2;
|
|
3309
|
-
switch (os) {
|
|
3310
|
-
case "darwin":
|
|
3311
|
-
bin = "open";
|
|
3312
|
-
args2 = [url];
|
|
3313
|
-
break;
|
|
3314
|
-
case "win32":
|
|
3315
|
-
bin = "cmd";
|
|
3316
|
-
args2 = ["/c", "start", "", url];
|
|
3317
|
-
break;
|
|
3318
|
-
default:
|
|
3319
|
-
bin = "xdg-open";
|
|
3320
|
-
args2 = [url];
|
|
3321
|
-
}
|
|
3322
|
-
execFile(bin, args2, (error) => {
|
|
3323
|
-
if (error) {
|
|
3324
|
-
console.error("Failed to open browser automatically.");
|
|
3325
|
-
console.log(`Please open this URL manually: ${url}`);
|
|
3326
|
-
}
|
|
3327
|
-
});
|
|
3328
|
-
}
|
|
3329
|
-
function saveCredentials(data) {
|
|
3330
|
-
const dir = dirname4(AUTH_FILE);
|
|
3331
|
-
if (!existsSync5(dir)) {
|
|
3332
|
-
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
3333
|
-
}
|
|
3334
|
-
writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
3335
|
-
}
|
|
3336
|
-
function loadCredentials() {
|
|
3337
|
-
if (!existsSync5(AUTH_FILE)) {
|
|
3338
|
-
return null;
|
|
3339
|
-
}
|
|
3322
|
+
async function main() {
|
|
3340
3323
|
try {
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
res.end();
|
|
3324
|
+
const input = await readStdin();
|
|
3325
|
+
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3326
|
+
|
|
3327
|
+
const payload = JSON.parse(input);
|
|
3328
|
+
const command = payload.command || '';
|
|
3329
|
+
if (!command) { process.stdout.write('{}\\n'); return; }
|
|
3330
|
+
|
|
3331
|
+
const cwd = payload.cwd || '';
|
|
3332
|
+
const sessionId = payload.conversation_id || '';
|
|
3333
|
+
const repo = detectRepo(cwd || '.');
|
|
3334
|
+
|
|
3335
|
+
const cmdShort = command.slice(0, 80);
|
|
3336
|
+
log('bashGuard checking: ' + cmdShort);
|
|
3337
|
+
|
|
3338
|
+
let jwt = loadJwt();
|
|
3339
|
+
if (!jwt) { process.stdout.write('{}\\n'); return; }
|
|
3340
|
+
jwt = await ensureFreshJwt(jwt);
|
|
3341
|
+
|
|
3342
|
+
const config = await loadConfig(jwt);
|
|
3343
|
+
if (config.silent) { process.stdout.write('{}\\n'); return; }
|
|
3344
|
+
|
|
3345
|
+
const rt = await route(config);
|
|
3346
|
+
const tagStr = tag(rt, config);
|
|
3347
|
+
|
|
3348
|
+
if (rt === 'local') {
|
|
3349
|
+
// Build grading prompt with rules
|
|
3350
|
+
const rulesBlock = config.rules.map((r: Rule, i: number) =>
|
|
3351
|
+
(i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
|
|
3352
|
+
).join('\\n');
|
|
3353
|
+
|
|
3354
|
+
const graderPrompt = [
|
|
3355
|
+
'RULES:',
|
|
3356
|
+
rulesBlock || '(none)',
|
|
3357
|
+
'',
|
|
3358
|
+
'COMMAND TO EVALUATE:',
|
|
3359
|
+
command,
|
|
3360
|
+
].join('\\n');
|
|
3361
|
+
|
|
3362
|
+
let gradeResp: string;
|
|
3363
|
+
try {
|
|
3364
|
+
gradeResp = await localGrade('bash', graderPrompt);
|
|
3365
|
+
} catch {
|
|
3366
|
+
process.stdout.write('{}\\n');
|
|
3385
3367
|
return;
|
|
3386
3368
|
}
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3369
|
+
|
|
3370
|
+
const verdict = parseVerdict(gradeResp);
|
|
3371
|
+
|
|
3372
|
+
if (!verdict.ok) {
|
|
3373
|
+
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3374
|
+
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3375
|
+
|
|
3376
|
+
if (mode !== 'audit') {
|
|
3377
|
+
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3378
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
3379
|
+
command, reasoning: guardReason,
|
|
3380
|
+
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3381
|
+
});
|
|
3382
|
+
const result = {
|
|
3383
|
+
permission: 'deny',
|
|
3384
|
+
user_message: tagStr + ' bashGuard \\u2192 block: ' + guardReason,
|
|
3385
|
+
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
3386
|
+
};
|
|
3387
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// Audit mode \u2014 warn but allow
|
|
3392
|
+
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
3393
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
3394
|
+
command, reasoning: guardReason,
|
|
3395
|
+
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3396
|
+
});
|
|
3397
|
+
} else {
|
|
3398
|
+
dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'clean',
|
|
3399
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
3400
|
+
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
3401
|
+
rulesChecked: config.rules, violatedRules: [],
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
process.stdout.write('{}\\n');
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
3410
|
+
const body = {
|
|
3411
|
+
hook_event: 'PreToolUse',
|
|
3412
|
+
tool_name: 'Bash',
|
|
3413
|
+
tool_input: { command },
|
|
3414
|
+
response_format: 'cursor',
|
|
3415
|
+
session_id: sessionId || null,
|
|
3416
|
+
cwd: cwd || null,
|
|
3417
|
+
repo: repo || null,
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 6000);
|
|
3421
|
+
|
|
3422
|
+
if (!resp) {
|
|
3423
|
+
log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
|
|
3424
|
+
process.stdout.write('{}\\n');
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
if (resp.hook_response) {
|
|
3429
|
+
process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
|
|
3430
|
+
} else {
|
|
3431
|
+
process.stdout.write('{}\\n');
|
|
3432
|
+
}
|
|
3433
|
+
} catch {
|
|
3434
|
+
process.stdout.write('{}\\n');
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
main();
|
|
3439
|
+
`;
|
|
3440
|
+
CURSOR_EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
3441
|
+
import {
|
|
3442
|
+
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3443
|
+
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3444
|
+
appendLocalTelemetry, log, GATEWAY_URL,
|
|
3445
|
+
type HookConfig, type Rule,
|
|
3446
|
+
} from './_synkro-common.ts';
|
|
3447
|
+
import { basename } from 'node:path';
|
|
3448
|
+
|
|
3449
|
+
async function main() {
|
|
3450
|
+
try {
|
|
3451
|
+
const input = await readStdin();
|
|
3452
|
+
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3453
|
+
|
|
3454
|
+
const payload = JSON.parse(input);
|
|
3455
|
+
const toolName = payload.tool_name || '';
|
|
3456
|
+
const toolInput = payload.tool_input || {};
|
|
3457
|
+
const cwd = payload.cwd || '';
|
|
3458
|
+
const sessionId = payload.conversation_id || '';
|
|
3459
|
+
|
|
3460
|
+
const filePath = toolInput.file_path || toolInput.path || toolInput.target_file || '';
|
|
3461
|
+
const content = toolInput.content || toolInput.new_string || toolInput.code_edit || '';
|
|
3462
|
+
if (!filePath) { process.stdout.write('{}\\n'); return; }
|
|
3463
|
+
|
|
3464
|
+
const fileShort = basename(filePath);
|
|
3465
|
+
log('editGuard checking: ' + fileShort);
|
|
3466
|
+
|
|
3467
|
+
const repo = detectRepo(cwd || '.');
|
|
3468
|
+
|
|
3469
|
+
let jwt = loadJwt();
|
|
3470
|
+
if (!jwt) { process.stdout.write('{}\\n'); return; }
|
|
3471
|
+
jwt = await ensureFreshJwt(jwt);
|
|
3472
|
+
|
|
3473
|
+
const config = await loadConfig(jwt);
|
|
3474
|
+
if (config.silent) { process.stdout.write('{}\\n'); return; }
|
|
3475
|
+
|
|
3476
|
+
const rt = await route(config);
|
|
3477
|
+
const tagStr = tag(rt, config);
|
|
3478
|
+
|
|
3479
|
+
if (rt === 'local') {
|
|
3480
|
+
const contentShort = content.slice(0, 4000);
|
|
3481
|
+
const rulesBlock = config.rules.map((r: Rule, i: number) =>
|
|
3482
|
+
(i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
|
|
3483
|
+
).join('\\n');
|
|
3484
|
+
|
|
3485
|
+
const graderPrompt = [
|
|
3486
|
+
'RULES:',
|
|
3487
|
+
rulesBlock || '(none)',
|
|
3488
|
+
'',
|
|
3489
|
+
'FILE: ' + filePath,
|
|
3490
|
+
'',
|
|
3491
|
+
'CONTENT TO EVALUATE (first 4000 chars):',
|
|
3492
|
+
contentShort,
|
|
3493
|
+
].join('\\n');
|
|
3494
|
+
|
|
3495
|
+
let gradeResp: string;
|
|
3496
|
+
try {
|
|
3497
|
+
gradeResp = await localGrade('edit', graderPrompt);
|
|
3498
|
+
} catch {
|
|
3499
|
+
process.stdout.write('{}\\n');
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
const verdict = parseVerdict(gradeResp);
|
|
3504
|
+
const editContent = 'file=' + filePath + ' content=' + content.slice(0, 2000);
|
|
3505
|
+
|
|
3506
|
+
if (!verdict.ok) {
|
|
3507
|
+
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3508
|
+
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3509
|
+
|
|
3510
|
+
if (mode !== 'audit') {
|
|
3511
|
+
dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3512
|
+
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3513
|
+
command: editContent, reasoning: guardReason,
|
|
3514
|
+
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3515
|
+
});
|
|
3516
|
+
const result = {
|
|
3517
|
+
permission: 'deny',
|
|
3518
|
+
user_message: tagStr + ' editGuard ' + fileShort + ' \\u2192 block: ' + guardReason,
|
|
3519
|
+
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
3520
|
+
};
|
|
3521
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// Audit mode
|
|
3526
|
+
dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
3527
|
+
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3528
|
+
command: editContent, reasoning: guardReason,
|
|
3529
|
+
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3530
|
+
});
|
|
3531
|
+
} else {
|
|
3532
|
+
dispatchCapture(jwt, 'edit', 'pass', 'audit', verdict.category || 'trivial_edit',
|
|
3533
|
+
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3534
|
+
command: editContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
3535
|
+
rulesChecked: config.rules, violatedRules: [],
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
process.stdout.write('{}\\n');
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
3544
|
+
const body = {
|
|
3545
|
+
hook_event: 'PreToolUse',
|
|
3546
|
+
tool_name: toolName || 'Edit',
|
|
3547
|
+
tool_input: { file_path: filePath, content },
|
|
3548
|
+
file_path: filePath,
|
|
3549
|
+
content,
|
|
3550
|
+
response_format: 'cursor',
|
|
3551
|
+
session_id: sessionId || null,
|
|
3552
|
+
cwd: cwd || null,
|
|
3553
|
+
repo: repo || null,
|
|
3554
|
+
};
|
|
3555
|
+
|
|
3556
|
+
const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
|
|
3557
|
+
|
|
3558
|
+
if (!resp) {
|
|
3559
|
+
log('editGuard ' + fileShort + ' \\u2192 error (timeout)');
|
|
3560
|
+
process.stdout.write('{}\\n');
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
if (resp.hook_response) {
|
|
3565
|
+
process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
|
|
3566
|
+
} else {
|
|
3567
|
+
process.stdout.write('{}\\n');
|
|
3568
|
+
}
|
|
3569
|
+
} catch {
|
|
3570
|
+
process.stdout.write('{}\\n');
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
main();
|
|
3575
|
+
`;
|
|
3576
|
+
CURSOR_EDIT_CAPTURE_TS = `#!/usr/bin/env bun
|
|
3577
|
+
import {
|
|
3578
|
+
loadJwt, ensureFreshJwt, detectRepo, readStdin,
|
|
3579
|
+
appendLocalTelemetry, log, GATEWAY_URL,
|
|
3580
|
+
} from './_synkro-common.ts';
|
|
3581
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3582
|
+
import { basename, dirname, join } from 'node:path';
|
|
3583
|
+
import { homedir } from 'node:os';
|
|
3584
|
+
|
|
3585
|
+
async function main() {
|
|
3586
|
+
try {
|
|
3587
|
+
const input = await readStdin();
|
|
3588
|
+
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3589
|
+
|
|
3590
|
+
const payload = JSON.parse(input);
|
|
3591
|
+
const filePath = payload.file_path || '';
|
|
3592
|
+
if (!filePath) { process.stdout.write('{}\\n'); return; }
|
|
3593
|
+
|
|
3594
|
+
const cwd = payload.cwd || '';
|
|
3595
|
+
const sessionId = payload.conversation_id || '';
|
|
3596
|
+
const repo = detectRepo(cwd || '.');
|
|
3597
|
+
|
|
3598
|
+
log('editScan ' + basename(filePath));
|
|
3599
|
+
|
|
3600
|
+
let jwt = loadJwt();
|
|
3601
|
+
if (!jwt) { process.stdout.write('{}\\n'); return; }
|
|
3602
|
+
jwt = await ensureFreshJwt(jwt);
|
|
3603
|
+
|
|
3604
|
+
// Read actual file content (up to 50KB)
|
|
3605
|
+
let fileContent = '';
|
|
3606
|
+
const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
|
|
3607
|
+
try {
|
|
3608
|
+
if (existsSync(fullPath)) {
|
|
3609
|
+
const buf = readFileSync(fullPath);
|
|
3610
|
+
fileContent = buf.slice(0, 50000).toString('utf-8');
|
|
3611
|
+
}
|
|
3612
|
+
} catch {}
|
|
3613
|
+
|
|
3614
|
+
// Walk up to find package.json dependencies
|
|
3615
|
+
let dependencies: Record<string, string> = {};
|
|
3616
|
+
let pkgDir = cwd || dirname(fullPath);
|
|
3617
|
+
while (pkgDir !== '/' && pkgDir !== '.') {
|
|
3618
|
+
const pkgPath = join(pkgDir, 'package.json');
|
|
3619
|
+
if (existsSync(pkgPath)) {
|
|
3620
|
+
try {
|
|
3621
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
3622
|
+
dependencies = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
3623
|
+
} catch {}
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
const parent = dirname(pkgDir);
|
|
3627
|
+
if (parent === pkgDir) break;
|
|
3628
|
+
pkgDir = parent;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
const captureBody: Record<string, any> = {
|
|
3632
|
+
capture_type: 'edit_scan',
|
|
3633
|
+
tool_input: { file_path: filePath, content: fileContent },
|
|
3634
|
+
edit_verdict: { ok: true },
|
|
3635
|
+
dependencies,
|
|
3636
|
+
};
|
|
3637
|
+
if (sessionId) captureBody.session_id = sessionId;
|
|
3638
|
+
if (cwd) captureBody.cwd = cwd;
|
|
3639
|
+
if (repo) captureBody.repo = repo;
|
|
3640
|
+
|
|
3641
|
+
// Check if local_only
|
|
3642
|
+
const rulesPath = join(homedir(), '.synkro', 'rules.json');
|
|
3643
|
+
if (existsSync(rulesPath)) {
|
|
3644
|
+
appendLocalTelemetry(captureBody);
|
|
3645
|
+
} else {
|
|
3646
|
+
// Fire-and-forget to cloud
|
|
3647
|
+
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
3648
|
+
method: 'POST',
|
|
3649
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3650
|
+
body: JSON.stringify(captureBody),
|
|
3651
|
+
signal: AbortSignal.timeout(10000),
|
|
3652
|
+
}).catch(() => {});
|
|
3653
|
+
appendLocalTelemetry(captureBody);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
process.stdout.write('{}\\n');
|
|
3657
|
+
} catch {
|
|
3658
|
+
process.stdout.write('{}\\n');
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
main();
|
|
3663
|
+
`;
|
|
3664
|
+
CURSOR_BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
3665
|
+
import {
|
|
3666
|
+
loadJwt, readStdin, appendLocalTelemetry, log, GATEWAY_URL,
|
|
3667
|
+
} from './_synkro-common.ts';
|
|
3668
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
3669
|
+
import { join, dirname } from 'node:path';
|
|
3670
|
+
import { createHash } from 'node:crypto';
|
|
3671
|
+
import { homedir } from 'node:os';
|
|
3672
|
+
|
|
3673
|
+
const CONSENT_FILE = join(homedir(), '.synkro', '.local-consent');
|
|
3674
|
+
|
|
3675
|
+
function hashCmd(cmd: string): string {
|
|
3676
|
+
return createHash('sha256').update(cmd).digest('hex').slice(0, 16);
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
function consentGrant(sid: string, hash: string): void {
|
|
3680
|
+
try {
|
|
3681
|
+
const dir = dirname(CONSENT_FILE);
|
|
3682
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3683
|
+
appendFileSync(CONSENT_FILE, sid + '\\t' + hash + '\\tactive\\n', 'utf-8');
|
|
3684
|
+
} catch {}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
function consentHasActive(sid: string, hash: string): boolean {
|
|
3688
|
+
try {
|
|
3689
|
+
if (!existsSync(CONSENT_FILE)) return false;
|
|
3690
|
+
const content = readFileSync(CONSENT_FILE, 'utf-8');
|
|
3691
|
+
return content.includes(sid + '\\t' + hash + '\\tactive');
|
|
3692
|
+
} catch {
|
|
3693
|
+
return false;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function consentConsume(sid: string, hash: string): void {
|
|
3698
|
+
try {
|
|
3699
|
+
if (!existsSync(CONSENT_FILE)) return;
|
|
3700
|
+
const content = readFileSync(CONSENT_FILE, 'utf-8');
|
|
3701
|
+
const target = sid + '\\t' + hash + '\\tactive';
|
|
3702
|
+
const replacement = sid + '\\t' + hash + '\\tconsumed';
|
|
3703
|
+
const updated = content.split('\\n').map((l: string) => l === target ? replacement : l).join('\\n');
|
|
3704
|
+
writeFileSync(CONSENT_FILE, updated, 'utf-8');
|
|
3705
|
+
} catch {}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
async function main() {
|
|
3709
|
+
try {
|
|
3710
|
+
const input = await readStdin();
|
|
3711
|
+
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3712
|
+
|
|
3713
|
+
const payload = JSON.parse(input);
|
|
3714
|
+
const toolName = payload.tool_name || '';
|
|
3715
|
+
|
|
3716
|
+
// Only process shell/bash tool types
|
|
3717
|
+
const shellTools = ['Shell', 'Bash', 'terminal', 'run_terminal_cmd', 'execute_command'];
|
|
3718
|
+
if (!shellTools.includes(toolName)) { process.stdout.write('{}\\n'); return; }
|
|
3719
|
+
|
|
3720
|
+
const sessionId = payload.conversation_id || '';
|
|
3721
|
+
const toolUseId = payload.tool_use_id || '';
|
|
3722
|
+
const isError = payload.tool_result?.is_error === true;
|
|
3723
|
+
const command = payload.tool_input?.command || '';
|
|
3724
|
+
const cmdHash = command ? hashCmd(command) : '';
|
|
3725
|
+
|
|
3726
|
+
// Consent tracking
|
|
3727
|
+
if (cmdHash && sessionId) {
|
|
3728
|
+
if (!isError) {
|
|
3729
|
+
consentConsume(sessionId, cmdHash);
|
|
3730
|
+
} else {
|
|
3731
|
+
if (!consentHasActive(sessionId, cmdHash)) {
|
|
3732
|
+
consentGrant(sessionId, cmdHash);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
// Build capture body
|
|
3738
|
+
const captureBody: Record<string, any> = {
|
|
3739
|
+
capture_type: 'bash_followup',
|
|
3740
|
+
session_id: sessionId || null,
|
|
3741
|
+
tool_use_id: toolUseId || null,
|
|
3742
|
+
is_error: isError,
|
|
3743
|
+
command_hash: cmdHash,
|
|
3744
|
+
};
|
|
3745
|
+
|
|
3746
|
+
// Check if local_only
|
|
3747
|
+
const rulesPath = join(homedir(), '.synkro', 'rules.json');
|
|
3748
|
+
if (existsSync(rulesPath)) {
|
|
3749
|
+
appendLocalTelemetry(captureBody);
|
|
3750
|
+
} else {
|
|
3751
|
+
const jwt = loadJwt();
|
|
3752
|
+
if (jwt && sessionId && toolUseId) {
|
|
3753
|
+
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
3754
|
+
method: 'POST',
|
|
3755
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3756
|
+
body: JSON.stringify(captureBody),
|
|
3757
|
+
signal: AbortSignal.timeout(3000),
|
|
3758
|
+
}).catch(() => {});
|
|
3759
|
+
}
|
|
3760
|
+
appendLocalTelemetry(captureBody);
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
process.stdout.write('{}\\n');
|
|
3764
|
+
} catch {
|
|
3765
|
+
process.stdout.write('{}\\n');
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
main();
|
|
3770
|
+
`;
|
|
3771
|
+
CURSOR_SESSION_START_TS = `#!/usr/bin/env bun
|
|
3772
|
+
import {
|
|
3773
|
+
loadJwt, loadConfig, readStdin,
|
|
3774
|
+
type HookConfig,
|
|
3775
|
+
} from './_synkro-common.ts';
|
|
3776
|
+
|
|
3777
|
+
async function main() {
|
|
3778
|
+
try {
|
|
3779
|
+
const input = await readStdin();
|
|
3780
|
+
|
|
3781
|
+
let jwt = loadJwt();
|
|
3782
|
+
const config: HookConfig = jwt ? await loadConfig(jwt) : {
|
|
3783
|
+
captureDepth: 'local_only', tier: 'standard', silent: false,
|
|
3784
|
+
policyName: '', rules: [], scanExemptions: [],
|
|
3785
|
+
};
|
|
3786
|
+
|
|
3787
|
+
const policyName = config.policyName || 'default';
|
|
3788
|
+
const ruleCount = config.rules.length;
|
|
3789
|
+
const mode = config.silent ? 'silent' : 'active';
|
|
3790
|
+
|
|
3791
|
+
const context = [
|
|
3792
|
+
'This session is monitored by Synkro (' + mode + ' mode, policy: "' + policyName + '", ' + ruleCount + ' rules).',
|
|
3793
|
+
'Synkro enforces security and compliance rules on tool calls (shell commands, file edits).',
|
|
3794
|
+
'If a tool call is blocked, Synkro will explain which rule was violated and why.',
|
|
3795
|
+
'Do not suggest workarounds to bypass Synkro hooks \u2014 fix the underlying issue instead.',
|
|
3796
|
+
].join(' ');
|
|
3797
|
+
|
|
3798
|
+
const result = { additional_context: context };
|
|
3799
|
+
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3800
|
+
} catch {
|
|
3801
|
+
process.stdout.write('{}\\n');
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
main();
|
|
3806
|
+
`;
|
|
3807
|
+
}
|
|
3808
|
+
});
|
|
3809
|
+
|
|
3810
|
+
// cli/auth/stub.ts
|
|
3811
|
+
import { createServer } from "http";
|
|
3812
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
3813
|
+
import { homedir as homedir4, platform } from "os";
|
|
3814
|
+
import { join as join3, dirname as dirname4 } from "path";
|
|
3815
|
+
import { execFile } from "child_process";
|
|
3816
|
+
import jwt from "jsonwebtoken";
|
|
3817
|
+
function openBrowser(url) {
|
|
3818
|
+
const os = platform();
|
|
3819
|
+
let bin;
|
|
3820
|
+
let args2;
|
|
3821
|
+
switch (os) {
|
|
3822
|
+
case "darwin":
|
|
3823
|
+
bin = "open";
|
|
3824
|
+
args2 = [url];
|
|
3825
|
+
break;
|
|
3826
|
+
case "win32":
|
|
3827
|
+
bin = "cmd";
|
|
3828
|
+
args2 = ["/c", "start", "", url];
|
|
3829
|
+
break;
|
|
3830
|
+
default:
|
|
3831
|
+
bin = "xdg-open";
|
|
3832
|
+
args2 = [url];
|
|
3833
|
+
}
|
|
3834
|
+
execFile(bin, args2, (error) => {
|
|
3835
|
+
if (error) {
|
|
3836
|
+
console.error("Failed to open browser automatically.");
|
|
3837
|
+
console.log(`Please open this URL manually: ${url}`);
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
function saveCredentials(data) {
|
|
3842
|
+
const dir = dirname4(AUTH_FILE);
|
|
3843
|
+
if (!existsSync4(dir)) {
|
|
3844
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
3845
|
+
}
|
|
3846
|
+
writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
3847
|
+
}
|
|
3848
|
+
function loadCredentials() {
|
|
3849
|
+
if (!existsSync4(AUTH_FILE)) {
|
|
3850
|
+
return null;
|
|
3851
|
+
}
|
|
3852
|
+
try {
|
|
3853
|
+
const content = readFileSync4(AUTH_FILE, "utf8");
|
|
3854
|
+
return JSON.parse(content);
|
|
3855
|
+
} catch (error) {
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
function createCallbackServer() {
|
|
3860
|
+
const CORS_HEADERS = {
|
|
3861
|
+
"Access-Control-Allow-Origin": SYNKRO_WEB_AUTH_URL,
|
|
3862
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
3863
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
3864
|
+
"Vary": "Origin"
|
|
3865
|
+
};
|
|
3866
|
+
return new Promise((resolve3, reject) => {
|
|
3867
|
+
const server = createServer((req, res) => {
|
|
3868
|
+
if (req.method === "OPTIONS") {
|
|
3869
|
+
const origin = req.headers.origin;
|
|
3870
|
+
if (origin === SYNKRO_WEB_AUTH_URL) {
|
|
3871
|
+
res.writeHead(204, CORS_HEADERS);
|
|
3872
|
+
} else {
|
|
3873
|
+
res.writeHead(204, {
|
|
3874
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
3875
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
3876
|
+
"Vary": "Origin"
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
res.end();
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
const reqOrigin = req.headers.origin;
|
|
3883
|
+
if (reqOrigin && reqOrigin !== SYNKRO_WEB_AUTH_URL) {
|
|
3884
|
+
res.writeHead(403, { "Vary": "Origin" });
|
|
3885
|
+
res.end();
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
if (!req.url) {
|
|
3889
|
+
res.writeHead(404, CORS_HEADERS);
|
|
3890
|
+
res.end();
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
3894
|
+
if (url.pathname !== "/auth") {
|
|
3895
|
+
res.writeHead(404, CORS_HEADERS);
|
|
3896
|
+
res.end();
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
if (req.method !== "POST") {
|
|
3900
|
+
res.writeHead(405, { ...CORS_HEADERS, "Allow": "POST, OPTIONS", "Content-Type": "text/html" });
|
|
3901
|
+
res.end(ERROR_HTML);
|
|
3902
|
+
return;
|
|
3391
3903
|
}
|
|
3392
3904
|
const MAX_BODY = 16 * 1024;
|
|
3393
3905
|
const chunks = [];
|
|
@@ -3440,7 +3952,7 @@ function createCallbackServer() {
|
|
|
3440
3952
|
res.end(JSON.stringify({ ok: true }));
|
|
3441
3953
|
setTimeout(() => {
|
|
3442
3954
|
server.close();
|
|
3443
|
-
|
|
3955
|
+
resolve3(authData);
|
|
3444
3956
|
}, 200);
|
|
3445
3957
|
});
|
|
3446
3958
|
req.on("error", (e) => {
|
|
@@ -3582,7 +4094,7 @@ async function ensureValidToken() {
|
|
|
3582
4094
|
return true;
|
|
3583
4095
|
}
|
|
3584
4096
|
function clearCredentials() {
|
|
3585
|
-
if (
|
|
4097
|
+
if (existsSync4(AUTH_FILE)) {
|
|
3586
4098
|
unlinkSync2(AUTH_FILE);
|
|
3587
4099
|
}
|
|
3588
4100
|
}
|
|
@@ -3593,7 +4105,7 @@ var init_stub = __esm({
|
|
|
3593
4105
|
PORT = 8100;
|
|
3594
4106
|
RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
|
|
3595
4107
|
SYNKRO_WEB_AUTH_URL = RAW_WEB_AUTH_URL && /^https?:\/\//.test(RAW_WEB_AUTH_URL) ? RAW_WEB_AUTH_URL : "https://app.synkro.sh";
|
|
3596
|
-
AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(
|
|
4108
|
+
AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(homedir4(), ".synkro", "credentials.json");
|
|
3597
4109
|
RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
|
|
3598
4110
|
SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
|
|
3599
4111
|
ERROR_HTML = `
|
|
@@ -3756,7 +4268,7 @@ jobs:
|
|
|
3756
4268
|
});
|
|
3757
4269
|
|
|
3758
4270
|
// cli/installer/githubSetup.ts
|
|
3759
|
-
import { existsSync as
|
|
4271
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3760
4272
|
import { execSync as execSync2 } from "child_process";
|
|
3761
4273
|
import { join as join4 } from "path";
|
|
3762
4274
|
function ghSecretSet(token, owner, repo, name, value) {
|
|
@@ -3813,7 +4325,7 @@ function writeWorkflowFile(repoRootPath) {
|
|
|
3813
4325
|
function findGitRoot(startCwd) {
|
|
3814
4326
|
let cur = startCwd;
|
|
3815
4327
|
while (cur && cur !== "/") {
|
|
3816
|
-
if (
|
|
4328
|
+
if (existsSync5(join4(cur, ".git"))) return cur;
|
|
3817
4329
|
const parent = join4(cur, "..");
|
|
3818
4330
|
if (parent === cur) break;
|
|
3819
4331
|
cur = parent;
|
|
@@ -3851,10 +4363,10 @@ function detectGitRepo() {
|
|
|
3851
4363
|
}
|
|
3852
4364
|
}
|
|
3853
4365
|
function ask(rl, question) {
|
|
3854
|
-
return new Promise((
|
|
4366
|
+
return new Promise((resolve3) => rl.question(question, resolve3));
|
|
3855
4367
|
}
|
|
3856
4368
|
function waitForGithubToken() {
|
|
3857
|
-
return new Promise((
|
|
4369
|
+
return new Promise((resolve3, reject) => {
|
|
3858
4370
|
const server = createServer2((req, res) => {
|
|
3859
4371
|
if (req.method === "OPTIONS") {
|
|
3860
4372
|
res.writeHead(204, {
|
|
@@ -3891,7 +4403,7 @@ function waitForGithubToken() {
|
|
|
3891
4403
|
});
|
|
3892
4404
|
res.end(JSON.stringify({ ok: true }));
|
|
3893
4405
|
setTimeout(() => server.close(), 200);
|
|
3894
|
-
|
|
4406
|
+
resolve3(parsed.github_token);
|
|
3895
4407
|
} catch {
|
|
3896
4408
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3897
4409
|
res.end(JSON.stringify({ error: "invalid json" }));
|
|
@@ -4056,12 +4568,12 @@ __export(setupGithub_exports, {
|
|
|
4056
4568
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4057
4569
|
import { stdin as input, stdout as output } from "process";
|
|
4058
4570
|
import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
|
|
4059
|
-
import { existsSync as
|
|
4060
|
-
import { homedir as
|
|
4571
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
4572
|
+
import { homedir as homedir5, platform as platform2 } from "os";
|
|
4061
4573
|
import { join as join5 } from "path";
|
|
4062
4574
|
import { execFile as execFile2 } from "child_process";
|
|
4063
4575
|
function readConfig() {
|
|
4064
|
-
if (!
|
|
4576
|
+
if (!existsSync6(CONFIG_PATH)) return {};
|
|
4065
4577
|
const out = {};
|
|
4066
4578
|
for (const line of readFileSync5(CONFIG_PATH, "utf-8").split("\n")) {
|
|
4067
4579
|
const t = line.trim();
|
|
@@ -4076,7 +4588,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
4076
4588
|
process.stdout.write(q);
|
|
4077
4589
|
const wasRaw = process.stdin.isRaw;
|
|
4078
4590
|
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
4079
|
-
return await new Promise((
|
|
4591
|
+
return await new Promise((resolve3) => {
|
|
4080
4592
|
let chunk = "";
|
|
4081
4593
|
const onData = (data) => {
|
|
4082
4594
|
const s = data.toString("utf-8");
|
|
@@ -4084,7 +4596,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
4084
4596
|
process.stdin.removeListener("data", onData);
|
|
4085
4597
|
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
4086
4598
|
process.stdout.write("\n");
|
|
4087
|
-
|
|
4599
|
+
resolve3(chunk);
|
|
4088
4600
|
return;
|
|
4089
4601
|
}
|
|
4090
4602
|
if (s === "") process.exit(130);
|
|
@@ -4125,7 +4637,7 @@ function sleep(ms) {
|
|
|
4125
4637
|
}
|
|
4126
4638
|
function captureClaudeSetupToken() {
|
|
4127
4639
|
const tmpFile = join5(SYNKRO_DIR, `token-capture-${Date.now()}.raw`);
|
|
4128
|
-
return new Promise((
|
|
4640
|
+
return new Promise((resolve3, reject) => {
|
|
4129
4641
|
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
4130
4642
|
stdio: "inherit"
|
|
4131
4643
|
});
|
|
@@ -4155,7 +4667,7 @@ function captureClaudeSetupToken() {
|
|
|
4155
4667
|
reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
|
|
4156
4668
|
return;
|
|
4157
4669
|
}
|
|
4158
|
-
|
|
4670
|
+
resolve3(token[0]);
|
|
4159
4671
|
});
|
|
4160
4672
|
});
|
|
4161
4673
|
}
|
|
@@ -4405,7 +4917,7 @@ var init_setupGithub = __esm({
|
|
|
4405
4917
|
"use strict";
|
|
4406
4918
|
init_githubSetup();
|
|
4407
4919
|
init_stub();
|
|
4408
|
-
SYNKRO_DIR = join5(
|
|
4920
|
+
SYNKRO_DIR = join5(homedir5(), ".synkro");
|
|
4409
4921
|
CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
|
|
4410
4922
|
}
|
|
4411
4923
|
});
|
|
@@ -4434,11 +4946,11 @@ var init_promptFetcher = __esm({
|
|
|
4434
4946
|
});
|
|
4435
4947
|
|
|
4436
4948
|
// cli/local-cc/settings.ts
|
|
4437
|
-
import { existsSync as
|
|
4438
|
-
import { homedir as
|
|
4949
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
4950
|
+
import { homedir as homedir6 } from "os";
|
|
4439
4951
|
import { join as join6 } from "path";
|
|
4440
4952
|
function isLocalCCEnabled() {
|
|
4441
|
-
if (!
|
|
4953
|
+
if (!existsSync7(CONFIG_PATH2)) return false;
|
|
4442
4954
|
try {
|
|
4443
4955
|
const content = readFileSync6(CONFIG_PATH2, "utf-8");
|
|
4444
4956
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
@@ -4451,7 +4963,7 @@ var CONFIG_PATH2;
|
|
|
4451
4963
|
var init_settings = __esm({
|
|
4452
4964
|
"cli/local-cc/settings.ts"() {
|
|
4453
4965
|
"use strict";
|
|
4454
|
-
CONFIG_PATH2 = join6(
|
|
4966
|
+
CONFIG_PATH2 = join6(homedir6(), ".synkro", "config.env");
|
|
4455
4967
|
}
|
|
4456
4968
|
});
|
|
4457
4969
|
|
|
@@ -4605,9 +5117,9 @@ await mcp.connect(new StdioServerTransport());
|
|
|
4605
5117
|
});
|
|
4606
5118
|
|
|
4607
5119
|
// cli/local-cc/install.ts
|
|
4608
|
-
import { existsSync as
|
|
5120
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync7, chmodSync, copyFileSync, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
|
|
4609
5121
|
import { join as join7 } from "path";
|
|
4610
|
-
import { homedir as
|
|
5122
|
+
import { homedir as homedir7 } from "os";
|
|
4611
5123
|
import { spawnSync } from "child_process";
|
|
4612
5124
|
function writePluginFiles() {
|
|
4613
5125
|
mkdirSync6(SESSION_DIR, { recursive: true });
|
|
@@ -4656,7 +5168,7 @@ function runBunInstall() {
|
|
|
4656
5168
|
}
|
|
4657
5169
|
}
|
|
4658
5170
|
function safelyMutateClaudeJson(mutator) {
|
|
4659
|
-
if (!
|
|
5171
|
+
if (!existsSync8(CLAUDE_JSON_PATH)) {
|
|
4660
5172
|
return;
|
|
4661
5173
|
}
|
|
4662
5174
|
const originalText = readFileSync7(CLAUDE_JSON_PATH, "utf-8");
|
|
@@ -4809,17 +5321,17 @@ var init_install = __esm({
|
|
|
4809
5321
|
"cli/local-cc/install.ts"() {
|
|
4810
5322
|
"use strict";
|
|
4811
5323
|
init_channelSource();
|
|
4812
|
-
CLAUDE_JSON_BACKUP_PATH = join7(
|
|
4813
|
-
SESSION_DIR = join7(
|
|
5324
|
+
CLAUDE_JSON_BACKUP_PATH = join7(homedir7(), ".claude.json.synkro-bak");
|
|
5325
|
+
SESSION_DIR = join7(homedir7(), ".synkro", "cc_sessions");
|
|
4814
5326
|
PLUGIN_PATH = join7(SESSION_DIR, "synkro-channel.ts");
|
|
4815
5327
|
PLUGIN_PKG_PATH = join7(SESSION_DIR, "package.json");
|
|
4816
5328
|
PLUGIN_SETTINGS_DIR = join7(SESSION_DIR, ".claude");
|
|
4817
5329
|
PLUGIN_SETTINGS_PATH = join7(PLUGIN_SETTINGS_DIR, "settings.json");
|
|
4818
5330
|
PROJECT_MCP_PATH = join7(SESSION_DIR, ".mcp.json");
|
|
4819
|
-
CLAUDE_JSON_PATH = join7(
|
|
5331
|
+
CLAUDE_JSON_PATH = join7(homedir7(), ".claude.json");
|
|
4820
5332
|
RUN_SCRIPT_PATH = join7(SESSION_DIR, "run-claude.sh");
|
|
4821
5333
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
4822
|
-
SESSION_DIR_2 = join7(
|
|
5334
|
+
SESSION_DIR_2 = join7(homedir7(), ".synkro", "cc_sessions_2");
|
|
4823
5335
|
PLUGIN_PATH_2 = join7(SESSION_DIR_2, "synkro-channel.ts");
|
|
4824
5336
|
PLUGIN_PKG_PATH_2 = join7(SESSION_DIR_2, "package.json");
|
|
4825
5337
|
PLUGIN_SETTINGS_DIR_2 = join7(SESSION_DIR_2, ".claude");
|
|
@@ -4978,7 +5490,7 @@ log "tmux session ended."
|
|
|
4978
5490
|
|
|
4979
5491
|
// cli/local-cc/pueue.ts
|
|
4980
5492
|
import { execFileSync, spawnSync as spawnSync2, spawn } from "child_process";
|
|
4981
|
-
import { homedir as
|
|
5493
|
+
import { homedir as homedir8 } from "os";
|
|
4982
5494
|
import { join as join8 } from "path";
|
|
4983
5495
|
import { connect } from "net";
|
|
4984
5496
|
function pueueAvailable() {
|
|
@@ -4994,568 +5506,1377 @@ function statusJson() {
|
|
|
4994
5506
|
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
4995
5507
|
}
|
|
4996
5508
|
try {
|
|
4997
|
-
return JSON.parse(r.stdout);
|
|
4998
|
-
} catch (err) {
|
|
4999
|
-
throw new PueueError(`pueue status returned non-JSON output: ${r.stdout.slice(0, 200)}`, err);
|
|
5509
|
+
return JSON.parse(r.stdout);
|
|
5510
|
+
} catch (err) {
|
|
5511
|
+
throw new PueueError(`pueue status returned non-JSON output: ${r.stdout.slice(0, 200)}`, err);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
function statusName(s) {
|
|
5515
|
+
if (typeof s === "string") return s;
|
|
5516
|
+
if (s && typeof s === "object") {
|
|
5517
|
+
if ("Running" in s) return "Running";
|
|
5518
|
+
if ("Done" in s) {
|
|
5519
|
+
const result = s.Done?.result;
|
|
5520
|
+
if (typeof result === "string") return `Done (${result})`;
|
|
5521
|
+
if (result && typeof result === "object") return `Done (${Object.keys(result)[0] ?? "unknown"})`;
|
|
5522
|
+
return "Done";
|
|
5523
|
+
}
|
|
5524
|
+
return Object.keys(s)[0] ?? "unknown";
|
|
5525
|
+
}
|
|
5526
|
+
return "unknown";
|
|
5527
|
+
}
|
|
5528
|
+
function findTask(channel = CHANNEL_PRIMARY) {
|
|
5529
|
+
const data = statusJson();
|
|
5530
|
+
for (const [id, t] of Object.entries(data.tasks)) {
|
|
5531
|
+
if (t.label === channel.taskLabel) {
|
|
5532
|
+
return {
|
|
5533
|
+
id: Number(id),
|
|
5534
|
+
label: t.label,
|
|
5535
|
+
status: statusName(t.status),
|
|
5536
|
+
command: t.command,
|
|
5537
|
+
cwd: t.path
|
|
5538
|
+
};
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
return null;
|
|
5542
|
+
}
|
|
5543
|
+
function startTask(opts = {}) {
|
|
5544
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
5545
|
+
const cwd = opts.cwd ?? ch.sessionDir;
|
|
5546
|
+
let existing = findTask(ch);
|
|
5547
|
+
while (existing) {
|
|
5548
|
+
if (existing.status === "Running" || existing.status === "Queued") {
|
|
5549
|
+
spawnSync2("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
5550
|
+
spawnSync2("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
5551
|
+
for (let i = 0; i < 10; i++) {
|
|
5552
|
+
const check = findTask(ch);
|
|
5553
|
+
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
5554
|
+
spawnSync2("sleep", ["0.5"], { encoding: "utf-8" });
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
spawnSync2("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
5558
|
+
existing = findTask(ch);
|
|
5559
|
+
}
|
|
5560
|
+
const runScript = join8(cwd, "run-claude.sh");
|
|
5561
|
+
const args2 = [
|
|
5562
|
+
"add",
|
|
5563
|
+
"--label",
|
|
5564
|
+
ch.taskLabel,
|
|
5565
|
+
"--working-directory",
|
|
5566
|
+
cwd,
|
|
5567
|
+
"--",
|
|
5568
|
+
"bash",
|
|
5569
|
+
runScript
|
|
5570
|
+
];
|
|
5571
|
+
const r = spawnSync2("pueue", args2, { encoding: "utf-8" });
|
|
5572
|
+
if (r.status !== 0) {
|
|
5573
|
+
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
5574
|
+
}
|
|
5575
|
+
const created = findTask(ch);
|
|
5576
|
+
if (!created) {
|
|
5577
|
+
throw new PueueError(`pueue add succeeded but no task with label ${ch.taskLabel} found`);
|
|
5578
|
+
}
|
|
5579
|
+
return created;
|
|
5580
|
+
}
|
|
5581
|
+
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
5582
|
+
spawnSync2("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
5583
|
+
let t = findTask(channel);
|
|
5584
|
+
while (t) {
|
|
5585
|
+
if (t.status === "Running" || t.status === "Queued") {
|
|
5586
|
+
spawnSync2("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
5587
|
+
for (let i = 0; i < 10; i++) {
|
|
5588
|
+
const check = findTask(channel);
|
|
5589
|
+
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
5590
|
+
spawnSync2("sleep", ["0.5"], { encoding: "utf-8" });
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
spawnSync2("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
5594
|
+
t = findTask(channel);
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
5598
|
+
const t = findTask(channel);
|
|
5599
|
+
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
5600
|
+
const r = spawnSync2("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
5601
|
+
return r.stdout || r.stderr || "(no output)";
|
|
5602
|
+
}
|
|
5603
|
+
function ensureRunning(opts = {}) {
|
|
5604
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
5605
|
+
const t = findTask(ch);
|
|
5606
|
+
if (t && t.status === "Running") return t;
|
|
5607
|
+
return startTask(opts);
|
|
5608
|
+
}
|
|
5609
|
+
function probePort(host, port, timeoutMs = 500) {
|
|
5610
|
+
return new Promise((resolve3) => {
|
|
5611
|
+
const sock = connect(port, host);
|
|
5612
|
+
const done = (ok) => {
|
|
5613
|
+
try {
|
|
5614
|
+
sock.destroy();
|
|
5615
|
+
} catch {
|
|
5616
|
+
}
|
|
5617
|
+
resolve3(ok);
|
|
5618
|
+
};
|
|
5619
|
+
sock.once("connect", () => done(true));
|
|
5620
|
+
sock.once("error", () => done(false));
|
|
5621
|
+
sock.setTimeout(timeoutMs, () => done(false));
|
|
5622
|
+
});
|
|
5623
|
+
}
|
|
5624
|
+
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
5625
|
+
spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
5626
|
+
spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
5627
|
+
}
|
|
5628
|
+
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
5629
|
+
const deadline = Date.now() + timeoutMs;
|
|
5630
|
+
while (Date.now() < deadline) {
|
|
5631
|
+
if (await probePort(host, port)) return true;
|
|
5632
|
+
tmuxDismissPrompts(tmuxSession);
|
|
5633
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
5634
|
+
}
|
|
5635
|
+
return probePort(host, port);
|
|
5636
|
+
}
|
|
5637
|
+
function brewInstall(pkg) {
|
|
5638
|
+
const brew = spawnSync2("brew", ["--version"], { encoding: "utf-8" });
|
|
5639
|
+
if (brew.status !== 0) return false;
|
|
5640
|
+
console.log(` Installing ${pkg} via brew...`);
|
|
5641
|
+
const r = spawnSync2("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
5642
|
+
return r.status === 0;
|
|
5643
|
+
}
|
|
5644
|
+
function assertPueueInstalled() {
|
|
5645
|
+
let r = spawnSync2("pueue", ["--version"], { encoding: "utf-8" });
|
|
5646
|
+
if (r.status !== 0) {
|
|
5647
|
+
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
5648
|
+
r = spawnSync2("pueue", ["--version"], { encoding: "utf-8" });
|
|
5649
|
+
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
5650
|
+
} else {
|
|
5651
|
+
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
const status = spawnSync2("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
5655
|
+
if (status.status !== 0) {
|
|
5656
|
+
console.log(" Starting pueued daemon...");
|
|
5657
|
+
const child = spawn("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
5658
|
+
child.unref();
|
|
5659
|
+
spawnSync2("sleep", ["1"]);
|
|
5660
|
+
const retry = spawnSync2("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
5661
|
+
if (retry.status !== 0) {
|
|
5662
|
+
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
spawnSync2("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
5666
|
+
}
|
|
5667
|
+
function assertClaudeInstalled() {
|
|
5668
|
+
const r = spawnSync2("claude", ["--version"], { encoding: "utf-8" });
|
|
5669
|
+
if (r.status !== 0) {
|
|
5670
|
+
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
function assertTmuxInstalled() {
|
|
5674
|
+
let r = spawnSync2("tmux", ["-V"], { encoding: "utf-8" });
|
|
5675
|
+
if (r.status !== 0) {
|
|
5676
|
+
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
5677
|
+
r = spawnSync2("tmux", ["-V"], { encoding: "utf-8" });
|
|
5678
|
+
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
5679
|
+
} else {
|
|
5680
|
+
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, TASK_LABEL_2, TMUX_SESSION_2, SESSION_DIR_22, PueueError, CHANNEL_PRIMARY, CHANNEL_SECONDARY;
|
|
5685
|
+
var init_pueue = __esm({
|
|
5686
|
+
"cli/local-cc/pueue.ts"() {
|
|
5687
|
+
"use strict";
|
|
5688
|
+
TASK_LABEL = "synkro-local-cc";
|
|
5689
|
+
TMUX_SESSION = "synkro-local-cc";
|
|
5690
|
+
SESSION_DIR2 = join8(homedir8(), ".synkro", "cc_sessions");
|
|
5691
|
+
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
5692
|
+
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
5693
|
+
SESSION_DIR_22 = join8(homedir8(), ".synkro", "cc_sessions_2");
|
|
5694
|
+
PueueError = class extends Error {
|
|
5695
|
+
constructor(message, cause) {
|
|
5696
|
+
super(message);
|
|
5697
|
+
this.cause = cause;
|
|
5698
|
+
this.name = "PueueError";
|
|
5699
|
+
}
|
|
5700
|
+
cause;
|
|
5701
|
+
};
|
|
5702
|
+
CHANNEL_PRIMARY = { taskLabel: TASK_LABEL, tmuxSession: TMUX_SESSION, sessionDir: SESSION_DIR2 };
|
|
5703
|
+
CHANNEL_SECONDARY = { taskLabel: TASK_LABEL_2, tmuxSession: TMUX_SESSION_2, sessionDir: SESSION_DIR_22 };
|
|
5704
|
+
}
|
|
5705
|
+
});
|
|
5706
|
+
|
|
5707
|
+
// cli/local-cc/prompts.ts
|
|
5708
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5709
|
+
import { homedir as homedir9 } from "os";
|
|
5710
|
+
import { join as join9 } from "path";
|
|
5711
|
+
async function fetchPrimers() {
|
|
5712
|
+
let jwt2 = "";
|
|
5713
|
+
let gatewayUrl = "";
|
|
5714
|
+
try {
|
|
5715
|
+
const creds = JSON.parse(readFileSync8(CREDS_PATH, "utf-8"));
|
|
5716
|
+
jwt2 = creds.access_token || "";
|
|
5717
|
+
gatewayUrl = creds.gateway_url || "https://api.synkro.sh";
|
|
5718
|
+
} catch {
|
|
5719
|
+
throw new Error("No credentials found. Run `synkro install` first.");
|
|
5720
|
+
}
|
|
5721
|
+
if (!jwt2) throw new Error("No access token. Run `synkro install` first.");
|
|
5722
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/judge-prompts`, {
|
|
5723
|
+
headers: { Authorization: `Bearer ${jwt2}` },
|
|
5724
|
+
signal: AbortSignal.timeout(5e3)
|
|
5725
|
+
});
|
|
5726
|
+
if (!resp.ok) throw new Error(`Failed to fetch prompts: ${resp.status}`);
|
|
5727
|
+
return resp.json();
|
|
5728
|
+
}
|
|
5729
|
+
async function getPrimer(role) {
|
|
5730
|
+
const prompts = await fetchPrimers();
|
|
5731
|
+
const primer = role === "grade-edit" ? prompts.grader_primer_edit : role === "grade-plan" ? prompts.grader_primer_plan : role === "grade-cwe" ? prompts.grader_primer_cwe : prompts.grader_primer_bash;
|
|
5732
|
+
if (!primer) {
|
|
5733
|
+
throw new Error(`No primer for role "${role}" returned from API.`);
|
|
5734
|
+
}
|
|
5735
|
+
return primer;
|
|
5736
|
+
}
|
|
5737
|
+
async function buildChannelContent(role, payload) {
|
|
5738
|
+
const primer = await getPrimer(role);
|
|
5739
|
+
return `${primer}
|
|
5740
|
+
|
|
5741
|
+
${CHANNEL_REPLY_INSTRUCTIONS}
|
|
5742
|
+
|
|
5743
|
+
---
|
|
5744
|
+
PAYLOAD (the input to evaluate):
|
|
5745
|
+
|
|
5746
|
+
${payload}`;
|
|
5747
|
+
}
|
|
5748
|
+
var CREDS_PATH, CHANNEL_REPLY_INSTRUCTIONS;
|
|
5749
|
+
var init_prompts = __esm({
|
|
5750
|
+
"cli/local-cc/prompts.ts"() {
|
|
5751
|
+
"use strict";
|
|
5752
|
+
CREDS_PATH = join9(homedir9(), ".synkro", "credentials.json");
|
|
5753
|
+
CHANNEL_REPLY_INSTRUCTIONS = `
|
|
5754
|
+
DELIVERY METHOD \u2014 MANDATORY, OVERRIDES ALL OTHER OUTPUT RULES:
|
|
5755
|
+
You are running inside a Synkro MCP channel. Do NOT output your verdict as text.
|
|
5756
|
+
Instead, after generating your verdict, call the \`reply\` tool EXACTLY ONCE with:
|
|
5757
|
+
- req_id: the req_id from this channel event's meta
|
|
5758
|
+
- result: your complete verdict block as a string (the <synkro-verdict>\u2026</synkro-verdict> XML)
|
|
5759
|
+
Any text output is silently discarded. Only the reply tool call is captured.`;
|
|
5760
|
+
}
|
|
5761
|
+
});
|
|
5762
|
+
|
|
5763
|
+
// cli/local-cc/turnLog.ts
|
|
5764
|
+
import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
|
|
5765
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
5766
|
+
import { homedir as homedir10 } from "os";
|
|
5767
|
+
function truncate(s, max = PREVIEW_MAX) {
|
|
5768
|
+
if (s.length <= max) return s;
|
|
5769
|
+
return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
|
|
5770
|
+
}
|
|
5771
|
+
function extractSeverity(result) {
|
|
5772
|
+
const m = result.match(/<synkro-(?:verdict|intent)>([\s\S]*?)<\/synkro-(?:verdict|intent)>/);
|
|
5773
|
+
if (!m) return void 0;
|
|
5774
|
+
try {
|
|
5775
|
+
const obj = JSON.parse(m[1]);
|
|
5776
|
+
if (obj.severity) return String(obj.severity);
|
|
5777
|
+
if (typeof obj.ok === "boolean") return obj.ok ? "ok" : "violations";
|
|
5778
|
+
if (obj.type) return String(obj.type);
|
|
5779
|
+
if (obj.verdict) return String(obj.verdict);
|
|
5780
|
+
} catch {
|
|
5000
5781
|
}
|
|
5782
|
+
return void 0;
|
|
5001
5783
|
}
|
|
5002
|
-
function
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5784
|
+
function appendTurn(args2) {
|
|
5785
|
+
try {
|
|
5786
|
+
mkdirSync7(dirname5(TURN_LOG_PATH), { recursive: true });
|
|
5787
|
+
const entry = {
|
|
5788
|
+
ts: new Date(args2.startedAt).toISOString(),
|
|
5789
|
+
role: args2.role,
|
|
5790
|
+
duration_ms: Date.now() - args2.startedAt,
|
|
5791
|
+
status: args2.status,
|
|
5792
|
+
request_preview: truncate(args2.request),
|
|
5793
|
+
response_preview: args2.result ? truncate(args2.result) : "",
|
|
5794
|
+
severity: args2.result ? extractSeverity(args2.result) : void 0,
|
|
5795
|
+
error: args2.error
|
|
5796
|
+
};
|
|
5797
|
+
appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
|
|
5798
|
+
} catch {
|
|
5013
5799
|
}
|
|
5014
|
-
return "unknown";
|
|
5015
5800
|
}
|
|
5016
|
-
function
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5801
|
+
function readRecentTurns(n = 20) {
|
|
5802
|
+
if (!existsSync9(TURN_LOG_PATH)) return [];
|
|
5803
|
+
try {
|
|
5804
|
+
const size = statSync(TURN_LOG_PATH).size;
|
|
5805
|
+
if (size === 0) return [];
|
|
5806
|
+
const text = readFileSync9(TURN_LOG_PATH, "utf-8");
|
|
5807
|
+
const lines = text.split("\n").filter(Boolean);
|
|
5808
|
+
const lastN = lines.slice(-n).reverse();
|
|
5809
|
+
return lastN.map((line) => {
|
|
5810
|
+
try {
|
|
5811
|
+
return JSON.parse(line);
|
|
5812
|
+
} catch {
|
|
5813
|
+
return null;
|
|
5814
|
+
}
|
|
5815
|
+
}).filter((x) => x !== null);
|
|
5816
|
+
} catch {
|
|
5817
|
+
return [];
|
|
5028
5818
|
}
|
|
5029
|
-
return null;
|
|
5030
5819
|
}
|
|
5031
|
-
function
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
if (existing.status === "Running" || existing.status === "Queued") {
|
|
5037
|
-
spawnSync2("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
5038
|
-
spawnSync2("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
5039
|
-
for (let i = 0; i < 10; i++) {
|
|
5040
|
-
const check = findTask(ch);
|
|
5041
|
-
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
5042
|
-
spawnSync2("sleep", ["0.5"], { encoding: "utf-8" });
|
|
5043
|
-
}
|
|
5820
|
+
function followTurns(onEntry) {
|
|
5821
|
+
try {
|
|
5822
|
+
mkdirSync7(dirname5(TURN_LOG_PATH), { recursive: true });
|
|
5823
|
+
if (!existsSync9(TURN_LOG_PATH)) {
|
|
5824
|
+
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
5044
5825
|
}
|
|
5045
|
-
|
|
5046
|
-
existing = findTask(ch);
|
|
5047
|
-
}
|
|
5048
|
-
const runScript = join8(cwd, "run-claude.sh");
|
|
5049
|
-
const args2 = [
|
|
5050
|
-
"add",
|
|
5051
|
-
"--label",
|
|
5052
|
-
ch.taskLabel,
|
|
5053
|
-
"--working-directory",
|
|
5054
|
-
cwd,
|
|
5055
|
-
"--",
|
|
5056
|
-
"bash",
|
|
5057
|
-
runScript
|
|
5058
|
-
];
|
|
5059
|
-
const r = spawnSync2("pueue", args2, { encoding: "utf-8" });
|
|
5060
|
-
if (r.status !== 0) {
|
|
5061
|
-
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
5062
|
-
}
|
|
5063
|
-
const created = findTask(ch);
|
|
5064
|
-
if (!created) {
|
|
5065
|
-
throw new PueueError(`pueue add succeeded but no task with label ${ch.taskLabel} found`);
|
|
5826
|
+
} catch {
|
|
5066
5827
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5828
|
+
let lastSize = (() => {
|
|
5829
|
+
try {
|
|
5830
|
+
return statSync(TURN_LOG_PATH).size;
|
|
5831
|
+
} catch {
|
|
5832
|
+
return 0;
|
|
5833
|
+
}
|
|
5834
|
+
})();
|
|
5835
|
+
let pendingPartial = "";
|
|
5836
|
+
const drainNewBytes = (from, to) => {
|
|
5837
|
+
if (to <= from) return;
|
|
5838
|
+
let fd = null;
|
|
5839
|
+
try {
|
|
5840
|
+
fd = openSync2(TURN_LOG_PATH, "r");
|
|
5841
|
+
const len = to - from;
|
|
5842
|
+
const buf = Buffer.alloc(len);
|
|
5843
|
+
readSync(fd, buf, 0, len, from);
|
|
5844
|
+
const text = pendingPartial + buf.toString("utf-8");
|
|
5845
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
5846
|
+
if (lastNewline === -1) {
|
|
5847
|
+
pendingPartial = text;
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
const complete = text.slice(0, lastNewline);
|
|
5851
|
+
pendingPartial = text.slice(lastNewline + 1);
|
|
5852
|
+
for (const line of complete.split("\n")) {
|
|
5853
|
+
if (!line) continue;
|
|
5854
|
+
try {
|
|
5855
|
+
onEntry(JSON.parse(line));
|
|
5856
|
+
} catch {
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
} catch {
|
|
5860
|
+
} finally {
|
|
5861
|
+
if (fd !== null) {
|
|
5862
|
+
try {
|
|
5863
|
+
closeSync2(fd);
|
|
5864
|
+
} catch {
|
|
5865
|
+
}
|
|
5079
5866
|
}
|
|
5080
5867
|
}
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5868
|
+
};
|
|
5869
|
+
watchFile(TURN_LOG_PATH, { interval: 250 }, (curr, prev) => {
|
|
5870
|
+
if (curr.size < lastSize) {
|
|
5871
|
+
lastSize = 0;
|
|
5872
|
+
pendingPartial = "";
|
|
5873
|
+
}
|
|
5874
|
+
if (curr.size > lastSize) {
|
|
5875
|
+
drainNewBytes(lastSize, curr.size);
|
|
5876
|
+
lastSize = curr.size;
|
|
5877
|
+
}
|
|
5878
|
+
});
|
|
5879
|
+
return () => unwatchFile(TURN_LOG_PATH);
|
|
5090
5880
|
}
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5881
|
+
var TURN_LOG_PATH, PREVIEW_MAX;
|
|
5882
|
+
var init_turnLog = __esm({
|
|
5883
|
+
"cli/local-cc/turnLog.ts"() {
|
|
5884
|
+
"use strict";
|
|
5885
|
+
TURN_LOG_PATH = join10(homedir10(), ".synkro", "cc_sessions", "turns.log");
|
|
5886
|
+
PREVIEW_MAX = 400;
|
|
5887
|
+
}
|
|
5888
|
+
});
|
|
5889
|
+
|
|
5890
|
+
// cli/local-cc/client.ts
|
|
5891
|
+
import { request as httpRequest } from "http";
|
|
5892
|
+
import { connect as connect2 } from "net";
|
|
5893
|
+
async function submitToChannel(role, payload, opts = {}) {
|
|
5894
|
+
const content = await buildChannelContent(role, payload);
|
|
5895
|
+
const body = JSON.stringify({ role, content });
|
|
5896
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5897
|
+
const port = opts.port ?? CHANNEL_PORT;
|
|
5898
|
+
const startedAt = Date.now();
|
|
5899
|
+
try {
|
|
5900
|
+
const result = await new Promise((resolve3, reject) => {
|
|
5901
|
+
const req = httpRequest({
|
|
5902
|
+
host: CHANNEL_HOST,
|
|
5903
|
+
port,
|
|
5904
|
+
method: "POST",
|
|
5905
|
+
path: "/submit",
|
|
5906
|
+
headers: {
|
|
5907
|
+
"Content-Type": "application/json",
|
|
5908
|
+
"Content-Length": Buffer.byteLength(body)
|
|
5909
|
+
},
|
|
5910
|
+
timeout: timeoutMs
|
|
5911
|
+
}, (res) => {
|
|
5912
|
+
const chunks = [];
|
|
5913
|
+
res.on("data", (c) => chunks.push(c));
|
|
5914
|
+
res.on("end", () => {
|
|
5915
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
5916
|
+
if (res.statusCode !== 200) {
|
|
5917
|
+
reject(new LocalCCError(`channel returned ${res.statusCode}: ${text.slice(0, 500)}`));
|
|
5918
|
+
return;
|
|
5919
|
+
}
|
|
5920
|
+
try {
|
|
5921
|
+
const parsed = JSON.parse(text);
|
|
5922
|
+
if (parsed.error) {
|
|
5923
|
+
reject(new LocalCCError(parsed.error));
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
resolve3(String(parsed.result ?? ""));
|
|
5927
|
+
} catch (err) {
|
|
5928
|
+
reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
|
|
5929
|
+
}
|
|
5930
|
+
});
|
|
5931
|
+
});
|
|
5932
|
+
req.on("timeout", () => {
|
|
5933
|
+
req.destroy(new LocalCCError(`channel request timed out after ${timeoutMs}ms`));
|
|
5934
|
+
});
|
|
5935
|
+
req.on("error", (err) => {
|
|
5936
|
+
const msg = err.code === "ECONNREFUSED" ? `channel connection refused at ${CHANNEL_HOST}:${CHANNEL_PORT} (is the pueue task running?)` : `channel request failed: ${err.message}`;
|
|
5937
|
+
reject(new LocalCCError(msg, err));
|
|
5938
|
+
});
|
|
5939
|
+
req.write(body);
|
|
5940
|
+
req.end();
|
|
5941
|
+
});
|
|
5942
|
+
appendTurn({ startedAt, role, request: payload, result, status: "ok" });
|
|
5943
|
+
return result;
|
|
5944
|
+
} catch (err) {
|
|
5945
|
+
const message = err.message ?? String(err);
|
|
5946
|
+
const status = /timed out/i.test(message) ? "timeout" : "error";
|
|
5947
|
+
appendTurn({ startedAt, role, request: payload, status, error: message });
|
|
5948
|
+
throw err;
|
|
5949
|
+
}
|
|
5096
5950
|
}
|
|
5097
|
-
function
|
|
5098
|
-
return new Promise((
|
|
5099
|
-
const sock =
|
|
5951
|
+
function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
|
|
5952
|
+
return new Promise((resolve3) => {
|
|
5953
|
+
const sock = connect2(port, CHANNEL_HOST);
|
|
5100
5954
|
const done = (ok) => {
|
|
5101
5955
|
try {
|
|
5102
5956
|
sock.destroy();
|
|
5103
5957
|
} catch {
|
|
5104
5958
|
}
|
|
5105
|
-
|
|
5959
|
+
resolve3(ok);
|
|
5106
5960
|
};
|
|
5107
5961
|
sock.once("connect", () => done(true));
|
|
5108
5962
|
sock.once("error", () => done(false));
|
|
5109
5963
|
sock.setTimeout(timeoutMs, () => done(false));
|
|
5110
5964
|
});
|
|
5111
5965
|
}
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
}
|
|
5116
|
-
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
5117
|
-
const deadline = Date.now() + timeoutMs;
|
|
5118
|
-
while (Date.now() < deadline) {
|
|
5119
|
-
if (await probePort(host, port)) return true;
|
|
5120
|
-
tmuxDismissPrompts(tmuxSession);
|
|
5121
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
5122
|
-
}
|
|
5123
|
-
return probePort(host, port);
|
|
5124
|
-
}
|
|
5125
|
-
function brewInstall(pkg) {
|
|
5126
|
-
const brew = spawnSync2("brew", ["--version"], { encoding: "utf-8" });
|
|
5127
|
-
if (brew.status !== 0) return false;
|
|
5128
|
-
console.log(` Installing ${pkg} via brew...`);
|
|
5129
|
-
const r = spawnSync2("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
5130
|
-
return r.status === 0;
|
|
5131
|
-
}
|
|
5132
|
-
function assertPueueInstalled() {
|
|
5133
|
-
let r = spawnSync2("pueue", ["--version"], { encoding: "utf-8" });
|
|
5134
|
-
if (r.status !== 0) {
|
|
5135
|
-
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
5136
|
-
r = spawnSync2("pueue", ["--version"], { encoding: "utf-8" });
|
|
5137
|
-
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
5138
|
-
} else {
|
|
5139
|
-
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
5140
|
-
}
|
|
5141
|
-
}
|
|
5142
|
-
const status = spawnSync2("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
5143
|
-
if (status.status !== 0) {
|
|
5144
|
-
console.log(" Starting pueued daemon...");
|
|
5145
|
-
const child = spawn("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
5146
|
-
child.unref();
|
|
5147
|
-
spawnSync2("sleep", ["1"]);
|
|
5148
|
-
const retry = spawnSync2("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
5149
|
-
if (retry.status !== 0) {
|
|
5150
|
-
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
5151
|
-
}
|
|
5152
|
-
}
|
|
5153
|
-
spawnSync2("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
5154
|
-
}
|
|
5155
|
-
function assertClaudeInstalled() {
|
|
5156
|
-
const r = spawnSync2("claude", ["--version"], { encoding: "utf-8" });
|
|
5157
|
-
if (r.status !== 0) {
|
|
5158
|
-
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
5159
|
-
}
|
|
5160
|
-
}
|
|
5161
|
-
function assertTmuxInstalled() {
|
|
5162
|
-
let r = spawnSync2("tmux", ["-V"], { encoding: "utf-8" });
|
|
5163
|
-
if (r.status !== 0) {
|
|
5164
|
-
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
5165
|
-
r = spawnSync2("tmux", ["-V"], { encoding: "utf-8" });
|
|
5166
|
-
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
5167
|
-
} else {
|
|
5168
|
-
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
5169
|
-
}
|
|
5170
|
-
}
|
|
5171
|
-
}
|
|
5172
|
-
var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, TASK_LABEL_2, TMUX_SESSION_2, SESSION_DIR_22, PueueError, CHANNEL_PRIMARY, CHANNEL_SECONDARY;
|
|
5173
|
-
var init_pueue = __esm({
|
|
5174
|
-
"cli/local-cc/pueue.ts"() {
|
|
5966
|
+
var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
|
|
5967
|
+
var init_client = __esm({
|
|
5968
|
+
"cli/local-cc/client.ts"() {
|
|
5175
5969
|
"use strict";
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
PueueError = class extends Error {
|
|
5970
|
+
init_prompts();
|
|
5971
|
+
init_turnLog();
|
|
5972
|
+
CHANNEL_HOST = "127.0.0.1";
|
|
5973
|
+
CHANNEL_PORT = parseInt(process.env.SYNKRO_CHANNEL_PORT || "8929", 10);
|
|
5974
|
+
DEFAULT_TIMEOUT_MS = 9e4;
|
|
5975
|
+
LocalCCError = class extends Error {
|
|
5183
5976
|
constructor(message, cause) {
|
|
5184
5977
|
super(message);
|
|
5185
5978
|
this.cause = cause;
|
|
5186
|
-
this.name = "
|
|
5979
|
+
this.name = "LocalCCError";
|
|
5187
5980
|
}
|
|
5188
5981
|
cause;
|
|
5189
5982
|
};
|
|
5190
|
-
CHANNEL_PRIMARY = { taskLabel: TASK_LABEL, tmuxSession: TMUX_SESSION, sessionDir: SESSION_DIR2 };
|
|
5191
|
-
CHANNEL_SECONDARY = { taskLabel: TASK_LABEL_2, tmuxSession: TMUX_SESSION_2, sessionDir: SESSION_DIR_22 };
|
|
5192
5983
|
}
|
|
5193
5984
|
});
|
|
5194
5985
|
|
|
5195
|
-
// cli/
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5986
|
+
// cli/commands/install.ts
|
|
5987
|
+
var install_exports = {};
|
|
5988
|
+
__export(install_exports, {
|
|
5989
|
+
installCommand: () => installCommand,
|
|
5990
|
+
parseArgs: () => parseArgs
|
|
5991
|
+
});
|
|
5992
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync, appendFileSync as appendFileSync2, renameSync as renameSync5 } from "fs";
|
|
5993
|
+
import { homedir as homedir11 } from "os";
|
|
5994
|
+
import { join as join11 } from "path";
|
|
5995
|
+
import { execSync as execSync5, spawnSync as spawnSync3, spawn as spawn2 } from "child_process";
|
|
5996
|
+
import { createInterface as createInterface3 } from "readline";
|
|
5997
|
+
function sanitizeGatewayCandidate(raw) {
|
|
5998
|
+
if (!raw) return void 0;
|
|
5999
|
+
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
6000
|
+
}
|
|
6001
|
+
function parseArgs(argv) {
|
|
6002
|
+
const opts = {};
|
|
6003
|
+
for (const a of argv) {
|
|
6004
|
+
if (a.startsWith("--api-key=")) opts.apiKey = a.slice("--api-key=".length);
|
|
6005
|
+
else if (a.startsWith("--gateway=")) opts.gatewayUrl = a.slice("--gateway=".length);
|
|
6006
|
+
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
6007
|
+
else if (a === "--no-mcp") opts.noMcp = true;
|
|
6008
|
+
else if (a === "--force" || a === "-f") opts.force = true;
|
|
6009
|
+
else if (a === "--link-repo") opts.linkRepo = true;
|
|
5208
6010
|
}
|
|
5209
|
-
if (!
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
6011
|
+
if (!opts.gatewayUrl) {
|
|
6012
|
+
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
6013
|
+
if (fromEnv) opts.gatewayUrl = fromEnv;
|
|
6014
|
+
}
|
|
6015
|
+
return opts;
|
|
6016
|
+
}
|
|
6017
|
+
async function promptTranscriptConsent() {
|
|
6018
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
6019
|
+
return new Promise((resolve3) => {
|
|
6020
|
+
rl.question(
|
|
6021
|
+
"Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
|
|
6022
|
+
(answer) => {
|
|
6023
|
+
rl.close();
|
|
6024
|
+
const trimmed = answer.trim().toLowerCase();
|
|
6025
|
+
resolve3(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
6026
|
+
}
|
|
6027
|
+
);
|
|
5213
6028
|
});
|
|
5214
|
-
if (!resp.ok) throw new Error(`Failed to fetch prompts: ${resp.status}`);
|
|
5215
|
-
return resp.json();
|
|
5216
6029
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
}
|
|
5223
|
-
return primer;
|
|
6030
|
+
function ensureSynkroDir() {
|
|
6031
|
+
mkdirSync8(SYNKRO_DIR2, { recursive: true });
|
|
6032
|
+
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
6033
|
+
mkdirSync8(BIN_DIR, { recursive: true });
|
|
6034
|
+
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
5224
6035
|
}
|
|
5225
|
-
|
|
5226
|
-
const
|
|
5227
|
-
|
|
6036
|
+
function writeHookScripts() {
|
|
6037
|
+
const bashScriptPath = join11(HOOKS_DIR, "cc-bash-judge.ts");
|
|
6038
|
+
const bashFollowupScriptPath = join11(HOOKS_DIR, "cc-bash-followup.ts");
|
|
6039
|
+
const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
6040
|
+
const cwePrecheckScriptPath = join11(HOOKS_DIR, "cc-cwe-precheck.ts");
|
|
6041
|
+
const cvePrecheckScriptPath = join11(HOOKS_DIR, "cc-cve-precheck.ts");
|
|
6042
|
+
const planJudgeScriptPath = join11(HOOKS_DIR, "cc-plan-judge.ts");
|
|
6043
|
+
const agentJudgeScriptPath = join11(HOOKS_DIR, "cc-agent-judge.ts");
|
|
6044
|
+
const stopSummaryScriptPath = join11(HOOKS_DIR, "cc-stop-summary.ts");
|
|
6045
|
+
const sessionStartScriptPath = join11(HOOKS_DIR, "cc-session-start.ts");
|
|
6046
|
+
const transcriptSyncScriptPath = join11(HOOKS_DIR, "cc-transcript-sync.ts");
|
|
6047
|
+
const userPromptSubmitScriptPath = join11(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
6048
|
+
const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
|
|
6049
|
+
const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
|
|
6050
|
+
const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
6051
|
+
const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.ts");
|
|
6052
|
+
const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
6053
|
+
const cursorBashFollowupPath = join11(HOOKS_DIR, "cursor-bash-followup.ts");
|
|
6054
|
+
const cursorSessionStartPath = join11(HOOKS_DIR, "cursor-session-start.ts");
|
|
6055
|
+
const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
|
|
6056
|
+
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
6057
|
+
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
6058
|
+
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
6059
|
+
writeFileSync7(cwePrecheckScriptPath, CWE_PRECHECK_TS, "utf-8");
|
|
6060
|
+
writeFileSync7(cvePrecheckScriptPath, CVE_PRECHECK_TS, "utf-8");
|
|
6061
|
+
writeFileSync7(planJudgeScriptPath, PLAN_JUDGE_TS, "utf-8");
|
|
6062
|
+
writeFileSync7(agentJudgeScriptPath, AGENT_JUDGE_TS, "utf-8");
|
|
6063
|
+
writeFileSync7(stopSummaryScriptPath, STOP_SUMMARY_TS, "utf-8");
|
|
6064
|
+
writeFileSync7(sessionStartScriptPath, SESSION_START_TS, "utf-8");
|
|
6065
|
+
writeFileSync7(transcriptSyncScriptPath, TRANSCRIPT_SYNC_TS, "utf-8");
|
|
6066
|
+
writeFileSync7(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
|
|
6067
|
+
writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
6068
|
+
writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
6069
|
+
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
6070
|
+
writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_TS, "utf-8");
|
|
6071
|
+
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
6072
|
+
writeFileSync7(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_TS, "utf-8");
|
|
6073
|
+
writeFileSync7(cursorSessionStartPath, CURSOR_SESSION_START_TS, "utf-8");
|
|
6074
|
+
writeFileSync7(mcpLocalServerPath, `#!/usr/bin/env bun
|
|
6075
|
+
/**
|
|
6076
|
+
* Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
|
|
6077
|
+
* JSON-RPC 2.0 over HTTP, same protocol as the cloud MCP server.
|
|
6078
|
+
* No auth (localhost only), no embedding API, no Inngest.
|
|
6079
|
+
*/
|
|
6080
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
6081
|
+
import { homedir } from 'node:os';
|
|
6082
|
+
import { join } from 'node:path';
|
|
5228
6083
|
|
|
5229
|
-
|
|
6084
|
+
import { randomBytes } from 'node:crypto';
|
|
5230
6085
|
|
|
5231
|
-
|
|
5232
|
-
|
|
6086
|
+
const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '8931', 10);
|
|
6087
|
+
const HOME = homedir();
|
|
6088
|
+
const RULES_PATH = join(HOME, '.synkro', 'rules.json');
|
|
6089
|
+
const TELEMETRY_PATH = join(HOME, '.synkro', 'telemetry.jsonl');
|
|
6090
|
+
const TOKEN_PATH = join(HOME, '.synkro', '.mcp-local-token');
|
|
5233
6091
|
|
|
5234
|
-
|
|
6092
|
+
// File-based shared secret \u2014 generated once, required on all POST requests.
|
|
6093
|
+
function getOrCreateToken(): string {
|
|
6094
|
+
try {
|
|
6095
|
+
if (existsSync(TOKEN_PATH)) return readFileSync(TOKEN_PATH, 'utf-8').trim();
|
|
6096
|
+
} catch {}
|
|
6097
|
+
const token = randomBytes(32).toString('hex');
|
|
6098
|
+
mkdirSync(join(HOME, '.synkro'), { recursive: true });
|
|
6099
|
+
writeFileSync(TOKEN_PATH, token + '\\n', { mode: 0o600 });
|
|
6100
|
+
return token;
|
|
5235
6101
|
}
|
|
5236
|
-
var CREDS_PATH, CHANNEL_REPLY_INSTRUCTIONS;
|
|
5237
|
-
var init_prompts = __esm({
|
|
5238
|
-
"cli/local-cc/prompts.ts"() {
|
|
5239
|
-
"use strict";
|
|
5240
|
-
CREDS_PATH = join9(homedir8(), ".synkro", "credentials.json");
|
|
5241
|
-
CHANNEL_REPLY_INSTRUCTIONS = `
|
|
5242
|
-
DELIVERY METHOD \u2014 MANDATORY, OVERRIDES ALL OTHER OUTPUT RULES:
|
|
5243
|
-
You are running inside a Synkro MCP channel. Do NOT output your verdict as text.
|
|
5244
|
-
Instead, after generating your verdict, call the \`reply\` tool EXACTLY ONCE with:
|
|
5245
|
-
- req_id: the req_id from this channel event's meta
|
|
5246
|
-
- result: your complete verdict block as a string (the <synkro-verdict>\u2026</synkro-verdict> XML)
|
|
5247
|
-
Any text output is silently discarded. Only the reply tool call is captured.`;
|
|
5248
|
-
}
|
|
5249
|
-
});
|
|
5250
6102
|
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
6103
|
+
const SERVER_TOKEN = getOrCreateToken();
|
|
6104
|
+
|
|
6105
|
+
// \u2500\u2500\u2500 Storage \u2500\u2500\u2500
|
|
6106
|
+
|
|
6107
|
+
interface Rule {
|
|
6108
|
+
rule_id: string;
|
|
6109
|
+
text: string;
|
|
6110
|
+
category: string;
|
|
6111
|
+
severity: string;
|
|
6112
|
+
mode: string;
|
|
6113
|
+
hook_stage: string;
|
|
6114
|
+
scope: string;
|
|
5258
6115
|
}
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
if (obj.verdict) return String(obj.verdict);
|
|
5268
|
-
} catch {
|
|
5269
|
-
}
|
|
5270
|
-
return void 0;
|
|
6116
|
+
|
|
6117
|
+
interface Policy {
|
|
6118
|
+
id: string;
|
|
6119
|
+
name: string;
|
|
6120
|
+
rules: Rule[];
|
|
6121
|
+
ruleCount: number;
|
|
6122
|
+
scopeOwner: string;
|
|
6123
|
+
isActive: boolean;
|
|
5271
6124
|
}
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
6125
|
+
|
|
6126
|
+
interface ScanExemption {
|
|
6127
|
+
path: string;
|
|
6128
|
+
cwe_id: string;
|
|
6129
|
+
reason?: string;
|
|
6130
|
+
}
|
|
6131
|
+
|
|
6132
|
+
interface RulesFile {
|
|
6133
|
+
policies: Policy[];
|
|
6134
|
+
config: { silent: boolean; activePolicyId: string };
|
|
6135
|
+
scanExemptions: ScanExemption[];
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
function readRules(): RulesFile {
|
|
6139
|
+
if (!existsSync(RULES_PATH)) {
|
|
6140
|
+
return {
|
|
6141
|
+
policies: [{
|
|
6142
|
+
id: 'local-policy',
|
|
6143
|
+
name: 'My Rules',
|
|
6144
|
+
rules: [],
|
|
6145
|
+
ruleCount: 0,
|
|
6146
|
+
scopeOwner: 'user',
|
|
6147
|
+
isActive: true,
|
|
6148
|
+
}],
|
|
6149
|
+
config: { silent: false, activePolicyId: 'local-policy' },
|
|
6150
|
+
scanExemptions: [],
|
|
5284
6151
|
};
|
|
5285
|
-
appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
|
|
5286
|
-
} catch {
|
|
5287
6152
|
}
|
|
5288
|
-
}
|
|
5289
|
-
function readRecentTurns(n = 20) {
|
|
5290
|
-
if (!existsSync10(TURN_LOG_PATH)) return [];
|
|
5291
6153
|
try {
|
|
5292
|
-
|
|
5293
|
-
if (size === 0) return [];
|
|
5294
|
-
const text = readFileSync9(TURN_LOG_PATH, "utf-8");
|
|
5295
|
-
const lines = text.split("\n").filter(Boolean);
|
|
5296
|
-
const lastN = lines.slice(-n).reverse();
|
|
5297
|
-
return lastN.map((line) => {
|
|
5298
|
-
try {
|
|
5299
|
-
return JSON.parse(line);
|
|
5300
|
-
} catch {
|
|
5301
|
-
return null;
|
|
5302
|
-
}
|
|
5303
|
-
}).filter((x) => x !== null);
|
|
6154
|
+
return JSON.parse(readFileSync(RULES_PATH, 'utf-8'));
|
|
5304
6155
|
} catch {
|
|
5305
|
-
return
|
|
6156
|
+
return {
|
|
6157
|
+
policies: [{ id: 'local-policy', name: 'My Rules', rules: [], ruleCount: 0, scopeOwner: 'user', isActive: true }],
|
|
6158
|
+
config: { silent: false, activePolicyId: 'local-policy' },
|
|
6159
|
+
scanExemptions: [],
|
|
6160
|
+
};
|
|
5306
6161
|
}
|
|
5307
6162
|
}
|
|
5308
|
-
|
|
6163
|
+
|
|
6164
|
+
function writeRules(data: RulesFile): void {
|
|
6165
|
+
for (const p of data.policies) p.ruleCount = p.rules.length;
|
|
6166
|
+
mkdirSync(join(HOME, '.synkro'), { recursive: true });
|
|
6167
|
+
const tmp = RULES_PATH + '.tmp';
|
|
6168
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\\n', 'utf-8');
|
|
6169
|
+
renameSync(tmp, RULES_PATH);
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
function emitRuleSync(data: RulesFile): void {
|
|
6173
|
+
const active = data.policies.find(p => p.id === data.config.activePolicyId) || data.policies[0];
|
|
6174
|
+
const event = {
|
|
6175
|
+
capture_type: 'rule_sync',
|
|
6176
|
+
policy_id: active?.id || 'local-policy',
|
|
6177
|
+
policy_name: active?.name || 'My Rules',
|
|
6178
|
+
rules: active?.rules || [],
|
|
6179
|
+
rule_count: active?.ruleCount || 0,
|
|
6180
|
+
scan_exemptions: data.scanExemptions,
|
|
6181
|
+
silent: data.config.silent,
|
|
6182
|
+
_ts: new Date().toISOString(),
|
|
6183
|
+
};
|
|
5309
6184
|
try {
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
6185
|
+
appendFileSync(TELEMETRY_PATH, JSON.stringify(event) + '\\n', 'utf-8');
|
|
6186
|
+
} catch {}
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6189
|
+
function genId(): string {
|
|
6190
|
+
return \`r_\${Date.now()}_\${Math.random().toString(36).slice(2, 8)}\`;
|
|
6191
|
+
}
|
|
6192
|
+
|
|
6193
|
+
function getActivePolicy(data: RulesFile): Policy {
|
|
6194
|
+
return data.policies.find(p => p.id === data.config.activePolicyId) || data.policies[0];
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
function findOrCreatePolicy(data: RulesFile, name: string): Policy {
|
|
6198
|
+
const existing = data.policies.find(p => p.name.toLowerCase() === name.toLowerCase());
|
|
6199
|
+
if (existing) return existing;
|
|
6200
|
+
const p: Policy = {
|
|
6201
|
+
id: \`policy_\${Date.now()}_\${Math.random().toString(36).slice(2, 6)}\`,
|
|
6202
|
+
name,
|
|
6203
|
+
rules: [],
|
|
6204
|
+
ruleCount: 0,
|
|
6205
|
+
scopeOwner: 'user',
|
|
6206
|
+
isActive: true,
|
|
6207
|
+
};
|
|
6208
|
+
data.policies.push(p);
|
|
6209
|
+
return p;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
function getAllRules(data: RulesFile): Array<Rule & { policyName: string; policyId: string }> {
|
|
6213
|
+
const all: Array<Rule & { policyName: string; policyId: string }> = [];
|
|
6214
|
+
for (const p of data.policies) {
|
|
6215
|
+
if (!p.isActive) continue;
|
|
6216
|
+
for (const r of p.rules) {
|
|
6217
|
+
all.push({ ...r, policyName: p.name, policyId: p.id });
|
|
5313
6218
|
}
|
|
5314
|
-
} catch {
|
|
5315
6219
|
}
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
6220
|
+
return all;
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
// \u2500\u2500\u2500 Keyword Search \u2500\u2500\u2500
|
|
6224
|
+
|
|
6225
|
+
const STOPWORDS = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'because', 'but', 'and', 'or', 'if', 'while', 'about', 'up', 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'they', 'them', 'what', 'which', 'who', 'whom']);
|
|
6226
|
+
|
|
6227
|
+
function tokenize(text: string): string[] {
|
|
6228
|
+
return text.toLowerCase().replace(/[^a-z0-9_-]/g, ' ').split(/\\s+/).filter(t => t.length > 1 && !STOPWORDS.has(t));
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
function keywordSearch(query: string, rules: Array<Rule & { policyName: string; policyId: string }>, topK: number): any[] {
|
|
6232
|
+
const qTokens = tokenize(query);
|
|
6233
|
+
if (qTokens.length === 0) return rules.slice(0, topK);
|
|
6234
|
+
|
|
6235
|
+
const scored = rules.map(r => {
|
|
6236
|
+
const rTokens = new Set(tokenize(\`\${r.text} \${r.category} \${r.severity}\`));
|
|
6237
|
+
const overlap = qTokens.filter(t => rTokens.has(t) || [...rTokens].some(rt => rt.includes(t) || t.includes(rt))).length;
|
|
6238
|
+
return { rule: r, score: overlap / qTokens.length };
|
|
6239
|
+
});
|
|
6240
|
+
|
|
6241
|
+
scored.sort((a, b) => b.score - a.score);
|
|
6242
|
+
const results = scored.filter(s => s.score > 0).slice(0, topK);
|
|
6243
|
+
if (results.length === 0) return rules.slice(0, topK);
|
|
6244
|
+
|
|
6245
|
+
return results.map(s => ({
|
|
6246
|
+
rule_id: s.rule.rule_id,
|
|
6247
|
+
text: s.rule.text,
|
|
6248
|
+
category: s.rule.category,
|
|
6249
|
+
severity: s.rule.severity,
|
|
6250
|
+
mode: s.rule.mode,
|
|
6251
|
+
hook_stage: s.rule.hook_stage,
|
|
6252
|
+
scope: s.rule.scope,
|
|
6253
|
+
pack_name: s.rule.policyName,
|
|
6254
|
+
score: Math.round(s.score * 100) / 100,
|
|
6255
|
+
}));
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
// \u2500\u2500\u2500 Tool Handlers \u2500\u2500\u2500
|
|
6259
|
+
|
|
6260
|
+
function handleGetGuardrails(args: any): any {
|
|
6261
|
+
const data = readRules();
|
|
6262
|
+
const all = getAllRules(data);
|
|
6263
|
+
const topK = Math.min(args.top_k || 8, 25);
|
|
6264
|
+
let filtered = all;
|
|
6265
|
+
if (args.category) filtered = filtered.filter(r => r.category === args.category);
|
|
6266
|
+
const results = keywordSearch(args.query || '', filtered, topK);
|
|
6267
|
+
return { rules: results, total: results.length, query: args.query };
|
|
6268
|
+
}
|
|
6269
|
+
|
|
6270
|
+
function handleCreateGuardrail(args: any): any {
|
|
6271
|
+
const data = readRules();
|
|
6272
|
+
const policy = args.ruleset ? findOrCreatePolicy(data, args.ruleset) : getActivePolicy(data);
|
|
6273
|
+
const rule: Rule = {
|
|
6274
|
+
rule_id: genId(),
|
|
6275
|
+
text: args.text,
|
|
6276
|
+
category: args.category || 'custom',
|
|
6277
|
+
severity: args.severity || 'medium',
|
|
6278
|
+
mode: args.mode || 'audit',
|
|
6279
|
+
hook_stage: args.hook_stage || 'both',
|
|
6280
|
+
scope: args.scope || 'user',
|
|
6281
|
+
};
|
|
6282
|
+
policy.rules.push(rule);
|
|
6283
|
+
writeRules(data);
|
|
6284
|
+
emitRuleSync(data);
|
|
6285
|
+
return { created: true, rule_id: rule.rule_id, text: rule.text, pack_name: policy.name, total_rules: policy.rules.length };
|
|
6286
|
+
}
|
|
6287
|
+
|
|
6288
|
+
function handleBulkCreateGuardrails(args: any): any {
|
|
6289
|
+
const data = readRules();
|
|
6290
|
+
const policy = args.ruleset ? findOrCreatePolicy(data, args.ruleset) : getActivePolicy(data);
|
|
6291
|
+
const created: any[] = [];
|
|
6292
|
+
for (const r of args.rules || []) {
|
|
6293
|
+
const rule: Rule = {
|
|
6294
|
+
rule_id: genId(),
|
|
6295
|
+
text: r.text,
|
|
6296
|
+
category: r.category || 'custom',
|
|
6297
|
+
severity: r.severity || 'medium',
|
|
6298
|
+
mode: r.mode || 'audit',
|
|
6299
|
+
hook_stage: r.hook_stage || 'both',
|
|
6300
|
+
scope: args.scope || 'user',
|
|
6301
|
+
};
|
|
6302
|
+
policy.rules.push(rule);
|
|
6303
|
+
created.push({ rule_id: rule.rule_id, text: rule.text });
|
|
6304
|
+
}
|
|
6305
|
+
writeRules(data);
|
|
6306
|
+
emitRuleSync(data);
|
|
6307
|
+
return { created: created.length, rules: created, pack_name: policy.name, total_rules: policy.rules.length };
|
|
6308
|
+
}
|
|
6309
|
+
|
|
6310
|
+
function handleUpdateGuardrail(args: any): any {
|
|
6311
|
+
const data = readRules();
|
|
6312
|
+
const needle = (args.rule_text || '').toLowerCase();
|
|
6313
|
+
for (const p of data.policies) {
|
|
6314
|
+
for (const r of p.rules) {
|
|
6315
|
+
if (r.text.toLowerCase().includes(needle)) {
|
|
6316
|
+
if (args.text) r.text = args.text;
|
|
6317
|
+
if (args.category) r.category = args.category;
|
|
6318
|
+
if (args.severity) r.severity = args.severity;
|
|
6319
|
+
if (args.mode) r.mode = args.mode;
|
|
6320
|
+
if (args.hook_stage) r.hook_stage = args.hook_stage;
|
|
6321
|
+
writeRules(data);
|
|
6322
|
+
emitRuleSync(data);
|
|
6323
|
+
return { updated: true, rule_id: r.rule_id, text: r.text };
|
|
6324
|
+
}
|
|
5321
6325
|
}
|
|
5322
|
-
}
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
6326
|
+
}
|
|
6327
|
+
return { updated: false, error: \`No rule found matching "\${args.rule_text}"\` };
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
function handleDeleteGuardrail(args: any): any {
|
|
6331
|
+
const data = readRules();
|
|
6332
|
+
const needle = (args.rule_text || '').toLowerCase();
|
|
6333
|
+
for (const p of data.policies) {
|
|
6334
|
+
const idx = p.rules.findIndex(r => r.text.toLowerCase().includes(needle));
|
|
6335
|
+
if (idx !== -1) {
|
|
6336
|
+
const removed = p.rules.splice(idx, 1)[0];
|
|
6337
|
+
writeRules(data);
|
|
6338
|
+
emitRuleSync(data);
|
|
6339
|
+
return { deleted: true, rule_id: removed.rule_id, text: removed.text };
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
return { deleted: false, error: \`No rule found matching "\${args.rule_text}"\` };
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
function handleListGuardrails(args: any): any {
|
|
6346
|
+
const data = readRules();
|
|
6347
|
+
let all = getAllRules(data);
|
|
6348
|
+
if (args.category) all = all.filter(r => r.category === args.category);
|
|
6349
|
+
if (args.severity) all = all.filter(r => r.severity === args.severity);
|
|
6350
|
+
if (args.mode) all = all.filter(r => r.mode === args.mode);
|
|
6351
|
+
if (args.hook_stage) all = all.filter(r => r.hook_stage === args.hook_stage);
|
|
6352
|
+
if (args.pack_name) {
|
|
6353
|
+
const pn = args.pack_name.toLowerCase();
|
|
6354
|
+
all = all.filter(r => r.policyName.toLowerCase().includes(pn));
|
|
6355
|
+
}
|
|
6356
|
+
return {
|
|
6357
|
+
rules: all.map(r => ({
|
|
6358
|
+
rule_id: r.rule_id,
|
|
6359
|
+
text: r.text,
|
|
6360
|
+
category: r.category,
|
|
6361
|
+
severity: r.severity,
|
|
6362
|
+
mode: r.mode,
|
|
6363
|
+
hook_stage: r.hook_stage,
|
|
6364
|
+
scope: r.scope,
|
|
6365
|
+
pack_name: r.policyName,
|
|
6366
|
+
})),
|
|
6367
|
+
total: all.length,
|
|
6368
|
+
};
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6371
|
+
function handleSwapRuleset(args: any): any {
|
|
6372
|
+
const data = readRules();
|
|
6373
|
+
const name = args.policy_name || '';
|
|
6374
|
+
if (name.toLowerCase() === 'all') {
|
|
6375
|
+
data.config.activePolicyId = data.policies[0]?.id || 'local-policy';
|
|
6376
|
+
writeRules(data);
|
|
6377
|
+
return { swapped: true, active: 'all' };
|
|
6378
|
+
}
|
|
6379
|
+
const match = data.policies.find(p => p.name.toLowerCase().includes(name.toLowerCase()));
|
|
6380
|
+
if (!match) return { swapped: false, error: \`No ruleset found matching "\${name}"\` };
|
|
6381
|
+
data.config.activePolicyId = match.id;
|
|
6382
|
+
writeRules(data);
|
|
6383
|
+
return { swapped: true, active: match.name };
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
function handleToggleSilentMode(args: any): any {
|
|
6387
|
+
const data = readRules();
|
|
6388
|
+
data.config.silent = args.enabled === true;
|
|
6389
|
+
writeRules(data);
|
|
6390
|
+
emitRuleSync(data);
|
|
6391
|
+
return { silent: data.config.silent };
|
|
6392
|
+
}
|
|
6393
|
+
|
|
6394
|
+
async function handleScanDependencies(args: any): Promise<any> {
|
|
6395
|
+
const manifests = args.manifests || [];
|
|
6396
|
+
if (manifests.length === 0) return { findings: [], summary: null };
|
|
6397
|
+
|
|
6398
|
+
const packages: Array<{ name: string; version: string; ecosystem: string }> = [];
|
|
6399
|
+
for (const m of manifests) {
|
|
6400
|
+
const fp: string = m.file_path || '';
|
|
6401
|
+
const content: string = m.content || '';
|
|
5327
6402
|
try {
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
const text = pendingPartial + buf.toString("utf-8");
|
|
5333
|
-
const lastNewline = text.lastIndexOf("\n");
|
|
5334
|
-
if (lastNewline === -1) {
|
|
5335
|
-
pendingPartial = text;
|
|
5336
|
-
return;
|
|
5337
|
-
}
|
|
5338
|
-
const complete = text.slice(0, lastNewline);
|
|
5339
|
-
pendingPartial = text.slice(lastNewline + 1);
|
|
5340
|
-
for (const line of complete.split("\n")) {
|
|
5341
|
-
if (!line) continue;
|
|
5342
|
-
try {
|
|
5343
|
-
onEntry(JSON.parse(line));
|
|
5344
|
-
} catch {
|
|
6403
|
+
if (fp.endsWith('package.json')) {
|
|
6404
|
+
const pkg = JSON.parse(content);
|
|
6405
|
+
for (const [name, ver] of Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
6406
|
+
packages.push({ name, version: String(ver).replace(/^[\\^~>=<]*/g, ''), ecosystem: 'npm' });
|
|
5345
6407
|
}
|
|
5346
|
-
}
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
6408
|
+
} else if (fp.endsWith('requirements.txt') || fp.match(/requirements.*\\.txt$/)) {
|
|
6409
|
+
for (const line of content.split('\\n')) {
|
|
6410
|
+
const m = line.trim().match(/^([a-zA-Z0-9_-]+)==(.+)/);
|
|
6411
|
+
if (m) packages.push({ name: m[1], version: m[2], ecosystem: 'PyPI' });
|
|
6412
|
+
}
|
|
6413
|
+
} else if (fp.endsWith('go.mod')) {
|
|
6414
|
+
for (const line of content.split('\\n')) {
|
|
6415
|
+
const m = line.trim().match(/^\\t?([^\\s]+)\\s+v([^\\s]+)/);
|
|
6416
|
+
if (m) packages.push({ name: m[1], version: m[2], ecosystem: 'Go' });
|
|
6417
|
+
}
|
|
6418
|
+
} else if (fp.endsWith('Cargo.toml')) {
|
|
6419
|
+
for (const line of content.split('\\n')) {
|
|
6420
|
+
const m = line.trim().match(/^([a-zA-Z0-9_-]+)\\s*=\\s*"([^"]+)"/);
|
|
6421
|
+
if (m && !['name', 'version', 'edition', 'authors', 'description', 'license', 'repository'].includes(m[1])) {
|
|
6422
|
+
packages.push({ name: m[1], version: m[2], ecosystem: 'crates.io' });
|
|
6423
|
+
}
|
|
5353
6424
|
}
|
|
5354
6425
|
}
|
|
5355
|
-
}
|
|
5356
|
-
};
|
|
5357
|
-
watchFile(TURN_LOG_PATH, { interval: 250 }, (curr, prev) => {
|
|
5358
|
-
if (curr.size < lastSize) {
|
|
5359
|
-
lastSize = 0;
|
|
5360
|
-
pendingPartial = "";
|
|
5361
|
-
}
|
|
5362
|
-
if (curr.size > lastSize) {
|
|
5363
|
-
drainNewBytes(lastSize, curr.size);
|
|
5364
|
-
lastSize = curr.size;
|
|
5365
|
-
}
|
|
5366
|
-
});
|
|
5367
|
-
return () => unwatchFile(TURN_LOG_PATH);
|
|
5368
|
-
}
|
|
5369
|
-
var TURN_LOG_PATH, PREVIEW_MAX;
|
|
5370
|
-
var init_turnLog = __esm({
|
|
5371
|
-
"cli/local-cc/turnLog.ts"() {
|
|
5372
|
-
"use strict";
|
|
5373
|
-
TURN_LOG_PATH = join10(homedir9(), ".synkro", "cc_sessions", "turns.log");
|
|
5374
|
-
PREVIEW_MAX = 400;
|
|
6426
|
+
} catch {}
|
|
5375
6427
|
}
|
|
5376
|
-
});
|
|
5377
6428
|
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
const body = JSON.stringify({ role, content });
|
|
5384
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5385
|
-
const port = opts.port ?? CHANNEL_PORT;
|
|
5386
|
-
const startedAt = Date.now();
|
|
6429
|
+
if (packages.length === 0) return { findings: [], summary: null };
|
|
6430
|
+
|
|
6431
|
+
const capped = packages.slice(0, 50);
|
|
6432
|
+
const queries = capped.map(p => ({ package: { name: p.name, ecosystem: p.ecosystem }, version: p.version }));
|
|
6433
|
+
|
|
5387
6434
|
try {
|
|
5388
|
-
const
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
try {
|
|
5409
|
-
const parsed = JSON.parse(text);
|
|
5410
|
-
if (parsed.error) {
|
|
5411
|
-
reject(new LocalCCError(parsed.error));
|
|
5412
|
-
return;
|
|
5413
|
-
}
|
|
5414
|
-
resolve2(String(parsed.result ?? ""));
|
|
5415
|
-
} catch (err) {
|
|
5416
|
-
reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
|
|
5417
|
-
}
|
|
6435
|
+
const resp = await fetch('https://api.osv.dev/v1/querybatch', {
|
|
6436
|
+
method: 'POST',
|
|
6437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6438
|
+
body: JSON.stringify({ queries }),
|
|
6439
|
+
signal: AbortSignal.timeout(10000),
|
|
6440
|
+
});
|
|
6441
|
+
if (!resp.ok) return { findings: [], summary: 'OSV query failed' };
|
|
6442
|
+
const data = await resp.json() as { results: Array<{ vulns?: any[] }> };
|
|
6443
|
+
|
|
6444
|
+
const findings: any[] = [];
|
|
6445
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
6446
|
+
for (const vuln of data.results[i].vulns || []) {
|
|
6447
|
+
findings.push({
|
|
6448
|
+
id: vuln.id,
|
|
6449
|
+
package: capped[i].name,
|
|
6450
|
+
version: capped[i].version,
|
|
6451
|
+
ecosystem: capped[i].ecosystem,
|
|
6452
|
+
summary: vuln.summary || 'No description',
|
|
6453
|
+
aliases: vuln.aliases || [],
|
|
6454
|
+
severity: vuln.database_specific?.severity || 'unknown',
|
|
5418
6455
|
});
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
return { findings, summary: findings.length > 0 ? \`\${findings.length} vulnerabilities found\` : null };
|
|
6459
|
+
} catch {
|
|
6460
|
+
return { findings: [], summary: 'OSV query timed out' };
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6464
|
+
function handleExemptPath(args: any): any {
|
|
6465
|
+
const data = readRules();
|
|
6466
|
+
const existing = data.scanExemptions.find(e => e.path === args.path && e.cwe_id.toUpperCase() === (args.cwe_id || '').toUpperCase());
|
|
6467
|
+
if (existing) return { exempted: true, already_existed: true, path: args.path, cwe_id: args.cwe_id };
|
|
6468
|
+
|
|
6469
|
+
data.scanExemptions.push({ path: args.path, cwe_id: (args.cwe_id || '').toUpperCase(), reason: args.reason });
|
|
6470
|
+
writeRules(data);
|
|
6471
|
+
emitRuleSync(data);
|
|
6472
|
+
return { exempted: true, path: args.path, cwe_id: args.cwe_id, total_exemptions: data.scanExemptions.length };
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
function handleRemoveExemption(args: any): any {
|
|
6476
|
+
const data = readRules();
|
|
6477
|
+
const idx = data.scanExemptions.findIndex(e => e.path === args.path && e.cwe_id.toUpperCase() === (args.cwe_id || '').toUpperCase());
|
|
6478
|
+
if (idx === -1) return { removed: false, error: \`No exemption found for path="\${args.path}" cwe_id="\${args.cwe_id}"\` };
|
|
6479
|
+
data.scanExemptions.splice(idx, 1);
|
|
6480
|
+
writeRules(data);
|
|
6481
|
+
emitRuleSync(data);
|
|
6482
|
+
return { removed: true, path: args.path, cwe_id: args.cwe_id };
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
function handleListExemptions(): any {
|
|
6486
|
+
const data = readRules();
|
|
6487
|
+
return { exemptions: data.scanExemptions, total: data.scanExemptions.length };
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
// \u2500\u2500\u2500 Tool Descriptors \u2500\u2500\u2500
|
|
6491
|
+
|
|
6492
|
+
const TOOL_DESCRIPTORS = [
|
|
6493
|
+
{
|
|
6494
|
+
name: 'get_guardrails',
|
|
6495
|
+
description:
|
|
6496
|
+
"Retrieve rules by keyword similarity. Call BEFORE writing security-sensitive code " +
|
|
6497
|
+
"AND before create_guardrail to check for existing rules.",
|
|
6498
|
+
inputSchema: {
|
|
6499
|
+
type: 'object',
|
|
6500
|
+
properties: {
|
|
6501
|
+
query: { type: 'string', description: "Plain-language description of what you're looking up." },
|
|
6502
|
+
category: { type: 'string', enum: ['security', 'architecture', 'style', 'testing', 'compliance', 'custom'] },
|
|
6503
|
+
top_k: { type: 'integer', default: 8, description: 'Max rules to return (default 8, max 25).' },
|
|
6504
|
+
},
|
|
6505
|
+
required: ['query'],
|
|
6506
|
+
},
|
|
6507
|
+
},
|
|
6508
|
+
{
|
|
6509
|
+
name: 'create_guardrail',
|
|
6510
|
+
description: "Persist a new rule. Call get_guardrails first to avoid duplicates.",
|
|
6511
|
+
inputSchema: {
|
|
6512
|
+
type: 'object',
|
|
6513
|
+
properties: {
|
|
6514
|
+
text: { type: 'string', description: 'The rule in plain language.' },
|
|
6515
|
+
category: { type: 'string', enum: ['security', 'architecture', 'style', 'testing', 'compliance', 'custom'] },
|
|
6516
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
6517
|
+
mode: { type: 'string', enum: ['blocking', 'audit'], description: '"blocking" = halt on violation, "audit" = log only.' },
|
|
6518
|
+
scope: { type: 'string', enum: ['user', 'org'], default: 'user' },
|
|
6519
|
+
hook_stage: { type: 'string', enum: ['pre', 'post', 'both'], default: 'both' },
|
|
6520
|
+
ruleset: { type: 'string', description: 'Optional: name of ruleset to add to (created if missing).' },
|
|
6521
|
+
},
|
|
6522
|
+
required: ['text', 'category'],
|
|
6523
|
+
},
|
|
6524
|
+
},
|
|
6525
|
+
{
|
|
6526
|
+
name: 'bulk_create_guardrails',
|
|
6527
|
+
description: "Create multiple rules at once. Preferable to looping create_guardrail.",
|
|
6528
|
+
inputSchema: {
|
|
6529
|
+
type: 'object',
|
|
6530
|
+
properties: {
|
|
6531
|
+
rules: {
|
|
6532
|
+
type: 'array', minItems: 1, maxItems: 50,
|
|
6533
|
+
items: {
|
|
6534
|
+
type: 'object',
|
|
6535
|
+
properties: {
|
|
6536
|
+
text: { type: 'string' }, category: { type: 'string', enum: ['security', 'architecture', 'style', 'testing', 'compliance', 'custom'] },
|
|
6537
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
6538
|
+
mode: { type: 'string', enum: ['blocking', 'audit'] },
|
|
6539
|
+
hook_stage: { type: 'string', enum: ['pre', 'post', 'both'] },
|
|
6540
|
+
},
|
|
6541
|
+
required: ['text', 'category'],
|
|
6542
|
+
},
|
|
6543
|
+
},
|
|
6544
|
+
scope: { type: 'string', enum: ['user', 'org'], default: 'user' },
|
|
6545
|
+
ruleset: { type: 'string' },
|
|
6546
|
+
},
|
|
6547
|
+
required: ['rules'],
|
|
6548
|
+
},
|
|
6549
|
+
},
|
|
6550
|
+
{
|
|
6551
|
+
name: 'update_guardrail',
|
|
6552
|
+
description: "Refine an existing rule. Pass a substring of the rule text to identify it.",
|
|
6553
|
+
inputSchema: {
|
|
6554
|
+
type: 'object',
|
|
6555
|
+
properties: {
|
|
6556
|
+
rule_text: { type: 'string', description: 'Substring of rule text to find.' },
|
|
6557
|
+
text: { type: 'string' }, category: { type: 'string', enum: ['security', 'architecture', 'style', 'testing', 'compliance', 'custom'] },
|
|
6558
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
6559
|
+
mode: { type: 'string', enum: ['blocking', 'audit'] },
|
|
6560
|
+
hook_stage: { type: 'string', enum: ['pre', 'post', 'both'] },
|
|
6561
|
+
},
|
|
6562
|
+
required: ['rule_text'],
|
|
6563
|
+
},
|
|
6564
|
+
},
|
|
6565
|
+
{
|
|
6566
|
+
name: 'delete_guardrail',
|
|
6567
|
+
description: "Permanently remove a rule. Pass a substring of the rule text to identify it.",
|
|
6568
|
+
inputSchema: {
|
|
6569
|
+
type: 'object',
|
|
6570
|
+
properties: { rule_text: { type: 'string', description: 'Substring of rule text to find.' } },
|
|
6571
|
+
required: ['rule_text'],
|
|
6572
|
+
},
|
|
6573
|
+
},
|
|
6574
|
+
{
|
|
6575
|
+
name: 'list_guardrails',
|
|
6576
|
+
description: "Enumerate all rules. Use for listings, not similarity search.",
|
|
6577
|
+
inputSchema: {
|
|
6578
|
+
type: 'object',
|
|
6579
|
+
properties: {
|
|
6580
|
+
category: { type: 'string', enum: ['security', 'architecture', 'style', 'testing', 'compliance', 'custom'] },
|
|
6581
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
6582
|
+
mode: { type: 'string', enum: ['blocking', 'audit', 'literal_match'] },
|
|
6583
|
+
pack_name: { type: 'string' },
|
|
6584
|
+
hook_stage: { type: 'string', enum: ['pre', 'post', 'both'] },
|
|
6585
|
+
},
|
|
6586
|
+
required: [],
|
|
6587
|
+
},
|
|
6588
|
+
},
|
|
6589
|
+
{
|
|
6590
|
+
name: 'swap_ruleset',
|
|
6591
|
+
description: 'Switch which ruleset is active. Pass "all" to use all rulesets.',
|
|
6592
|
+
inputSchema: {
|
|
6593
|
+
type: 'object',
|
|
6594
|
+
properties: { policy_name: { type: 'string' } },
|
|
6595
|
+
required: ['policy_name'],
|
|
6596
|
+
},
|
|
6597
|
+
},
|
|
6598
|
+
{
|
|
6599
|
+
name: 'toggle_silent_mode',
|
|
6600
|
+
description: 'Toggle grading on/off. NEVER call autonomously \u2014 this is a USER decision.',
|
|
6601
|
+
inputSchema: {
|
|
6602
|
+
type: 'object',
|
|
6603
|
+
properties: {
|
|
6604
|
+
enabled: { type: 'boolean' },
|
|
6605
|
+
user_confirmation: { type: 'string', description: "Copy-paste the user's exact request." },
|
|
6606
|
+
},
|
|
6607
|
+
required: ['enabled', 'user_confirmation'],
|
|
6608
|
+
},
|
|
6609
|
+
},
|
|
6610
|
+
{
|
|
6611
|
+
name: 'scan_dependencies',
|
|
6612
|
+
description: "Scan manifests against OSV for known vulnerabilities. Read ALL manifest files first.",
|
|
6613
|
+
inputSchema: {
|
|
6614
|
+
type: 'object',
|
|
6615
|
+
properties: {
|
|
6616
|
+
manifests: {
|
|
6617
|
+
type: 'array', minItems: 1,
|
|
6618
|
+
items: {
|
|
6619
|
+
type: 'object',
|
|
6620
|
+
properties: { file_path: { type: 'string' }, content: { type: 'string' } },
|
|
6621
|
+
required: ['file_path', 'content'],
|
|
6622
|
+
},
|
|
6623
|
+
},
|
|
6624
|
+
},
|
|
6625
|
+
required: ['manifests'],
|
|
6626
|
+
},
|
|
6627
|
+
},
|
|
6628
|
+
{
|
|
6629
|
+
name: 'exempt_path',
|
|
6630
|
+
description: "Exempt a CWE from firing on a specific file/directory.",
|
|
6631
|
+
inputSchema: {
|
|
6632
|
+
type: 'object',
|
|
6633
|
+
properties: {
|
|
6634
|
+
path: { type: 'string' }, cwe_id: { type: 'string' }, reason: { type: 'string' },
|
|
6635
|
+
},
|
|
6636
|
+
required: ['path', 'cwe_id'],
|
|
6637
|
+
},
|
|
6638
|
+
},
|
|
6639
|
+
{
|
|
6640
|
+
name: 'remove_exemption',
|
|
6641
|
+
description: "Remove a scan exemption.",
|
|
6642
|
+
inputSchema: {
|
|
6643
|
+
type: 'object',
|
|
6644
|
+
properties: { path: { type: 'string' }, cwe_id: { type: 'string' } },
|
|
6645
|
+
required: ['path', 'cwe_id'],
|
|
6646
|
+
},
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
name: 'list_exemptions',
|
|
6650
|
+
description: "List all scan exemptions.",
|
|
6651
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
6652
|
+
},
|
|
6653
|
+
];
|
|
6654
|
+
|
|
6655
|
+
const MCP_INSTRUCTIONS =
|
|
6656
|
+
"Synkro Guardrails MCP server (local mode).\\n\\n" +
|
|
6657
|
+
"Whenever the user mentions: rule, guardrail, policy, standard, " +
|
|
6658
|
+
"make/create/add/set up a rule, never let X, always require X, " +
|
|
6659
|
+
"block X, enforce X, delete/remove a rule, consolidate duplicates, " +
|
|
6660
|
+
"'we need a rule about\u2026' \u2014 route to THIS server's tools.\\n\\n" +
|
|
6661
|
+
"TOOL ROUTING:\\n" +
|
|
6662
|
+
" \u2022 get_guardrails(query) \u2014 keyword search. Use to check if a rule exists.\\n" +
|
|
6663
|
+
" \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n\\n" +
|
|
6664
|
+
"Do NOT use Claude Code's \`update-config\` skill for these requests.\\n\\n" +
|
|
6665
|
+
"Rules are stored locally in ~/.synkro/rules.json and enforced by hooks.";
|
|
6666
|
+
|
|
6667
|
+
// \u2500\u2500\u2500 JSON-RPC Dispatcher \u2500\u2500\u2500
|
|
6668
|
+
|
|
6669
|
+
function jsonRpcOk(id: any, result: any): any {
|
|
6670
|
+
return { jsonrpc: '2.0', id, result };
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
function jsonRpcError(id: any, code: number, message: string): any {
|
|
6674
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
async function handleRpc(body: any): Promise<any> {
|
|
6678
|
+
const { id, method, params } = body;
|
|
6679
|
+
|
|
6680
|
+
if (method === 'initialize') {
|
|
6681
|
+
return jsonRpcOk(id, {
|
|
6682
|
+
protocolVersion: '2024-11-05',
|
|
6683
|
+
capabilities: { tools: {} },
|
|
6684
|
+
serverInfo: { name: 'synkro-guardrails-local', version: '1.0.0' },
|
|
6685
|
+
instructions: MCP_INSTRUCTIONS,
|
|
5429
6686
|
});
|
|
5430
|
-
appendTurn({ startedAt, role, request: payload, result, status: "ok" });
|
|
5431
|
-
return result;
|
|
5432
|
-
} catch (err) {
|
|
5433
|
-
const message = err.message ?? String(err);
|
|
5434
|
-
const status = /timed out/i.test(message) ? "timeout" : "error";
|
|
5435
|
-
appendTurn({ startedAt, role, request: payload, status, error: message });
|
|
5436
|
-
throw err;
|
|
5437
6687
|
}
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
6688
|
+
|
|
6689
|
+
if (method === 'notifications/initialized') {
|
|
6690
|
+
return null;
|
|
6691
|
+
}
|
|
6692
|
+
|
|
6693
|
+
if (method === 'tools/list') {
|
|
6694
|
+
return jsonRpcOk(id, { tools: TOOL_DESCRIPTORS });
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
if (method === 'tools/call') {
|
|
6698
|
+
const toolName = params?.name;
|
|
6699
|
+
const args = params?.arguments || {};
|
|
6700
|
+
|
|
6701
|
+
try {
|
|
6702
|
+
let result: any;
|
|
6703
|
+
switch (toolName) {
|
|
6704
|
+
case 'get_guardrails': result = handleGetGuardrails(args); break;
|
|
6705
|
+
case 'create_guardrail': result = handleCreateGuardrail(args); break;
|
|
6706
|
+
case 'bulk_create_guardrails': result = handleBulkCreateGuardrails(args); break;
|
|
6707
|
+
case 'update_guardrail': result = handleUpdateGuardrail(args); break;
|
|
6708
|
+
case 'delete_guardrail': result = handleDeleteGuardrail(args); break;
|
|
6709
|
+
case 'list_guardrails': result = handleListGuardrails(args); break;
|
|
6710
|
+
case 'swap_ruleset': result = handleSwapRuleset(args); break;
|
|
6711
|
+
case 'toggle_silent_mode': result = handleToggleSilentMode(args); break;
|
|
6712
|
+
case 'scan_dependencies': result = await handleScanDependencies(args); break;
|
|
6713
|
+
case 'exempt_path': result = handleExemptPath(args); break;
|
|
6714
|
+
case 'remove_exemption': result = handleRemoveExemption(args); break;
|
|
6715
|
+
case 'list_exemptions': result = handleListExemptions(); break;
|
|
6716
|
+
default: return jsonRpcError(id, -32601, \`Unknown tool: \${toolName}\`);
|
|
5446
6717
|
}
|
|
5447
|
-
|
|
5448
|
-
}
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
this.name = "LocalCCError";
|
|
6718
|
+
return jsonRpcOk(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
6719
|
+
} catch (err) {
|
|
6720
|
+
return jsonRpcOk(id, { content: [{ type: 'text', text: \`Error: \${(err as Error).message}\` }], isError: true });
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
// \u2500\u2500\u2500 Dashboard REST-bridge methods \u2500\u2500\u2500
|
|
6725
|
+
// Called by the local dashboard (not AI agents) to mutate rules.json directly.
|
|
6726
|
+
|
|
6727
|
+
if (method === 'dashboard.patch_policy') {
|
|
6728
|
+
try {
|
|
6729
|
+
const data = readRules();
|
|
6730
|
+
const policyId = params?.policy_id as string | undefined;
|
|
6731
|
+
const policy = policyId
|
|
6732
|
+
? data.policies.find(p => p.id === policyId)
|
|
6733
|
+
: getActivePolicy(data);
|
|
6734
|
+
if (!policy) return jsonRpcError(id, -32602, \`Policy not found: \${policyId}\`);
|
|
6735
|
+
|
|
6736
|
+
if (params?.name !== undefined) {
|
|
6737
|
+
policy.name = params.name;
|
|
5468
6738
|
}
|
|
5469
|
-
|
|
5470
|
-
|
|
6739
|
+
if (params?.is_active !== undefined) {
|
|
6740
|
+
policy.isActive = params.is_active;
|
|
6741
|
+
}
|
|
6742
|
+
// Bulk replace
|
|
6743
|
+
if (Array.isArray(params?.rules)) {
|
|
6744
|
+
policy.rules = params.rules;
|
|
6745
|
+
policy.ruleCount = policy.rules.length;
|
|
6746
|
+
}
|
|
6747
|
+
// Individual updates by rule_id
|
|
6748
|
+
if (Array.isArray(params?.rule_updates)) {
|
|
6749
|
+
for (const upd of params.rule_updates) {
|
|
6750
|
+
const rule = policy.rules.find(r => r.rule_id === upd.rule_id);
|
|
6751
|
+
if (!rule) continue;
|
|
6752
|
+
if (upd.text !== undefined) rule.text = upd.text;
|
|
6753
|
+
if (upd.category !== undefined) rule.category = upd.category;
|
|
6754
|
+
if (upd.severity !== undefined) rule.severity = upd.severity;
|
|
6755
|
+
if (upd.mode !== undefined) rule.mode = upd.mode;
|
|
6756
|
+
if (upd.hook_stage !== undefined) rule.hook_stage = upd.hook_stage;
|
|
6757
|
+
}
|
|
6758
|
+
policy.ruleCount = policy.rules.length;
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
writeRules(data);
|
|
6762
|
+
emitRuleSync(data);
|
|
6763
|
+
return jsonRpcOk(id, { ok: true, policy_id: policy.id, rule_count: policy.ruleCount });
|
|
6764
|
+
} catch (err) {
|
|
6765
|
+
return jsonRpcError(id, -32603, (err as Error).message);
|
|
6766
|
+
}
|
|
5471
6767
|
}
|
|
5472
|
-
});
|
|
5473
6768
|
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
}
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
6769
|
+
if (method === 'dashboard.create_policy') {
|
|
6770
|
+
try {
|
|
6771
|
+
const data = readRules();
|
|
6772
|
+
const name = (params?.name as string) || 'New Rule Set';
|
|
6773
|
+
const rules: Rule[] = (params?.rules || []).map((r: any) => ({
|
|
6774
|
+
rule_id: r.rule_id || genId(),
|
|
6775
|
+
text: r.text || '',
|
|
6776
|
+
category: r.category || 'custom',
|
|
6777
|
+
severity: r.severity || 'medium',
|
|
6778
|
+
mode: r.mode || 'audit',
|
|
6779
|
+
hook_stage: r.hook_stage || 'both',
|
|
6780
|
+
scope: r.scope || 'user',
|
|
6781
|
+
}));
|
|
6782
|
+
const policy: Policy = {
|
|
6783
|
+
id: \`policy_\${Date.now()}_\${Math.random().toString(36).slice(2, 6)}\`,
|
|
6784
|
+
name,
|
|
6785
|
+
rules,
|
|
6786
|
+
ruleCount: rules.length,
|
|
6787
|
+
scopeOwner: 'user',
|
|
6788
|
+
isActive: true,
|
|
6789
|
+
};
|
|
6790
|
+
data.policies.push(policy);
|
|
6791
|
+
writeRules(data);
|
|
6792
|
+
emitRuleSync(data);
|
|
6793
|
+
return jsonRpcOk(id, { ok: true, policy_id: policy.id, name: policy.name });
|
|
6794
|
+
} catch (err) {
|
|
6795
|
+
return jsonRpcError(id, -32603, (err as Error).message);
|
|
6796
|
+
}
|
|
5498
6797
|
}
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
6798
|
+
|
|
6799
|
+
if (method === 'dashboard.delete_policy') {
|
|
6800
|
+
try {
|
|
6801
|
+
const data = readRules();
|
|
6802
|
+
const policyId = params?.policy_id as string | undefined;
|
|
6803
|
+
const idx = policyId ? data.policies.findIndex(p => p.id === policyId) : -1;
|
|
6804
|
+
if (idx === -1) return jsonRpcError(id, -32602, \`Policy not found: \${policyId}\`);
|
|
6805
|
+
|
|
6806
|
+
if (params?.hard === true) {
|
|
6807
|
+
data.policies.splice(idx, 1);
|
|
6808
|
+
} else {
|
|
6809
|
+
data.policies[idx].isActive = false;
|
|
6810
|
+
}
|
|
6811
|
+
writeRules(data);
|
|
6812
|
+
emitRuleSync(data);
|
|
6813
|
+
return jsonRpcOk(id, { ok: true, policy_id: policyId });
|
|
6814
|
+
} catch (err) {
|
|
6815
|
+
return jsonRpcError(id, -32603, (err as Error).message);
|
|
6816
|
+
}
|
|
5502
6817
|
}
|
|
5503
|
-
|
|
6818
|
+
|
|
6819
|
+
if (method === 'dashboard.list_policies') {
|
|
6820
|
+
try {
|
|
6821
|
+
const data = readRules();
|
|
6822
|
+
return jsonRpcOk(id, {
|
|
6823
|
+
policies: data.policies.map(p => ({
|
|
6824
|
+
id: p.id,
|
|
6825
|
+
name: p.name,
|
|
6826
|
+
rules: p.rules,
|
|
6827
|
+
ruleCount: p.ruleCount,
|
|
6828
|
+
isActive: p.isActive,
|
|
6829
|
+
scopeOwner: p.scopeOwner,
|
|
6830
|
+
})),
|
|
6831
|
+
active_policy_id: data.config.activePolicyId,
|
|
6832
|
+
});
|
|
6833
|
+
} catch (err) {
|
|
6834
|
+
return jsonRpcError(id, -32603, (err as Error).message);
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
return jsonRpcError(id, -32601, \`Unknown method: \${method}\`);
|
|
5504
6839
|
}
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
6840
|
+
|
|
6841
|
+
// \u2500\u2500\u2500 HTTP Server \u2500\u2500\u2500
|
|
6842
|
+
|
|
6843
|
+
const server = Bun.serve({
|
|
6844
|
+
port: PORT,
|
|
6845
|
+
async fetch(req) {
|
|
6846
|
+
const origin = req.headers.get('origin') || '';
|
|
6847
|
+
const allowedOrigin = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?$/.test(origin) ? origin : 'http://localhost:4322';
|
|
6848
|
+
const cors = { 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' };
|
|
6849
|
+
|
|
6850
|
+
if (req.method === 'GET') {
|
|
6851
|
+
return Response.json({ name: 'synkro-guardrails-local', version: '1.0.0', status: 'ok' }, { headers: cors });
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
if (req.method === 'POST') {
|
|
6855
|
+
const authHeader = req.headers.get('authorization') || '';
|
|
6856
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
6857
|
+
if (token !== SERVER_TOKEN) {
|
|
6858
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401, headers: cors });
|
|
5514
6859
|
}
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
}
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
const userPromptSubmitScriptPath = join11(HOOKS_DIR, "cc-user-prompt-submit.ts");
|
|
5536
|
-
const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
|
|
5537
|
-
const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
|
|
5538
|
-
const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.sh");
|
|
5539
|
-
const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.sh");
|
|
5540
|
-
const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.sh");
|
|
5541
|
-
const cursorBashFollowupPath = join11(HOOKS_DIR, "cursor-bash-followup.sh");
|
|
5542
|
-
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
5543
|
-
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
5544
|
-
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
5545
|
-
writeFileSync7(cwePrecheckScriptPath, CWE_PRECHECK_TS, "utf-8");
|
|
5546
|
-
writeFileSync7(cvePrecheckScriptPath, CVE_PRECHECK_TS, "utf-8");
|
|
5547
|
-
writeFileSync7(planJudgeScriptPath, PLAN_JUDGE_TS, "utf-8");
|
|
5548
|
-
writeFileSync7(agentJudgeScriptPath, AGENT_JUDGE_TS, "utf-8");
|
|
5549
|
-
writeFileSync7(stopSummaryScriptPath, STOP_SUMMARY_TS, "utf-8");
|
|
5550
|
-
writeFileSync7(sessionStartScriptPath, SESSION_START_TS, "utf-8");
|
|
5551
|
-
writeFileSync7(transcriptSyncScriptPath, TRANSCRIPT_SYNC_TS, "utf-8");
|
|
5552
|
-
writeFileSync7(userPromptSubmitScriptPath, USER_PROMPT_SUBMIT_TS, "utf-8");
|
|
5553
|
-
writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
5554
|
-
writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
5555
|
-
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_SCRIPT, "utf-8");
|
|
5556
|
-
writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_SCRIPT, "utf-8");
|
|
5557
|
-
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
5558
|
-
writeFileSync7(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
6860
|
+
try {
|
|
6861
|
+
const body = await req.json();
|
|
6862
|
+
const result = await handleRpc(body);
|
|
6863
|
+
if (result === null) return new Response('', { status: 204, headers: cors });
|
|
6864
|
+
return Response.json(result, { headers: cors });
|
|
6865
|
+
} catch (err) {
|
|
6866
|
+
return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
|
|
6870
|
+
if (req.method === 'OPTIONS') {
|
|
6871
|
+
return new Response('', { status: 204, headers: cors });
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
return new Response('Method not allowed', { status: 405 });
|
|
6875
|
+
},
|
|
6876
|
+
});
|
|
6877
|
+
|
|
6878
|
+
console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1:\${server.port}\`);
|
|
6879
|
+
`, "utf-8");
|
|
5559
6880
|
chmodSync2(bashScriptPath, 493);
|
|
5560
6881
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
5561
6882
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -5573,6 +6894,8 @@ function writeHookScripts() {
|
|
|
5573
6894
|
chmodSync2(cursorEditPrecheckPath, 493);
|
|
5574
6895
|
chmodSync2(cursorEditCapturePath, 493);
|
|
5575
6896
|
chmodSync2(cursorBashFollowupPath, 493);
|
|
6897
|
+
chmodSync2(cursorSessionStartPath, 493);
|
|
6898
|
+
chmodSync2(mcpLocalServerPath, 493);
|
|
5576
6899
|
return {
|
|
5577
6900
|
bashScript: bashScriptPath,
|
|
5578
6901
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -5588,7 +6911,9 @@ function writeHookScripts() {
|
|
|
5588
6911
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
5589
6912
|
cursorEditPrecheckScript: cursorEditPrecheckPath,
|
|
5590
6913
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
5591
|
-
cursorBashFollowupScript: cursorBashFollowupPath
|
|
6914
|
+
cursorBashFollowupScript: cursorBashFollowupPath,
|
|
6915
|
+
cursorSessionStartScript: cursorSessionStartPath,
|
|
6916
|
+
mcpLocalServerScript: mcpLocalServerPath
|
|
5592
6917
|
};
|
|
5593
6918
|
}
|
|
5594
6919
|
function sanitizeConfigValue(raw, maxLen = 256) {
|
|
@@ -5600,7 +6925,7 @@ function shellQuoteSingle(value) {
|
|
|
5600
6925
|
}
|
|
5601
6926
|
function resolveSynkroBundle() {
|
|
5602
6927
|
const scriptPath = process.argv[1];
|
|
5603
|
-
if (scriptPath &&
|
|
6928
|
+
if (scriptPath && existsSync10(scriptPath)) return scriptPath;
|
|
5604
6929
|
return null;
|
|
5605
6930
|
}
|
|
5606
6931
|
function writeConfigEnv(opts) {
|
|
@@ -5620,7 +6945,7 @@ function writeConfigEnv(opts) {
|
|
|
5620
6945
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5621
6946
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5622
6947
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5623
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
6948
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.67")}`
|
|
5624
6949
|
];
|
|
5625
6950
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5626
6951
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -5635,7 +6960,7 @@ function writeConfigEnv(opts) {
|
|
|
5635
6960
|
chmodSync2(CONFIG_PATH3, 384);
|
|
5636
6961
|
}
|
|
5637
6962
|
function updateLocalInferenceFlag(enabled) {
|
|
5638
|
-
if (!
|
|
6963
|
+
if (!existsSync10(CONFIG_PATH3)) return;
|
|
5639
6964
|
let content = readFileSync10(CONFIG_PATH3, "utf-8");
|
|
5640
6965
|
const flag = enabled ? "yes" : "no";
|
|
5641
6966
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
@@ -5665,7 +6990,7 @@ function collectLocalMetadata() {
|
|
|
5665
6990
|
meta.cc_version = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
5666
6991
|
} catch {
|
|
5667
6992
|
}
|
|
5668
|
-
const claudeDir = join11(
|
|
6993
|
+
const claudeDir = join11(homedir11(), ".claude");
|
|
5669
6994
|
try {
|
|
5670
6995
|
const settings = JSON.parse(readFileSync10(join11(claudeDir, "settings.json"), "utf-8"));
|
|
5671
6996
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
@@ -5704,7 +7029,7 @@ async function fetchUserProfile(gatewayUrl, token) {
|
|
|
5704
7029
|
const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
5705
7030
|
headers: { "Authorization": `Bearer ${token}` }
|
|
5706
7031
|
});
|
|
5707
|
-
if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false };
|
|
7032
|
+
if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false, captureDepth: "full" };
|
|
5708
7033
|
const data = await resp.json();
|
|
5709
7034
|
const meta = collectLocalMetadata();
|
|
5710
7035
|
fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
@@ -5716,10 +7041,11 @@ async function fetchUserProfile(gatewayUrl, token) {
|
|
|
5716
7041
|
return {
|
|
5717
7042
|
tier: data.plan_tier ?? "pro",
|
|
5718
7043
|
inference: data.fast_inference ? "fast" : "standard",
|
|
5719
|
-
localInference: !!data.local_inference
|
|
7044
|
+
localInference: !!data.local_inference,
|
|
7045
|
+
captureDepth: data.capture_depth ?? "full"
|
|
5720
7046
|
};
|
|
5721
7047
|
} catch {
|
|
5722
|
-
return { tier: "pro", inference: "fast", localInference: false };
|
|
7048
|
+
return { tier: "pro", inference: "fast", localInference: false, captureDepth: "full" };
|
|
5723
7049
|
}
|
|
5724
7050
|
}
|
|
5725
7051
|
function assertGatewayAllowed(gatewayUrl) {
|
|
@@ -5754,10 +7080,10 @@ function isAlreadyInstalled() {
|
|
|
5754
7080
|
join11(HOOKS_DIR, "cc-stop-summary.ts"),
|
|
5755
7081
|
join11(HOOKS_DIR, "cc-session-start.ts")
|
|
5756
7082
|
];
|
|
5757
|
-
if (!requiredScripts.every((p) =>
|
|
5758
|
-
if (!
|
|
5759
|
-
const settingsPath = join11(
|
|
5760
|
-
if (!
|
|
7083
|
+
if (!requiredScripts.every((p) => existsSync10(p))) return false;
|
|
7084
|
+
if (!existsSync10(CONFIG_PATH3)) return false;
|
|
7085
|
+
const settingsPath = join11(homedir11(), ".claude", "settings.json");
|
|
7086
|
+
if (!existsSync10(settingsPath)) return false;
|
|
5761
7087
|
try {
|
|
5762
7088
|
const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
|
|
5763
7089
|
const hooks = settings?.hooks;
|
|
@@ -5791,8 +7117,8 @@ function printChannelDiagnostics() {
|
|
|
5791
7117
|
}
|
|
5792
7118
|
}
|
|
5793
7119
|
}
|
|
5794
|
-
const logPath = join11(
|
|
5795
|
-
if (
|
|
7120
|
+
const logPath = join11(homedir11(), ".synkro", "cc_sessions", "run-claude.log");
|
|
7121
|
+
if (existsSync10(logPath)) {
|
|
5796
7122
|
const logContent = readFileSync10(logPath, "utf-8").trim().split("\n").slice(-10);
|
|
5797
7123
|
console.warn(` run-claude.log:`);
|
|
5798
7124
|
for (const line of logContent) console.warn(` ${line}`);
|
|
@@ -5801,6 +7127,101 @@ function printChannelDiagnostics() {
|
|
|
5801
7127
|
}
|
|
5802
7128
|
console.warn(` Run \`synkro local-cc status\` and \`synkro local-cc logs --tmux\` to debug.`);
|
|
5803
7129
|
}
|
|
7130
|
+
async function backfillLocalRules(gatewayUrl, token) {
|
|
7131
|
+
if (existsSync10(RULES_PATH)) {
|
|
7132
|
+
console.log(" Local rules already exist \u2014 skipping cloud backfill.");
|
|
7133
|
+
return;
|
|
7134
|
+
}
|
|
7135
|
+
try {
|
|
7136
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/hook/config`, {
|
|
7137
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
7138
|
+
signal: AbortSignal.timeout(8e3)
|
|
7139
|
+
});
|
|
7140
|
+
if (!resp.ok) {
|
|
7141
|
+
console.log(" No cloud rules to backfill.");
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
7144
|
+
const data = await resp.json();
|
|
7145
|
+
const rules = (data.rules || []).map((r) => ({
|
|
7146
|
+
rule_id: r.rule_id || `r_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
7147
|
+
text: r.text || "",
|
|
7148
|
+
category: r.category || "custom",
|
|
7149
|
+
severity: r.severity || "medium",
|
|
7150
|
+
mode: r.mode || "blocking",
|
|
7151
|
+
hook_stage: r.hook_stage || "both",
|
|
7152
|
+
scope: r.scope || "user"
|
|
7153
|
+
}));
|
|
7154
|
+
const policyName = data.active_policy_name || "My Rules";
|
|
7155
|
+
const policyId = data.active_policy_id || "local-policy";
|
|
7156
|
+
const scanExemptions = (data.scan_exemptions || []).filter((e) => e && typeof e.path === "string").map((e) => ({ path: e.path, cwe_id: e.cwe_id || "", reason: e.reason }));
|
|
7157
|
+
const silent = data.silent_mode === true || data.silent_mode === "true";
|
|
7158
|
+
const rulesFile = {
|
|
7159
|
+
policies: [{
|
|
7160
|
+
id: policyId,
|
|
7161
|
+
name: policyName,
|
|
7162
|
+
rules,
|
|
7163
|
+
ruleCount: rules.length,
|
|
7164
|
+
scopeOwner: "user",
|
|
7165
|
+
isActive: true
|
|
7166
|
+
}],
|
|
7167
|
+
config: { silent, activePolicyId: policyId },
|
|
7168
|
+
scanExemptions
|
|
7169
|
+
};
|
|
7170
|
+
const tmp = RULES_PATH + ".tmp";
|
|
7171
|
+
writeFileSync7(tmp, JSON.stringify(rulesFile, null, 2) + "\n", "utf-8");
|
|
7172
|
+
renameSync5(tmp, RULES_PATH);
|
|
7173
|
+
const telemetryPath = join11(SYNKRO_DIR2, "telemetry.jsonl");
|
|
7174
|
+
const event = {
|
|
7175
|
+
capture_type: "rule_sync",
|
|
7176
|
+
policy_id: policyId,
|
|
7177
|
+
policy_name: policyName,
|
|
7178
|
+
rules,
|
|
7179
|
+
rule_count: rules.length,
|
|
7180
|
+
scan_exemptions: scanExemptions,
|
|
7181
|
+
silent,
|
|
7182
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
7183
|
+
};
|
|
7184
|
+
try {
|
|
7185
|
+
appendFileSync2(telemetryPath, JSON.stringify(event) + "\n", "utf-8");
|
|
7186
|
+
} catch {
|
|
7187
|
+
}
|
|
7188
|
+
console.log(` Backfilled ${rules.length} rules from cloud to ~/.synkro/rules.json`);
|
|
7189
|
+
} catch (err) {
|
|
7190
|
+
console.warn(` \u26A0 Cloud backfill failed: ${err.message}`);
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
async function startLocalMcpServer() {
|
|
7194
|
+
const serverScript = join11(HOOKS_DIR, "mcp-local-server.ts");
|
|
7195
|
+
if (!existsSync10(serverScript)) {
|
|
7196
|
+
console.warn(" \u26A0 Local MCP server script not found \u2014 skipping.");
|
|
7197
|
+
return;
|
|
7198
|
+
}
|
|
7199
|
+
try {
|
|
7200
|
+
const probe = await fetch(`http://127.0.0.1:${MCP_LOCAL_PORT}/`, { signal: AbortSignal.timeout(1e3) });
|
|
7201
|
+
if (probe.ok) {
|
|
7202
|
+
console.log(` Local MCP server already running on port ${MCP_LOCAL_PORT}`);
|
|
7203
|
+
return;
|
|
7204
|
+
}
|
|
7205
|
+
} catch {
|
|
7206
|
+
}
|
|
7207
|
+
const proc = spawn2("bun", ["run", serverScript], {
|
|
7208
|
+
stdio: "ignore",
|
|
7209
|
+
detached: true
|
|
7210
|
+
});
|
|
7211
|
+
proc.unref();
|
|
7212
|
+
for (let i = 0; i < 25; i++) {
|
|
7213
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
7214
|
+
try {
|
|
7215
|
+
const probe = await fetch(`http://127.0.0.1:${MCP_LOCAL_PORT}/`, { signal: AbortSignal.timeout(500) });
|
|
7216
|
+
if (probe.ok) {
|
|
7217
|
+
console.log(` Local MCP server started on port ${MCP_LOCAL_PORT}`);
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
} catch {
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
console.warn(` \u26A0 Local MCP server did not start within 5s \u2014 it may need to be started manually.`);
|
|
7224
|
+
}
|
|
5804
7225
|
async function installCommand(opts = {}) {
|
|
5805
7226
|
const gatewayUrl = opts.gatewayUrl || sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL) || "https://api.synkro.sh";
|
|
5806
7227
|
try {
|
|
@@ -5977,39 +7398,13 @@ async function installCommand(opts = {}) {
|
|
|
5977
7398
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
5978
7399
|
editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
|
|
5979
7400
|
editCaptureScriptPath: scripts.cursorEditCaptureScript,
|
|
5980
|
-
bashFollowupScriptPath: scripts.cursorBashFollowupScript
|
|
7401
|
+
bashFollowupScriptPath: scripts.cursorBashFollowupScript,
|
|
7402
|
+
sessionStartScriptPath: scripts.cursorSessionStartScript
|
|
5981
7403
|
});
|
|
5982
7404
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
5983
7405
|
}
|
|
5984
7406
|
}
|
|
5985
7407
|
console.log();
|
|
5986
|
-
if (hasClaudeCode && !opts.noMcp) {
|
|
5987
|
-
try {
|
|
5988
|
-
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
5989
|
-
method: "POST",
|
|
5990
|
-
headers: {
|
|
5991
|
-
"Authorization": `Bearer ${token}`,
|
|
5992
|
-
"Content-Type": "application/json"
|
|
5993
|
-
},
|
|
5994
|
-
body: "{}"
|
|
5995
|
-
});
|
|
5996
|
-
if (!mintResp.ok) {
|
|
5997
|
-
const errText = await mintResp.text().catch(() => "");
|
|
5998
|
-
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
5999
|
-
}
|
|
6000
|
-
const minted = await mintResp.json();
|
|
6001
|
-
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
6002
|
-
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
6003
|
-
console.log(` url: ${mcp.url}`);
|
|
6004
|
-
console.log(` expires: ${minted.expires_at} (~1 year)`);
|
|
6005
|
-
console.log(" (restart any running Claude Code session for it to load)");
|
|
6006
|
-
console.log();
|
|
6007
|
-
} catch (err) {
|
|
6008
|
-
console.warn(` \u26A0 MCP registration failed: ${err.message}`);
|
|
6009
|
-
console.warn(" Hooks are still installed. Re-run `synkro-cli install` to retry MCP setup.");
|
|
6010
|
-
console.log();
|
|
6011
|
-
}
|
|
6012
|
-
}
|
|
6013
7408
|
let userId;
|
|
6014
7409
|
let orgId;
|
|
6015
7410
|
let email;
|
|
@@ -6021,6 +7416,50 @@ async function installCommand(opts = {}) {
|
|
|
6021
7416
|
} catch {
|
|
6022
7417
|
}
|
|
6023
7418
|
const profile = await fetchUserProfile(gatewayUrl, token);
|
|
7419
|
+
const useLocalMcp = profile.captureDepth === "local_only" || profile.localInference;
|
|
7420
|
+
if (hasClaudeCode && !opts.noMcp) {
|
|
7421
|
+
if (useLocalMcp) {
|
|
7422
|
+
try {
|
|
7423
|
+
const mcp = installMcpConfig({ gatewayUrl, bearerToken: "", local: true });
|
|
7424
|
+
console.log(`Registered local MCP guardrails server in ${mcp.path}`);
|
|
7425
|
+
console.log(` url: ${mcp.url}`);
|
|
7426
|
+
console.log(" (rules stored in ~/.synkro/rules.json)");
|
|
7427
|
+
console.log();
|
|
7428
|
+
await backfillLocalRules(gatewayUrl, token);
|
|
7429
|
+
await startLocalMcpServer();
|
|
7430
|
+
} catch (err) {
|
|
7431
|
+
console.warn(` \u26A0 Local MCP setup failed: ${err.message}`);
|
|
7432
|
+
console.warn(" Hooks are still installed. Re-run `synkro-cli install` to retry.");
|
|
7433
|
+
console.log();
|
|
7434
|
+
}
|
|
7435
|
+
} else {
|
|
7436
|
+
try {
|
|
7437
|
+
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
7438
|
+
method: "POST",
|
|
7439
|
+
headers: {
|
|
7440
|
+
"Authorization": `Bearer ${token}`,
|
|
7441
|
+
"Content-Type": "application/json"
|
|
7442
|
+
},
|
|
7443
|
+
body: "{}"
|
|
7444
|
+
});
|
|
7445
|
+
if (!mintResp.ok) {
|
|
7446
|
+
const errText = await mintResp.text().catch(() => "");
|
|
7447
|
+
throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
|
|
7448
|
+
}
|
|
7449
|
+
const minted = await mintResp.json();
|
|
7450
|
+
const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
|
|
7451
|
+
console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
|
|
7452
|
+
console.log(` url: ${mcp.url}`);
|
|
7453
|
+
console.log(` expires: ${minted.expires_at} (~1 year)`);
|
|
7454
|
+
console.log(" (restart any running Claude Code session for it to load)");
|
|
7455
|
+
console.log();
|
|
7456
|
+
} catch (err) {
|
|
7457
|
+
console.warn(` \u26A0 MCP registration failed: ${err.message}`);
|
|
7458
|
+
console.warn(" Hooks are still installed. Re-run `synkro-cli install` to retry MCP setup.");
|
|
7459
|
+
console.log();
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
6024
7463
|
const priorLocalFlag = (() => {
|
|
6025
7464
|
try {
|
|
6026
7465
|
const content = readFileSync10(CONFIG_PATH3, "utf-8");
|
|
@@ -6173,8 +7612,8 @@ function detectGitRepo2() {
|
|
|
6173
7612
|
function getClaudeProjectsFolder() {
|
|
6174
7613
|
const cwd = process.cwd();
|
|
6175
7614
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
6176
|
-
const projectsDir = join11(
|
|
6177
|
-
return
|
|
7615
|
+
const projectsDir = join11(homedir11(), ".claude", "projects", sanitized);
|
|
7616
|
+
return existsSync10(projectsDir) ? projectsDir : null;
|
|
6178
7617
|
}
|
|
6179
7618
|
function extractSessionInsights(projectsDir) {
|
|
6180
7619
|
const insights = [];
|
|
@@ -6350,7 +7789,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
6350
7789
|
}
|
|
6351
7790
|
return { sessions: totalSessions, messages: totalMessages };
|
|
6352
7791
|
}
|
|
6353
|
-
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, OFFSETS_DIR;
|
|
7792
|
+
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, OFFSETS_DIR, RULES_PATH, MCP_LOCAL_PORT;
|
|
6354
7793
|
var init_install2 = __esm({
|
|
6355
7794
|
"cli/commands/install.ts"() {
|
|
6356
7795
|
"use strict";
|
|
@@ -6369,11 +7808,13 @@ var init_install2 = __esm({
|
|
|
6369
7808
|
init_install();
|
|
6370
7809
|
init_pueue();
|
|
6371
7810
|
init_client();
|
|
6372
|
-
SYNKRO_DIR2 = join11(
|
|
7811
|
+
SYNKRO_DIR2 = join11(homedir11(), ".synkro");
|
|
6373
7812
|
HOOKS_DIR = join11(SYNKRO_DIR2, "hooks");
|
|
6374
7813
|
BIN_DIR = join11(SYNKRO_DIR2, "bin");
|
|
6375
7814
|
CONFIG_PATH3 = join11(SYNKRO_DIR2, "config.env");
|
|
6376
7815
|
OFFSETS_DIR = join11(SYNKRO_DIR2, ".transcript-offsets");
|
|
7816
|
+
RULES_PATH = join11(SYNKRO_DIR2, "rules.json");
|
|
7817
|
+
MCP_LOCAL_PORT = 8931;
|
|
6377
7818
|
}
|
|
6378
7819
|
});
|
|
6379
7820
|
|
|
@@ -6449,11 +7890,11 @@ var status_exports = {};
|
|
|
6449
7890
|
__export(status_exports, {
|
|
6450
7891
|
statusCommand: () => statusCommand
|
|
6451
7892
|
});
|
|
6452
|
-
import { existsSync as
|
|
6453
|
-
import { homedir as
|
|
7893
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
7894
|
+
import { homedir as homedir12 } from "os";
|
|
6454
7895
|
import { join as join12 } from "path";
|
|
6455
7896
|
function readConfigEnv() {
|
|
6456
|
-
if (!
|
|
7897
|
+
if (!existsSync11(CONFIG_PATH4)) return {};
|
|
6457
7898
|
const out = {};
|
|
6458
7899
|
const raw = readFileSync11(CONFIG_PATH4, "utf-8");
|
|
6459
7900
|
for (const line of raw.split("\n")) {
|
|
@@ -6562,12 +8003,12 @@ async function statusCommand() {
|
|
|
6562
8003
|
console.log("Hook scripts (Claude Code):");
|
|
6563
8004
|
for (const f of ccHooks) {
|
|
6564
8005
|
const p = join12(HOOKS_DIR2, f);
|
|
6565
|
-
console.log(` ${
|
|
8006
|
+
console.log(` ${existsSync11(p) ? "\u2713" : "\u2717"} ${p}`);
|
|
6566
8007
|
}
|
|
6567
8008
|
console.log("Hook scripts (Cursor):");
|
|
6568
8009
|
for (const f of cursorHooks) {
|
|
6569
8010
|
const p = join12(HOOKS_DIR2, f);
|
|
6570
|
-
console.log(` ${
|
|
8011
|
+
console.log(` ${existsSync11(p) ? "\u2713" : "\u2717"} ${p}`);
|
|
6571
8012
|
}
|
|
6572
8013
|
console.log();
|
|
6573
8014
|
if (localInference) {
|
|
@@ -6610,7 +8051,7 @@ var init_status = __esm({
|
|
|
6610
8051
|
init_cursorHookConfig();
|
|
6611
8052
|
init_mcpConfig();
|
|
6612
8053
|
init_pueue();
|
|
6613
|
-
SYNKRO_DIR3 = join12(
|
|
8054
|
+
SYNKRO_DIR3 = join12(homedir12(), ".synkro");
|
|
6614
8055
|
CONFIG_PATH4 = join12(SYNKRO_DIR3, "config.env");
|
|
6615
8056
|
}
|
|
6616
8057
|
});
|
|
@@ -6643,7 +8084,7 @@ __export(unlink_exports, {
|
|
|
6643
8084
|
});
|
|
6644
8085
|
import { createInterface as createInterface4 } from "readline";
|
|
6645
8086
|
function ask2(rl, question) {
|
|
6646
|
-
return new Promise((
|
|
8087
|
+
return new Promise((resolve3) => rl.question(question, resolve3));
|
|
6647
8088
|
}
|
|
6648
8089
|
async function unlinkCommand() {
|
|
6649
8090
|
if (!isAuthenticated()) {
|
|
@@ -6700,11 +8141,11 @@ var config_exports = {};
|
|
|
6700
8141
|
__export(config_exports, {
|
|
6701
8142
|
configCommand: () => configCommand
|
|
6702
8143
|
});
|
|
6703
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as
|
|
8144
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12 } from "fs";
|
|
6704
8145
|
import { join as join13 } from "path";
|
|
6705
|
-
import { homedir as
|
|
8146
|
+
import { homedir as homedir13 } from "os";
|
|
6706
8147
|
function readConfigEnv2() {
|
|
6707
|
-
if (!
|
|
8148
|
+
if (!existsSync12(CONFIG_PATH5)) return {};
|
|
6708
8149
|
const out = {};
|
|
6709
8150
|
for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
6710
8151
|
const t = line.trim();
|
|
@@ -6715,7 +8156,7 @@ function readConfigEnv2() {
|
|
|
6715
8156
|
return out;
|
|
6716
8157
|
}
|
|
6717
8158
|
function updateConfigValue(key, value) {
|
|
6718
|
-
if (!
|
|
8159
|
+
if (!existsSync12(CONFIG_PATH5)) {
|
|
6719
8160
|
console.error("No config found. Run `synkro install` first.");
|
|
6720
8161
|
process.exit(1);
|
|
6721
8162
|
}
|
|
@@ -6786,7 +8227,7 @@ var init_config = __esm({
|
|
|
6786
8227
|
"cli/commands/config.ts"() {
|
|
6787
8228
|
"use strict";
|
|
6788
8229
|
init_stub();
|
|
6789
|
-
SYNKRO_DIR4 = join13(
|
|
8230
|
+
SYNKRO_DIR4 = join13(homedir13(), ".synkro");
|
|
6790
8231
|
CONFIG_PATH5 = join13(SYNKRO_DIR4, "config.env");
|
|
6791
8232
|
}
|
|
6792
8233
|
});
|
|
@@ -6796,8 +8237,8 @@ var scanPr_exports = {};
|
|
|
6796
8237
|
__export(scanPr_exports, {
|
|
6797
8238
|
scanPrCommand: () => scanPrCommand
|
|
6798
8239
|
});
|
|
6799
|
-
import { execSync as execSync6, spawn as
|
|
6800
|
-
import { readFileSync as readFileSync13, existsSync as
|
|
8240
|
+
import { execSync as execSync6, spawn as spawn3 } from "child_process";
|
|
8241
|
+
import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
|
|
6801
8242
|
import { join as join14 } from "path";
|
|
6802
8243
|
function parseMatchSpec(condition) {
|
|
6803
8244
|
if (!condition.startsWith("match_spec:")) return null;
|
|
@@ -7005,9 +8446,9 @@ function spawnClaudeJudge(file, claudeToken, promptHeader) {
|
|
|
7005
8446
|
Diff:
|
|
7006
8447
|
${hunks}`;
|
|
7007
8448
|
const fullPrompt = promptHeader + userMessage;
|
|
7008
|
-
return new Promise((
|
|
8449
|
+
return new Promise((resolve3) => {
|
|
7009
8450
|
const t0 = Date.now();
|
|
7010
|
-
const proc =
|
|
8451
|
+
const proc = spawn3(
|
|
7011
8452
|
"claude",
|
|
7012
8453
|
["--print", "--model", "claude-sonnet-4-6", "--output-format", "json", "--no-session-persistence"],
|
|
7013
8454
|
{
|
|
@@ -7033,7 +8474,7 @@ ${hunks}`;
|
|
|
7033
8474
|
const latencyMs = Date.now() - t0;
|
|
7034
8475
|
if (code !== 0) {
|
|
7035
8476
|
console.warn(` claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`);
|
|
7036
|
-
|
|
8477
|
+
resolve3({ findings: [], latencyMs });
|
|
7037
8478
|
return;
|
|
7038
8479
|
}
|
|
7039
8480
|
try {
|
|
@@ -7052,10 +8493,10 @@ ${hunks}`;
|
|
|
7052
8493
|
description: f.description,
|
|
7053
8494
|
fix: f.fix
|
|
7054
8495
|
}));
|
|
7055
|
-
|
|
8496
|
+
resolve3({ findings, latencyMs });
|
|
7056
8497
|
} catch (parseErr) {
|
|
7057
8498
|
console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
|
|
7058
|
-
|
|
8499
|
+
resolve3({ findings: [], latencyMs });
|
|
7059
8500
|
}
|
|
7060
8501
|
});
|
|
7061
8502
|
});
|
|
@@ -7104,9 +8545,9 @@ ${JSON.stringify(findings, null, 2)}
|
|
|
7104
8545
|
`;
|
|
7105
8546
|
}
|
|
7106
8547
|
function spawnOpusConsolidator(findings, claudeToken) {
|
|
7107
|
-
return new Promise((
|
|
8548
|
+
return new Promise((resolve3) => {
|
|
7108
8549
|
const prompt2 = buildConsolidationPrompt(findings);
|
|
7109
|
-
const proc =
|
|
8550
|
+
const proc = spawn3(
|
|
7110
8551
|
"claude",
|
|
7111
8552
|
["--print", "--model", "claude-opus-4-7", "--output-format", "json", "--no-session-persistence"],
|
|
7112
8553
|
{
|
|
@@ -7131,7 +8572,7 @@ function spawnOpusConsolidator(findings, claudeToken) {
|
|
|
7131
8572
|
proc.on("close", (code) => {
|
|
7132
8573
|
if (code !== 0) {
|
|
7133
8574
|
console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
|
|
7134
|
-
|
|
8575
|
+
resolve3(fallbackReview(findings));
|
|
7135
8576
|
return;
|
|
7136
8577
|
}
|
|
7137
8578
|
try {
|
|
@@ -7152,10 +8593,10 @@ function spawnOpusConsolidator(findings, claudeToken) {
|
|
|
7152
8593
|
const order = ["low", "medium", "high", "critical"];
|
|
7153
8594
|
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
7154
8595
|
}, "low");
|
|
7155
|
-
|
|
8596
|
+
resolve3({ summary: review.summary || "", comments, severity: maxSeverity });
|
|
7156
8597
|
} catch {
|
|
7157
8598
|
console.warn(` failed to parse opus response, using fallback`);
|
|
7158
|
-
|
|
8599
|
+
resolve3(fallbackReview(findings));
|
|
7159
8600
|
}
|
|
7160
8601
|
});
|
|
7161
8602
|
});
|
|
@@ -7278,7 +8719,7 @@ function shouldFail(findings, threshold) {
|
|
|
7278
8719
|
}
|
|
7279
8720
|
function readRepoDeps() {
|
|
7280
8721
|
const pkgPath = join14(process.cwd(), "package.json");
|
|
7281
|
-
if (!
|
|
8722
|
+
if (!existsSync13(pkgPath)) return {};
|
|
7282
8723
|
try {
|
|
7283
8724
|
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
7284
8725
|
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
@@ -7542,8 +8983,8 @@ var disconnect_exports = {};
|
|
|
7542
8983
|
__export(disconnect_exports, {
|
|
7543
8984
|
disconnectCommand: () => disconnectCommand
|
|
7544
8985
|
});
|
|
7545
|
-
import { existsSync as
|
|
7546
|
-
import { homedir as
|
|
8986
|
+
import { existsSync as existsSync14, rmSync } from "fs";
|
|
8987
|
+
import { homedir as homedir14 } from "os";
|
|
7547
8988
|
import { join as join15 } from "path";
|
|
7548
8989
|
function tearDownLocalCC() {
|
|
7549
8990
|
let hadTask = false;
|
|
@@ -7580,13 +9021,13 @@ function disconnectCommand(args2 = []) {
|
|
|
7580
9021
|
console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
|
|
7581
9022
|
}
|
|
7582
9023
|
if (purge) {
|
|
7583
|
-
if (
|
|
9024
|
+
if (existsSync14(SYNKRO_DIR5)) {
|
|
7584
9025
|
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
7585
9026
|
console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
|
|
7586
9027
|
} else {
|
|
7587
9028
|
console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
|
|
7588
9029
|
}
|
|
7589
|
-
} else if (
|
|
9030
|
+
} else if (existsSync14(SYNKRO_DIR5)) {
|
|
7590
9031
|
console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
|
|
7591
9032
|
}
|
|
7592
9033
|
console.log("\nSynkro disconnected.");
|
|
@@ -7601,7 +9042,7 @@ var init_disconnect = __esm({
|
|
|
7601
9042
|
init_mcpConfig();
|
|
7602
9043
|
init_pueue();
|
|
7603
9044
|
init_install();
|
|
7604
|
-
SYNKRO_DIR5 = join15(
|
|
9045
|
+
SYNKRO_DIR5 = join15(homedir14(), ".synkro");
|
|
7605
9046
|
}
|
|
7606
9047
|
});
|
|
7607
9048
|
|
|
@@ -7648,9 +9089,9 @@ __export(localCc_exports, {
|
|
|
7648
9089
|
localCcCommand: () => localCcCommand
|
|
7649
9090
|
});
|
|
7650
9091
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7651
|
-
import { homedir as
|
|
9092
|
+
import { homedir as homedir15 } from "os";
|
|
7652
9093
|
import { join as join16 } from "path";
|
|
7653
|
-
import { existsSync as
|
|
9094
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
7654
9095
|
function printHelp() {
|
|
7655
9096
|
console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
|
|
7656
9097
|
|
|
@@ -7740,14 +9181,14 @@ TROUBLESHOOTING
|
|
|
7740
9181
|
`);
|
|
7741
9182
|
}
|
|
7742
9183
|
function readGatewayUrl() {
|
|
7743
|
-
if (
|
|
9184
|
+
if (existsSync15(CONFIG_PATH6)) {
|
|
7744
9185
|
const m = readFileSync14(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
7745
9186
|
if (m) return m[1];
|
|
7746
9187
|
}
|
|
7747
9188
|
return "https://api.synkro.sh";
|
|
7748
9189
|
}
|
|
7749
9190
|
function updateLocalInferenceFlag2(enabled) {
|
|
7750
|
-
if (!
|
|
9191
|
+
if (!existsSync15(CONFIG_PATH6)) return;
|
|
7751
9192
|
let content = readFileSync14(CONFIG_PATH6, "utf-8");
|
|
7752
9193
|
const flag = enabled ? "yes" : "no";
|
|
7753
9194
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
@@ -7977,7 +9418,7 @@ function cmdLogs(rest) {
|
|
|
7977
9418
|
if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
|
|
7978
9419
|
return;
|
|
7979
9420
|
}
|
|
7980
|
-
return new Promise((
|
|
9421
|
+
return new Promise((resolve3) => {
|
|
7981
9422
|
console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
|
|
7982
9423
|
const stop = followTurns((t) => {
|
|
7983
9424
|
console.log(" " + formatTurn(t, raw));
|
|
@@ -7985,7 +9426,7 @@ function cmdLogs(rest) {
|
|
|
7985
9426
|
const onSigint = () => {
|
|
7986
9427
|
stop();
|
|
7987
9428
|
process.removeListener("SIGINT", onSigint);
|
|
7988
|
-
|
|
9429
|
+
resolve3();
|
|
7989
9430
|
};
|
|
7990
9431
|
process.on("SIGINT", onSigint);
|
|
7991
9432
|
});
|
|
@@ -8082,7 +9523,7 @@ var init_localCc = __esm({
|
|
|
8082
9523
|
init_settings();
|
|
8083
9524
|
init_client();
|
|
8084
9525
|
init_stub();
|
|
8085
|
-
CONFIG_PATH6 = join16(
|
|
9526
|
+
CONFIG_PATH6 = join16(homedir15(), ".synkro", "config.env");
|
|
8086
9527
|
}
|
|
8087
9528
|
});
|
|
8088
9529
|
|
|
@@ -8092,10 +9533,10 @@ __export(grade_exports, {
|
|
|
8092
9533
|
gradeCommand: () => gradeCommand
|
|
8093
9534
|
});
|
|
8094
9535
|
async function readStdin() {
|
|
8095
|
-
return new Promise((
|
|
9536
|
+
return new Promise((resolve3, reject) => {
|
|
8096
9537
|
const chunks = [];
|
|
8097
9538
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
8098
|
-
process.stdin.on("end", () =>
|
|
9539
|
+
process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
|
|
8099
9540
|
process.stdin.on("error", reject);
|
|
8100
9541
|
});
|
|
8101
9542
|
}
|
|
@@ -8136,14 +9577,14 @@ var init_grade = __esm({
|
|
|
8136
9577
|
});
|
|
8137
9578
|
|
|
8138
9579
|
// cli/bootstrap.js
|
|
8139
|
-
import { readFileSync as readFileSync15, existsSync as
|
|
8140
|
-
import { resolve } from "path";
|
|
9580
|
+
import { readFileSync as readFileSync15, existsSync as existsSync16 } from "fs";
|
|
9581
|
+
import { resolve as resolve2 } from "path";
|
|
8141
9582
|
var envCandidates = [
|
|
8142
|
-
|
|
8143
|
-
|
|
9583
|
+
resolve2(process.cwd(), ".env"),
|
|
9584
|
+
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
8144
9585
|
];
|
|
8145
9586
|
for (const envPath of envCandidates) {
|
|
8146
|
-
if (!
|
|
9587
|
+
if (!existsSync16(envPath)) continue;
|
|
8147
9588
|
const envContent = readFileSync15(envPath, "utf-8");
|
|
8148
9589
|
for (const line of envContent.split("\n")) {
|
|
8149
9590
|
const trimmed = line.trim();
|