@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/dist/commands/chat.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
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
|
+
}
|
|
3
11
|
export const chatCommand = new Command("chat")
|
|
4
12
|
.description("Communicate with the dashboard chat");
|
|
5
13
|
chatCommand
|
|
@@ -160,3 +168,502 @@ chatCommand
|
|
|
160
168
|
process.exit(1);
|
|
161
169
|
}
|
|
162
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
|
+
});
|
|
@@ -135,7 +135,7 @@ ${subcommandCases}
|
|
|
135
135
|
return 0
|
|
136
136
|
;;
|
|
137
137
|
--agent)
|
|
138
|
-
COMPREPLY=( $(compgen -W "claude-code gemini-cli
|
|
138
|
+
COMPREPLY=( $(compgen -W "claude-code gemini-cli custom" -- \${cur}) )
|
|
139
139
|
return 0
|
|
140
140
|
;;
|
|
141
141
|
--type)
|
|
@@ -268,7 +268,7 @@ complete -c husky -l project -d "Project ID" -r
|
|
|
268
268
|
complete -c husky -l status -d "Filter by status" -r -a "backlog in_progress review done pending running completed failed"
|
|
269
269
|
complete -c husky -l priority -d "Priority level" -r -a "low medium high urgent must should could wont"
|
|
270
270
|
complete -c husky -l assignee -d "Assignee type" -r -a "human llm unassigned"
|
|
271
|
-
complete -c husky -l agent -d "Agent type" -r -a "claude-code gemini-cli
|
|
271
|
+
complete -c husky -l agent -d "Agent type" -r -a "claude-code gemini-cli custom"
|
|
272
272
|
complete -c husky -l type -d "Type filter" -r -a "global project architecture patterns decisions learnings"
|
|
273
273
|
complete -c husky -l value-stream -d "Value stream" -r -a "order_to_delivery procure_to_pay returns_management product_lifecycle customer_service marketing_sales finance_accounting hr_operations it_operations general"
|
|
274
274
|
complete -c husky -l action -d "Action type" -r -a "manual semi_automated fully_automated"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
type AgentRole = "supervisor" | "worker" | "reviewer" | "e2e_agent" | "pr_agent" | "support";
|
|
2
3
|
interface Config {
|
|
3
4
|
apiUrl?: string;
|
|
4
5
|
apiKey?: string;
|
|
5
6
|
workerId?: string;
|
|
6
7
|
workerName?: string;
|
|
8
|
+
role?: AgentRole;
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
roleLastChecked?: string;
|
|
7
11
|
billbeeApiKey?: string;
|
|
8
12
|
billbeeUsername?: string;
|
|
9
13
|
billbeePassword?: string;
|
|
@@ -17,8 +21,31 @@ interface Config {
|
|
|
17
21
|
qdrantApiKey?: string;
|
|
18
22
|
gcpProjectId?: string;
|
|
19
23
|
gcpLocation?: string;
|
|
24
|
+
gotessToken?: string;
|
|
25
|
+
gotessBookId?: string;
|
|
20
26
|
}
|
|
21
27
|
export declare function getConfig(): Config;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch role and permissions from /api/auth/whoami
|
|
30
|
+
* Caches the result in config for 1 hour
|
|
31
|
+
*/
|
|
32
|
+
export declare function fetchAndCacheRole(): Promise<{
|
|
33
|
+
role?: AgentRole;
|
|
34
|
+
permissions?: string[];
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Check if current config has a specific permission
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasPermission(permission: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get current role from config (may be undefined if not fetched)
|
|
42
|
+
*/
|
|
43
|
+
export declare function getRole(): AgentRole | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Clear the role cache to force a refresh on next fetchAndCacheRole call
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearRoleCache(): void;
|
|
22
48
|
export declare function setConfig(key: "apiUrl" | "apiKey" | "workerId" | "workerName", value: string): void;
|
|
49
|
+
export declare function setGotessConfig(token: string, bookId: string): void;
|
|
23
50
|
export declare const configCommand: Command;
|
|
24
51
|
export {};
|