@controlvector/cv-agent 0.1.1 → 1.1.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/README.md +164 -0
- package/dist/bundle.cjs +286 -39
- package/dist/bundle.cjs.map +3 -3
- package/dist/commands/agent-git.d.ts +2 -1
- package/dist/commands/agent-git.d.ts.map +1 -1
- package/dist/commands/agent-git.js +2 -11
- package/dist/commands/agent-git.js.map +1 -1
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +222 -44
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/auth.js +2 -2
- package/dist/commands/auth.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/api.d.ts +18 -0
- package/dist/utils/api.d.ts.map +1 -1
- package/dist/utils/api.js +36 -0
- package/dist/utils/api.js.map +1 -1
- package/dist/utils/output-parser.d.ts +26 -0
- package/dist/utils/output-parser.d.ts.map +1 -0
- package/dist/utils/output-parser.js +65 -0
- package/dist/utils/output-parser.js.map +1 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# CV-Agent
|
|
2
|
+
|
|
3
|
+
**Remote task dispatch daemon for CV-Hub. Bridges Claude Code with CV-Hub's agentic task system.**
|
|
4
|
+
|
|
5
|
+
CV-Agent (`cva`) runs as a daemon on your machine, polls CV-Hub for dispatched tasks, spawns Claude Code to execute them, and reports results back. It handles permission relay, git remote management, and executor heartbeats.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@controlvector/cv-agent)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @controlvector/cv-agent
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/build-with-claude/claude-code) to be installed and on your PATH.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Authenticate with CV-Hub
|
|
26
|
+
cva auth login
|
|
27
|
+
|
|
28
|
+
# Add the CV-Hub remote to your repo
|
|
29
|
+
cd your-project
|
|
30
|
+
cva remote add
|
|
31
|
+
|
|
32
|
+
# Start the agent daemon (auto-approves Claude Code tool permissions)
|
|
33
|
+
cva agent --auto-approve
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The agent will register as an executor with CV-Hub, poll for tasks, and execute them using Claude Code.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### `cva agent [options]`
|
|
43
|
+
|
|
44
|
+
Start the agent daemon. Polls CV-Hub for pending tasks and executes them with Claude Code.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cva agent # Start with permission relay to CV-Hub
|
|
48
|
+
cva agent --auto-approve # Auto-approve all Claude Code permissions locally
|
|
49
|
+
cva agent --dir /path # Override working directory
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
- `--auto-approve` — Auto-approve all tool permission prompts (no CV-Hub relay)
|
|
54
|
+
- `--dir <path>` — Working directory for Claude Code sessions
|
|
55
|
+
|
|
56
|
+
### `cva auth login`
|
|
57
|
+
|
|
58
|
+
Authenticate with CV-Hub. Opens a browser for device auth flow, or accepts a token paste.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cva auth login
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Token is stored in `~/.cva/config.json`.
|
|
65
|
+
|
|
66
|
+
### `cva remote add [--name <n>]`
|
|
67
|
+
|
|
68
|
+
Add or update the CV-Hub git remote for the current repository.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cva remote add # Auto-detects from CV-Hub
|
|
72
|
+
cva remote add --name cvhub # Custom remote name (default: cvhub)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `cva remote setup <owner/repo>`
|
|
76
|
+
|
|
77
|
+
Create a repo on CV-Hub and configure the local remote in one step.
|
|
78
|
+
|
|
79
|
+
### `cva task list [--status <status>]`
|
|
80
|
+
|
|
81
|
+
List tasks for the current executor.
|
|
82
|
+
|
|
83
|
+
### `cva task logs <task-id>`
|
|
84
|
+
|
|
85
|
+
Stream task logs and progress.
|
|
86
|
+
|
|
87
|
+
### `cva status`
|
|
88
|
+
|
|
89
|
+
Show executor registration status, active tasks, and recent completions.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## How It Works
|
|
94
|
+
|
|
95
|
+
1. `cva agent` registers this machine as an **executor** with CV-Hub
|
|
96
|
+
2. It polls CV-Hub every 5 seconds for pending tasks
|
|
97
|
+
3. When a task is claimed, it spawns Claude Code with the task prompt
|
|
98
|
+
4. **Permission handling**: Claude Code tool calls require approval:
|
|
99
|
+
- `--auto-approve`: writes `y` to stdin automatically
|
|
100
|
+
- Default: relays prompts to CV-Hub for remote approval (e.g., from Claude.ai)
|
|
101
|
+
5. When Claude Code exits, the agent reports the result (files changed, commits, exit code) to CV-Hub
|
|
102
|
+
6. Heartbeats are sent every 30 seconds to keep the executor registration alive
|
|
103
|
+
|
|
104
|
+
### Git Remote Management
|
|
105
|
+
|
|
106
|
+
Before each task, the agent ensures a `cvhub` remote exists pointing to the correct CV-Hub repo URL. Task prompts are prepended with instructions to push to `cvhub` instead of `origin`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Prerequisites
|
|
111
|
+
|
|
112
|
+
| Requirement | Version | Notes |
|
|
113
|
+
|-------------|---------|-------|
|
|
114
|
+
| Node.js | >= 20 | Runtime |
|
|
115
|
+
| Claude Code | Latest | Must be on PATH as `claude` |
|
|
116
|
+
| CV-Hub account | — | Sign up at [hub.controlvector.io](https://hub.controlvector.io) |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
Config file: `~/.cva/config.json`
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"hub": {
|
|
127
|
+
"api": "https://api.hub.controlvector.io",
|
|
128
|
+
"token": "cvh_xxxxxxxxxxxx"
|
|
129
|
+
},
|
|
130
|
+
"agent": {
|
|
131
|
+
"pollInterval": 5000,
|
|
132
|
+
"heartbeatInterval": 30000,
|
|
133
|
+
"autoApprove": false,
|
|
134
|
+
"maxConcurrentTasks": 1
|
|
135
|
+
},
|
|
136
|
+
"defaults": {
|
|
137
|
+
"remoteName": "cvhub"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Why a Separate Binary?
|
|
145
|
+
|
|
146
|
+
CV-Git (`cv`) and CV-Agent (`cva`) are separate packages to avoid a recursion trap: `cv agent` would spawn Claude Code, which might call `cv` commands, creating a circular dependency. By using `cva` as the agent binary, Claude Code sessions use standard `git` commands only.
|
|
147
|
+
|
|
148
|
+
| Binary | Package | Responsibility |
|
|
149
|
+
|--------|---------|---------------|
|
|
150
|
+
| `cv` | `@controlvector/cv-git` | Git operations, knowledge graph, code analysis, local dev |
|
|
151
|
+
| `cva` | `@controlvector/cv-agent` | Agent daemon, task dispatch, CV-Hub remote management |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Related Projects
|
|
156
|
+
|
|
157
|
+
- [CV-Git](https://www.npmjs.com/package/@controlvector/cv-git) (`cv`) — AI-native version control CLI
|
|
158
|
+
- [CV-Hub](https://hub.controlvector.io) — AI-native Git platform (web app + API)
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/dist/bundle.cjs
CHANGED
|
@@ -3743,6 +3743,28 @@ async function getTaskLogs(creds, taskId) {
|
|
|
3743
3743
|
const data = await res.json();
|
|
3744
3744
|
return data.logs || [];
|
|
3745
3745
|
}
|
|
3746
|
+
async function postTaskEvent(creds, taskId, event) {
|
|
3747
|
+
const res = await apiCall(creds, "POST", `/api/v1/tasks/${taskId}/events`, event);
|
|
3748
|
+
if (!res.ok) throw new Error(`Post event failed: ${res.status}`);
|
|
3749
|
+
return await res.json();
|
|
3750
|
+
}
|
|
3751
|
+
async function getEventResponse(creds, taskId, eventId) {
|
|
3752
|
+
const res = await apiCall(creds, "GET", `/api/v1/tasks/${taskId}/events?after_id=&limit=200`);
|
|
3753
|
+
if (!res.ok) throw new Error(`Get events failed: ${res.status}`);
|
|
3754
|
+
const events = await res.json();
|
|
3755
|
+
const event = events.find((e) => e.id === eventId);
|
|
3756
|
+
return {
|
|
3757
|
+
response: event?.response ?? null,
|
|
3758
|
+
responded_at: event?.respondedAt ?? event?.responded_at ?? null
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
async function getRedirects(creds, taskId, afterTimestamp) {
|
|
3762
|
+
const qs = afterTimestamp ? `?after_timestamp=${encodeURIComponent(afterTimestamp)}` : "";
|
|
3763
|
+
const res = await apiCall(creds, "GET", `/api/v1/tasks/${taskId}/events${qs}`);
|
|
3764
|
+
if (!res.ok) return [];
|
|
3765
|
+
const events = await res.json();
|
|
3766
|
+
return events.filter((e) => (e.eventType ?? e.event_type) === "redirect").map((e) => ({ id: e.id, content: e.content }));
|
|
3767
|
+
}
|
|
3746
3768
|
async function createRepo(creds, name, description) {
|
|
3747
3769
|
const res = await apiCall(creds, "POST", "/api/v1/repos", {
|
|
3748
3770
|
name,
|
|
@@ -3755,6 +3777,44 @@ async function createRepo(creds, name, description) {
|
|
|
3755
3777
|
return await res.json();
|
|
3756
3778
|
}
|
|
3757
3779
|
|
|
3780
|
+
// src/utils/output-parser.ts
|
|
3781
|
+
function parseClaudeCodeOutput(line) {
|
|
3782
|
+
const trimmed = line.trim();
|
|
3783
|
+
if (!trimmed) return null;
|
|
3784
|
+
const thinkingMatch = trimmed.match(/^\[THINKING\]\s*(.+)/);
|
|
3785
|
+
if (thinkingMatch) {
|
|
3786
|
+
return { eventType: "thinking", content: thinkingMatch[1], needsResponse: false };
|
|
3787
|
+
}
|
|
3788
|
+
const decisionMatch = trimmed.match(/^\[DECISION\]\s*(.+)/);
|
|
3789
|
+
if (decisionMatch) {
|
|
3790
|
+
return { eventType: "decision", content: decisionMatch[1], needsResponse: false };
|
|
3791
|
+
}
|
|
3792
|
+
const questionMatch = trimmed.match(/^\[QUESTION\]\s*(.+)/);
|
|
3793
|
+
if (questionMatch) {
|
|
3794
|
+
return { eventType: "question", content: questionMatch[1], needsResponse: true };
|
|
3795
|
+
}
|
|
3796
|
+
const progressMatch = trimmed.match(/^\[PROGRESS\]\s*(.+)/);
|
|
3797
|
+
if (progressMatch) {
|
|
3798
|
+
return { eventType: "progress", content: progressMatch[1], needsResponse: false };
|
|
3799
|
+
}
|
|
3800
|
+
const fileChangeMatch = trimmed.match(
|
|
3801
|
+
/(?:Created|Modified|Wrote to|Deleted)\s+(?:file:\s*)?(.+)/i
|
|
3802
|
+
);
|
|
3803
|
+
if (fileChangeMatch) {
|
|
3804
|
+
const action = trimmed.split(/\s/)[0].toLowerCase();
|
|
3805
|
+
return {
|
|
3806
|
+
eventType: "file_change",
|
|
3807
|
+
content: { path: fileChangeMatch[1].trim(), action },
|
|
3808
|
+
needsResponse: false
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
const errorMatch = trimmed.match(/^(?:Error|FATAL|FAILED|panic|Traceback)/i);
|
|
3812
|
+
if (errorMatch) {
|
|
3813
|
+
return { eventType: "error", content: trimmed, needsResponse: false };
|
|
3814
|
+
}
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3758
3818
|
// src/commands/agent-git.ts
|
|
3759
3819
|
var import_node_child_process = require("node:child_process");
|
|
3760
3820
|
function gitExec(cmd, cwd) {
|
|
@@ -3862,7 +3922,7 @@ function capturePostTaskState(cwd, preState) {
|
|
|
3862
3922
|
pushRemote
|
|
3863
3923
|
};
|
|
3864
3924
|
}
|
|
3865
|
-
function buildCompletionPayload(exitCode, preState, postState, startTime) {
|
|
3925
|
+
function buildCompletionPayload(exitCode, preState, postState, startTime, output = "") {
|
|
3866
3926
|
const durationSec = Math.floor((Date.now() - startTime) / 1e3);
|
|
3867
3927
|
const durationStr = durationSec >= 60 ? `${Math.floor(durationSec / 60)}m ${durationSec % 60}s` : `${durationSec}s`;
|
|
3868
3928
|
const totalChanged = postState.filesAdded.length + postState.filesModified.length + postState.filesDeleted.length;
|
|
@@ -3884,6 +3944,7 @@ function buildCompletionPayload(exitCode, preState, postState, startTime) {
|
|
|
3884
3944
|
}
|
|
3885
3945
|
return {
|
|
3886
3946
|
summary: parts.join(" "),
|
|
3947
|
+
output,
|
|
3887
3948
|
commit: {
|
|
3888
3949
|
sha: postState.headSha,
|
|
3889
3950
|
branch: postState.branch,
|
|
@@ -3996,7 +4057,7 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
|
|
|
3996
4057
|
// src/commands/agent.ts
|
|
3997
4058
|
function buildClaudePrompt(task) {
|
|
3998
4059
|
let prompt = "";
|
|
3999
|
-
prompt += `You are executing a task dispatched
|
|
4060
|
+
prompt += `You are executing a task dispatched via CV-Hub.
|
|
4000
4061
|
|
|
4001
4062
|
`;
|
|
4002
4063
|
prompt += `## Task: ${task.title}
|
|
@@ -4144,34 +4205,108 @@ var PERMISSION_PATTERNS = [
|
|
|
4144
4205
|
/Do you want to proceed\? \(y\/n\)/,
|
|
4145
4206
|
/\? \(y\/n\)/
|
|
4146
4207
|
];
|
|
4208
|
+
var MAX_OUTPUT_BYTES = 200 * 1024;
|
|
4209
|
+
var OUTPUT_PROGRESS_INTERVAL = 4096;
|
|
4147
4210
|
async function launchAutoApproveMode(prompt, options) {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4160
|
-
|
|
4211
|
+
const sessionId = options.taskId ? options.taskId.replace(/-/g, "").slice(0, 32).padEnd(32, "0").replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5") : void 0;
|
|
4212
|
+
const pendingQuestionIds = [];
|
|
4213
|
+
let fullOutput = "";
|
|
4214
|
+
let lastProgressBytes = 0;
|
|
4215
|
+
const runOnce = (inputPrompt, isContinue) => {
|
|
4216
|
+
return new Promise((resolve, reject) => {
|
|
4217
|
+
const args = isContinue ? ["-p", inputPrompt, "--continue", "--allowedTools", ...ALLOWED_TOOLS] : ["-p", inputPrompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
4218
|
+
if (sessionId && !isContinue) {
|
|
4219
|
+
args.push("--session-id", sessionId);
|
|
4220
|
+
}
|
|
4221
|
+
const child = (0, import_node_child_process2.spawn)("claude", args, {
|
|
4222
|
+
cwd: options.cwd,
|
|
4223
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
4224
|
+
env: { ...process.env }
|
|
4225
|
+
});
|
|
4226
|
+
_activeChild = child;
|
|
4227
|
+
let stderr = "";
|
|
4228
|
+
let lineBuffer = "";
|
|
4229
|
+
child.stdout?.on("data", (data) => {
|
|
4230
|
+
const text = data.toString();
|
|
4231
|
+
process.stdout.write(data);
|
|
4232
|
+
fullOutput += text;
|
|
4233
|
+
if (fullOutput.length > MAX_OUTPUT_BYTES) {
|
|
4234
|
+
fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
|
|
4235
|
+
}
|
|
4236
|
+
if (options.creds && options.taskId) {
|
|
4237
|
+
lineBuffer += text;
|
|
4238
|
+
const lines = lineBuffer.split("\n");
|
|
4239
|
+
lineBuffer = lines.pop() ?? "";
|
|
4240
|
+
for (const line of lines) {
|
|
4241
|
+
const event = parseClaudeCodeOutput(line);
|
|
4242
|
+
if (event) {
|
|
4243
|
+
postTaskEvent(options.creds, options.taskId, {
|
|
4244
|
+
event_type: event.eventType,
|
|
4245
|
+
content: event.content,
|
|
4246
|
+
needs_response: event.needsResponse
|
|
4247
|
+
}).then((created) => {
|
|
4248
|
+
if (event.needsResponse && created?.id) {
|
|
4249
|
+
pendingQuestionIds.push(created.id);
|
|
4250
|
+
}
|
|
4251
|
+
}).catch(() => {
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
if (fullOutput.length - lastProgressBytes >= OUTPUT_PROGRESS_INTERVAL) {
|
|
4256
|
+
const chunk = fullOutput.slice(lastProgressBytes).slice(-OUTPUT_PROGRESS_INTERVAL);
|
|
4257
|
+
lastProgressBytes = fullOutput.length;
|
|
4258
|
+
if (options.executorId) {
|
|
4259
|
+
sendTaskLog(
|
|
4260
|
+
options.creds,
|
|
4261
|
+
options.executorId,
|
|
4262
|
+
options.taskId,
|
|
4263
|
+
"progress",
|
|
4264
|
+
"Claude Code output",
|
|
4265
|
+
{ output_chunk: chunk }
|
|
4266
|
+
).catch(() => {
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
});
|
|
4272
|
+
child.stderr?.on("data", (data) => {
|
|
4273
|
+
stderr += data.toString();
|
|
4274
|
+
process.stderr.write(data);
|
|
4275
|
+
});
|
|
4276
|
+
child.on("close", (code, signal) => {
|
|
4277
|
+
_activeChild = null;
|
|
4278
|
+
resolve({ exitCode: signal === "SIGKILL" ? 137 : code ?? 1, stderr, output: fullOutput });
|
|
4279
|
+
});
|
|
4280
|
+
child.on("error", (err) => {
|
|
4281
|
+
_activeChild = null;
|
|
4282
|
+
reject(err);
|
|
4283
|
+
});
|
|
4161
4284
|
});
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4285
|
+
};
|
|
4286
|
+
let result = await runOnce(prompt, false);
|
|
4287
|
+
if (options.creds && options.taskId && result.exitCode === 0) {
|
|
4288
|
+
let followUps = 0;
|
|
4289
|
+
while (pendingQuestionIds.length > 0 && followUps < 3) {
|
|
4290
|
+
const questionId = pendingQuestionIds.shift();
|
|
4291
|
+
console.log(source_default.gray(` [auto-approve] Waiting for planner response to question...`));
|
|
4292
|
+
const response = await pollForEventResponse(
|
|
4293
|
+
options.creds,
|
|
4294
|
+
options.taskId,
|
|
4295
|
+
questionId,
|
|
4296
|
+
3e5
|
|
4297
|
+
);
|
|
4298
|
+
if (response) {
|
|
4299
|
+
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
4300
|
+
console.log(source_default.gray(` [auto-approve] Planner responded, continuing with --continue`));
|
|
4301
|
+
result = await runOnce(responseText, true);
|
|
4302
|
+
followUps++;
|
|
4166
4303
|
} else {
|
|
4167
|
-
|
|
4304
|
+
console.log(source_default.yellow(` [auto-approve] No response received, continuing without.`));
|
|
4305
|
+
break;
|
|
4168
4306
|
}
|
|
4169
|
-
}
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
reject(err);
|
|
4173
|
-
});
|
|
4174
|
-
});
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return result;
|
|
4175
4310
|
}
|
|
4176
4311
|
async function launchRelayMode(prompt, options) {
|
|
4177
4312
|
return new Promise((resolve, reject) => {
|
|
@@ -4183,11 +4318,85 @@ async function launchRelayMode(prompt, options) {
|
|
|
4183
4318
|
_activeChild = child;
|
|
4184
4319
|
let stderr = "";
|
|
4185
4320
|
let stdoutBuffer = "";
|
|
4321
|
+
let fullOutput = "";
|
|
4322
|
+
let lastProgressBytes = 0;
|
|
4186
4323
|
child.stdin?.write(prompt + "\n");
|
|
4324
|
+
let lastRedirectCheck = Date.now();
|
|
4325
|
+
let lineBuffer = "";
|
|
4187
4326
|
child.stdout?.on("data", async (data) => {
|
|
4188
4327
|
const text = data.toString();
|
|
4189
4328
|
process.stdout.write(data);
|
|
4190
4329
|
stdoutBuffer += text;
|
|
4330
|
+
fullOutput += text;
|
|
4331
|
+
if (fullOutput.length > MAX_OUTPUT_BYTES) {
|
|
4332
|
+
fullOutput = fullOutput.slice(-MAX_OUTPUT_BYTES);
|
|
4333
|
+
}
|
|
4334
|
+
lineBuffer += text;
|
|
4335
|
+
const lines = lineBuffer.split("\n");
|
|
4336
|
+
lineBuffer = lines.pop() ?? "";
|
|
4337
|
+
for (const line of lines) {
|
|
4338
|
+
const event = parseClaudeCodeOutput(line);
|
|
4339
|
+
if (event) {
|
|
4340
|
+
try {
|
|
4341
|
+
const created = await postTaskEvent(options.creds, options.taskId, {
|
|
4342
|
+
event_type: event.eventType,
|
|
4343
|
+
content: event.content,
|
|
4344
|
+
needs_response: event.needsResponse
|
|
4345
|
+
});
|
|
4346
|
+
if (event.needsResponse && created?.id) {
|
|
4347
|
+
console.log(source_default.gray(` [stream] Question detected, waiting for planner response...`));
|
|
4348
|
+
const response = await pollForEventResponse(
|
|
4349
|
+
options.creds,
|
|
4350
|
+
options.taskId,
|
|
4351
|
+
created.id,
|
|
4352
|
+
3e5
|
|
4353
|
+
);
|
|
4354
|
+
if (response) {
|
|
4355
|
+
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
4356
|
+
child.stdin?.write(responseText + "\n");
|
|
4357
|
+
console.log(source_default.gray(` [stream] Planner responded.`));
|
|
4358
|
+
} else {
|
|
4359
|
+
child.stdin?.write("[No response received within timeout. Continue with your best judgment.]\n");
|
|
4360
|
+
console.log(source_default.yellow(` [stream] Question timed out.`));
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
} catch {
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
if (fullOutput.length - lastProgressBytes >= OUTPUT_PROGRESS_INTERVAL) {
|
|
4368
|
+
const chunk = fullOutput.slice(lastProgressBytes).slice(-OUTPUT_PROGRESS_INTERVAL);
|
|
4369
|
+
lastProgressBytes = fullOutput.length;
|
|
4370
|
+
sendTaskLog(
|
|
4371
|
+
options.creds,
|
|
4372
|
+
options.executorId,
|
|
4373
|
+
options.taskId,
|
|
4374
|
+
"progress",
|
|
4375
|
+
"Claude Code output",
|
|
4376
|
+
{ output_chunk: chunk }
|
|
4377
|
+
).catch(() => {
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
if (Date.now() - lastRedirectCheck > 1e4) {
|
|
4381
|
+
try {
|
|
4382
|
+
const redirects = await getRedirects(
|
|
4383
|
+
options.creds,
|
|
4384
|
+
options.taskId,
|
|
4385
|
+
new Date(lastRedirectCheck).toISOString()
|
|
4386
|
+
);
|
|
4387
|
+
for (const redirect of redirects) {
|
|
4388
|
+
const instruction = redirect.content?.instruction;
|
|
4389
|
+
if (instruction) {
|
|
4390
|
+
child.stdin?.write(`
|
|
4391
|
+
[REDIRECT FROM PLANNER]: ${instruction}
|
|
4392
|
+
`);
|
|
4393
|
+
console.log(source_default.gray(` [stream] Redirect received from planner.`));
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
} catch {
|
|
4397
|
+
}
|
|
4398
|
+
lastRedirectCheck = Date.now();
|
|
4399
|
+
}
|
|
4191
4400
|
for (const pattern of PERMISSION_PATTERNS) {
|
|
4192
4401
|
const match = stdoutBuffer.match(pattern);
|
|
4193
4402
|
if (match) {
|
|
@@ -4210,6 +4419,12 @@ async function launchRelayMode(prompt, options) {
|
|
|
4210
4419
|
"approval",
|
|
4211
4420
|
["y", "n"]
|
|
4212
4421
|
);
|
|
4422
|
+
postTaskEvent(options.creds, options.taskId, {
|
|
4423
|
+
event_type: "approval_request",
|
|
4424
|
+
content: { prompt_text: promptText },
|
|
4425
|
+
needs_response: true
|
|
4426
|
+
}).catch(() => {
|
|
4427
|
+
});
|
|
4213
4428
|
const timeoutMs = 5 * 60 * 1e3;
|
|
4214
4429
|
const startPoll = Date.now();
|
|
4215
4430
|
let answered = false;
|
|
@@ -4255,9 +4470,9 @@ async function launchRelayMode(prompt, options) {
|
|
|
4255
4470
|
child.on("close", (code, signal) => {
|
|
4256
4471
|
_activeChild = null;
|
|
4257
4472
|
if (signal === "SIGKILL") {
|
|
4258
|
-
resolve({ exitCode: 137, stderr });
|
|
4473
|
+
resolve({ exitCode: 137, stderr, output: fullOutput });
|
|
4259
4474
|
} else {
|
|
4260
|
-
resolve({ exitCode: code ?? 1, stderr });
|
|
4475
|
+
resolve({ exitCode: code ?? 1, stderr, output: fullOutput });
|
|
4261
4476
|
}
|
|
4262
4477
|
});
|
|
4263
4478
|
child.on("error", (err) => {
|
|
@@ -4266,6 +4481,20 @@ async function launchRelayMode(prompt, options) {
|
|
|
4266
4481
|
});
|
|
4267
4482
|
});
|
|
4268
4483
|
}
|
|
4484
|
+
async function pollForEventResponse(creds, taskId, eventId, timeoutMs) {
|
|
4485
|
+
const start = Date.now();
|
|
4486
|
+
while (Date.now() - start < timeoutMs) {
|
|
4487
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
4488
|
+
try {
|
|
4489
|
+
const result = await getEventResponse(creds, taskId, eventId);
|
|
4490
|
+
if (result.response !== null) {
|
|
4491
|
+
return result.response;
|
|
4492
|
+
}
|
|
4493
|
+
} catch {
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
return null;
|
|
4497
|
+
}
|
|
4269
4498
|
async function runAgent(options) {
|
|
4270
4499
|
const creds = await readCredentials();
|
|
4271
4500
|
if (!creds.CV_HUB_API) {
|
|
@@ -4300,7 +4529,7 @@ async function runAgent(options) {
|
|
|
4300
4529
|
}
|
|
4301
4530
|
const machineName = options.machine || await getMachineName();
|
|
4302
4531
|
const pollInterval = Math.max(3, parseInt(options.pollInterval, 10)) * 1e3;
|
|
4303
|
-
const workingDir = options.workingDir;
|
|
4532
|
+
const workingDir = options.workingDir === "." ? process.cwd() : options.workingDir;
|
|
4304
4533
|
if (!options.machine) {
|
|
4305
4534
|
const credCheck = await readCredentials();
|
|
4306
4535
|
if (!credCheck.CV_HUB_MACHINE_NAME) {
|
|
@@ -4371,6 +4600,7 @@ async function executeTask(task, state, creds, options) {
|
|
|
4371
4600
|
const startTime = Date.now();
|
|
4372
4601
|
state.currentTaskId = task.id;
|
|
4373
4602
|
process.stdout.write("\r\x1B[K");
|
|
4603
|
+
console.log(`\u{1F4E5} ${source_default.bold.cyan("RECEIVED")} \u2014 Task: ${task.title} (${task.priority})`);
|
|
4374
4604
|
console.log(source_default.bold("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
4375
4605
|
console.log(source_default.bold(`\u2502 Task: ${(task.title || "").substring(0, 53).padEnd(53)}\u2502`));
|
|
4376
4606
|
console.log(source_default.bold(`\u2502 ID: ${task.id.padEnd(55)}\u2502`));
|
|
@@ -4413,14 +4643,19 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4413
4643
|
const prompt = buildClaudePrompt(task);
|
|
4414
4644
|
try {
|
|
4415
4645
|
const mode = options.autoApprove ? "auto-approve" : "relay";
|
|
4416
|
-
console.log(source_default.
|
|
4646
|
+
console.log(`\u{1F680} ${source_default.bold.green("RUNNING")} \u2014 Launching Claude Code (${mode})...`);
|
|
4417
4647
|
if (options.autoApprove) {
|
|
4418
4648
|
console.log(source_default.gray(" Allowed tools: ") + ALLOWED_TOOLS.join(", "));
|
|
4419
4649
|
}
|
|
4420
4650
|
console.log(source_default.gray("-".repeat(60)));
|
|
4421
4651
|
let result;
|
|
4422
4652
|
if (options.autoApprove) {
|
|
4423
|
-
result = await launchAutoApproveMode(prompt, {
|
|
4653
|
+
result = await launchAutoApproveMode(prompt, {
|
|
4654
|
+
cwd: options.workingDir,
|
|
4655
|
+
creds,
|
|
4656
|
+
taskId: task.id,
|
|
4657
|
+
executorId: state.executorId
|
|
4658
|
+
});
|
|
4424
4659
|
} else {
|
|
4425
4660
|
result = await launchRelayMode(prompt, {
|
|
4426
4661
|
cwd: options.workingDir,
|
|
@@ -4431,13 +4666,22 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4431
4666
|
}
|
|
4432
4667
|
console.log(source_default.gray("\n" + "-".repeat(60)));
|
|
4433
4668
|
const postGitState = capturePostTaskState(options.workingDir, preGitState);
|
|
4434
|
-
const payload = buildCompletionPayload(result.exitCode, preGitState, postGitState, startTime);
|
|
4669
|
+
const payload = buildCompletionPayload(result.exitCode, preGitState, postGitState, startTime, result.output);
|
|
4435
4670
|
const elapsed = formatDuration(Date.now() - startTime);
|
|
4436
4671
|
const allChangedFiles = [
|
|
4437
4672
|
...postGitState.filesAdded,
|
|
4438
4673
|
...postGitState.filesModified,
|
|
4439
4674
|
...postGitState.filesDeleted
|
|
4440
4675
|
];
|
|
4676
|
+
postTaskEvent(creds, task.id, {
|
|
4677
|
+
event_type: "completed",
|
|
4678
|
+
content: {
|
|
4679
|
+
exit_code: result.exitCode,
|
|
4680
|
+
duration_seconds: Math.round((Date.now() - startTime) / 1e3),
|
|
4681
|
+
files_changed: allChangedFiles.length
|
|
4682
|
+
}
|
|
4683
|
+
}).catch(() => {
|
|
4684
|
+
});
|
|
4441
4685
|
if (result.exitCode === 0) {
|
|
4442
4686
|
if (allChangedFiles.length > 0) {
|
|
4443
4687
|
sendTaskLog(
|
|
@@ -4459,6 +4703,7 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4459
4703
|
100
|
|
4460
4704
|
);
|
|
4461
4705
|
console.log();
|
|
4706
|
+
console.log(`\u2705 ${source_default.bold.green("COMPLETED")} \u2014 Duration: ${elapsed}`);
|
|
4462
4707
|
printBanner("COMPLETED", elapsed, allChangedFiles, postGitState.headSha);
|
|
4463
4708
|
await withRetry(
|
|
4464
4709
|
() => completeTask(creds, state.executorId, task.id, payload),
|
|
@@ -4475,6 +4720,7 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4475
4720
|
"Task aborted by user (Ctrl+C)"
|
|
4476
4721
|
);
|
|
4477
4722
|
console.log();
|
|
4723
|
+
console.log(`\u23F9 ${source_default.bold.yellow("ABORTED")} \u2014 Duration: ${elapsed}`);
|
|
4478
4724
|
printBanner("ABORTED", elapsed, [], null);
|
|
4479
4725
|
try {
|
|
4480
4726
|
await failTask(creds, state.executorId, task.id, "Aborted by user (Ctrl+C)");
|
|
@@ -4492,6 +4738,7 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4492
4738
|
stderrTail ? { stderr_tail: stderrTail } : void 0
|
|
4493
4739
|
);
|
|
4494
4740
|
console.log();
|
|
4741
|
+
console.log(`\u274C ${source_default.bold.red("FAILED")} \u2014 Duration: ${elapsed} (exit code ${result.exitCode})`);
|
|
4495
4742
|
printBanner("FAILED", elapsed, allChangedFiles, postGitState.headSha);
|
|
4496
4743
|
const errorDetail = result.stderr.trim() ? `${result.stderr.trim().slice(-1500)}
|
|
4497
4744
|
|
|
@@ -4530,10 +4777,10 @@ ${source_default.red("!")} Task error: ${err.message}`);
|
|
|
4530
4777
|
}
|
|
4531
4778
|
function agentCommand() {
|
|
4532
4779
|
const cmd = new Command("agent");
|
|
4533
|
-
cmd.description("Listen for tasks dispatched
|
|
4534
|
-
cmd.option("--machine <name>", "
|
|
4535
|
-
cmd.option("--poll-interval <seconds>", "How often to check for tasks", "5");
|
|
4536
|
-
cmd.option("--working-dir <path>", "Working directory for Claude Code",
|
|
4780
|
+
cmd.description("Listen for tasks dispatched via CV-Hub and execute them with Claude Code");
|
|
4781
|
+
cmd.option("--machine <name>", "Override auto-detected machine name");
|
|
4782
|
+
cmd.option("--poll-interval <seconds>", "How often to check for tasks, minimum 3 (default: 5)", "5");
|
|
4783
|
+
cmd.option("--working-dir <path>", "Working directory for Claude Code (default: current directory)", ".");
|
|
4537
4784
|
cmd.option("--auto-approve", "Pre-approve all tool permissions (uses -p mode)", false);
|
|
4538
4785
|
cmd.action(async (opts) => {
|
|
4539
4786
|
await runAgent(opts);
|
|
@@ -4547,7 +4794,7 @@ async function authLogin(opts) {
|
|
|
4547
4794
|
const apiUrl = opts.apiUrl || "https://api.hub.controlvector.io";
|
|
4548
4795
|
console.log(source_default.gray("Validating token..."));
|
|
4549
4796
|
try {
|
|
4550
|
-
const res = await fetch(`${apiUrl}/api/
|
|
4797
|
+
const res = await fetch(`${apiUrl}/api/auth/me`, {
|
|
4551
4798
|
headers: { "Authorization": `Bearer ${token}` }
|
|
4552
4799
|
});
|
|
4553
4800
|
if (!res.ok) {
|
|
@@ -4594,7 +4841,7 @@ async function authStatus() {
|
|
|
4594
4841
|
console.log(`API: ${source_default.cyan(apiUrl)}`);
|
|
4595
4842
|
console.log(`Token: ${source_default.gray(maskedToken)}`);
|
|
4596
4843
|
try {
|
|
4597
|
-
const res = await fetch(`${apiUrl}/api/
|
|
4844
|
+
const res = await fetch(`${apiUrl}/api/auth/me`, {
|
|
4598
4845
|
headers: { "Authorization": `Bearer ${creds.CV_HUB_PAT}` }
|
|
4599
4846
|
});
|
|
4600
4847
|
if (res.ok) {
|
|
@@ -4834,7 +5081,7 @@ function statusCommand() {
|
|
|
4834
5081
|
|
|
4835
5082
|
// src/index.ts
|
|
4836
5083
|
var program2 = new Command();
|
|
4837
|
-
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("
|
|
5084
|
+
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("1.0.0");
|
|
4838
5085
|
program2.addCommand(agentCommand());
|
|
4839
5086
|
program2.addCommand(authCommand());
|
|
4840
5087
|
program2.addCommand(remoteCommand());
|