@rely-ai/caliber 1.44.2 → 1.45.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.
Files changed (2) hide show
  1. package/dist/bin.js +324 -49
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -84,6 +84,12 @@ function resolveFromEnv() {
84
84
  model: process.env.CALIBER_MODEL || DEFAULT_MODELS["claude-cli"]
85
85
  };
86
86
  }
87
+ if (process.env.CALIBER_USE_OPENCODE === "1" || process.env.CALIBER_USE_OPENCODE === "true") {
88
+ return {
89
+ provider: "opencode",
90
+ model: process.env.CALIBER_MODEL || DEFAULT_MODELS.opencode
91
+ };
92
+ }
87
93
  return null;
88
94
  }
89
95
  function readConfigFile() {
@@ -91,7 +97,7 @@ function readConfigFile() {
91
97
  if (!fs4.existsSync(CONFIG_FILE)) return null;
92
98
  const raw = fs4.readFileSync(CONFIG_FILE, "utf-8");
93
99
  const parsed = JSON.parse(raw);
94
- if (!parsed.provider || !["anthropic", "vertex", "openai", "minimax", "cursor", "claude-cli"].includes(
100
+ if (!parsed.provider || !["anthropic", "vertex", "openai", "minimax", "cursor", "claude-cli", "opencode"].includes(
95
101
  parsed.provider
96
102
  )) {
97
103
  return null;
@@ -118,6 +124,9 @@ function getDisplayModel(config) {
118
124
  if (config.model === "default" && config.provider === "claude-cli") {
119
125
  return process.env.ANTHROPIC_MODEL || "default (inherited from Claude Code)";
120
126
  }
127
+ if (config.model === "default" && config.provider === "opencode") {
128
+ return "default (inherited from OpenCode)";
129
+ }
121
130
  return config.model;
122
131
  }
123
132
  function getFastModel() {
@@ -143,7 +152,8 @@ var init_config = __esm({
143
152
  openai: "gpt-5.4-mini",
144
153
  minimax: "MiniMax-M2.7",
145
154
  cursor: "sonnet-4.6",
146
- "claude-cli": "default"
155
+ "claude-cli": "default",
156
+ opencode: "default"
147
157
  };
148
158
  MODEL_CONTEXT_WINDOWS = {
149
159
  "claude-sonnet-4-6": 2e5,
@@ -267,7 +277,11 @@ var SEAT_BASED_PROVIDERS;
267
277
  var init_types = __esm({
268
278
  "src/llm/types.ts"() {
269
279
  "use strict";
270
- SEAT_BASED_PROVIDERS = /* @__PURE__ */ new Set(["cursor", "claude-cli"]);
280
+ SEAT_BASED_PROVIDERS = /* @__PURE__ */ new Set([
281
+ "cursor",
282
+ "claude-cli",
283
+ "opencode"
284
+ ]);
271
285
  }
272
286
  });
273
287
 
@@ -897,7 +911,7 @@ var init_builtin_skills = __esm({
897
911
  });
898
912
 
899
913
  // src/utils/editor.ts
900
- import { execSync as execSync15, spawn as spawn3 } from "child_process";
914
+ import { execSync as execSync16, spawn as spawn4 } from "child_process";
901
915
  import fs29 from "fs";
902
916
  import path25 from "path";
903
917
  import os6 from "os";
@@ -910,7 +924,7 @@ function getEmptyFilePath(proposedPath) {
910
924
  function commandExists(cmd) {
911
925
  try {
912
926
  const check = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
913
- execSync15(check, { stdio: "ignore" });
927
+ execSync16(check, { stdio: "ignore" });
914
928
  return true;
915
929
  } catch {
916
930
  return false;
@@ -928,22 +942,22 @@ function openDiffsInEditor(editor, files) {
928
942
  for (const file of files) {
929
943
  try {
930
944
  const leftPath = file.originalPath ?? getEmptyFilePath(file.proposedPath);
931
- if (IS_WINDOWS4) {
945
+ if (IS_WINDOWS5) {
932
946
  const quote = (s) => `"${s}"`;
933
- spawn3([cmd, "--diff", quote(leftPath), quote(file.proposedPath)].join(" "), { shell: true, stdio: "ignore", detached: true }).unref();
947
+ spawn4([cmd, "--diff", quote(leftPath), quote(file.proposedPath)].join(" "), { shell: true, stdio: "ignore", detached: true }).unref();
934
948
  } else {
935
- spawn3(cmd, ["--diff", leftPath, file.proposedPath], { stdio: "ignore", detached: true }).unref();
949
+ spawn4(cmd, ["--diff", leftPath, file.proposedPath], { stdio: "ignore", detached: true }).unref();
936
950
  }
937
951
  } catch {
938
952
  continue;
939
953
  }
940
954
  }
941
955
  }
942
- var IS_WINDOWS4, DIFF_TEMP_DIR;
956
+ var IS_WINDOWS5, DIFF_TEMP_DIR;
943
957
  var init_editor = __esm({
944
958
  "src/utils/editor.ts"() {
945
959
  "use strict";
946
- IS_WINDOWS4 = process.platform === "win32";
960
+ IS_WINDOWS5 = process.platform === "win32";
947
961
  DIFF_TEMP_DIR = path25.join(os6.tmpdir(), "caliber-diff");
948
962
  }
949
963
  });
@@ -3132,6 +3146,215 @@ function isClaudeCliLoggedIn() {
3132
3146
  return cachedLoggedIn;
3133
3147
  }
3134
3148
 
3149
+ // src/llm/opencode.ts
3150
+ import { spawn as spawn3, execSync as execSync8 } from "child_process";
3151
+ var OPENCODE_BIN = "opencode";
3152
+ var DEFAULT_TIMEOUT_MS3 = 10 * 60 * 1e3;
3153
+ var IS_WINDOWS3 = process.platform === "win32";
3154
+ var cachedLoggedIn2 = null;
3155
+ function isOpenCodeAvailable() {
3156
+ try {
3157
+ const cmd = IS_WINDOWS3 ? `where ${OPENCODE_BIN}` : `which ${OPENCODE_BIN}`;
3158
+ execSync8(cmd, { stdio: "ignore" });
3159
+ return true;
3160
+ } catch {
3161
+ return false;
3162
+ }
3163
+ }
3164
+ function isOpenCodeLoggedIn() {
3165
+ if (cachedLoggedIn2 !== null) return cachedLoggedIn2;
3166
+ try {
3167
+ const result = execSync8("opencode auth status", {
3168
+ stdio: ["ignore", "pipe", "pipe"],
3169
+ timeout: 5e3
3170
+ });
3171
+ const output = result.toString().trim();
3172
+ try {
3173
+ const status = JSON.parse(output);
3174
+ cachedLoggedIn2 = status.loggedIn === true;
3175
+ } catch {
3176
+ cachedLoggedIn2 = !output.toLowerCase().includes("not logged in");
3177
+ }
3178
+ } catch {
3179
+ cachedLoggedIn2 = false;
3180
+ }
3181
+ return cachedLoggedIn2;
3182
+ }
3183
+ function spawnOpenCode(args) {
3184
+ const env = { ...process.env, OPENCODE_DISABLE_AUTOCOMPACT: "TRUE" };
3185
+ if (IS_WINDOWS3) {
3186
+ return spawn3([OPENCODE_BIN, ...args].join(" "), {
3187
+ cwd: process.cwd(),
3188
+ stdio: ["pipe", "pipe", "pipe"],
3189
+ env,
3190
+ shell: true
3191
+ });
3192
+ } else {
3193
+ return spawn3(OPENCODE_BIN, args, {
3194
+ cwd: process.cwd(),
3195
+ stdio: ["pipe", "pipe", "pipe"],
3196
+ env
3197
+ });
3198
+ }
3199
+ }
3200
+ function runCommand(args, input, timeoutMs) {
3201
+ return new Promise((resolve3, reject) => {
3202
+ const child = spawnOpenCode(args);
3203
+ const stderrChunks = [];
3204
+ child.stdin.end(input);
3205
+ let stdoutData = Buffer.alloc(0);
3206
+ child.stdout.on("data", (chunk) => {
3207
+ stdoutData = Buffer.concat([stdoutData, chunk]);
3208
+ });
3209
+ child.stderr.on("data", (chunk) => {
3210
+ stderrChunks.push(chunk);
3211
+ });
3212
+ const timer = setTimeout(() => {
3213
+ child.kill("SIGTERM");
3214
+ reject(
3215
+ new Error(
3216
+ `OpenCode timed out after ${timeoutMs / 1e3}s. Set CALIBER_OPENCODE_TIMEOUT_MS to increase.`
3217
+ )
3218
+ );
3219
+ }, timeoutMs);
3220
+ child.on("error", (err) => {
3221
+ clearTimeout(timer);
3222
+ reject(err);
3223
+ });
3224
+ child.on("close", (code, signal) => {
3225
+ clearTimeout(timer);
3226
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
3227
+ if (code === 0) {
3228
+ resolve3(stdoutData.toString("utf-8").trim());
3229
+ } else {
3230
+ const friendly = parseSeatBasedError(stderr, code);
3231
+ const base = signal ? `OpenCode killed (${signal})` : code != null ? `OpenCode exited with code ${code}` : "OpenCode exited";
3232
+ const detail = friendly || stderr;
3233
+ reject(new Error(detail ? `${base}. ${detail}` : base));
3234
+ }
3235
+ });
3236
+ });
3237
+ }
3238
+ function runCommandStream(args, input, callbacks, timeoutMs) {
3239
+ return new Promise((resolve3, reject) => {
3240
+ const child = spawnOpenCode(args);
3241
+ const stderrChunks = [];
3242
+ let settled = false;
3243
+ let lineBuffer = "";
3244
+ child.stdin.end(input);
3245
+ child.stdout.on("data", (chunk) => {
3246
+ const text = chunk.toString("utf-8");
3247
+ lineBuffer += text;
3248
+ const lines = lineBuffer.split("\n");
3249
+ lineBuffer = lines.pop() || "";
3250
+ for (const line of lines) {
3251
+ if (!line.trim()) continue;
3252
+ try {
3253
+ const event = JSON.parse(line);
3254
+ if (event.type === "text" && event.part?.text) {
3255
+ callbacks.onText(event.part.text);
3256
+ }
3257
+ } catch {
3258
+ }
3259
+ }
3260
+ });
3261
+ child.stderr.on("data", (chunk) => {
3262
+ stderrChunks.push(chunk);
3263
+ });
3264
+ const timer = setTimeout(() => {
3265
+ child.kill("SIGTERM");
3266
+ if (!settled) {
3267
+ settled = true;
3268
+ reject(
3269
+ new Error(
3270
+ `OpenCode timed out after ${timeoutMs / 1e3}s. Set CALIBER_OPENCODE_TIMEOUT_MS to increase.`
3271
+ )
3272
+ );
3273
+ }
3274
+ }, timeoutMs);
3275
+ child.on("error", (err) => {
3276
+ clearTimeout(timer);
3277
+ if (!settled) {
3278
+ settled = true;
3279
+ reject(err);
3280
+ }
3281
+ });
3282
+ child.on("close", (code, signal) => {
3283
+ clearTimeout(timer);
3284
+ if (settled) return;
3285
+ settled = true;
3286
+ if (code === 0) {
3287
+ if (lineBuffer.trim()) {
3288
+ try {
3289
+ const event = JSON.parse(lineBuffer);
3290
+ if (event.type === "text" && event.part?.text) {
3291
+ callbacks.onText(event.part.text);
3292
+ }
3293
+ } catch {
3294
+ }
3295
+ }
3296
+ callbacks.onEnd({ stopReason: "end_turn" });
3297
+ resolve3();
3298
+ } else {
3299
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
3300
+ const friendly = parseSeatBasedError(stderr, code);
3301
+ const base = signal ? `OpenCode killed (${signal})` : code != null ? `OpenCode exited with code ${code}` : "OpenCode exited";
3302
+ const detail = friendly || stderr;
3303
+ reject(new Error(detail ? `${base}. ${detail}` : base));
3304
+ }
3305
+ });
3306
+ });
3307
+ }
3308
+ var OpenCodeProvider = class {
3309
+ defaultModel;
3310
+ timeoutMs;
3311
+ constructor(config) {
3312
+ this.defaultModel = config.model || "default";
3313
+ const envTimeout = process.env.CALIBER_OPENCODE_TIMEOUT_MS;
3314
+ this.timeoutMs = envTimeout ? parseInt(envTimeout, 10) : DEFAULT_TIMEOUT_MS3;
3315
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs < 1e3) {
3316
+ this.timeoutMs = DEFAULT_TIMEOUT_MS3;
3317
+ }
3318
+ }
3319
+ async call(options) {
3320
+ const system = options.system || "";
3321
+ const prompt = options.prompt || "";
3322
+ const combined = system + "\n\n" + prompt;
3323
+ const model = options.model || this.defaultModel;
3324
+ const args = ["run", "--format", "json", "--model", model, "--", "-"];
3325
+ const result = await runCommand(args, combined, this.timeoutMs);
3326
+ trackUsage(model, {
3327
+ inputTokens: estimateTokens(combined),
3328
+ outputTokens: estimateTokens(result)
3329
+ });
3330
+ return result;
3331
+ }
3332
+ async stream(options, callbacks) {
3333
+ const system = options.system || "";
3334
+ const prompt = options.prompt || "";
3335
+ const combined = system + "\n\n" + prompt;
3336
+ const model = options.model || this.defaultModel;
3337
+ const args = ["run", "--format", "json", "--model", model, "--", "-"];
3338
+ const inputEstimate = estimateTokens(combined);
3339
+ let outputChars = 0;
3340
+ const wrappedCallbacks = {
3341
+ onText: (text) => {
3342
+ outputChars += text.length;
3343
+ callbacks.onText(text);
3344
+ },
3345
+ onEnd: (meta) => {
3346
+ trackUsage(model, {
3347
+ inputTokens: inputEstimate,
3348
+ outputTokens: Math.ceil(outputChars / 4)
3349
+ });
3350
+ callbacks.onEnd(meta);
3351
+ },
3352
+ onError: (err) => callbacks.onError(err)
3353
+ };
3354
+ return runCommandStream(args, combined, wrappedCallbacks, this.timeoutMs);
3355
+ }
3356
+ };
3357
+
3135
3358
  // src/llm/model-recovery.ts
3136
3359
  init_config();
3137
3360
  init_resolve_caliber();
@@ -3155,7 +3378,8 @@ var KNOWN_MODELS = {
3155
3378
  openai: ["gpt-5.4-mini", "gpt-4o", "gpt-4o-mini", "o3-mini"],
3156
3379
  minimax: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"],
3157
3380
  cursor: ["auto", "composer-1.5"],
3158
- "claude-cli": []
3381
+ "claude-cli": [],
3382
+ opencode: []
3159
3383
  };
3160
3384
  function isModelNotAvailableError(error) {
3161
3385
  const msg = error.message.toLowerCase();
@@ -3285,6 +3509,19 @@ function createProvider(config) {
3285
3509
  }
3286
3510
  return new ClaudeCliProvider(config);
3287
3511
  }
3512
+ case "opencode": {
3513
+ if (!isOpenCodeAvailable()) {
3514
+ throw new Error(
3515
+ "OpenCode provider requires the OpenCode CLI. Install it from https://opencode.ai then run `opencode auth login`. Alternatively set ANTHROPIC_API_KEY or choose another provider."
3516
+ );
3517
+ }
3518
+ if (!isOpenCodeLoggedIn()) {
3519
+ throw new Error(
3520
+ "OpenCode CLI is installed but not logged in. Run `opencode auth login` in your terminal to authenticate, then retry."
3521
+ );
3522
+ }
3523
+ return new OpenCodeProvider(config);
3524
+ }
3288
3525
  default:
3289
3526
  throw new Error(`Unknown provider: ${config.provider}`);
3290
3527
  }
@@ -3294,7 +3531,7 @@ function getProvider() {
3294
3531
  const config = loadConfig();
3295
3532
  if (!config) {
3296
3533
  throw new Error(
3297
- `No LLM provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, MINIMAX_API_KEY, or VERTEX_PROJECT_ID; or run \`${resolveCaliber()} config\` and choose Cursor or Claude Code; or set CALIBER_USE_CURSOR_SEAT=1 / CALIBER_USE_CLAUDE_CLI=1.`
3534
+ `No LLM provider configured. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, MINIMAX_API_KEY, or VERTEX_PROJECT_ID; or run \`${resolveCaliber()} config\` and choose Cursor, Claude Code, or OpenCode; or set CALIBER_USE_CURSOR_SEAT=1 / CALIBER_USE_CLAUDE_CLI=1 / CALIBER_USE_OPENCODE=1.`
3298
3535
  );
3299
3536
  }
3300
3537
  cachedConfig = config;
@@ -3945,7 +4182,7 @@ init_config();
3945
4182
  import fs8 from "fs";
3946
4183
  import path7 from "path";
3947
4184
  import crypto from "crypto";
3948
- import { execSync as execSync8 } from "child_process";
4185
+ import { execSync as execSync9 } from "child_process";
3949
4186
  var CACHE_VERSION = 1;
3950
4187
  var CACHE_DIR = ".caliber/cache";
3951
4188
  var CACHE_FILE = "fingerprint.json";
@@ -3954,7 +4191,7 @@ function getCachePath(dir) {
3954
4191
  }
3955
4192
  function getGitHead(dir) {
3956
4193
  try {
3957
- return execSync8("git rev-parse HEAD", {
4194
+ return execSync9("git rev-parse HEAD", {
3958
4195
  cwd: dir,
3959
4196
  encoding: "utf-8",
3960
4197
  stdio: ["pipe", "pipe", "pipe"],
@@ -3966,7 +4203,7 @@ function getGitHead(dir) {
3966
4203
  }
3967
4204
  function getDirtySignature(dir) {
3968
4205
  try {
3969
- const output = execSync8("git diff --name-only HEAD", {
4206
+ const output = execSync9("git diff --name-only HEAD", {
3970
4207
  cwd: dir,
3971
4208
  encoding: "utf-8",
3972
4209
  stdio: ["pipe", "pipe", "pipe"],
@@ -4309,7 +4546,7 @@ function getCursorConfigDir() {
4309
4546
  init_resolve_caliber();
4310
4547
  import fs11 from "fs";
4311
4548
  import path10 from "path";
4312
- import { execSync as execSync9 } from "child_process";
4549
+ import { execSync as execSync10 } from "child_process";
4313
4550
  var SETTINGS_PATH = path10.join(".claude", "settings.json");
4314
4551
  var REFRESH_TAIL = "refresh --quiet";
4315
4552
  var HOOK_DESCRIPTION = "Caliber: auto-refreshing docs based on code changes";
@@ -4514,7 +4751,7 @@ ${PRECOMMIT_END}`;
4514
4751
  }
4515
4752
  function getGitHooksDir() {
4516
4753
  try {
4517
- const gitDir = execSync9("git rev-parse --git-dir", {
4754
+ const gitDir = execSync10("git rev-parse --git-dir", {
4518
4755
  encoding: "utf-8",
4519
4756
  stdio: ["pipe", "pipe", "pipe"]
4520
4757
  }).trim();
@@ -5278,16 +5515,20 @@ async function generateSetup(fingerprint, targetAgent, prompt, callbacks, failin
5278
5515
  ({ platform, topic }) => generateSkill(skillContext, topic, fastModel).then((skill) => ({ platform, skill }))
5279
5516
  )
5280
5517
  );
5281
- const { succeeded, failed: failedCount } = mergeSkillResults(skillResults, setup);
5518
+ const { succeeded, failed: failedCount, failedNames } = mergeSkillResults(skillResults, setup);
5282
5519
  if (failedCount > 0 && callbacks) {
5283
5520
  const msg = succeeded === 0 ? `${failedCount} skill${failedCount === 1 ? "" : "s"} failed to generate \u2014 config saved without skills` : `Warning: ${failedCount} of ${failedCount + succeeded} skill${failedCount === 1 ? "" : "s"} failed to generate`;
5284
5521
  callbacks.onStatus(msg);
5522
+ for (const name of failedNames) {
5523
+ callbacks.onStatus(` \u2192 ${name}`);
5524
+ }
5285
5525
  }
5286
5526
  return coreResult;
5287
5527
  }
5288
5528
  function mergeSkillResults(results, setup) {
5289
5529
  let succeeded = 0;
5290
5530
  let failed = 0;
5531
+ const failedNames = [];
5291
5532
  for (const result of results) {
5292
5533
  if (result.status === "fulfilled") {
5293
5534
  const { platform, skill } = result.value;
@@ -5303,9 +5544,11 @@ function mergeSkillResults(results, setup) {
5303
5544
  succeeded++;
5304
5545
  } else {
5305
5546
  failed++;
5547
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
5548
+ failedNames.push(reason);
5306
5549
  }
5307
5550
  }
5308
- return { succeeded, failed };
5551
+ return { succeeded, failed, failedNames };
5309
5552
  }
5310
5553
  var MAX_SKILL_TOPICS = 5;
5311
5554
  function collectSkillTopics(setup, targetAgent, fingerprint) {
@@ -5630,8 +5873,14 @@ async function generateSkillsForSetup(setup, fingerprint, targetAgent, onStatus)
5630
5873
  ({ platform, topic }) => generateSkill(skillContext, topic, fastModel).then((skill) => ({ platform, skill }))
5631
5874
  )
5632
5875
  );
5633
- const { succeeded, failed } = mergeSkillResults(skillResults, setup);
5634
- if (failed > 0) onStatus?.(`${succeeded} generated, ${failed} failed`);
5876
+ const { succeeded, failed, failedNames } = mergeSkillResults(skillResults, setup);
5877
+ if (failed > 0) {
5878
+ const msg = failed === skillTopics.length ? `All ${failed} skills failed to generate` : `${failed}/${skillTopics.length} skills failed to generate`;
5879
+ onStatus?.(msg);
5880
+ for (const name of failedNames) {
5881
+ onStatus?.(` \u2192 ${name}`);
5882
+ }
5883
+ }
5635
5884
  return succeeded;
5636
5885
  }
5637
5886
  var LIMITS = {
@@ -6622,7 +6871,7 @@ init_resolve_caliber();
6622
6871
  // src/lib/state.ts
6623
6872
  import fs25 from "fs";
6624
6873
  import path21 from "path";
6625
- import { execSync as execSync10 } from "child_process";
6874
+ import { execSync as execSync11 } from "child_process";
6626
6875
  var STATE_FILE = path21.join(CALIBER_DIR, ".caliber-state.json");
6627
6876
  function normalizeTargetAgent(value) {
6628
6877
  if (Array.isArray(value)) return value;
@@ -6650,7 +6899,7 @@ function writeState(state) {
6650
6899
  }
6651
6900
  function getCurrentHeadSha() {
6652
6901
  try {
6653
- return execSync10("git rev-parse HEAD", {
6902
+ return execSync11("git rev-parse HEAD", {
6654
6903
  encoding: "utf-8",
6655
6904
  stdio: ["pipe", "pipe", "pipe"]
6656
6905
  }).trim();
@@ -6681,9 +6930,10 @@ init_config();
6681
6930
  import chalk3 from "chalk";
6682
6931
  import select2 from "@inquirer/select";
6683
6932
  import confirm from "@inquirer/confirm";
6684
- var IS_WINDOWS3 = process.platform === "win32";
6933
+ var IS_WINDOWS4 = process.platform === "win32";
6685
6934
  var PROVIDER_CHOICES = [
6686
6935
  { name: "Claude Code \u2014 use your existing subscription (no API key)", value: "claude-cli" },
6936
+ { name: "OpenCode \u2014 use your existing subscription (no API key)", value: "opencode" },
6687
6937
  { name: "Cursor \u2014 use your existing subscription (no API key)", value: "cursor" },
6688
6938
  { name: "Anthropic \u2014 API key from console.anthropic.com", value: "anthropic" },
6689
6939
  { name: "Google Vertex AI \u2014 Claude models via GCP", value: "vertex" },
@@ -6726,10 +6976,23 @@ async function runInteractiveProviderSetup(options) {
6726
6976
  }
6727
6977
  break;
6728
6978
  }
6979
+ case "opencode": {
6980
+ if (!isOpenCodeAvailable()) {
6981
+ console.log(chalk3.yellow("\n OpenCode CLI not found."));
6982
+ console.log(chalk3.dim(" Install it from: ") + chalk3.hex("#83D1EB")("https://opencode.ai"));
6983
+ console.log(
6984
+ chalk3.dim(" Then run ") + chalk3.hex("#83D1EB")("opencode auth login") + chalk3.dim(" to authenticate.\n")
6985
+ );
6986
+ const proceed = await confirm({ message: "Continue anyway?" });
6987
+ if (!proceed) throw new Error("__exit__");
6988
+ }
6989
+ config.model = await promptInput(`Model (default: ${DEFAULT_MODELS.opencode}):`) || DEFAULT_MODELS.opencode;
6990
+ break;
6991
+ }
6729
6992
  case "cursor": {
6730
6993
  if (!isCursorAgentAvailable()) {
6731
6994
  console.log(chalk3.yellow("\n Cursor Agent CLI not found."));
6732
- if (IS_WINDOWS3) {
6995
+ if (IS_WINDOWS4) {
6733
6996
  console.log(
6734
6997
  chalk3.dim(" Install it from: ") + chalk3.hex("#83D1EB")("https://www.cursor.com/downloads")
6735
6998
  );
@@ -7336,7 +7599,7 @@ function checkGrounding(dir) {
7336
7599
 
7337
7600
  // src/scoring/checks/accuracy.ts
7338
7601
  import { existsSync as existsSync4, statSync } from "fs";
7339
- import { execSync as execSync11 } from "child_process";
7602
+ import { execSync as execSync12 } from "child_process";
7340
7603
  import { join as join5 } from "path";
7341
7604
  init_resolve_caliber();
7342
7605
  function validateReferences(dir) {
@@ -7346,13 +7609,13 @@ function validateReferences(dir) {
7346
7609
  }
7347
7610
  function detectGitDrift(dir) {
7348
7611
  try {
7349
- execSync11("git rev-parse --git-dir", { cwd: dir, stdio: ["pipe", "pipe", "pipe"] });
7612
+ execSync12("git rev-parse --git-dir", { cwd: dir, stdio: ["pipe", "pipe", "pipe"] });
7350
7613
  } catch {
7351
7614
  return { commitsSinceConfigUpdate: 0, lastConfigCommit: null, isGitRepo: false };
7352
7615
  }
7353
7616
  const configFiles = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".cursor/rules"];
7354
7617
  try {
7355
- const headTimestamp = execSync11(
7618
+ const headTimestamp = execSync12(
7356
7619
  "git log -1 --format=%ct HEAD",
7357
7620
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7358
7621
  ).trim();
@@ -7373,7 +7636,7 @@ function detectGitDrift(dir) {
7373
7636
  let latestConfigCommitHash = null;
7374
7637
  for (const file of configFiles) {
7375
7638
  try {
7376
- const hash = execSync11(
7639
+ const hash = execSync12(
7377
7640
  `git log -1 --format=%H -- "${file}"`,
7378
7641
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7379
7642
  ).trim();
@@ -7382,7 +7645,7 @@ function detectGitDrift(dir) {
7382
7645
  latestConfigCommitHash = hash;
7383
7646
  } else {
7384
7647
  try {
7385
- execSync11(
7648
+ execSync12(
7386
7649
  `git merge-base --is-ancestor ${latestConfigCommitHash} ${hash}`,
7387
7650
  { cwd: dir, stdio: ["pipe", "pipe", "pipe"] }
7388
7651
  );
@@ -7397,12 +7660,12 @@ function detectGitDrift(dir) {
7397
7660
  return { commitsSinceConfigUpdate: 0, lastConfigCommit: null, isGitRepo: true };
7398
7661
  }
7399
7662
  try {
7400
- const countStr = execSync11(
7663
+ const countStr = execSync12(
7401
7664
  `git rev-list --count ${latestConfigCommitHash}..HEAD`,
7402
7665
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7403
7666
  ).trim();
7404
7667
  const commitsSince = parseInt(countStr, 10) || 0;
7405
- const lastDate = execSync11(
7668
+ const lastDate = execSync12(
7406
7669
  `git log -1 --format=%ci ${latestConfigCommitHash}`,
7407
7670
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7408
7671
  ).trim();
@@ -7475,12 +7738,12 @@ function checkAccuracy(dir) {
7475
7738
  // src/scoring/checks/freshness.ts
7476
7739
  init_resolve_caliber();
7477
7740
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
7478
- import { execSync as execSync12 } from "child_process";
7741
+ import { execSync as execSync13 } from "child_process";
7479
7742
  import { join as join6 } from "path";
7480
7743
  function getCommitsSinceConfigUpdate(dir) {
7481
7744
  const configFiles = ["CLAUDE.md", "AGENTS.md", ".cursorrules"];
7482
7745
  try {
7483
- const headTimestamp = execSync12(
7746
+ const headTimestamp = execSync13(
7484
7747
  "git log -1 --format=%ct HEAD",
7485
7748
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7486
7749
  ).trim();
@@ -7500,12 +7763,12 @@ function getCommitsSinceConfigUpdate(dir) {
7500
7763
  }
7501
7764
  for (const file of configFiles) {
7502
7765
  try {
7503
- const hash = execSync12(
7766
+ const hash = execSync13(
7504
7767
  `git log -1 --format=%H -- "${file}"`,
7505
7768
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7506
7769
  ).trim();
7507
7770
  if (hash) {
7508
- const countStr = execSync12(
7771
+ const countStr = execSync13(
7509
7772
  `git rev-list --count ${hash}..HEAD`,
7510
7773
  { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7511
7774
  ).trim();
@@ -7623,7 +7886,7 @@ function checkFreshness(dir) {
7623
7886
 
7624
7887
  // src/scoring/checks/bonus.ts
7625
7888
  import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
7626
- import { execSync as execSync13 } from "child_process";
7889
+ import { execSync as execSync14 } from "child_process";
7627
7890
  import { join as join7 } from "path";
7628
7891
  init_resolve_caliber();
7629
7892
  init_pre_commit_block();
@@ -7642,7 +7905,7 @@ function configContentSuggestsPinnedModel(lower) {
7642
7905
  // src/scoring/checks/bonus.ts
7643
7906
  function hasPreCommitHook(dir) {
7644
7907
  try {
7645
- const gitDir = execSync13("git rev-parse --git-dir", {
7908
+ const gitDir = execSync14("git rev-parse --git-dir", {
7646
7909
  cwd: dir,
7647
7910
  encoding: "utf-8",
7648
7911
  stdio: ["pipe", "pipe", "pipe"]
@@ -8112,7 +8375,7 @@ import fs27 from "fs";
8112
8375
  import path23 from "path";
8113
8376
  import os5 from "os";
8114
8377
  import crypto4 from "crypto";
8115
- import { execSync as execSync14 } from "child_process";
8378
+ import { execSync as execSync15 } from "child_process";
8116
8379
  var CONFIG_DIR2 = path23.join(os5.homedir(), ".caliber");
8117
8380
  var CONFIG_FILE2 = path23.join(CONFIG_DIR2, "config.json");
8118
8381
  var runtimeDisabled = false;
@@ -8164,7 +8427,7 @@ var PERSONAL_DOMAINS = /* @__PURE__ */ new Set([
8164
8427
  ]);
8165
8428
  function getGitEmail() {
8166
8429
  try {
8167
- const email = execSync14("git config user.email", { encoding: "utf-8" }).trim();
8430
+ const email = execSync15("git config user.email", { encoding: "utf-8" }).trim();
8168
8431
  return email || void 0;
8169
8432
  } catch {
8170
8433
  return void 0;
@@ -8183,7 +8446,7 @@ function getGitEmailInfo() {
8183
8446
  }
8184
8447
  function getRepoHash() {
8185
8448
  try {
8186
- const remote = execSync14("git remote get-url origin || git rev-parse --show-toplevel", {
8449
+ const remote = execSync15("git remote get-url origin || git rev-parse --show-toplevel", {
8187
8450
  encoding: "utf-8",
8188
8451
  stdio: ["pipe", "pipe", "pipe"]
8189
8452
  }).trim();
@@ -10647,7 +10910,7 @@ function getScoreTrend(entries) {
10647
10910
  }
10648
10911
 
10649
10912
  // src/commands/init.ts
10650
- var IS_WINDOWS5 = process.platform === "win32";
10913
+ var IS_WINDOWS6 = process.platform === "win32";
10651
10914
  function log(verbose, ...args) {
10652
10915
  if (verbose) console.log(chalk14.dim(` [verbose] ${args.map(String).join(" ")}`));
10653
10916
  }
@@ -10691,6 +10954,14 @@ async function initCommand(options) {
10691
10954
  const report = options.debugReport ? new DebugReport() : null;
10692
10955
  console.log(title.bold(" Step 1/3 \u2014 Connect\n"));
10693
10956
  let config = loadConfig();
10957
+ if (!config && options.agent?.includes("opencode")) {
10958
+ if (isOpenCodeAvailable()) {
10959
+ console.log(chalk14.dim(" Detected: OpenCode (uses your existing subscription)\n"));
10960
+ const autoConfig = { provider: "opencode", model: DEFAULT_MODELS.opencode };
10961
+ writeConfigFile(autoConfig);
10962
+ config = autoConfig;
10963
+ }
10964
+ }
10694
10965
  if (!config && !options.autoApprove) {
10695
10966
  if (isClaudeCliAvailable() && isClaudeCliLoggedIn()) {
10696
10967
  console.log(chalk14.dim(" Detected: Claude Code CLI (uses your Pro/Max/Team subscription)\n"));
@@ -10720,6 +10991,10 @@ async function initCommand(options) {
10720
10991
  const autoConfig = { provider: "cursor", model: "sonnet-4.6" };
10721
10992
  writeConfigFile(autoConfig);
10722
10993
  config = autoConfig;
10994
+ } else if (isOpenCodeAvailable()) {
10995
+ const autoConfig = { provider: "opencode", model: DEFAULT_MODELS.opencode };
10996
+ writeConfigFile(autoConfig);
10997
+ config = autoConfig;
10723
10998
  }
10724
10999
  }
10725
11000
  if (!config) {
@@ -10783,7 +11058,7 @@ async function initCommand(options) {
10783
11058
  console.log(` ${chalk14.green("\u2713")} Onboarding hook \u2014 nudges new team members to set up`);
10784
11059
  installSessionStartHook();
10785
11060
  console.log(` ${chalk14.green("\u2713")} Freshness hook \u2014 warns when configs are stale`);
10786
- if (IS_WINDOWS5) {
11061
+ if (IS_WINDOWS6) {
10787
11062
  console.log(
10788
11063
  chalk14.yellow(
10789
11064
  "\n Note: hooks use shell syntax and require Git Bash (included with Git for Windows)."
@@ -11798,7 +12073,7 @@ import ora6 from "ora";
11798
12073
  import pLimit from "p-limit";
11799
12074
 
11800
12075
  // src/lib/git-diff.ts
11801
- import { execSync as execSync16 } from "child_process";
12076
+ import { execSync as execSync17 } from "child_process";
11802
12077
  var MAX_DIFF_BYTES = 1e5;
11803
12078
  var DOC_PATTERNS = [
11804
12079
  "CLAUDE.md",
@@ -11824,7 +12099,7 @@ function excludeArgs() {
11824
12099
  }
11825
12100
  function safeExec(cmd) {
11826
12101
  try {
11827
- return execSync16(cmd, {
12102
+ return execSync17(cmd, {
11828
12103
  encoding: "utf-8",
11829
12104
  stdio: ["pipe", "pipe", "pipe"],
11830
12105
  maxBuffer: 10 * 1024 * 1024
@@ -13694,13 +13969,13 @@ async function learnObserveCommand(options) {
13694
13969
  try {
13695
13970
  const { resolveCaliber: resolveCaliber2, isNpxResolution: isNpxResolution2 } = await Promise.resolve().then(() => (init_resolve_caliber(), resolve_caliber_exports));
13696
13971
  const bin = resolveCaliber2();
13697
- const { spawn: spawn4 } = await import("child_process");
13972
+ const { spawn: spawn5 } = await import("child_process");
13698
13973
  const logPath = path38.join(getLearningDir(), LEARNING_FINALIZE_LOG);
13699
13974
  if (!fs47.existsSync(getLearningDir())) fs47.mkdirSync(getLearningDir(), { recursive: true });
13700
13975
  const logFd = fs47.openSync(logPath, "a");
13701
13976
  const NPX_SUFFIX = " --yes @rely-ai/caliber";
13702
13977
  const [exe, binArgs] = isNpxResolution2() ? [bin.slice(0, -NPX_SUFFIX.length) || "npx", ["--yes", "@rely-ai/caliber"]] : [bin, []];
13703
- spawn4(exe, [...binArgs, "learn", "finalize", "--auto", "--incremental"], {
13978
+ spawn5(exe, [...binArgs, "learn", "finalize", "--auto", "--incremental"], {
13704
13979
  detached: true,
13705
13980
  stdio: ["ignore", logFd, logFd]
13706
13981
  }).unref();
@@ -14798,7 +15073,7 @@ learn.command("add <content>").description("Add a learning directly (used by age
14798
15073
  import fs53 from "fs";
14799
15074
  import path43 from "path";
14800
15075
  import { fileURLToPath as fileURLToPath2 } from "url";
14801
- import { execSync as execSync17, execFileSync as execFileSync5 } from "child_process";
15076
+ import { execSync as execSync18, execFileSync as execFileSync5 } from "child_process";
14802
15077
  import chalk29 from "chalk";
14803
15078
  import ora8 from "ora";
14804
15079
  import confirm4 from "@inquirer/confirm";
@@ -14826,7 +15101,7 @@ function isNewer(registry, current) {
14826
15101
  }
14827
15102
  function getInstalledVersion() {
14828
15103
  try {
14829
- const globalRoot = execSync17("npm root -g", {
15104
+ const globalRoot = execSync18("npm root -g", {
14830
15105
  encoding: "utf-8",
14831
15106
  stdio: ["pipe", "pipe", "pipe"]
14832
15107
  }).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "1.44.2",
3
+ "version": "1.45.0",
4
4
  "description": "AI context infrastructure for coding agents — keeps CLAUDE.md, Cursor rules, and skills in sync as your codebase evolves",
5
5
  "type": "module",
6
6
  "bin": {