@controlvector/cv-agent 0.1.0 → 1.0.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 +262 -41
- package/dist/bundle.cjs.map +3 -3
- package/dist/commands/agent-git.d.ts.map +1 -1
- package/dist/commands/agent-git.js +0 -10
- 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 +204 -47
- 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) {
|
|
@@ -3996,7 +4056,7 @@ ${source_default.yellow("\u26A0")} ${label} failed, retrying in ${delay}s... (${
|
|
|
3996
4056
|
// src/commands/agent.ts
|
|
3997
4057
|
function buildClaudePrompt(task) {
|
|
3998
4058
|
let prompt = "";
|
|
3999
|
-
prompt += `You are executing a task dispatched
|
|
4059
|
+
prompt += `You are executing a task dispatched via CV-Hub.
|
|
4000
4060
|
|
|
4001
4061
|
`;
|
|
4002
4062
|
prompt += `## Task: ${task.title}
|
|
@@ -4042,23 +4102,38 @@ ${task.input.context}`;
|
|
|
4042
4102
|
`;
|
|
4043
4103
|
});
|
|
4044
4104
|
}
|
|
4045
|
-
|
|
4046
|
-
prompt += `
|
|
4105
|
+
prompt += `
|
|
4047
4106
|
|
|
4048
|
-
## Git
|
|
4107
|
+
## Git & CV-Hub Instructions
|
|
4108
|
+
`;
|
|
4109
|
+
prompt += `You have the \`cv\` CLI (@controlvector/cv-git) available for all CV-Hub git operations.
|
|
4110
|
+
`;
|
|
4111
|
+
prompt += `Use \`cv\` instead of raw \`git\` commands when interacting with CV-Hub repositories:
|
|
4049
4112
|
`;
|
|
4050
|
-
|
|
4113
|
+
prompt += ` cv push # push to CV-Hub
|
|
4051
4114
|
`;
|
|
4052
|
-
|
|
4115
|
+
prompt += ` cv pr create --title "..." # create pull request
|
|
4053
4116
|
`;
|
|
4054
|
-
|
|
4117
|
+
prompt += ` cv issue list # list issues
|
|
4118
|
+
`;
|
|
4119
|
+
prompt += ` cv repo info # repo details
|
|
4120
|
+
`;
|
|
4121
|
+
prompt += `
|
|
4055
4122
|
`;
|
|
4123
|
+
prompt += `For standard git operations (commit, branch, diff, log, status), use regular \`git\` commands.
|
|
4124
|
+
`;
|
|
4125
|
+
if (task.owner && task.repo) {
|
|
4056
4126
|
prompt += `
|
|
4127
|
+
Target repository: ${task.owner}/${task.repo}
|
|
4057
4128
|
`;
|
|
4058
|
-
prompt += `
|
|
4129
|
+
if (task.branch) prompt += `Target branch: ${task.branch}
|
|
4059
4130
|
`;
|
|
4060
4131
|
}
|
|
4061
4132
|
prompt += `
|
|
4133
|
+
`;
|
|
4134
|
+
prompt += `IMPORTANT: Do NOT run \`cva\` commands. The \`cva\` binary is the agent daemon that launched you \u2014 calling it would cause recursion.
|
|
4135
|
+
`;
|
|
4136
|
+
prompt += `
|
|
4062
4137
|
|
|
4063
4138
|
---
|
|
4064
4139
|
`;
|
|
@@ -4130,33 +4205,84 @@ var PERMISSION_PATTERNS = [
|
|
|
4130
4205
|
/\? \(y\/n\)/
|
|
4131
4206
|
];
|
|
4132
4207
|
async function launchAutoApproveMode(prompt, options) {
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4208
|
+
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;
|
|
4209
|
+
const pendingQuestionIds = [];
|
|
4210
|
+
const runOnce = (inputPrompt, isContinue) => {
|
|
4211
|
+
return new Promise((resolve, reject) => {
|
|
4212
|
+
const args = isContinue ? ["-p", inputPrompt, "--continue", "--allowedTools", ...ALLOWED_TOOLS] : ["-p", inputPrompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
4213
|
+
if (sessionId && !isContinue) {
|
|
4214
|
+
args.push("--session-id", sessionId);
|
|
4215
|
+
}
|
|
4216
|
+
const child = (0, import_node_child_process2.spawn)("claude", args, {
|
|
4217
|
+
cwd: options.cwd,
|
|
4218
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
4219
|
+
env: { ...process.env }
|
|
4220
|
+
});
|
|
4221
|
+
_activeChild = child;
|
|
4222
|
+
let stderr = "";
|
|
4223
|
+
let lineBuffer = "";
|
|
4224
|
+
child.stdout?.on("data", (data) => {
|
|
4225
|
+
const text = data.toString();
|
|
4226
|
+
process.stdout.write(data);
|
|
4227
|
+
if (options.creds && options.taskId) {
|
|
4228
|
+
lineBuffer += text;
|
|
4229
|
+
const lines = lineBuffer.split("\n");
|
|
4230
|
+
lineBuffer = lines.pop() ?? "";
|
|
4231
|
+
for (const line of lines) {
|
|
4232
|
+
const event = parseClaudeCodeOutput(line);
|
|
4233
|
+
if (event) {
|
|
4234
|
+
postTaskEvent(options.creds, options.taskId, {
|
|
4235
|
+
event_type: event.eventType,
|
|
4236
|
+
content: event.content,
|
|
4237
|
+
needs_response: event.needsResponse
|
|
4238
|
+
}).then((created) => {
|
|
4239
|
+
if (event.needsResponse && created?.id) {
|
|
4240
|
+
pendingQuestionIds.push(created.id);
|
|
4241
|
+
}
|
|
4242
|
+
}).catch(() => {
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
});
|
|
4248
|
+
child.stderr?.on("data", (data) => {
|
|
4249
|
+
stderr += data.toString();
|
|
4250
|
+
process.stderr.write(data);
|
|
4251
|
+
});
|
|
4252
|
+
child.on("close", (code, signal) => {
|
|
4253
|
+
_activeChild = null;
|
|
4254
|
+
resolve({ exitCode: signal === "SIGKILL" ? 137 : code ?? 1, stderr });
|
|
4255
|
+
});
|
|
4256
|
+
child.on("error", (err) => {
|
|
4257
|
+
_activeChild = null;
|
|
4258
|
+
reject(err);
|
|
4259
|
+
});
|
|
4146
4260
|
});
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4261
|
+
};
|
|
4262
|
+
let result = await runOnce(prompt, false);
|
|
4263
|
+
if (options.creds && options.taskId && result.exitCode === 0) {
|
|
4264
|
+
let followUps = 0;
|
|
4265
|
+
while (pendingQuestionIds.length > 0 && followUps < 3) {
|
|
4266
|
+
const questionId = pendingQuestionIds.shift();
|
|
4267
|
+
console.log(source_default.gray(` [auto-approve] Waiting for planner response to question...`));
|
|
4268
|
+
const response = await pollForEventResponse(
|
|
4269
|
+
options.creds,
|
|
4270
|
+
options.taskId,
|
|
4271
|
+
questionId,
|
|
4272
|
+
3e5
|
|
4273
|
+
);
|
|
4274
|
+
if (response) {
|
|
4275
|
+
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
4276
|
+
console.log(source_default.gray(` [auto-approve] Planner responded, continuing with --continue`));
|
|
4277
|
+
result = await runOnce(responseText, true);
|
|
4278
|
+
followUps++;
|
|
4151
4279
|
} else {
|
|
4152
|
-
|
|
4280
|
+
console.log(source_default.yellow(` [auto-approve] No response received, continuing without.`));
|
|
4281
|
+
break;
|
|
4153
4282
|
}
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
reject(err);
|
|
4158
|
-
});
|
|
4159
|
-
});
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return result;
|
|
4160
4286
|
}
|
|
4161
4287
|
async function launchRelayMode(prompt, options) {
|
|
4162
4288
|
return new Promise((resolve, reject) => {
|
|
@@ -4169,10 +4295,65 @@ async function launchRelayMode(prompt, options) {
|
|
|
4169
4295
|
let stderr = "";
|
|
4170
4296
|
let stdoutBuffer = "";
|
|
4171
4297
|
child.stdin?.write(prompt + "\n");
|
|
4298
|
+
let lastRedirectCheck = Date.now();
|
|
4299
|
+
let lineBuffer = "";
|
|
4172
4300
|
child.stdout?.on("data", async (data) => {
|
|
4173
4301
|
const text = data.toString();
|
|
4174
4302
|
process.stdout.write(data);
|
|
4175
4303
|
stdoutBuffer += text;
|
|
4304
|
+
lineBuffer += text;
|
|
4305
|
+
const lines = lineBuffer.split("\n");
|
|
4306
|
+
lineBuffer = lines.pop() ?? "";
|
|
4307
|
+
for (const line of lines) {
|
|
4308
|
+
const event = parseClaudeCodeOutput(line);
|
|
4309
|
+
if (event) {
|
|
4310
|
+
try {
|
|
4311
|
+
const created = await postTaskEvent(options.creds, options.taskId, {
|
|
4312
|
+
event_type: event.eventType,
|
|
4313
|
+
content: event.content,
|
|
4314
|
+
needs_response: event.needsResponse
|
|
4315
|
+
});
|
|
4316
|
+
if (event.needsResponse && created?.id) {
|
|
4317
|
+
console.log(source_default.gray(` [stream] Question detected, waiting for planner response...`));
|
|
4318
|
+
const response = await pollForEventResponse(
|
|
4319
|
+
options.creds,
|
|
4320
|
+
options.taskId,
|
|
4321
|
+
created.id,
|
|
4322
|
+
3e5
|
|
4323
|
+
);
|
|
4324
|
+
if (response) {
|
|
4325
|
+
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
4326
|
+
child.stdin?.write(responseText + "\n");
|
|
4327
|
+
console.log(source_default.gray(` [stream] Planner responded.`));
|
|
4328
|
+
} else {
|
|
4329
|
+
child.stdin?.write("[No response received within timeout. Continue with your best judgment.]\n");
|
|
4330
|
+
console.log(source_default.yellow(` [stream] Question timed out.`));
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
} catch {
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
if (Date.now() - lastRedirectCheck > 1e4) {
|
|
4338
|
+
try {
|
|
4339
|
+
const redirects = await getRedirects(
|
|
4340
|
+
options.creds,
|
|
4341
|
+
options.taskId,
|
|
4342
|
+
new Date(lastRedirectCheck).toISOString()
|
|
4343
|
+
);
|
|
4344
|
+
for (const redirect of redirects) {
|
|
4345
|
+
const instruction = redirect.content?.instruction;
|
|
4346
|
+
if (instruction) {
|
|
4347
|
+
child.stdin?.write(`
|
|
4348
|
+
[REDIRECT FROM PLANNER]: ${instruction}
|
|
4349
|
+
`);
|
|
4350
|
+
console.log(source_default.gray(` [stream] Redirect received from planner.`));
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
} catch {
|
|
4354
|
+
}
|
|
4355
|
+
lastRedirectCheck = Date.now();
|
|
4356
|
+
}
|
|
4176
4357
|
for (const pattern of PERMISSION_PATTERNS) {
|
|
4177
4358
|
const match = stdoutBuffer.match(pattern);
|
|
4178
4359
|
if (match) {
|
|
@@ -4195,6 +4376,12 @@ async function launchRelayMode(prompt, options) {
|
|
|
4195
4376
|
"approval",
|
|
4196
4377
|
["y", "n"]
|
|
4197
4378
|
);
|
|
4379
|
+
postTaskEvent(options.creds, options.taskId, {
|
|
4380
|
+
event_type: "approval_request",
|
|
4381
|
+
content: { prompt_text: promptText },
|
|
4382
|
+
needs_response: true
|
|
4383
|
+
}).catch(() => {
|
|
4384
|
+
});
|
|
4198
4385
|
const timeoutMs = 5 * 60 * 1e3;
|
|
4199
4386
|
const startPoll = Date.now();
|
|
4200
4387
|
let answered = false;
|
|
@@ -4251,6 +4438,20 @@ async function launchRelayMode(prompt, options) {
|
|
|
4251
4438
|
});
|
|
4252
4439
|
});
|
|
4253
4440
|
}
|
|
4441
|
+
async function pollForEventResponse(creds, taskId, eventId, timeoutMs) {
|
|
4442
|
+
const start = Date.now();
|
|
4443
|
+
while (Date.now() - start < timeoutMs) {
|
|
4444
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
4445
|
+
try {
|
|
4446
|
+
const result = await getEventResponse(creds, taskId, eventId);
|
|
4447
|
+
if (result.response !== null) {
|
|
4448
|
+
return result.response;
|
|
4449
|
+
}
|
|
4450
|
+
} catch {
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
return null;
|
|
4454
|
+
}
|
|
4254
4455
|
async function runAgent(options) {
|
|
4255
4456
|
const creds = await readCredentials();
|
|
4256
4457
|
if (!creds.CV_HUB_API) {
|
|
@@ -4276,9 +4477,16 @@ async function runAgent(options) {
|
|
|
4276
4477
|
console.log();
|
|
4277
4478
|
process.exit(1);
|
|
4278
4479
|
}
|
|
4480
|
+
try {
|
|
4481
|
+
(0, import_node_child_process2.execSync)("cv --version", { stdio: "pipe", timeout: 5e3 });
|
|
4482
|
+
} catch {
|
|
4483
|
+
console.log(source_default.yellow("!") + " cv-git CLI not found. Claude Code will fall back to raw git commands.");
|
|
4484
|
+
console.log(` Install it: ${source_default.cyan("npm install -g @controlvector/cv-git")}`);
|
|
4485
|
+
console.log();
|
|
4486
|
+
}
|
|
4279
4487
|
const machineName = options.machine || await getMachineName();
|
|
4280
4488
|
const pollInterval = Math.max(3, parseInt(options.pollInterval, 10)) * 1e3;
|
|
4281
|
-
const workingDir = options.workingDir;
|
|
4489
|
+
const workingDir = options.workingDir === "." ? process.cwd() : options.workingDir;
|
|
4282
4490
|
if (!options.machine) {
|
|
4283
4491
|
const credCheck = await readCredentials();
|
|
4284
4492
|
if (!credCheck.CV_HUB_MACHINE_NAME) {
|
|
@@ -4398,7 +4606,11 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4398
4606
|
console.log(source_default.gray("-".repeat(60)));
|
|
4399
4607
|
let result;
|
|
4400
4608
|
if (options.autoApprove) {
|
|
4401
|
-
result = await launchAutoApproveMode(prompt, {
|
|
4609
|
+
result = await launchAutoApproveMode(prompt, {
|
|
4610
|
+
cwd: options.workingDir,
|
|
4611
|
+
creds,
|
|
4612
|
+
taskId: task.id
|
|
4613
|
+
});
|
|
4402
4614
|
} else {
|
|
4403
4615
|
result = await launchRelayMode(prompt, {
|
|
4404
4616
|
cwd: options.workingDir,
|
|
@@ -4416,6 +4628,15 @@ ${source_default.red("Timeout")} Task timed out after ${formatDuration(timeoutMs
|
|
|
4416
4628
|
...postGitState.filesModified,
|
|
4417
4629
|
...postGitState.filesDeleted
|
|
4418
4630
|
];
|
|
4631
|
+
postTaskEvent(creds, task.id, {
|
|
4632
|
+
event_type: "completed",
|
|
4633
|
+
content: {
|
|
4634
|
+
exit_code: result.exitCode,
|
|
4635
|
+
duration_seconds: Math.round((Date.now() - startTime) / 1e3),
|
|
4636
|
+
files_changed: allChangedFiles.length
|
|
4637
|
+
}
|
|
4638
|
+
}).catch(() => {
|
|
4639
|
+
});
|
|
4419
4640
|
if (result.exitCode === 0) {
|
|
4420
4641
|
if (allChangedFiles.length > 0) {
|
|
4421
4642
|
sendTaskLog(
|
|
@@ -4508,10 +4729,10 @@ ${source_default.red("!")} Task error: ${err.message}`);
|
|
|
4508
4729
|
}
|
|
4509
4730
|
function agentCommand() {
|
|
4510
4731
|
const cmd = new Command("agent");
|
|
4511
|
-
cmd.description("Listen for tasks dispatched
|
|
4512
|
-
cmd.option("--machine <name>", "
|
|
4513
|
-
cmd.option("--poll-interval <seconds>", "How often to check for tasks", "5");
|
|
4514
|
-
cmd.option("--working-dir <path>", "Working directory for Claude Code",
|
|
4732
|
+
cmd.description("Listen for tasks dispatched via CV-Hub and execute them with Claude Code");
|
|
4733
|
+
cmd.option("--machine <name>", "Override auto-detected machine name");
|
|
4734
|
+
cmd.option("--poll-interval <seconds>", "How often to check for tasks, minimum 3 (default: 5)", "5");
|
|
4735
|
+
cmd.option("--working-dir <path>", "Working directory for Claude Code (default: current directory)", ".");
|
|
4515
4736
|
cmd.option("--auto-approve", "Pre-approve all tool permissions (uses -p mode)", false);
|
|
4516
4737
|
cmd.action(async (opts) => {
|
|
4517
4738
|
await runAgent(opts);
|
|
@@ -4525,7 +4746,7 @@ async function authLogin(opts) {
|
|
|
4525
4746
|
const apiUrl = opts.apiUrl || "https://api.hub.controlvector.io";
|
|
4526
4747
|
console.log(source_default.gray("Validating token..."));
|
|
4527
4748
|
try {
|
|
4528
|
-
const res = await fetch(`${apiUrl}/api/
|
|
4749
|
+
const res = await fetch(`${apiUrl}/api/auth/me`, {
|
|
4529
4750
|
headers: { "Authorization": `Bearer ${token}` }
|
|
4530
4751
|
});
|
|
4531
4752
|
if (!res.ok) {
|
|
@@ -4572,7 +4793,7 @@ async function authStatus() {
|
|
|
4572
4793
|
console.log(`API: ${source_default.cyan(apiUrl)}`);
|
|
4573
4794
|
console.log(`Token: ${source_default.gray(maskedToken)}`);
|
|
4574
4795
|
try {
|
|
4575
|
-
const res = await fetch(`${apiUrl}/api/
|
|
4796
|
+
const res = await fetch(`${apiUrl}/api/auth/me`, {
|
|
4576
4797
|
headers: { "Authorization": `Bearer ${creds.CV_HUB_PAT}` }
|
|
4577
4798
|
});
|
|
4578
4799
|
if (res.ok) {
|
|
@@ -4812,7 +5033,7 @@ function statusCommand() {
|
|
|
4812
5033
|
|
|
4813
5034
|
// src/index.ts
|
|
4814
5035
|
var program2 = new Command();
|
|
4815
|
-
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("
|
|
5036
|
+
program2.name("cva").description("CV-Hub Agent \u2014 bridges Claude Code with CV-Hub task dispatch").version("1.0.0");
|
|
4816
5037
|
program2.addCommand(agentCommand());
|
|
4817
5038
|
program2.addCommand(authCommand());
|
|
4818
5039
|
program2.addCommand(remoteCommand());
|