@controlvector/cv-agent 1.3.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
  }
@@ -3697,12 +3697,13 @@ async function failTask(creds, executorId, taskId, error) {
3697
3697
  const res = await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/fail`, { error });
3698
3698
  if (!res.ok) throw new Error(`Fail failed: ${res.status}`);
3699
3699
  }
3700
- async function sendHeartbeat(creds, executorId, taskId, message) {
3701
- 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(() => {
3702
3703
  });
3703
3704
  if (taskId) {
3704
- const body = message ? { message, log_type: "heartbeat" } : void 0;
3705
- 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(() => {
3706
3707
  });
3707
3708
  }
3708
3709
  }
@@ -4067,7 +4068,95 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
4067
4068
  throw new Error("unreachable");
4068
4069
  }
4069
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
+
4070
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
+ }
4071
4160
  function buildClaudePrompt(task) {
4072
4161
  let prompt = "";
4073
4162
  prompt += `You are executing a task dispatched via CV-Hub.
@@ -4235,11 +4324,13 @@ async function launchAutoApproveMode(prompt, options) {
4235
4324
  const child = (0, import_node_child_process2.spawn)("claude", args, {
4236
4325
  cwd: options.cwd,
4237
4326
  stdio: ["inherit", "pipe", "pipe"],
4238
- env: { ...process.env }
4327
+ env: options.spawnEnv || { ...process.env }
4239
4328
  });
4240
4329
  _activeChild = child;
4241
4330
  let stderr = "";
4242
4331
  let lineBuffer = "";
4332
+ let authFailure = false;
4333
+ const spawnTime = Date.now();
4243
4334
  child.stdout?.on("data", (data) => {
4244
4335
  const text = data.toString();
4245
4336
  process.stdout.write(data);
@@ -4247,6 +4338,23 @@ async function launchAutoApproveMode(prompt, options) {
4247
4338
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4248
4339
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4249
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
+ }
4250
4358
  if (options.creds && options.taskId) {
4251
4359
  lineBuffer += text;
4252
4360
  const lines = lineBuffer.split("\n");
@@ -4289,12 +4397,30 @@ async function launchAutoApproveMode(prompt, options) {
4289
4397
  }
4290
4398
  });
4291
4399
  child.stderr?.on("data", (data) => {
4292
- stderr += data.toString();
4400
+ const text = data.toString();
4401
+ stderr += text;
4293
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
+ }
4294
4415
  });
4295
4416
  child.on("close", (code, signal) => {
4296
4417
  _activeChild = null;
4297
- 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
+ });
4298
4424
  });
