@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.
- package/README.md +56 -13
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +669 -0
- package/dist/commands/completion.js +0 -9
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -0
- package/dist/commands/interactive/tasks.js +93 -9
- package/dist/commands/interactive.js +0 -5
- package/dist/commands/llm-context.d.ts +0 -4
- package/dist/commands/llm-context.js +4 -14
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.js +161 -0
- package/dist/commands/task.js +66 -11
- package/dist/index.js +11 -5
- package/dist/lib/project-resolver.d.ts +26 -0
- package/dist/lib/project-resolver.js +111 -0
- package/package.json +1 -1
- package/dist/commands/interactive/jules-sessions.d.ts +0 -1
- package/dist/commands/interactive/jules-sessions.js +0 -460
- package/dist/commands/jules.d.ts +0 -2
- package/dist/commands/jules.js +0 -593
- package/dist/commands/services.d.ts +0 -2
- package/dist/commands/services.js +0 -381
|
@@ -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
|
+
});
|