@synkro-sh/cli 1.6.32 → 1.6.34
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 +330 -109
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -601,6 +601,58 @@ var init_mcpConfig = __esm({
|
|
|
601
601
|
}
|
|
602
602
|
});
|
|
603
603
|
|
|
604
|
+
// cli/installer/skillParser.ts
|
|
605
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
606
|
+
import { resolve as resolve2, basename } from "path";
|
|
607
|
+
function parseSection(heading, body) {
|
|
608
|
+
const lines = body.split("\n");
|
|
609
|
+
const meta = {};
|
|
610
|
+
const textLines = [];
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
const m = line.match(/^(mode|severity|category)\s*:\s*(.+)/i);
|
|
613
|
+
if (m && META_KEYS.has(m[1].toLowerCase())) {
|
|
614
|
+
meta[m[1].toLowerCase()] = m[2].trim();
|
|
615
|
+
} else if (line.trim()) {
|
|
616
|
+
textLines.push(line.trim());
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const description = textLines.join(" ").trim();
|
|
620
|
+
const text = description ? `${heading}: ${description}` : heading;
|
|
621
|
+
return {
|
|
622
|
+
text,
|
|
623
|
+
mode: meta.mode || "ask",
|
|
624
|
+
severity: meta.severity || "medium",
|
|
625
|
+
category: meta.category || "custom"
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function parseSkillFile(filePath) {
|
|
629
|
+
if (!existsSync4(filePath)) return null;
|
|
630
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
631
|
+
const source = `skill:${basename(filePath)}`;
|
|
632
|
+
const sections = content.split(/^## /m).slice(1);
|
|
633
|
+
if (sections.length === 0) return null;
|
|
634
|
+
const rules = [];
|
|
635
|
+
for (const section of sections) {
|
|
636
|
+
const newlineIdx = section.indexOf("\n");
|
|
637
|
+
if (newlineIdx === -1) continue;
|
|
638
|
+
const heading = section.slice(0, newlineIdx).trim();
|
|
639
|
+
const body = section.slice(newlineIdx + 1);
|
|
640
|
+
if (!heading) continue;
|
|
641
|
+
rules.push(parseSection(heading, body));
|
|
642
|
+
}
|
|
643
|
+
return rules.length > 0 ? { source, rules } : null;
|
|
644
|
+
}
|
|
645
|
+
function resolveSkillPaths(skills, repoRoot) {
|
|
646
|
+
return skills.filter((s) => s.endsWith(".md")).map((s) => resolve2(repoRoot, s)).filter((p) => existsSync4(p));
|
|
647
|
+
}
|
|
648
|
+
var META_KEYS;
|
|
649
|
+
var init_skillParser = __esm({
|
|
650
|
+
"cli/installer/skillParser.ts"() {
|
|
651
|
+
"use strict";
|
|
652
|
+
META_KEYS = /* @__PURE__ */ new Set(["mode", "severity", "category"]);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
604
656
|
// cli/installer/hookScripts.ts
|
|
605
657
|
var SYNKRO_COMMON_SCRIPT;
|
|
606
658
|
var init_hookScripts = __esm({
|
|
@@ -1144,15 +1196,19 @@ export function findGitRoot(cwd: string): string {
|
|
|
1144
1196
|
|
|
1145
1197
|
export interface SynkroFileConfig {
|
|
1146
1198
|
version: number;
|
|
1199
|
+
harness: ('claude-code' | 'cursor')[];
|
|
1147
1200
|
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
|
|
1148
1201
|
ruleset: string;
|
|
1202
|
+
skills: string[];
|
|
1149
1203
|
scanning: { cwe: boolean; cve: boolean };
|
|
1150
1204
|
}
|
|
1151
1205
|
|
|
1152
1206
|
const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
|
|
1153
1207
|
version: 1,
|
|
1208
|
+
harness: ['claude-code', 'cursor'],
|
|
1154
1209
|
grader: { pool: 'auto' },
|
|
1155
1210
|
ruleset: 'default',
|
|
1211
|
+
skills: [],
|
|
1156
1212
|
scanning: { cwe: true, cve: true },
|
|
1157
1213
|
};
|
|
1158
1214
|
|
|
@@ -1166,13 +1222,19 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
|
1166
1222
|
try {
|
|
1167
1223
|
if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1168
1224
|
const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
1225
|
+
const validHarness = ['claude-code', 'cursor'] as const;
|
|
1226
|
+
const harness = Array.isArray(parsed.harness)
|
|
1227
|
+
? parsed.harness.filter((h: string) => validHarness.includes(h as any))
|
|
1228
|
+
: ['claude-code', 'cursor'];
|
|
1169
1229
|
_synkroFileCache = {
|
|
1170
1230
|
version: parsed.version || 1,
|
|
1231
|
+
harness: harness.length > 0 ? harness : ['claude-code', 'cursor'],
|
|
1171
1232
|
grader: {
|
|
1172
1233
|
pool: ['auto', 'claude', 'cursor'].includes(parsed.grader?.pool) ? parsed.grader.pool : 'auto',
|
|
1173
1234
|
mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
|
|
1174
1235
|
},
|
|
1175
1236
|
ruleset: typeof parsed.ruleset === 'string' ? parsed.ruleset : 'default',
|
|
1237
|
+
skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s: unknown) => typeof s === 'string') : [],
|
|
1176
1238
|
scanning: {
|
|
1177
1239
|
cwe: parsed.scanning?.cwe !== false,
|
|
1178
1240
|
cve: parsed.scanning?.cve !== false,
|
|
@@ -5939,7 +6001,7 @@ __export(stub_exports, {
|
|
|
5939
6001
|
saveCredentials: () => saveCredentials
|
|
5940
6002
|
});
|
|
5941
6003
|
import { createServer } from "http";
|
|
5942
|
-
import { writeFileSync as writeFileSync4, readFileSync as
|
|
6004
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
5943
6005
|
import { homedir as homedir4, platform } from "os";
|
|
5944
6006
|
import { join as join3, dirname as dirname4 } from "path";
|
|
5945
6007
|
import { execFile } from "child_process";
|
|
@@ -5970,17 +6032,17 @@ function openBrowser(url) {
|
|
|
5970
6032
|
}
|
|
5971
6033
|
function saveCredentials(data) {
|
|
5972
6034
|
const dir = dirname4(AUTH_FILE);
|
|
5973
|
-
if (!
|
|
6035
|
+
if (!existsSync5(dir)) {
|
|
5974
6036
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
5975
6037
|
}
|
|
5976
6038
|
writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5977
6039
|
}
|
|
5978
6040
|
function loadCredentials() {
|
|
5979
|
-
if (!
|
|
6041
|
+
if (!existsSync5(AUTH_FILE)) {
|
|
5980
6042
|
return null;
|
|
5981
6043
|
}
|
|
5982
6044
|
try {
|
|
5983
|
-
const content =
|
|
6045
|
+
const content = readFileSync5(AUTH_FILE, "utf8");
|
|
5984
6046
|
return JSON.parse(content);
|
|
5985
6047
|
} catch (error) {
|
|
5986
6048
|
return null;
|
|
@@ -5993,7 +6055,7 @@ function createCallbackServer() {
|
|
|
5993
6055
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
5994
6056
|
"Vary": "Origin"
|
|
5995
6057
|
};
|
|
5996
|
-
return new Promise((
|
|
6058
|
+
return new Promise((resolve4, reject) => {
|
|
5997
6059
|
const server = createServer((req, res) => {
|
|
5998
6060
|
if (req.method === "OPTIONS") {
|
|
5999
6061
|
const origin = req.headers.origin;
|
|
@@ -6082,7 +6144,7 @@ function createCallbackServer() {
|
|
|
6082
6144
|
res.end(JSON.stringify({ ok: true }));
|
|
6083
6145
|
setTimeout(() => {
|
|
6084
6146
|
server.close();
|
|
6085
|
-
|
|
6147
|
+
resolve4(authData);
|
|
6086
6148
|
}, 200);
|
|
6087
6149
|
});
|
|
6088
6150
|
req.on("error", (e) => {
|
|
@@ -6235,7 +6297,7 @@ async function ensureValidToken() {
|
|
|
6235
6297
|
return true;
|
|
6236
6298
|
}
|
|
6237
6299
|
function clearCredentials() {
|
|
6238
|
-
if (
|
|
6300
|
+
if (existsSync5(AUTH_FILE)) {
|
|
6239
6301
|
unlinkSync2(AUTH_FILE);
|
|
6240
6302
|
}
|
|
6241
6303
|
}
|
|
@@ -6415,7 +6477,7 @@ jobs:
|
|
|
6415
6477
|
});
|
|
6416
6478
|
|
|
6417
6479
|
// cli/installer/githubSetup.ts
|
|
6418
|
-
import { existsSync as
|
|
6480
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
6419
6481
|
import { execSync as execSync2 } from "child_process";
|
|
6420
6482
|
import { join as join4 } from "path";
|
|
6421
6483
|
function ghSecretSet(token, owner, repo, name, value) {
|
|
@@ -6472,7 +6534,7 @@ function writeWorkflowFile(repoRootPath) {
|
|
|
6472
6534
|
function findGitRoot(startCwd) {
|
|
6473
6535
|
let cur = startCwd;
|
|
6474
6536
|
while (cur && cur !== "/") {
|
|
6475
|
-
if (
|
|
6537
|
+
if (existsSync6(join4(cur, ".git"))) return cur;
|
|
6476
6538
|
const parent = join4(cur, "..");
|
|
6477
6539
|
if (parent === cur) break;
|
|
6478
6540
|
cur = parent;
|
|
@@ -6538,10 +6600,10 @@ function detectSubdirRepos() {
|
|
|
6538
6600
|
}
|
|
6539
6601
|
}
|
|
6540
6602
|
function ask(rl, question) {
|
|
6541
|
-
return new Promise((
|
|
6603
|
+
return new Promise((resolve4) => rl.question(question, resolve4));
|
|
6542
6604
|
}
|
|
6543
6605
|
function waitForGithubToken() {
|
|
6544
|
-
return new Promise((
|
|
6606
|
+
return new Promise((resolve4, reject) => {
|
|
6545
6607
|
const server = createServer2((req, res) => {
|
|
6546
6608
|
if (req.method === "OPTIONS") {
|
|
6547
6609
|
res.writeHead(204, {
|
|
@@ -6578,7 +6640,7 @@ function waitForGithubToken() {
|
|
|
6578
6640
|
});
|
|
6579
6641
|
res.end(JSON.stringify({ ok: true }));
|
|
6580
6642
|
setTimeout(() => server.close(), 200);
|
|
6581
|
-
|
|
6643
|
+
resolve4(parsed.github_token);
|
|
6582
6644
|
} catch {
|
|
6583
6645
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6584
6646
|
res.end(JSON.stringify({ error: "invalid json" }));
|
|
@@ -6838,7 +6900,7 @@ __export(macKeychain_exports, {
|
|
|
6838
6900
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
6839
6901
|
writeRefreshAgent: () => writeRefreshAgent
|
|
6840
6902
|
});
|
|
6841
|
-
import { existsSync as
|
|
6903
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
|
|
6842
6904
|
import { homedir as homedir5, platform as platform2 } from "os";
|
|
6843
6905
|
import { join as join5 } from "path";
|
|
6844
6906
|
import { spawnSync } from "child_process";
|
|
@@ -6866,7 +6928,7 @@ function exportKeychainCreds() {
|
|
|
6866
6928
|
}
|
|
6867
6929
|
function cursorApiKeyConfigured() {
|
|
6868
6930
|
try {
|
|
6869
|
-
return
|
|
6931
|
+
return existsSync7(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
|
|
6870
6932
|
} catch {
|
|
6871
6933
|
return false;
|
|
6872
6934
|
}
|
|
@@ -6882,7 +6944,7 @@ function writeCursorApiKey(key) {
|
|
|
6882
6944
|
async function validateCursorApiKey() {
|
|
6883
6945
|
let key;
|
|
6884
6946
|
try {
|
|
6885
|
-
key =
|
|
6947
|
+
key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
|
|
6886
6948
|
} catch {
|
|
6887
6949
|
return null;
|
|
6888
6950
|
}
|
|
@@ -6901,7 +6963,7 @@ async function validateCursorApiKey() {
|
|
|
6901
6963
|
}
|
|
6902
6964
|
}
|
|
6903
6965
|
function credsAreStale() {
|
|
6904
|
-
if (!
|
|
6966
|
+
if (!existsSync7(CLAUDE_CREDS_FILE)) return true;
|
|
6905
6967
|
try {
|
|
6906
6968
|
const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
|
|
6907
6969
|
return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
|
|
@@ -6964,7 +7026,7 @@ function uninstallRefreshAgent() {
|
|
|
6964
7026
|
timeout: 5e3
|
|
6965
7027
|
});
|
|
6966
7028
|
try {
|
|
6967
|
-
if (
|
|
7029
|
+
if (existsSync7(LAUNCHD_PLIST)) {
|
|
6968
7030
|
__require("fs").unlinkSync(LAUNCHD_PLIST);
|
|
6969
7031
|
}
|
|
6970
7032
|
} catch {
|
|
@@ -6976,7 +7038,7 @@ function refreshCreds() {
|
|
|
6976
7038
|
}
|
|
6977
7039
|
function readExportedCreds() {
|
|
6978
7040
|
try {
|
|
6979
|
-
return
|
|
7041
|
+
return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
|
|
6980
7042
|
} catch {
|
|
6981
7043
|
return null;
|
|
6982
7044
|
}
|
|
@@ -7025,7 +7087,7 @@ __export(dockerInstall_exports, {
|
|
|
7025
7087
|
splitWorkers: () => splitWorkers,
|
|
7026
7088
|
waitForContainerReady: () => waitForContainerReady
|
|
7027
7089
|
});
|
|
7028
|
-
import { copyFileSync, existsSync as
|
|
7090
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
7029
7091
|
import { homedir as homedir6 } from "os";
|
|
7030
7092
|
import { join as join6 } from "path";
|
|
7031
7093
|
import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
|
|
@@ -7051,8 +7113,8 @@ function readSynkroFilePool() {
|
|
|
7051
7113
|
const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7052
7114
|
if (!root) return "auto";
|
|
7053
7115
|
const fp = join6(root, ".synkro");
|
|
7054
|
-
if (!
|
|
7055
|
-
const parsed = JSON.parse(
|
|
7116
|
+
if (!existsSync8(fp)) return "auto";
|
|
7117
|
+
const parsed = JSON.parse(readFileSync7(fp, "utf-8"));
|
|
7056
7118
|
const pool = parsed?.grader?.pool;
|
|
7057
7119
|
if (pool === "cursor" || pool === "claude") return pool;
|
|
7058
7120
|
} catch {
|
|
@@ -7179,10 +7241,10 @@ async function dockerInstall(opts = {}) {
|
|
|
7179
7241
|
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
7180
7242
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
7181
7243
|
const hostClaudeJson = join6(homedir6(), ".claude.json");
|
|
7182
|
-
if (
|
|
7244
|
+
if (existsSync8(hostClaudeJson)) {
|
|
7183
7245
|
copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
|
|
7184
7246
|
}
|
|
7185
|
-
if (!
|
|
7247
|
+
if (!existsSync8(MCP_JWT_PATH)) {
|
|
7186
7248
|
throw new DockerInstallError(
|
|
7187
7249
|
`MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
|
|
7188
7250
|
);
|
|
@@ -7417,7 +7479,7 @@ async function dockerSafeStart() {
|
|
|
7417
7479
|
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
7418
7480
|
}
|
|
7419
7481
|
const pgCheck = checkPgdata();
|
|
7420
|
-
if (
|
|
7482
|
+
if (existsSync8(PGDATA_PATH) && readdirSync2(PGDATA_PATH).length > 0) {
|
|
7421
7483
|
if (pgCheck.healthy) {
|
|
7422
7484
|
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
7423
7485
|
} else {
|
|
@@ -7457,7 +7519,7 @@ async function dockerSafeRestart() {
|
|
|
7457
7519
|
return { ok: startResult.ok, stop: stopResult, start: startResult };
|
|
7458
7520
|
}
|
|
7459
7521
|
function checkPgdata() {
|
|
7460
|
-
if (!
|
|
7522
|
+
if (!existsSync8(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
7461
7523
|
const entries = readdirSync2(PGDATA_PATH);
|
|
7462
7524
|
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
7463
7525
|
const hasPidFile = entries.includes("postmaster.pid");
|
|
@@ -7506,14 +7568,14 @@ __export(setupGithub_exports, {
|
|
|
7506
7568
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7507
7569
|
import { stdin as input, stdout as output } from "process";
|
|
7508
7570
|
import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
|
|
7509
|
-
import { existsSync as
|
|
7571
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
7510
7572
|
import { homedir as homedir7, platform as platform3 } from "os";
|
|
7511
7573
|
import { join as join7 } from "path";
|
|
7512
7574
|
import { execFile as execFile2 } from "child_process";
|
|
7513
7575
|
function readConfig() {
|
|
7514
|
-
if (!
|
|
7576
|
+
if (!existsSync9(CONFIG_PATH)) return {};
|
|
7515
7577
|
const out = {};
|
|
7516
|
-
for (const line of
|
|
7578
|
+
for (const line of readFileSync8(CONFIG_PATH, "utf-8").split("\n")) {
|
|
7517
7579
|
const t = line.trim();
|
|
7518
7580
|
if (!t || t.startsWith("#")) continue;
|
|
7519
7581
|
const eq = t.indexOf("=");
|
|
@@ -7526,7 +7588,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
7526
7588
|
process.stdout.write(q);
|
|
7527
7589
|
const wasRaw = process.stdin.isRaw;
|
|
7528
7590
|
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
7529
|
-
return await new Promise((
|
|
7591
|
+
return await new Promise((resolve4) => {
|
|
7530
7592
|
let chunk = "";
|
|
7531
7593
|
const onData = (data) => {
|
|
7532
7594
|
const s = data.toString("utf-8");
|
|
@@ -7534,7 +7596,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
7534
7596
|
process.stdin.removeListener("data", onData);
|
|
7535
7597
|
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
7536
7598
|
process.stdout.write("\n");
|
|
7537
|
-
|
|
7599
|
+
resolve4(chunk);
|
|
7538
7600
|
return;
|
|
7539
7601
|
}
|
|
7540
7602
|
if (s === "") process.exit(130);
|
|
@@ -7575,7 +7637,7 @@ function sleep(ms) {
|
|
|
7575
7637
|
}
|
|
7576
7638
|
function captureClaudeSetupToken() {
|
|
7577
7639
|
const tmpFile = join7(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
|
|
7578
|
-
return new Promise((
|
|
7640
|
+
return new Promise((resolve4, reject) => {
|
|
7579
7641
|
const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
|
|
7580
7642
|
stdio: "inherit"
|
|
7581
7643
|
});
|
|
@@ -7583,7 +7645,7 @@ function captureClaudeSetupToken() {
|
|
|
7583
7645
|
proc.on("close", (code) => {
|
|
7584
7646
|
let raw = "";
|
|
7585
7647
|
try {
|
|
7586
|
-
raw =
|
|
7648
|
+
raw = readFileSync8(tmpFile, "utf-8");
|
|
7587
7649
|
} catch (e) {
|
|
7588
7650
|
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
7589
7651
|
return;
|
|
@@ -7605,7 +7667,7 @@ function captureClaudeSetupToken() {
|
|
|
7605
7667
|
reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
|
|
7606
7668
|
return;
|
|
7607
7669
|
}
|
|
7608
|
-
|
|
7670
|
+
resolve4(token[0]);
|
|
7609
7671
|
});
|
|
7610
7672
|
});
|
|
7611
7673
|
}
|
|
@@ -7865,9 +7927,12 @@ var install_exports = {};
|
|
|
7865
7927
|
__export(install_exports, {
|
|
7866
7928
|
detectGitRepo: () => detectGitRepo2,
|
|
7867
7929
|
installCommand: () => installCommand,
|
|
7868
|
-
parseArgs: () => parseArgs
|
|
7930
|
+
parseArgs: () => parseArgs,
|
|
7931
|
+
reconcileHarness: () => reconcileHarness,
|
|
7932
|
+
syncSkillFiles: () => syncSkillFiles,
|
|
7933
|
+
writeHookScripts: () => writeHookScripts
|
|
7869
7934
|
});
|
|
7870
|
-
import { existsSync as
|
|
7935
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
|
|
7871
7936
|
import { homedir as homedir8 } from "os";
|
|
7872
7937
|
import { join as join8 } from "path";
|
|
7873
7938
|
import { execSync as execSync6, spawnSync as spawnSync3 } from "child_process";
|
|
@@ -7899,20 +7964,20 @@ async function promptAgentSelection(detected) {
|
|
|
7899
7964
|
detected.forEach((a, i) => console.log(` ${i + 1}. ${a.name}`));
|
|
7900
7965
|
console.log(` ${detected.length + 1}. Both / all (default)`);
|
|
7901
7966
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7902
|
-
const ask2 = () => new Promise((
|
|
7967
|
+
const ask2 = () => new Promise((resolve4) => {
|
|
7903
7968
|
rl.question(`Pick [1-${detected.length + 1}] (default: all): `, (answer) => {
|
|
7904
7969
|
const t = answer.trim().toLowerCase();
|
|
7905
7970
|
if (t === "" || t === String(detected.length + 1) || t === "both" || t === "all") {
|
|
7906
7971
|
rl.close();
|
|
7907
|
-
return
|
|
7972
|
+
return resolve4(detected);
|
|
7908
7973
|
}
|
|
7909
7974
|
const n = parseInt(t, 10);
|
|
7910
7975
|
if (Number.isInteger(n) && n >= 1 && n <= detected.length) {
|
|
7911
7976
|
rl.close();
|
|
7912
|
-
return
|
|
7977
|
+
return resolve4([detected[n - 1]]);
|
|
7913
7978
|
}
|
|
7914
7979
|
console.log("Invalid choice. Try again.");
|
|
7915
|
-
|
|
7980
|
+
resolve4(ask2());
|
|
7916
7981
|
});
|
|
7917
7982
|
});
|
|
7918
7983
|
return ask2();
|
|
@@ -7935,12 +8000,12 @@ async function promptCursorApiKey(opts) {
|
|
|
7935
8000
|
return;
|
|
7936
8001
|
}
|
|
7937
8002
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7938
|
-
const key = await new Promise((
|
|
8003
|
+
const key = await new Promise((resolve4) => {
|
|
7939
8004
|
rl.question(
|
|
7940
8005
|
"Cursor grading needs a Cursor API key (cursor.com \u2192 Settings \u2192 API Keys).\nPaste it now, or press Enter to skip (Cursor workers stay idle until set): ",
|
|
7941
8006
|
(answer) => {
|
|
7942
8007
|
rl.close();
|
|
7943
|
-
|
|
8008
|
+
resolve4(answer.trim());
|
|
7944
8009
|
}
|
|
7945
8010
|
);
|
|
7946
8011
|
});
|
|
@@ -7954,12 +8019,12 @@ async function promptCursorApiKey(opts) {
|
|
|
7954
8019
|
async function promptGradingMode() {
|
|
7955
8020
|
if (!process.stdin.isTTY) return "local";
|
|
7956
8021
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7957
|
-
return new Promise((
|
|
8022
|
+
return new Promise((resolve4) => {
|
|
7958
8023
|
rl.question(
|
|
7959
8024
|
"Where should grading run?\n local \u2014 on this machine, via the Synkro container (default)\n byok \u2014 via an LLM API using your own provider key\nChoose [local] / byok: ",
|
|
7960
8025
|
(answer) => {
|
|
7961
8026
|
rl.close();
|
|
7962
|
-
|
|
8027
|
+
resolve4(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
|
|
7963
8028
|
}
|
|
7964
8029
|
);
|
|
7965
8030
|
});
|
|
@@ -7967,25 +8032,25 @@ async function promptGradingMode() {
|
|
|
7967
8032
|
async function promptStorageMode() {
|
|
7968
8033
|
if (!process.stdin.isTTY) return "local";
|
|
7969
8034
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7970
|
-
return new Promise((
|
|
8035
|
+
return new Promise((resolve4) => {
|
|
7971
8036
|
rl.question(
|
|
7972
8037
|
"Where should telemetry be stored?\n local \u2014 on this machine only (default)\n cloud \u2014 sent to Synkro cloud\nChoose [local] / cloud: ",
|
|
7973
8038
|
(answer) => {
|
|
7974
8039
|
rl.close();
|
|
7975
|
-
|
|
8040
|
+
resolve4(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
|
|
7976
8041
|
}
|
|
7977
8042
|
);
|
|
7978
8043
|
});
|
|
7979
8044
|
}
|
|
7980
8045
|
async function promptTranscriptConsent() {
|
|
7981
8046
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
7982
|
-
return new Promise((
|
|
8047
|
+
return new Promise((resolve4) => {
|
|
7983
8048
|
rl.question(
|
|
7984
8049
|
"Import and embed your Claude Code session history?\nThis indexes past sessions so Ask Synkro can answer questions\nabout your coding patterns and the dashboard shows full history. (Y/n) ",
|
|
7985
8050
|
(answer) => {
|
|
7986
8051
|
rl.close();
|
|
7987
8052
|
const trimmed = answer.trim().toLowerCase();
|
|
7988
|
-
|
|
8053
|
+
resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
7989
8054
|
}
|
|
7990
8055
|
);
|
|
7991
8056
|
});
|
|
@@ -8093,7 +8158,7 @@ function shellQuoteSingle(value) {
|
|
|
8093
8158
|
}
|
|
8094
8159
|
function resolveSynkroBundle() {
|
|
8095
8160
|
const scriptPath = process.argv[1];
|
|
8096
|
-
if (scriptPath &&
|
|
8161
|
+
if (scriptPath && existsSync10(scriptPath)) return scriptPath;
|
|
8097
8162
|
return null;
|
|
8098
8163
|
}
|
|
8099
8164
|
function writeConfigEnv(opts) {
|
|
@@ -8113,7 +8178,7 @@ function writeConfigEnv(opts) {
|
|
|
8113
8178
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8114
8179
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8115
8180
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8116
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8181
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.34")}`
|
|
8117
8182
|
];
|
|
8118
8183
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8119
8184
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8135,8 +8200,8 @@ function resolveDeploymentMode() {
|
|
|
8135
8200
|
const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
|
|
8136
8201
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
8137
8202
|
try {
|
|
8138
|
-
if (
|
|
8139
|
-
const m =
|
|
8203
|
+
if (existsSync10(CONFIG_PATH2)) {
|
|
8204
|
+
const m = readFileSync9(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
8140
8205
|
const val = m?.[1]?.toLowerCase();
|
|
8141
8206
|
if (val === "bare-host" || val === "docker") return val;
|
|
8142
8207
|
}
|
|
@@ -8165,14 +8230,14 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8165
8230
|
}
|
|
8166
8231
|
const claudeDir = join8(homedir8(), ".claude");
|
|
8167
8232
|
try {
|
|
8168
|
-
const settings = JSON.parse(
|
|
8233
|
+
const settings = JSON.parse(readFileSync9(join8(claudeDir, "settings.json"), "utf-8"));
|
|
8169
8234
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
8170
8235
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
8171
8236
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
8172
8237
|
} catch {
|
|
8173
8238
|
}
|
|
8174
8239
|
try {
|
|
8175
|
-
const mcpCache = JSON.parse(
|
|
8240
|
+
const mcpCache = JSON.parse(readFileSync9(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
8176
8241
|
const mcpNames = Object.keys(mcpCache);
|
|
8177
8242
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
8178
8243
|
} catch {
|
|
@@ -8187,7 +8252,7 @@ function collectLocalMetadata(includeClaudeCode = true) {
|
|
|
8187
8252
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
8188
8253
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
8189
8254
|
for (const f of files) {
|
|
8190
|
-
const s = JSON.parse(
|
|
8255
|
+
const s = JSON.parse(readFileSync9(join8(sessionsDir, f), "utf-8"));
|
|
8191
8256
|
if (s.version) {
|
|
8192
8257
|
meta.cc_version = meta.cc_version || s.version;
|
|
8193
8258
|
break;
|
|
@@ -8319,7 +8384,7 @@ async function installCommand(opts = {}) {
|
|
|
8319
8384
|
for (const mode of ["edit", "bash"]) {
|
|
8320
8385
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
8321
8386
|
try {
|
|
8322
|
-
const pid = parseInt(
|
|
8387
|
+
const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
|
|
8323
8388
|
if (pid > 0) {
|
|
8324
8389
|
process.kill(pid, "SIGTERM");
|
|
8325
8390
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -8453,7 +8518,7 @@ async function installCommand(opts = {}) {
|
|
|
8453
8518
|
try {
|
|
8454
8519
|
if (useLocalMcp) {
|
|
8455
8520
|
const jwtPath = join8(SYNKRO_DIR4, ".mcp-jwt");
|
|
8456
|
-
if (!
|
|
8521
|
+
if (!existsSync10(jwtPath)) {
|
|
8457
8522
|
const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
|
|
8458
8523
|
method: "POST",
|
|
8459
8524
|
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -8543,7 +8608,7 @@ async function installCommand(opts = {}) {
|
|
|
8543
8608
|
const ready = await waitForContainerReady(6e4);
|
|
8544
8609
|
if (ready) {
|
|
8545
8610
|
console.log(" \u2713 container ready");
|
|
8546
|
-
const mcpJwt =
|
|
8611
|
+
const mcpJwt = readFileSync9(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8547
8612
|
try {
|
|
8548
8613
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
8549
8614
|
method: "POST",
|
|
@@ -8560,6 +8625,7 @@ async function installCommand(opts = {}) {
|
|
|
8560
8625
|
} catch {
|
|
8561
8626
|
console.warn(" \u26A0 ingest endpoint unreachable \u2014 telemetry spool may not drain.");
|
|
8562
8627
|
}
|
|
8628
|
+
await syncSkillFiles();
|
|
8563
8629
|
} else {
|
|
8564
8630
|
console.error(" \u2717 container did not become healthy within 60s");
|
|
8565
8631
|
console.error(" Run `docker logs synkro-server` to debug.");
|
|
@@ -8574,7 +8640,7 @@ async function installCommand(opts = {}) {
|
|
|
8574
8640
|
try {
|
|
8575
8641
|
let mcpToken = "";
|
|
8576
8642
|
try {
|
|
8577
|
-
mcpToken =
|
|
8643
|
+
mcpToken = readFileSync9(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8578
8644
|
} catch {
|
|
8579
8645
|
}
|
|
8580
8646
|
if (mcpToken) {
|
|
@@ -8643,15 +8709,23 @@ function writeSynkroFileIfMissing(opts) {
|
|
|
8643
8709
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8644
8710
|
if (!root) return;
|
|
8645
8711
|
const fp = join8(root, ".synkro");
|
|
8646
|
-
if (
|
|
8712
|
+
if (existsSync10(fp)) {
|
|
8647
8713
|
console.log(` .synkro: ${fp} (existing, respected)`);
|
|
8648
8714
|
return;
|
|
8649
8715
|
}
|
|
8650
8716
|
let pool = "auto";
|
|
8651
|
-
|
|
8652
|
-
|
|
8717
|
+
const harness = [];
|
|
8718
|
+
if (opts.hasClaudeCode) {
|
|
8719
|
+
harness.push("claude-code");
|
|
8720
|
+
if (!opts.hasCursor) pool = "claude";
|
|
8721
|
+
}
|
|
8722
|
+
if (opts.hasCursor) {
|
|
8723
|
+
harness.push("cursor");
|
|
8724
|
+
if (!opts.hasClaudeCode) pool = "cursor";
|
|
8725
|
+
}
|
|
8653
8726
|
const config = {
|
|
8654
8727
|
version: 1,
|
|
8728
|
+
harness,
|
|
8655
8729
|
grader: {
|
|
8656
8730
|
pool,
|
|
8657
8731
|
mode: opts.gradingMode === "byok" ? "byok" : "local"
|
|
@@ -8669,14 +8743,146 @@ function readSynkroFilePool2() {
|
|
|
8669
8743
|
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8670
8744
|
if (!root) return "auto";
|
|
8671
8745
|
const fp = join8(root, ".synkro");
|
|
8672
|
-
if (!
|
|
8673
|
-
const parsed = JSON.parse(
|
|
8746
|
+
if (!existsSync10(fp)) return "auto";
|
|
8747
|
+
const parsed = JSON.parse(readFileSync9(fp, "utf-8"));
|
|
8674
8748
|
const pool = parsed?.grader?.pool;
|
|
8675
8749
|
if (pool === "cursor" || pool === "claude") return pool;
|
|
8676
8750
|
} catch {
|
|
8677
8751
|
}
|
|
8678
8752
|
return "auto";
|
|
8679
8753
|
}
|
|
8754
|
+
function readFullSynkroFile() {
|
|
8755
|
+
try {
|
|
8756
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8757
|
+
if (!root) return null;
|
|
8758
|
+
const fp = join8(root, ".synkro");
|
|
8759
|
+
if (!existsSync10(fp)) return null;
|
|
8760
|
+
const parsed = JSON.parse(readFileSync9(fp, "utf-8"));
|
|
8761
|
+
const valid = ["claude-code", "cursor"];
|
|
8762
|
+
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
8763
|
+
return {
|
|
8764
|
+
harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
|
|
8765
|
+
grader: {
|
|
8766
|
+
pool: ["auto", "claude", "cursor"].includes(parsed.grader?.pool) ? parsed.grader.pool : "auto",
|
|
8767
|
+
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
|
|
8768
|
+
},
|
|
8769
|
+
ruleset: parsed.ruleset || "default",
|
|
8770
|
+
skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === "string" && s.endsWith(".md")) : [],
|
|
8771
|
+
scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false },
|
|
8772
|
+
_repoRoot: root
|
|
8773
|
+
};
|
|
8774
|
+
} catch {
|
|
8775
|
+
return null;
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
function reconcileHarness() {
|
|
8779
|
+
const sf = readFullSynkroFile();
|
|
8780
|
+
if (!sf) {
|
|
8781
|
+
console.log("No .synkro file found in repo root \u2014 skipping harness reconciliation.");
|
|
8782
|
+
return null;
|
|
8783
|
+
}
|
|
8784
|
+
const wantCC = sf.harness.includes("claude-code");
|
|
8785
|
+
const wantCursor = sf.harness.includes("cursor");
|
|
8786
|
+
console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
|
|
8787
|
+
const scripts = writeHookScripts();
|
|
8788
|
+
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
8789
|
+
const ccSettings = join8(homedir8(), ".claude", "settings.json");
|
|
8790
|
+
if (wantCC) {
|
|
8791
|
+
installCCHooks(ccSettings, {
|
|
8792
|
+
bashJudgeScriptPath: scripts.bashScript,
|
|
8793
|
+
bashFollowupScriptPath: scripts.bashFollowupScript,
|
|
8794
|
+
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
8795
|
+
cwePrecheckScriptPath: scripts.cwePrecheckScript,
|
|
8796
|
+
cvePrecheckScriptPath: scripts.cvePrecheckScript,
|
|
8797
|
+
planJudgeScriptPath: scripts.planJudgeScript,
|
|
8798
|
+
agentJudgeScriptPath: scripts.agentJudgeScript,
|
|
8799
|
+
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
8800
|
+
sessionStartScriptPath: scripts.sessionStartScript,
|
|
8801
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
8802
|
+
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
8803
|
+
installScanScriptPath: scripts.installScanScript
|
|
8804
|
+
});
|
|
8805
|
+
console.log(" \u2713 Claude Code hooks registered");
|
|
8806
|
+
try {
|
|
8807
|
+
const mcpJwt = readFileSync9(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8808
|
+
if (mcpJwt) {
|
|
8809
|
+
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
8810
|
+
console.log(" \u2713 Claude Code MCP registered");
|
|
8811
|
+
}
|
|
8812
|
+
} catch {
|
|
8813
|
+
}
|
|
8814
|
+
} else {
|
|
8815
|
+
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
8816
|
+
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
8817
|
+
}
|
|
8818
|
+
const cursorHooks = join8(homedir8(), ".cursor", "hooks.json");
|
|
8819
|
+
if (wantCursor) {
|
|
8820
|
+
installCursorHooks(cursorHooks, {
|
|
8821
|
+
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
8822
|
+
editCaptureScriptPath: scripts.cursorEditCaptureScript,
|
|
8823
|
+
agentCaptureScriptPath: scripts.cursorAgentCaptureScript,
|
|
8824
|
+
bashFollowupScriptPath: scripts.bashFollowupScript,
|
|
8825
|
+
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
8826
|
+
cwePrecheckScriptPath: scripts.cwePrecheckScript,
|
|
8827
|
+
cvePrecheckScriptPath: scripts.cvePrecheckScript,
|
|
8828
|
+
planJudgeScriptPath: scripts.planJudgeScript,
|
|
8829
|
+
agentJudgeScriptPath: scripts.agentJudgeScript,
|
|
8830
|
+
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
8831
|
+
sessionStartScriptPath: scripts.sessionStartScript,
|
|
8832
|
+
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
8833
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
8834
|
+
installScanScriptPath: scripts.installScanScript
|
|
8835
|
+
});
|
|
8836
|
+
console.log(" \u2713 Cursor hooks registered");
|
|
8837
|
+
try {
|
|
8838
|
+
installCursorMcpConfig({ gatewayUrl: "", bearerToken: "", local: true });
|
|
8839
|
+
console.log(" \u2713 Cursor MCP registered");
|
|
8840
|
+
} catch {
|
|
8841
|
+
}
|
|
8842
|
+
} else {
|
|
8843
|
+
if (uninstallCursorHooks(cursorHooks)) console.log(" \u2717 Cursor hooks removed");
|
|
8844
|
+
if (uninstallCursorMcpConfig()) console.log(" \u2717 Cursor MCP removed");
|
|
8845
|
+
}
|
|
8846
|
+
const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
8847
|
+
const providers = [];
|
|
8848
|
+
if (sf.grader.pool === "cursor") {
|
|
8849
|
+
providers.push("cursor");
|
|
8850
|
+
} else if (sf.grader.pool === "claude") {
|
|
8851
|
+
providers.push("claude_code");
|
|
8852
|
+
} else {
|
|
8853
|
+
if (wantCC) providers.push("claude_code");
|
|
8854
|
+
if (wantCursor) providers.push("cursor");
|
|
8855
|
+
}
|
|
8856
|
+
if (providers.length === 0) providers.push("claude_code");
|
|
8857
|
+
return splitWorkers(total, providers);
|
|
8858
|
+
}
|
|
8859
|
+
async function syncSkillFiles() {
|
|
8860
|
+
const sf = readFullSynkroFile();
|
|
8861
|
+
if (!sf || sf.skills.length === 0) return;
|
|
8862
|
+
const resolved = resolveSkillPaths(sf.skills, sf._repoRoot);
|
|
8863
|
+
if (resolved.length === 0) return;
|
|
8864
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || "18931";
|
|
8865
|
+
for (const fp of resolved) {
|
|
8866
|
+
const skill = parseSkillFile(fp);
|
|
8867
|
+
if (!skill || skill.rules.length === 0) continue;
|
|
8868
|
+
try {
|
|
8869
|
+
const resp = await fetch(`http://127.0.0.1:${mcpPort}/api/local/skills/sync`, {
|
|
8870
|
+
method: "POST",
|
|
8871
|
+
headers: { "Content-Type": "application/json" },
|
|
8872
|
+
body: JSON.stringify(skill),
|
|
8873
|
+
signal: AbortSignal.timeout(1e4)
|
|
8874
|
+
});
|
|
8875
|
+
if (resp.ok) {
|
|
8876
|
+
const result = await resp.json();
|
|
8877
|
+
console.log(` \u2713 skill ${skill.source}: ${result.created} rules synced (${result.removed} removed)`);
|
|
8878
|
+
} else {
|
|
8879
|
+
console.warn(` \u26A0 skill ${skill.source}: sync failed (${resp.status})`);
|
|
8880
|
+
}
|
|
8881
|
+
} catch (e) {
|
|
8882
|
+
console.warn(` \u26A0 skill ${skill.source}: ${e.message}`);
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8680
8886
|
function detectGitRepo2() {
|
|
8681
8887
|
const run = (cmd2) => {
|
|
8682
8888
|
try {
|
|
@@ -8696,7 +8902,7 @@ function getClaudeProjectsFolder() {
|
|
|
8696
8902
|
const cwd = process.cwd();
|
|
8697
8903
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
8698
8904
|
const projectsDir = join8(homedir8(), ".claude", "projects", sanitized);
|
|
8699
|
-
return
|
|
8905
|
+
return existsSync10(projectsDir) ? projectsDir : null;
|
|
8700
8906
|
}
|
|
8701
8907
|
function extractSessionInsights(projectsDir) {
|
|
8702
8908
|
const insights = [];
|
|
@@ -8705,7 +8911,7 @@ function extractSessionInsights(projectsDir) {
|
|
|
8705
8911
|
const sessionId = file.replace(".jsonl", "");
|
|
8706
8912
|
const filePath = join8(projectsDir, file);
|
|
8707
8913
|
try {
|
|
8708
|
-
const content =
|
|
8914
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
8709
8915
|
const lines = content.split("\n").filter(Boolean);
|
|
8710
8916
|
for (let i = 0; i < lines.length; i++) {
|
|
8711
8917
|
try {
|
|
@@ -8781,7 +8987,7 @@ function extractTextContent(content) {
|
|
|
8781
8987
|
return "";
|
|
8782
8988
|
}
|
|
8783
8989
|
function parseTranscriptFile(filePath) {
|
|
8784
|
-
const content =
|
|
8990
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
8785
8991
|
const lines = content.split("\n").filter(Boolean);
|
|
8786
8992
|
const messages = [];
|
|
8787
8993
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -8851,7 +9057,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
8851
9057
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
8852
9058
|
}
|
|
8853
9059
|
try {
|
|
8854
|
-
const content =
|
|
9060
|
+
const content = readFileSync9(join8(projectsDir, file), "utf-8");
|
|
8855
9061
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8856
9062
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8857
9063
|
} catch {
|
|
@@ -8905,7 +9111,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
8905
9111
|
const sessionId = file.replace(".jsonl", "");
|
|
8906
9112
|
const filePath = join8(projectsDir, file);
|
|
8907
9113
|
try {
|
|
8908
|
-
const content =
|
|
9114
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
8909
9115
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8910
9116
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8911
9117
|
} catch {
|
|
@@ -8922,6 +9128,7 @@ var init_install = __esm({
|
|
|
8922
9128
|
init_ccHookConfig();
|
|
8923
9129
|
init_cursorHookConfig();
|
|
8924
9130
|
init_mcpConfig();
|
|
9131
|
+
init_skillParser();
|
|
8925
9132
|
init_hookScripts();
|
|
8926
9133
|
init_hookScriptsTs();
|
|
8927
9134
|
init_stub();
|
|
@@ -8984,7 +9191,7 @@ rl.on('line', async (line) => {
|
|
|
8984
9191
|
});
|
|
8985
9192
|
|
|
8986
9193
|
// cli/local-cc/install.ts
|
|
8987
|
-
import { existsSync as
|
|
9194
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync10, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
|
|
8988
9195
|
import { join as join9 } from "path";
|
|
8989
9196
|
import { homedir as homedir9 } from "os";
|
|
8990
9197
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -9020,10 +9227,10 @@ function runBunInstall() {
|
|
|
9020
9227
|
}
|
|
9021
9228
|
}
|
|
9022
9229
|
function safelyMutateClaudeJson(mutator) {
|
|
9023
|
-
if (!
|
|
9230
|
+
if (!existsSync11(CLAUDE_JSON_PATH)) {
|
|
9024
9231
|
return;
|
|
9025
9232
|
}
|
|
9026
|
-
const originalText =
|
|
9233
|
+
const originalText = readFileSync10(CLAUDE_JSON_PATH, "utf-8");
|
|
9027
9234
|
let parsed;
|
|
9028
9235
|
try {
|
|
9029
9236
|
parsed = JSON.parse(originalText);
|
|
@@ -9474,7 +9681,7 @@ var disconnect_exports = {};
|
|
|
9474
9681
|
__export(disconnect_exports, {
|
|
9475
9682
|
disconnectCommand: () => disconnectCommand
|
|
9476
9683
|
});
|
|
9477
|
-
import { existsSync as
|
|
9684
|
+
import { existsSync as existsSync12, rmSync, readdirSync as readdirSync4 } from "fs";
|
|
9478
9685
|
import { homedir as homedir10 } from "os";
|
|
9479
9686
|
import { join as join10 } from "path";
|
|
9480
9687
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -9515,10 +9722,10 @@ function confirmPurge() {
|
|
|
9515
9722
|
return Promise.resolve(false);
|
|
9516
9723
|
}
|
|
9517
9724
|
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
9518
|
-
return new Promise((
|
|
9725
|
+
return new Promise((resolve4) => {
|
|
9519
9726
|
rl.question(" Type 'yes' to wipe everything (anything else cancels): ", (answer) => {
|
|
9520
9727
|
rl.close();
|
|
9521
|
-
|
|
9728
|
+
resolve4(answer.trim().toLowerCase() === "yes");
|
|
9522
9729
|
});
|
|
9523
9730
|
});
|
|
9524
9731
|
}
|
|
@@ -9550,7 +9757,7 @@ async function disconnectCommand(args2 = []) {
|
|
|
9550
9757
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
9551
9758
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
9552
9759
|
}
|
|
9553
|
-
if (
|
|
9760
|
+
if (existsSync12(SYNKRO_DIR5)) {
|
|
9554
9761
|
if (purge) {
|
|
9555
9762
|
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
9556
9763
|
console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
|
|
@@ -9593,7 +9800,7 @@ var init_disconnect = __esm({
|
|
|
9593
9800
|
});
|
|
9594
9801
|
|
|
9595
9802
|
// cli/local-cc/turnLog.ts
|
|
9596
|
-
import { appendFileSync, existsSync as
|
|
9803
|
+
import { appendFileSync, existsSync as existsSync13, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
9597
9804
|
import { dirname as dirname6, join as join11 } from "path";
|
|
9598
9805
|
import { homedir as homedir11 } from "os";
|
|
9599
9806
|
function truncate(s, max = PREVIEW_MAX) {
|
|
@@ -9631,11 +9838,11 @@ function appendTurn(args2) {
|
|
|
9631
9838
|
}
|
|
9632
9839
|
}
|
|
9633
9840
|
function readRecentTurns(n = 20) {
|
|
9634
|
-
if (!
|
|
9841
|
+
if (!existsSync13(TURN_LOG_PATH)) return [];
|
|
9635
9842
|
try {
|
|
9636
9843
|
const size = statSync2(TURN_LOG_PATH).size;
|
|
9637
9844
|
if (size === 0) return [];
|
|
9638
|
-
const text =
|
|
9845
|
+
const text = readFileSync11(TURN_LOG_PATH, "utf-8");
|
|
9639
9846
|
const lines = text.split("\n").filter(Boolean);
|
|
9640
9847
|
const lastN = lines.slice(-n).reverse();
|
|
9641
9848
|
return lastN.map((line) => {
|
|
@@ -9652,7 +9859,7 @@ function readRecentTurns(n = 20) {
|
|
|
9652
9859
|
function followTurns(onEntry) {
|
|
9653
9860
|
try {
|
|
9654
9861
|
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
9655
|
-
if (!
|
|
9862
|
+
if (!existsSync13(TURN_LOG_PATH)) {
|
|
9656
9863
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
9657
9864
|
}
|
|
9658
9865
|
} catch {
|
|
@@ -9728,7 +9935,7 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
9728
9935
|
const port = opts.port ?? CHANNEL_PORT;
|
|
9729
9936
|
const startedAt = Date.now();
|
|
9730
9937
|
try {
|
|
9731
|
-
const result = await new Promise((
|
|
9938
|
+
const result = await new Promise((resolve4, reject) => {
|
|
9732
9939
|
const req = httpRequest({
|
|
9733
9940
|
host: CHANNEL_HOST,
|
|
9734
9941
|
port,
|
|
@@ -9754,7 +9961,7 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
9754
9961
|
reject(new LocalCCError(parsed.error));
|
|
9755
9962
|
return;
|
|
9756
9963
|
}
|
|
9757
|
-
|
|
9964
|
+
resolve4(String(parsed.result ?? ""));
|
|
9758
9965
|
} catch (err) {
|
|
9759
9966
|
reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
|
|
9760
9967
|
}
|
|
@@ -9780,14 +9987,14 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
9780
9987
|
}
|
|
9781
9988
|
}
|
|
9782
9989
|
function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
|
|
9783
|
-
return new Promise((
|
|
9990
|
+
return new Promise((resolve4) => {
|
|
9784
9991
|
const sock = connect(port, CHANNEL_HOST);
|
|
9785
9992
|
const done = (ok) => {
|
|
9786
9993
|
try {
|
|
9787
9994
|
sock.destroy();
|
|
9788
9995
|
} catch {
|
|
9789
9996
|
}
|
|
9790
|
-
|
|
9997
|
+
resolve4(ok);
|
|
9791
9998
|
};
|
|
9792
9999
|
sock.once("connect", () => done(true));
|
|
9793
10000
|
sock.once("error", () => done(false));
|
|
@@ -9819,10 +10026,10 @@ __export(grade_exports, {
|
|
|
9819
10026
|
gradeCommand: () => gradeCommand
|
|
9820
10027
|
});
|
|
9821
10028
|
async function readStdin() {
|
|
9822
|
-
return new Promise((
|
|
10029
|
+
return new Promise((resolve4, reject) => {
|
|
9823
10030
|
const chunks = [];
|
|
9824
10031
|
process.stdin.on("data", (c) => chunks.push(c));
|
|
9825
|
-
process.stdin.on("end", () =>
|
|
10032
|
+
process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
|
|
9826
10033
|
process.stdin.on("error", reject);
|
|
9827
10034
|
});
|
|
9828
10035
|
}
|
|
@@ -9981,14 +10188,14 @@ function ensureRunning(opts = {}) {
|
|
|
9981
10188
|
return startTask(opts);
|
|
9982
10189
|
}
|
|
9983
10190
|
function probePort(host, port, timeoutMs = 500) {
|
|
9984
|
-
return new Promise((
|
|
10191
|
+
return new Promise((resolve4) => {
|
|
9985
10192
|
const sock = connect2(port, host);
|
|
9986
10193
|
const done = (ok) => {
|
|
9987
10194
|
try {
|
|
9988
10195
|
sock.destroy();
|
|
9989
10196
|
} catch {
|
|
9990
10197
|
}
|
|
9991
|
-
|
|
10198
|
+
resolve4(ok);
|
|
9992
10199
|
};
|
|
9993
10200
|
sock.once("connect", () => done(true));
|
|
9994
10201
|
sock.once("error", () => done(false));
|
|
@@ -10081,13 +10288,13 @@ var init_pueue = __esm({
|
|
|
10081
10288
|
});
|
|
10082
10289
|
|
|
10083
10290
|
// cli/local-cc/settings.ts
|
|
10084
|
-
import { existsSync as
|
|
10291
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
|
|
10085
10292
|
import { homedir as homedir13 } from "os";
|
|
10086
10293
|
import { join as join13 } from "path";
|
|
10087
10294
|
function isLocalCCEnabled() {
|
|
10088
|
-
if (!
|
|
10295
|
+
if (!existsSync14(CONFIG_PATH3)) return false;
|
|
10089
10296
|
try {
|
|
10090
|
-
const content =
|
|
10297
|
+
const content = readFileSync12(CONFIG_PATH3, "utf-8");
|
|
10091
10298
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
10092
10299
|
return match?.[1] === "yes";
|
|
10093
10300
|
} catch {
|
|
@@ -10111,7 +10318,7 @@ import { spawnSync as spawnSync7 } from "child_process";
|
|
|
10111
10318
|
import { homedir as homedir14 } from "os";
|
|
10112
10319
|
import { join as join14 } from "path";
|
|
10113
10320
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
10114
|
-
import { existsSync as
|
|
10321
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
10115
10322
|
function deploymentMode() {
|
|
10116
10323
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
10117
10324
|
if (env === "docker") return "docker";
|
|
@@ -10217,15 +10424,15 @@ TROUBLESHOOTING
|
|
|
10217
10424
|
`);
|
|
10218
10425
|
}
|
|
10219
10426
|
function readGatewayUrl() {
|
|
10220
|
-
if (
|
|
10221
|
-
const m =
|
|
10427
|
+
if (existsSync15(CONFIG_PATH4)) {
|
|
10428
|
+
const m = readFileSync13(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
10222
10429
|
if (m) return m[1];
|
|
10223
10430
|
}
|
|
10224
10431
|
return "https://api.synkro.sh";
|
|
10225
10432
|
}
|
|
10226
10433
|
function updateLocalInferenceFlag(enabled) {
|
|
10227
|
-
if (!
|
|
10228
|
-
let content =
|
|
10434
|
+
if (!existsSync15(CONFIG_PATH4)) return;
|
|
10435
|
+
let content = readFileSync13(CONFIG_PATH4, "utf-8");
|
|
10229
10436
|
const flag = enabled ? "yes" : "no";
|
|
10230
10437
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
10231
10438
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -10400,11 +10607,24 @@ function cmdStop() {
|
|
|
10400
10607
|
}
|
|
10401
10608
|
async function cmdRestart(rest = []) {
|
|
10402
10609
|
if (inDockerMode()) {
|
|
10403
|
-
const {
|
|
10610
|
+
const { explicit } = resolveWorkerConfig(rest);
|
|
10611
|
+
let claudeWorkers;
|
|
10612
|
+
let cursorWorkers;
|
|
10613
|
+
if (explicit) {
|
|
10614
|
+
({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
|
|
10615
|
+
} else {
|
|
10616
|
+
const reconciled = reconcileHarness();
|
|
10617
|
+
if (reconciled) {
|
|
10618
|
+
({ claudeWorkers, cursorWorkers } = reconciled);
|
|
10619
|
+
} else {
|
|
10620
|
+
({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10404
10623
|
console.log(`Restarting synkro-server container (${claudeWorkers} claude + ${cursorWorkers} cursor, pulling latest image)...`);
|
|
10405
10624
|
await dockerUpdate({ claudeWorkers, cursorWorkers });
|
|
10406
10625
|
const ready = await waitForContainerReady(6e4);
|
|
10407
10626
|
console.log(ready ? "\u2713 container ready" : "\u26A0 container did not pass /healthz within 60s");
|
|
10627
|
+
if (ready) await syncSkillFiles();
|
|
10408
10628
|
return;
|
|
10409
10629
|
}
|
|
10410
10630
|
stopTask(CHANNEL_PRIMARY);
|
|
@@ -10519,7 +10739,7 @@ function cmdLogs(rest) {
|
|
|
10519
10739
|
if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
|
|
10520
10740
|
return;
|
|
10521
10741
|
}
|
|
10522
|
-
return new Promise((
|
|
10742
|
+
return new Promise((resolve4) => {
|
|
10523
10743
|
console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
|
|
10524
10744
|
const stop = followTurns((t) => {
|
|
10525
10745
|
console.log(" " + formatTurn(t, raw));
|
|
@@ -10527,7 +10747,7 @@ function cmdLogs(rest) {
|
|
|
10527
10747
|
const onSigint = () => {
|
|
10528
10748
|
stop();
|
|
10529
10749
|
process.removeListener("SIGINT", onSigint);
|
|
10530
|
-
|
|
10750
|
+
resolve4();
|
|
10531
10751
|
};
|
|
10532
10752
|
process.on("SIGINT", onSigint);
|
|
10533
10753
|
});
|
|
@@ -10644,6 +10864,7 @@ var init_localCc = __esm({
|
|
|
10644
10864
|
init_settings();
|
|
10645
10865
|
init_macKeychain();
|
|
10646
10866
|
init_dockerInstall();
|
|
10867
|
+
init_install();
|
|
10647
10868
|
init_client();
|
|
10648
10869
|
init_stub();
|
|
10649
10870
|
SYNKRO_CONFIG_PATH = join14(homedir14(), ".synkro", "config.env");
|
|
@@ -10754,13 +10975,13 @@ var config_exports = {};
|
|
|
10754
10975
|
__export(config_exports, {
|
|
10755
10976
|
configCommand: () => configCommand
|
|
10756
10977
|
});
|
|
10757
|
-
import { readFileSync as
|
|
10978
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync16 } from "fs";
|
|
10758
10979
|
import { join as join15 } from "path";
|
|
10759
10980
|
import { homedir as homedir15 } from "os";
|
|
10760
10981
|
function readConfigEnv() {
|
|
10761
|
-
if (!
|
|
10982
|
+
if (!existsSync16(CONFIG_PATH5)) return {};
|
|
10762
10983
|
const out = {};
|
|
10763
|
-
for (const line of
|
|
10984
|
+
for (const line of readFileSync14(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
10764
10985
|
const t = line.trim();
|
|
10765
10986
|
if (!t || t.startsWith("#")) continue;
|
|
10766
10987
|
const eq = t.indexOf("=");
|
|
@@ -10769,11 +10990,11 @@ function readConfigEnv() {
|
|
|
10769
10990
|
return out;
|
|
10770
10991
|
}
|
|
10771
10992
|
function updateConfigValue(key, value) {
|
|
10772
|
-
if (!
|
|
10993
|
+
if (!existsSync16(CONFIG_PATH5)) {
|
|
10773
10994
|
console.error("No config found. Run `synkro install` first.");
|
|
10774
10995
|
process.exit(1);
|
|
10775
10996
|
}
|
|
10776
|
-
const lines =
|
|
10997
|
+
const lines = readFileSync14(CONFIG_PATH5, "utf-8").split("\n");
|
|
10777
10998
|
const pattern = new RegExp(`^${key}=`);
|
|
10778
10999
|
let found = false;
|
|
10779
11000
|
const updated = lines.map((line) => {
|
|
@@ -10900,14 +11121,14 @@ var init_config = __esm({
|
|
|
10900
11121
|
});
|
|
10901
11122
|
|
|
10902
11123
|
// cli/bootstrap.js
|
|
10903
|
-
import { readFileSync as
|
|
10904
|
-
import { resolve as
|
|
11124
|
+
import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
|
|
11125
|
+
import { resolve as resolve3 } from "path";
|
|
10905
11126
|
var envCandidates = [
|
|
10906
|
-
|
|
11127
|
+
resolve3(process.env.HOME ?? "", ".synkro", "config.env")
|
|
10907
11128
|
];
|
|
10908
11129
|
for (const envPath of envCandidates) {
|
|
10909
|
-
if (!
|
|
10910
|
-
const envContent =
|
|
11130
|
+
if (!existsSync17(envPath)) continue;
|
|
11131
|
+
const envContent = readFileSync15(envPath, "utf-8");
|
|
10911
11132
|
for (const line of envContent.split("\n")) {
|
|
10912
11133
|
const trimmed = line.trim();
|
|
10913
11134
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -10922,7 +11143,7 @@ var args = process.argv.slice(2);
|
|
|
10922
11143
|
var cmd = args[0] || "";
|
|
10923
11144
|
var subArgs = args.slice(1);
|
|
10924
11145
|
function printVersion() {
|
|
10925
|
-
console.log("1.6.
|
|
11146
|
+
console.log("1.6.34");
|
|
10926
11147
|
}
|
|
10927
11148
|
function printHelp2() {
|
|
10928
11149
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|