@simonfestl/husky-cli 0.9.7 → 1.2.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.
@@ -0,0 +1,669 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+ const execAsync = promisify(exec);
6
+ // Helper to get the Husky API URL (for Google Chat integration)
7
+ function getHuskyApiUrl() {
8
+ const config = getConfig();
9
+ return config.apiUrl || null;
10
+ }
11
+ export const chatCommand = new Command("chat")
12
+ .description("Communicate with the dashboard chat");
13
+ chatCommand
14
+ .command("pending")
15
+ .description("Get pending messages from user")
16
+ .option("--json", "Output as JSON")
17
+ .action(async (options) => {
18
+ const config = getConfig();
19
+ if (!config.apiUrl) {
20
+ console.error("Error: API URL not configured.");
21
+ process.exit(1);
22
+ }
23
+ try {
24
+ const res = await fetch(`${config.apiUrl}/api/chat/pending`, {
25
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
26
+ });
27
+ if (!res.ok) {
28
+ throw new Error(`API error: ${res.status}`);
29
+ }
30
+ const data = await res.json();
31
+ const messages = data.messages || [];
32
+ if (options.json) {
33
+ console.log(JSON.stringify(messages, null, 2));
34
+ return;
35
+ }
36
+ if (messages.length === 0) {
37
+ console.log("No pending messages.");
38
+ return;
39
+ }
40
+ console.log("\n Pending Messages");
41
+ console.log(" " + "─".repeat(60));
42
+ for (const msg of messages) {
43
+ const time = new Date(msg.createdAt).toLocaleTimeString();
44
+ console.log(` [${time}] ${msg.content.slice(0, 60)}${msg.content.length > 60 ? "..." : ""}`);
45
+ if (msg.taskId) {
46
+ console.log(` Task: ${msg.taskId}`);
47
+ }
48
+ }
49
+ console.log("");
50
+ }
51
+ catch (error) {
52
+ console.error("Error fetching messages:", error);
53
+ process.exit(1);
54
+ }
55
+ });
56
+ chatCommand
57
+ .command("list")
58
+ .description("List recent chat messages")
59
+ .option("--limit <n>", "Number of messages", "20")
60
+ .option("--json", "Output as JSON")
61
+ .action(async (options) => {
62
+ const config = getConfig();
63
+ if (!config.apiUrl) {
64
+ console.error("Error: API URL not configured.");
65
+ process.exit(1);
66
+ }
67
+ try {
68
+ const res = await fetch(`${config.apiUrl}/api/chat?limit=${options.limit}`, {
69
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
70
+ });
71
+ if (!res.ok) {
72
+ throw new Error(`API error: ${res.status}`);
73
+ }
74
+ const data = await res.json();
75
+ const messages = data.messages || [];
76
+ if (options.json) {
77
+ console.log(JSON.stringify(messages, null, 2));
78
+ return;
79
+ }
80
+ if (messages.length === 0) {
81
+ console.log("No messages.");
82
+ return;
83
+ }
84
+ console.log("\n Chat History");
85
+ console.log(" " + "─".repeat(60));
86
+ for (const msg of messages) {
87
+ const time = new Date(msg.createdAt).toLocaleTimeString();
88
+ const role = msg.role === "user" ? "USER" : msg.role === "supervisor" ? "SUPV" : "SYS";
89
+ const icon = msg.role === "user" ? "👤" : msg.role === "supervisor" ? "🤖" : "⚙️";
90
+ console.log(` ${icon} [${time}] ${role}: ${msg.content.slice(0, 50)}${msg.content.length > 50 ? "..." : ""}`);
91
+ }
92
+ console.log("");
93
+ }
94
+ catch (error) {
95
+ console.error("Error fetching messages:", error);
96
+ process.exit(1);
97
+ }
98
+ });
99
+ chatCommand
100
+ .command("send <message>")
101
+ .description("Send a message as supervisor")
102
+ .option("--task-id <id>", "Link to a specific task")
103
+ .action(async (message, options) => {
104
+ const config = getConfig();
105
+ if (!config.apiUrl) {
106
+ console.error("Error: API URL not configured.");
107
+ process.exit(1);
108
+ }
109
+ try {
110
+ const res = await fetch(`${config.apiUrl}/api/chat/supervisor`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
115
+ },
116
+ body: JSON.stringify({
117
+ content: message,
118
+ taskId: options.taskId,
119
+ }),
120
+ });
121
+ if (!res.ok) {
122
+ throw new Error(`API error: ${res.status}`);
123
+ }
124
+ console.log("Message sent.");
125
+ }
126
+ catch (error) {
127
+ console.error("Error sending message:", error);
128
+ process.exit(1);
129
+ }
130
+ });
131
+ chatCommand
132
+ .command("reply <messageId> <response>")
133
+ .description("Reply to a specific user message")
134
+ .option("--task-id <id>", "Link to a specific task")
135
+ .action(async (messageId, response, options) => {
136
+ const config = getConfig();
137
+ if (!config.apiUrl) {
138
+ console.error("Error: API URL not configured.");
139
+ process.exit(1);
140
+ }
141
+ try {
142
+ const res = await fetch(`${config.apiUrl}/api/chat/${messageId}/reply`, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
147
+ },
148
+ body: JSON.stringify({
149
+ content: response,
150
+ taskId: options.taskId,
151
+ }),
152
+ });
153
+ if (!res.ok) {
154
+ throw new Error(`API error: ${res.status}`);
155
+ }
156
+ await fetch(`${config.apiUrl}/api/chat`, {
157
+ method: "PATCH",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
161
+ },
162
+ body: JSON.stringify({ messageIds: [messageId] }),
163
+ });
164
+ console.log("Reply sent and message marked as read.");
165
+ }
166
+ catch (error) {
167
+ console.error("Error replying:", error);
168
+ process.exit(1);
169
+ }
170
+ });
171
+ chatCommand
172
+ .command("review <question>")
173
+ .description("Request human review via Google Chat")
174
+ .option("--task-id <id>", "Link to a specific task")
175
+ .option("--context <text>", "Additional context for the reviewer")
176
+ .option("--priority <level>", "Priority: low, normal, urgent", "normal")
177
+ .option("--wait", "Wait for human response (polling)")
178
+ .option("--timeout <seconds>", "Timeout for waiting (default: 300)", "300")
179
+ .option("--json", "Output as JSON")
180
+ .action(async (question, options) => {
181
+ const config = getConfig();
182
+ const huskyApiUrl = getHuskyApiUrl();
183
+ if (!huskyApiUrl) {
184
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
185
+ process.exit(1);
186
+ }
187
+ const workerId = process.env.HUSKY_WORKER_ID || `agent-${process.pid}`;
188
+ try {
189
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/request-review`, {
190
+ method: "POST",
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
194
+ },
195
+ body: JSON.stringify({
196
+ agentId: workerId,
197
+ taskId: options.taskId,
198
+ question,
199
+ context: options.context,
200
+ priority: options.priority,
201
+ }),
202
+ });
203
+ if (!res.ok) {
204
+ const error = await res.text();
205
+ throw new Error(`API error: ${res.status} - ${error}`);
206
+ }
207
+ const data = await res.json();
208
+ if (!options.wait) {
209
+ if (options.json) {
210
+ console.log(JSON.stringify(data, null, 2));
211
+ }
212
+ else {
213
+ console.log(`Review requested (ID: ${data.id})`);
214
+ console.log(`Status: ${data.status}`);
215
+ console.log(`\nTo check status: husky chat review-status ${data.id}`);
216
+ console.log(`To wait for response: husky chat review-wait ${data.id}`);
217
+ }
218
+ return;
219
+ }
220
+ console.log(`Review requested (ID: ${data.id}). Waiting for human response...`);
221
+ const timeoutMs = parseInt(options.timeout, 10) * 1000;
222
+ const startTime = Date.now();
223
+ const pollInterval = 5000;
224
+ while (Date.now() - startTime < timeoutMs) {
225
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
226
+ const pollRes = await fetch(`${huskyApiUrl}/api/google-chat/review/${data.id}/poll`, {
227
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
228
+ });
229
+ if (!pollRes.ok)
230
+ continue;
231
+ const pollData = await pollRes.json();
232
+ if (pollData.status === "answered" && pollData.response) {
233
+ if (options.json) {
234
+ console.log(JSON.stringify(pollData, null, 2));
235
+ }
236
+ else {
237
+ console.log(`\nHuman response received from ${pollData.respondedBy || "unknown"}:`);
238
+ console.log(`\n${pollData.response}`);
239
+ }
240
+ return;
241
+ }
242
+ process.stdout.write(".");
243
+ }
244
+ console.error("\nTimeout waiting for human response.");
245
+ process.exit(1);
246
+ }
247
+ catch (error) {
248
+ console.error("Error requesting review:", error);
249
+ process.exit(1);
250
+ }
251
+ });
252
+ chatCommand
253
+ .command("review-status <reviewId>")
254
+ .description("Check status of a human review request")
255
+ .option("--json", "Output as JSON")
256
+ .action(async (reviewId, options) => {
257
+ const config = getConfig();
258
+ const huskyApiUrl = getHuskyApiUrl();
259
+ if (!huskyApiUrl) {
260
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
261
+ process.exit(1);
262
+ }
263
+ try {
264
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/review/${reviewId}`, {
265
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
266
+ });
267
+ if (!res.ok) {
268
+ if (res.status === 404) {
269
+ console.error("Review not found.");
270
+ process.exit(1);
271
+ }
272
+ throw new Error(`API error: ${res.status}`);
273
+ }
274
+ const data = await res.json();
275
+ if (options.json) {
276
+ console.log(JSON.stringify(data, null, 2));
277
+ return;
278
+ }
279
+ console.log(`\nReview: ${data.id}`);
280
+ console.log(`Status: ${data.status}`);
281
+ console.log(`Question: ${data.question}`);
282
+ if (data.response) {
283
+ console.log(`\nResponse from ${data.respondedBy || "unknown"}:`);
284
+ console.log(data.response);
285
+ }
286
+ }
287
+ catch (error) {
288
+ console.error("Error checking review status:", error);
289
+ process.exit(1);
290
+ }
291
+ });
292
+ // ============================================
293
+ // SUPERVISOR INBOX COMMANDS (Google Chat <-> Supervisor)
294
+ // ============================================
295
+ chatCommand
296
+ .command("inbox")
297
+ .description("Get messages from Google Chat (supervisor inbox)")
298
+ .option("--unread", "Only show unread messages")
299
+ .option("--limit <n>", "Number of messages", "10")
300
+ .option("--json", "Output as JSON")
301
+ .action(async (options) => {
302
+ const config = getConfig();
303
+ const huskyApiUrl = getHuskyApiUrl();
304
+ if (!huskyApiUrl) {
305
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
306
+ process.exit(1);
307
+ }
308
+ try {
309
+ const params = new URLSearchParams();
310
+ if (options.unread)
311
+ params.set("unread", "true");
312
+ if (options.limit)
313
+ params.set("limit", options.limit);
314
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/inbox?${params}`, {
315
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
316
+ });
317
+ if (!res.ok) {
318
+ throw new Error(`API error: ${res.status}`);
319
+ }
320
+ const data = await res.json();
321
+ if (options.json) {
322
+ console.log(JSON.stringify(data, null, 2));
323
+ return;
324
+ }
325
+ if (!data.messages || data.messages.length === 0) {
326
+ console.log(options.unread ? "📭 No unread messages." : "📭 No messages in inbox.");
327
+ return;
328
+ }
329
+ console.log("\n 📬 Supervisor Inbox");
330
+ console.log(" " + "─".repeat(60));
331
+ for (const msg of data.messages) {
332
+ const time = new Date(msg.createdAt).toLocaleString();
333
+ const readIcon = msg.read ? "✓" : "●";
334
+ console.log(` ${readIcon} [${msg.id.slice(0, 8)}] ${msg.senderName} (${time})`);
335
+ console.log(` "${msg.text}"`);
336
+ console.log("");
337
+ }
338
+ }
339
+ catch (error) {
340
+ console.error("Error fetching inbox:", error);
341
+ process.exit(1);
342
+ }
343
+ });
344
+ chatCommand
345
+ .command("reply-chat <message>")
346
+ .description("Send a message to Google Chat (supervisor -> human)")
347
+ .option("--space <name>", "Target space (e.g., spaces/ABC123)")
348
+ .option("--thread <name>", "Reply in thread (e.g., spaces/ABC123/threads/XYZ)")
349
+ .action(async (message, options) => {
350
+ const config = getConfig();
351
+ const huskyApiUrl = getHuskyApiUrl();
352
+ if (!huskyApiUrl) {
353
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
354
+ process.exit(1);
355
+ }
356
+ try {
357
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/send`, {
358
+ method: "POST",
359
+ headers: {
360
+ "Content-Type": "application/json",
361
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
362
+ },
363
+ body: JSON.stringify({
364
+ text: message,
365
+ spaceName: options.space,
366
+ threadName: options.thread,
367
+ }),
368
+ });
369
+ if (!res.ok) {
370
+ const error = await res.text();
371
+ throw new Error(`API error: ${res.status} - ${error}`);
372
+ }
373
+ console.log("✅ Message sent to Google Chat.");
374
+ }
375
+ catch (error) {
376
+ console.error("Error sending message:", error);
377
+ process.exit(1);
378
+ }
379
+ });
380
+ chatCommand
381
+ .command("reply-to <messageId> <response>")
382
+ .description("Reply to a specific inbox message in its thread (supports both GitHub and Google Chat)")
383
+ .action(async (messageId, response) => {
384
+ const config = getConfig();
385
+ const huskyApiUrl = getHuskyApiUrl();
386
+ if (!huskyApiUrl) {
387
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
388
+ process.exit(1);
389
+ }
390
+ try {
391
+ // Fetch inbox to find the message
392
+ const inboxRes = await fetch(`${huskyApiUrl}/api/google-chat/inbox?limit=50`, {
393
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
394
+ });
395
+ if (!inboxRes.ok) {
396
+ throw new Error(`Failed to fetch inbox: ${inboxRes.status}`);
397
+ }
398
+ const data = await inboxRes.json();
399
+ // Require exact match or at least 8 characters for prefix matching to avoid misdirected replies
400
+ const msg = data.messages.find(m => m.id === messageId || (messageId.length >= 8 && m.id.startsWith(messageId)));
401
+ if (!msg) {
402
+ console.error(`Message ${messageId} not found in inbox.`);
403
+ if (messageId.length < 8) {
404
+ console.error("Hint: Provide at least 8 characters of the message ID for prefix matching.");
405
+ }
406
+ process.exit(1);
407
+ }
408
+ // Check if it's a GitHub message
409
+ const isGitHub = msg.spaceName?.startsWith("github:");
410
+ if (isGitHub) {
411
+ // Use GitHub reply endpoint
412
+ const sendRes = await fetch(`${huskyApiUrl}/api/github/inbox/${msg.id}/reply`, {
413
+ method: "POST",
414
+ headers: {
415
+ "Content-Type": "application/json",
416
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
417
+ },
418
+ body: JSON.stringify({ text: response }),
419
+ });
420
+ if (!sendRes.ok) {
421
+ const error = await sendRes.text();
422
+ throw new Error(`API error: ${sendRes.status} - ${error}`);
423
+ }
424
+ console.log("✅ Reply posted to GitHub issue.");
425
+ }
426
+ else {
427
+ // Use Google Chat reply
428
+ const sendRes = await fetch(`${huskyApiUrl}/api/google-chat/send`, {
429
+ method: "POST",
430
+ headers: {
431
+ "Content-Type": "application/json",
432
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
433
+ },
434
+ body: JSON.stringify({
435
+ text: response,
436
+ spaceName: msg.spaceName,
437
+ threadName: msg.threadName,
438
+ }),
439
+ });
440
+ if (!sendRes.ok) {
441
+ const error = await sendRes.text();
442
+ throw new Error(`API error: ${sendRes.status} - ${error}`);
443
+ }
444
+ // Mark as read
445
+ await fetch(`${huskyApiUrl}/api/google-chat/inbox/${msg.id}/read`, {
446
+ method: "POST",
447
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
448
+ });
449
+ console.log("✅ Reply sent to Google Chat and message marked as read.");
450
+ }
451
+ }
452
+ catch (error) {
453
+ console.error("Error replying:", error);
454
+ process.exit(1);
455
+ }
456
+ });
457
+ chatCommand
458
+ .command("mark-read <messageId>")
459
+ .description("Mark a message as read")
460
+ .action(async (messageId) => {
461
+ const config = getConfig();
462
+ const huskyApiUrl = getHuskyApiUrl();
463
+ if (!huskyApiUrl) {
464
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
465
+ process.exit(1);
466
+ }
467
+ try {
468
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/inbox/${messageId}/read`, {
469
+ method: "POST",
470
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
471
+ });
472
+ if (!res.ok) {
473
+ throw new Error(`API error: ${res.status}`);
474
+ }
475
+ console.log("✅ Message marked as read.");
476
+ }
477
+ catch (error) {
478
+ console.error("Error marking message as read:", error);
479
+ process.exit(1);
480
+ }
481
+ });
482
+ chatCommand
483
+ .command("watch")
484
+ .description("Watch for new messages (blocking, for supervisor agent)")
485
+ .option("--poll-interval <seconds>", "Poll interval in seconds", "10")
486
+ .action(async (options) => {
487
+ const config = getConfig();
488
+ const huskyApiUrl = getHuskyApiUrl();
489
+ if (!huskyApiUrl) {
490
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
491
+ process.exit(1);
492
+ }
493
+ console.log("👀 Watching for new messages... (Ctrl+C to stop)");
494
+ const pollInterval = parseInt(options.pollInterval, 10) * 1000;
495
+ let lastSeenId = "";
496
+ const poll = async () => {
497
+ try {
498
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/inbox?unread=true&limit=5`, {
499
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
500
+ });
501
+ if (!res.ok)
502
+ return;
503
+ const data = await res.json();
504
+ for (const msg of data.messages || []) {
505
+ if (msg.id !== lastSeenId) {
506
+ lastSeenId = msg.id;
507
+ const time = new Date(msg.createdAt).toLocaleTimeString();
508
+ console.log(`\n📨 [${time}] ${msg.senderName}: ${msg.text}`);
509
+ }
510
+ }
511
+ }
512
+ catch { }
513
+ };
514
+ await poll();
515
+ setInterval(poll, pollInterval);
516
+ process.on("SIGINT", () => {
517
+ console.log("\n👋 Stopped watching.");
518
+ process.exit(0);
519
+ });
520
+ await new Promise(() => { });
521
+ });
522
+ chatCommand
523
+ .command("watch-inject")
524
+ .description("Watch for messages and inject them into a tmux session")
525
+ .option("--poll-interval <seconds>", "Poll interval in seconds", "2")
526
+ .option("--tmux-session <name>", "Target tmux session name", "supervisor")
527
+ .option("--tmux-window <name>", "Target tmux window name or index (default: 0)", "0")
528
+ .option("--hint", "Show reply hint after messages (default: true)", true)
529
+ .option("--no-hint", "Hide reply hint")
530
+ .action(async (options) => {
531
+ const config = getConfig();
532
+ const huskyApiUrl = getHuskyApiUrl();
533
+ if (!huskyApiUrl) {
534
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
535
+ process.exit(1);
536
+ }
537
+ const tmuxSession = options.tmuxSession;
538
+ const tmuxWindow = options.tmuxWindow;
539
+ const tmuxTarget = `${tmuxSession}:${tmuxWindow}`;
540
+ const pollInterval = parseInt(options.pollInterval, 10) * 1000;
541
+ const processedIds = new Set();
542
+ console.log(`📡 Watching for messages (Google Chat & GitHub)...`);
543
+ console.log(` Target: ${tmuxTarget}`);
544
+ console.log(` Poll interval: ${options.pollInterval}s`);
545
+ console.log(` Press Ctrl+C to stop\n`);
546
+ const injectToTmux = async (text, senderName, spaceName) => {
547
+ // Detect platform from spaceName
548
+ const isGitHub = spaceName?.startsWith("github:");
549
+ const platform = isGitHub ? "GitHub" : "Google Chat";
550
+ let formattedMessage = `[${platform}] ${senderName}: ${text}`;
551
+ if (options.hint) {
552
+ if (isGitHub) {
553
+ // Extract repo info from spaceName (format: github:owner/repo)
554
+ const repoInfo = spaceName?.replace("github:", "") || "";
555
+ formattedMessage += `\n💡 Reply on GitHub: ${repoInfo}`;
556
+ }
557
+ else {
558
+ formattedMessage += `\n💡 Tip: Use \`husky chat reply-chat "your response"\` to reply`;
559
+ }
560
+ }
561
+ const escapedMessage = formattedMessage
562
+ .replace(/\\/g, "\\\\")
563
+ .replace(/"/g, '\\"')
564
+ .replace(/\$/g, "\\$")
565
+ .replace(/`/g, "\\`")
566
+ .replace(/'/g, "'\\''");
567
+ try {
568
+ await execAsync(`tmux send-keys -t "${tmuxTarget}" "${escapedMessage}" Enter`, { timeout: 5000 });
569
+ return true;
570
+ }
571
+ catch (error) {
572
+ const err = error;
573
+ console.error(` ❌ Failed to inject: ${err.message}`);
574
+ return false;
575
+ }
576
+ };
577
+ const markAsRead = async (messageId) => {
578
+ try {
579
+ await fetch(`${huskyApiUrl}/api/google-chat/inbox/${messageId}/read`, {
580
+ method: "POST",
581
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
582
+ });
583
+ }
584
+ catch { }
585
+ };
586
+ const poll = async () => {
587
+ try {
588
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/inbox?unread=true&limit=10`, {
589
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
590
+ });
591
+ if (!res.ok)
592
+ return;
593
+ const data = await res.json();
594
+ const messages = (data.messages || []).reverse();
595
+ for (const msg of messages) {
596
+ if (processedIds.has(msg.id))
597
+ continue;
598
+ processedIds.add(msg.id);
599
+ const time = new Date(msg.createdAt).toLocaleTimeString();
600
+ const platform = msg.spaceName?.startsWith("github:") ? "GitHub" : "Google Chat";
601
+ console.log(`📨 [${time}] Injecting ${platform} message from ${msg.senderName}`);
602
+ const success = await injectToTmux(msg.text, msg.senderName, msg.spaceName);
603
+ if (success) {
604
+ await markAsRead(msg.id);
605
+ console.log(` ✓ Injected and marked as read`);
606
+ }
607
+ }
608
+ }
609
+ catch (error) {
610
+ const err = error;
611
+ console.error(`Poll error: ${err.message}`);
612
+ }
613
+ };
614
+ await poll();
615
+ setInterval(poll, pollInterval);
616
+ process.on("SIGINT", () => {
617
+ console.log("\n👋 Stopped watching.");
618
+ process.exit(0);
619
+ });
620
+ await new Promise(() => { });
621
+ });
622
+ // ============================================
623
+ // REVIEW COMMANDS (kept for backwards compatibility)
624
+ // ============================================
625
+ chatCommand
626
+ .command("review-wait <reviewId>")
627
+ .description("Wait for a human review response")
628
+ .option("--timeout <seconds>", "Timeout in seconds (default: 300)", "300")
629
+ .option("--json", "Output as JSON")
630
+ .action(async (reviewId, options) => {
631
+ const config = getConfig();
632
+ const huskyApiUrl = getHuskyApiUrl();
633
+ if (!huskyApiUrl) {
634
+ console.error("Error: API URL not configured. Set husky-api-url or api-url.");
635
+ process.exit(1);
636
+ }
637
+ console.log(`Waiting for response to review ${reviewId}...`);
638
+ const timeoutMs = parseInt(options.timeout, 10) * 1000;
639
+ const startTime = Date.now();
640
+ const pollInterval = 5000;
641
+ try {
642
+ while (Date.now() - startTime < timeoutMs) {
643
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/review/${reviewId}/poll`, {
644
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
645
+ });
646
+ if (res.ok) {
647
+ const data = await res.json();
648
+ if (data.status === "answered" && data.response) {
649
+ if (options.json) {
650
+ console.log(JSON.stringify(data, null, 2));
651
+ }
652
+ else {
653
+ console.log(`\nHuman response received from ${data.respondedBy || "unknown"}:`);
654
+ console.log(`\n${data.response}`);
655
+ }
656
+ return;
657
+ }
658
+ }
659
+ process.stdout.write(".");
660
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
661
+ }
662
+ console.error("\nTimeout waiting for human response.");
663
+ process.exit(1);
664
+ }
665
+ catch (error) {
666
+ console.error("Error waiting for review:", error);
667
+ process.exit(1);
668
+ }
669
+ });