@synkro-sh/cli 1.4.0 → 1.4.2

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 CHANGED
@@ -3370,87 +3370,6 @@ var init_promptFetcher = __esm({
3370
3370
  }
3371
3371
  });
3372
3372
 
3373
- // cli/storage/local.ts
3374
- import Database from "better-sqlite3";
3375
- import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
3376
- import { homedir as homedir6 } from "os";
3377
- import { join as join7 } from "path";
3378
- function getSetting(key) {
3379
- const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
3380
- return row?.value ?? null;
3381
- }
3382
- function setSetting(key, value) {
3383
- db.prepare(
3384
- `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now'))
3385
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
3386
- ).run(key, value);
3387
- }
3388
- var SYNKRO_DIR2, DB_PATH, db;
3389
- var init_local = __esm({
3390
- "cli/storage/local.ts"() {
3391
- "use strict";
3392
- SYNKRO_DIR2 = join7(homedir6(), ".synkro");
3393
- DB_PATH = join7(SYNKRO_DIR2, "sessions.db");
3394
- if (!existsSync8(SYNKRO_DIR2)) {
3395
- mkdirSync6(SYNKRO_DIR2, { recursive: true });
3396
- }
3397
- try {
3398
- db = new Database(DB_PATH);
3399
- } catch (err) {
3400
- const msg = err instanceof Error ? err.message : String(err);
3401
- console.error(`Failed to initialize database at ${DB_PATH}: ${msg}`);
3402
- console.error("Check that ~/.synkro/ is writable and disk is not full.");
3403
- process.exit(1);
3404
- }
3405
- db.exec(`
3406
- CREATE TABLE IF NOT EXISTS command_history (
3407
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3408
- command TEXT NOT NULL,
3409
- created_at TEXT DEFAULT (datetime('now'))
3410
- );
3411
- `);
3412
- db.exec(`
3413
- CREATE TABLE IF NOT EXISTS project_keys (
3414
- slug TEXT PRIMARY KEY,
3415
- project_id TEXT NOT NULL,
3416
- project_name TEXT NOT NULL,
3417
- api_key TEXT NOT NULL,
3418
- created_at TEXT DEFAULT (datetime('now'))
3419
- );
3420
- `);
3421
- db.exec(`
3422
- CREATE TABLE IF NOT EXISTS settings (
3423
- key TEXT PRIMARY KEY,
3424
- value TEXT NOT NULL,
3425
- updated_at TEXT DEFAULT (datetime('now'))
3426
- );
3427
- `);
3428
- process.on("exit", () => {
3429
- db.close();
3430
- });
3431
- }
3432
- });
3433
-
3434
- // cli/local-cc/settings.ts
3435
- function getInferenceProvider() {
3436
- const raw = getSetting(KEY);
3437
- return raw === "local-cc" ? "local-cc" : "inngest";
3438
- }
3439
- function setInferenceProvider(value) {
3440
- setSetting(KEY, value);
3441
- }
3442
- function isLocalCCEnabled() {
3443
- return getInferenceProvider() === "local-cc";
3444
- }
3445
- var KEY;
3446
- var init_settings = __esm({
3447
- "cli/local-cc/settings.ts"() {
3448
- "use strict";
3449
- init_local();
3450
- KEY = "inference_provider";
3451
- }
3452
- });
3453
-
3454
3373
  // cli/local-cc/channelSource.ts
3455
3374
  var CHANNEL_PLUGIN_SOURCE;
3456
3375
  var init_channelSource = __esm({
@@ -3601,13 +3520,13 @@ await mcp.connect(new StdioServerTransport());
3601
3520
  });
3602
3521
 
3603
3522
  // cli/local-cc/install.ts
3604
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6, readFileSync as readFileSync6, chmodSync, copyFileSync, renameSync as renameSync3, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
3605
- import { join as join8 } from "path";
3606
- import { homedir as homedir7 } from "os";
3523
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync6, chmodSync, copyFileSync, renameSync as renameSync3, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
3524
+ import { join as join7 } from "path";
3525
+ import { homedir as homedir6 } from "os";
3607
3526
  import { spawnSync } from "child_process";
3608
3527
  function writePluginFiles() {
3609
- mkdirSync7(SESSION_DIR, { recursive: true });
3610
- mkdirSync7(PLUGIN_SETTINGS_DIR, { recursive: true });
3528
+ mkdirSync6(SESSION_DIR, { recursive: true });
3529
+ mkdirSync6(PLUGIN_SETTINGS_DIR, { recursive: true });
3611
3530
  writeFileSync6(PLUGIN_PATH, CHANNEL_PLUGIN_SOURCE, "utf-8");
3612
3531
  chmodSync(PLUGIN_PATH, 493);
3613
3532
  writeFileSync6(PLUGIN_PKG_PATH, PLUGIN_PACKAGE_JSON, "utf-8");
@@ -3639,7 +3558,7 @@ function runBunInstall() {
3639
3558
  }
3640
3559
  }
3641
3560
  function safelyMutateClaudeJson(mutator) {
3642
- if (!existsSync9(CLAUDE_JSON_PATH)) {
3561
+ if (!existsSync8(CLAUDE_JSON_PATH)) {
3643
3562
  return;
3644
3563
  }
3645
3564
  const originalText = readFileSync6(CLAUDE_JSON_PATH, "utf-8");
@@ -3767,15 +3686,15 @@ var init_install = __esm({
3767
3686
  "cli/local-cc/install.ts"() {
3768
3687
  "use strict";
3769
3688
  init_channelSource();
3770
- CLAUDE_JSON_BACKUP_PATH = join8(homedir7(), ".claude.json.synkro-bak");
3771
- SESSION_DIR = join8(homedir7(), ".synkro", "cc_sessions");
3772
- PLUGIN_PATH = join8(SESSION_DIR, "synkro-channel.ts");
3773
- PLUGIN_PKG_PATH = join8(SESSION_DIR, "package.json");
3774
- PLUGIN_SETTINGS_DIR = join8(SESSION_DIR, ".claude");
3775
- PLUGIN_SETTINGS_PATH = join8(PLUGIN_SETTINGS_DIR, "settings.json");
3776
- PROJECT_MCP_PATH = join8(SESSION_DIR, ".mcp.json");
3777
- CLAUDE_JSON_PATH = join8(homedir7(), ".claude.json");
3778
- RUN_SCRIPT_PATH = join8(SESSION_DIR, "run-claude.sh");
3689
+ CLAUDE_JSON_BACKUP_PATH = join7(homedir6(), ".claude.json.synkro-bak");
3690
+ SESSION_DIR = join7(homedir6(), ".synkro", "cc_sessions");
3691
+ PLUGIN_PATH = join7(SESSION_DIR, "synkro-channel.ts");
3692
+ PLUGIN_PKG_PATH = join7(SESSION_DIR, "package.json");
3693
+ PLUGIN_SETTINGS_DIR = join7(SESSION_DIR, ".claude");
3694
+ PLUGIN_SETTINGS_PATH = join7(PLUGIN_SETTINGS_DIR, "settings.json");
3695
+ PROJECT_MCP_PATH = join7(SESSION_DIR, ".mcp.json");
3696
+ CLAUDE_JSON_PATH = join7(homedir6(), ".claude.json");
3697
+ RUN_SCRIPT_PATH = join7(SESSION_DIR, "run-claude.sh");
3779
3698
  TMUX_SESSION_NAME = "synkro-local-cc";
3780
3699
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
3781
3700
  # Auto-generated by \`synkro install\`. Do not edit.
@@ -3827,8 +3746,8 @@ done
3827
3746
 
3828
3747
  // cli/local-cc/pueue.ts
3829
3748
  import { execFileSync, spawnSync as spawnSync2 } from "child_process";
3830
- import { homedir as homedir8 } from "os";
3831
- import { join as join9 } from "path";
3749
+ import { homedir as homedir7 } from "os";
3750
+ import { join as join8 } from "path";
3832
3751
  import { connect } from "net";
3833
3752
  function pueueAvailable() {
3834
3753
  const r = spawnSync2("pueue", ["--version"], { encoding: "utf-8" });
@@ -3883,7 +3802,7 @@ function startTask(opts = {}) {
3883
3802
  if (existing) {
3884
3803
  spawnSync2("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
3885
3804
  }
3886
- const runScript = join9(cwd, "run-claude.sh");
3805
+ const runScript = join8(cwd, "run-claude.sh");
3887
3806
  const args2 = [
3888
3807
  "add",
3889
3808
  "--label",
@@ -3975,7 +3894,7 @@ var init_pueue = __esm({
3975
3894
  "use strict";
3976
3895
  TASK_LABEL = "synkro-local-cc";
3977
3896
  TMUX_SESSION = "synkro-local-cc";
3978
- SESSION_DIR2 = join9(homedir8(), ".synkro", "cc_sessions");
3897
+ SESSION_DIR2 = join8(homedir7(), ".synkro", "cc_sessions");
3979
3898
  PueueError = class extends Error {
3980
3899
  constructor(message, cause) {
3981
3900
  super(message);
@@ -3987,189 +3906,453 @@ var init_pueue = __esm({
3987
3906
  }
3988
3907
  });
3989
3908
 
3990
- // cli/commands/install.ts
3991
- var install_exports = {};
3992
- __export(install_exports, {
3993
- installCommand: () => installCommand,
3994
- parseArgs: () => parseArgs
3995
- });
3996
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync } from "fs";
3997
- import { homedir as homedir9 } from "os";
3998
- import { join as join10 } from "path";
3999
- import { execSync as execSync5 } from "child_process";
4000
- import { createInterface as createInterface3 } from "readline";
4001
- function sanitizeGatewayCandidate(raw) {
4002
- if (!raw) return void 0;
4003
- return /^https?:\/\//.test(raw) ? raw : void 0;
4004
- }
4005
- function parseArgs(argv) {
4006
- const opts = {};
4007
- for (const a of argv) {
4008
- if (a.startsWith("--api-key=")) opts.apiKey = a.slice("--api-key=".length);
4009
- else if (a.startsWith("--gateway=")) opts.gatewayUrl = a.slice("--gateway=".length);
4010
- else if (a === "--skip-auth") opts.skipAuth = true;
4011
- else if (a === "--no-mcp") opts.noMcp = true;
4012
- else if (a === "--force" || a === "-f") opts.force = true;
4013
- else if (a === "--link-repo") opts.linkRepo = true;
3909
+ // cli/local-cc/prompts.ts
3910
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3911
+ import { homedir as homedir8 } from "os";
3912
+ import { join as join9 } from "path";
3913
+ function loadCachedPrompts() {
3914
+ if (_cached) return _cached;
3915
+ if (!existsSync9(CACHE_PATH2)) {
3916
+ throw new Error("Prompts cache not found. Run `synkro install` or `synkro update` first.");
4014
3917
  }
4015
- if (!opts.gatewayUrl) {
4016
- const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
4017
- if (fromEnv) opts.gatewayUrl = fromEnv;
3918
+ try {
3919
+ _cached = JSON.parse(readFileSync7(CACHE_PATH2, "utf-8"));
3920
+ return _cached;
3921
+ } catch {
3922
+ throw new Error("Prompts cache is corrupted. Run `synkro update` to refresh.");
4018
3923
  }
4019
- return opts;
4020
- }
4021
- async function promptTranscriptConsent() {
4022
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
4023
- return new Promise((resolve2) => {
4024
- rl.question(
4025
- "Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
4026
- (answer) => {
4027
- rl.close();
4028
- const trimmed = answer.trim().toLowerCase();
4029
- resolve2(trimmed === "" || trimmed === "y" || trimmed === "yes");
4030
- }
4031
- );
4032
- });
4033
- }
4034
- function ensureSynkroDir() {
4035
- mkdirSync8(SYNKRO_DIR3, { recursive: true });
4036
- mkdirSync8(HOOKS_DIR, { recursive: true });
4037
- mkdirSync8(BIN_DIR, { recursive: true });
4038
- mkdirSync8(OFFSETS_DIR, { recursive: true });
4039
- }
4040
- function writeHookScripts() {
4041
- const bashScriptPath = join10(HOOKS_DIR, "cc-bash-judge.sh");
4042
- const bashFollowupScriptPath = join10(HOOKS_DIR, "cc-bash-followup.sh");
4043
- const editCaptureScriptPath = join10(HOOKS_DIR, "cc-edit-capture.sh");
4044
- const editPrecheckScriptPath = join10(HOOKS_DIR, "cc-edit-precheck.sh");
4045
- const stopSummaryScriptPath = join10(HOOKS_DIR, "cc-stop-summary.sh");
4046
- const sessionStartScriptPath = join10(HOOKS_DIR, "cc-session-start.sh");
4047
- const transcriptSyncScriptPath = join10(HOOKS_DIR, "cc-transcript-sync.sh");
4048
- writeFileSync7(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4049
- writeFileSync7(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4050
- writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4051
- writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4052
- writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4053
- writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4054
- writeFileSync7(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4055
- chmodSync2(bashScriptPath, 493);
4056
- chmodSync2(bashFollowupScriptPath, 493);
4057
- chmodSync2(editCaptureScriptPath, 493);
4058
- chmodSync2(editPrecheckScriptPath, 493);
4059
- chmodSync2(stopSummaryScriptPath, 493);
4060
- chmodSync2(sessionStartScriptPath, 493);
4061
- chmodSync2(transcriptSyncScriptPath, 493);
4062
- return {
4063
- bashScript: bashScriptPath,
4064
- bashFollowupScript: bashFollowupScriptPath,
4065
- editCaptureScript: editCaptureScriptPath,
4066
- editPrecheckScript: editPrecheckScriptPath,
4067
- stopSummaryScript: stopSummaryScriptPath,
4068
- sessionStartScript: sessionStartScriptPath,
4069
- transcriptSyncScript: transcriptSyncScriptPath
4070
- };
4071
- }
4072
- function sanitizeConfigValue(raw, maxLen = 256) {
4073
- if (!raw) return "";
4074
- return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
4075
3924
  }
4076
- function shellQuoteSingle(value) {
4077
- return `'${value.replace(/'/g, "'\\''")}'`;
3925
+ function getPrimer(role) {
3926
+ const cache = loadCachedPrompts();
3927
+ const primer = role === "grade-edit" ? cache.grader_primer_edit : cache.grader_primer_bash;
3928
+ if (!primer) {
3929
+ throw new Error(`No cached primer for role "${role}". Run \`synkro update\` to refresh prompts.`);
3930
+ }
3931
+ return primer;
4078
3932
  }
