@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 +303 -26
- package/dist/bundle.cjs.map +4 -4
- package/dist/commands/agent.d.ts +29 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +262 -11
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +32 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/utils/api.d.ts +6 -2
- package/dist/utils/api.d.ts.map +1 -1
- package/dist/utils/api.js +27 -8
- package/dist/utils/api.js.map +1 -1
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js.map +1 -1
- package/package.json +2 -7
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
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
3692
|
-
await apiCall(creds, "POST", `/api/v1/executors/${executorId}/tasks/${taskId}/heartbeat`,
|
|
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
|
-
|
|
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({
|
|
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.
|
|
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());
|