@controlvector/cv-agent 1.2.0 → 1.4.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/bundle.cjs CHANGED
@@ -960,7 +960,7 @@ var require_command = __commonJS({
960
960
  var EventEmitter = require("node:events").EventEmitter;
961
961
  var childProcess = require("node:child_process");
962
962
  var path = require("node:path");
963
- var fs2 = require("node:fs");
963
+ var fs3 = require("node:fs");
964
964
  var process3 = require("node:process");
965
965
  var { Argument: Argument2, humanReadableArgName } = require_argument();
966
966
  var { CommanderError: CommanderError2 } = require_error();
@@ -1893,10 +1893,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1893
1893
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1894
1894
  function findFile(baseDir, baseName) {
1895
1895
  const localBin = path.resolve(baseDir, baseName);
1896
- if (fs2.existsSync(localBin)) return localBin;
1896
+ if (fs3.existsSync(localBin)) return localBin;
1897
1897
  if (sourceExt.includes(path.extname(baseName))) return void 0;
1898
1898
  const foundExt = sourceExt.find(
1899
- (ext) => fs2.existsSync(`${localBin}${ext}`)
1899
+ (ext) => fs3.existsSync(`${localBin}${ext}`)
1900
1900
  );
1901
1901
  if (foundExt) return `${localBin}${foundExt}`;
1902
1902
  return void 0;
@@ -1908,7 +1908,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1908
1908
  if (this._scriptPath) {
1909
1909
  let resolvedScriptPath;
1910
1910
  try {
1911
- resolvedScriptPath = fs2.realpathSync(this._scriptPath);
1911
+ resolvedScriptPath = fs3.realpathSync(this._scriptPath);
1912
1912
  } catch (err) {
1913
1913
  resolvedScriptPath = this._scriptPath;
1914
1914
  }
@@ -3643,9 +3643,8 @@ async function apiCall(creds, method, path, body) {
3643
3643
  body: body ? JSON.stringify(body) : void 0
3644
3644
  });
3645
3645
  }
3646
- async function registerExecutor(creds, machineName, workingDir) {
3647
- const hostname2 = (await import("node:os")).hostname();
3648
- const res = await apiCall(creds, "POST", "/api/v1/executors", {
3646
+ async function registerExecutor(creds, machineName, workingDir, repositoryId) {
3647
+ const body = {
3649
3648
  name: `cva:${machineName}`,
3650
3649
  machine_name: machineName,
3651
3650
  type: "claude_code",
@@ -3654,7 +3653,11 @@ async function registerExecutor(creds, machineName, workingDir) {
3654
3653
  tools: ["bash", "read", "write", "edit", "glob", "grep"],
3655
3654
  maxConcurrentTasks: 1
3656
3655
  }
3657
- });
3656
+ };
3657
+ if (repositoryId) {
3658
+ body.repository_id = repositoryId;
3659
+ }
3660
+ const res = await apiCall(creds, "POST", "/api/v1/executors", body);
3658
3661
  if (!res.ok) {
3659
3662
  const err = await res.text();
3660
3663
  throw new Error(`Failed to register executor: ${res.status} ${err}`);
@@ -3662,6 +3665,16 @@ async function registerExecutor(creds, machineName, workingDir) {
3662
3665
  const data = await res.json();
3663
3666
  return { id: data.executor.id, name: data.executor.name };
3664
3667
  }
3668
+ async function resolveRepoId(creds, owner, repo) {
3669
+ try {
3670
+ const res = await apiCall(creds, "GET", `/api/v1/repos/${owner}/${repo}`);
3671
+ if (!res.ok) return null;
3672
+ const data = await res.json();
3673
+ return data.repository ? { id: data.repository.id, slug: data.repository.slug } : null;
3674
+ } catch {
3675
+ return null;
3676
+ }
3677
+ }
3665
3678
  async function markOffline(creds, executorId) {
3666
3679
  await apiCall(creds, "POST", `/api/v1/executors/${executorId}/offline`).catch(() => {
3667
3680
  });
@@ -3684,12 +3697,13 @@ async function failTask(creds, executorId, taskId, error) {
3684
3697
  const res = await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/fail`, { error });
3685
3698
  if (!res.ok) throw new Error(`Fail failed: ${res.status}`);
3686
3699
  }
3687
- async function sendHeartbeat(creds, executorId, taskId, message) {
3688
- await apiCall(creds, "POST", `/api/v1/executors/${executorId}/heartbeat`).catch(() => {
3700
+ async function sendHeartbeat(creds, executorId, taskId, message, authStatus2) {
3701
+ const body = authStatus2 ? { auth_status: authStatus2 } : void 0;
3702
+ await apiCall(creds, "POST", `/api/v1/executors/${executorId}/heartbeat`, body).catch(() => {
3689
3703
  });
3690
3704
  if (taskId) {
3691
- const body = message ? { message, log_type: "heartbeat" } : void 0;
3692
- await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/heartbeat`, body).catch(() => {
3705
+ const taskBody = message ? { message, log_type: "heartbeat" } : void 0;
3706
+ await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/heartbeat`, taskBody).catch(() => {
3693
3707
  });
3694
3708
  }
3695
3709
  }
@@ -4054,7 +4068,95 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
4054
4068
  throw new Error("unreachable");
4055
4069
  }
4056
4070
 
4071
+ // src/utils/config.ts
4072
+ var import_fs2 = require("fs");
4073
+ var import_os2 = require("os");
4074
+ var import_path2 = require("path");
4075
+ function getConfigPath() {
4076
+ return (0, import_path2.join)((0, import_os2.homedir)(), ".config", "cva", "config.json");
4077
+ }
4078
+ async function readConfig() {
4079
+ try {
4080
+ const content = await import_fs2.promises.readFile(getConfigPath(), "utf-8");
4081
+ return JSON.parse(content);
4082
+ } catch {
4083
+ return {};
4084
+ }
4085
+ }
4086
+ async function writeConfig(config) {
4087
+ const configPath = getConfigPath();
4088
+ await import_fs2.promises.mkdir((0, import_path2.dirname)(configPath), { recursive: true });
4089
+ await import_fs2.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
4090
+ }
4091
+
4057
4092
  // src/commands/agent.ts
4093
+ var AUTH_ERROR_PATTERNS = [
4094
+ "Not logged in",
4095
+ "Please run /login",
4096
+ "authentication required",
4097
+ "unauthorized",
4098
+ "expired token",
4099
+ "not authenticated",
4100
+ "login required"
4101
+ ];
4102
+ function containsAuthError(text) {
4103
+ const lower = text.toLowerCase();
4104
+ for (const pattern of AUTH_ERROR_PATTERNS) {
4105
+ if (lower.includes(pattern.toLowerCase())) {
4106
+ return pattern;
4107
+ }
4108
+ }
4109
+ return null;
4110
+ }
4111
+ async function checkClaudeAuth() {
4112
+ try {
4113
+ const output = (0, import_node_child_process2.execSync)("claude --version 2>&1", {
4114
+ encoding: "utf8",
4115
+ timeout: 1e4,
4116
+ env: { ...process.env }
4117
+ });
4118
+ const authError = containsAuthError(output);
4119
+ if (authError) {
4120
+ return { status: "expired", error: authError };
4121
+ }
4122
+ return { status: "authenticated" };
4123
+ } catch (err) {
4124
+ const output = (err.stdout || "") + (err.stderr || "") + (err.message || "");
4125
+ const authError = containsAuthError(output);
4126
+ if (authError) {
4127
+ return { status: "expired", error: authError };
4128
+ }
4129
+ return { status: "not_configured", error: output.slice(0, 500) };
4130
+ }
4131
+ }
4132
+ async function getClaudeEnv() {
4133
+ const env2 = { ...process.env };
4134
+ let usingApiKey = false;
4135
+ if (!env2.ANTHROPIC_API_KEY) {
4136
+ try {
4137
+ const config = await readConfig();
4138
+ if (config.anthropic_api_key) {
4139
+ env2.ANTHROPIC_API_KEY = config.anthropic_api_key;
4140
+ usingApiKey = true;
4141
+ }
4142
+ } catch {
4143
+ }
4144
+ }
4145
+ return { env: env2, usingApiKey };
4146
+ }
4147
+ function buildAuthFailureMessage(errorString, machineName, hasApiKeyFallback) {
4148
+ let msg = `CLAUDE_AUTH_REQUIRED: ${errorString}
4149
+ `;
4150
+ msg += `Machine: ${machineName}
4151
+ `;
4152
+ msg += `Fix: SSH into ${machineName} and run: claude /login
4153
+ `;
4154
+ if (!hasApiKeyFallback) {
4155
+ msg += `Alternative: Set an API key fallback with: cva auth set-api-key sk-ant-...
4156
+ `;
4157
+ }
4158
+ return msg;
4159
+ }
4058
4160
  function buildClaudePrompt(task) {
4059
4161
  let prompt = "";
4060
4162
  prompt += `You are executing a task dispatched via CV-Hub.
@@ -4222,11 +4324,13 @@ async function launchAutoApproveMode(prompt, options) {
4222
4324
  const child = (0, import_node_child_process2.spawn)("claude", args, {
4223
4325
  cwd: options.cwd,
4224
4326
  stdio: ["inherit", "pipe", "pipe"],
4225
- env: { ...process.env }
4327
+ env: options.spawnEnv || { ...process.env }
4226
4328
  });
4227
4329
  _activeChild = child;
4228
4330
  let stderr = "";
4229
4331
  let lineBuffer = "";
4332
+ let authFailure = false;
4333
+ const spawnTime = Date.now();
4230
4334
  child.stdout?.on("data", (data) => {
4231
4335
  const text = data.toString();
4232
4336
  process.stdout.write(data);
@@ -4234,6 +4338,23 @@ async function launchAutoApproveMode(prompt, options) {
4234
4338
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4235
4339
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4236
4340
  }
4341
+ if (Date.now() - spawnTime < 1e4) {
4342
+ const authError = containsAuthError(fullOutput + stderr);
4343
+ if (authError) {
4344
+ authFailure = true;
4345
+ console.log(`
4346
+ ${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
4347
+ console.log(source_default.yellow(` Killing process \u2014 it won't recover without re-authentication.`));
4348
+ if (options.machineName) {
4349
+ console.log(source_default.cyan(` Fix: SSH into ${options.machineName} and run: claude /login`));
4350
+ }
4351
+ try {
4352
+ child.kill("SIGTERM");
4353
+ } catch {
4354
+ }
4355
+ return;
4356
+ }
4357
+ }
4237
4358
  if (options.creds && options.taskId) {
4238
4359
  lineBuffer += text;
4239
4360
  const lines = lineBuffer.split("\n");
@@ -4276,12 +4397,30 @@ async function launchAutoApproveMode(prompt, options) {
4276
4397
  }
4277
4398
  });
4278
4399
  child.stderr?.on("data", (data) => {
4279
- stderr += data.toString();
4400
+ const text = data.toString();
4401
+ stderr += text;
4280
4402
  process.stderr.write(data);
4403
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4404
+ const authError = containsAuthError(text);
4405
+ if (authError) {
4406
+ authFailure = true;
4407
+ console.log(`
4408
+ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
4409
+ try {
4410
+ child.kill("SIGTERM");
4411
+ } catch {
4412
+ }
4413
+ }
4414
+ }
4281
4415
  });
4282
4416
  child.on("close", (code, signal) => {
4283
4417
  _activeChild = null;
4284
- resolve({ exitCode: signal === "SIGKILL" ? 137 : code ?? 1, stderr, output: fullOutput });
4418
+ resolve({
4419
+ exitCode: signal === "SIGKILL" ? 137 : code ?? 1,
4420
+ stderr,
4421
+ output: fullOutput,
4422
+ authFailure
4423
+ });
4285
4424
  });
4286
4425
  child.on("error", (err) => {
4287
4426
  _activeChild = null;
@@ -4319,13 +4458,15 @@ async function launchRelayMode(prompt, options) {
4319
4458
  const child = (0, import_node_child_process2.spawn)("claude", [], {
4320
4459
  cwd: options.cwd,
4321
4460
  stdio: ["pipe", "pipe", "pipe"],
4322
- env: { ...process.env }
4461
+ env: options.spawnEnv || { ...process.env }
4323
4462
  });
4324
4463
  _activeChild = child;
4325
4464
  let stderr = "";
4326
4465
  let stdoutBuffer = "";
4327
4466
  let fullOutput = "";
4328
4467
  let lastProgressBytes = 0;
4468
+ let authFailure = false;
4469
+ const spawnTime = Date.now();
4329
4470
  child.stdin?.write(prompt + "\n");
4330
4471
  let lastRedirectCheck = Date.now();
4331
4472
  let lineBuffer = "";
@@ -4337,6 +4478,19 @@ async function launchRelayMode(prompt, options) {
4337
4478
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4338
4479
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4339
4480
  }
4481
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4482
+ const authError = containsAuthError(fullOutput + stderr);
4483
+ if (authError) {
4484
+ authFailure = true;
4485
+ console.log(`
4486
+ ${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
4487
+ try {
4488
+ child.kill("SIGTERM");
4489
+ } catch {
4490
+ }
4491
+ return;
4492
+ }
4493
+ }
4340
4494
  lineBuffer += text;
4341
4495
  const lines = lineBuffer.split("\n");
4342
4496
  lineBuffer = lines.pop() ?? "";
@@ -4477,13 +4631,25 @@ async function launchRelayMode(prompt, options) {
4477
4631
  const text = data.toString();
4478
4632
  stderr += text;
4479
4633
  process.stderr.write(data);
4634
+ if (Date.now() - spawnTime < 1e4 && !authFailure) {
4635
+ const authError = containsAuthError(text);
4636
+ if (authError) {
4637
+ authFailure = true;
4638
+ console.log(`
4639
+ ${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
4640
+ try {
4641
+ child.kill("SIGTERM");
4642
+ } catch {
4643
+ }
4644
+ }
4645
+ }
4480
4646
  });
4481
4647
  child.on("close", (code, signal) => {
4482
4648
  _activeChild = null;
4483
4649
  if (signal === "SIGKILL") {
4484
- resolve({ exitCode: 137, stderr, output: fullOutput });
4650
+ resolve({ exitCode: 137, stderr, output: fullOutput, authFailure });
4485
4651
  } else {
4486
- resolve({ exitCode: code ?? 1, stderr, output: fullOutput });
4652
+ resolve({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
4487
4653
  }
4488
4654
  });
4489
4655
  child.on("error", (err) => {
@@ -4593,6 +4759,26 @@ async function runAgent(options) {
4593
4759
  console.log();
4594
4760
  process.exit(1);
4595
4761
  }
4762
+ const { env: claudeEnv, usingApiKey } = await getClaudeEnv();
4763
+ const authCheck = await checkClaudeAuth();
4764
+ let currentAuthStatus = authCheck.status;
4765
+ if (authCheck.status === "expired") {
4766
+ if (usingApiKey) {
4767
+ console.log(source_default.yellow("!") + " Claude Code OAuth expired, but API key fallback is configured.");
4768
+ currentAuthStatus = "api_key_fallback";
4769
+ } else {
4770
+ console.log();
4771
+ console.log(source_default.red("Claude Code auth expired: ") + source_default.yellow(authCheck.error || "unknown"));
4772
+ console.log(` Fix: Run ${source_default.cyan("claude /login")} to re-authenticate.`);
4773
+ console.log(` Alternative: ${source_default.cyan("cva auth set-api-key sk-ant-...")} for API key fallback.`);
4774
+ console.log();
4775
+ console.log(source_default.gray("Agent will start but pause task claims until auth is resolved."));
4776
+ console.log();
4777
+ }
4778
+ } else if (usingApiKey) {
4779
+ console.log(source_default.gray(" Using Anthropic API key from config as fallback."));
4780
+ currentAuthStatus = "api_key_fallback";
4781
+ }
4596
4782
  try {
4597
4783
  (0, import_node_child_process2.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
4598
4784
  } catch {
@@ -4612,8 +4798,31 @@ async function runAgent(options) {
4612
4798
  console.log();
4613
4799
  }
4614
4800
  }
4801
+ let detectedRepoId;
4802
+ try {
4803
+ const remoteUrl = (0, import_node_child_process2.execSync)("git remote get-url origin 2>/dev/null", {
4804
+ cwd: workingDir,
4805
+ encoding: "utf8",
4806
+ timeout: 5e3
4807
+ }).trim();
4808
+ const cvHubMatch = remoteUrl.match(
4809
+ /git\.hub\.controlvector\.io[:/]([^/]+)\/([^/.]+)/
4810
+ );
4811
+ if (cvHubMatch) {
4812
+ const [, repoOwner, repoSlug] = cvHubMatch;
4813
+ try {
4814
+ const repoData = await resolveRepoId(creds, repoOwner, repoSlug);
4815
+ if (repoData?.id) {
4816
+ detectedRepoId = repoData.id;
4817
+ console.log(source_default.gray(` Repo: ${repoOwner}/${repoSlug}`));
4818
+ }
4819
+ } catch {
4820
+ }
4821
+ }
4822
+ } catch {
4823
+ }
4615
4824
  const executor = await withRetry(
4616
- () => registerExecutor(creds, machineName, workingDir),
4825
+ () => registerExecutor(creds, machineName, workingDir, detectedRepoId),
4617
4826
  "Executor registration"
4618
4827
  );
4619
4828
  const mode = options.autoApprove ? "auto-approve" : "relay";
@@ -4636,7 +4845,9 @@ async function runAgent(options) {
4636
4845
  failedCount: 0,
4637
4846
  lastPoll: Date.now(),
4638
4847
  lastTaskEnd: Date.now(),
4639
- running: true
4848
+ running: true,
4849
+ authStatus: currentAuthStatus,
4850
+ machineName
4640
4851
  };
4641
4852
  installSignalHandlers(
4642
4853
  () => state,
@@ -4647,13 +4858,26 @@ async function runAgent(options) {
4647
4858
  );
4648
4859
  while (state.running) {
4649
4860
  try {
4861
+ if (state.authStatus === "expired") {
4862
+ const recheck = await checkClaudeAuth();
4863
+ if (recheck.status === "authenticated") {
4864
+ state.authStatus = "authenticated";
4865
+ console.log(`
4866
+ ${source_default.green("\u2713")} Claude Code auth restored. Resuming task claims.`);
4867
+ } else {
4868
+ sendHeartbeat(creds, state.executorId, void 0, void 0, state.authStatus).catch(() => {
4869
+ });
4870
+ await new Promise((r) => setTimeout(r, pollInterval));
4871
+ continue;
4872
+ }
4873
+ }
4650
4874
  const task = await withRetry(
4651
4875
  () => pollForTask(creds, state.executorId),
4652
4876
  "Task poll"
4653
4877
  );
4654
4878
  state.lastPoll = Date.now();
4655
4879
  if (task) {
4656
- await executeTask(task, state, creds, options);
4880
+ await executeTask(task, state, creds, options, claudeEnv);
4657
4881
  } else {
4658
4882
  updateStatusLine(
4659
4883
  formatDuration(Date.now() - state.lastTaskEnd),
@@ -4669,7 +4893,7 @@ ${source_default.red("!")} Error: ${err.message}`);
4669
4893
  await new Promise((r) => setTimeout(r, pollInterval));
4670
4894
  }
4671
4895
  }
4672
- async function executeTask(task, state, creds, options) {
4896
+ async function executeTask(task, state, creds, options, claudeEnv) {
4673
4897
  const startTime = Date.now();
4674
4898
  state.currentTaskId = task.id;
4675
4899
  if (task.task_type === "_system_update") {
@@ -4697,7 +4921,7 @@ async function executeTask(task, state, creds, options) {
4697
4921
  try {
4698
4922
  const elapsed = formatDuration(Date.now() - startTime);
4699
4923
  setTerminalTitle(`cva: ${task.title} (${elapsed})`);
4700
- await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)`);
4924
+ await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)`, state.authStatus);
4701
4925
  } catch {
4702
4926
  }
4703
4927
  }, 3e4);
@@ -4731,14 +4955,18 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4731
4955
  cwd: options.workingDir,
4732
4956
  creds,
4733
4957
  taskId: task.id,
4734
- executorId: state.executorId
4958
+ executorId: state.executorId,
4959
+ spawnEnv: claudeEnv,
4960
+ machineName: state.machineName
4735
4961
  });
4736
4962
  } else {
4737
4963
  result = await launchRelayMode(prompt, {
4738
4964
  cwd: options.workingDir,
4739
4965
  creds,
4740
4966
  executorId: state.executorId,
4741
- taskId: task.id
4967
+ taskId: task.id,
4968
+ spawnEnv: claudeEnv,
4969
+ machineName: state.machineName
4742
4970
  });
4743
4971
  }
4744
4972
  console.log(source_default.gray("\n" + "-".repeat(60)));
@@ -4813,6 +5041,31 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4813
5041
  } catch {
4814
5042
  }
4815
5043
  state.failedCount++;
5044
+ } else if (result.authFailure) {
5045
+ const authErrorStr = containsAuthError(result.output + result.stderr) || "auth failure";
5046
+ const hasApiKey = !!claudeEnv?.ANTHROPIC_API_KEY;
5047
+ const authMsg = buildAuthFailureMessage(authErrorStr, state.machineName, hasApiKey);
5048
+ sendTaskLog(creds, state.executorId, task.id, "error", authMsg);
5049
+ console.log();
5050
+ console.log(`\u{1F511} ${source_default.bold.red("AUTH FAILED")} \u2014 Claude Code is not authenticated`);
5051
+ console.log(source_default.yellow(` ${authMsg.replace(/\n/g, "\n ")}`));
5052
+ postTaskEvent(creds, task.id, {
5053
+ event_type: "auth_failure",
5054
+ content: {
5055
+ error: authErrorStr,
5056
+ machine: state.machineName,
5057
+ fix_command: `claude /login`,
5058
+ api_key_configured: hasApiKey
5059
+ }
5060
+ }).catch(() => {
5061
+ });
5062
+ await withRetry(
5063
+ () => failTask(creds, state.executorId, task.id, authMsg),
5064
+ "Report auth failure"
5065
+ );
5066
+ state.failedCount++;
5067
+ state.authStatus = "expired";
5068
+ console.log(source_default.yellow(" Pausing task claims until auth is restored."));
4816
5069
  } else {
4817
5070
  const stderrTail = result.stderr.trim().slice(-500);
4818
5071
  sendTaskLog(
@@ -4942,11 +5195,35 @@ async function authStatus() {
4942
5195
  console.log(`Status: ${source_default.red("unreachable")} (${err.message})`);
4943
5196
  }
4944
5197
  }
5198
+ async function authSetApiKey(key) {
5199
+ if (!key.startsWith("sk-ant-")) {
5200
+ console.log(source_default.red("Invalid API key.") + " Anthropic API keys start with sk-ant-");
5201
+ process.exit(1);
5202
+ }
5203
+ const config = await readConfig();
5204
+ config.anthropic_api_key = key;
5205
+ await writeConfig(config);
5206
+ const masked = key.substring(0, 10) + "..." + key.slice(-4);
5207
+ console.log(source_default.green("API key saved") + ` (${masked})`);
5208
+ console.log(source_default.gray("This key will be used as fallback when Claude Code OAuth expires."));
5209
+ }
5210
+ async function authRemoveApiKey() {
5211
+ const config = await readConfig();
5212
+ if (!config.anthropic_api_key) {
5213
+ console.log(source_default.yellow("No API key configured."));
5214
+ return;
5215
+ }
5216
+ delete config.anthropic_api_key;
5217
+ await writeConfig(config);
5218
+ console.log(source_default.green("API key removed."));
5219
+ }
4945
5220
  function authCommand() {
4946
5221
  const cmd = new Command("auth");
4947
5222
  cmd.description("Manage CV-Hub authentication");
4948
5223
  cmd.command("login").description("Authenticate with CV-Hub using a PAT token").option("--token <token>", "PAT token (or enter interactively)").option("--api-url <url>", "CV-Hub API URL", "https://api.hub.controlvector.io").action(authLogin);
4949
5224
  cmd.command("status").description("Show current authentication status").action(authStatus);
5225
+ cmd.command("set-api-key").description("Set Anthropic API key as fallback for Claude Code OAuth").argument("<key>", "Anthropic API key (sk-ant-...)").action(authSetApiKey);
5226
+ cmd.command("remove-api-key").description("Remove stored Anthropic API key").action(authRemoveApiKey);
4950
5227
  return cmd;
4951
5228
  }
4952
5229
 
@@ -5167,7 +5444,7 @@ function statusCommand() {
5167
5444
 
5168
5445
  // src/index.ts
5169
5446
  var program2 = new Command();
5170
- program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.2.0" : "1.1.0");
5447
+ program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.4.0" : "1.1.0");
5171
5448
  program2.addCommand(agentCommand());
5172
5449
  program2.addCommand(authCommand());
5173
5450
  program2.addCommand(remoteCommand());