@denisvieiradev/gitwise-core 0.1.0 → 1.3.0

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/index.js CHANGED
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@denisvieiradev/gitwise-core",
10
- version: "0.1.0",
10
+ version: "1.3.0",
11
11
  description: "Shared logic for gitwise: non-interactive commit/review/pr/release commands, LLM providers, git/github primitives, prompt templates.",
12
12
  type: "module",
13
13
  main: "./dist/index.js",
@@ -60,7 +60,7 @@ var package_default = {
60
60
  node: ">=22.12.0"
61
61
  },
62
62
  dependencies: {
63
- "@anthropic-ai/sdk": "^0.109.0"
63
+ "@anthropic-ai/sdk": "^0.127.0"
64
64
  }
65
65
  };
66
66
 
@@ -486,10 +486,10 @@ async function stashDropNamed(cwd, stashName) {
486
486
  async function cleanForced(cwd) {
487
487
  await run(["clean", "-fd"], cwd);
488
488
  }
489
- async function showFileAtHead(cwd, path3) {
490
- debug("git command", { args: ["show", `HEAD:${path3}`], cwd });
489
+ async function showFileAtHead(cwd, path7) {
490
+ debug("git command", { args: ["show", `HEAD:${path7}`], cwd });
491
491
  try {
492
- const result = await exec("git", ["show", `HEAD:${path3}`], {
492
+ const result = await exec("git", ["show", `HEAD:${path7}`], {
493
493
  cwd,
494
494
  timeout: GIT_TIMEOUT_MS,
495
495
  maxBuffer: GIT_MAX_BUFFER
@@ -890,22 +890,16 @@ function defaultIsProcessAlive(pid) {
890
890
  }
891
891
 
892
892
  // src/providers/claude-code.ts
893
- import { execFile as execFile3, execSync, spawn } from "child_process";
893
+ import os2 from "os";
894
+ import path3 from "path";
895
+
896
+ // src/providers/cli-subprocess.ts
897
+ import { execSync, spawn } from "child_process";
894
898
  import fs from "fs";
895
899
  import os from "os";
896
900
  import path2 from "path";
897
- import { promisify as promisify3 } from "util";
898
- var execFileAsync = promisify3(execFile3);
899
- var LARGE_PROMPT_THRESHOLD = 1e5;
900
- var DEFAULT_TIMEOUT_MS = 12e4;
901
- var COMMON_CLAUDE_PATHS = [
902
- // Native installs (Homebrew, manual) — preferred over npm
903
- "/opt/homebrew/bin/claude",
904
- "/usr/local/bin/claude",
905
- path2.join(os.homedir(), ".claude", "local", "claude"),
906
- // npm global installs — fallback
907
- path2.join(os.homedir(), ".npm-global", "bin", "claude")
908
- ];
901
+ import { StringDecoder } from "string_decoder";
902
+ import { clearTimeout, setTimeout as setTimeout2 } from "timers";
909
903
  function isExecutable(filePath) {
910
904
  try {
911
905
  fs.accessSync(filePath, fs.constants.X_OK);
@@ -914,207 +908,441 @@ function isExecutable(filePath) {
914
908
  return false;
915
909
  }
916
910
  }
917
- function resolveClaudeBinary(customPath) {
918
- if (customPath) {
919
- if (isExecutable(customPath)) return customPath;
920
- return null;
921
- }
922
- for (const candidate of COMMON_CLAUDE_PATHS) {
911
+ function resolveCliBinary(name, commonPaths, customPath) {
912
+ if (customPath) return isExecutable(customPath) ? customPath : null;
913
+ for (const candidate of commonPaths) {
923
914
  if (isExecutable(candidate)) return candidate;
924
915
  }
925
916
  try {
926
- const found = execSync("which claude", { stdio: "pipe" }).toString().trim();
917
+ const found = execSync(`which ${name}`, { stdio: "pipe" }).toString().trim();
927
918
  if (found && isExecutable(found)) return found;
928
919
  } catch {
929
920
  }
930
921
  const nvmDir = path2.join(os.homedir(), ".nvm", "versions", "node");
931
922
  try {
932
- const versions = fs.readdirSync(nvmDir);
933
- for (const version2 of versions) {
934
- const candidate = path2.join(nvmDir, version2, "bin", "claude");
923
+ for (const version2 of fs.readdirSync(nvmDir)) {
924
+ const candidate = path2.join(nvmDir, version2, "bin", name);
935
925
  if (isExecutable(candidate)) return candidate;
936
926
  }
937
927
  } catch {
938
928
  }
939
929
  return null;
940
930
  }
941
- var ClaudeCodeProvider = class {
942
- models;
943
- claudeBinaryPath;
944
- constructor(models, claudeCliPath) {
931
+ var LARGE_PROMPT_THRESHOLD = 1e5;
932
+ var DEFAULT_TIMEOUT_MS = 12e4;
933
+ var KILL_GRACE_MS = 5e3;
934
+ var CliSubprocessProvider = class {
935
+ constructor(spec, models, cliPath) {
936
+ this.spec = spec;
945
937
  this.models = models;
946
- this.claudeBinaryPath = claudeCliPath ?? resolveClaudeBinary() ?? "claude";
938
+ this.binaryPath = cliPath || spec.resolveBinary() || spec.defaultCommand;
947
939
  }
940
+ binaryPath;
948
941
  async chat(req) {
949
- const modelId = this.resolveModel(req.tier);
950
- debug("Calling Claude Code CLI", { model: modelId, tier: req.tier, binary: this.claudeBinaryPath });
951
- const userContent = req.userMessage;
952
- const args = this.buildArgs(req.systemPrompt, modelId, userContent);
953
- const result = userContent.length > LARGE_PROMPT_THRESHOLD ? await this.callViaStdin(args, userContent) : await this.callViaCli(args);
942
+ const modelId = this.models[req.tier];
943
+ debug(`Calling ${this.spec.toolName}`, { model: modelId, tier: req.tier, binary: this.binaryPath });
944
+ const prompt = this.spec.foldSystemPrompt ? `${req.systemPrompt}
945
+
946
+ ${req.userMessage}` : req.userMessage;
947
+ const large = Buffer.byteLength(prompt, "utf8") > LARGE_PROMPT_THRESHOLD;
948
+ const args = this.spec.buildArgs({ prompt, systemPrompt: req.systemPrompt, modelId, large });
949
+ const stdout = await this.spawnCli(args, large ? prompt : "");
950
+ const parsed = this.spec.parseOutput(stdout);
954
951
  return {
955
- content: result.result,
956
- tokens: {
957
- input: result.usage.input_tokens,
958
- output: result.usage.output_tokens
959
- }
952
+ content: parsed.content,
953
+ tokens: parsed.tokens ?? { input: 0, output: 0 },
954
+ tokensAvailable: parsed.tokens !== null
960
955
  };
961
956
  }
962
- buildArgs(systemPrompt, modelId, userContent) {
963
- const args = [
964
- "-p",
965
- ...userContent.length <= LARGE_PROMPT_THRESHOLD ? [userContent] : [],
966
- "--system-prompt",
967
- systemPrompt,
968
- "--model",
969
- modelId,
970
- "--output-format",
971
- "json"
972
- ];
973
- return args;
974
- }
975
- async callViaCli(args) {
976
- try {
977
- const { stdout } = await execFileAsync(this.claudeBinaryPath, args, {
978
- timeout: DEFAULT_TIMEOUT_MS,
979
- maxBuffer: 10 * 1024 * 1024,
980
- ...{ input: "" }
957
+ spawnCli(args, input) {
958
+ return new Promise((resolve, reject) => {
959
+ const ownGroup = process.platform !== "win32";
960
+ const child = spawn(this.binaryPath, args, {
961
+ stdio: ["pipe", "pipe", "pipe"],
962
+ detached: ownGroup
981
963
  });
982
- return this.parseResponse(stdout);
983
- } catch (err) {
984
- const execErr = err;
985
- if (execErr.stdout) {
964
+ const killTree = (sig) => {
986
965
  try {
987
- const parsed = JSON.parse(execErr.stdout);
988
- if (parsed.is_error) {
989
- throw new Error(`Claude CLI error: ${parsed.result}`);
990
- }
991
- } catch (parseErr) {
992
- if (parseErr instanceof Error && parseErr.message.startsWith("Claude CLI error:")) {
993
- throw parseErr;
994
- }
966
+ if (ownGroup && child.pid !== void 0) process.kill(-child.pid, sig);
967
+ else child.kill(sig);
968
+ } catch {
995
969
  }
970
+ };
971
+ const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
972
+ let timedOut = false;
973
+ let escalation;
974
+ const timer = setTimeout2(() => {
975
+ timedOut = true;
976
+ killTree("SIGTERM");
977
+ escalation = setTimeout2(() => killTree("SIGKILL"), KILL_GRACE_MS);
978
+ escalation.unref();
979
+ }, timeoutMs);
980
+ const reapOnExit = () => killTree("SIGKILL");
981
+ let interruptedBy;
982
+ const onSignal = (sig) => {
983
+ interruptedBy = sig;
984
+ killTree("SIGKILL");
985
+ reject(new Error(`${this.spec.toolName} was interrupted by ${sig}`));
986
+ cleanup();
987
+ if (process.listenerCount(sig) === 0) process.kill(process.pid, sig);
988
+ };
989
+ const onSigint = () => onSignal("SIGINT");
990
+ const onSigterm = () => onSignal("SIGTERM");
991
+ process.once("exit", reapOnExit);
992
+ process.on("SIGINT", onSigint);
993
+ process.on("SIGTERM", onSigterm);
994
+ function cleanup() {
995
+ clearTimeout(timer);
996
+ clearTimeout(escalation);
997
+ process.off("exit", reapOnExit);
998
+ process.off("SIGINT", onSigint);
999
+ process.off("SIGTERM", onSigterm);
996
1000
  }
997
- const stderr = execErr.stderr?.replace(/Warning: no stdin data.*\n?/g, "").trim();
998
- if (stderr) {
999
- throw new Error(`Claude CLI failed: ${stderr}`);
1000
- }
1001
- throw this.wrapError(err);
1002
- }
1003
- }
1004
- async callViaStdin(args, input) {
1005
- return new Promise((resolve, reject) => {
1006
- const child = spawn(this.claudeBinaryPath, args, {
1007
- stdio: ["pipe", "pipe", "pipe"],
1008
- timeout: DEFAULT_TIMEOUT_MS
1009
- });
1010
1001
  let stdout = "";
1011
1002
  let stderr = "";
1003
+ const outDecoder = new StringDecoder("utf8");
1004
+ const errDecoder = new StringDecoder("utf8");
1012
1005
  child.stdout.on("data", (data) => {
1013
- stdout += data.toString();
1006
+ stdout += outDecoder.write(data);
1014
1007
  });
1015
1008
  child.stderr.on("data", (data) => {
1016
- stderr += data.toString();
1009
+ stderr += errDecoder.write(data);
1017
1010
  });
1018
- child.on("close", (code) => {
1019
- if (code !== 0) {
1020
- if (stdout) {
1021
- try {
1022
- const parsed = JSON.parse(stdout);
1023
- if (parsed.is_error) {
1024
- reject(new Error(`Claude CLI error: ${parsed.result}`));
1025
- return;
1026
- }
1027
- } catch {
1028
- }
1029
- }
1030
- const filteredStderr = stderr.replace(/Warning: no stdin data.*\n?/g, "").trim();
1031
- reject(
1032
- new Error(
1033
- `Claude CLI exited with code ${code}${filteredStderr ? `: ${filteredStderr}` : ""}`
1034
- )
1035
- );
1011
+ child.on("close", (code, signal) => {
1012
+ if (timedOut) killTree("SIGKILL");
1013
+ cleanup();
1014
+ stdout += outDecoder.end();
1015
+ stderr += errDecoder.end();
1016
+ if (interruptedBy) return;
1017
+ if (timedOut) {
1018
+ reject(new Error(`${this.spec.toolName} timed out after ${Math.round(timeoutMs / 1e3)}s`));
1036
1019
  return;
1037
1020
  }
1038
- try {
1039
- resolve(this.parseResponse(stdout));
1040
- } catch (err) {
1041
- reject(err);
1021
+ if (code === null && signal) {
1022
+ reject(new Error(`${this.spec.toolName} was terminated by signal ${signal}`));
1023
+ return;
1042
1024
  }
1025
+ if (code !== 0) {
1026
+ reject(new Error(this.exitErrorMessage(code, stdout, stderr)));
1027
+ return;
1028
+ }
1029
+ resolve(stdout);
1043
1030
  });
1044
1031
  child.on("error", (err) => {
1032
+ cleanup();
1045
1033
  reject(this.wrapError(err));
1046
1034
  });
1035
+ child.stdin.on("error", () => void 0);
1047
1036
  child.stdin.write(input);
1048
1037
  child.stdin.end();
1049
1038
  });
1050
1039
  }
1051
- parseResponse(stdout) {
1040
+ exitErrorMessage(code, stdout, stderr) {
1041
+ if (this.spec.formatExitError) return this.spec.formatExitError(code, stdout, stderr);
1042
+ const trimmed = stderr.trim();
1043
+ return `${this.spec.toolName} exited with code ${code}${trimmed ? `: ${trimmed}` : ""}`;
1044
+ }
1045
+ wrapError(err) {
1046
+ const message = err instanceof Error ? err.message : String(err?.message ?? err);
1047
+ const code = err?.code;
1048
+ if (code === "ENOENT" || message.includes("ENOENT")) {
1049
+ return new GitwiseError({
1050
+ code: "PROVIDER_UNAVAILABLE",
1051
+ message: `${this.spec.toolName} not found at "${this.binaryPath}". ${this.spec.installHint}`,
1052
+ exitCode: EXIT_CODES.API_FAILED,
1053
+ cause: err
1054
+ });
1055
+ }
1056
+ return err instanceof Error ? err : new Error(message);
1057
+ }
1058
+ };
1059
+
1060
+ // src/providers/claude-code.ts
1061
+ var COMMON_CLAUDE_PATHS = [
1062
+ // Native installs (Homebrew, manual) — preferred over npm
1063
+ "/opt/homebrew/bin/claude",
1064
+ "/usr/local/bin/claude",
1065
+ path3.join(os2.homedir(), ".claude", "local", "claude"),
1066
+ // npm global installs — fallback
1067
+ path3.join(os2.homedir(), ".npm-global", "bin", "claude")
1068
+ ];
1069
+ function resolveClaudeBinary(customPath) {
1070
+ return resolveCliBinary("claude", COMMON_CLAUDE_PATHS, customPath);
1071
+ }
1072
+ var claudeCodeSpec = {
1073
+ toolName: "Claude Code CLI",
1074
+ installHint: "Re-run `gw config` to reconfigure.",
1075
+ defaultCommand: "claude",
1076
+ foldSystemPrompt: false,
1077
+ resolveBinary: resolveClaudeBinary,
1078
+ buildArgs({ prompt, systemPrompt, modelId, large }) {
1079
+ return [
1080
+ "-p",
1081
+ ...large ? [] : [prompt],
1082
+ "--system-prompt",
1083
+ systemPrompt,
1084
+ "--model",
1085
+ modelId,
1086
+ "--output-format",
1087
+ "json"
1088
+ ];
1089
+ },
1090
+ parseOutput(stdout) {
1052
1091
  const parsed = JSON.parse(stdout);
1053
1092
  if (parsed.is_error) {
1054
1093
  throw new Error(`Claude CLI returned error: ${parsed.result}`);
1055
1094
  }
1056
- const usage = { input_tokens: 0, output_tokens: 0 };
1057
- if (parsed.usage) {
1058
- usage.input_tokens = parsed.usage.input_tokens ?? 0;
1059
- usage.output_tokens = parsed.usage.output_tokens ?? 0;
1060
- }
1061
1095
  return {
1062
- result: parsed.result ?? "",
1063
- is_error: false,
1064
- usage
1096
+ content: parsed.result ?? "",
1097
+ tokens: {
1098
+ input: parsed.usage?.input_tokens ?? 0,
1099
+ output: parsed.usage?.output_tokens ?? 0
1100
+ }
1065
1101
  };
1102
+ },
1103
+ formatExitError(code, stdout, stderr) {
1104
+ if (stdout) {
1105
+ try {
1106
+ const parsed = JSON.parse(stdout);
1107
+ if (parsed.is_error) return `Claude CLI error: ${parsed.result}`;
1108
+ } catch {
1109
+ }
1110
+ }
1111
+ const filteredStderr = stderr.replace(/Warning: no stdin data.*\n?/g, "").trim();
1112
+ return `Claude CLI exited with code ${code}${filteredStderr ? `: ${filteredStderr}` : ""}`;
1066
1113
  }
1067
- resolveModel(tier) {
1068
- return this.models[tier];
1114
+ };
1115
+ var ClaudeCodeProvider = class extends CliSubprocessProvider {
1116
+ constructor(models, claudeCliPath) {
1117
+ super(claudeCodeSpec, models, claudeCliPath);
1069
1118
  }
1070
- wrapError(err) {
1071
- if (err instanceof Error) {
1072
- if (err.message.includes("ENOENT")) {
1073
- return new GitwiseError({
1074
- code: "PROVIDER_UNAVAILABLE",
1075
- message: `Claude Code CLI not found at "${this.claudeBinaryPath}". Re-run \`gw config\` to reconfigure.`,
1076
- exitCode: EXIT_CODES.API_FAILED,
1077
- cause: err
1078
- });
1119
+ };
1120
+
1121
+ // src/providers/codex.ts
1122
+ import os3 from "os";
1123
+ import path4 from "path";
1124
+ var COMMON_CODEX_PATHS = [
1125
+ // Native installs (Homebrew cask, manual, standalone installer) — preferred over npm
1126
+ "/opt/homebrew/bin/codex",
1127
+ "/usr/local/bin/codex",
1128
+ path4.join(os3.homedir(), ".local", "bin", "codex"),
1129
+ // npm global installs (`npm install -g @openai/codex`) — fallback
1130
+ path4.join(os3.homedir(), ".npm-global", "bin", "codex")
1131
+ ];
1132
+ function resolveCodexBinary(customPath) {
1133
+ return resolveCliBinary("codex", COMMON_CODEX_PATHS, customPath);
1134
+ }
1135
+ function parseEvents(stdout) {
1136
+ const events = [];
1137
+ for (const line of stdout.split("\n")) {
1138
+ const trimmed = line.trim();
1139
+ if (!trimmed) continue;
1140
+ try {
1141
+ events.push(JSON.parse(trimmed));
1142
+ } catch {
1143
+ }
1144
+ }
1145
+ return events;
1146
+ }
1147
+ var codexSpec = {
1148
+ toolName: "Codex CLI",
1149
+ installHint: "Install it (`npm install -g @openai/codex`) or re-run `gw provider` to choose another provider.",
1150
+ defaultCommand: "codex",
1151
+ foldSystemPrompt: true,
1152
+ // Full agent turns take longer than a single Claude Code completion.
1153
+ timeoutMs: 3e5,
1154
+ resolveBinary: resolveCodexBinary,
1155
+ buildArgs({ prompt, modelId, large }) {
1156
+ return [
1157
+ "exec",
1158
+ "--json",
1159
+ "--ephemeral",
1160
+ "--skip-git-repo-check",
1161
+ "--sandbox",
1162
+ "read-only",
1163
+ "--model",
1164
+ modelId,
1165
+ "--",
1166
+ large ? "-" : prompt
1167
+ ];
1168
+ },
1169
+ parseOutput(stdout) {
1170
+ let content;
1171
+ let tokens = null;
1172
+ for (const event of parseEvents(stdout)) {
1173
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
1174
+ content = event.item.text ?? "";
1175
+ } else if (event.type === "turn.completed" && event.usage) {
1176
+ tokens = {
1177
+ input: event.usage.input_tokens ?? 0,
1178
+ output: event.usage.output_tokens ?? 0
1179
+ };
1079
1180
  }
1080
- return err;
1081
1181
  }
1082
- return new Error(String(err));
1182
+ if (content === void 0) {
1183
+ throw new Error("Codex CLI returned no final agent message");
1184
+ }
1185
+ return { content, tokens };
1186
+ },
1187
+ // The CLI's own error text lives in the JSONL stream, not stderr; surface it
1188
+ // verbatim, falling back to stderr when the stream carries none.
1189
+ formatExitError(code, stdout, stderr) {
1190
+ const messages = /* @__PURE__ */ new Set();
1191
+ for (const event of parseEvents(stdout)) {
1192
+ if (event.type === "error" && event.message) messages.add(event.message);
1193
+ if (event.type === "turn.failed" && event.error?.message) messages.add(event.error.message);
1194
+ }
1195
+ const detail = messages.size > 0 ? [...messages].join("\n") : stderr.trim();
1196
+ return `Codex CLI exited with code ${code}${detail ? `: ${detail}` : ""}`;
1197
+ }
1198
+ };
1199
+
1200
+ // src/providers/copilot.ts
1201
+ import os4 from "os";
1202
+ import path5 from "path";
1203
+ var COMMON_COPILOT_PATHS = [
1204
+ // Native/Homebrew installs — preferred over npm
1205
+ "/opt/homebrew/bin/copilot",
1206
+ "/usr/local/bin/copilot",
1207
+ // Copilot's install script (non-root) target
1208
+ path5.join(os4.homedir(), ".local", "bin", "copilot"),
1209
+ // npm global installs (`npm install -g @github/copilot`) — fallback
1210
+ path5.join(os4.homedir(), ".npm-global", "bin", "copilot")
1211
+ ];
1212
+ function resolveCopilotBinary(customPath) {
1213
+ return resolveCliBinary("copilot", COMMON_COPILOT_PATHS, customPath);
1214
+ }
1215
+ var copilotSpec = {
1216
+ toolName: "Copilot CLI",
1217
+ installHint: "Install it (`npm install -g @github/copilot`) or re-run `gw provider` to choose another provider.",
1218
+ defaultCommand: "copilot",
1219
+ foldSystemPrompt: true,
1220
+ // Full agent turns take longer than a single Claude Code completion.
1221
+ timeoutMs: 3e5,
1222
+ resolveBinary: resolveCopilotBinary,
1223
+ buildArgs({ prompt, modelId, large }) {
1224
+ return [...large ? [] : [`--prompt=${prompt}`], "--no-ask-user", "--silent", "--model", modelId];
1225
+ },
1226
+ parseOutput(stdout) {
1227
+ const content = stdout.trim();
1228
+ if (!content) throw new Error("Copilot CLI returned an empty response");
1229
+ return { content, tokens: null };
1230
+ }
1231
+ };
1232
+
1233
+ // src/providers/kiro.ts
1234
+ import os5 from "os";
1235
+ import path6 from "path";
1236
+ import { stripVTControlCharacters } from "util";
1237
+ var COMMON_KIRO_PATHS = [
1238
+ // macOS app bundle and the installer's symlink location — preferred
1239
+ "/Applications/Kiro CLI.app/Contents/MacOS/kiro-cli",
1240
+ path6.join(os5.homedir(), ".local", "bin", "kiro-cli"),
1241
+ "/opt/homebrew/bin/kiro-cli",
1242
+ "/usr/local/bin/kiro-cli"
1243
+ ];
1244
+ function resolveKiroBinary(customPath) {
1245
+ return resolveCliBinary("kiro-cli", COMMON_KIRO_PATHS, customPath);
1246
+ }
1247
+ var kiroSpec = {
1248
+ toolName: "Kiro CLI",
1249
+ installHint: "Install it from https://kiro.dev/docs/cli/ or re-run `gw provider` to choose another provider.",
1250
+ defaultCommand: "kiro-cli",
1251
+ foldSystemPrompt: true,
1252
+ // Full agent turns take longer than a single Claude Code completion.
1253
+ timeoutMs: 3e5,
1254
+ resolveBinary: resolveKiroBinary,
1255
+ buildArgs({ prompt, modelId, large }) {
1256
+ return [
1257
+ "chat",
1258
+ "--no-interactive",
1259
+ "--trust-tools=",
1260
+ "--wrap",
1261
+ "never",
1262
+ "--model",
1263
+ modelId,
1264
+ // `--` (standard for kiro-cli's clap-style parser) keeps a prompt that
1265
+ // starts with "-" from being read as an option.
1266
+ ...large ? [] : ["--", prompt]
1267
+ ];
1268
+ },
1269
+ parseOutput(stdout) {
1270
+ const content = stripVTControlCharacters(stdout).trim();
1271
+ if (!content) throw new Error("Kiro CLI returned an empty response");
1272
+ return { content, tokens: null };
1273
+ },
1274
+ formatExitError(code, stdout, stderr) {
1275
+ const detail = stderr.trim() || stripVTControlCharacters(stdout).trim();
1276
+ return `Kiro CLI exited with code ${code}${detail ? `: ${detail}` : ""}`;
1083
1277
  }
1084
1278
  };
1085
1279
 
1086
1280
  // src/config/types.ts
1281
+ var CLAUDE_MODELS = {
1282
+ fast: "claude-haiku-4-5-20251001",
1283
+ balanced: "claude-sonnet-4-6",
1284
+ powerful: "claude-opus-4-7"
1285
+ };
1087
1286
  var DEFAULT_USER_CONFIG = {
1088
1287
  provider: "api",
1089
1288
  models: {
1090
- fast: "claude-haiku-4-5-20251001",
1091
- balanced: "claude-sonnet-4-6",
1092
- powerful: "claude-opus-4-7"
1289
+ api: { ...CLAUDE_MODELS },
1290
+ "claude-code": { ...CLAUDE_MODELS },
1291
+ codex: {
1292
+ fast: "gpt-6-luna",
1293
+ balanced: "gpt-6-sol",
1294
+ powerful: "gpt-6-astra"
1295
+ },
1296
+ copilot: {
1297
+ fast: "claude-haiku-4.5",
1298
+ balanced: "claude-sonnet-4.6",
1299
+ powerful: "claude-opus-4.7"
1300
+ },
1301
+ kiro: {
1302
+ fast: "claude-haiku-4.5",
1303
+ balanced: "claude-sonnet-4.5",
1304
+ powerful: "claude-sonnet-4.5"
1305
+ }
1093
1306
  },
1094
1307
  language: "en",
1095
1308
  commitConvention: "conventional"
1096
1309
  };
1097
1310
 
1098
1311
  // src/config/merge.ts
1099
- import os3 from "os";
1312
+ import os7 from "os";
1100
1313
 
1101
1314
  // src/config/user.ts
1102
1315
  import { join as join2 } from "path";
1103
- import os2 from "os";
1316
+ import os6 from "os";
1317
+
1318
+ // src/providers/types.ts
1319
+ var PROVIDER_KINDS = ["api", "claude-code", "codex", "copilot", "kiro"];
1320
+
1321
+ // src/config/user.ts
1104
1322
  var GITWISE_DIR = ".gitwise";
1105
1323
  var USER_CONFIG_FILE = "config.json";
1106
1324
  function getUserConfigPath(homeDir) {
1107
- return join2(homeDir ?? os2.homedir(), GITWISE_DIR, USER_CONFIG_FILE);
1325
+ return join2(homeDir ?? os6.homedir(), GITWISE_DIR, USER_CONFIG_FILE);
1326
+ }
1327
+ function isLegacyFlatModels(value) {
1328
+ if (!value || typeof value !== "object") return false;
1329
+ const v = value;
1330
+ return typeof v["fast"] === "string" && typeof v["balanced"] === "string" && typeof v["powerful"] === "string";
1331
+ }
1332
+ function migrateFlatModels(flat, provider) {
1333
+ const target = provider === void 0 ? DEFAULT_USER_CONFIG.provider : provider;
1334
+ const migrated = { ...DEFAULT_USER_CONFIG.models };
1335
+ if (typeof target === "string" && PROVIDER_KINDS.includes(target)) {
1336
+ migrated[target] = { ...flat };
1337
+ }
1338
+ return migrated;
1108
1339
  }
1109
1340
  function mergeWithDefaults(partial) {
1110
- return {
1111
- ...DEFAULT_USER_CONFIG,
1112
- ...partial,
1113
- models: {
1114
- ...DEFAULT_USER_CONFIG.models,
1115
- ...partial.models ?? {}
1116
- }
1117
- };
1341
+ const models = {};
1342
+ for (const kind of PROVIDER_KINDS) {
1343
+ models[kind] = { ...DEFAULT_USER_CONFIG.models[kind], ...partial.models?.[kind] ?? {} };
1344
+ }
1345
+ return { ...DEFAULT_USER_CONFIG, ...partial, models };
1118
1346
  }
1119
1347
  async function readUserConfig(homeDir) {
1120
1348
  const configPath = getUserConfigPath(homeDir);
@@ -1123,6 +1351,17 @@ async function readUserConfig(homeDir) {
1123
1351
  return { ...DEFAULT_USER_CONFIG };
1124
1352
  }
1125
1353
  const raw = await readJSON(configPath);
1354
+ if (isLegacyFlatModels(raw.models)) {
1355
+ const migratedModels = migrateFlatModels(raw.models, raw.provider);
1356
+ const merged = mergeWithDefaults({ ...raw, models: migratedModels });
1357
+ debug("Migrated legacy flat models config to per-provider shape", { path: configPath });
1358
+ try {
1359
+ await writeJSON(configPath, merged);
1360
+ } catch (err) {
1361
+ debug("Could not persist migrated config; using it in memory", { path: configPath, error: String(err) });
1362
+ }
1363
+ return merged;
1364
+ }
1126
1365
  return mergeWithDefaults(raw);
1127
1366
  }
1128
1367
  async function writeUserConfig(partial, homeDir) {
@@ -1133,7 +1372,7 @@ async function writeUserConfig(partial, homeDir) {
1133
1372
  await writeJSON(configPath, updated);
1134
1373
  }
1135
1374
  async function writeApiKey(value, homeDir) {
1136
- const home = homeDir ?? os2.homedir();
1375
+ const home = homeDir ?? os6.homedir();
1137
1376
  await writeEnvVar(home, "ANTHROPIC_API_KEY", value);
1138
1377
  }
1139
1378
 
@@ -1160,6 +1399,26 @@ async function readRepoConfig(cwd) {
1160
1399
  }
1161
1400
 
1162
1401
  // src/config/merge.ts
1402
+ function isPlainObject(value) {
1403
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1404
+ }
1405
+ var MODEL_TIERS = ["fast", "balanced", "powerful"];
1406
+ function mergeRepoModels(base, override) {
1407
+ if (!isPlainObject(override)) return base.models;
1408
+ const source = override;
1409
+ const flat = {};
1410
+ for (const tier of MODEL_TIERS) {
1411
+ const value = source[tier];
1412
+ if (typeof value === "string") flat[tier] = value;
1413
+ }
1414
+ const merged = { ...base.models };
1415
+ merged[base.provider] = { ...base.models[base.provider], ...flat };
1416
+ for (const kind of PROVIDER_KINDS) {
1417
+ const block = source[kind];
1418
+ if (isPlainObject(block)) merged[kind] = { ...merged[kind], ...block };
1419
+ }
1420
+ return merged;
1421
+ }
1163
1422
  function deepMerge(base, override) {
1164
1423
  return {
1165
1424
  ...base,
@@ -1169,10 +1428,7 @@ function deepMerge(base, override) {
1169
1428
  ...override.templatesPath !== void 0 && { templatesPath: override.templatesPath },
1170
1429
  ...override.releaseStrategy !== void 0 && { releaseStrategy: override.releaseStrategy },
1171
1430
  ...override.developBranch !== void 0 && { developBranch: override.developBranch },
1172
- models: {
1173
- ...base.models,
1174
- ...override.models ?? {}
1175
- }
1431
+ models: mergeRepoModels(base, override.models)
1176
1432
  };
1177
1433
  }
1178
1434
  async function getMergedConfig(options) {
@@ -1185,7 +1441,7 @@ async function getMergedConfig(options) {
1185
1441
  return deepMerge(userConfig, repoConfig);
1186
1442
  }
1187
1443
  async function getApiKey(homeDir) {
1188
- const home = homeDir ?? os3.homedir();
1444
+ const home = homeDir ?? os7.homedir();
1189
1445
  return read("ANTHROPIC_API_KEY", home);
1190
1446
  }
1191
1447
 
@@ -1193,7 +1449,7 @@ async function getApiKey(homeDir) {
1193
1449
  import { readFile as readFile4 } from "fs/promises";
1194
1450
  import { dirname as dirname2, join as join4 } from "path";
1195
1451
  import { fileURLToPath } from "url";
1196
- import os4 from "os";
1452
+ import os8 from "os";
1197
1453
 
1198
1454
  // src/template/interpolate.ts
1199
1455
  function interpolate(template, ctx) {
@@ -1221,7 +1477,7 @@ function validateTemplateName(name) {
1221
1477
  async function loadTemplate(name, options = {}) {
1222
1478
  validateTemplateName(name);
1223
1479
  const repoRoot = options.repoRoot ?? process.cwd();
1224
- const userTemplatesPath = options.templatesPath ?? join4(os4.homedir(), ".gitwise", "templates");
1480
+ const userTemplatesPath = options.templatesPath ?? join4(os8.homedir(), ".gitwise", "templates");
1225
1481
  const repoOverride = join4(repoRoot, ".gitwise", "templates", `${name}.md`);
1226
1482
  if (await fileExists(repoOverride)) {
1227
1483
  debug("Loading repo-level template override", { path: repoOverride });
@@ -1462,20 +1718,22 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
1462
1718
  const response = await provider.chat({ systemPrompt: effectiveSystemPrompt, userMessage, tier });
1463
1719
  const parsed = parseCommitResponse(response.content);
1464
1720
  const tokens = { input: response.tokens.input, output: response.tokens.output };
1721
+ const tokensAvailable = response.tokensAvailable;
1465
1722
  if (opts.generateAlternatives) {
1466
1723
  const options = parseAlternativesResponse(response.content);
1467
1724
  if (options && options.length > 0) {
1468
- return { kind: "alternatives", options, tokens };
1725
+ return { kind: "alternatives", options, tokens, tokensAvailable };
1469
1726
  }
1470
1727
  const fallbackMsg = parsed.type === "single" ? parsed.message : parsed.commits[0]?.message ?? response.content.trim().slice(0, 100);
1471
- return { kind: "alternatives", options: [fallbackMsg], tokens };
1728
+ return { kind: "alternatives", options: [fallbackMsg], tokens, tokensAvailable };
1472
1729
  }
1473
1730
  if (split === "never") {
1474
1731
  const message2 = parsed.type === "single" ? parsed.message : parsed.commits.map((c) => c.message).join("\n\n");
1475
1732
  return {
1476
1733
  kind: "single",
1477
1734
  commits: [{ message: message2, files: stagedFiles }],
1478
- tokens
1735
+ tokens,
1736
+ tokensAvailable
1479
1737
  };
1480
1738
  }
1481
1739
  if (parsed.type === "plan" && parsed.commits.length > 1) {
@@ -1488,7 +1746,8 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
1488
1746
  return {
1489
1747
  kind: "split",
1490
1748
  commits: parsed.commits,
1491
- tokens
1749
+ tokens,
1750
+ tokensAvailable
1492
1751
  };
1493
1752
  }
1494
1753
  }
@@ -1503,7 +1762,8 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
1503
1762
  return {
1504
1763
  kind: "single",
1505
1764
  commits: [{ message, files: stagedFiles }],
1506
- tokens
1765
+ tokens,
1766
+ tokensAvailable
1507
1767
  };
1508
1768
  }
1509
1769
  function takeNamedStashStep(cwd, stashName) {
@@ -1713,7 +1973,8 @@ Additional context: ${prompt}` : "");
1713
1973
  suggestions: parsed.suggestions,
1714
1974
  nitpicks: parsed.nitpicks,
1715
1975
  markdown,
1716
- tokens
1976
+ tokens,
1977
+ tokensAvailable: response.tokensAvailable
1717
1978
  };
1718
1979
  }
1719
1980
  async function resolveBaseBranch(cwd) {
@@ -1740,9 +2001,9 @@ ${stderr}`;
1740
2001
  }
1741
2002
 
1742
2003
  // src/commands/pr.ts
1743
- import { execFile as execFile4 } from "child_process";
1744
- import { promisify as promisify4 } from "util";
1745
- var exec3 = promisify4(execFile4);
2004
+ import { execFile as execFile3 } from "child_process";
2005
+ import { promisify as promisify3 } from "util";
2006
+ var exec3 = promisify3(execFile3);
1746
2007
  function parsePrResponse(content) {
1747
2008
  const titleMatch = content.match(/^TITLE:\s*(.+)$/m);
1748
2009
  const title = titleMatch ? titleMatch[1].trim() : "Update";
@@ -1821,7 +2082,8 @@ Additional context: ${prompt}` : "");
1821
2082
  title,
1822
2083
  body,
1823
2084
  existingPrNumber,
1824
- tokens
2085
+ tokens,
2086
+ tokensAvailable: response.tokensAvailable
1825
2087
  };
1826
2088
  }
1827
2089
  async function applyPr(draft, opts) {
@@ -1938,7 +2200,7 @@ async function loadReleasePlan(cwd) {
1938
2200
  exitCode: EXIT_CODES.CONFIG_INVALID
1939
2201
  });
1940
2202
  }
1941
- return parsed;
2203
+ return { ...parsed, tokensAvailable: parsed.tokensAvailable ?? true };
1942
2204
  }
1943
2205
  function isPersistedReleasePlan(value) {
1944
2206
  if (!value || typeof value !== "object") return false;
@@ -1961,6 +2223,7 @@ function isPersistedReleasePlan(value) {
1961
2223
  const tokens = p.tokens;
1962
2224
  if (typeof tokens.input !== "number" || !Number.isFinite(tokens.input)) return false;
1963
2225
  if (typeof tokens.output !== "number" || !Number.isFinite(tokens.output)) return false;
2226
+ if (p.tokensAvailable !== void 0 && typeof p.tokensAvailable !== "boolean") return false;
1964
2227
  return true;
1965
2228
  }
1966
2229
  async function deleteReleasePlan(cwd) {
@@ -2092,6 +2355,7 @@ async function release(opts) {
2092
2355
  const tier = resolveModelTier("release");
2093
2356
  let totalInput = 0;
2094
2357
  let totalOutput = 0;
2358
+ let tokensAvailable = true;
2095
2359
  let suggestedBump;
2096
2360
  if (opts.bump) {
2097
2361
  suggestedBump = opts.bump;
@@ -2109,6 +2373,7 @@ ${commits}`,
2109
2373
  });
2110
2374
  totalInput += versionResponse.tokens.input;
2111
2375
  totalOutput += versionResponse.tokens.output;
2376
+ tokensAvailable = versionResponse.tokensAvailable;
2112
2377
  const suggestion = parseVersionSuggestion(versionResponse.content);
2113
2378
  suggestedBump = suggestion?.suggestion ?? heuristicBump(commits);
2114
2379
  }
@@ -2126,6 +2391,7 @@ ${commits}`,
2126
2391
  });
2127
2392
  totalInput += changelogResponse.tokens.input;
2128
2393
  totalOutput += changelogResponse.tokens.output;
2394
+ tokensAvailable = tokensAvailable && changelogResponse.tokensAvailable;
2129
2395
  const changelog = changelogResponse.content;
2130
2396
  const notesTemplate = await loadTemplate("release-notes", templateOpts);
2131
2397
  const notesPrompt = interpolate(notesTemplate, {
@@ -2144,6 +2410,7 @@ ${commits}`,
2144
2410
  });
2145
2411
  totalInput += notesResponse.tokens.input;
2146
2412
  totalOutput += notesResponse.tokens.output;
2413
+ tokensAvailable = tokensAvailable && notesResponse.tokensAvailable;
2147
2414
  const notes = notesResponse.content;
2148
2415
  return {
2149
2416
  suggestedBump,
@@ -2152,7 +2419,8 @@ ${commits}`,
2152
2419
  changelog,
2153
2420
  notes,
2154
2421
  commits,
2155
- tokens: { input: totalInput, output: totalOutput }
2422
+ tokens: { input: totalInput, output: totalOutput },
2423
+ tokensAvailable
2156
2424
  };
2157
2425
  }
2158
2426
  function createReleaseBranchStep(cwd, branchName, startPoint) {
@@ -2300,10 +2568,10 @@ async function prepareRelease(opts) {
2300
2568
  });
2301
2569
  try {
2302
2570
  const dirtyEntries = (await status(cwd)).split("\n").map((line) => line.replace(/\s+$/, "")).filter((line) => line.length >= 3).filter((line) => {
2303
- const path3 = line.slice(3).trim();
2304
- if (path3 === ".gitignore") return false;
2305
- if (path3 === ".gitwise/" || path3 === ".gitwise") return false;
2306
- if (path3.startsWith(".gitwise/")) return false;
2571
+ const path7 = line.slice(3).trim();
2572
+ if (path7 === ".gitignore") return false;
2573
+ if (path7 === ".gitwise/" || path7 === ".gitwise") return false;
2574
+ if (path7.startsWith(".gitwise/")) return false;
2307
2575
  return true;
2308
2576
  });
2309
2577
  if (dirtyEntries.length > 0) {
@@ -2401,7 +2669,8 @@ ${dirtyEntries.join("\n")}`,
2401
2669
  baseCommit,
2402
2670
  targetBranch,
2403
2671
  releaseBranchCreated: releaseBranch !== null,
2404
- tokens: plan.tokens
2672
+ tokens: plan.tokens,
2673
+ tokensAvailable: plan.tokensAvailable
2405
2674
  };
2406
2675
  await tx.run(savePlanStep(cwd, persistedPlan));
2407
2676
  debug("release.prepare.plan.saved", {
@@ -2466,13 +2735,36 @@ ${dirty}`,
2466
2735
  baseCommit: await headSha(cwd),
2467
2736
  targetBranch: await getBranch(cwd),
2468
2737
  releaseBranchCreated: false,
2469
- tokens: plan.tokens
2738
+ tokens: plan.tokens,
2739
+ tokensAvailable: plan.tokensAvailable
2470
2740
  };
2471
2741
  await ensureGitignored(cwd, RELEASE_PLAN_REL_PATH);
2472
2742
  await ensureGitignored(cwd, RELEASE_NOTES_GLOB_REL_PATH);
2473
2743
  await saveReleasePlan(cwd, persistedPlan);
2474
2744
  await finishRelease({ cwd, tagAndPush, createGhRelease, workspacePropagation, signTags });
2475
2745
  }
2746
+ function finishPushFailure(opts) {
2747
+ const { stage, tag, mainBranch, developBranch, newVersion, err } = opts;
2748
+ const cause = err instanceof Error ? err.message : String(err);
2749
+ const action = stage === "tag" ? `create the tag "${tag}"` : stage === "push-main" ? `push "${mainBranch}" (with tags) to origin` : `push "${developBranch}" to origin`;
2750
+ const recoverySteps = stage === "tag" ? [
2751
+ `git tag -a ${tag} -F .gitwise/release-${newVersion}.md`,
2752
+ `git push origin ${mainBranch} --follow-tags`
2753
+ ] : [
2754
+ `git ls-remote --tags origin ${tag} # check whether the tag already reached origin`,
2755
+ `git fetch origin`,
2756
+ `git merge origin/${mainBranch} # do NOT rebase \u2014 that changes the hash ${tag} points to`,
2757
+ `git push origin ${mainBranch} --follow-tags`
2758
+ ];
2759
+ return new GitwiseError({
2760
+ code: "FINISH_PUSH_FAILED",
2761
+ message: `Failed to ${action} while finishing v${newVersion}: ${cause}. The release plan file has already been deleted, so "gw release finish" cannot be re-run, and the release commit already exists locally, so "gw release prepare" will refuse with NO_COMMITS. Recover manually:
2762
+ ${recoverySteps.map((s) => ` ${s}`).join("\n")}`,
2763
+ exitCode: EXIT_CODES.GIT_FAILED,
2764
+ cause: err,
2765
+ details: { stage, tag, mainBranch, developBranch, newVersion }
2766
+ });
2767
+ }
2476
2768
  async function finishRelease(opts) {
2477
2769
  const {
2478
2770
  cwd,
@@ -2663,15 +2955,27 @@ ${cause}`,
2663
2955
  await checkout(cwd, mainBranch);
2664
2956
  }
2665
2957
  if (tagAndPush) {
2666
- await createTag(cwd, tag, notes, { signed: signTags !== false });
2667
- await pushWithTags(cwd, "origin", mainBranch);
2958
+ try {
2959
+ await createTag(cwd, tag, notes, { signed: signTags !== false });
2960
+ } catch (err) {
2961
+ throw finishPushFailure({ stage: "tag", tag, mainBranch, newVersion: plan.newVersion, err });
2962
+ }
2963
+ try {
2964
+ await pushWithTags(cwd, "origin", mainBranch);
2965
+ } catch (err) {
2966
+ throw finishPushFailure({ stage: "push-main", tag, mainBranch, newVersion: plan.newVersion, err });
2967
+ }
2668
2968
  debug("release.finish.tag.pushed", {
2669
2969
  tag,
2670
2970
  branch: mainBranch,
2671
2971
  remote: "origin"
2672
2972
  });
2673
2973
  if (strategy.requiresDevelop()) {
2674
- await push(cwd, "origin", developBranch);
2974
+ try {
2975
+ await push(cwd, "origin", developBranch);
2976
+ } catch (err) {
2977
+ throw finishPushFailure({ stage: "push-develop", tag, mainBranch, developBranch, newVersion: plan.newVersion, err });
2978
+ }
2675
2979
  }
2676
2980
  }
2677
2981
  if (createGhRelease) {
@@ -2781,13 +3085,35 @@ async function gitignoreMatchesPrepareOutput(cwd) {
2781
3085
  expected = applyGitignoreEntry(expected, RELEASE_NOTES_GLOB_REL_PATH);
2782
3086
  return currentContent === expected;
2783
3087
  }
2784
- function writeWorkspaceVersionStep(manifestPath, newVersion) {
3088
+ var CROSS_WORKSPACE_DEPENDENCY_FIELDS = [
3089
+ "dependencies",
3090
+ "devDependencies",
3091
+ "optionalDependencies",
3092
+ "peerDependencies"
3093
+ ];
3094
+ function updateCrossWorkspaceDependencies(parsed, workspaceNames, newVersion) {
3095
+ for (const field of CROSS_WORKSPACE_DEPENDENCY_FIELDS) {
3096
+ const deps = parsed[field];
3097
+ if (!deps || typeof deps !== "object") continue;
3098
+ const depsRecord = deps;
3099
+ for (const depName of Object.keys(depsRecord)) {
3100
+ const spec = depsRecord[depName];
3101
+ if (!workspaceNames.has(depName) || typeof spec !== "string") continue;
3102
+ const prefix = spec.startsWith("^") || spec.startsWith("~") ? spec[0] : "";
3103
+ depsRecord[depName] = `${prefix}${newVersion}`;
3104
+ }
3105
+ }
3106
+ }
3107
+ function writeWorkspaceVersionStep(manifestPath, newVersion, workspaceNames) {
2785
3108
  return {
2786
3109
  name: `write-version:${manifestPath}`,
2787
3110
  apply: async () => {
2788
3111
  const priorBytes = await readFile6(manifestPath);
2789
3112
  const parsed = JSON.parse(priorBytes.toString("utf-8"));
2790
3113
  parsed["version"] = newVersion;
3114
+ if (workspaceNames && workspaceNames.size > 0) {
3115
+ updateCrossWorkspaceDependencies(parsed, workspaceNames, newVersion);
3116
+ }
2791
3117
  await writeJSON(manifestPath, parsed);
2792
3118
  return priorBytes;
2793
3119
  },
@@ -2826,17 +3152,28 @@ async function propagateVersionToWorkspaces(cwd, version2) {
2826
3152
  async function runWorkspaceVersionStepsInto(tx, cwd, version2) {
2827
3153
  const patterns = await readWorkspacePatterns(cwd);
2828
3154
  const workspaceDirs = (await expandWorkspacePatterns(cwd, patterns)).sort();
3155
+ const workspaceNames = /* @__PURE__ */ new Set();
3156
+ for (const dir of workspaceDirs) {
3157
+ const pkgPath = join6(dir, "package.json");
3158
+ if (!await fileExists(pkgPath)) continue;
3159
+ const parsed = await readJSON(pkgPath);
3160
+ if (typeof parsed.name === "string") workspaceNames.add(parsed.name);
3161
+ }
2829
3162
  const modified = [];
2830
3163
  for (const dir of workspaceDirs) {
2831
3164
  const pkgPath = join6(dir, "package.json");
2832
3165
  if (await fileExists(pkgPath)) {
2833
- await tx.run(writeWorkspaceVersionStep(pkgPath, version2));
3166
+ await tx.run(writeWorkspaceVersionStep(pkgPath, version2, workspaceNames));
2834
3167
  modified.push(relative(cwd, pkgPath));
2835
3168
  }
2836
- const pluginPath = join6(dir, "plugin.json");
2837
- if (await fileExists(pluginPath)) {
2838
- await tx.run(writeWorkspaceVersionStep(pluginPath, version2));
2839
- modified.push(relative(cwd, pluginPath));
3169
+ for (const pluginPath of [
3170
+ join6(dir, ".claude-plugin", "plugin.json"),
3171
+ join6(dir, "plugin.json")
3172
+ ]) {
3173
+ if (await fileExists(pluginPath)) {
3174
+ await tx.run(writeWorkspaceVersionStep(pluginPath, version2));
3175
+ modified.push(relative(cwd, pluginPath));
3176
+ }
2840
3177
  }
2841
3178
  }
2842
3179
  return modified;
@@ -2928,6 +3265,11 @@ function segmentToRegex(segment) {
2928
3265
  return new RegExp(`^${escaped}$`);
2929
3266
  }
2930
3267
 
3268
+ // src/commands/token-format.ts
3269
+ function formatTokens(tokens, tokensAvailable) {
3270
+ return tokensAvailable ? `${tokens.input} in / ${tokens.output} out` : "n/a";
3271
+ }
3272
+
2931
3273
  // src/providers/anthropic.ts
2932
3274
  import Anthropic from "@anthropic-ai/sdk";
2933
3275
  var DEFAULT_MAX_TOKENS = 4096;
@@ -2984,7 +3326,8 @@ var AnthropicProvider = class {
2984
3326
  tokens: {
2985
3327
  input: response.usage.input_tokens,
2986
3328
  output: response.usage.output_tokens
2987
- }
3329
+ },
3330
+ tokensAvailable: true
2988
3331
  };
2989
3332
  }
2990
3333
  resolveModel(tier) {
@@ -3003,10 +3346,36 @@ var AnthropicProvider = class {
3003
3346
 
3004
3347
  // src/providers/factory.ts
3005
3348
  function createProvider(config) {
3006
- if (config.kind === "claude-code") {
3007
- return new ClaudeCodeProvider(config.models, config.claudeCliPath);
3349
+ switch (config.kind) {
3350
+ case "claude-code":
3351
+ return new ClaudeCodeProvider(config.models, config.claudeCliPath);
3352
+ case "codex":
3353
+ return new CliSubprocessProvider(codexSpec, config.models, config.codexCliPath);
3354
+ case "copilot":
3355
+ return new CliSubprocessProvider(copilotSpec, config.models, config.copilotCliPath);
3356
+ case "kiro":
3357
+ return new CliSubprocessProvider(kiroSpec, config.models, config.kiroCliPath);
3358
+ case "api":
3359
+ return new AnthropicProvider(config.apiKey, config.models);
3360
+ default: {
3361
+ const unhandled = config.kind;
3362
+ throw new GitwiseError({
3363
+ code: "CONFIG_INVALID",
3364
+ message: `Unknown provider "${String(unhandled)}" in config. Re-run \`gw provider\` to choose a supported provider.`
3365
+ });
3366
+ }
3008
3367
  }
3009
- return new AnthropicProvider(config.apiKey, config.models);
3368
+ }
3369
+ function buildProviderConfig(merged, apiKey) {
3370
+ return {
3371
+ kind: merged.provider,
3372
+ models: merged.models[merged.provider],
3373
+ apiKey,
3374
+ claudeCliPath: merged.claudeCliPath,
3375
+ codexCliPath: merged.codexCliPath,
3376
+ copilotCliPath: merged.copilotCliPath,
3377
+ kiroCliPath: merged.kiroCliPath
3378
+ };
3010
3379
  }
3011
3380
 
3012
3381
  // src/index.ts
@@ -3016,6 +3385,7 @@ export {
3016
3385
  DEFAULT_USER_CONFIG,
3017
3386
  EXIT_CODES,
3018
3387
  GitwiseError,
3388
+ PROVIDER_KINDS,
3019
3389
  STALE_LOCK_MS,
3020
3390
  SUPPORTED_COMMANDS,
3021
3391
  Transaction,
@@ -3026,6 +3396,7 @@ export {
3026
3396
  applyOneCommitStep,
3027
3397
  applyPr,
3028
3398
  applyRelease,
3399
+ buildProviderConfig,
3029
3400
  bumpVersion,
3030
3401
  commit2 as commit,
3031
3402
  createProvider,
@@ -3039,6 +3410,7 @@ export {
3039
3410
  error,
3040
3411
  fileExists,
3041
3412
  finishRelease,
3413
+ formatTokens,
3042
3414
  getApiKey,
3043
3415
  getMergedConfig,
3044
3416
  git_exports as git,
@@ -3059,6 +3431,9 @@ export {
3059
3431
  readUserConfig,
3060
3432
  release,
3061
3433
  resolveClaudeBinary,
3434
+ resolveCodexBinary,
3435
+ resolveCopilotBinary,
3436
+ resolveKiroBinary,
3062
3437
  resolveModelTier,
3063
3438
  review,
3064
3439
  runReleaseInProcess,