@simonfestl/husky-cli 1.2.0 → 1.3.1

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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const agentMsgCommand: Command;
@@ -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
+ });
@@ -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
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const gotessCommand: Command;
3
+ export default gotessCommand;