4079
- function resolveSynkroBundle() {
4080
- const scriptPath = process.argv[1];
4081
- if (scriptPath && existsSync10(scriptPath)) return scriptPath;
4082
- return null;
3933
+ function buildChannelContent(role, payload) {
3934
+ return `${getPrimer(role)}
3935
+
3936
+ ---
3937
+ PAYLOAD (the input to evaluate):
3938
+
3939
+ ${payload}`;
4083
3940
  }
4084
- function writeConfigEnv(opts) {
4085
- const credsPath = join10(SYNKRO_DIR3, "credentials.json");
4086
- const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
4087
- const safeUserId = sanitizeConfigValue(opts.userId);
4088
- const safeOrgId = sanitizeConfigValue(opts.orgId);
4089
- const safeEmail = sanitizeConfigValue(opts.email);
4090
- const safeTier = sanitizeConfigValue(opts.tier ?? "pro", 32);
4091
- const safeInference = sanitizeConfigValue(opts.inference ?? "fast", 16);
4092
- const safeSynkroBin = sanitizeConfigValue(opts.synkroBin ?? "", 1024);
4093
- const lines = [
4094
- "# Synkro CLI config (managed by synkro install)",
4095
- "# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
4096
- "# and send Authorization: Bearer <access_token> on every gateway call.",
4097
- `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
4098
- `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4099
- `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4100
- `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4101
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.0")}`
4102
- ];
4103
- if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4104
- if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
4105
- if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
4106
- if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
4107
- if (opts.transcriptConsent !== void 0) {
4108
- lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
3941
+ var CACHE_PATH2, _cached;
3942
+ var init_prompts = __esm({
3943
+ "cli/local-cc/prompts.ts"() {
3944
+ "use strict";
3945
+ CACHE_PATH2 = join9(homedir8(), ".synkro", "prompts", "judge-prompts.json");
3946
+ _cached = null;
4109
3947
  }
4110
- lines.push("");
4111
- writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
4112
- chmodSync2(CONFIG_PATH2, 384);
3948
+ });
3949
+
3950
+ // cli/local-cc/turnLog.ts
3951
+ import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync8, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
3952
+ import { dirname as dirname4, join as join10 } from "path";
3953
+ import { homedir as homedir9 } from "os";
3954
+ function truncate(s, max = PREVIEW_MAX) {
3955
+ if (s.length <= max) return s;
3956
+ return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
4113
3957
  }
4114
- function collectLocalMetadata() {
4115
- const meta = { platform: process.platform };
4116
- try {
4117
- meta.display_name = execSync5("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
4118
- } catch {
4119
- }
4120
- try {
4121
- const remote = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 3e3 }).trim();
4122
- const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
4123
- const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
4124
- const m = sshMatch || httpMatch;
4125
- if (m) meta.active_repo = m[1];
4126
- } catch {
4127
- }
4128
- try {
4129
- meta.cc_version = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
4130
- } catch {
4131
- }
4132
- const claudeDir = join10(homedir9(), ".claude");
3958
+ function extractSeverity(result) {
3959
+ const m = result.match(/<synkro-(?:verdict|intent)>([\s\S]*?)<\/synkro-(?:verdict|intent)>/);
3960
+ if (!m) return void 0;
4133
3961
  try {
4134
- const settings = JSON.parse(readFileSync7(join10(claudeDir, "settings.json"), "utf-8"));
4135
- const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
4136
- if (plugins.length) meta.enabled_plugins = plugins;
4137
- if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
3962
+ const obj = JSON.parse(m[1]);
3963
+ if (obj.severity) return String(obj.severity);
3964
+ if (typeof obj.ok === "boolean") return obj.ok ? "ok" : "violations";
3965
+ if (obj.type) return String(obj.type);
3966
+ if (obj.verdict) return String(obj.verdict);
4138
3967
  } catch {
4139
3968
  }
3969
+ return void 0;
3970
+ }
3971
+ function appendTurn(args2) {
4140
3972
  try {
4141
- const mcpCache = JSON.parse(readFileSync7(join10(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4142
- const mcpNames = Object.keys(mcpCache);
4143
- if (mcpNames.length) meta.mcp_servers = mcpNames;
3973
+ mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
3974
+ const entry = {
3975
+ ts: new Date(args2.startedAt).toISOString(),
3976
+ role: args2.role,
3977
+ duration_ms: Date.now() - args2.startedAt,
3978
+ status: args2.status,
3979
+ request_preview: truncate(args2.request),
3980
+ response_preview: args2.result ? truncate(args2.result) : "",
3981
+ severity: args2.result ? extractSeverity(args2.result) : void 0,
3982
+ error: args2.error
3983
+ };
3984
+ appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
4144
3985
  } catch {
4145
3986
  }
3987
+ }
3988
+ function readRecentTurns(n = 20) {
3989
+ if (!existsSync10(TURN_LOG_PATH)) return [];
4146
3990
  try {
4147
- const mcpList = execSync5("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
4148
- const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
4149
- if (connected.length) meta.mcp_servers_connected = connected;
3991
+ const size = statSync(TURN_LOG_PATH).size;
3992
+ if (size === 0) return [];
3993
+ const text = readFileSync8(TURN_LOG_PATH, "utf-8");
3994
+ const lines = text.split("\n").filter(Boolean);
3995
+ const lastN = lines.slice(-n).reverse();
3996
+ return lastN.map((line) => {
3997
+ try {
3998
+ return JSON.parse(line);
3999
+ } catch {
4000
+ return null;
4001
+ }
4002
+ }).filter((x) => x !== null);
4150
4003
  } catch {
4004
+ return [];
4151
4005
  }
4006
+ }
4007
+ function followTurns(onEntry) {
4152
4008
  try {
4153
- const sessionsDir = join10(claudeDir, "sessions");
4154
- const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
4155
- for (const f of files) {
4156
- const s = JSON.parse(readFileSync7(join10(sessionsDir, f), "utf-8"));
4157
- if (s.version) {
4158
- meta.cc_version = meta.cc_version || s.version;
4159
- break;
4160
- }
4009
+ mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4010
+ if (!existsSync10(TURN_LOG_PATH)) {
4011
+ appendFileSync(TURN_LOG_PATH, "", "utf-8");
4161
4012
  }
4162
4013
  } catch {
4163
4014
  }
4164
- return meta;
4165
- }
4166
- async function fetchUserProfile(gatewayUrl, token) {
4167
- try {
4168
- const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
4169
- headers: { "Authorization": `Bearer ${token}` }
4170
- });
4171
- if (!resp.ok) return { tier: "pro", inference: "fast" };
4172
- const data = await resp.json();
4015
+ let lastSize = (() => {
4016
+ try {
4017
+ return statSync(TURN_LOG_PATH).size;
4018
+ } catch {
4019
+ return 0;
4020
+ }
4021
+ })();
4022
+ let pendingPartial = "";
4023
+ const drainNewBytes = (from, to) => {
4024
+ if (to <= from) return;
4025
+ let fd = null;
4026
+ try {
4027
+ fd = openSync2(TURN_LOG_PATH, "r");
4028
+ const len = to - from;
4029
+ const buf = Buffer.alloc(len);
4030
+ readSync(fd, buf, 0, len, from);
4031
+ const text = pendingPartial + buf.toString("utf-8");
4032
+ const lastNewline = text.lastIndexOf("\n");
4033
+ if (lastNewline === -1) {
4034
+ pendingPartial = text;
4035
+ return;
4036
+ }
4037
+ const complete = text.slice(0, lastNewline);
4038
+ pendingPartial = text.slice(lastNewline + 1);
4039
+ for (const line of complete.split("\n")) {
4040
+ if (!line) continue;
4041
+ try {
4042
+ onEntry(JSON.parse(line));
4043
+ } catch {
4044
+ }
4045
+ }
4046
+ } catch {
4047
+ } finally {
4048
+ if (fd !== null) {
4049
+ try {
4050
+ closeSync2(fd);
4051
+ } catch {
4052
+ }
4053
+ }
4054
+ }
4055
+ };
4056
+ watchFile(TURN_LOG_PATH, { interval: 250 }, (curr, prev) => {
4057
+ if (curr.size < lastSize) {
4058
+ lastSize = 0;
4059
+ pendingPartial = "";
4060
+ }
4061
+ if (curr.size > lastSize) {
4062
+ drainNewBytes(lastSize, curr.size);
4063
+ lastSize = curr.size;
4064
+ }
4065
+ });
4066
+ return () => unwatchFile(TURN_LOG_PATH);
4067
+ }
4068
+ var TURN_LOG_PATH, PREVIEW_MAX;
4069
+ var init_turnLog = __esm({
4070
+ "cli/local-cc/turnLog.ts"() {
4071
+ "use strict";
4072
+ TURN_LOG_PATH = join10(homedir9(), ".synkro", "cc_sessions", "turns.log");
4073
+ PREVIEW_MAX = 400;
4074
+ }
4075
+ });
4076
+
4077
+ // cli/local-cc/client.ts
4078
+ import { request as httpRequest } from "http";
4079
+ import { connect as connect2 } from "net";
4080
+ async function submitToChannel(role, payload, opts = {}) {
4081
+ const content = buildChannelContent(role, payload);
4082
+ const body = JSON.stringify({ role, content });
4083
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4084
+ const startedAt = Date.now();
4085
+ try {
4086
+ const result = await new Promise((resolve2, reject) => {
4087
+ const req = httpRequest({
4088
+ host: CHANNEL_HOST,
4089
+ port: CHANNEL_PORT,
4090
+ method: "POST",
4091
+ path: "/submit",
4092
+ headers: {
4093
+ "Content-Type": "application/json",
4094
+ "Content-Length": Buffer.byteLength(body)
4095
+ },
4096
+ timeout: timeoutMs
4097
+ }, (res) => {
4098
+ const chunks = [];
4099
+ res.on("data", (c) => chunks.push(c));
4100
+ res.on("end", () => {
4101
+ const text = Buffer.concat(chunks).toString("utf-8");
4102
+ if (res.statusCode !== 200) {
4103
+ reject(new LocalCCError(`channel returned ${res.statusCode}: ${text.slice(0, 500)}`));
4104
+ return;
4105
+ }
4106
+ try {
4107
+ const parsed = JSON.parse(text);
4108
+ if (parsed.error) {
4109
+ reject(new LocalCCError(parsed.error));
4110
+ return;
4111
+ }
4112
+ resolve2(String(parsed.result ?? ""));
4113
+ } catch (err) {
4114
+ reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
4115
+ }
4116
+ });
4117
+ });
4118
+ req.on("timeout", () => {
4119
+ req.destroy(new LocalCCError(`channel request timed out after ${timeoutMs}ms`));
4120
+ });
4121
+ req.on("error", (err) => {
4122
+ const msg = err.code === "ECONNREFUSED" ? `channel connection refused at ${CHANNEL_HOST}:${CHANNEL_PORT} (is the pueue task running?)` : `channel request failed: ${err.message}`;
4123
+ reject(new LocalCCError(msg, err));
4124
+ });
4125
+ req.write(body);
4126
+ req.end();
4127
+ });
4128
+ appendTurn({ startedAt, role, request: payload, result, status: "ok" });
4129
+ return result;
4130
+ } catch (err) {
4131
+ const message = err.message ?? String(err);
4132
+ const status = /timed out/i.test(message) ? "timeout" : "error";
4133
+ appendTurn({ startedAt, role, request: payload, status, error: message });
4134
+ throw err;
4135
+ }
4136
+ }
4137
+ function isChannelAvailable(timeoutMs = 500) {
4138
+ return new Promise((resolve2) => {
4139
+ const sock = connect2(CHANNEL_PORT, CHANNEL_HOST);
4140
+ const done = (ok) => {
4141
+ try {
4142
+ sock.destroy();
4143
+ } catch {
4144
+ }
4145
+ resolve2(ok);
4146
+ };
4147
+ sock.once("connect", () => done(true));
4148
+ sock.once("error", () => done(false));
4149
+ sock.setTimeout(timeoutMs, () => done(false));
4150
+ });
4151
+ }
4152
+ var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
4153
+ var init_client = __esm({
4154
+ "cli/local-cc/client.ts"() {
4155
+ "use strict";
4156
+ init_prompts();
4157
+ init_turnLog();
4158
+ CHANNEL_HOST = "127.0.0.1";
4159
+ CHANNEL_PORT = parseInt(process.env.SYNKRO_CHANNEL_PORT || "8929", 10);
4160
+ DEFAULT_TIMEOUT_MS = 9e4;
4161
+ LocalCCError = class extends Error {
4162
+ constructor(message, cause) {
4163
+ super(message);
4164
+ this.cause = cause;
4165
+ this.name = "LocalCCError";
4166
+ }
4167
+ cause;
4168
+ };
4169
+ }
4170
+ });
4171
+
4172
+ // cli/commands/install.ts
4173
+ var install_exports = {};
4174
+ __export(install_exports, {
4175
+ installCommand: () => installCommand,
4176
+ parseArgs: () => parseArgs
4177
+ });
4178
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync9, readdirSync } from "fs";
4179
+ import { homedir as homedir10 } from "os";
4180
+ import { join as join11 } from "path";
4181
+ import { execSync as execSync5 } from "child_process";
4182
+ import { createInterface as createInterface3 } from "readline";
4183
+ function sanitizeGatewayCandidate(raw) {
4184
+ if (!raw) return void 0;
4185
+ return /^https?:\/\//.test(raw) ? raw : void 0;
4186
+ }
4187
+ function parseArgs(argv) {
4188
+ const opts = {};
4189
+ for (const a of argv) {
4190
+ if (a.startsWith("--api-key=")) opts.apiKey = a.slice("--api-key=".length);
4191
+ else if (a.startsWith("--gateway=")) opts.gatewayUrl = a.slice("--gateway=".length);
4192
+ else if (a === "--skip-auth") opts.skipAuth = true;
4193
+ else if (a === "--no-mcp") opts.noMcp = true;
4194
+ else if (a === "--force" || a === "-f") opts.force = true;
4195
+ else if (a === "--link-repo") opts.linkRepo = true;
4196
+ }
4197
+ if (!opts.gatewayUrl) {
4198
+ const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
4199
+ if (fromEnv) opts.gatewayUrl = fromEnv;
4200
+ }
4201
+ return opts;
4202
+ }
4203
+ async function promptTranscriptConsent() {
4204
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
4205
+ return new Promise((resolve2) => {
4206
+ rl.question(
4207
+ "Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
4208
+ (answer) => {
4209
+ rl.close();
4210
+ const trimmed = answer.trim().toLowerCase();
4211
+ resolve2(trimmed === "" || trimmed === "y" || trimmed === "yes");
4212
+ }
4213
+ );
4214
+ });
4215
+ }
4216
+ function ensureSynkroDir() {
4217
+ mkdirSync8(SYNKRO_DIR2, { recursive: true });
4218
+ mkdirSync8(HOOKS_DIR, { recursive: true });
4219
+ mkdirSync8(BIN_DIR, { recursive: true });
4220
+ mkdirSync8(OFFSETS_DIR, { recursive: true });
4221
+ }
4222
+ function writeHookScripts() {
4223
+ const bashScriptPath = join11(HOOKS_DIR, "cc-bash-judge.sh");
4224
+ const bashFollowupScriptPath = join11(HOOKS_DIR, "cc-bash-followup.sh");
4225
+ const editCaptureScriptPath = join11(HOOKS_DIR, "cc-edit-capture.sh");
4226
+ const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.sh");
4227
+ const stopSummaryScriptPath = join11(HOOKS_DIR, "cc-stop-summary.sh");
4228
+ const sessionStartScriptPath = join11(HOOKS_DIR, "cc-session-start.sh");
4229
+ const transcriptSyncScriptPath = join11(HOOKS_DIR, "cc-transcript-sync.sh");
4230
+ writeFileSync7(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4231
+ writeFileSync7(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4232
+ writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4233
+ writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4234
+ writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4235
+ writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4236
+ writeFileSync7(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4237
+ chmodSync2(bashScriptPath, 493);
4238
+ chmodSync2(bashFollowupScriptPath, 493);
4239
+ chmodSync2(editCaptureScriptPath, 493);
4240
+ chmodSync2(editPrecheckScriptPath, 493);
4241
+ chmodSync2(stopSummaryScriptPath, 493);
4242
+ chmodSync2(sessionStartScriptPath, 493);
4243
+ chmodSync2(transcriptSyncScriptPath, 493);
4244
+ return {
4245
+ bashScript: bashScriptPath,
4246
+ bashFollowupScript: bashFollowupScriptPath,
4247
+ editCaptureScript: editCaptureScriptPath,
4248
+ editPrecheckScript: editPrecheckScriptPath,
4249
+ stopSummaryScript: stopSummaryScriptPath,
4250
+ sessionStartScript: sessionStartScriptPath,
4251
+ transcriptSyncScript: transcriptSyncScriptPath
4252
+ };
4253
+ }
4254
+ function sanitizeConfigValue(raw, maxLen = 256) {
4255
+ if (!raw) return "";
4256
+ return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
4257
+ }
4258
+ function shellQuoteSingle(value) {
4259
+ return `'${value.replace(/'/g, "'\\''")}'`;
4260
+ }
4261
+ function resolveSynkroBundle() {
4262
+ const scriptPath = process.argv[1];
4263
+ if (scriptPath && existsSync11(scriptPath)) return scriptPath;
4264
+ return null;
4265
+ }
4266
+ function writeConfigEnv(opts) {
4267
+ const credsPath = join11(SYNKRO_DIR2, "credentials.json");
4268
+ const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
4269
+ const safeUserId = sanitizeConfigValue(opts.userId);
4270
+ const safeOrgId = sanitizeConfigValue(opts.orgId);
4271
+ const safeEmail = sanitizeConfigValue(opts.email);
4272
+ const safeTier = sanitizeConfigValue(opts.tier ?? "pro", 32);
4273
+ const safeInference = sanitizeConfigValue(opts.inference ?? "fast", 16);
4274
+ const safeSynkroBin = sanitizeConfigValue(opts.synkroBin ?? "", 1024);
4275
+ const lines = [
4276
+ "# Synkro CLI config (managed by synkro install)",
4277
+ "# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
4278
+ "# and send Authorization: Bearer <access_token> on every gateway call.",
4279
+ `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
4280
+ `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4281
+ `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4282
+ `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4283
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.2")}`
4284
+ ];
4285
+ if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4286
+ if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
4287
+ if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
4288
+ if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
4289
+ if (opts.transcriptConsent !== void 0) {
4290
+ lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
4291
+ }
4292
+ lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
4293
+ lines.push("");
4294
+ writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
4295
+ chmodSync2(CONFIG_PATH2, 384);
4296
+ }
4297
+ function collectLocalMetadata() {
4298
+ const meta = { platform: process.platform };
4299
+ try {
4300
+ meta.display_name = execSync5("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
4301
+ } catch {
4302
+ }
4303
+ try {
4304
+ const remote = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 3e3 }).trim();
4305
+ const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
4306
+ const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
4307
+ const m = sshMatch || httpMatch;
4308
+ if (m) meta.active_repo = m[1];
4309
+ } catch {
4310
+ }
4311
+ try {
4312
+ meta.cc_version = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
4313
+ } catch {
4314
+ }
4315
+ const claudeDir = join11(homedir10(), ".claude");
4316
+ try {
4317
+ const settings = JSON.parse(readFileSync9(join11(claudeDir, "settings.json"), "utf-8"));
4318
+ const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
4319
+ if (plugins.length) meta.enabled_plugins = plugins;
4320
+ if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
4321
+ } catch {
4322
+ }
4323
+ try {
4324
+ const mcpCache = JSON.parse(readFileSync9(join11(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4325
+ const mcpNames = Object.keys(mcpCache);
4326
+ if (mcpNames.length) meta.mcp_servers = mcpNames;
4327
+ } catch {
4328
+ }
4329
+ try {
4330
+ const mcpList = execSync5("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
4331
+ const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
4332
+ if (connected.length) meta.mcp_servers_connected = connected;
4333
+ } catch {
4334
+ }
4335
+ try {
4336
+ const sessionsDir = join11(claudeDir, "sessions");
4337
+ const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
4338
+ for (const f of files) {
4339
+ const s = JSON.parse(readFileSync9(join11(sessionsDir, f), "utf-8"));
4340
+ if (s.version) {
4341
+ meta.cc_version = meta.cc_version || s.version;
4342
+ break;
4343
+ }
4344
+ }
4345
+ } catch {
4346
+ }
4347
+ return meta;
4348
+ }
4349
+ async function fetchUserProfile(gatewayUrl, token) {
4350
+ try {
4351
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
4352
+ headers: { "Authorization": `Bearer ${token}` }
4353
+ });
4354
+ if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false };
4355
+ const data = await resp.json();
4173
4356
  const meta = collectLocalMetadata();
4174
4357
  fetch(`${gatewayUrl}/api/v1/cli/me`, {
4175
4358
  method: "PATCH",
@@ -4179,10 +4362,11 @@ async function fetchUserProfile(gatewayUrl, token) {
4179
4362
  });
4180
4363
  return {
4181
4364
  tier: data.plan_tier ?? "pro",
4182
- inference: data.fast_inference ? "fast" : "standard"
4365
+ inference: data.fast_inference ? "fast" : "standard",
4366
+ localInference: !!data.local_inference
4183
4367
  };
4184
4368
  } catch {
4185
- return { tier: "pro", inference: "fast" };
4369
+ return { tier: "pro", inference: "fast", localInference: false };
4186
4370
  }
4187
4371
  }
4188
4372
  function assertGatewayAllowed(gatewayUrl) {
@@ -4208,19 +4392,19 @@ function assertGatewayAllowed(gatewayUrl) {
4208
4392
  }
4209
4393
  function isAlreadyInstalled() {
4210
4394
  const requiredScripts = [
4211
- join10(HOOKS_DIR, "cc-bash-judge.sh"),
4212
- join10(HOOKS_DIR, "cc-bash-followup.sh"),
4213
- join10(HOOKS_DIR, "cc-edit-precheck.sh"),
4214
- join10(HOOKS_DIR, "cc-edit-capture.sh"),
4215
- join10(HOOKS_DIR, "cc-stop-summary.sh"),
4216
- join10(HOOKS_DIR, "cc-session-start.sh")
4395
+ join11(HOOKS_DIR, "cc-bash-judge.sh"),
4396
+ join11(HOOKS_DIR, "cc-bash-followup.sh"),
4397
+ join11(HOOKS_DIR, "cc-edit-precheck.sh"),
4398
+ join11(HOOKS_DIR, "cc-edit-capture.sh"),
4399
+ join11(HOOKS_DIR, "cc-stop-summary.sh"),
4400
+ join11(HOOKS_DIR, "cc-session-start.sh")
4217
4401
  ];
4218
- if (!requiredScripts.every((p) => existsSync10(p))) return false;
4219
- if (!existsSync10(CONFIG_PATH2)) return false;
4220
- const settingsPath = join10(homedir9(), ".claude", "settings.json");
4221
- if (!existsSync10(settingsPath)) return false;
4402
+ if (!requiredScripts.every((p) => existsSync11(p))) return false;
4403
+ if (!existsSync11(CONFIG_PATH2)) return false;
4404
+ const settingsPath = join11(homedir10(), ".claude", "settings.json");
4405
+ if (!existsSync11(settingsPath)) return false;
4222
4406
  try {
4223
- const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4407
+ const settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
4224
4408
  const hooks = settings?.hooks;
4225
4409
  if (!hooks || typeof hooks !== "object") return false;
4226
4410
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -4328,9 +4512,9 @@ async function installCommand(opts = {}) {
4328
4512
  console.log(` ${scripts.transcriptSyncScript}
4329
4513
  `);
4330
4514
  for (const mode of ["edit", "bash"]) {
4331
- const pidFile = join10(SYNKRO_DIR3, "daemon", mode, "daemon.pid");
4515
+ const pidFile = join11(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
4332
4516
  try {
4333
- const pid = parseInt(readFileSync7(pidFile, "utf-8").trim(), 10);
4517
+ const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
4334
4518
  if (pid > 0) {
4335
4519
  process.kill(pid, "SIGTERM");
4336
4520
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -4338,20 +4522,6 @@ async function installCommand(opts = {}) {
4338
4522
  } catch {
4339
4523
  }
4340
4524
  }
4341
- if (isLocalCCEnabled()) {
4342
- try {
4343
- assertClaudeInstalled();
4344
- assertPueueInstalled();
4345
- const r = installLocalCC();
4346
- console.log(`Installed local-CC channel plugin at ${r.pluginPath}`);
4347
- const t = ensureRunning();
4348
- console.log(`Local-CC pueue task: id=${t.id} status=${t.status}
4349
- `);
4350
- } catch (err) {
4351
- console.warn(` \u26A0 Local-CC setup skipped: ${err.message}`);
4352
- console.warn(" Run `synkro local-cc enable` after fixing the issue.\n");
4353
- }
4354
- }
4355
4525
  let transcriptConsent = true;
4356
4526
  if (process.stdin.isTTY) {
4357
4527
  transcriptConsent = await promptTranscriptConsent();
@@ -4418,9 +4588,10 @@ async function installCommand(opts = {}) {
4418
4588
  }
4419
4589
  const profile = await fetchUserProfile(gatewayUrl, token);
4420
4590
  const synkroBundle = resolveSynkroBundle();
4421
- writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent });
4591
+ writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference });
4422
4592
  console.log(`Wrote config to ${CONFIG_PATH2}`);
4423
4593
  console.log(` inference: ${profile.inference} (server-side grading)`);
4594
+ if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
4424
4595
  if (synkroBundle) console.log(` SYNKRO_CLI_BIN=${synkroBundle}`);
4425
4596
  else console.warn(" \u26A0 Could not resolve synkro bundle path; hooks will fall back to PATH lookup of `synkro`.");
4426
4597
  try {
@@ -4430,6 +4601,26 @@ async function installCommand(opts = {}) {
4430
4601
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
4431
4602
  }
4432
4603
  console.log();
4604
+ if (profile.localInference) {
4605
+ try {
4606
+ assertClaudeInstalled();
4607
+ assertPueueInstalled();
4608
+ assertTmuxInstalled();
4609
+ const r = installLocalCC();
4610
+ console.log(`Installed local-CC channel plugin at ${r.pluginPath}`);
4611
+ const t = ensureRunning();
4612
+ console.log(`Local-CC pueue task: id=${t.id} status=${t.status}`);
4613
+ console.log("Waiting for channel...");
4614
+ const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
4615
+ if (ready) console.log(` channel ready at ${CHANNEL_HOST}:${CHANNEL_PORT}
4616
+ `);
4617
+ else console.warn(` \u26A0 channel did not come up within 60s \u2014 check \`synkro local-cc logs\`
4618
+ `);
4619
+ } catch (err) {
4620
+ console.warn(` \u26A0 Local-CC setup skipped: ${err.message}`);
4621
+ console.warn(" Install pueue, tmux, and claude, then re-run install.\n");
4622
+ }
4623
+ }
4433
4624
  if (transcriptConsent) {
4434
4625
  try {
4435
4626
  const repo = detectGitRepo2();
@@ -4476,17 +4667,17 @@ function detectGitRepo2() {
4476
4667
  function getClaudeProjectsFolder() {
4477
4668
  const cwd = process.cwd();
4478
4669
  const sanitized = "-" + cwd.replace(/\//g, "-");
4479
- const projectsDir = join10(homedir9(), ".claude", "projects", sanitized);
4480
- return existsSync10(projectsDir) ? projectsDir : null;
4670
+ const projectsDir = join11(homedir10(), ".claude", "projects", sanitized);
4671
+ return existsSync11(projectsDir) ? projectsDir : null;
4481
4672
  }
4482
4673
  function extractSessionInsights(projectsDir) {
4483
4674
  const insights = [];
4484
4675
  const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
4485
4676
  for (const file of files) {
4486
4677
  const sessionId = file.replace(".jsonl", "");
4487
- const filePath = join10(projectsDir, file);
4678
+ const filePath = join11(projectsDir, file);
4488
4679
  try {
4489
- const content = readFileSync7(filePath, "utf-8");
4680
+ const content = readFileSync9(filePath, "utf-8");
4490
4681
  const lines = content.split("\n").filter(Boolean);
4491
4682
  for (let i = 0; i < lines.length; i++) {
4492
4683
  try {
@@ -4562,7 +4753,7 @@ function extractTextContent(content) {
4562
4753
  return "";
4563
4754
  }
4564
4755
  function parseTranscriptFile(filePath) {
4565
- const content = readFileSync7(filePath, "utf-8");
4756
+ const content = readFileSync9(filePath, "utf-8");
4566
4757
  const lines = content.split("\n").filter(Boolean);
4567
4758
  const messages = [];
4568
4759
  for (let i = 0; i < lines.length; i++) {
@@ -4613,7 +4804,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
4613
4804
  const sessions = [];
4614
4805
  for (const file of batch) {
4615
4806
  const sessionId = file.replace(".jsonl", "");
4616
- const filePath = join10(projectsDir, file);
4807
+ const filePath = join11(projectsDir, file);
4617
4808
  try {
4618
4809
  const allMessages = parseTranscriptFile(filePath);
4619
4810
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -4642,18 +4833,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
4642
4833
  }
4643
4834
  for (const file of batch) {
4644
4835
  const sessionId = file.replace(".jsonl", "");
4645
- const filePath = join10(projectsDir, file);
4836
+ const filePath = join11(projectsDir, file);
4646
4837
  try {
4647
- const content = readFileSync7(filePath, "utf-8");
4838
+ const content = readFileSync9(filePath, "utf-8");
4648
4839
  const lineCount = content.split("\n").filter(Boolean).length;
4649
- writeFileSync7(join10(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
4840
+ writeFileSync7(join11(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
4650
4841
  } catch {
4651
4842
  }
4652
4843
  }
4653
4844
  }
4654
4845
  return { sessions: totalSessions, messages: totalMessages };
4655
4846
  }
4656
- var SYNKRO_DIR3, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, OFFSETS_DIR;
4847
+ var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, OFFSETS_DIR;
4657
4848
  var init_install2 = __esm({
4658
4849
  "cli/commands/install.ts"() {
4659
4850
  "use strict";
@@ -4666,14 +4857,14 @@ var init_install2 = __esm({
4666
4857
  init_projects();
4667
4858
  init_setupGithub();
4668
4859
  init_promptFetcher();
4669
- init_settings();
4670
4860
  init_install();
4671
4861
  init_pueue();
4672
- SYNKRO_DIR3 = join10(homedir9(), ".synkro");
4673
- HOOKS_DIR = join10(SYNKRO_DIR3, "hooks");
4674
- BIN_DIR = join10(SYNKRO_DIR3, "bin");
4675
- CONFIG_PATH2 = join10(SYNKRO_DIR3, "config.env");
4676
- OFFSETS_DIR = join10(SYNKRO_DIR3, ".transcript-offsets");
4862
+ init_client();
4863
+ SYNKRO_DIR2 = join11(homedir10(), ".synkro");
4864
+ HOOKS_DIR = join11(SYNKRO_DIR2, "hooks");
4865
+ BIN_DIR = join11(SYNKRO_DIR2, "bin");
4866
+ CONFIG_PATH2 = join11(SYNKRO_DIR2, "config.env");
4867
+ OFFSETS_DIR = join11(SYNKRO_DIR2, ".transcript-offsets");
4677
4868
  }
4678
4869
  });
4679
4870
 
@@ -4748,14 +4939,14 @@ var init_logout = __esm({
4748
4939
  var status_exports = {};
4749
4940
  __export(status_exports, {
4750
4941
  statusCommand: () => statusCommand
4751
- });
4752
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4753
- import { homedir as homedir10 } from "os";
4754
- import { join as join11 } from "path";
4942
+ });
4943
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4944
+ import { homedir as homedir11 } from "os";
4945
+ import { join as join12 } from "path";
4755
4946
  function readConfigEnv() {
4756
- if (!existsSync11(CONFIG_PATH3)) return {};
4947
+ if (!existsSync12(CONFIG_PATH3)) return {};
4757
4948
  const out = {};
4758
- const raw = readFileSync8(CONFIG_PATH3, "utf-8");
4949
+ const raw = readFileSync10(CONFIG_PATH3, "utf-8");
4759
4950
  for (const line of raw.split("\n")) {
4760
4951
  const trimmed = line.trim();
4761
4952
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4828,19 +5019,19 @@ async function statusCommand() {
4828
5019
  }
4829
5020
  }
4830
5021
  console.log();
4831
- const bashScript = join11(SYNKRO_DIR4, "hooks", "cc-bash-judge.sh");
4832
- const bashFollowupScript = join11(SYNKRO_DIR4, "hooks", "cc-bash-followup.sh");
4833
- const editPrecheckScript = join11(SYNKRO_DIR4, "hooks", "cc-edit-precheck.sh");
4834
- const editCaptureScript = join11(SYNKRO_DIR4, "hooks", "cc-edit-capture.sh");
4835
- const stopSummaryScript = join11(SYNKRO_DIR4, "hooks", "cc-stop-summary.sh");
4836
- const sessionStartScript = join11(SYNKRO_DIR4, "hooks", "cc-session-start.sh");
5022
+ const bashScript = join12(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
5023
+ const bashFollowupScript = join12(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
5024
+ const editPrecheckScript = join12(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
5025
+ const editCaptureScript = join12(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
5026
+ const stopSummaryScript = join12(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
5027
+ const sessionStartScript = join12(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
4837
5028
  console.log("Hook scripts:");
4838
- console.log(` ${existsSync11(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
4839
- console.log(` ${existsSync11(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
4840
- console.log(` ${existsSync11(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
4841
- console.log(` ${existsSync11(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
4842
- console.log(` ${existsSync11(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
4843
- console.log(` ${existsSync11(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
5029
+ console.log(` ${existsSync12(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
5030
+ console.log(` ${existsSync12(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
5031
+ console.log(` ${existsSync12(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
5032
+ console.log(` ${existsSync12(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
5033
+ console.log(` ${existsSync12(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
5034
+ console.log(` ${existsSync12(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
4844
5035
  console.log();
4845
5036
  const mcp = inspectMcpConfig();
4846
5037
  console.log("Guardrails MCP server (Claude Code):");
@@ -4852,7 +5043,7 @@ async function statusCommand() {
4852
5043
  console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
4853
5044
  }
4854
5045
  }
4855
- var SYNKRO_DIR4, CONFIG_PATH3;
5046
+ var SYNKRO_DIR3, CONFIG_PATH3;
4856
5047
  var init_status = __esm({
4857
5048
  "cli/commands/status.ts"() {
4858
5049
  "use strict";
@@ -4860,8 +5051,8 @@ var init_status = __esm({
4860
5051
  init_agentDetect();
4861
5052
  init_ccHookConfig();
4862
5053
  init_mcpConfig();
4863
- SYNKRO_DIR4 = join11(homedir10(), ".synkro");
4864
- CONFIG_PATH3 = join11(SYNKRO_DIR4, "config.env");
5054
+ SYNKRO_DIR3 = join12(homedir11(), ".synkro");
5055
+ CONFIG_PATH3 = join12(SYNKRO_DIR3, "config.env");
4865
5056
  }
4866
5057
  });
4867
5058
 
@@ -4950,13 +5141,13 @@ var config_exports = {};
4950
5141
  __export(config_exports, {
4951
5142
  configCommand: () => configCommand
4952
5143
  });
4953
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync12 } from "fs";
4954
- import { join as join12 } from "path";
4955
- import { homedir as homedir11 } from "os";
5144
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
5145
+ import { join as join13 } from "path";
5146
+ import { homedir as homedir12 } from "os";
4956
5147
  function readConfigEnv2() {
4957
- if (!existsSync12(CONFIG_PATH4)) return {};
5148
+ if (!existsSync13(CONFIG_PATH4)) return {};
4958
5149
  const out = {};
4959
- for (const line of readFileSync9(CONFIG_PATH4, "utf-8").split("\n")) {
5150
+ for (const line of readFileSync11(CONFIG_PATH4, "utf-8").split("\n")) {
4960
5151
  const t = line.trim();
4961
5152
  if (!t || t.startsWith("#")) continue;
4962
5153
  const eq = t.indexOf("=");
@@ -4965,11 +5156,11 @@ function readConfigEnv2() {
4965
5156
  return out;
4966
5157
  }
4967
5158
  function updateConfigValue(key, value) {
4968
- if (!existsSync12(CONFIG_PATH4)) {
5159
+ if (!existsSync13(CONFIG_PATH4)) {
4969
5160
  console.error("No config found. Run `synkro install` first.");
4970
5161
  process.exit(1);
4971
5162
  }
4972
- const lines = readFileSync9(CONFIG_PATH4, "utf-8").split("\n");
5163
+ const lines = readFileSync11(CONFIG_PATH4, "utf-8").split("\n");
4973
5164
  const pattern = new RegExp(`^${key}=`);
4974
5165
  let found = false;
4975
5166
  const updated = lines.map((line) => {
@@ -5031,13 +5222,13 @@ To change: synkro config --inference fast|standard`);
5031
5222
  updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
5032
5223
  console.log(`\u2713 Inference set to '${inferenceValue}'.`);
5033
5224
  }
5034
- var SYNKRO_DIR5, CONFIG_PATH4;
5225
+ var SYNKRO_DIR4, CONFIG_PATH4;
5035
5226
  var init_config = __esm({
5036
5227
  "cli/commands/config.ts"() {
5037
5228
  "use strict";
5038
5229
  init_stub();
5039
- SYNKRO_DIR5 = join12(homedir11(), ".synkro");
5040
- CONFIG_PATH4 = join12(SYNKRO_DIR5, "config.env");
5230
+ SYNKRO_DIR4 = join13(homedir12(), ".synkro");
5231
+ CONFIG_PATH4 = join13(SYNKRO_DIR4, "config.env");
5041
5232
  }
5042
5233
  });
5043
5234
 
@@ -5047,8 +5238,8 @@ __export(scanPr_exports, {
5047
5238
  scanPrCommand: () => scanPrCommand
5048
5239
  });
5049
5240
  import { execSync as execSync6, spawn } from "child_process";
5050
- import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
5051
- import { join as join13 } from "path";
5241
+ import { readFileSync as readFileSync12, existsSync as existsSync14 } from "fs";
5242
+ import { join as join14 } from "path";
5052
5243
  function parseMatchSpec(condition) {
5053
5244
  if (!condition.startsWith("match_spec:")) return null;
5054
5245
  try {
@@ -5527,10 +5718,10 @@ function shouldFail(findings, threshold) {
5527
5718
  return findings.some((f) => order.indexOf(f.severity) >= thresholdIdx);
5528
5719
  }
5529
5720
  function readRepoDeps() {
5530
- const pkgPath = join13(process.cwd(), "package.json");
5531
- if (!existsSync13(pkgPath)) return {};
5721
+ const pkgPath = join14(process.cwd(), "package.json");
5722
+ if (!existsSync14(pkgPath)) return {};
5532
5723
  try {
5533
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
5724
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
5534
5725
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5535
5726
  } catch {
5536
5727
  return {};
@@ -5737,414 +5928,173 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
5737
5928
  sha: activeSha,
5738
5929
  findings: allFindings,
5739
5930
  filesScanned: eligible.length,
5740
- totalLatencyMs
5741
- });
5742
- console.log(`
5743
- \u2713 Scan complete. Status: ${conclusion}.`);
5744
- if (conclusion === "failure") {
5745
- process.exit(1);
5746
- }
5747
- }
5748
- var SKIP_FILE_PATTERNS, MAX_DIFF_LINES_PER_FILE, MAX_PARALLEL_FILES;
5749
- var init_scanPr = __esm({
5750
- "cli/commands/scanPr.ts"() {
5751
- "use strict";
5752
- SKIP_FILE_PATTERNS = [
5753
- /\.lock$/i,
5754
- /\.min\./i,
5755
- /\.map$/i,
5756
- /^dist\//,
5757
- /^build\//,
5758
- /^vendor\//,
5759
- /^node_modules\//,
5760
- /^\.next\//,
5761
- /package-lock\.json$/,
5762
- /yarn\.lock$/,
5763
- /pnpm-lock\.yaml$/,
5764
- /Cargo\.lock$/,
5765
- /go\.sum$/
5766
- ];
5767
- MAX_DIFF_LINES_PER_FILE = 1e3;
5768
- MAX_PARALLEL_FILES = 10;
5769
- }
5770
- });
5771
-
5772
- // cli/commands/update.ts
5773
- var update_exports = {};
5774
- __export(update_exports, {
5775
- updateCommand: () => updateCommand
5776
- });
5777
- async function updateCommand() {
5778
- console.log("Refreshing Synkro hook configs and prompts...\n");
5779
- await installCommand();
5780
- console.log("\n\u2713 Synkro updated.");
5781
- console.log("To upgrade the CLI itself, run: npm install -g @synkro-sh/cli@latest");
5782
- }
5783
- var init_update = __esm({
5784
- "cli/commands/update.ts"() {
5785
- "use strict";
5786
- init_install2();
5787
- }
5788
- });
5789
-
5790
- // cli/commands/disconnect.ts
5791
- var disconnect_exports = {};
5792
- __export(disconnect_exports, {
5793
- disconnectCommand: () => disconnectCommand
5794
- });
5795
- import { existsSync as existsSync14, rmSync } from "fs";
5796
- import { homedir as homedir12 } from "os";
5797
- import { join as join14 } from "path";
5798
- function tearDownLocalCC() {
5799
- let hadTask = false;
5800
- try {
5801
- hadTask = !!findTask();
5802
- stopTask();
5803
- } catch {
5804
- }
5805
- console.log(`${hadTask ? "\u2713" : "\xB7"} local-cc runtime: ${hadTask ? "stopped pueue task + tmux session" : "no live task"}`);
5806
- uninstallLocalCC();
5807
- console.log("\u2713 local-cc config: cleaned ~/.claude.json entries");
5808
- }
5809
- function disconnectCommand(args2 = []) {
5810
- const purge = args2.includes("--purge");
5811
- console.log("Synkro disconnect starting...\n");
5812
- tearDownLocalCC();
5813
- const agents = detectAgents();
5814
- let sawClaudeCode = false;
5815
- for (const agent of agents) {
5816
- if (agent.kind === "claude_code") {
5817
- sawClaudeCode = true;
5818
- const removed = uninstallCCHooks(agent.settingsPath);
5819
- console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
5820
- }
5821
- }
5822
- if (sawClaudeCode) {
5823
- const mcpRemoved = uninstallMcpConfig();
5824
- console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
5825
- }
5826
- if (purge) {
5827
- if (existsSync14(SYNKRO_DIR6)) {
5828
- rmSync(SYNKRO_DIR6, { recursive: true, force: true });
5829
- console.log(`\u2713 Removed ${SYNKRO_DIR6}`);
5830
- } else {
5831
- console.log(`\xB7 ${SYNKRO_DIR6} already gone, nothing to remove`);
5832
- }
5833
- } else if (existsSync14(SYNKRO_DIR6)) {
5834
- console.log(`Config preserved at ${SYNKRO_DIR6}. Run with --purge to remove.`);
5835
- }
5836
- console.log("\nSynkro disconnected.");
5837
- }
5838
- var SYNKRO_DIR6;
5839
- var init_disconnect = __esm({
5840
- "cli/commands/disconnect.ts"() {
5841
- "use strict";
5842
- init_agentDetect();
5843
- init_ccHookConfig();
5844
- init_mcpConfig();
5845
- init_pueue();
5846
- init_install();
5847
- SYNKRO_DIR6 = join14(homedir12(), ".synkro");
5848
- }
5849
- });
5850
-
5851
- // cli/commands/uninstall.ts
5852
- var uninstall_exports = {};
5853
- __export(uninstall_exports, {
5854
- uninstallCommand: () => uninstallCommand
5855
- });
5856
- function uninstallCommand() {
5857
- console.log("Uninstalling Synkro...\n");
5858
- disconnectCommand(["--purge"]);
5859
- console.log("\nTo reinstall later: synkro install");
5860
- }
5861
- var init_uninstall = __esm({
5862
- "cli/commands/uninstall.ts"() {
5863
- "use strict";
5864
- init_disconnect();
5865
- }
5866
- });
5867
-
5868
- // cli/commands/reinstall.ts
5869
- var reinstall_exports = {};
5870
- __export(reinstall_exports, {
5871
- reinstallCommand: () => reinstallCommand
5872
- });
5873
- async function reinstallCommand() {
5874
- console.log("Reinstalling Synkro...\n");
5875
- disconnectCommand(["--purge"]);
5876
- console.log("");
5877
- await installCommand({ force: true });
5878
- console.log("\n\u2713 Synkro reinstalled.");
5879
- }
5880
- var init_reinstall = __esm({
5881
- "cli/commands/reinstall.ts"() {
5882
- "use strict";
5883
- init_disconnect();
5884
- init_install2();
5885
- }
5886
- });
5887
-
5888
- // cli/local-cc/turnLog.ts
5889
- import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync9, openSync as openSync2, readFileSync as readFileSync11, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
5890
- import { dirname as dirname5, join as join15 } from "path";
5891
- import { homedir as homedir13 } from "os";
5892
- function truncate(s, max = PREVIEW_MAX) {
5893
- if (s.length <= max) return s;
5894
- return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
5895
- }
5896
- function extractSeverity(result) {
5897
- const m = result.match(/<synkro-(?:verdict|intent)>([\s\S]*?)<\/synkro-(?:verdict|intent)>/);
5898
- if (!m) return void 0;
5899
- try {
5900
- const obj = JSON.parse(m[1]);
5901
- if (obj.severity) return String(obj.severity);
5902
- if (typeof obj.ok === "boolean") return obj.ok ? "ok" : "violations";
5903
- if (obj.type) return String(obj.type);
5904
- if (obj.verdict) return String(obj.verdict);
5905
- } catch {
5906
- }
5907
- return void 0;
5908
- }
5909
- function appendTurn(args2) {
5910
- try {
5911
- mkdirSync9(dirname5(TURN_LOG_PATH), { recursive: true });
5912
- const entry = {
5913
- ts: new Date(args2.startedAt).toISOString(),
5914
- role: args2.role,
5915
- duration_ms: Date.now() - args2.startedAt,
5916
- status: args2.status,
5917
- request_preview: truncate(args2.request),
5918
- response_preview: args2.result ? truncate(args2.result) : "",
5919
- severity: args2.result ? extractSeverity(args2.result) : void 0,
5920
- error: args2.error
5921
- };
5922
- appendFileSync(TURN_LOG_PATH, JSON.stringify(entry) + "\n", "utf-8");
5923
- } catch {
5924
- }
5925
- }
5926
- function readRecentTurns(n = 20) {
5927
- if (!existsSync15(TURN_LOG_PATH)) return [];
5928
- try {
5929
- const size = statSync(TURN_LOG_PATH).size;
5930
- if (size === 0) return [];
5931
- const text = readFileSync11(TURN_LOG_PATH, "utf-8");
5932
- const lines = text.split("\n").filter(Boolean);
5933
- const lastN = lines.slice(-n).reverse();
5934
- return lastN.map((line) => {
5935
- try {
5936
- return JSON.parse(line);
5937
- } catch {
5938
- return null;
5939
- }
5940
- }).filter((x) => x !== null);
5941
- } catch {
5942
- return [];
5943
- }
5944
- }
5945
- function followTurns(onEntry) {
5946
- try {
5947
- mkdirSync9(dirname5(TURN_LOG_PATH), { recursive: true });
5948
- if (!existsSync15(TURN_LOG_PATH)) {
5949
- appendFileSync(TURN_LOG_PATH, "", "utf-8");
5950
- }
5951
- } catch {
5952
- }
5953
- let lastSize = (() => {
5954
- try {
5955
- return statSync(TURN_LOG_PATH).size;
5956
- } catch {
5957
- return 0;
5958
- }
5959
- })();
5960
- let pendingPartial = "";
5961
- const drainNewBytes = (from, to) => {
5962
- if (to <= from) return;
5963
- let fd = null;
5964
- try {
5965
- fd = openSync2(TURN_LOG_PATH, "r");
5966
- const len = to - from;
5967
- const buf = Buffer.alloc(len);
5968
- readSync(fd, buf, 0, len, from);
5969
- const text = pendingPartial + buf.toString("utf-8");
5970
- const lastNewline = text.lastIndexOf("\n");
5971
- if (lastNewline === -1) {
5972
- pendingPartial = text;
5973
- return;
5974
- }
5975
- const complete = text.slice(0, lastNewline);
5976
- pendingPartial = text.slice(lastNewline + 1);
5977
- for (const line of complete.split("\n")) {
5978
- if (!line) continue;
5979
- try {
5980
- onEntry(JSON.parse(line));
5981
- } catch {
5982
- }
5983
- }
5984
- } catch {
5985
- } finally {
5986
- if (fd !== null) {
5987
- try {
5988
- closeSync2(fd);
5989
- } catch {
5990
- }
5991
- }
5992
- }
5993
- };
5994
- watchFile(TURN_LOG_PATH, { interval: 250 }, (curr, prev) => {
5995
- if (curr.size < lastSize) {
5996
- lastSize = 0;
5997
- pendingPartial = "";
5998
- }
5999
- if (curr.size > lastSize) {
6000
- drainNewBytes(lastSize, curr.size);
6001
- lastSize = curr.size;
6002
- }
5931
+ totalLatencyMs
6003
5932
  });
6004
- return () => unwatchFile(TURN_LOG_PATH);
5933
+ console.log(`
5934
+ \u2713 Scan complete. Status: ${conclusion}.`);
5935
+ if (conclusion === "failure") {
5936
+ process.exit(1);
5937
+ }
6005
5938
  }
6006
- var TURN_LOG_PATH, PREVIEW_MAX;
6007
- var init_turnLog = __esm({
6008
- "cli/local-cc/turnLog.ts"() {
5939
+ var SKIP_FILE_PATTERNS, MAX_DIFF_LINES_PER_FILE, MAX_PARALLEL_FILES;
5940
+ var init_scanPr = __esm({
5941
+ "cli/commands/scanPr.ts"() {
6009
5942
  "use strict";
6010
- TURN_LOG_PATH = join15(homedir13(), ".synkro", "cc_sessions", "turns.log");
6011
- PREVIEW_MAX = 400;
5943
+ SKIP_FILE_PATTERNS = [
5944
+ /\.lock$/i,
5945
+ /\.min\./i,
5946
+ /\.map$/i,
5947
+ /^dist\//,
5948
+ /^build\//,
5949
+ /^vendor\//,
5950
+ /^node_modules\//,
5951
+ /^\.next\//,
5952
+ /package-lock\.json$/,
5953
+ /yarn\.lock$/,
5954
+ /pnpm-lock\.yaml$/,
5955
+ /Cargo\.lock$/,
5956
+ /go\.sum$/
5957
+ ];
5958
+ MAX_DIFF_LINES_PER_FILE = 1e3;
5959
+ MAX_PARALLEL_FILES = 10;
6012
5960
  }
6013
5961
  });
6014
5962
 
6015
- // cli/local-cc/prompts.ts
6016
- import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
6017
- import { homedir as homedir14 } from "os";
6018
- import { join as join16 } from "path";
6019
- function loadCachedPrompts() {
6020
- if (_cached) return _cached;
6021
- if (!existsSync16(CACHE_PATH2)) {
6022
- throw new Error("Prompts cache not found. Run `synkro install` or `synkro update` first.");
5963
+ // cli/commands/update.ts
5964
+ var update_exports = {};
5965
+ __export(update_exports, {
5966
+ updateCommand: () => updateCommand
5967
+ });
5968
+ async function updateCommand() {
5969
+ console.log("Refreshing Synkro hook configs and prompts...\n");
5970
+ await installCommand();
5971
+ console.log("\n\u2713 Synkro updated.");
5972
+ console.log("To upgrade the CLI itself, run: npm install -g @synkro-sh/cli@latest");
5973
+ }
5974
+ var init_update = __esm({
5975
+ "cli/commands/update.ts"() {
5976
+ "use strict";
5977
+ init_install2();
6023
5978
  }
5979
+ });
5980
+
5981
+ // cli/commands/disconnect.ts
5982
+ var disconnect_exports = {};
5983
+ __export(disconnect_exports, {
5984
+ disconnectCommand: () => disconnectCommand
5985
+ });
5986
+ import { existsSync as existsSync15, rmSync } from "fs";
5987
+ import { homedir as homedir13 } from "os";
5988
+ import { join as join15 } from "path";
5989
+ function tearDownLocalCC() {
5990
+ let hadTask = false;
6024
5991
  try {
6025
- _cached = JSON.parse(readFileSync12(CACHE_PATH2, "utf-8"));
6026
- return _cached;
5992
+ hadTask = !!findTask();
5993
+ stopTask();
6027
5994
  } catch {
6028
- throw new Error("Prompts cache is corrupted. Run `synkro update` to refresh.");
6029
5995
  }
5996
+ console.log(`${hadTask ? "\u2713" : "\xB7"} local-cc runtime: ${hadTask ? "stopped pueue task + tmux session" : "no live task"}`);
5997
+ uninstallLocalCC();
5998
+ console.log("\u2713 local-cc config: cleaned ~/.claude.json entries");
6030
5999
  }
6031
- function getPrimer(role) {
6032
- const cache = loadCachedPrompts();
6033
- const primer = role === "grade-edit" ? cache.grader_primer_edit : cache.grader_primer_bash;
6034
- if (!primer) {
6035
- throw new Error(`No cached primer for role "${role}". Run \`synkro update\` to refresh prompts.`);
6000
+ function disconnectCommand(args2 = []) {
6001
+ const purge = args2.includes("--purge");
6002
+ console.log("Synkro disconnect starting...\n");
6003
+ tearDownLocalCC();
6004
+ const agents = detectAgents();
6005
+ let sawClaudeCode = false;
6006
+ for (const agent of agents) {
6007
+ if (agent.kind === "claude_code") {
6008
+ sawClaudeCode = true;
6009
+ const removed = uninstallCCHooks(agent.settingsPath);
6010
+ console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
6011
+ }
6036
6012
  }
6037
- return primer;
6013
+ if (sawClaudeCode) {
6014
+ const mcpRemoved = uninstallMcpConfig();
6015
+ console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
6016
+ }
6017
+ if (purge) {
6018
+ if (existsSync15(SYNKRO_DIR5)) {
6019
+ rmSync(SYNKRO_DIR5, { recursive: true, force: true });
6020
+ console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
6021
+ } else {
6022
+ console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
6023
+ }
6024
+ } else if (existsSync15(SYNKRO_DIR5)) {
6025
+ console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
6026
+ }
6027
+ console.log("\nSynkro disconnected.");
6038
6028
  }
6039
- function buildChannelContent(role, payload) {
6040
- return `${getPrimer(role)}
6029
+ var SYNKRO_DIR5;
6030
+ var init_disconnect = __esm({
6031
+ "cli/commands/disconnect.ts"() {
6032
+ "use strict";
6033
+ init_agentDetect();
6034
+ init_ccHookConfig();
6035
+ init_mcpConfig();
6036
+ init_pueue();
6037
+ init_install();
6038
+ SYNKRO_DIR5 = join15(homedir13(), ".synkro");
6039
+ }
6040
+ });
6041
6041
 
6042
- ---
6043
- PAYLOAD (the input to evaluate):
6042
+ // cli/commands/uninstall.ts
6043
+ var uninstall_exports = {};
6044
+ __export(uninstall_exports, {
6045
+ uninstallCommand: () => uninstallCommand
6046
+ });
6047
+ function uninstallCommand() {
6048
+ console.log("Uninstalling Synkro...\n");
6049
+ disconnectCommand(["--purge"]);
6050
+ console.log("\nTo reinstall later: synkro install");
6051
+ }
6052
+ var init_uninstall = __esm({
6053
+ "cli/commands/uninstall.ts"() {
6054
+ "use strict";
6055
+ init_disconnect();
6056
+ }
6057
+ });
6044
6058
 
6045
- ${payload}`;
6059
+ // cli/commands/reinstall.ts
6060
+ var reinstall_exports = {};
6061
+ __export(reinstall_exports, {
6062
+ reinstallCommand: () => reinstallCommand
6063
+ });
6064
+ async function reinstallCommand() {
6065
+ console.log("Reinstalling Synkro...\n");
6066
+ disconnectCommand(["--purge"]);
6067
+ console.log("");
6068
+ await installCommand({ force: true });
6069
+ console.log("\n\u2713 Synkro reinstalled.");
6046
6070
  }
6047
- var CACHE_PATH2, _cached;
6048
- var init_prompts = __esm({
6049
- "cli/local-cc/prompts.ts"() {
6071
+ var init_reinstall = __esm({
6072
+ "cli/commands/reinstall.ts"() {
6050
6073
  "use strict";
6051
- CACHE_PATH2 = join16(homedir14(), ".synkro", "prompts", "judge-prompts.json");
6052
- _cached = null;
6074
+ init_disconnect();
6075
+ init_install2();
6053
6076
  }
6054
6077
  });
6055
6078
 
6056
- // cli/local-cc/client.ts
6057
- import { request as httpRequest } from "http";
6058
- import { connect as connect2 } from "net";
6059
- async function submitToChannel(role, payload, opts = {}) {
6060
- const content = buildChannelContent(role, payload);
6061
- const body = JSON.stringify({ role, content });
6062
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6063
- const startedAt = Date.now();
6079
+ // cli/local-cc/settings.ts
6080
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
6081
+ import { homedir as homedir14 } from "os";
6082
+ import { join as join16 } from "path";
6083
+ function isLocalCCEnabled() {
6084
+ if (!existsSync16(CONFIG_PATH5)) return false;
6064
6085
  try {
6065
- const result = await new Promise((resolve2, reject) => {
6066
- const req = httpRequest({
6067
- host: CHANNEL_HOST,
6068
- port: CHANNEL_PORT,
6069
- method: "POST",
6070
- path: "/submit",
6071
- headers: {
6072
- "Content-Type": "application/json",
6073
- "Content-Length": Buffer.byteLength(body)
6074
- },
6075
- timeout: timeoutMs
6076
- }, (res) => {
6077
- const chunks = [];
6078
- res.on("data", (c) => chunks.push(c));
6079
- res.on("end", () => {
6080
- const text = Buffer.concat(chunks).toString("utf-8");
6081
- if (res.statusCode !== 200) {
6082
- reject(new LocalCCError(`channel returned ${res.statusCode}: ${text.slice(0, 500)}`));
6083
- return;
6084
- }
6085
- try {
6086
- const parsed = JSON.parse(text);
6087
- if (parsed.error) {
6088
- reject(new LocalCCError(parsed.error));
6089
- return;
6090
- }
6091
- resolve2(String(parsed.result ?? ""));
6092
- } catch (err) {
6093
- reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
6094
- }
6095
- });
6096
- });
6097
- req.on("timeout", () => {
6098
- req.destroy(new LocalCCError(`channel request timed out after ${timeoutMs}ms`));
6099
- });
6100
- req.on("error", (err) => {
6101
- const msg = err.code === "ECONNREFUSED" ? `channel connection refused at ${CHANNEL_HOST}:${CHANNEL_PORT} (is the pueue task running?)` : `channel request failed: ${err.message}`;
6102
- reject(new LocalCCError(msg, err));
6103
- });
6104
- req.write(body);
6105
- req.end();
6106
- });
6107
- appendTurn({ startedAt, role, request: payload, result, status: "ok" });
6108
- return result;
6109
- } catch (err) {
6110
- const message = err.message ?? String(err);
6111
- const status = /timed out/i.test(message) ? "timeout" : "error";
6112
- appendTurn({ startedAt, role, request: payload, status, error: message });
6113
- throw err;
6086
+ const content = readFileSync13(CONFIG_PATH5, "utf-8");
6087
+ const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
6088
+ return match?.[1] === "yes";
6089
+ } catch {
6090
+ return false;
6114
6091
  }
6115
6092
  }
6116
- function isChannelAvailable(timeoutMs = 500) {
6117
- return new Promise((resolve2) => {
6118
- const sock = connect2(CHANNEL_PORT, CHANNEL_HOST);
6119
- const done = (ok) => {
6120
- try {
6121
- sock.destroy();
6122
- } catch {
6123
- }
6124
- resolve2(ok);
6125
- };
6126
- sock.once("connect", () => done(true));
6127
- sock.once("error", () => done(false));
6128
- sock.setTimeout(timeoutMs, () => done(false));
6129
- });
6130
- }
6131
- var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
6132
- var init_client = __esm({
6133
- "cli/local-cc/client.ts"() {
6093
+ var CONFIG_PATH5;
6094
+ var init_settings = __esm({
6095
+ "cli/local-cc/settings.ts"() {
6134
6096
  "use strict";
6135
- init_prompts();
6136
- init_turnLog();
6137
- CHANNEL_HOST = "127.0.0.1";
6138
- CHANNEL_PORT = parseInt(process.env.SYNKRO_CHANNEL_PORT || "8929", 10);
6139
- DEFAULT_TIMEOUT_MS = 9e4;
6140
- LocalCCError = class extends Error {
6141
- constructor(message, cause) {
6142
- super(message);
6143
- this.cause = cause;
6144
- this.name = "LocalCCError";
6145
- }
6146
- cause;
6147
- };
6097
+ CONFIG_PATH5 = join16(homedir14(), ".synkro", "config.env");
6148
6098
  }
6149
6099
  });
6150
6100
 
@@ -6156,8 +6106,8 @@ __export(localCc_exports, {
6156
6106
  import { spawnSync as spawnSync3 } from "child_process";
6157
6107
  import { homedir as homedir15 } from "os";
6158
6108
  import { join as join17 } from "path";
6109
+ import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
6159
6110
  function printHelp() {
6160
- const dbPath = join17(homedir15(), ".synkro", "sessions.db");
6161
6111
  console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
6162
6112
 
6163
6113
  OVERVIEW
@@ -6199,11 +6149,10 @@ SUBCOMMANDS
6199
6149
  help Show this message
6200
6150
 
6201
6151
  CONFIGURATION
6202
- Provider toggle (single global switch, default 'inngest'):
6203
- Stored in ${dbPath} (settings table, key='inference_provider').
6152
+ Provider toggle:
6153
+ Stored server-side in your inference settings (gradingProvider).
6204
6154
  Toggle via: synkro local-cc enable / disable
6205
- Or directly via SQL:
6206
- INSERT OR REPLACE INTO settings (key, value) VALUES ('inference_provider', 'local-cc');
6155
+ Or via dashboard inference settings (set grading provider to claude-code).
6207
6156
 
6208
6157
  Claude Code session settings (scoped to ~/.synkro/cc_sessions only):
6209
6158
  ${PLUGIN_SETTINGS_PATH}
@@ -6246,8 +6195,44 @@ TROUBLESHOOTING
6246
6195
  synkro local-cc attach
6247
6196
  `);
6248
6197
  }
6198
+ function readGatewayUrl() {
6199
+ if (existsSync17(CONFIG_PATH6)) {
6200
+ const m = readFileSync14(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
6201
+ if (m) return m[1];
6202
+ }
6203
+ return "https://api.synkro.sh";
6204
+ }
6205
+ function updateLocalInferenceFlag(enabled) {
6206
+ if (!existsSync17(CONFIG_PATH6)) return;
6207
+ let content = readFileSync14(CONFIG_PATH6, "utf-8");
6208
+ const flag = enabled ? "yes" : "no";
6209
+ if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
6210
+ content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
6211
+ } else {
6212
+ content = content.trimEnd() + `
6213
+ SYNKRO_LOCAL_INFERENCE='${flag}'
6214
+ `;
6215
+ }
6216
+ writeFileSync9(CONFIG_PATH6, content, "utf-8");
6217
+ }
6218
+ async function setServerGradingProvider(provider) {
6219
+ await ensureValidToken();
6220
+ const jwt2 = getAccessToken();
6221
+ if (!jwt2) throw new Error("Not authenticated. Run `synkro install` first.");
6222
+ const gatewayUrl = readGatewayUrl();
6223
+ const body = provider ? { roles: { grading: { provider, model: "default" } } } : { roles: { grading: { provider: null, model: null } } };
6224
+ const resp = await fetch(`${gatewayUrl}/api/settings/inference?scope=user`, {
6225
+ method: "PUT",
6226
+ headers: { "Authorization": `Bearer ${jwt2}`, "Content-Type": "application/json" },
6227
+ body: JSON.stringify(body)
6228
+ });
6229
+ if (!resp.ok) {
6230
+ const text = await resp.text().catch(() => "");
6231
+ throw new Error(`Failed to update inference settings: ${resp.status} ${text.slice(0, 200)}`);
6232
+ }
6233
+ }
6249
6234
  async function cmdStatus() {
6250
- console.log(`Inference provider: ${getInferenceProvider()}`);
6235
+ console.log(`Local inference: ${isLocalCCEnabled() ? "enabled" : "disabled"}`);
6251
6236
  try {
6252
6237
  assertPueueInstalled();
6253
6238
  } catch (err) {
@@ -6281,12 +6266,16 @@ async function cmdEnable() {
6281
6266
  const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
6282
6267
  if (ready) console.log(` channel ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
6283
6268
  else console.warn(` \u26A0 channel did not come up within 60s \u2014 check \`synkro local-cc logs\``);
6284
- setInferenceProvider("local-cc");
6285
- console.log("Inference provider set to local-cc.");
6269
+ console.log("Updating inference settings...");
6270
+ await setServerGradingProvider("claude-code");
6271
+ updateLocalInferenceFlag(true);
6272
+ console.log("Grading provider set to claude-code (local inference enabled).");
6286
6273
  }
6287
- function cmdDisable() {
6288
- setInferenceProvider("inngest");
6289
- console.log("Inference provider set to inngest. (Pueue task left running \u2014 use `synkro local-cc stop` to terminate.)");
6274
+ async function cmdDisable() {
6275
+ console.log("Updating inference settings...");
6276
+ await setServerGradingProvider(null);
6277
+ updateLocalInferenceFlag(false);
6278
+ console.log("Grading provider cleared (remote inference restored). Pueue task left running \u2014 use `synkro local-cc stop` to terminate.");
6290
6279
  }
6291
6280
  async function cmdStart() {
6292
6281
  assertClaudeInstalled();
@@ -6489,6 +6478,7 @@ async function localCcCommand(args2) {
6489
6478
  process.exit(1);
6490
6479
  }
6491
6480
  }
6481
+ var CONFIG_PATH6;
6492
6482
  var init_localCc = __esm({
6493
6483
  "cli/commands/localCc.ts"() {
6494
6484
  "use strict";
@@ -6497,6 +6487,8 @@ var init_localCc = __esm({
6497
6487
  init_pueue();
6498
6488
  init_settings();
6499
6489
  init_client();
6490
+ init_stub();
6491
+ CONFIG_PATH6 = join17(homedir15(), ".synkro", "config.env");
6500
6492
  }
6501
6493
  });
6502
6494
 
@@ -6548,15 +6540,15 @@ var init_grade = __esm({
6548
6540
  });
6549
6541
 
6550
6542
  // cli/bootstrap.js
6551
- import { readFileSync as readFileSync13, existsSync as existsSync17 } from "fs";
6543
+ import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
6552
6544
  import { resolve } from "path";
6553
6545
  var envCandidates = [
6554
6546
  resolve(process.cwd(), ".env"),
6555
6547
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
6556
6548
  ];
6557
6549
  for (const envPath of envCandidates) {
6558
- if (!existsSync17(envPath)) continue;
6559
- const envContent = readFileSync13(envPath, "utf-8");
6550
+ if (!existsSync18(envPath)) continue;
6551
+ const envContent = readFileSync15(envPath, "utf-8");
6560
6552
  for (const line of envContent.split("\n")) {
6561
6553
  const trimmed = line.trim();
6562
6554
  if (!trimmed || trimmed.startsWith("#")) continue;