@controlvector/cv-agent 1.3.0 → 1.5.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 +597 -36
- 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 +282 -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/commands/deploy-manifest.d.ts +67 -0
- package/dist/commands/deploy-manifest.d.ts.map +1 -0
- package/dist/commands/deploy-manifest.js +211 -0
- package/dist/commands/deploy-manifest.js.map +1 -0
- package/dist/commands/git-safety.d.ts +29 -0
- package/dist/commands/git-safety.d.ts.map +1 -0
- package/dist/commands/git-safety.js +116 -0
- package/dist/commands/git-safety.js.map +1 -0
- package/dist/utils/api.d.ts +1 -1
- package/dist/utils/api.d.ts.map +1 -1
- package/dist/utils/api.js +5 -4
- 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 +1 -1
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
|
}
|
|
@@ -3037,7 +3037,7 @@ var {
|
|
|
3037
3037
|
} = import_index.default;
|
|
3038
3038
|
|
|
3039
3039
|
// src/commands/agent.ts
|
|
3040
|
-
var
|
|
3040
|
+
var import_node_child_process4 = require("node:child_process");
|
|
3041
3041
|
|
|
3042
3042
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
3043
3043
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -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
|
-
|
|
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
|
|
3705
|
-
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(() => {
|
|
3706
3707
|
});
|
|
3707
3708
|
}
|
|
3708
3709
|
}
|
|
@@ -4067,7 +4068,366 @@ ${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
|
+
|
|
4092
|
+
// src/commands/git-safety.ts
|
|
4093
|
+
var import_node_child_process2 = require("node:child_process");
|
|
4094
|
+
function git(cmd, cwd) {
|
|
4095
|
+
return (0, import_node_child_process2.execSync)(cmd, { cwd, encoding: "utf8", timeout: 3e4 }).trim();
|
|
4096
|
+
}
|
|
4097
|
+
function gitSafetyNet(workspaceRoot, taskTitle, taskId, branch) {
|
|
4098
|
+
const targetBranch = branch || "main";
|
|
4099
|
+
try {
|
|
4100
|
+
let statusOutput;
|
|
4101
|
+
try {
|
|
4102
|
+
statusOutput = git("git status --porcelain", workspaceRoot);
|
|
4103
|
+
} catch {
|
|
4104
|
+
return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false, error: "git status failed" };
|
|
4105
|
+
}
|
|
4106
|
+
const lines = statusOutput.split("\n").filter(Boolean);
|
|
4107
|
+
if (lines.length === 0) {
|
|
4108
|
+
try {
|
|
4109
|
+
const unpushed = git(`git log origin/${targetBranch}..HEAD --oneline 2>/dev/null`, workspaceRoot);
|
|
4110
|
+
if (unpushed) {
|
|
4111
|
+
console.log(` [git-safety] Found unpushed commits \u2014 pushing now`);
|
|
4112
|
+
git(`git push origin ${targetBranch}`, workspaceRoot);
|
|
4113
|
+
return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: true };
|
|
4114
|
+
}
|
|
4115
|
+
} catch {
|
|
4116
|
+
}
|
|
4117
|
+
return { hadChanges: false, filesAdded: 0, filesModified: 0, filesDeleted: 0, pushed: false };
|
|
4118
|
+
}
|
|
4119
|
+
let added = 0, modified = 0, deleted = 0;
|
|
4120
|
+
for (const line of lines) {
|
|
4121
|
+
const code = line.substring(0, 2);
|
|
4122
|
+
if (code.includes("?")) added++;
|
|
4123
|
+
else if (code.includes("D")) deleted++;
|
|
4124
|
+
else if (code.includes("M") || code.includes("A")) modified++;
|
|
4125
|
+
else added++;
|
|
4126
|
+
}
|
|
4127
|
+
console.log(
|
|
4128
|
+
` [git-safety] ${lines.length} uncommitted changes (${added} new, ${modified} modified, ${deleted} deleted) \u2014 committing now`
|
|
4129
|
+
);
|
|
4130
|
+
git("git add -A", workspaceRoot);
|
|
4131
|
+
const shortId = taskId.substring(0, 8);
|
|
4132
|
+
const commitMsg = `task: ${taskTitle} [${shortId}]
|
|
4133
|
+
|
|
4134
|
+
Auto-committed by cv-agent git safety net.
|
|
4135
|
+
Task ID: ${taskId}
|
|
4136
|
+
Files: ${added} added, ${modified} modified, ${deleted} deleted`;
|
|
4137
|
+
let commitSha;
|
|
4138
|
+
try {
|
|
4139
|
+
const commitOutput = git(`git commit -m ${JSON.stringify(commitMsg)}`, workspaceRoot);
|
|
4140
|
+
const shaMatch = commitOutput.match(/\[[\w/]+ ([a-f0-9]+)\]/);
|
|
4141
|
+
commitSha = shaMatch ? shaMatch[1] : void 0;
|
|
4142
|
+
} catch (e) {
|
|
4143
|
+
if (!e.message?.includes("nothing to commit")) {
|
|
4144
|
+
return {
|
|
4145
|
+
hadChanges: true,
|
|
4146
|
+
filesAdded: added,
|
|
4147
|
+
filesModified: modified,
|
|
4148
|
+
filesDeleted: deleted,
|
|
4149
|
+
pushed: false,
|
|
4150
|
+
error: `Commit failed: ${e.message}`
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
try {
|
|
4155
|
+
git(`git push origin ${targetBranch}`, workspaceRoot);
|
|
4156
|
+
console.log(` [git-safety] Committed and pushed: ${commitSha || "ok"}`);
|
|
4157
|
+
} catch (pushErr) {
|
|
4158
|
+
console.log(` [git-safety] Push failed: ${pushErr.message}`);
|
|
4159
|
+
return {
|
|
4160
|
+
hadChanges: true,
|
|
4161
|
+
filesAdded: added,
|
|
4162
|
+
filesModified: modified,
|
|
4163
|
+
filesDeleted: deleted,
|
|
4164
|
+
commitSha,
|
|
4165
|
+
pushed: false,
|
|
4166
|
+
error: `Push failed: ${pushErr.message}`
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
return {
|
|
4170
|
+
hadChanges: true,
|
|
4171
|
+
filesAdded: added,
|
|
4172
|
+
filesModified: modified,
|
|
4173
|
+
filesDeleted: deleted,
|
|
4174
|
+
commitSha,
|
|
4175
|
+
pushed: true
|
|
4176
|
+
};
|
|
4177
|
+
} catch (err) {
|
|
4178
|
+
return {
|
|
4179
|
+
hadChanges: false,
|
|
4180
|
+
filesAdded: 0,
|
|
4181
|
+
filesModified: 0,
|
|
4182
|
+
filesDeleted: 0,
|
|
4183
|
+
pushed: false,
|
|
4184
|
+
error: err.message
|
|
4185
|
+
};
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
// src/commands/deploy-manifest.ts
|
|
4190
|
+
var import_node_child_process3 = require("node:child_process");
|
|
4191
|
+
var import_node_fs = require("node:fs");
|
|
4192
|
+
var import_node_path = require("node:path");
|
|
4193
|
+
function exec(cmd, cwd, timeoutMs = 3e5, env2) {
|
|
4194
|
+
try {
|
|
4195
|
+
const stdout = (0, import_node_child_process3.execSync)(cmd, {
|
|
4196
|
+
cwd,
|
|
4197
|
+
encoding: "utf8",
|
|
4198
|
+
timeout: timeoutMs,
|
|
4199
|
+
env: env2 ? { ...process.env, ...env2 } : void 0,
|
|
4200
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4201
|
+
});
|
|
4202
|
+
return { ok: true, stdout, stderr: "" };
|
|
4203
|
+
} catch (err) {
|
|
4204
|
+
return { ok: false, stdout: err.stdout || "", stderr: err.stderr || err.message || "" };
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
async function httpStatus(url, timeoutMs = 1e4) {
|
|
4208
|
+
try {
|
|
4209
|
+
const controller = new AbortController();
|
|
4210
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4211
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
4212
|
+
clearTimeout(timer);
|
|
4213
|
+
return res.status;
|
|
4214
|
+
} catch {
|
|
4215
|
+
return 0;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
async function checkHealth(url, timeoutSeconds) {
|
|
4219
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
4220
|
+
while (Date.now() < deadline) {
|
|
4221
|
+
const status = await httpStatus(url);
|
|
4222
|
+
if (status >= 200 && status < 500) return true;
|
|
4223
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
4224
|
+
}
|
|
4225
|
+
return false;
|
|
4226
|
+
}
|
|
4227
|
+
function getHeadCommit(cwd) {
|
|
4228
|
+
try {
|
|
4229
|
+
return (0, import_node_child_process3.execSync)("git rev-parse HEAD", { cwd, encoding: "utf8", timeout: 5e3 }).trim();
|
|
4230
|
+
} catch {
|
|
4231
|
+
return null;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
function loadDeployManifest(workspaceRoot) {
|
|
4235
|
+
const manifestPath = (0, import_node_path.join)(workspaceRoot, ".cva", "deploy.json");
|
|
4236
|
+
if (!(0, import_node_fs.existsSync)(manifestPath)) return null;
|
|
4237
|
+
try {
|
|
4238
|
+
const raw = (0, import_node_fs.readFileSync)(manifestPath, "utf8");
|
|
4239
|
+
const manifest = JSON.parse(raw);
|
|
4240
|
+
if (!manifest.version || manifest.version < 1) {
|
|
4241
|
+
console.log(" [deploy] Invalid manifest version");
|
|
4242
|
+
return null;
|
|
4243
|
+
}
|
|
4244
|
+
return manifest;
|
|
4245
|
+
} catch (err) {
|
|
4246
|
+
console.log(` [deploy] Failed to read .cva/deploy.json: ${err.message}`);
|
|
4247
|
+
return null;
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
async function postTaskDeploy(workspaceRoot, taskId, log) {
|
|
4251
|
+
const manifest = loadDeployManifest(workspaceRoot);
|
|
4252
|
+
if (!manifest) {
|
|
4253
|
+
return { deployed: false, steps: [{ step: "manifest", status: "skipped", message: "No .cva/deploy.json" }] };
|
|
4254
|
+
}
|
|
4255
|
+
const steps = [];
|
|
4256
|
+
const preDeployCommit = getHeadCommit(workspaceRoot);
|
|
4257
|
+
if (manifest.build?.steps) {
|
|
4258
|
+
for (const buildStep of manifest.build.steps) {
|
|
4259
|
+
const stepName = `build:${buildStep.name}`;
|
|
4260
|
+
console.log(` [deploy] Building: ${buildStep.name}`);
|
|
4261
|
+
log("lifecycle", `Building: ${buildStep.name}`);
|
|
4262
|
+
const start = Date.now();
|
|
4263
|
+
const cwd = (0, import_node_path.join)(workspaceRoot, buildStep.working_dir || ".");
|
|
4264
|
+
const timeoutMs = (buildStep.timeout_seconds || 300) * 1e3;
|
|
4265
|
+
const result = exec(buildStep.command, cwd, timeoutMs);
|
|
4266
|
+
if (!result.ok) {
|
|
4267
|
+
const msg = `Build failed: ${buildStep.name}
|
|
4268
|
+
${result.stderr.slice(-500)}`;
|
|
4269
|
+
steps.push({ step: stepName, status: "failed", message: msg, durationMs: Date.now() - start });
|
|
4270
|
+
log("error", msg);
|
|
4271
|
+
return { deployed: false, steps, error: msg };
|
|
4272
|
+
}
|
|
4273
|
+
steps.push({ step: stepName, status: "ok", durationMs: Date.now() - start });
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
if (manifest.migrate?.command) {
|
|
4277
|
+
console.log(" [deploy] Running migration...");
|
|
4278
|
+
log("lifecycle", "Applying database migration");
|
|
4279
|
+
const start = Date.now();
|
|
4280
|
+
const env2 = manifest.migrate.env || {};
|
|
4281
|
+
const result = exec(manifest.migrate.command, workspaceRoot, 6e4, env2);
|
|
4282
|
+
if (!result.ok) {
|
|
4283
|
+
const msg = `Migration failed:
|
|
4284
|
+
${result.stderr.slice(-500)}`;
|
|
4285
|
+
steps.push({ step: "migrate", status: "failed", message: msg, durationMs: Date.now() - start });
|
|
4286
|
+
log("error", msg);
|
|
4287
|
+
return { deployed: false, steps, error: msg };
|
|
4288
|
+
}
|
|
4289
|
+
steps.push({ step: "migrate", status: "ok", durationMs: Date.now() - start });
|
|
4290
|
+
}
|
|
4291
|
+
if (manifest.service && manifest.service.type !== "none" && manifest.service.restart_command) {
|
|
4292
|
+
console.log(` [deploy] Restarting service: ${manifest.service.name || "default"}`);
|
|
4293
|
+
log("lifecycle", `Restarting service: ${manifest.service.name || "default"}`);
|
|
4294
|
+
const start = Date.now();
|
|
4295
|
+
const result = exec(manifest.service.restart_command, workspaceRoot, 3e4);
|
|
4296
|
+
if (!result.ok) {
|
|
4297
|
+
const msg = `Restart failed:
|
|
4298
|
+
${result.stderr.slice(-300)}`;
|
|
4299
|
+
steps.push({ step: "restart", status: "failed", message: msg, durationMs: Date.now() - start });
|
|
4300
|
+
} else {
|
|
4301
|
+
steps.push({ step: "restart", status: "ok", durationMs: Date.now() - start });
|
|
4302
|
+
}
|
|
4303
|
+
const waitMs = (manifest.service.startup_wait_seconds || 5) * 1e3;
|
|
4304
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
4305
|
+
}
|
|
4306
|
+
if (manifest.verify) {
|
|
4307
|
+
console.log(" [deploy] Verifying deployment...");
|
|
4308
|
+
log("lifecycle", "Verifying deployment");
|
|
4309
|
+
const timeoutSec = manifest.verify.timeout_seconds || 30;
|
|
4310
|
+
if (manifest.verify.health_url) {
|
|
4311
|
+
const healthy = await checkHealth(manifest.verify.health_url, timeoutSec);
|
|
4312
|
+
if (!healthy) {
|
|
4313
|
+
const msg = `Health check failed: ${manifest.verify.health_url} did not respond within ${timeoutSec}s`;
|
|
4314
|
+
steps.push({ step: "verify:health", status: "failed", message: msg });
|
|
4315
|
+
log("error", msg);
|
|
4316
|
+
if (manifest.rollback?.auto_rollback_on_verify_failure && preDeployCommit) {
|
|
4317
|
+
await rollback(workspaceRoot, preDeployCommit, manifest, log);
|
|
4318
|
+
return { deployed: false, steps, error: msg, rolledBack: true };
|
|
4319
|
+
}
|
|
4320
|
+
return { deployed: false, steps, error: msg };
|
|
4321
|
+
}
|
|
4322
|
+
steps.push({ step: "verify:health", status: "ok" });
|
|
4323
|
+
}
|
|
4324
|
+
for (const test of manifest.verify.smoke_tests || []) {
|
|
4325
|
+
const status = await httpStatus(test.url);
|
|
4326
|
+
if (!test.expected_status.includes(status)) {
|
|
4327
|
+
const msg = `Smoke test "${test.name}" failed: got ${status}, expected ${test.expected_status.join("|")}`;
|
|
4328
|
+
steps.push({ step: `verify:${test.name}`, status: "failed", message: msg });
|
|
4329
|
+
log("error", msg);
|
|
4330
|
+
if (manifest.rollback?.auto_rollback_on_verify_failure && preDeployCommit) {
|
|
4331
|
+
await rollback(workspaceRoot, preDeployCommit, manifest, log);
|
|
4332
|
+
return { deployed: false, steps, error: msg, rolledBack: true };
|
|
4333
|
+
}
|
|
4334
|
+
return { deployed: false, steps, error: msg };
|
|
4335
|
+
}
|
|
4336
|
+
steps.push({ step: `verify:${test.name}`, status: "ok" });
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
console.log(" [deploy] Deployment verified");
|
|
4340
|
+
log("lifecycle", "Deployment verified successfully");
|
|
4341
|
+
return { deployed: true, steps };
|
|
4342
|
+
}
|
|
4343
|
+
async function rollback(workspaceRoot, targetCommit, manifest, log) {
|
|
4344
|
+
console.log(` [deploy] Rolling back to ${targetCommit.substring(0, 8)}`);
|
|
4345
|
+
log("lifecycle", `Rolling back to ${targetCommit.substring(0, 8)}`);
|
|
4346
|
+
try {
|
|
4347
|
+
exec(`git checkout ${targetCommit}`, workspaceRoot, 1e4);
|
|
4348
|
+
} catch {
|
|
4349
|
+
log("error", "Rollback: git checkout failed");
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
if (manifest.build?.steps) {
|
|
4353
|
+
for (const step of manifest.build.steps) {
|
|
4354
|
+
exec(step.command, (0, import_node_path.join)(workspaceRoot, step.working_dir || "."), (step.timeout_seconds || 300) * 1e3);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
if (manifest.service?.restart_command) {
|
|
4358
|
+
exec(manifest.service.restart_command, workspaceRoot, 3e4);
|
|
4359
|
+
}
|
|
4360
|
+
log("lifecycle", "Rollback complete");
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4070
4363
|
// src/commands/agent.ts
|
|
4364
|
+
var AUTH_ERROR_PATTERNS = [
|
|
4365
|
+
"Not logged in",
|
|
4366
|
+
"Please run /login",
|
|
4367
|
+
"authentication required",
|
|
4368
|
+
"unauthorized",
|
|
4369
|
+
"expired token",
|
|
4370
|
+
"not authenticated",
|
|
4371
|
+
"login required"
|
|
4372
|
+
];
|
|
4373
|
+
function containsAuthError(text) {
|
|
4374
|
+
const lower = text.toLowerCase();
|
|
4375
|
+
for (const pattern of AUTH_ERROR_PATTERNS) {
|
|
4376
|
+
if (lower.includes(pattern.toLowerCase())) {
|
|
4377
|
+
return pattern;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
async function checkClaudeAuth() {
|
|
4383
|
+
try {
|
|
4384
|
+
const output = (0, import_node_child_process4.execSync)("claude --version 2>&1", {
|
|
4385
|
+
encoding: "utf8",
|
|
4386
|
+
timeout: 1e4,
|
|
4387
|
+
env: { ...process.env }
|
|
4388
|
+
});
|
|
4389
|
+
const authError = containsAuthError(output);
|
|
4390
|
+
if (authError) {
|
|
4391
|
+
return { status: "expired", error: authError };
|
|
4392
|
+
}
|
|
4393
|
+
return { status: "authenticated" };
|
|
4394
|
+
} catch (err) {
|
|
4395
|
+
const output = (err.stdout || "") + (err.stderr || "") + (err.message || "");
|
|
4396
|
+
const authError = containsAuthError(output);
|
|
4397
|
+
if (authError) {
|
|
4398
|
+
return { status: "expired", error: authError };
|
|
4399
|
+
}
|
|
4400
|
+
return { status: "not_configured", error: output.slice(0, 500) };
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
async function getClaudeEnv() {
|
|
4404
|
+
const env2 = { ...process.env };
|
|
4405
|
+
let usingApiKey = false;
|
|
4406
|
+
if (!env2.ANTHROPIC_API_KEY) {
|
|
4407
|
+
try {
|
|
4408
|
+
const config = await readConfig();
|
|
4409
|
+
if (config.anthropic_api_key) {
|
|
4410
|
+
env2.ANTHROPIC_API_KEY = config.anthropic_api_key;
|
|
4411
|
+
usingApiKey = true;
|
|
4412
|
+
}
|
|
4413
|
+
} catch {
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
return { env: env2, usingApiKey };
|
|
4417
|
+
}
|
|
4418
|
+
function buildAuthFailureMessage(errorString, machineName, hasApiKeyFallback) {
|
|
4419
|
+
let msg = `CLAUDE_AUTH_REQUIRED: ${errorString}
|
|
4420
|
+
`;
|
|
4421
|
+
msg += `Machine: ${machineName}
|
|
4422
|
+
`;
|
|
4423
|
+
msg += `Fix: SSH into ${machineName} and run: claude /login
|
|
4424
|
+
`;
|
|
4425
|
+
if (!hasApiKeyFallback) {
|
|
4426
|
+
msg += `Alternative: Set an API key fallback with: cva auth set-api-key sk-ant-...
|
|
4427
|
+
`;
|
|
4428
|
+
}
|
|
4429
|
+
return msg;
|
|
4430
|
+
}
|
|
4071
4431
|
function buildClaudePrompt(task) {
|
|
4072
4432
|
let prompt = "";
|
|
4073
4433
|
prompt += `You are executing a task dispatched via CV-Hub.
|
|
@@ -4232,14 +4592,16 @@ async function launchAutoApproveMode(prompt, options) {
|
|
|
4232
4592
|
if (sessionId && !isContinue) {
|
|
4233
4593
|
args.push("--session-id", sessionId);
|
|
4234
4594
|
}
|
|
4235
|
-
const child = (0,
|
|
4595
|
+
const child = (0, import_node_child_process4.spawn)("claude", args, {
|
|
4236
4596
|
cwd: options.cwd,
|
|
4237
4597
|
stdio: ["inherit", "pipe", "pipe"],
|
|
4238
|
-
env: { ...process.env }
|
|
4598
|
+
env: options.spawnEnv || { ...process.env }
|
|
4239
4599
|
});
|
|
4240
4600
|
_activeChild = child;
|
|
4241
4601
|
let stderr = "";
|
|
4242
4602
|
let lineBuffer = "";
|
|
4603
|
+
let authFailure = false;
|
|
4604
|
+
const spawnTime = Date.now();
|
|
4243
4605
|
child.stdout?.on("data", (data) => {
|
|
4244
4606
|
const text = data.toString();
|
|
4245
4607
|
process.stdout.write(data);
|
|
@@ -4247,6 +4609,23 @@ async function launchAutoApproveMode(prompt, options) {
|
|
|
4247
4609
|
if (fullOutput.length > MAX_OUTPUT_BYTES) {
|
|
4248
4610
|
fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
|
|
4249
4611
|
}
|
|
4612
|
+
if (Date.now() - spawnTime < 1e4) {
|
|
4613
|
+
const authError = containsAuthError(fullOutput + stderr);
|
|
4614
|
+
if (authError) {
|
|
4615
|
+
authFailure = true;
|
|
4616
|
+
console.log(`
|
|
4617
|
+
${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
|
|
4618
|
+
console.log(source_default.yellow(` Killing process \u2014 it won't recover without re-authentication.`));
|
|
4619
|
+
if (options.machineName) {
|
|
4620
|
+
console.log(source_default.cyan(` Fix: SSH into ${options.machineName} and run: claude /login`));
|
|
4621
|
+
}
|
|
4622
|
+
try {
|
|
4623
|
+
child.kill("SIGTERM");
|
|
4624
|
+
} catch {
|
|
4625
|
+
}
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4250
4629
|
if (options.creds && options.taskId) {
|
|
4251
4630
|
lineBuffer += text;
|
|
4252
4631
|
const lines = lineBuffer.split("\n");
|
|
@@ -4289,12 +4668,30 @@ async function launchAutoApproveMode(prompt, options) {
|
|
|
4289
4668
|
}
|
|
4290
4669
|
});
|
|
4291
4670
|
child.stderr?.on("data", (data) => {
|
|
4292
|
-
|
|
4671
|
+
const text = data.toString();
|
|
4672
|
+
stderr += text;
|
|
4293
4673
|
process.stderr.write(data);
|
|
4674
|
+
if (Date.now() - spawnTime < 1e4 && !authFailure) {
|
|
4675
|
+
const authError = containsAuthError(text);
|
|
4676
|
+
if (authError) {
|
|
4677
|
+
authFailure = true;
|
|
4678
|
+
console.log(`
|
|
4679
|
+
${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
|
|
4680
|
+
try {
|
|
4681
|
+
child.kill("SIGTERM");
|
|
4682
|
+
} catch {
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4294
4686
|
});
|
|
4295
4687
|
child.on("close", (code, signal) => {
|
|
4296
4688
|
_activeChild = null;
|
|
4297
|
-
resolve({
|
|
4689
|
+
resolve({
|
|
4690
|
+
exitCode: signal === "SIGKILL" ? 137 : code ?? 1,
|
|
4691
|
+
stderr,
|
|
4692
|
+
output: fullOutput,
|
|
4693
|
+
authFailure
|
|
4694
|
+
});
|
|
4298
4695
|
});
|
|
4299
4696
|
child.on("error", (err) => {
|
|
4300
4697
|
_activeChild = null;
|
|
@@ -4329,16 +4726,18 @@ async function launchAutoApproveMode(prompt, options) {
|
|
|
4329
4726
|
}
|
|
4330
4727
|
async function launchRelayMode(prompt, options) {
|
|
4331
4728
|
return new Promise((resolve, reject) => {
|
|
4332
|
-
const child = (0,
|
|
4729
|
+
const child = (0, import_node_child_process4.spawn)("claude", [], {
|
|
4333
4730
|
cwd: options.cwd,
|
|
4334
4731
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4335
|
-
env: { ...process.env }
|
|
4732
|
+
env: options.spawnEnv || { ...process.env }
|
|
4336
4733
|
});
|
|
4337
4734
|
_activeChild = child;
|
|
4338
4735
|
let stderr = "";
|
|
4339
4736
|
let stdoutBuffer = "";
|
|
4340
4737
|
let fullOutput = "";
|
|
4341
4738
|
let lastProgressBytes = 0;
|
|
4739
|
+
let authFailure = false;
|
|
4740
|
+
const spawnTime = Date.now();
|
|
4342
4741
|
child.stdin?.write(prompt + "\n");
|
|
4343
4742
|
let lastRedirectCheck = Date.now();
|
|
4344
4743
|
let lineBuffer = "";
|
|
@@ -4350,6 +4749,19 @@ async function launchRelayMode(prompt, options) {
|
|
|
4350
4749
|
if (fullOutput.length > MAX_OUTPUT_BYTES) {
|
|
4351
4750
|
fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
|
|
4352
4751
|
}
|
|
4752
|
+
if (Date.now() - spawnTime < 1e4 && !authFailure) {
|
|
4753
|
+
const authError = containsAuthError(fullOutput + stderr);
|
|
4754
|
+
if (authError) {
|
|
4755
|
+
authFailure = true;
|
|
4756
|
+
console.log(`
|
|
4757
|
+
${source_default.red("!")} Claude Code auth failure detected: "${authError}"`);
|
|
4758
|
+
try {
|
|
4759
|
+
child.kill("SIGTERM");
|
|
4760
|
+
} catch {
|
|
4761
|
+
}
|
|
4762
|
+
return;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4353
4765
|
lineBuffer += text;
|
|
4354
4766
|
const lines = lineBuffer.split("\n");
|
|
4355
4767
|
lineBuffer = lines.pop() ?? "";
|
|
@@ -4490,13 +4902,25 @@ async function launchRelayMode(prompt, options) {
|
|
|
4490
4902
|
const text = data.toString();
|
|
4491
4903
|
stderr += text;
|
|
4492
4904
|
process.stderr.write(data);
|
|
4905
|
+
if (Date.now() - spawnTime < 1e4 && !authFailure) {
|
|
4906
|
+
const authError = containsAuthError(text);
|
|
4907
|
+
if (authError) {
|
|
4908
|
+
authFailure = true;
|
|
4909
|
+
console.log(`
|
|
4910
|
+
${source_default.red("!")} Claude Code auth failure (stderr): "${authError}"`);
|
|
4911
|
+
try {
|
|
4912
|
+
child.kill("SIGTERM");
|
|
4913
|
+
} catch {
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4493
4917
|
});
|
|
4494
4918
|
child.on("close", (code, signal) => {
|
|
4495
4919
|
_activeChild = null;
|
|
4496
4920
|
if (signal === "SIGKILL") {
|
|
4497
|
-
resolve({ exitCode: 137, stderr, output: fullOutput });
|
|
4921
|
+
resolve({ exitCode: 137, stderr, output: fullOutput, authFailure });
|
|
4498
4922
|
} else {
|
|
4499
|
-
resolve({ exitCode: code ?? 1, stderr, output: fullOutput });
|
|
4923
|
+
resolve({ exitCode: code ?? 1, stderr, output: fullOutput, authFailure });
|
|
4500
4924
|
}
|
|
4501
4925
|
});
|
|
4502
4926
|
child.on("error", (err) => {
|
|
@@ -4528,19 +4952,19 @@ async function handleSelfUpdate(task, state, creds) {
|
|
|
4528
4952
|
let output = "";
|
|
4529
4953
|
if (source === "npm" || source.startsWith("npm:")) {
|
|
4530
4954
|
const pkg = source === "npm" ? "@controlvector/cv-agent@latest" : source.replace("npm:", "");
|
|
4531
|
-
output = (0,
|
|
4955
|
+
output = (0, import_node_child_process4.execSync)(`npm install -g ${pkg} 2>&1`, { encoding: "utf8", timeout: 12e4 });
|
|
4532
4956
|
} else if (source.startsWith("git:")) {
|
|
4533
4957
|
const repoPath = source.replace("git:", "");
|
|
4534
|
-
output = (0,
|
|
4958
|
+
output = (0, import_node_child_process4.execSync)(`cd ${repoPath} && git pull && npm install && npm run build && npm link 2>&1`, {
|
|
4535
4959
|
encoding: "utf8",
|
|
4536
4960
|
timeout: 3e5
|
|
4537
4961
|
});
|
|
4538
4962
|
} else {
|
|
4539
|
-
output = (0,
|
|
4963
|
+
output = (0, import_node_child_process4.execSync)(`npm install -g @controlvector/cv-agent@latest 2>&1`, { encoding: "utf8", timeout: 12e4 });
|
|
4540
4964
|
}
|
|
4541
4965
|
let newVersion = "unknown";
|
|
4542
4966
|
try {
|
|
4543
|
-
newVersion = (0,
|
|
4967
|
+
newVersion = (0, import_node_child_process4.execSync)("cva --version 2>/dev/null || echo unknown", { encoding: "utf8" }).trim();
|
|
4544
4968
|
} catch {
|
|
4545
4969
|
}
|
|
4546
4970
|
postTaskEvent(creds, task.id, {
|
|
@@ -4566,7 +4990,7 @@ async function handleSelfUpdate(task, state, creds) {
|
|
|
4566
4990
|
if (task.input?.constraints?.includes("restart")) {
|
|
4567
4991
|
console.log(source_default.yellow("Restarting agent with updated binary..."));
|
|
4568
4992
|
const args = process.argv.slice(1).join(" ");
|
|
4569
|
-
(0,
|
|
4993
|
+
(0, import_node_child_process4.execSync)(`nohup cva ${args} > /tmp/cva-restart.log 2>&1 &`, { stdio: "ignore" });
|
|
4570
4994
|
process.exit(0);
|
|
4571
4995
|
}
|
|
4572
4996
|
} catch (err) {
|
|
@@ -4598,7 +5022,7 @@ async function runAgent(options) {
|
|
|
4598
5022
|
process.exit(1);
|
|
4599
5023
|
}
|
|
4600
5024
|
try {
|
|
4601
|
-
(0,
|
|
5025
|
+
(0, import_node_child_process4.execSync)("claude --version", { stdio: "pipe", timeout: 5e3 });
|
|
4602
5026
|
} catch {
|
|
4603
5027
|
console.log();
|
|
4604
5028
|
console.log(source_default.red("Claude Code CLI not found.") + " Install it first:");
|
|
@@ -4606,8 +5030,28 @@ async function runAgent(options) {
|
|
|
4606
5030
|
console.log();
|
|
4607
5031
|
process.exit(1);
|
|
4608
5032
|
}
|
|
5033
|
+
const { env: claudeEnv, usingApiKey } = await getClaudeEnv();
|
|
5034
|
+
const authCheck = await checkClaudeAuth();
|
|
5035
|
+
let currentAuthStatus = authCheck.status;
|
|
5036
|
+
if (authCheck.status === "expired") {
|
|
5037
|
+
if (usingApiKey) {
|
|
5038
|
+
console.log(source_default.yellow("!") + " Claude Code OAuth expired, but API key fallback is configured.");
|
|
5039
|
+
currentAuthStatus = "api_key_fallback";
|
|
5040
|
+
} else {
|
|
5041
|
+
console.log();
|
|
5042
|
+
console.log(source_default.red("Claude Code auth expired: ") + source_default.yellow(authCheck.error || "unknown"));
|
|
5043
|
+
console.log(` Fix: Run ${source_default.cyan("claude /login")} to re-authenticate.`);
|
|
5044
|
+
console.log(` Alternative: ${source_default.cyan("cva auth set-api-key sk-ant-...")} for API key fallback.`);
|
|
5045
|
+
console.log();
|
|
5046
|
+
console.log(source_default.gray("Agent will start but pause task claims until auth is resolved."));
|
|
5047
|
+
console.log();
|
|
5048
|
+
}
|
|
5049
|
+
} else if (usingApiKey) {
|
|
5050
|
+
console.log(source_default.gray(" Using Anthropic API key from config as fallback."));
|
|
5051
|
+
currentAuthStatus = "api_key_fallback";
|
|
5052
|
+
}
|
|
4609
5053
|
try {
|
|
4610
|
-
(0,
|
|
5054
|
+
(0, import_node_child_process4.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
|
|
4611
5055
|
} catch {
|
|
4612
5056
|
console.log(source_default.yellow("!") + " cv-git CLI not found. Claude Code will fall back to raw git commands.");
|
|
4613
5057
|
console.log(` Install it: ${source_default.cyan("npm install -g @controlvector/cv-git")}`);
|
|
@@ -4627,7 +5071,7 @@ async function runAgent(options) {
|
|
|
4627
5071
|
}
|
|
4628
5072
|
let detectedRepoId;
|
|
4629
5073
|
try {
|
|
4630
|
-
const remoteUrl = (0,
|
|
5074
|
+
const remoteUrl = (0, import_node_child_process4.execSync)("git remote get-url origin 2>/dev/null", {
|
|
4631
5075
|
cwd: workingDir,
|
|
4632
5076
|
encoding: "utf8",
|
|
4633
5077
|
timeout: 5e3
|
|
@@ -4672,7 +5116,9 @@ async function runAgent(options) {
|
|
|
4672
5116
|
failedCount: 0,
|
|
4673
5117
|
lastPoll: Date.now(),
|
|
4674
5118
|
lastTaskEnd: Date.now(),
|
|
4675
|
-
running: true
|
|
5119
|
+
running: true,
|
|
5120
|
+
authStatus: currentAuthStatus,
|
|
5121
|
+
machineName
|
|
4676
5122
|
};
|
|
4677
5123
|
installSignalHandlers(
|
|
4678
5124
|
() => state,
|
|
@@ -4683,13 +5129,26 @@ async function runAgent(options) {
|
|
|
4683
5129
|
);
|
|
4684
5130
|
while (state.running) {
|
|
4685
5131
|
try {
|
|
5132
|
+
if (state.authStatus === "expired") {
|
|
5133
|
+
const recheck = await checkClaudeAuth();
|
|
5134
|
+
if (recheck.status === "authenticated") {
|
|
5135
|
+
state.authStatus = "authenticated";
|
|
5136
|
+
console.log(`
|
|
5137
|
+
${source_default.green("\u2713")} Claude Code auth restored. Resuming task claims.`);
|
|
5138
|
+
} else {
|
|
5139
|
+
sendHeartbeat(creds, state.executorId, void 0, void 0, state.authStatus).catch(() => {
|
|
5140
|
+
});
|
|
5141
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
4686
5145
|
const task = await withRetry(
|
|
4687
5146
|
() => pollForTask(creds, state.executorId),
|
|
4688
5147
|
"Task poll"
|
|
4689
5148
|
);
|
|
4690
5149
|
state.lastPoll = Date.now();
|
|
4691
5150
|
if (task) {
|
|
4692
|
-
await executeTask(task, state, creds, options);
|
|
5151
|
+
await executeTask(task, state, creds, options, claudeEnv);
|
|
4693
5152
|
} else {
|
|
4694
5153
|
updateStatusLine(
|
|
4695
5154
|
formatDuration(Date.now() - state.lastTaskEnd),
|
|
@@ -4705,7 +5164,7 @@ ${source_default.red("!")} Error: ${err.message}`);
|
|
|
4705
5164
|
await new Promise((r) => setTimeout(r, pollInterval));
|
|
4706
5165
|
}
|
|
4707
5166
|
}
|
|
4708
|
-
async function executeTask(task, state, creds, options) {
|
|
5167
|
+
async function executeTask(task, state, creds, options, claudeEnv) {
|
|
4709
5168
|
const startTime = Date.now();
|
|
4710
5169
|
state.currentTaskId = task.id;
|
|
4711
5170
|
if (task.task_type === "_system_update") {
|
|
@@ -4733,7 +5192,7 @@ async function executeTask(task, state, creds, options) {
|
|
|
4733
5192
|
try {
|
|
4734
5193
|
const elapsed = formatDuration(Date.now() - startTime);
|
|
4735
5194
|
setTerminalTitle(`cva: ${task.title} (${elapsed})`);
|
|
4736
|
-
await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)
|
|
5195
|
+
await sendHeartbeat(creds, state.executorId, task.id, `Claude Code running (${elapsed} elapsed)`, state.authStatus);
|
|
4737
5196
|
} catch {
|
|
4738
5197
|
}
|
|
4739
5198
|
}, 3e4);
|
|
@@ -4767,17 +5226,48 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4767
5226
|
cwd: options.workingDir,
|
|
4768
5227
|
creds,
|
|
4769
5228
|
taskId: task.id,
|
|
4770
|
-
executorId: state.executorId
|
|
5229
|
+
executorId: state.executorId,
|
|
5230
|
+
spawnEnv: claudeEnv,
|
|
5231
|
+
machineName: state.machineName
|
|
4771
5232
|
});
|
|
4772
5233
|
} else {
|
|
4773
5234
|
result = await launchRelayMode(prompt, {
|
|
4774
5235
|
cwd: options.workingDir,
|
|
4775
5236
|
creds,
|
|
4776
5237
|
executorId: state.executorId,
|
|
4777
|
-
taskId: task.id
|
|
5238
|
+
taskId: task.id,
|
|
5239
|
+
spawnEnv: claudeEnv,
|
|
5240
|
+
machineName: state.machineName
|
|
4778
5241
|
});
|
|
4779
5242
|
}
|
|
4780
5243
|
console.log(source_default.gray("\n" + "-".repeat(60)));
|
|
5244
|
+
if (result.exitCode === 0 || result.exitCode === 1) {
|
|
5245
|
+
try {
|
|
5246
|
+
const gitSafety = gitSafetyNet(
|
|
5247
|
+
options.workingDir,
|
|
5248
|
+
task.title,
|
|
5249
|
+
task.id,
|
|
5250
|
+
task.branch
|
|
5251
|
+
);
|
|
5252
|
+
if (gitSafety.hadChanges) {
|
|
5253
|
+
sendTaskLog(
|
|
5254
|
+
creds,
|
|
5255
|
+
state.executorId,
|
|
5256
|
+
task.id,
|
|
5257
|
+
"git",
|
|
5258
|
+
`Safety net: committed ${gitSafety.filesAdded + gitSafety.filesModified} files (${gitSafety.commitSha || "ok"})`,
|
|
5259
|
+
{ added: gitSafety.filesAdded, modified: gitSafety.filesModified, deleted: gitSafety.filesDeleted }
|
|
5260
|
+
);
|
|
5261
|
+
} else if (gitSafety.pushed) {
|
|
5262
|
+
sendTaskLog(creds, state.executorId, task.id, "git", "Safety net: pushed unpushed commits");
|
|
5263
|
+
}
|
|
5264
|
+
if (gitSafety.error) {
|
|
5265
|
+
sendTaskLog(creds, state.executorId, task.id, "info", `Git safety warning: ${gitSafety.error}`);
|
|
5266
|
+
}
|
|
5267
|
+
} catch (gitErr) {
|
|
5268
|
+
sendTaskLog(creds, state.executorId, task.id, "info", `Git safety net error: ${gitErr.message}`);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
4781
5271
|
const postGitState = capturePostTaskState(options.workingDir, preGitState);
|
|
4782
5272
|
const payload = buildCompletionPayload(result.exitCode, preGitState, postGitState, startTime, result.output);
|
|
4783
5273
|
const elapsed = formatDuration(Date.now() - startTime);
|
|
@@ -4786,6 +5276,28 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4786
5276
|
...postGitState.filesModified,
|
|
4787
5277
|
...postGitState.filesDeleted
|
|
4788
5278
|
];
|
|
5279
|
+
let deployResult;
|
|
5280
|
+
if (result.exitCode === 0) {
|
|
5281
|
+
try {
|
|
5282
|
+
const logFn = (type, msg) => {
|
|
5283
|
+
sendTaskLog(creds, state.executorId, task.id, type, msg);
|
|
5284
|
+
};
|
|
5285
|
+
deployResult = await postTaskDeploy(options.workingDir, task.id, logFn);
|
|
5286
|
+
if (deployResult.deployed) {
|
|
5287
|
+
sendTaskLog(creds, state.executorId, task.id, "lifecycle", "Post-task deploy: verified");
|
|
5288
|
+
} else if (deployResult.error) {
|
|
5289
|
+
sendTaskLog(
|
|
5290
|
+
creds,
|
|
5291
|
+
state.executorId,
|
|
5292
|
+
task.id,
|
|
5293
|
+
"error",
|
|
5294
|
+
`Post-task deploy failed: ${deployResult.error}${deployResult.rolledBack ? " (rolled back)" : ""}`
|
|
5295
|
+
);
|
|
5296
|
+
}
|
|
5297
|
+
} catch (deployErr) {
|
|
5298
|
+
sendTaskLog(creds, state.executorId, task.id, "info", `Deploy manifest error: ${deployErr.message}`);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
4789
5301
|
postTaskEvent(creds, task.id, {
|
|
4790
5302
|
event_type: "completed",
|
|
4791
5303
|
content: {
|
|
@@ -4849,6 +5361,31 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4849
5361
|
} catch {
|
|
4850
5362
|
}
|
|
4851
5363
|
state.failedCount++;
|
|
5364
|
+
} else if (result.authFailure) {
|
|
5365
|
+
const authErrorStr = containsAuthError(result.output + result.stderr) || "auth failure";
|
|
5366
|
+
const hasApiKey = !!claudeEnv?.ANTHROPIC_API_KEY;
|
|
5367
|
+
const authMsg = buildAuthFailureMessage(authErrorStr, state.machineName, hasApiKey);
|
|
5368
|
+
sendTaskLog(creds, state.executorId, task.id, "error", authMsg);
|
|
5369
|
+
console.log();
|
|
5370
|
+
console.log(`\u{1F511} ${source_default.bold.red("AUTH FAILED")} \u2014 Claude Code is not authenticated`);
|
|
5371
|
+
console.log(source_default.yellow(` ${authMsg.replace(/\n/g, "\n ")}`));
|
|
5372
|
+
postTaskEvent(creds, task.id, {
|
|
5373
|
+
event_type: "auth_failure",
|
|
5374
|
+
content: {
|
|
5375
|
+
error: authErrorStr,
|
|
5376
|
+
machine: state.machineName,
|
|
5377
|
+
fix_command: `claude /login`,
|
|
5378
|
+
api_key_configured: hasApiKey
|
|
5379
|
+
}
|
|
5380
|
+
}).catch(() => {
|
|
5381
|
+
});
|
|
5382
|
+
await withRetry(
|
|
5383
|
+
() => failTask(creds, state.executorId, task.id, authMsg),
|
|
5384
|
+
"Report auth failure"
|
|
5385
|
+
);
|
|
5386
|
+
state.failedCount++;
|
|
5387
|
+
state.authStatus = "expired";
|
|
5388
|
+
console.log(source_default.yellow(" Pausing task claims until auth is restored."));
|
|
4852
5389
|
} else {
|
|
4853
5390
|
const stderrTail = result.stderr.trim().slice(-500);
|
|
4854
5391
|
sendTaskLog(
|
|
@@ -4978,16 +5515,40 @@ async function authStatus() {
|
|
|
4978
5515
|
console.log(`Status: ${source_default.red("unreachable")} (${err.message})`);
|
|
4979
5516
|
}
|
|
4980
5517
|
}
|
|
5518
|
+
async function authSetApiKey(key) {
|
|
5519
|
+
if (!key.startsWith("sk-ant-")) {
|
|
5520
|
+
console.log(source_default.red("Invalid API key.") + " Anthropic API keys start with sk-ant-");
|
|
5521
|
+
process.exit(1);
|
|
5522
|
+
}
|
|
5523
|
+
const config = await readConfig();
|
|
5524
|
+
config.anthropic_api_key = key;
|
|
5525
|
+
await writeConfig(config);
|
|
5526
|
+
const masked = key.substring(0, 10) + "..." + key.slice(-4);
|
|
5527
|
+
console.log(source_default.green("API key saved") + ` (${masked})`);
|
|
5528
|
+
console.log(source_default.gray("This key will be used as fallback when Claude Code OAuth expires."));
|
|
5529
|
+
}
|
|
5530
|
+
async function authRemoveApiKey() {
|
|
5531
|
+
const config = await readConfig();
|
|
5532
|
+
if (!config.anthropic_api_key) {
|
|
5533
|
+
console.log(source_default.yellow("No API key configured."));
|
|
5534
|
+
return;
|
|
5535
|
+
}
|
|
5536
|
+
delete config.anthropic_api_key;
|
|
5537
|
+
await writeConfig(config);
|
|
5538
|
+
console.log(source_default.green("API key removed."));
|
|
5539
|
+
}
|
|
4981
5540
|
function authCommand() {
|
|
4982
5541
|
const cmd = new Command("auth");
|
|
4983
5542
|
cmd.description("Manage CV-Hub authentication");
|
|
4984
5543
|
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
5544
|
cmd.command("status").description("Show current authentication status").action(authStatus);
|
|
5545
|
+
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);
|
|
5546
|
+
cmd.command("remove-api-key").description("Remove stored Anthropic API key").action(authRemoveApiKey);
|
|
4986
5547
|
return cmd;
|
|
4987
5548
|
}
|
|
4988
5549
|
|
|
4989
5550
|
// src/commands/remote.ts
|
|
4990
|
-
var
|
|
5551
|
+
var import_node_child_process5 = require("node:child_process");
|
|
4991
5552
|
function getGitHost(apiUrl) {
|
|
4992
5553
|
return apiUrl.replace(/^https?:\/\//, "").replace(/^api\./, "git.");
|
|
4993
5554
|
}
|
|
@@ -5002,16 +5563,16 @@ async function remoteAdd(ownerRepo) {
|
|
|
5002
5563
|
}
|
|
5003
5564
|
const remoteUrl = `https://${gitHost}/${owner}/${repo}.git`;
|
|
5004
5565
|
try {
|
|
5005
|
-
const existing = (0,
|
|
5566
|
+
const existing = (0, import_node_child_process5.execSync)("git remote get-url cvhub", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
5006
5567
|
if (existing === remoteUrl) {
|
|
5007
5568
|
console.log(source_default.gray(`Remote 'cvhub' already set to ${remoteUrl}`));
|
|
5008
5569
|
return;
|
|
5009
5570
|
}
|
|
5010
|
-
(0,
|
|
5571
|
+
(0, import_node_child_process5.execSync)(`git remote set-url cvhub ${remoteUrl}`, { timeout: 5e3 });
|
|
5011
5572
|
console.log(source_default.green("Updated") + ` remote 'cvhub' -> ${remoteUrl}`);
|
|
5012
5573
|
} catch {
|
|
5013
5574
|
try {
|
|
5014
|
-
(0,
|
|
5575
|
+
(0, import_node_child_process5.execSync)(`git remote add cvhub ${remoteUrl}`, { timeout: 5e3 });
|
|
5015
5576
|
console.log(source_default.green("Added") + ` remote 'cvhub' -> ${remoteUrl}`);
|
|
5016
5577
|
} catch (err) {
|
|
5017
5578
|
console.log(source_default.red("Failed to add remote:") + ` ${err.message}`);
|
|
@@ -5203,7 +5764,7 @@ function statusCommand() {
|
|
|
5203
5764
|
|
|
5204
5765
|
// src/index.ts
|
|
5205
5766
|
var program2 = new Command();
|
|
5206
|
-
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.
|
|
5767
|
+
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version(true ? "1.5.0" : "1.1.0");
|
|
5207
5768
|
program2.addCommand(agentCommand());
|
|
5208
5769
|
program2.addCommand(authCommand());
|
|
5209
5770
|
program2.addCommand(remoteCommand());
|