@synkro-sh/cli 1.3.12 → 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
@@ -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,323 +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
- const rl = createInterface2({ input, output });
2945
- let ghToken = opts.githubToken || "";
2946
- if (!ghToken) {
2947
- try {
2948
- const connResp = await fetch(`${gatewayUrl}/api/integrations/github/token`, {
2949
- headers: { "Authorization": `Bearer ${jwt2}` }
2950
- });
2951
- if (connResp.ok) {
2952
- const data = await connResp.json();
2953
- ghToken = data.token;
2954
- console.log(" \u2713 Using existing GitHub connection");
2955
- }
2956
- } catch {
2957
- }
2958
- }
2959
- if (!ghToken) {
2960
- console.log("Opening browser for GitHub authorization...");
2961
- const RAW_WEB_AUTH_URL3 = process.env.SYNKRO_WEB_AUTH_URL;
2962
- const webAuthUrl = RAW_WEB_AUTH_URL3 && /^https?:\/\//.test(RAW_WEB_AUTH_URL3) ? RAW_WEB_AUTH_URL3 : "https://app.synkro.sh";
2963
- openBrowser2(`${webAuthUrl}/cli-github?port=8101`);
2964
- console.log("Waiting for GitHub authorization...");
2965
- try {
2966
- ghToken = await waitForGithubToken();
2967
- console.log(" \u2713 GitHub authorized");
2968
- } catch (err) {
2969
- console.error(`GitHub authorization failed: ${err.message}`);
2970
- rl.close();
2971
- process.exit(1);
2972
- }
2973
- }
2974
- if (!ghToken) {
2975
- console.error("No GitHub token available.");
2976
- rl.close();
2977
- process.exit(1);
2978
- }
2979
- let claudeToken = opts.claudeOauthToken || "";
2980
- if (!claudeToken && !opts.skipClaudeToken) {
2981
- try {
2982
- const { execSync: execSyncImport } = await import("child_process");
2983
- const { tmpdir } = await import("os");
2984
- const tmpFile = join5(tmpdir(), `synkro-claude-token-${Date.now()}.txt`);
2985
- console.log("\nGenerating Claude Code OAuth token \u2014 complete the browser auth...");
2986
- try {
2987
- if (process.platform === "darwin" || process.platform === "linux") {
2988
- execSyncImport(`script -q ${tmpFile} claude setup-token`, {
2989
- timeout: 12e4,
2990
- stdio: "inherit"
2991
- });
2992
- } else {
2993
- execSyncImport(`claude setup-token > "${tmpFile}" 2>&1`, {
2994
- timeout: 12e4,
2995
- stdio: "inherit",
2996
- shell: true
2997
- });
2998
- }
2999
- const { readFileSync: readSync, unlinkSync: unlinkSync3 } = await import("fs");
3000
- const rawOutput = readSync(tmpFile, "utf-8");
3001
- try {
3002
- unlinkSync3(tmpFile);
3003
- } catch {
3004
- }
3005
- const cleaned = rawOutput.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/[\r\n]+/g, " ");
3006
- const match = cleaned.match(/(sk-ant-oat01-[A-Za-z0-9_-]{50,})/);
3007
- claudeToken = match?.[1] || "";
3008
- } catch {
3009
- try {
3010
- const { unlinkSync: unlinkSync3 } = await import("fs");
3011
- unlinkSync3(tmpFile);
3012
- } catch {
3013
- }
3014
- }
3015
- if (claudeToken) {
3016
- console.log(" \u2713 Captured Claude Code OAuth token.");
3017
- process.stdout.write(" Verifying token... ");
3018
- try {
3019
- const { execSync: verifyExec } = await import("child_process");
3020
- const result = verifyExec('claude --print "respond with just ok"', {
3021
- encoding: "utf-8",
3022
- timeout: 3e4,
3023
- env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken },
3024
- stdio: ["pipe", "pipe", "pipe"]
3025
- }).trim().toLowerCase();
3026
- if (result.includes("ok")) {
3027
- console.log("\u2713 inference works.\n");
3028
- } else {
3029
- console.log("\u26A0 unexpected response, token may not work.\n");
3030
- }
3031
- } catch {
3032
- console.log("\u26A0 verification failed, token may not work.\n");
3033
- }
3034
- } else {
3035
- console.warn(" \u26A0 Could not capture token. PR scans will use BYOK inference only.\n");
3036
- }
3037
- } catch {
3038
- console.warn(" \u26A0 claude setup-token failed. PR scans will use BYOK inference only.\n");
3039
- }
3040
- }
3041
- console.log("\nFetching accessible repos...");
3042
- const repos = await listAccessibleRepos({ token: ghToken });
3043
- if (repos.length === 0) {
3044
- console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3045
- rl.close();
3046
- process.exit(1);
3047
- }
3048
- let selected;
3049
- if (opts.nonInteractive) {
3050
- let currentFullName = null;
3051
- try {
3052
- const { execSync: gs } = await import("child_process");
3053
- const remoteUrl = gs("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
3054
- const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
3055
- if (m) currentFullName = m[1];
3056
- } catch {
3057
- }
3058
- if (currentFullName) {
3059
- const match = repos.find((r) => r.full_name === currentFullName);
3060
- if (match) {
3061
- selected = [match];
3062
- console.log(` Auto-selected current repo: ${match.full_name}`);
3063
- } else {
3064
- console.error(`Current repo "${currentFullName}" not found in accessible repos.`);
3065
- rl.close();
3066
- process.exit(1);
3067
- }
3068
- } else {
3069
- console.error("Not in a GitHub repo. Cannot auto-select in non-interactive mode.");
3070
- rl.close();
3071
- process.exit(1);
3072
- }
3073
- } else {
3074
- console.log(`
3075
- Found ${repos.length} accessible repo(s):
3076
- `);
3077
- repos.slice(0, 100).forEach((r, i) => {
3078
- console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
3079
- });
3080
- console.log();
3081
- const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
3082
- const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
3083
- if (selectedIdx.length === 0) {
3084
- console.error("No valid selections.");
3085
- rl.close();
3086
- process.exit(1);
3087
- }
3088
- selected = selectedIdx.map((i) => repos[i]);
3089
- console.log(`
3090
- Will push secrets to ${selected.length} repo(s):`);
3091
- for (const r of selected) console.log(` \u2022 ${r.full_name}`);
3092
- console.log();
3093
- const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
3094
- if (confirm !== "yes" && confirm !== "y") {
3095
- console.log("Cancelled.");
3096
- rl.close();
3097
- process.exit(0);
3098
- }
3099
- }
3100
- rl.close();
3101
- console.log();
3102
- for (const r of selected) {
3103
- process.stdout.write(`Pushing secrets to ${r.full_name}... `);
3104
- try {
3105
- await pushSecretsToRepo(
3106
- { token: ghToken },
3107
- r.owner,
3108
- r.repo,
3109
- {
3110
- claudeCodeOauthToken: claudeToken,
3111
- synkroApiKey: synkroCiApiKey
3112
- }
3113
- );
3114
- console.log("\u2713");
3115
- } catch (err) {
3116
- console.log(`\u2717 (${err.message})`);
3117
- }
3118
- }
3119
- console.log();
3120
- const gitRoot = findGitRoot(process.cwd());
3121
- if (gitRoot) {
3122
- const written = writeWorkflowFile(gitRoot);
3123
- if (written) {
3124
- console.log(`Wrote workflow: ${written}`);
3125
- console.log("Commit and push it to enable PR scanning.");
3126
- }
3127
- } else {
3128
- console.log("Not in a git repo. To enable scanning, add this file to your repo:");
3129
- console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
3130
- console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
3131
- }
3132
- console.log();
3133
- console.log("\u2713 PR scan setup complete.");
3134
- console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
3135
- console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
3136
- }
3137
- var SYNKRO_DIR, CONFIG_PATH;
3138
- var init_setupGithub = __esm({
3139
- "cli/commands/setupGithub.ts"() {
3140
- "use strict";
3141
- init_githubSetup();
3142
- init_stub();
3143
- init_repoConnect();
3144
- SYNKRO_DIR = join5(homedir4(), ".synkro");
3145
- CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
3146
- }
3147
- });
3148
-
3149
2832
  // cli/commands/install.ts
