@synkro-sh/cli 1.3.13 → 1.3.14

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
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
4
10
  var __esm = (fn, res) => function __init() {
5
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
12
  };
@@ -2432,13 +2438,10 @@ var init_auth = __esm({
2432
2438
 
2433
2439
  // cli/api/projects.ts
2434
2440
  import { config } from "dotenv";
2435
- function resolveApiUrl() {
2436
- for (const v of [process.env.SYNKRO_CRUD_URL, process.env.SYNKRO_API_URL]) {
2437
- if (v && /^https?:\/\//.test(v)) return v;
2438
- }
2439
- return "https://api.synkro.sh/api";
2440
- }
2441
2441
  async function callApi(method, endpoint, body) {
2442
+ if (!API_URL) {
2443
+ throw new Error("SYNKRO_CRUD_URL (or SYNKRO_API_URL) is not set. Add it to your .env file.");
2444
+ }
2442
2445
  const url = `${API_URL}${endpoint}`;
2443
2446
  const accessToken = getAccessToken();
2444
2447
  const headers = {
@@ -2475,7 +2478,7 @@ var init_projects = __esm({
2475
2478
  "use strict";
2476
2479
  init_auth();
2477
2480
  config({ quiet: true });
2478
- API_URL = resolveApiUrl();
2481
+ API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
2479
2482
  }
2480
2483
  });
2481
2484
 
@@ -2512,7 +2515,7 @@ jobs:
2512
2515
  run: |
2513
2516
  npm config set prefix ~/.npm-global
2514
2517
  npm install -g @synkro-sh/cli @anthropic-ai/claude-code
2515
- echo "~/.npm-global/bin" >> $GITHUB_PATH
2518
+ echo "$HOME/.npm-global/bin" >> $GITHUB_PATH
2516
2519
 
2517
2520
  - name: Run Synkro PR scan
2518
2521
  run: synkro-cli scan-pr
@@ -2580,7 +2583,7 @@ async function listAccessibleRepos(opts) {
2580
2583
  const repos = [];
2581
2584
  let page = 1;
2582
2585
  while (page <= 5) {
2583
- const url = `https://api.github.com/user/repos?per_page=100&page=${page}&affiliation=owner,collaborator,organization_member`;
2586
+ const url = `https://api.github.com/user/repos?per_page=100&page=${page}&affiliation=owner,collaborator`;
2584
2587
  const resp = await fetch(url, {
2585
2588
  headers: {
2586
2589
  Authorization: `Bearer ${opts.token}`,
@@ -2603,9 +2606,7 @@ async function listAccessibleRepos(opts) {
2603
2606
  }
2604
2607
  async function pushSecretsToRepo(opts, owner, repo, secrets) {
2605
2608
  const pubkey = await getRepoPublicKey(opts, owner, repo);
2606
- if (secrets.claudeCodeOauthToken) {
2607
- await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
2608
- }
2609
+ await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
2609
2610
  await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
2610
2611
  }
2611
2612
  function writeWorkflowFile(repoRootPath) {
@@ -2639,7 +2640,7 @@ var init_githubSetup = __esm({
2639
2640
  });
2640
2641
 
2641
2642
  // cli/commands/repoConnect.ts
2642
- import { execSync as execSync2, execFileSync } from "child_process";
2643
+ import { execSync as execSync2 } from "child_process";
2643
2644
  import { createServer as createServer2 } from "http";
2644
2645
  import { createInterface } from "readline";
2645
2646
  function detectGitRepo() {
@@ -2712,10 +2713,11 @@ function waitForGithubToken() {
2712
2713
  });
2713
2714
  }
2714
2715
  function openBrowser2(url) {
2716
+ const { execFile: execFile2 } = __require("child_process");
2715
2717
  const plat = process.platform;
2716
- if (plat === "darwin") execFileSync("open", [url], { stdio: "ignore" });
2717
- else if (plat === "win32") execFileSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
2718
- else execFileSync("xdg-open", [url], { stdio: "ignore" });
2718
+ if (plat === "darwin") execFile2("open", [url]);
2719
+ else if (plat === "win32") execFile2("cmd", ["/c", "start", "", url]);
2720
+ else execFile2("xdg-open", [url]);
2719
2721
  }
2720
2722
  async function connectGithubAndSelectRepos() {
2721
2723
  const url = `${SYNKRO_WEB_AUTH_URL2}/cli-github?port=${GITHUB_PORT}`;
@@ -2748,30 +2750,8 @@ async function connectGithubAndSelectRepos() {
2748
2750
  rl.close();
2749
2751
  }
2750
2752
  }
2751
- async function promptRepoConnection(opts) {
2753
+ async function promptRepoConnection() {
2752
2754
  const localRepo = detectGitRepo();
2753
- if (opts?.linkRepo !== void 0) {
2754
- if (opts.linkRepo && localRepo) {
2755
- try {
2756
- const existing = await listProjects();
2757
- const alreadyLinked = existing.some(
2758
- (p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
2759
- );
2760
- if (!alreadyLinked) {
2761
- await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
2762
- console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
2763
- } else {
2764
- console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
2765
- }
2766
- } catch (err) {
2767
- console.warn(` \u26A0 Could not link repo: ${err.message}`);
2768
- }
2769
- } else if (opts.linkRepo) {
2770
- console.warn(" \u26A0 --link-repo specified but not in a git repo. Skipping.");
2771
- }
2772
- console.log();
2773
- return;
2774
- }
2775
2755
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2776
2756
  try {
2777
2757
  console.log("Connect repos to Synkro:\n");
@@ -2849,330 +2829,16 @@ var init_repoConnect = __esm({
2849
2829
  }
2850
2830
  });
2851
2831
 
2852
- // cli/commands/setupGithub.ts
2853
- var setupGithub_exports = {};
2854
- __export(setupGithub_exports, {
2855
- setupGithubCommand: () => setupGithubCommand
2856
- });
2857
- import { createInterface as createInterface2 } from "readline/promises";
2858
- import { stdin as input, stdout as output } from "process";
2859
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2860
- import { homedir as homedir4 } from "os";
2861
- import { join as join5 } from "path";
2862
- function readConfig() {
2863
- if (!existsSync6(CONFIG_PATH)) return {};
2864
- const out = {};
2865
- for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
2866
- const t = line.trim();
2867
- if (!t || t.startsWith("#")) continue;
2868
- const eq = t.indexOf("=");
2869
- if (eq > 0) {
2870
- let val = t.slice(eq + 1).trim();
2871
- if (val.startsWith("'") && val.endsWith("'") || val.startsWith('"') && val.endsWith('"')) {
2872
- val = val.slice(1, -1);
2873
- }
2874
- out[t.slice(0, eq).trim()] = val;
2875
- }
2876
- }
2877
- return out;
2878
- }
2879
- async function prompt(rl, q, opts = {}) {
2880
- if (opts.silent) {
2881
- process.stdout.write(q);
2882
- const wasRaw = process.stdin.isRaw;
2883
- if (process.stdin.setRawMode) process.stdin.setRawMode(true);
2884
- return await new Promise((resolve2) => {
2885
- let chunk = "";
2886
- const onData = (data) => {
2887
- const s = data.toString("utf-8");
2888
- if (s === "\r" || s === "\n" || s === "\r\n") {
2889
- process.stdin.removeListener("data", onData);
2890
- if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
2891
- process.stdout.write("\n");
2892
- resolve2(chunk);
2893
- return;
2894
- }
2895
- if (s === "") {
2896
- process.exit(130);
2897
- }
2898
- if (s === "\x7F" || s === "\b") {
2899
- chunk = chunk.slice(0, -1);
2900
- return;
2901
- }
2902
- chunk += s;
2903
- };
2904
- process.stdin.on("data", onData);
2905
- });
2906
- }
2907
- return await rl.question(q);
2908
- }
2909
- async function setupGithubCommand(opts = {}) {
2910
- if (!isAuthenticated()) {
2911
- console.error("Not authenticated. Run `synkro-cli login` first.");
2912
- process.exit(1);
2913
- }
2914
- const config2 = readConfig();
2915
- const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
2916
- const jwt2 = getAccessToken();
2917
- if (!jwt2) {
2918
- console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
2919
- process.exit(1);
2920
- }
2921
- console.log("Requesting CI API key from Synkro...");
2922
- let synkroCiApiKey;
2923
- try {
2924
- const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
2925
- method: "POST",
2926
- headers: {
2927
- "Authorization": `Bearer ${jwt2}`,
2928
- "Content-Type": "application/json"
2929
- },
2930
- body: "{}"
2931
- });
2932
- if (!resp.ok) {
2933
- const errText = await resp.text().catch(() => "");
2934
- console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
2935
- process.exit(1);
2936
- }
2937
- const minted = await resp.json();
2938
- synkroCiApiKey = minted.api_key;
2939
- console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
2940
- } catch (err) {
2941
- console.error(`Failed to mint CI API key: ${err.message}`);
2942
- process.exit(1);
2943
- }
2944
- let _rl = null;
2945
- function getRL() {
2946
- if (!_rl) _rl = createInterface2({ input, output });
2947
- return _rl;
2948
- }
2949
- let ghToken = opts.githubToken || "";
2950
- if (!ghToken) {
2951
- try {
2952
- const connResp = await fetch(`${gatewayUrl}/api/integrations/github/token`, {
2953
- headers: { "Authorization": `Bearer ${jwt2}` }
2954
- });
2955
- if (connResp.ok) {
2956
- const data = await connResp.json();
2957
- ghToken = data.token;
2958
- console.log(" \u2713 Using existing GitHub connection");
2959
- }
2960
- } catch {
2961
- }
2962
- }
2963
- if (!ghToken) {
2964
- console.log("Opening browser for GitHub authorization...");
2965
- const RAW_WEB_AUTH_URL3 = process.env.SYNKRO_WEB_AUTH_URL;
2966
- const webAuthUrl = RAW_WEB_AUTH_URL3 && /^https?:\/\//.test(RAW_WEB_AUTH_URL3) ? RAW_WEB_AUTH_URL3 : "https://app.synkro.sh";
2967
- openBrowser2(`${webAuthUrl}/cli-github?port=8101`);
2968
- console.log("Waiting for GitHub authorization...");
2969
- try {
2970
- ghToken = await waitForGithubToken();
2971
- console.log(" \u2713 GitHub authorized");
2972
- } catch (err) {
2973
- console.error(`GitHub authorization failed: ${err.message}`);
2974
- _rl?.close();
2975
- process.exit(1);
2976
- }
2977
- }
2978
- if (!ghToken) {
2979
- console.error("No GitHub token available.");
2980
- _rl?.close();
2981
- process.exit(1);
2982
- }
2983
- let claudeToken = opts.claudeOauthToken || "";
2984
- if (!claudeToken && !opts.skipClaudeToken) {
2985
- try {
2986
- const { execSync: execSyncImport } = await import("child_process");
2987
- const { tmpdir } = await import("os");
2988
- const tmpFile = join5(tmpdir(), `synkro-claude-token-${Date.now()}.txt`);
2989
- console.log("\nGenerating Claude Code OAuth token \u2014 complete the browser auth...");
2990
- try {
2991
- if (process.platform === "darwin" || process.platform === "linux") {
2992
- execSyncImport(`script -q ${tmpFile} claude setup-token`, {
2993
- timeout: 12e4,
2994
- stdio: "inherit"
2995
- });
2996
- } else {
2997
- execSyncImport(`claude setup-token > "${tmpFile}" 2>&1`, {
2998
- timeout: 12e4,
2999
- stdio: "inherit",
3000
- shell: true
3001
- });
3002
- }
3003
- const { readFileSync: readSync, unlinkSync: unlinkSync3 } = await import("fs");
3004
- const rawOutput = readSync(tmpFile, "utf-8");
3005
- try {
3006
- unlinkSync3(tmpFile);
3007
- } catch {
3008
- }
3009
- const cleaned = rawOutput.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/[\r\n]+/g, " ");
3010
- const match = cleaned.match(/(sk-ant-oat01-[A-Za-z0-9_-]{50,})/);
3011
- claudeToken = match?.[1] || "";
3012
- } catch {
3013
- try {
3014
- const { unlinkSync: unlinkSync3 } = await import("fs");
3015
- unlinkSync3(tmpFile);
3016
- } catch {
3017
- }
3018
- }
3019
- if (claudeToken) {
3020
- console.log(" \u2713 Captured Claude Code OAuth token.");
3021
- process.stdout.write(" Verifying token... ");
3022
- try {
3023
- const { execSync: verifyExec } = await import("child_process");
3024
- const result = verifyExec('claude --print "respond with just ok"', {
3025
- encoding: "utf-8",
3026
- timeout: 3e4,
3027
- env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken },
3028
- stdio: ["pipe", "pipe", "pipe"]
3029
- }).trim().toLowerCase();
3030
- if (result.includes("ok")) {
3031
- console.log("\u2713 inference works.\n");
3032
- } else {
3033
- console.log("\u26A0 unexpected response, token may not work.\n");
3034
- }
3035
- } catch {
3036
- console.log("\u26A0 verification failed, token may not work.\n");
3037
- }
3038
- } else {
3039
- console.warn(" \u26A0 Could not capture token. PR scans will use BYOK inference only.\n");
3040
- }
3041
- } catch {
3042
- console.warn(" \u26A0 claude setup-token failed. PR scans will use BYOK inference only.\n");
3043
- }
3044
- }
3045
- console.log("\nFetching accessible repos...");
3046
- const repos = await listAccessibleRepos({ token: ghToken });
3047
- if (repos.length === 0) {
3048
- console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3049
- _rl?.close();
3050
- process.exit(1);
3051
- }
3052
- let selected;
3053
- if (opts.nonInteractive) {
3054
- let currentFullName = null;
3055
- try {
3056
- const { execSync: gs } = await import("child_process");
3057
- const remoteUrl = gs("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
3058
- const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
3059
- if (m) currentFullName = m[1];
3060
- } catch {
3061
- }
3062
- if (currentFullName) {
3063
- const match = repos.find(
3064
- (r) => r.full_name === currentFullName || r.full_name.toLowerCase() === currentFullName.toLowerCase()
3065
- );
3066
- if (match) {
3067
- selected = [match];
3068
- console.log(` Auto-selected current repo: ${match.full_name}`);
3069
- } else {
3070
- console.warn(` \u26A0 Current repo "${currentFullName}" not found in accessible repos. Skipping PR scan setup.`);
3071
- console.warn(" Run `synkro-cli setup-github` manually to select a repo.");
3072
- return;
3073
- }
3074
- } else {
3075
- console.warn(" \u26A0 Not in a GitHub repo. Skipping PR scan setup.");
3076
- console.warn(" Run `synkro-cli setup-github` from inside a repo.");
3077
- return;
3078
- }
3079
- } else {
3080
- console.log(`
3081
- Found ${repos.length} accessible repo(s):
3082
- `);
3083
- repos.slice(0, 100).forEach((r, i) => {
3084
- console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
3085
- });
3086
- console.log();
3087
- const rl = getRL();
3088
- const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
3089
- const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
3090
- if (selectedIdx.length === 0) {
3091
- console.error("No valid selections.");
3092
- rl.close();
3093
- process.exit(1);
3094
- }
3095
- selected = selectedIdx.map((i) => repos[i]);
3096
- console.log(`
3097
- Will push secrets to ${selected.length} repo(s):`);
3098
- for (const r of selected) console.log(` \u2022 ${r.full_name}`);
3099
- console.log();
3100
- const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
3101
- if (confirm !== "yes" && confirm !== "y") {
3102
- console.log("Cancelled.");
3103
- rl.close();
3104
- process.exit(0);
3105
- }
3106
- }
3107
- _rl?.close();
3108
- console.log();
3109
- for (const r of selected) {
3110
- process.stdout.write(`Pushing secrets to ${r.full_name}... `);
3111
- try {
3112
- await pushSecretsToRepo(
3113
- { token: ghToken },
3114
- r.owner,
3115
- r.repo,
3116
- {
3117
- claudeCodeOauthToken: claudeToken,
3118
- synkroApiKey: synkroCiApiKey
3119
- }
3120
- );
3121
- console.log("\u2713");
3122
- } catch (err) {
3123
- console.log(`\u2717 (${err instanceof Error ? err.message : String(err)})`);
3124
- }
3125
- }
3126
- console.log();
3127
- const gitRoot = findGitRoot(process.cwd());
3128
- if (gitRoot) {
3129
- const written = writeWorkflowFile(gitRoot);
3130
- if (written) {
3131
- console.log(`Wrote workflow: ${written}`);
3132
- console.log("Commit and push it to enable PR scanning.");
3133
- }
3134
- } else {
3135
- console.log("Not in a git repo. To enable scanning, add this file to your repo:");
3136
- console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
3137
- console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
3138
- }
3139
- console.log();
3140
- console.log("\u2713 PR scan setup complete.");
3141
- console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
3142
- console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
3143
- }
3144
- var SYNKRO_DIR, CONFIG_PATH;
3145
- var init_setupGithub = __esm({
3146
- "cli/commands/setupGithub.ts"() {
3147
- "use strict";
3148
- init_githubSetup();
3149
- init_stub();
3150
- init_repoConnect();
3151
- SYNKRO_DIR = join5(homedir4(), ".synkro");
3152
- CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
3153
- }
3154
- });
3155
-
3156
2832
  // cli/commands/install.ts
3157
2833
  var install_exports = {};
3158
2834
  __export(install_exports, {
3159
2835
  installCommand: () => installCommand,
3160
2836
  parseArgs: () => parseArgs
3161
2837
  });
3162
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
3163
- import { createInterface as createInterface3 } from "readline/promises";
3164
- import { stdin as stdinStream, stdout as stdoutStream } from "process";
3165
- import { homedir as homedir5 } from "os";
3166
- import { join as join6 } from "path";
2838
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync4, readdirSync } from "fs";
2839
+ import { homedir as homedir4 } from "os";
2840
+ import { join as join5 } from "path";
3167
2841
  import { execSync as execSync3 } from "child_process";
3168
- async function promptYesNo(question, defaultYes = true) {
3169
- const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3170
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
3171
- const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
3172
- rl.close();
3173
- if (answer === "") return defaultYes;
3174
- return answer === "y" || answer === "yes";
3175
- }
3176
2842
  function sanitizeGatewayCandidate(raw) {
3177
2843
  if (!raw) return void 0;
3178
2844
  return /^https?:\/\//.test(raw) ? raw : void 0;
@@ -3185,17 +2851,6 @@ function parseArgs(argv) {
3185
2851
  else if (a === "--skip-auth") opts.skipAuth = true;
3186
2852
  else if (a === "--no-mcp") opts.noMcp = true;
3187
2853
  else if (a === "--force" || a === "-f") opts.force = true;
3188
- else if (a === "--non-interactive") opts.nonInteractive = true;
3189
- else if (a === "--link-repo") opts.linkRepo = true;
3190
- else if (a === "--sync-transcripts") opts.syncTranscripts = true;
3191
- else if (a === "--no-sync-transcripts") opts.syncTranscripts = false;
3192
- else if (a === "--pr-scan=claude-oauth") opts.prScan = "claude-oauth";
3193
- else if (a === "--pr-scan=byok") opts.prScan = "byok";
3194
- else if (a === "--pr-scan=skip") opts.prScan = "skip";
3195
- else if (a.startsWith("--pr-scan=")) opts.prScan = a.slice("--pr-scan=".length);
3196
- else if (a.startsWith("--inference-key=")) opts.inferenceKey = a.slice("--inference-key=".length);
3197
- else if (a.startsWith("--inference-model=")) opts.inferenceModel = a.slice("--inference-model=".length);
3198
- else if (a.startsWith("--github-token=")) opts.githubToken = a.slice("--github-token=".length);
3199
2854
  }
3200
2855
  if (!opts.gatewayUrl) {
3201
2856
  const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
@@ -3204,7 +2859,7 @@ function parseArgs(argv) {
3204
2859
  return opts;
3205
2860
  }
3206
2861
  function ensureSynkroDir() {
3207
- mkdirSync5(SYNKRO_DIR2, { recursive: true });
2862
+ mkdirSync5(SYNKRO_DIR, { recursive: true });
3208
2863
  mkdirSync5(HOOKS_DIR, { recursive: true });
3209
2864
  mkdirSync5(BIN_DIR, { recursive: true });
3210
2865
  mkdirSync5(OFFSETS_DIR, { recursive: true });
@@ -3218,13 +2873,13 @@ function writeGraderDaemon() {
3218
2873
  chmodSync(GRADER_PRIMER_BASH_PATH, 420);
3219
2874
  }
3220
2875
  function writeHookScripts() {
3221
- const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
3222
- const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
3223
- const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
3224
- const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
3225
- const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
3226
- const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
3227
- const transcriptSyncScriptPath = join6(HOOKS_DIR, "cc-transcript-sync.sh");
2876
+ const bashScriptPath = join5(HOOKS_DIR, "cc-bash-judge.sh");
2877
+ const bashFollowupScriptPath = join5(HOOKS_DIR, "cc-bash-followup.sh");
2878
+ const editCaptureScriptPath = join5(HOOKS_DIR, "cc-edit-capture.sh");
2879
+ const editPrecheckScriptPath = join5(HOOKS_DIR, "cc-edit-precheck.sh");
2880
+ const stopSummaryScriptPath = join5(HOOKS_DIR, "cc-stop-summary.sh");
2881
+ const sessionStartScriptPath = join5(HOOKS_DIR, "cc-session-start.sh");
2882
+ const transcriptSyncScriptPath = join5(HOOKS_DIR, "cc-transcript-sync.sh");
3228
2883
  writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
3229
2884
  writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
3230
2885
  writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
@@ -3257,7 +2912,7 @@ function shellQuoteSingle(value) {
3257
2912
  return `'${value.replace(/'/g, "'\\''")}'`;
3258
2913
  }
3259
2914
  function writeConfigEnv(opts) {
3260
- const credsPath = join6(SYNKRO_DIR2, "credentials.json");
2915
+ const credsPath = join5(SYNKRO_DIR, "credentials.json");
3261
2916
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
3262
2917
  const safeUserId = sanitizeConfigValue(opts.userId);
3263
2918
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -3270,14 +2925,14 @@ function writeConfigEnv(opts) {
3270
2925
  `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
3271
2926
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3272
2927
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3273
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.13")}`
2928
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.14")}`
3274
2929
  ];
3275
2930
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3276
2931
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
3277
2932
  if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
3278
2933
  lines.push("");
3279
- writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
3280
- chmodSync(CONFIG_PATH2, 384);
2934
+ writeFileSync5(CONFIG_PATH, lines.join("\n"), "utf-8");
2935
+ chmodSync(CONFIG_PATH, 384);
3281
2936
  }
3282
2937
  function assertGatewayAllowed(gatewayUrl) {
3283
2938
  let parsed;
@@ -3302,19 +2957,19 @@ function assertGatewayAllowed(gatewayUrl) {
3302
2957
  }
3303
2958
  function isAlreadyInstalled() {
3304
2959
  const requiredScripts = [
3305
- join6(HOOKS_DIR, "cc-bash-judge.sh"),
3306
- join6(HOOKS_DIR, "cc-bash-followup.sh"),
3307
- join6(HOOKS_DIR, "cc-edit-precheck.sh"),
3308
- join6(HOOKS_DIR, "cc-edit-capture.sh"),
3309
- join6(HOOKS_DIR, "cc-stop-summary.sh"),
3310
- join6(HOOKS_DIR, "cc-session-start.sh")
2960
+ join5(HOOKS_DIR, "cc-bash-judge.sh"),
2961
+ join5(HOOKS_DIR, "cc-bash-followup.sh"),
2962
+ join5(HOOKS_DIR, "cc-edit-precheck.sh"),
2963
+ join5(HOOKS_DIR, "cc-edit-capture.sh"),
2964
+ join5(HOOKS_DIR, "cc-stop-summary.sh"),
2965
+ join5(HOOKS_DIR, "cc-session-start.sh")
3311
2966
  ];
3312
- if (!requiredScripts.every((p) => existsSync7(p))) return false;
3313
- if (!existsSync7(CONFIG_PATH2)) return false;
3314
- const settingsPath = join6(homedir5(), ".claude", "settings.json");
3315
- if (!existsSync7(settingsPath)) return false;
2967
+ if (!requiredScripts.every((p) => existsSync6(p))) return false;
2968
+ if (!existsSync6(CONFIG_PATH)) return false;
2969
+ const settingsPath = join5(homedir4(), ".claude", "settings.json");
2970
+ if (!existsSync6(settingsPath)) return false;
3316
2971
  try {
3317
- const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2972
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
3318
2973
  const hooks = settings?.hooks;
3319
2974
  if (!hooks || typeof hooks !== "object") return false;
3320
2975
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -3373,11 +3028,7 @@ async function installCommand(opts = {}) {
3373
3028
  console.error("No access token available after auth.");
3374
3029
  process.exit(1);
3375
3030
  }
3376
- if (opts.nonInteractive) {
3377
- await promptRepoConnection({ linkRepo: opts.linkRepo ?? true });
3378
- } else {
3379
- await promptRepoConnection();
3380
- }
3031
+ await promptRepoConnection();
3381
3032
  const agents = detectAgents();
3382
3033
  if (agents.length === 0) {
3383
3034
  console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
@@ -3401,9 +3052,9 @@ async function installCommand(opts = {}) {
3401
3052
  `);
3402
3053
  writeGraderDaemon();
3403
3054
  for (const mode of ["edit", "bash"]) {
3404
- const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
3055
+ const pidFile = join5(SYNKRO_DIR, "daemon", mode, "daemon.pid");
3405
3056
  try {
3406
- const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
3057
+ const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
3407
3058
  if (pid > 0) {
3408
3059
  process.kill(pid, "SIGTERM");
3409
3060
  console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
@@ -3471,158 +3122,38 @@ async function installCommand(opts = {}) {
3471
3122
  } catch {
3472
3123
  }
3473
3124
  writeConfigEnv({ gatewayUrl, userId, orgId, email });
3474
- console.log(`Wrote config to ${CONFIG_PATH2}
3475
- `);
3476
- const repo = detectGitRepo2();
3477
- if (repo && getClaudeProjectsFolder()) {
3478
- const syncHistory = opts.nonInteractive ? opts.syncTranscripts ?? true : await promptYesNo("Sync Claude Code session history for this repo?");
3479
- if (syncHistory) {
3480
- try {
3481
- const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
3482
- if (ingested > 0) {
3483
- console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
3484
- console.log(" This helps the safety judge understand your workflow.\n");
3485
- }
3486
- } catch (err) {
3487
- console.warn(` \u26A0 Session indexing skipped: ${err.message}
3488
- `);
3489
- }
3490
- try {
3491
- const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
3492
- if (result.messages > 0) {
3493
- console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
3494
- console.log(" This data will be used to suggest guardrail rules.\n");
3495
- }
3496
- } catch (err) {
3497
- console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3125
+ console.log(`Wrote config to ${CONFIG_PATH}
3498
3126
  `);
3127
+ try {
3128
+ const repo = detectGitRepo2();
3129
+ if (repo) {
3130
+ const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
3131
+ if (ingested > 0) {
3132
+ console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
3133
+ console.log(" This helps the safety judge understand your workflow.\n");
3499
3134
  }
3500
- } else {
3501
- console.log(" Skipped session history sync.\n");
3502
3135
  }
3136
+ } catch (err) {
3137
+ console.warn(` \u26A0 Session indexing skipped: ${err.message}
3138
+ `);
3503
3139
  }
3504
- function detectProviderFromKey(key) {
3505
- if (key.startsWith("sk-ant-")) return "anthropic";
3506
- if (key.startsWith("sk-") || key.startsWith("sk-proj-")) return "openai";
3507
- if (key.startsWith("AIza")) return "gemini";
3508
- return null;
3509
- }
3510
- const PROVIDER_MODELS = {
3511
- anthropic: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
3512
- openai: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
3513
- gemini: ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"]
3514
- };
3515
- if (repo) {
3516
- let ghConnected = false;
3517
- let ghLogin = "";
3518
- try {
3519
- const resp = await fetch(`${gatewayUrl}/api/integrations/github/connect`, {
3520
- headers: { "Authorization": `Bearer ${token}` }
3521
- });
3522
- if (resp.ok) {
3523
- const data = await resp.json();
3524
- ghConnected = data.connected;
3525
- ghLogin = data.github_login || "";
3140
+ try {
3141
+ const repo = detectGitRepo2();
3142
+ if (repo) {
3143
+ const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
3144
+ if (result.messages > 0) {
3145
+ console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
3146
+ console.log(" This data will be used to suggest guardrail rules.\n");
3526
3147
  }
3527
- } catch {
3528
- }
3529
- if (ghConnected) {
3530
- console.log(`GitHub is connected (${ghLogin || "linked via dashboard"}).`);
3531
3148
  }
3532
- const prScanChoice = opts.nonInteractive ? opts.prScan ?? "skip" : null;
3533
- const setupGh = prScanChoice !== null ? prScanChoice !== "skip" : await promptYesNo("Set up GitHub PR scanning?");
3534
- if (setupGh) {
3535
- let choice;
3536
- if (prScanChoice) {
3537
- choice = prScanChoice === "claude-oauth" ? "1" : prScanChoice === "byok" ? "2" : "3";
3538
- } else {
3539
- const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3540
- console.log("\nHow should PR scans authenticate for AI review?\n");
3541
- console.log(" 1. Claude Code OAuth \u2014 opens browser, auto-captures token");
3542
- console.log(" 2. Use your own API key \u2014 paste an Anthropic, OpenAI, or Gemini key");
3543
- console.log(" 3. Skip for now\n");
3544
- choice = (await rl.question("Choice [1/2/3]: ")).trim();
3545
- rl.close();
3546
- }
3547
- if (choice === "1") {
3548
- try {
3549
- await setupGithubCommand({ githubToken: opts.githubToken, nonInteractive: opts.nonInteractive });
3550
- } catch (err) {
3551
- console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3552
- console.warn(" Run `synkro-cli setup-github` to retry.\n");
3553
- }
3554
- } else if (choice === "2") {
3555
- let apiKey = opts.inferenceKey || "";
3556
- if (!apiKey) {
3557
- const rl2 = createInterface3({ input: stdinStream, output: stdoutStream });
3558
- apiKey = (await rl2.question("Paste your API key: ")).trim();
3559
- rl2.close();
3560
- }
3561
- if (apiKey) {
3562
- const provider = detectProviderFromKey(apiKey);
3563
- if (provider) {
3564
- let selectedModel = opts.inferenceModel || "";
3565
- if (!selectedModel) {
3566
- const models = PROVIDER_MODELS[provider];
3567
- console.log(`
3568
- Detected provider: ${provider}`);
3569
- console.log(" Select a model for classification + grading:\n");
3570
- models.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
3571
- console.log();
3572
- const rl3 = createInterface3({ input: stdinStream, output: stdoutStream });
3573
- const modelChoice = (await rl3.question(` Model [1-${models.length}]: `)).trim();
3574
- rl3.close();
3575
- const modelIdx = parseInt(modelChoice, 10) - 1;
3576
- selectedModel = modelIdx >= 0 && modelIdx < models.length ? models[modelIdx] : models[0];
3577
- }
3578
- console.log(` Using: ${selectedModel}`);
3579
- try {
3580
- const resp = await fetch(`${gatewayUrl}/api/v1/settings/inference`, {
3581
- method: "PUT",
3582
- headers: {
3583
- "Authorization": `Bearer ${token}`,
3584
- "Content-Type": "application/json"
3585
- },
3586
- body: JSON.stringify({
3587
- providers: { [provider]: { api_key: apiKey } },
3588
- roles: {
3589
- classification: { provider, model: selectedModel },
3590
- grading: { provider, model: selectedModel }
3591
- }
3592
- })
3593
- });
3594
- if (resp.ok) {
3595
- console.log(` \u2713 Saved ${provider} key and configured classification + grading with ${selectedModel}.
3596
- `);
3597
- } else {
3598
- const err = await resp.text().catch(() => "");
3599
- console.warn(` \u26A0 Failed to save key: ${err.slice(0, 200)}
3600
- `);
3601
- }
3602
- } catch (err) {
3603
- console.warn(` \u26A0 Failed to save key: ${err.message}
3149
+ } catch (err) {
3150
+ console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3604
3151
  `);
3605
- }
3606
- } else {
3607
- console.warn(" \u26A0 Could not detect provider from key format. Configure manually in the dashboard.\n");
3608
- }
3609
- }
3610
- try {
3611
- await setupGithubCommand({ skipClaudeToken: true, githubToken: opts.githubToken, nonInteractive: opts.nonInteractive });
3612
- } catch (err) {
3613
- console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3614
- console.warn(" Run `synkro-cli setup-github` to retry.\n");
3615
- }
3616
- } else {
3617
- console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3618
- }
3619
- } else {
3620
- console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3621
- }
3622
3152
  }
3623
3153
  console.log("\u2713 Synkro installed.");
3624
3154
  console.log();
3625
3155
  console.log("Next steps:");
3156
+ console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
3626
3157
  console.log(" \u2022 synkro-cli status (check what is configured)");
3627
3158
  }
3628
3159
  function detectGitRepo2() {
@@ -3636,18 +3167,18 @@ function detectGitRepo2() {
3636
3167
  }
3637
3168
  function getClaudeProjectsFolder() {
3638
3169
  const cwd = process.cwd();
3639
- const sanitized = cwd.replace(/\//g, "-");
3640
- const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
3641
- return existsSync7(projectsDir) ? projectsDir : null;
3170
+ const sanitized = "-" + cwd.replace(/\//g, "-");
3171
+ const projectsDir = join5(homedir4(), ".claude", "projects", sanitized);
3172
+ return existsSync6(projectsDir) ? projectsDir : null;
3642
3173
  }
3643
3174
  function extractSessionInsights(projectsDir) {
3644
3175
  const insights = [];
3645
3176
  const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
3646
3177
  for (const file of files) {
3647
3178
  const sessionId = file.replace(".jsonl", "");
3648
- const filePath = join6(projectsDir, file);
3179
+ const filePath = join5(projectsDir, file);
3649
3180
  try {
3650
- const content = readFileSync5(filePath, "utf-8");
3181
+ const content = readFileSync4(filePath, "utf-8");
3651
3182
  const lines = content.split("\n").filter(Boolean);
3652
3183
  for (let i = 0; i < lines.length; i++) {
3653
3184
  try {
@@ -3723,7 +3254,7 @@ function extractTextContent(content) {
3723
3254
  return "";
3724
3255
  }
3725
3256
  function parseTranscriptFile(filePath) {
3726
- const content = readFileSync5(filePath, "utf-8");
3257
+ const content = readFileSync4(filePath, "utf-8");
3727
3258
  const lines = content.split("\n").filter(Boolean);
3728
3259
  const messages = [];
3729
3260
  for (let i = 0; i < lines.length; i++) {
@@ -3774,7 +3305,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3774
3305
  const sessions = [];
3775
3306
  for (const file of batch) {
3776
3307
  const sessionId = file.replace(".jsonl", "");
3777
- const filePath = join6(projectsDir, file);
3308
+ const filePath = join5(projectsDir, file);
3778
3309
  try {
3779
3310
  const allMessages = parseTranscriptFile(filePath);
3780
3311
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -3803,18 +3334,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3803
3334
  }
3804
3335
  for (const file of batch) {
3805
3336
  const sessionId = file.replace(".jsonl", "");
3806
- const filePath = join6(projectsDir, file);
3337
+ const filePath = join5(projectsDir, file);
3807
3338
  try {
3808
- const content = readFileSync5(filePath, "utf-8");
3339
+ const content = readFileSync4(filePath, "utf-8");
3809
3340
  const lineCount = content.split("\n").filter(Boolean).length;
3810
- writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3341
+ writeFileSync5(join5(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3811
3342
  } catch {
3812
3343
  }
3813
3344
  }
3814
3345
  }
3815
3346
  return { sessions: totalSessions, messages: totalMessages };
3816
3347
  }
3817
- var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
3348
+ var SYNKRO_DIR, HOOKS_DIR, BIN_DIR, CONFIG_PATH, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
3818
3349
  var init_install = __esm({
3819
3350
  "cli/commands/install.ts"() {
3820
3351
  "use strict";
@@ -3825,15 +3356,14 @@ var init_install = __esm({
3825
3356
  init_graderDaemon();
3826
3357
  init_stub();
3827
3358
  init_repoConnect();
3828
- init_setupGithub();
3829
- SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3830
- HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
3831
- BIN_DIR = join6(SYNKRO_DIR2, "bin");
3832
- CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
3833
- GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
3834
- GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
3835
- GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
3836
- OFFSETS_DIR = join6(SYNKRO_DIR2, ".transcript-offsets");
3359
+ SYNKRO_DIR = join5(homedir4(), ".synkro");
3360
+ HOOKS_DIR = join5(SYNKRO_DIR, "hooks");
3361
+ BIN_DIR = join5(SYNKRO_DIR, "bin");
3362
+ CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
3363
+ GRADER_DAEMON_PATH = join5(BIN_DIR, "grader_daemon.py");
3364
+ GRADER_PRIMER_EDIT_PATH = join5(SYNKRO_DIR, "grader-primer-edit.txt");
3365
+ GRADER_PRIMER_BASH_PATH = join5(SYNKRO_DIR, "grader-primer-bash.txt");
3366
+ OFFSETS_DIR = join5(SYNKRO_DIR, ".transcript-offsets");
3837
3367
  }
3838
3368
  });
3839
3369
 
@@ -3909,13 +3439,13 @@ var status_exports = {};
3909
3439
  __export(status_exports, {
3910
3440
  statusCommand: () => statusCommand
3911
3441
  });
3912
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3913
- import { homedir as homedir6 } from "os";
3914
- import { join as join7 } from "path";
3442
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
3443
+ import { homedir as homedir5 } from "os";
3444
+ import { join as join6 } from "path";
3915
3445
  function readConfigEnv() {
3916
- if (!existsSync8(CONFIG_PATH3)) return {};
3446
+ if (!existsSync7(CONFIG_PATH2)) return {};
3917
3447
  const out = {};
3918
- const raw = readFileSync6(CONFIG_PATH3, "utf-8");
3448
+ const raw = readFileSync5(CONFIG_PATH2, "utf-8");
3919
3449
  for (const line of raw.split("\n")) {
3920
3450
  const trimmed = line.trim();
3921
3451
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -3946,10 +3476,10 @@ function statusCommand() {
3946
3476
  console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
3947
3477
  const info2 = getUserInfo();
3948
3478
  const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
3949
- const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
3479
+ const tierCacheFile = join6(SYNKRO_DIR2, `.tier-cache-${userId}`);
3950
3480
  let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
3951
- if (!inferenceTier && existsSync8(tierCacheFile)) {
3952
- inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
3481
+ if (!inferenceTier && existsSync7(tierCacheFile)) {
3482
+ inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
3953
3483
  }
3954
3484
  const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
3955
3485
  console.log(` inference: ${tierLabel}`);
@@ -3976,19 +3506,19 @@ function statusCommand() {
3976
3506
  }
3977
3507
  }
3978
3508
  console.log();
3979
- const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
3980
- const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
3981
- const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
3982
- const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
3983
- const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
3984
- const sessionStartScript = join7(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
3509
+ const bashScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-judge.sh");
3510
+ const bashFollowupScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-followup.sh");
3511
+ const editPrecheckScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-precheck.sh");
3512
+ const editCaptureScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-capture.sh");
3513
+ const stopSummaryScript = join6(SYNKRO_DIR2, "hooks", "cc-stop-summary.sh");
3514
+ const sessionStartScript = join6(SYNKRO_DIR2, "hooks", "cc-session-start.sh");
3985
3515
  console.log("Hook scripts:");
3986
- console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
3987
- console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
3988
- console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
3989
- console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
3990
- console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
3991
- console.log(` ${existsSync8(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
3516
+ console.log(` ${existsSync7(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
3517
+ console.log(` ${existsSync7(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
3518
+ console.log(` ${existsSync7(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
3519
+ console.log(` ${existsSync7(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
3520
+ console.log(` ${existsSync7(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
3521
+ console.log(` ${existsSync7(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
3992
3522
  console.log();
3993
3523
  const mcp = inspectMcpConfig();
3994
3524
  console.log("Guardrails MCP server (Claude Code):");
@@ -4000,7 +3530,7 @@ function statusCommand() {
4000
3530
  console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
4001
3531
  }
4002
3532
  }
4003
- var SYNKRO_DIR3, CONFIG_PATH3;
3533
+ var SYNKRO_DIR2, CONFIG_PATH2;
4004
3534
  var init_status = __esm({
4005
3535
  "cli/commands/status.ts"() {
4006
3536
  "use strict";
@@ -4008,8 +3538,8 @@ var init_status = __esm({
4008
3538
  init_agentDetect();
4009
3539
  init_ccHookConfig();
4010
3540
  init_mcpConfig();
4011
- SYNKRO_DIR3 = join7(homedir6(), ".synkro");
4012
- CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
3541
+ SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3542
+ CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
4013
3543
  }
4014
3544
  });
4015
3545
 
@@ -4039,7 +3569,7 @@ var unlink_exports = {};
4039
3569
  __export(unlink_exports, {
4040
3570
  unlinkCommand: () => unlinkCommand
4041
3571
  });
4042
- import { createInterface as createInterface4 } from "readline";
3572
+ import { createInterface as createInterface2 } from "readline";
4043
3573
  function ask2(rl, question) {
4044
3574
  return new Promise((resolve2) => rl.question(question, resolve2));
4045
3575
  }
@@ -4067,7 +3597,7 @@ async function unlinkCommand() {
4067
3597
  console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
4068
3598
  });
4069
3599
  console.log();
4070
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3600
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
4071
3601
  try {
4072
3602
  const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
4073
3603
  const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
@@ -4093,6 +3623,194 @@ var init_unlink = __esm({
4093
3623
  }
4094
3624
  });
4095
3625
 
3626
+ // cli/commands/setupGithub.ts
3627
+ var setupGithub_exports = {};
3628
+ __export(setupGithub_exports, {
3629
+ setupGithubCommand: () => setupGithubCommand
3630
+ });
3631
+ import { createInterface as createInterface3 } from "readline/promises";
3632
+ import { stdin as input, stdout as output } from "process";
3633
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3634
+ import { homedir as homedir6 } from "os";
3635
+ import { join as join7 } from "path";
3636
+ function readConfig() {
3637
+ if (!existsSync8(CONFIG_PATH3)) return {};
3638
+ const out = {};
3639
+ for (const line of readFileSync6(CONFIG_PATH3, "utf-8").split("\n")) {
3640
+ const t = line.trim();
3641
+ if (!t || t.startsWith("#")) continue;
3642
+ const eq = t.indexOf("=");
3643
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
3644
+ }
3645
+ return out;
3646
+ }
3647
+ async function prompt(rl, q, opts = {}) {
3648
+ if (opts.silent) {
3649
+ process.stdout.write(q);
3650
+ const wasRaw = process.stdin.isRaw;
3651
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
3652
+ return await new Promise((resolve2) => {
3653
+ let chunk = "";
3654
+ const onData = (data) => {
3655
+ const s = data.toString("utf-8");
3656
+ if (s === "\r" || s === "\n" || s === "\r\n") {
3657
+ process.stdin.removeListener("data", onData);
3658
+ if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
3659
+ process.stdout.write("\n");
3660
+ resolve2(chunk);
3661
+ return;
3662
+ }
3663
+ if (s === "") {
3664
+ process.exit(130);
3665
+ }
3666
+ if (s === "\x7F" || s === "\b") {
3667
+ chunk = chunk.slice(0, -1);
3668
+ return;
3669
+ }
3670
+ chunk += s;
3671
+ };
3672
+ process.stdin.on("data", onData);
3673
+ });
3674
+ }
3675
+ return await rl.question(q);
3676
+ }
3677
+ async function setupGithubCommand() {
3678
+ if (!isAuthenticated()) {
3679
+ console.error("Not authenticated. Run `synkro-cli login` first.");
3680
+ process.exit(1);
3681
+ }
3682
+ const config2 = readConfig();
3683
+ const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
3684
+ const jwt2 = getAccessToken();
3685
+ if (!jwt2) {
3686
+ console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
3687
+ process.exit(1);
3688
+ }
3689
+ console.log("Requesting CI API key from Synkro...");
3690
+ let synkroCiApiKey;
3691
+ try {
3692
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
3693
+ method: "POST",
3694
+ headers: {
3695
+ "Authorization": `Bearer ${jwt2}`,
3696
+ "Content-Type": "application/json"
3697
+ },
3698
+ body: "{}"
3699
+ });
3700
+ if (!resp.ok) {
3701
+ const errText = await resp.text().catch(() => "");
3702
+ console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
3703
+ process.exit(1);
3704
+ }
3705
+ const minted = await resp.json();
3706
+ synkroCiApiKey = minted.api_key;
3707
+ console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
3708
+ } catch (err) {
3709
+ console.error(`Failed to mint CI API key: ${err.message}`);
3710
+ process.exit(1);
3711
+ }
3712
+ const rl = createInterface3({ input, output });
3713
+ console.log("Synkro PR scan setup\n");
3714
+ console.log("Requirements:");
3715
+ console.log(" \u2022 Claude Code Pro or Max subscription (for `claude setup-token`)");
3716
+ console.log(" \u2022 A GitHub personal access token with `repo` scope");
3717
+ console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
3718
+ const ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
3719
+ if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
3720
+ console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
3721
+ rl.close();
3722
+ process.exit(1);
3723
+ }
3724
+ console.log("\nNow get your Claude Code OAuth token:");
3725
+ console.log(" 1. In another terminal, run: claude setup-token");
3726
+ console.log(" 2. Complete the browser flow");
3727
+ console.log(" 3. Copy the resulting sk-ant-oat01-... token\n");
3728
+ const claudeToken = (await prompt(rl, "Claude Code OAuth token (paste): ", { silent: true })).trim();
3729
+ if (!claudeToken.startsWith("sk-ant-oat01-")) {
3730
+ console.error("Invalid token. Expected sk-ant-oat01-... \u2014 generate one with `claude setup-token`.");
3731
+ rl.close();
3732
+ process.exit(1);
3733
+ }
3734
+ console.log("\nFetching accessible repos...");
3735
+ const repos = await listAccessibleRepos({ token: ghToken });
3736
+ if (repos.length === 0) {
3737
+ console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3738
+ rl.close();
3739
+ process.exit(1);
3740
+ }
3741
+ console.log(`
3742
+ Found ${repos.length} accessible repo(s):
3743
+ `);
3744
+ repos.slice(0, 100).forEach((r, i) => {
3745
+ console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
3746
+ });
3747
+ console.log();
3748
+ const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
3749
+ const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
3750
+ if (selectedIdx.length === 0) {
3751
+ console.error("No valid selections.");
3752
+ rl.close();
3753
+ process.exit(1);
3754
+ }
3755
+ const selected = selectedIdx.map((i) => repos[i]);
3756
+ console.log(`
3757
+ Will push secrets to ${selected.length} repo(s):`);
3758
+ for (const r of selected) console.log(` \u2022 ${r.full_name}`);
3759
+ console.log();
3760
+ const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
3761
+ if (confirm !== "yes" && confirm !== "y") {
3762
+ console.log("Cancelled.");
3763
+ rl.close();
3764
+ process.exit(0);
3765
+ }
3766
+ rl.close();
3767
+ console.log();
3768
+ for (const r of selected) {
3769
+ process.stdout.write(`Pushing secrets to ${r.full_name}... `);
3770
+ try {
3771
+ await pushSecretsToRepo(
3772
+ { token: ghToken },
3773
+ r.owner,
3774
+ r.repo,
3775
+ {
3776
+ claudeCodeOauthToken: claudeToken,
3777
+ synkroApiKey: synkroCiApiKey
3778
+ }
3779
+ );
3780
+ console.log("\u2713");
3781
+ } catch (err) {
3782
+ console.log(`\u2717 (${err.message})`);
3783
+ }
3784
+ }
3785
+ console.log();
3786
+ const gitRoot = findGitRoot(process.cwd());
3787
+ if (gitRoot) {
3788
+ const written = writeWorkflowFile(gitRoot);
3789
+ if (written) {
3790
+ console.log(`Wrote workflow: ${written}`);
3791
+ console.log("Commit and push it to enable PR scanning.");
3792
+ }
3793
+ } else {
3794
+ console.log("Not in a git repo. To enable scanning, add this file to your repo:");
3795
+ console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
3796
+ console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
3797
+ }
3798
+ console.log();
3799
+ console.log("\u2713 PR scan setup complete.");
3800
+ console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
3801
+ console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
3802
+ }
3803
+ var SYNKRO_DIR3, CONFIG_PATH3;
3804
+ var init_setupGithub = __esm({
3805
+ "cli/commands/setupGithub.ts"() {
3806
+ "use strict";
3807
+ init_githubSetup();
3808
+ init_stub();
3809
+ SYNKRO_DIR3 = join7(homedir6(), ".synkro");
3810
+ CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
3811
+ }
3812
+ });
3813
+
4096
3814
  // cli/commands/scanPr.ts
4097
3815
  var scanPr_exports = {};
4098
3816
  __export(scanPr_exports, {
@@ -4270,14 +3988,17 @@ ${hunks}`;
4270
3988
  }
4271
3989
  );
4272
3990
  let stdout = "";
3991
+ let stderr = "";
4273
3992
  proc.stdout.on("data", (chunk) => {
4274
3993
  stdout += chunk.toString();
4275
3994
  });
4276
- proc.stderr.on("data", () => {
3995
+ proc.stderr.on("data", (chunk) => {
3996
+ stderr += chunk.toString();
4277
3997
  });
4278
3998
  proc.on("close", (code) => {
4279
3999
  const latencyMs = Date.now() - t0;
4280
4000
  if (code !== 0) {
4001
+ console.warn(` claude exited ${code}: ${stderr.slice(0, 300)}`);
4281
4002
  resolve2({ findings: [], latencyMs });
4282
4003
  return;
4283
4004
  }
@@ -4298,7 +4019,8 @@ ${hunks}`;
4298
4019
  fix: f.fix
4299
4020
  }));
4300
4021
  resolve2({ findings, latencyMs });
4301
- } catch {
4022
+ } catch (parseErr) {
4023
+ console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
4302
4024
  resolve2({ findings: [], latencyMs });
4303
4025
  }
4304
4026
  });
@@ -4628,16 +4350,7 @@ Usage:
4628
4350
  synkro <command> [options] (alias)
4629
4351
 
4630
4352
  Commands:
4631
- install [options] Install Synkro hooks for detected agents (Claude Code, etc.)
4632
- --force Reinstall from scratch
4633
- --non-interactive Skip all interactive prompts (use flags)
4634
- --link-repo Auto-link the local git repo
4635
- --sync-transcripts Sync CC session history (default in non-interactive)
4636
- --no-sync-transcripts Skip transcript sync
4637
- --pr-scan=MODE PR scan auth: claude-oauth, byok, or skip
4638
- --inference-key=KEY BYOK API key (Anthropic, OpenAI, or Gemini)
4639
- --inference-model=M Model for classification + grading
4640
- --github-token=TOK GitHub PAT for pushing secrets + workflow
4353
+ install [--force] Install Synkro hooks for detected agents (Claude Code, etc.)
4641
4354
  login Authenticate with Synkro (browser OAuth via WorkOS)
4642
4355
  logout Clear local credentials
4643
4356
  status Show current setup state