@simonfestl/husky-cli 1.0.0 → 1.3.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 +37 -0
- package/dist/commands/agent-msg.d.ts +2 -0
- package/dist/commands/agent-msg.js +252 -0
- package/dist/commands/agent.js +270 -0
- package/dist/commands/biz/gotess.d.ts +3 -0
- package/dist/commands/biz/gotess.js +320 -0
- package/dist/commands/biz.js +5 -1
- package/dist/commands/chat.js +507 -0
- package/dist/commands/completion.js +2 -2
- package/dist/commands/config.d.ts +27 -0
- package/dist/commands/config.js +117 -28
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -0
- package/dist/commands/interactive/vm-sessions.js +0 -1
- package/dist/commands/llm-context.js +27 -0
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.js +161 -0
- package/dist/commands/task.js +3 -0
- package/dist/commands/vm.js +7 -2
- package/dist/index.js +6 -0
- package/dist/lib/biz/gotess.d.ts +97 -0
- package/dist/lib/biz/gotess.js +202 -0
- package/dist/lib/permissions.d.ts +78 -0
- package/dist/lib/permissions.js +139 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,6 +174,32 @@ husky vm-config update <config-id> --machine-type e2-standard-2
|
|
|
174
174
|
husky vm-config delete <config-id>
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
### Chat / Messaging
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Check pending messages
|
|
181
|
+
husky chat pending
|
|
182
|
+
husky chat pending --json
|
|
183
|
+
|
|
184
|
+
# View inbox (GitHub + Google Chat)
|
|
185
|
+
husky chat inbox
|
|
186
|
+
husky chat inbox --unread
|
|
187
|
+
|
|
188
|
+
# Reply to any message (auto-detects platform)
|
|
189
|
+
husky chat reply-to <messageId> "Your response"
|
|
190
|
+
|
|
191
|
+
# Reply in Google Chat thread
|
|
192
|
+
husky chat reply-chat "Message" --thread <threadName>
|
|
193
|
+
|
|
194
|
+
# Mark message as read
|
|
195
|
+
husky chat mark-read <messageId>
|
|
196
|
+
|
|
197
|
+
# Watch for messages (inject into tmux)
|
|
198
|
+
husky chat watch-inject --tmux-session supervisor
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The `reply-to` command automatically detects whether the message is from GitHub or Google Chat and uses the appropriate API to send the reply.
|
|
202
|
+
|
|
177
203
|
### Settings
|
|
178
204
|
|
|
179
205
|
```bash
|
|
@@ -270,6 +296,17 @@ husky --version
|
|
|
270
296
|
|
|
271
297
|
## Changelog
|
|
272
298
|
|
|
299
|
+
### v1.1.0 (2026-01-09) - Unified Reply System
|
|
300
|
+
|
|
301
|
+
**New Features:**
|
|
302
|
+
- `husky chat reply-to` now supports both GitHub and Google Chat
|
|
303
|
+
- Auto-detects platform from message metadata
|
|
304
|
+
- GitHub replies use GitHub App (no PAT required on VM)
|
|
305
|
+
|
|
306
|
+
**Improvements:**
|
|
307
|
+
- Require 8+ character prefix for messageId matching (prevents misdirected replies)
|
|
308
|
+
- Better error messages for short messageId prefixes
|
|
309
|
+
|
|
273
310
|
### v1.0.0 (2026-01-08) - Supervisor Architecture
|
|
274
311
|
|
|
275
312
|
**BREAKING CHANGES:**
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
async function apiCall(path, options = {}) {
|
|
4
|
+
const config = getConfig();
|
|
5
|
+
if (!config.apiUrl || !config.apiKey) {
|
|
6
|
+
console.error("Error: API URL and key required. Run: husky config test");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
10
|
+
...options,
|
|
11
|
+
headers: {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"x-api-key": config.apiKey,
|
|
14
|
+
...options.headers,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const error = await res.text();
|
|
19
|
+
throw new Error(`API error ${res.status}: ${error}`);
|
|
20
|
+
}
|
|
21
|
+
return res.json();
|
|
22
|
+
}
|
|
23
|
+
export const agentMsgCommand = new Command("agent-msg")
|
|
24
|
+
.description("Agent-to-agent messaging (worker <-> supervisor)");
|
|
25
|
+
agentMsgCommand
|
|
26
|
+
.command("send")
|
|
27
|
+
.description("Send message to supervisor or another agent")
|
|
28
|
+
.requiredOption("--type <type>", "Message type: approval_request, status_update, error_report, completion, query")
|
|
29
|
+
.requiredOption("--title <title>", "Message title")
|
|
30
|
+
.option("--description <desc>", "Message description")
|
|
31
|
+
.option("--target <id>", "Target agent ID (default: supervisor)")
|
|
32
|
+
.option("--expires <minutes>", "Expiration in minutes", "60")
|
|
33
|
+
.option("--json", "Output as JSON")
|
|
34
|
+
.action(async (options) => {
|
|
35
|
+
try {
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
const sessionId = config.workerId || "unknown";
|
|
38
|
+
const payload = {
|
|
39
|
+
sessionId,
|
|
40
|
+
type: options.type,
|
|
41
|
+
payload: {
|
|
42
|
+
title: options.title,
|
|
43
|
+
description: options.description,
|
|
44
|
+
},
|
|
45
|
+
expiresInMinutes: parseInt(options.expires, 10),
|
|
46
|
+
};
|
|
47
|
+
if (options.target) {
|
|
48
|
+
payload.targetId = options.target;
|
|
49
|
+
}
|
|
50
|
+
const result = await apiCall("/api/agent-messages", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: JSON.stringify(payload),
|
|
53
|
+
});
|
|
54
|
+
if (options.json) {
|
|
55
|
+
console.log(JSON.stringify(result, null, 2));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(`✅ Message sent (ID: ${result.id})`);
|
|
59
|
+
console.log(` Type: ${result.type}`);
|
|
60
|
+
console.log(` Title: ${result.payload.title}`);
|
|
61
|
+
if (result.targetId)
|
|
62
|
+
console.log(` Target: ${result.targetId}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
agentMsgCommand
|
|
71
|
+
.command("list")
|
|
72
|
+
.description("List messages")
|
|
73
|
+
.option("--status <status>", "Filter by status: pending, responded, expired")
|
|
74
|
+
.option("--target <id>", "Filter by target agent ID")
|
|
75
|
+
.option("--limit <n>", "Number of messages", "20")
|
|
76
|
+
.option("--json", "Output as JSON")
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
const params = new URLSearchParams();
|
|
80
|
+
if (options.status)
|
|
81
|
+
params.set("status", options.status);
|
|
82
|
+
if (options.target)
|
|
83
|
+
params.set("targetId", options.target);
|
|
84
|
+
if (options.limit)
|
|
85
|
+
params.set("limit", options.limit);
|
|
86
|
+
const messages = await apiCall(`/api/agent-messages?${params}`);
|
|
87
|
+
if (options.json) {
|
|
88
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!messages || messages.length === 0) {
|
|
92
|
+
console.log("No messages found.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log("\n Agent Messages");
|
|
96
|
+
console.log(" " + "─".repeat(70));
|
|
97
|
+
for (const msg of messages) {
|
|
98
|
+
const status = msg.status === "pending" ? "⏳" : msg.status === "responded" ? "✅" : "⌛";
|
|
99
|
+
console.log(` ${status} [${msg.type}] ${msg.payload.title}`);
|
|
100
|
+
console.log(` ID: ${msg.id} | From: ${msg.sessionId}`);
|
|
101
|
+
if (msg.response) {
|
|
102
|
+
console.log(` Response: ${msg.response.approved ? "Approved" : "Rejected"} - ${msg.response.message || ""}`);
|
|
103
|
+
}
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
agentMsgCommand
|
|
113
|
+
.command("pending")
|
|
114
|
+
.description("List pending messages (for supervisor)")
|
|
115
|
+
.option("--target <id>", "Filter by target agent ID")
|
|
116
|
+
.option("--json", "Output as JSON")
|
|
117
|
+
.action(async (options) => {
|
|
118
|
+
try {
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
if (options.target)
|
|
121
|
+
params.set("targetId", options.target);
|
|
122
|
+
const messages = await apiCall(`/api/agent-messages/pending?${params}`);
|
|
123
|
+
if (options.json) {
|
|
124
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!messages || messages.length === 0) {
|
|
128
|
+
console.log("No pending messages.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(`\n 📬 ${messages.length} Pending Message(s)`);
|
|
132
|
+
console.log(" " + "─".repeat(70));
|
|
133
|
+
for (const msg of messages) {
|
|
134
|
+
console.log(` ⏳ [${msg.type}] ${msg.payload.title}`);
|
|
135
|
+
console.log(` ID: ${msg.id} | From: ${msg.sessionId}`);
|
|
136
|
+
if (msg.payload.description) {
|
|
137
|
+
console.log(` ${msg.payload.description.slice(0, 100)}...`);
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
agentMsgCommand
|
|
148
|
+
.command("respond <messageId>")
|
|
149
|
+
.description("Respond to a pending message")
|
|
150
|
+
.requiredOption("--approve", "Approve the request")
|
|
151
|
+
.requiredOption("--reject", "Reject the request")
|
|
152
|
+
.option("--message <msg>", "Response message")
|
|
153
|
+
.action(async (messageId, options) => {
|
|
154
|
+
try {
|
|
155
|
+
if (options.approve && options.reject) {
|
|
156
|
+
console.error("Error: Cannot both approve and reject");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
const approved = !!options.approve;
|
|
160
|
+
const config = getConfig();
|
|
161
|
+
const result = await apiCall(`/api/agent-messages/${messageId}/respond`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
approved,
|
|
165
|
+
message: options.message,
|
|
166
|
+
respondedBy: config.workerId || "supervisor",
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
console.log(`✅ Message ${approved ? "approved" : "rejected"}`);
|
|
170
|
+
if (options.message)
|
|
171
|
+
console.log(` Response: ${options.message}`);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
agentMsgCommand
|
|
179
|
+
.command("get <messageId>")
|
|
180
|
+
.description("Get message details")
|
|
181
|
+
.option("--json", "Output as JSON")
|
|
182
|
+
.action(async (messageId, options) => {
|
|
183
|
+
try {
|
|
184
|
+
const msg = await apiCall(`/api/agent-messages/${messageId}`);
|
|
185
|
+
if (options.json) {
|
|
186
|
+
console.log(JSON.stringify(msg, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log("\n Message Details");
|
|
190
|
+
console.log(" " + "─".repeat(50));
|
|
191
|
+
console.log(` ID: ${msg.id}`);
|
|
192
|
+
console.log(` Type: ${msg.type}`);
|
|
193
|
+
console.log(` Status: ${msg.status}`);
|
|
194
|
+
console.log(` From: ${msg.sessionId}`);
|
|
195
|
+
if (msg.targetId)
|
|
196
|
+
console.log(` Target: ${msg.targetId}`);
|
|
197
|
+
console.log(` Title: ${msg.payload.title}`);
|
|
198
|
+
if (msg.payload.description) {
|
|
199
|
+
console.log(` Desc: ${msg.payload.description}`);
|
|
200
|
+
}
|
|
201
|
+
if (msg.response) {
|
|
202
|
+
console.log(` Response: ${msg.response.approved ? "✅ Approved" : "❌ Rejected"}`);
|
|
203
|
+
if (msg.response.message)
|
|
204
|
+
console.log(` Message: ${msg.response.message}`);
|
|
205
|
+
console.log(` By: ${msg.response.respondedBy}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
agentMsgCommand
|
|
214
|
+
.command("wait <messageId>")
|
|
215
|
+
.description("Wait for a response to a message (blocking)")
|
|
216
|
+
.option("--timeout <seconds>", "Timeout in seconds", "300")
|
|
217
|
+
.option("--json", "Output as JSON")
|
|
218
|
+
.action(async (messageId, options) => {
|
|
219
|
+
const timeout = parseInt(options.timeout, 10) * 1000;
|
|
220
|
+
const start = Date.now();
|
|
221
|
+
const pollInterval = 2000;
|
|
222
|
+
process.stdout.write("Waiting for response");
|
|
223
|
+
while (Date.now() - start < timeout) {
|
|
224
|
+
try {
|
|
225
|
+
const msg = await apiCall(`/api/agent-messages/${messageId}`);
|
|
226
|
+
if (msg.status === "responded") {
|
|
227
|
+
console.log("\n");
|
|
228
|
+
if (options.json) {
|
|
229
|
+
console.log(JSON.stringify(msg, null, 2));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.log(`✅ Response received: ${msg.response.approved ? "Approved" : "Rejected"}`);
|
|
233
|
+
if (msg.response.message)
|
|
234
|
+
console.log(` Message: ${msg.response.message}`);
|
|
235
|
+
}
|
|
236
|
+
process.exit(msg.response.approved ? 0 : 1);
|
|
237
|
+
}
|
|
238
|
+
if (msg.status === "expired") {
|
|
239
|
+
console.log("\n⌛ Message expired without response");
|
|
240
|
+
process.exit(2);
|
|
241
|
+
}
|
|
242
|
+
process.stdout.write(".");
|
|
243
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error("\nError:", error instanceof Error ? error.message : error);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
console.log("\n⏱️ Timeout waiting for response");
|
|
251
|
+
process.exit(2);
|
|
252
|
+
});
|
package/dist/commands/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { StreamClient, updateSessionStatus, submitPlan, waitForApproval, } from "../lib/streaming.js";
|
|
4
|
+
import { getConfig } from "./config.js";
|
|
4
5
|
// ============================================
|
|
5
6
|
// DEPRECATION NOTICE
|
|
6
7
|
// ============================================
|
|
@@ -277,3 +278,272 @@ function parsePlanFromOutput(output) {
|
|
|
277
278
|
estimatedRuntime: 5, // 5 minutes placeholder
|
|
278
279
|
};
|
|
279
280
|
}
|
|
281
|
+
// ============================================
|
|
282
|
+
// AGENT-TO-AGENT MESSAGING COMMANDS
|
|
283
|
+
// ============================================
|
|
284
|
+
// Role to emoji mapping for formatted messages
|
|
285
|
+
const ROLE_EMOJI = {
|
|
286
|
+
supervisor: "\uD83D\uDC15", // Dog emoji for supervisor
|
|
287
|
+
worker: "\uD83D\uDD27", // Wrench emoji for worker
|
|
288
|
+
reviewer: "\uD83D\uDCDD", // Memo emoji for reviewer
|
|
289
|
+
support: "\uD83C\uDFA7", // Headphones emoji for support
|
|
290
|
+
};
|
|
291
|
+
// husky agent message
|
|
292
|
+
agentCommand
|
|
293
|
+
.command("message")
|
|
294
|
+
.alias("msg")
|
|
295
|
+
.description("Send message to another agent")
|
|
296
|
+
.requiredOption("--to <agent>", "Target agent ID (supervisor, worker-1, reviewer, etc.)")
|
|
297
|
+
.option("--from <agent>", "Sender agent ID (default: from HUSKY_AGENT_ID env)")
|
|
298
|
+
.argument("<message>", "Message to send")
|
|
299
|
+
.action(async (message, options) => {
|
|
300
|
+
const config = getConfig();
|
|
301
|
+
if (!config.apiUrl) {
|
|
302
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const senderId = options.from || process.env.HUSKY_AGENT_ID;
|
|
306
|
+
if (!senderId) {
|
|
307
|
+
console.error("Error: Sender agent ID not specified.");
|
|
308
|
+
console.error(" Either use --from <agent-id> or set HUSKY_AGENT_ID environment variable.");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const targetId = options.to;
|
|
312
|
+
try {
|
|
313
|
+
// 1. Lookup sender agent
|
|
314
|
+
const senderRes = await fetch(`${config.apiUrl}/api/agents/${senderId}`, {
|
|
315
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
316
|
+
});
|
|
317
|
+
if (!senderRes.ok) {
|
|
318
|
+
if (senderRes.status === 404) {
|
|
319
|
+
console.error(`Error: Sender agent '${senderId}' not found.`);
|
|
320
|
+
console.error(" Register first with: husky agent register --id <id> --name <name> --role <role>");
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
console.error(`Error fetching sender agent: ${senderRes.status}`);
|
|
324
|
+
}
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const sender = (await senderRes.json());
|
|
328
|
+
// 2. Lookup target agent
|
|
329
|
+
const targetRes = await fetch(`${config.apiUrl}/api/agents/${targetId}`, {
|
|
330
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
331
|
+
});
|
|
332
|
+
if (!targetRes.ok) {
|
|
333
|
+
if (targetRes.status === 404) {
|
|
334
|
+
console.error(`Error: Target agent '${targetId}' not found.`);
|
|
335
|
+
console.error(" Available agents: husky agent list");
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.error(`Error fetching target agent: ${targetRes.status}`);
|
|
339
|
+
}
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const target = (await targetRes.json());
|
|
343
|
+
// 3. Format the message with emojis
|
|
344
|
+
const senderEmoji = sender.emoji || ROLE_EMOJI[sender.role] || "\uD83E\uDD16";
|
|
345
|
+
const targetEmoji = target.emoji || ROLE_EMOJI[target.role] || "\uD83E\uDD16";
|
|
346
|
+
const formattedMessage = `[${senderEmoji} ${sender.name} -> ${targetEmoji} ${target.name}]\n${message}`;
|
|
347
|
+
// 4. Send the message via API
|
|
348
|
+
const sendRes = await fetch(`${config.apiUrl}/api/agents/${targetId}/message`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
"Content-Type": "application/json",
|
|
352
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
from: senderId,
|
|
356
|
+
message,
|
|
357
|
+
formatted: formattedMessage,
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
if (!sendRes.ok) {
|
|
361
|
+
const error = await sendRes.text();
|
|
362
|
+
console.error(`Error sending message: ${sendRes.status} - ${error}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
const result = (await sendRes.json());
|
|
366
|
+
if (result.ok) {
|
|
367
|
+
console.log(`Message queued for ${target.name} (id: ${result.messageId})`);
|
|
368
|
+
console.log(` Delivery via supervisor-bridge to tmux session: ${target.tmuxSession || targetId}`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(`Message delivery uncertain for ${target.name}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
console.error("Error sending message:", error);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
// husky agent register
|
|
380
|
+
agentCommand
|
|
381
|
+
.command("register")
|
|
382
|
+
.description("Register this agent with the platform")
|
|
383
|
+
.requiredOption("--id <id>", "Agent ID")
|
|
384
|
+
.requiredOption("--name <name>", "Agent display name")
|
|
385
|
+
.requiredOption("--role <role>", "Agent role (supervisor, worker, reviewer, support)")
|
|
386
|
+
.option("--emoji <emoji>", "Agent emoji", "\uD83E\uDD16")
|
|
387
|
+
.option("--tmux <session>", "tmux session name")
|
|
388
|
+
.option("--vm <vmName>", "VM name")
|
|
389
|
+
.action(async (options) => {
|
|
390
|
+
const config = getConfig();
|
|
391
|
+
if (!config.apiUrl) {
|
|
392
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
// Validate role
|
|
396
|
+
const validRoles = ["supervisor", "worker", "reviewer", "support"];
|
|
397
|
+
if (!validRoles.includes(options.role)) {
|
|
398
|
+
console.error(`Error: Invalid role '${options.role}'.`);
|
|
399
|
+
console.error(` Valid roles: ${validRoles.join(", ")}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const res = await fetch(`${config.apiUrl}/api/agents/register`, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/json",
|
|
407
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify({
|
|
410
|
+
id: options.id,
|
|
411
|
+
name: options.name,
|
|
412
|
+
role: options.role,
|
|
413
|
+
emoji: options.emoji,
|
|
414
|
+
tmuxSession: options.tmux,
|
|
415
|
+
vmName: options.vm,
|
|
416
|
+
}),
|
|
417
|
+
});
|
|
418
|
+
if (!res.ok) {
|
|
419
|
+
const error = await res.text();
|
|
420
|
+
console.error(`Error registering agent: ${res.status} - ${error}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const agent = (await res.json());
|
|
424
|
+
console.log("\n Agent Registered");
|
|
425
|
+
console.log(" " + "-".repeat(40));
|
|
426
|
+
console.log(` ID: ${agent.id}`);
|
|
427
|
+
console.log(` Name: ${agent.emoji} ${agent.name}`);
|
|
428
|
+
console.log(` Role: ${agent.role}`);
|
|
429
|
+
if (agent.tmuxSession) {
|
|
430
|
+
console.log(` Tmux: ${agent.tmuxSession}`);
|
|
431
|
+
}
|
|
432
|
+
if (agent.vmName) {
|
|
433
|
+
console.log(` VM: ${agent.vmName}`);
|
|
434
|
+
}
|
|
435
|
+
console.log("");
|
|
436
|
+
console.log(" Set HUSKY_AGENT_ID to use this agent:");
|
|
437
|
+
console.log(` export HUSKY_AGENT_ID="${agent.id}"`);
|
|
438
|
+
console.log("");
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
console.error("Error registering agent:", error);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// husky agent list
|
|
446
|
+
agentCommand
|
|
447
|
+
.command("list")
|
|
448
|
+
.description("List all registered agents")
|
|
449
|
+
.option("--status <status>", "Filter by status (online, offline, busy)")
|
|
450
|
+
.option("--role <role>", "Filter by role (supervisor, worker, reviewer, support)")
|
|
451
|
+
.option("--json", "Output as JSON")
|
|
452
|
+
.action(async (options) => {
|
|
453
|
+
const config = getConfig();
|
|
454
|
+
if (!config.apiUrl) {
|
|
455
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const params = new URLSearchParams();
|
|
460
|
+
if (options.status)
|
|
461
|
+
params.set("status", options.status);
|
|
462
|
+
if (options.role)
|
|
463
|
+
params.set("role", options.role);
|
|
464
|
+
const res = await fetch(`${config.apiUrl}/api/agents?${params}`, {
|
|
465
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
466
|
+
});
|
|
467
|
+
if (!res.ok) {
|
|
468
|
+
throw new Error(`API error: ${res.status}`);
|
|
469
|
+
}
|
|
470
|
+
const data = (await res.json());
|
|
471
|
+
const agents = data.agents || [];
|
|
472
|
+
if (options.json) {
|
|
473
|
+
console.log(JSON.stringify(agents, null, 2));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (agents.length === 0) {
|
|
477
|
+
console.log("No agents registered.");
|
|
478
|
+
console.log("\nRegister an agent with:");
|
|
479
|
+
console.log(" husky agent register --id <id> --name <name> --role <role>");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
console.log("\n Registered Agents");
|
|
483
|
+
console.log(" " + "-".repeat(60));
|
|
484
|
+
for (const agent of agents) {
|
|
485
|
+
const statusIcon = agent.status === "online" ? "\u2714" :
|
|
486
|
+
agent.status === "busy" ? "\u231B" : "\u2717";
|
|
487
|
+
const lastSeen = agent.lastHeartbeat
|
|
488
|
+
? new Date(agent.lastHeartbeat).toLocaleString()
|
|
489
|
+
: "never";
|
|
490
|
+
console.log(` ${statusIcon} ${agent.emoji} ${agent.name} (${agent.id})`);
|
|
491
|
+
console.log(` Role: ${agent.role} | Status: ${agent.status} | Last seen: ${lastSeen}`);
|
|
492
|
+
if (agent.tmuxSession) {
|
|
493
|
+
console.log(` Tmux: ${agent.tmuxSession}`);
|
|
494
|
+
}
|
|
495
|
+
if (agent.vmName) {
|
|
496
|
+
console.log(` VM: ${agent.vmName}`);
|
|
497
|
+
}
|
|
498
|
+
console.log("");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
console.error("Error fetching agents:", error);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
// husky agent heartbeat
|
|
507
|
+
agentCommand
|
|
508
|
+
.command("heartbeat")
|
|
509
|
+
.description("Send heartbeat to mark agent as online")
|
|
510
|
+
.option("--id <id>", "Agent ID (default: from HUSKY_AGENT_ID env)")
|
|
511
|
+
.action(async (options) => {
|
|
512
|
+
const config = getConfig();
|
|
513
|
+
if (!config.apiUrl) {
|
|
514
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
const agentId = options.id || process.env.HUSKY_AGENT_ID;
|
|
518
|
+
if (!agentId) {
|
|
519
|
+
console.error("Error: Agent ID not specified.");
|
|
520
|
+
console.error(" Either use --id <agent-id> or set HUSKY_AGENT_ID environment variable.");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const res = await fetch(`${config.apiUrl}/api/agents/${agentId}/heartbeat`, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: {
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
if (res.status === 404) {
|
|
533
|
+
console.error(`Error: Agent '${agentId}' not found.`);
|
|
534
|
+
console.error(" Register first with: husky agent register --id <id> --name <name> --role <role>");
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const error = await res.text();
|
|
538
|
+
console.error(`Error sending heartbeat: ${res.status} - ${error}`);
|
|
539
|
+
}
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
const result = (await res.json());
|
|
543
|
+
console.log(`Heartbeat sent. Agent '${agentId}' is now ${result.status}.`);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
console.error("Error sending heartbeat:", error);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
});
|