4299
4425
  child.on("error", (err) => {
4300
4426
  _activeChild = null;
@@ -4332,13 +4458,15 @@ async function launchRelayMode(prompt, options) {
4332
4458
  const child = (0, import_node_child_process2.spawn)("claude", [], {
4333
4459
  cwd: options.cwd,
4334
4460
  stdio: ["pipe", "pipe", "pipe"],
4335
- env: { ...process.env }
4461
+ env: options.spawnEnv || { ...process.env }
4336
4462
  });
4337
4463
  _activeChild = child;
4338
4464
  let stderr = "";
4339
4465
  let stdoutBuffer = "";
4340
4466
  let fullOutput = "";
4341
4467
  let lastProgressBytes = 0;
4468
+ let authFailure = false;
4469
+ const spawnTime = Date.now();
4342
4470
  child.stdin?.write(prompt + "\n");
4343
4471
  let lastRedirectCheck = Date.now();
4344
4472
  let lineBuffer = "";
@@ -4350,6 +4478,19 @@ async function launchRelayMode(prompt, options) {
4350
4478
  if (fullOutput.length > MAX_OUTPUT_BYTES) {
4351
4479
  fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
4352
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
+ }
4353
4494
  lineBuffer += text;
4354
4495
  const lines = lineBuffer.split("\n");
4355
4496
  lineBuffer = lines.pop() ?? "";
@@ -4490,13 +4631,25 @@ async function launchRelayMode(prompt, options) {
4490
4631
  const text = data.toString();
4491
4632
  stderr += text;
4492
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
+ }
4493
4646
  });
4494
4647
  child.on("close", (code, signal) => {
4495
4648
  _activeChild = null;
4496
4649
  if (signal === "SIGKILL") {
4497
- resolve({ exitCode: 137, stderr, output: fullOutput });
4650
+ resolve({ exitCode: 137, stderr, output: fullOutput, authFailure });
4498
4651
  } else {
4499
- resolve({ exitCode: code ?? 1, stderr, output: fullOutput });
4652
+ resolve({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
4500
4653
  }
4501
4654
  });
4502
4655
  child.on("error", (err) => {
@@ -4606,6 +4759,26 @@ async function runAgent(options) {
4606
4759
  console.log();
4607
4760
  process.exit(1);
4608
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
+ }
4609
4782
  try {
4610
4783
  (0, import_node_child_process2.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
4611
4784
  } catch {
@@ -4672,7 +4845,9 @@ async function runAgent(options) {
4672
4845
  failedCount: 0,
4673
4846
  lastPoll: Date.now(),
4674
4847
  lastTaskEnd: Date.now(),
4675
- running: true
4848
+ running: true,
4849
+ authStatus: currentAuthStatus,
4850
+ machineName
4676
4851
  };
4677
4852
  installSignalHandlers(
4678
4853
  () => state,
@@ -4683,13 +4858,26 @@ async function runAgent(options) {
4683
4858
  );
4684
4859
  while (state.running) {
4685
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
+ }
4686
4874
  const task = await withRetry(
4687
4875
  () => pollForTask(creds, state.executorId),
4688
4876
  "Task poll"
4689
4877
  );
4690
4878
  state.lastPoll = Date.now();
4691
4879
  if (task) {
4692
- await executeTask(task, state, creds, options);
4880
+ await executeTask(task, state, creds, options, claudeEnv);
4693
4881
  } else {
4694
4882
  updateStatusLine(
4695
4883
  formatDuration(Date.now() - state.lastTaskEnd),
@@ -4705,7 +4893,7 @@ ${source_default.red("!")} Error: ${err.message}`);
4705
4893
  await new Promise((r) => setTimeout(r, pollInterval));
4706
4894
  }
4707
4895
  }
4708
- async function executeTask(task, state, creds, options) {
4896
+ async function executeTask(task, state, creds, options, claudeEnv) {
4709
4897
  const startTime = Date.now();
4710
4898
  state.currentTaskId = task.id;
4711
4899
  if (task.task_type === "_system_update") {
@@ -4733,7 +4921,7 @@ async function executeTask(task, state, creds, options) {
4733
4921
  try {
4734
4922
  const elapsed = formatDuration(Date.now() - startTime);
4735
4923
  setTerminalTitle(`cva: ${task.title} (${elapsed})`);
4736
- 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);
4737
4925
  } catch {
4738
4926
  }
4739
4927
  }, 3e4);
@@ -4767,14 +4955,18 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4767
4955
  cwd: options.workingDir,
4768
4956
  creds,
4769
4957
  taskId: task.id,
4770
- executorId: state.executorId
4958
+ executorId: state.executorId,
4959
+ spawnEnv: claudeEnv,
4960
+ machineName: state.machineName
4771
4961
  });
4772
4962
  } else {
4773
4963
  result = await launchRelayMode(prompt, {
4774
4964
  cwd: options.workingDir,
4775
4965
  creds,
4776
4966
  executorId: state.executorId,
4777
- taskId: task.id
4967
+ taskId: task.id,
4968
+ spawnEnv: claudeEnv,
4969
+ machineName: state.machineName
4778
4970
  });
4779
4971
  }
4780
4972
  console.log(source_default.gray("\n" + "-".repeat(60)));
@@ -4849,6 +5041,31 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
4849
5041
  } catch {
4850
5042
  }
4851
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."));
4852
5069
  } else {
4853
5070
  const stderrTail = result.stderr.trim().slice(-500);
4854
5071
  sendTaskLog(
@@ -4978,11 +5195,35 @@ async function authStatus() {
4978
5195
  console.log(`Status: ${source_default.red("unreachable")} (${err.message})`);
4979
5196
  }
4980
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
+ }
4981
5220
  function authCommand() {
4982
5221
  const cmd = new Command("auth");
4983
5222
  cmd.description("Manage CV-Hub authentication");
4984
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);
4985
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);
4986
5227
  return cmd;
4987
5228
  }
4988
5229
 
@@ -5203,7 +5444,7 @@ function statusCommand() {
5203
5444
 
5204
5445
  // src/index.ts
5205
5446
  var program2 = new Command();
5206
- program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.3.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");
5207
5448
  program2.addCommand(agentCommand());
5208
5449
  program2.addCommand(authCommand());
5209
5450
  program2.addCommand(remoteCommand());