3150
2833
  var install_exports = {};
3151
2834
  __export(install_exports, {
3152
2835
  installCommand: () => installCommand,
3153
2836
  parseArgs: () => parseArgs
3154
2837
  });
3155
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
3156
- import { createInterface as createInterface3 } from "readline/promises";
3157
- import { stdin as stdinStream, stdout as stdoutStream } from "process";
3158
- import { homedir as homedir5 } from "os";
3159
- 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";
3160
2841
  import { execSync as execSync3 } from "child_process";
3161
- async function promptYesNo(question, defaultYes = true) {
3162
- const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3163
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
3164
- const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
3165
- rl.close();
3166
- if (answer === "") return defaultYes;
3167
- return answer === "y" || answer === "yes";
3168
- }
3169
2842
  function sanitizeGatewayCandidate(raw) {
3170
2843
  if (!raw) return void 0;
3171
2844
  return /^https?:\/\//.test(raw) ? raw : void 0;
@@ -3178,17 +2851,6 @@ function parseArgs(argv) {
3178
2851
  else if (a === "--skip-auth") opts.skipAuth = true;
3179
2852
  else if (a === "--no-mcp") opts.noMcp = true;
3180
2853
  else if (a === "--force" || a === "-f") opts.force = true;
3181
- else if (a === "--non-interactive") opts.nonInteractive = true;
3182
- else if (a === "--link-repo") opts.linkRepo = true;
3183
- else if (a === "--sync-transcripts") opts.syncTranscripts = true;
3184
- else if (a === "--no-sync-transcripts") opts.syncTranscripts = false;
3185
- else if (a === "--pr-scan=claude-oauth") opts.prScan = "claude-oauth";
3186
- else if (a === "--pr-scan=byok") opts.prScan = "byok";
3187
- else if (a === "--pr-scan=skip") opts.prScan = "skip";
3188
- else if (a.startsWith("--pr-scan=")) opts.prScan = a.slice("--pr-scan=".length);
3189
- else if (a.startsWith("--inference-key=")) opts.inferenceKey = a.slice("--inference-key=".length);
3190
- else if (a.startsWith("--inference-model=")) opts.inferenceModel = a.slice("--inference-model=".length);
3191
- else if (a.startsWith("--github-token=")) opts.githubToken = a.slice("--github-token=".length);
3192
2854
  }
3193
2855
  if (!opts.gatewayUrl) {
3194
2856
  const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
@@ -3197,7 +2859,7 @@ function parseArgs(argv) {
3197
2859
  return opts;
3198
2860
  }
3199
2861
  function ensureSynkroDir() {
3200
- mkdirSync5(SYNKRO_DIR2, { recursive: true });
2862
+ mkdirSync5(SYNKRO_DIR, { recursive: true });
3201
2863
  mkdirSync5(HOOKS_DIR, { recursive: true });
3202
2864
  mkdirSync5(BIN_DIR, { recursive: true });
3203
2865
  mkdirSync5(OFFSETS_DIR, { recursive: true });
@@ -3211,13 +2873,13 @@ function writeGraderDaemon() {
3211
2873
  chmodSync(GRADER_PRIMER_BASH_PATH, 420);
3212
2874
  }
3213
2875
  function writeHookScripts() {
3214
- const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
3215
- const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
3216
- const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
3217
- const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
3218
- const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
3219
- const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
3220
- 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");
3221
2883
  writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
3222
2884
  writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
3223
2885
  writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
@@ -3250,7 +2912,7 @@ function shellQuoteSingle(value) {
3250
2912
  return `'${value.replace(/'/g, "'\\''")}'`;
3251
2913
  }
3252
2914
  function writeConfigEnv(opts) {
3253
- const credsPath = join6(SYNKRO_DIR2, "credentials.json");
2915
+ const credsPath = join5(SYNKRO_DIR, "credentials.json");
3254
2916
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
3255
2917
  const safeUserId = sanitizeConfigValue(opts.userId);
3256
2918
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -3263,14 +2925,14 @@ function writeConfigEnv(opts) {
3263
2925
  `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
3264
2926
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3265
2927
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3266
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.12")}`
2928
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.14")}`
3267
2929
  ];
3268
2930
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3269
2931
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
3270
2932
  if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
3271
2933
  lines.push("");
3272
- writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
3273
- chmodSync(CONFIG_PATH2, 384);
2934
+ writeFileSync5(CONFIG_PATH, lines.join("\n"), "utf-8");
2935
+ chmodSync(CONFIG_PATH, 384);
3274
2936
  }
3275
2937
  function assertGatewayAllowed(gatewayUrl) {
3276
2938
  let parsed;
@@ -3295,19 +2957,19 @@ function assertGatewayAllowed(gatewayUrl) {
3295
2957
  }
3296
2958
  function isAlreadyInstalled() {
3297
2959
  const requiredScripts = [
3298
- join6(HOOKS_DIR, "cc-bash-judge.sh"),
3299
- join6(HOOKS_DIR, "cc-bash-followup.sh"),
3300
- join6(HOOKS_DIR, "cc-edit-precheck.sh"),
3301
- join6(HOOKS_DIR, "cc-edit-capture.sh"),
3302
- join6(HOOKS_DIR, "cc-stop-summary.sh"),
3303
- 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")
3304
2966
  ];
3305
- if (!requiredScripts.every((p) => existsSync7(p))) return false;
3306
- if (!existsSync7(CONFIG_PATH2)) return false;
3307
- const settingsPath = join6(homedir5(), ".claude", "settings.json");
3308
- 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;
3309
2971
  try {
3310
- const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2972
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
3311
2973
  const hooks = settings?.hooks;
3312
2974
  if (!hooks || typeof hooks !== "object") return false;
3313
2975
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -3366,11 +3028,7 @@ async function installCommand(opts = {}) {
3366
3028
  console.error("No access token available after auth.");
3367
3029
  process.exit(1);
3368
3030
  }
3369
- if (opts.nonInteractive) {
3370
- await promptRepoConnection({ linkRepo: opts.linkRepo ?? true });
3371
- } else {
3372
- await promptRepoConnection();
3373
- }
3031
+ await promptRepoConnection();
3374
3032
  const agents = detectAgents();
3375
3033
  if (agents.length === 0) {
3376
3034
  console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
@@ -3394,9 +3052,9 @@ async function installCommand(opts = {}) {
3394
3052
  `);
3395
3053
  writeGraderDaemon();
3396
3054
  for (const mode of ["edit", "bash"]) {
3397
- const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
3055
+ const pidFile = join5(SYNKRO_DIR, "daemon", mode, "daemon.pid");
3398
3056
  try {
3399
- const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
3057
+ const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
3400
3058
  if (pid > 0) {
3401
3059
  process.kill(pid, "SIGTERM");
3402
3060
  console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
@@ -3464,158 +3122,38 @@ async function installCommand(opts = {}) {
3464
3122
  } catch {
3465
3123
  }
3466
3124
  writeConfigEnv({ gatewayUrl, userId, orgId, email });
3467
- console.log(`Wrote config to ${CONFIG_PATH2}
3468
- `);
3469
- const repo = detectGitRepo2();
3470
- if (repo && getClaudeProjectsFolder()) {
3471
- const syncHistory = opts.nonInteractive ? opts.syncTranscripts ?? true : await promptYesNo("Sync Claude Code session history for this repo?");
3472
- if (syncHistory) {
3473
- try {
3474
- const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
3475
- if (ingested > 0) {
3476
- console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
3477
- console.log(" This helps the safety judge understand your workflow.\n");
3478
- }
3479
- } catch (err) {
3480
- console.warn(` \u26A0 Session indexing skipped: ${err.message}
3481
- `);
3482
- }
3483
- try {
3484
- const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
3485
- if (result.messages > 0) {
3486
- console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
3487
- console.log(" This data will be used to suggest guardrail rules.\n");
3488
- }
3489
- } catch (err) {
3490
- console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3125
+ console.log(`Wrote config to ${CONFIG_PATH}
3491
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");
3492
3134
  }
3493
- } else {
3494
- console.log(" Skipped session history sync.\n");
3495
3135
  }
3136
+ } catch (err) {
3137
+ console.warn(` \u26A0 Session indexing skipped: ${err.message}
3138
+ `);
3496
3139
  }
3497
- function detectProviderFromKey(key) {
3498
- if (key.startsWith("sk-ant-")) return "anthropic";
3499
- if (key.startsWith("sk-") || key.startsWith("sk-proj-")) return "openai";
3500
- if (key.startsWith("AIza")) return "gemini";
3501
- return null;
3502
- }
3503
- const PROVIDER_MODELS = {
3504
- anthropic: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
3505
- openai: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
3506
- gemini: ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"]
3507
- };
3508
- if (repo) {
3509
- let ghConnected = false;
3510
- let ghLogin = "";
3511
- try {
3512
- const resp = await fetch(`${gatewayUrl}/api/integrations/github/connect`, {
3513
- headers: { "Authorization": `Bearer ${token}` }
3514
- });
3515
- if (resp.ok) {
3516
- const data = await resp.json();
3517
- ghConnected = data.connected;
3518
- 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");
3519
3147
  }
3520
- } catch {
3521
- }
3522
- if (ghConnected) {
3523
- console.log(`GitHub is connected (${ghLogin || "linked via dashboard"}).`);
3524
3148
  }
3525
- const prScanChoice = opts.nonInteractive ? opts.prScan ?? "skip" : null;
3526
- const setupGh = prScanChoice !== null ? prScanChoice !== "skip" : await promptYesNo("Set up GitHub PR scanning?");
3527
- if (setupGh) {
3528
- let choice;
3529
- if (prScanChoice) {
3530
- choice = prScanChoice === "claude-oauth" ? "1" : prScanChoice === "byok" ? "2" : "3";
3531
- } else {
3532
- const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3533
- console.log("\nHow should PR scans authenticate for AI review?\n");
3534
- console.log(" 1. Claude Code OAuth \u2014 opens browser, auto-captures token");
3535
- console.log(" 2. Use your own API key \u2014 paste an Anthropic, OpenAI, or Gemini key");
3536
- console.log(" 3. Skip for now\n");
3537
- choice = (await rl.question("Choice [1/2/3]: ")).trim();
3538
- rl.close();
3539
- }
3540
- if (choice === "1") {
3541
- try {
3542
- await setupGithubCommand({ githubToken: opts.githubToken, nonInteractive: opts.nonInteractive });
3543
- } catch (err) {
3544
- console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3545
- console.warn(" Run `synkro-cli setup-github` to retry.\n");
3546
- }
3547
- } else if (choice === "2") {
3548
- let apiKey = opts.inferenceKey || "";
3549
- if (!apiKey) {
3550
- const rl2 = createInterface3({ input: stdinStream, output: stdoutStream });
3551
- apiKey = (await rl2.question("Paste your API key: ")).trim();
3552
- rl2.close();
3553
- }
3554
- if (apiKey) {
3555
- const provider = detectProviderFromKey(apiKey);
3556
- if (provider) {
3557
- let selectedModel = opts.inferenceModel || "";
3558
- if (!selectedModel) {
3559
- const models = PROVIDER_MODELS[provider];
3560
- console.log(`
3561
- Detected provider: ${provider}`);
3562
- console.log(" Select a model for classification + grading:\n");
3563
- models.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
3564
- console.log();
3565
- const rl3 = createInterface3({ input: stdinStream, output: stdoutStream });
3566
- const modelChoice = (await rl3.question(` Model [1-${models.length}]: `)).trim();
3567
- rl3.close();
3568
- const modelIdx = parseInt(modelChoice, 10) - 1;
3569
- selectedModel = modelIdx >= 0 && modelIdx < models.length ? models[modelIdx] : models[0];
3570
- }
3571
- console.log(` Using: ${selectedModel}`);
3572
- try {
3573
- const resp = await fetch(`${gatewayUrl}/api/v1/settings/inference`, {
3574
- method: "PUT",
3575
- headers: {
3576
- "Authorization": `Bearer ${token}`,
3577
- "Content-Type": "application/json"
3578
- },
3579
- body: JSON.stringify({
3580
- providers: { [provider]: { api_key: apiKey } },
3581
- roles: {
3582
- classification: { provider, model: selectedModel },
3583
- grading: { provider, model: selectedModel }
3584
- }
3585
- })
3586
- });
3587
- if (resp.ok) {
3588
- console.log(` \u2713 Saved ${provider} key and configured classification + grading with ${selectedModel}.
3589
- `);
3590
- } else {
3591
- const err = await resp.text().catch(() => "");
3592
- console.warn(` \u26A0 Failed to save key: ${err.slice(0, 200)}
3593
- `);
3594
- }
3595
- } catch (err) {
3596
- console.warn(` \u26A0 Failed to save key: ${err.message}
3149
+ } catch (err) {
3150
+ console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3597
3151
  `);
3598
- }
3599
- } else {
3600
- console.warn(" \u26A0 Could not detect provider from key format. Configure manually in the dashboard.\n");
3601
- }
3602
- }
3603
- try {
3604
- await setupGithubCommand({ skipClaudeToken: true, githubToken: opts.githubToken, nonInteractive: opts.nonInteractive });
3605
- } catch (err) {
3606
- console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3607
- console.warn(" Run `synkro-cli setup-github` to retry.\n");
3608
- }
3609
- } else {
3610
- console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3611
- }
3612
- } else {
3613
- console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3614
- }
3615
3152
  }
3616
3153
  console.log("\u2713 Synkro installed.");
3617
3154
  console.log();
3618
3155
  console.log("Next steps:");
3156
+ console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
3619
3157
  console.log(" \u2022 synkro-cli status (check what is configured)");
3620
3158
  }
3621
3159
  function detectGitRepo2() {
@@ -3629,18 +3167,18 @@ function detectGitRepo2() {
3629
3167
  }
3630
3168
  function getClaudeProjectsFolder() {
3631
3169
  const cwd = process.cwd();
3632
- const sanitized = cwd.replace(/\//g, "-");
3633
- const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
3634
- 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;
3635
3173
  }
3636
3174
  function extractSessionInsights(projectsDir) {
3637
3175
  const insights = [];
3638
3176
  const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
3639
3177
  for (const file of files) {
3640
3178
  const sessionId = file.replace(".jsonl", "");
3641
- const filePath = join6(projectsDir, file);
3179
+ const filePath = join5(projectsDir, file);
3642
3180
  try {
3643
- const content = readFileSync5(filePath, "utf-8");
3181
+ const content = readFileSync4(filePath, "utf-8");
3644
3182
  const lines = content.split("\n").filter(Boolean);
3645
3183
  for (let i = 0; i < lines.length; i++) {
3646
3184
  try {
@@ -3716,7 +3254,7 @@ function extractTextContent(content) {
3716
3254
  return "";
3717
3255
  }
3718
3256
  function parseTranscriptFile(filePath) {
3719
- const content = readFileSync5(filePath, "utf-8");
3257
+ const content = readFileSync4(filePath, "utf-8");
3720
3258
  const lines = content.split("\n").filter(Boolean);
3721
3259
  const messages = [];
3722
3260
  for (let i = 0; i < lines.length; i++) {
@@ -3767,7 +3305,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3767
3305
  const sessions = [];
3768
3306
  for (const file of batch) {
3769
3307
  const sessionId = file.replace(".jsonl", "");
3770
- const filePath = join6(projectsDir, file);
3308
+ const filePath = join5(projectsDir, file);
3771
3309
  try {
3772
3310
  const allMessages = parseTranscriptFile(filePath);
3773
3311
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -3796,18 +3334,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3796
3334
  }
3797
3335
  for (const file of batch) {
3798
3336
  const sessionId = file.replace(".jsonl", "");
3799
- const filePath = join6(projectsDir, file);
3337
+ const filePath = join5(projectsDir, file);
3800
3338
  try {
3801
- const content = readFileSync5(filePath, "utf-8");
3339
+ const content = readFileSync4(filePath, "utf-8");
3802
3340
  const lineCount = content.split("\n").filter(Boolean).length;
3803
- writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3341
+ writeFileSync5(join5(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3804
3342
  } catch {
3805
3343
  }
3806
3344
  }
3807
3345
  }
3808
3346
  return { sessions: totalSessions, messages: totalMessages };
3809
3347
  }
3810
- 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;
3811
3349
  var init_install = __esm({
3812
3350
  "cli/commands/install.ts"() {
3813
3351
  "use strict";
@@ -3818,15 +3356,14 @@ var init_install = __esm({
3818
3356
  init_graderDaemon();
3819
3357
  init_stub();
3820
3358
  init_repoConnect();
3821
- init_setupGithub();
3822
- SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3823
- HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
3824
- BIN_DIR = join6(SYNKRO_DIR2, "bin");
3825
- CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
3826
- GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
3827
- GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
3828
- GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
3829
- 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");
3830
3367
  }
3831
3368
  });
3832
3369
 
@@ -3902,13 +3439,13 @@ var status_exports = {};
3902
3439
  __export(status_exports, {
3903
3440
  statusCommand: () => statusCommand
3904
3441
  });
3905
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3906
- import { homedir as homedir6 } from "os";
3907
- 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";
3908
3445
  function readConfigEnv() {
3909
- if (!existsSync8(CONFIG_PATH3)) return {};
3446
+ if (!existsSync7(CONFIG_PATH2)) return {};
3910
3447
  const out = {};
3911
- const raw = readFileSync6(CONFIG_PATH3, "utf-8");
3448
+ const raw = readFileSync5(CONFIG_PATH2, "utf-8");
3912
3449
  for (const line of raw.split("\n")) {
3913
3450
  const trimmed = line.trim();
3914
3451
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -3939,10 +3476,10 @@ function statusCommand() {
3939
3476
  console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
3940
3477
  const info2 = getUserInfo();
3941
3478
  const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
3942
- const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
3479
+ const tierCacheFile = join6(SYNKRO_DIR2, `.tier-cache-${userId}`);
3943
3480
  let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
3944
- if (!inferenceTier && existsSync8(tierCacheFile)) {
3945
- inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
3481
+ if (!inferenceTier && existsSync7(tierCacheFile)) {
3482
+ inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
3946
3483
  }
3947
3484
  const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
3948
3485
  console.log(` inference: ${tierLabel}`);
@@ -3969,19 +3506,19 @@ function statusCommand() {
3969
3506
  }
3970
3507
  }
3971
3508
  console.log();
3972
- const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
3973
- const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
3974
- const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
3975
- const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
3976
- const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
3977
- 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");
3978
3515
  console.log("Hook scripts:");
3979
- console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
3980
- console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
3981
- console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
3982
- console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
3983
- console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
3984
- 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}`);
3985
3522
  console.log();
3986
3523
  const mcp = inspectMcpConfig();
3987
3524
  console.log("Guardrails MCP server (Claude Code):");
@@ -3993,7 +3530,7 @@ function statusCommand() {
3993
3530
  console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
3994
3531
  }
3995
3532
  }
3996
- var SYNKRO_DIR3, CONFIG_PATH3;
3533
+ var SYNKRO_DIR2, CONFIG_PATH2;
3997
3534
  var init_status = __esm({
3998
3535
  "cli/commands/status.ts"() {
3999
3536
  "use strict";
@@ -4001,8 +3538,8 @@ var init_status = __esm({
4001
3538
  init_agentDetect();
4002
3539
  init_ccHookConfig();
4003
3540
  init_mcpConfig();
4004
- SYNKRO_DIR3 = join7(homedir6(), ".synkro");
4005
- CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
3541
+ SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3542
+ CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
4006
3543
  }
4007
3544
  });
4008
3545
 
@@ -4032,7 +3569,7 @@ var unlink_exports = {};
4032
3569
  __export(unlink_exports, {
4033
3570
  unlinkCommand: () => unlinkCommand
4034
3571
  });
4035
- import { createInterface as createInterface4 } from "readline";
3572
+ import { createInterface as createInterface2 } from "readline";
4036
3573
  function ask2(rl, question) {
4037
3574
  return new Promise((resolve2) => rl.question(question, resolve2));
4038
3575
  }
@@ -4060,7 +3597,7 @@ async function unlinkCommand() {
4060
3597
  console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
4061
3598
  });
4062
3599
  console.log();
4063
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3600
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
4064
3601
  try {
4065
3602
  const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
4066
3603
  const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
@@ -4086,6 +3623,194 @@ var init_unlink = __esm({
4086
3623
  }
4087
3624
  });
4088
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
+
4089
3814
  // cli/commands/scanPr.ts
4090
3815
  var scanPr_exports = {};
4091
3816
  __export(scanPr_exports, {
@@ -4263,14 +3988,17 @@ ${hunks}`;
4263
3988
  }
4264
3989
  );
4265
3990
  let stdout = "";
3991
+ let stderr = "";
4266
3992
  proc.stdout.on("data", (chunk) => {
4267
3993
  stdout += chunk.toString();
4268
3994
  });
4269
- proc.stderr.on("data", () => {
3995
+ proc.stderr.on("data", (chunk) => {
3996
+ stderr += chunk.toString();
4270
3997
  });
4271
3998
  proc.on("close", (code) => {
4272
3999
  const latencyMs = Date.now() - t0;
4273
4000
  if (code !== 0) {
4001
+ console.warn(` claude exited ${code}: ${stderr.slice(0, 300)}`);
4274
4002
  resolve2({ findings: [], latencyMs });
4275
4003
  return;
4276
4004
  }
@@ -4291,7 +4019,8 @@ ${hunks}`;
4291
4019
  fix: f.fix
4292
4020
  }));
4293
4021
  resolve2({ findings, latencyMs });
4294
- } catch {
4022
+ } catch (parseErr) {
4023
+ console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
4295
4024
  resolve2({ findings: [], latencyMs });
4296
4025
  }
4297
4026
  });
@@ -4621,16 +4350,7 @@ Usage:
4621
4350
  synkro <command> [options] (alias)
4622
4351
 
4623
4352
  Commands:
4624
- install [options] Install Synkro hooks for detected agents (Claude Code, etc.)
4625
- --force Reinstall from scratch
4626
- --non-interactive Skip all interactive prompts (use flags)
4627
- --link-repo Auto-link the local git repo
4628
- --sync-transcripts Sync CC session history (default in non-interactive)
4629
- --no-sync-transcripts Skip transcript sync
4630
- --pr-scan=MODE PR scan auth: claude-oauth, byok, or skip
4631
- --inference-key=KEY BYOK API key (Anthropic, OpenAI, or Gemini)
4632
- --inference-model=M Model for classification + grading
4633
- --github-token=TOK GitHub PAT for pushing secrets + workflow
4353
+ install [--force] Install Synkro hooks for detected agents (Claude Code, etc.)
4634
4354
  login Authenticate with Synkro (browser OAuth via WorkOS)
4635
4355
  logout Clear local credentials
4636
4356
  status Show current setup state