@simonfestl/husky-cli 1.33.0 ā 1.34.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/dist/commands/biz/tickets.d.ts +7 -1
- package/dist/commands/biz/tickets.js +62 -15
- package/dist/commands/supervisor.d.ts +2 -0
- package/dist/commands/supervisor.js +528 -0
- package/dist/commands/task.js +2 -1
- package/dist/commands/youtube.js +86 -9
- package/dist/index.js +15 -6
- package/dist/lib/biz/index.d.ts +1 -0
- package/dist/lib/biz/index.js +1 -0
- package/dist/lib/biz/zendesk-proxy.d.ts +55 -0
- package/dist/lib/biz/zendesk-proxy.js +113 -0
- package/dist/lib/version-check.d.ts +4 -0
- package/dist/lib/version-check.js +112 -0
- package/package.json +1 -1
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Husky Biz Tickets Command
|
|
3
3
|
*
|
|
4
|
-
* Manages support tickets via Zendesk API
|
|
4
|
+
* Manages support tickets via Zendesk API.
|
|
5
|
+
* Supports API proxy (server-side credentials) with fallback to direct API.
|
|
6
|
+
*
|
|
7
|
+
* Proxy Support:
|
|
8
|
+
* - list, get, create, reply, search commands try proxy first
|
|
9
|
+
* - Other commands use direct API (local credentials required)
|
|
10
|
+
* - Use --no-proxy flag to skip proxy
|
|
5
11
|
*/
|
|
6
12
|
import { Command } from "commander";
|
|
7
13
|
export declare const ticketsCommand: Command;
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Husky Biz Tickets Command
|
|
3
3
|
*
|
|
4
|
-
* Manages support tickets via Zendesk API
|
|
4
|
+
* Manages support tickets via Zendesk API.
|
|
5
|
+
* Supports API proxy (server-side credentials) with fallback to direct API.
|
|
6
|
+
*
|
|
7
|
+
* Proxy Support:
|
|
8
|
+
* - list, get, create, reply, search commands try proxy first
|
|
9
|
+
* - Other commands use direct API (local credentials required)
|
|
10
|
+
* - Use --no-proxy flag to skip proxy
|
|
5
11
|
*/
|
|
6
12
|
import { Command } from "commander";
|
|
7
|
-
import { ZendeskClient } from "../../lib/biz/index.js";
|
|
13
|
+
import { ZendeskClient, tryZendeskProxy } from "../../lib/biz/index.js";
|
|
8
14
|
import { AgentBrain } from "../../lib/biz/agent-brain.js";
|
|
9
15
|
import * as fs from "fs";
|
|
10
16
|
import * as path from "path";
|
|
11
17
|
import { errorWithAutoHint } from "../../lib/error-hints.js";
|
|
18
|
+
// Helper to get client - tries proxy first for supported operations
|
|
19
|
+
async function getClient(options = {}) {
|
|
20
|
+
if (!options.noProxy) {
|
|
21
|
+
const proxy = await tryZendeskProxy();
|
|
22
|
+
if (proxy) {
|
|
23
|
+
return proxy;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return ZendeskClient.fromConfig();
|
|
27
|
+
}
|
|
28
|
+
// Type guard to check if client is proxy client
|
|
29
|
+
function isProxyClient(client) {
|
|
30
|
+
return 'isAvailable' in client;
|
|
31
|
+
}
|
|
12
32
|
export const ticketsCommand = new Command("tickets")
|
|
13
33
|
.description("Manage support tickets (Zendesk)");
|
|
14
34
|
// husky biz tickets list
|
|
@@ -18,9 +38,10 @@ ticketsCommand
|
|
|
18
38
|
.option("-s, --status <status>", "Filter by status (new, open, pending, solved, closed)")
|
|
19
39
|
.option("-l, --limit <num>", "Number of tickets", "25")
|
|
20
40
|
.option("--json", "Output as JSON")
|
|
41
|
+
.option("--no-proxy", "Skip API proxy, use direct API")
|
|
21
42
|
.action(async (options) => {
|
|
22
43
|
try {
|
|
23
|
-
const client =
|
|
44
|
+
const client = await getClient(options);
|
|
24
45
|
const tickets = await client.listTickets({
|
|
25
46
|
per_page: parseInt(options.limit, 10),
|
|
26
47
|
status: options.status,
|
|
@@ -29,7 +50,8 @@ ticketsCommand
|
|
|
29
50
|
console.log(JSON.stringify(tickets, null, 2));
|
|
30
51
|
return;
|
|
31
52
|
}
|
|
32
|
-
|
|
53
|
+
const proxyLabel = isProxyClient(client) ? " (via proxy)" : "";
|
|
54
|
+
console.log(`\n š« Tickets (${tickets.length})${proxyLabel}\n`);
|
|
33
55
|
if (tickets.length === 0) {
|
|
34
56
|
console.log(" No tickets found.");
|
|
35
57
|
return;
|
|
@@ -51,14 +73,24 @@ ticketsCommand
|
|
|
51
73
|
.command("get <id>")
|
|
52
74
|
.description("Get ticket with conversation history")
|
|
53
75
|
.option("--json", "Output as JSON")
|
|
76
|
+
.option("--no-proxy", "Skip API proxy, use direct API")
|
|
54
77
|
.action(async (id, options) => {
|
|
55
78
|
try {
|
|
56
|
-
const client =
|
|
79
|
+
const client = await getClient(options);
|
|
57
80
|
const ticketId = parseInt(id, 10);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
let ticket;
|
|
82
|
+
let comments;
|
|
83
|
+
if (isProxyClient(client)) {
|
|
84
|
+
const result = await client.getTicket(ticketId);
|
|
85
|
+
ticket = result.ticket;
|
|
86
|
+
comments = result.comments;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
[ticket, comments] = await Promise.all([
|
|
90
|
+
client.getTicket(ticketId),
|
|
91
|
+
client.getTicketComments(ticketId),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
62
94
|
if (options.json) {
|
|
63
95
|
console.log(JSON.stringify({ ticket, comments }, null, 2));
|
|
64
96
|
return;
|
|
@@ -95,11 +127,18 @@ ticketsCommand
|
|
|
95
127
|
.command("reply <id>")
|
|
96
128
|
.description("Reply to a ticket (public)")
|
|
97
129
|
.requiredOption("-m, --message <text>", "Reply message")
|
|
130
|
+
.option("--no-proxy", "Skip API proxy, use direct API")
|
|
98
131
|
.action(async (id, options) => {
|
|
99
132
|
try {
|
|
100
|
-
const client =
|
|
101
|
-
const
|
|
102
|
-
|
|
133
|
+
const client = await getClient(options);
|
|
134
|
+
const ticketId = parseInt(id, 10);
|
|
135
|
+
if (isProxyClient(client)) {
|
|
136
|
+
await client.replyToTicket(ticketId, options.message, true);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await client.addComment(ticketId, options.message, true);
|
|
140
|
+
}
|
|
141
|
+
console.log(`ā Public reply added to ticket #${id}`);
|
|
103
142
|
}
|
|
104
143
|
catch (error) {
|
|
105
144
|
console.error("Error:", error.message);
|
|
@@ -296,9 +335,10 @@ ticketsCommand
|
|
|
296
335
|
.option("-e, --email <email>", "Requester email")
|
|
297
336
|
.option("-p, --priority <priority>", "Priority (low, normal, high, urgent)")
|
|
298
337
|
.option("--json", "Output as JSON")
|
|
338
|
+
.option("--no-proxy", "Skip API proxy, use direct API")
|
|
299
339
|
.action(async (options) => {
|
|
300
340
|
try {
|
|
301
|
-
const client =
|
|
341
|
+
const client = await getClient(options);
|
|
302
342
|
const ticketData = {
|
|
303
343
|
subject: options.subject,
|
|
304
344
|
comment: { body: options.message },
|
|
@@ -309,7 +349,13 @@ ticketsCommand
|
|
|
309
349
|
if (options.priority) {
|
|
310
350
|
ticketData.priority = options.priority;
|
|
311
351
|
}
|
|
312
|
-
|
|
352
|
+
let ticket;
|
|
353
|
+
if (isProxyClient(client)) {
|
|
354
|
+
ticket = await client.createTicket(ticketData);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
ticket = await client.createTicket(ticketData);
|
|
358
|
+
}
|
|
313
359
|
if (options.json) {
|
|
314
360
|
console.log(JSON.stringify(ticket, null, 2));
|
|
315
361
|
return;
|
|
@@ -328,9 +374,10 @@ ticketsCommand
|
|
|
328
374
|
.command("search <query>")
|
|
329
375
|
.description("Search tickets (Zendesk search syntax)")
|
|
330
376
|
.option("--json", "Output as JSON")
|
|
377
|
+
.option("--no-proxy", "Skip API proxy, use direct API")
|
|
331
378
|
.action(async (query, options) => {
|
|
332
379
|
try {
|
|
333
|
-
const client =
|
|
380
|
+
const client = await getClient(options);
|
|
334
381
|
const tickets = await client.searchTickets(query);
|
|
335
382
|
if (options.json) {
|
|
336
383
|
console.log(JSON.stringify(tickets, null, 2));
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { spawn, execSync } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
export const supervisorCommand = new Command("supervisor")
|
|
8
|
+
.description("Manage Husky Supervisor Agent");
|
|
9
|
+
// supervisor start
|
|
10
|
+
supervisorCommand
|
|
11
|
+
.command("start")
|
|
12
|
+
.description("Start the supervisor agent")
|
|
13
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
14
|
+
.option("--id <id>", "Agent ID", "supervisor")
|
|
15
|
+
.option("--name <name>", "Agent display name", "Husky Supervisor")
|
|
16
|
+
.option("--vm <name>", "VM name (auto-detected if not provided)")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
if (!config.apiUrl) {
|
|
20
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log("š Starting Husky Supervisor Agent...");
|
|
24
|
+
// Set environment variables
|
|
25
|
+
const env = {
|
|
26
|
+
...process.env,
|
|
27
|
+
HUSKY_API_URL: config.apiUrl,
|
|
28
|
+
HUSKY_API_KEY: config.apiKey,
|
|
29
|
+
HUSKY_AGENT_ID: options.id,
|
|
30
|
+
HUSKY_SUPERVISOR_SESSION: options.session,
|
|
31
|
+
};
|
|
32
|
+
// Get the supervisor script path
|
|
33
|
+
const scriptPath = new URL("../scripts/supervisor-start.sh", import.meta.url).pathname;
|
|
34
|
+
try {
|
|
35
|
+
// Run the supervisor script
|
|
36
|
+
const child = spawn(scriptPath, [], {
|
|
37
|
+
env,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
});
|
|
40
|
+
child.on("close", (code) => {
|
|
41
|
+
if (code !== 0) {
|
|
42
|
+
console.error(`Supervisor script exited with code ${code}`);
|
|
43
|
+
process.exit(code);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error("Error starting supervisor:", error);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// supervisor status
|
|
53
|
+
supervisorCommand
|
|
54
|
+
.command("status")
|
|
55
|
+
.description("Check supervisor status")
|
|
56
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
try {
|
|
59
|
+
const { stdout } = await execAsync(`tmux has-session -t "${options.session}"`);
|
|
60
|
+
console.log("ā
Supervisor is running");
|
|
61
|
+
console.log(`Session: ${options.session}`);
|
|
62
|
+
// List windows
|
|
63
|
+
const { stdout: windows } = await execAsync(`tmux list-windows -t "${options.session}"`);
|
|
64
|
+
console.log("\nActive windows:");
|
|
65
|
+
console.log(windows);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.log("ā Supervisor is not running");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// supervisor stop
|
|
73
|
+
supervisorCommand
|
|
74
|
+
.command("stop")
|
|
75
|
+
.description("Stop the supervisor agent")
|
|
76
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
await execAsync(`tmux kill-session -t "${options.session}"`);
|
|
80
|
+
console.log("ā
Supervisor stopped successfully");
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.log("ā Supervisor is not running or failed to stop");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// supervisor restart
|
|
88
|
+
supervisorCommand
|
|
89
|
+
.command("restart")
|
|
90
|
+
.description("Restart the supervisor agent")
|
|
91
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
92
|
+
.action(async (options) => {
|
|
93
|
+
try {
|
|
94
|
+
console.log("š Restarting supervisor...");
|
|
95
|
+
await execAsync(`tmux kill-session -t "${options.session}"`);
|
|
96
|
+
console.log("ā
Old supervisor stopped");
|
|
97
|
+
// Start new supervisor
|
|
98
|
+
const scriptPath = new URL("../scripts/supervisor-start.sh", import.meta.url).pathname;
|
|
99
|
+
const config = getConfig();
|
|
100
|
+
const child = spawn(scriptPath, [], {
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
HUSKY_API_URL: config.apiUrl,
|
|
104
|
+
HUSKY_API_KEY: config.apiKey,
|
|
105
|
+
},
|
|
106
|
+
stdio: "inherit",
|
|
107
|
+
});
|
|
108
|
+
child.on("close", (code) => {
|
|
109
|
+
if (code !== 0) {
|
|
110
|
+
console.error(`Supervisor restart failed with code ${code}`);
|
|
111
|
+
process.exit(code);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error("Error restarting supervisor:", error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// supervisor attach
|
|
121
|
+
supervisorCommand
|
|
122
|
+
.command("attach")
|
|
123
|
+
.description("Attach to the supervisor tmux session")
|
|
124
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
125
|
+
.option("--window <name>", "Window name or number (default: supervisor)", "supervisor")
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
try {
|
|
128
|
+
const { stdout } = await execAsync(`tmux has-session -t "${options.session}"`);
|
|
129
|
+
// Attach to specific window
|
|
130
|
+
spawn("tmux", ["attach", "-t", `${options.session}:${options.window}`], {
|
|
131
|
+
stdio: "inherit",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error("ā Supervisor is not running");
|
|
136
|
+
console.log("Start it with: husky supervisor start");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// supervisor logs
|
|
141
|
+
supervisorCommand
|
|
142
|
+
.command("logs")
|
|
143
|
+
.description("Show logs from supervisor windows")
|
|
144
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
145
|
+
.option("--window <name>", "Window name (supervisor, messages, tasks, vms, heartbeat)")
|
|
146
|
+
.option("--lines <n>", "Number of lines to show", "50")
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
try {
|
|
149
|
+
await execAsync(`tmux has-session -t "${options.session}"`);
|
|
150
|
+
const window = options.window || "supervisor";
|
|
151
|
+
const lines = options.lines;
|
|
152
|
+
console.log(`š Showing last ${lines} lines from '${window}' window:\n`);
|
|
153
|
+
// Capture output from tmux pane
|
|
154
|
+
const { stdout } = await execAsync(`tmux capture-pane -t "${options.session}:${window}" -p | tail -n ${lines}`);
|
|
155
|
+
console.log(stdout);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("ā Failed to get logs");
|
|
159
|
+
if (error instanceof Error && error.message.includes("session")) {
|
|
160
|
+
console.error("Supervisor is not running");
|
|
161
|
+
}
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// supervisor message
|
|
166
|
+
supervisorCommand
|
|
167
|
+
.command("message")
|
|
168
|
+
.description("Send a message to the supervisor interface")
|
|
169
|
+
.argument("<message>", "Message to send")
|
|
170
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
171
|
+
.action(async (message, options) => {
|
|
172
|
+
try {
|
|
173
|
+
await execAsync(`tmux has-session -t "${options.session}"`);
|
|
174
|
+
// Send message to supervisor window
|
|
175
|
+
const escapedMessage = message
|
|
176
|
+
.replace(/\\/g, "\\\\")
|
|
177
|
+
.replace(/"/g, '\\"')
|
|
178
|
+
.replace(/\$/g, "\\$");
|
|
179
|
+
await execAsync(`tmux send-keys -t "${options.session}:supervisor" "${escapedMessage}" Enter`);
|
|
180
|
+
console.log("ā
Message sent to supervisor");
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error("ā Failed to send message");
|
|
184
|
+
if (error instanceof Error && error.message.includes("session")) {
|
|
185
|
+
console.error("Supervisor is not running");
|
|
186
|
+
}
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
// supervisor execute
|
|
191
|
+
supervisorCommand
|
|
192
|
+
.command("execute")
|
|
193
|
+
.description("Execute a command in the supervisor interface")
|
|
194
|
+
.argument("<command>", "Command to execute")
|
|
195
|
+
.option("--session <name>", "Tmux session name", "supervisor")
|
|
196
|
+
.option("--window <name>", "Window to execute in", "supervisor")
|
|
197
|
+
.action(async (command, options) => {
|
|
198
|
+
try {
|
|
199
|
+
await execAsync(`tmux has-session -t "${options.session}"`);
|
|
200
|
+
// Send command to specified window
|
|
201
|
+
const escapedCommand = command
|
|
202
|
+
.replace(/\\/g, "\\\\")
|
|
203
|
+
.replace(/"/g, '\\"')
|
|
204
|
+
.replace(/\$/g, "\\$");
|
|
205
|
+
await execAsync(`tmux send-keys -t "${options.session}:${options.window}" "${escapedCommand}" Enter`);
|
|
206
|
+
console.log(`ā
Command sent to ${options.window} window`);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error("ā Failed to execute command");
|
|
210
|
+
if (error instanceof Error && error.message.includes("session")) {
|
|
211
|
+
console.error("Supervisor is not running");
|
|
212
|
+
}
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// supervisor workflow
|
|
217
|
+
supervisorCommand
|
|
218
|
+
.command("workflow")
|
|
219
|
+
.description("Show supervisor workflow status and next actions")
|
|
220
|
+
.option("--json", "Output as JSON")
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
const config = getConfig();
|
|
223
|
+
if (!config.apiUrl) {
|
|
224
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
// Get task backlog
|
|
229
|
+
const tasksRes = await fetch(`${config.apiUrl}/api/tasks?status=backlog&assignee=llm&limit=10`, {
|
|
230
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
231
|
+
});
|
|
232
|
+
// Get VM status
|
|
233
|
+
const vmCommand = "gcloud compute instances list --filter='name~husky-' --format='json(name,status,zone,machineType)'";
|
|
234
|
+
const { stdout: vmOutput } = await execAsync(vmCommand);
|
|
235
|
+
const vms = JSON.parse(vmOutput || "[]");
|
|
236
|
+
// Get registered agents
|
|
237
|
+
const agentsRes = await fetch(`${config.apiUrl}/api/agents`, {
|
|
238
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
239
|
+
});
|
|
240
|
+
const tasks = tasksRes.ok ? await tasksRes.json() : { tasks: [] };
|
|
241
|
+
const agents = agentsRes.ok ? await agentsRes.json() : { agents: [] };
|
|
242
|
+
if (options.json) {
|
|
243
|
+
console.log(JSON.stringify({
|
|
244
|
+
tasks: tasks.tasks || [],
|
|
245
|
+
vms,
|
|
246
|
+
agents: agents.agents || [],
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
}, null, 2));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log("\nš Husky Supervisor Workflow Status");
|
|
252
|
+
console.log("=".repeat(50));
|
|
253
|
+
// Task status
|
|
254
|
+
console.log("\nš Task Backlog:");
|
|
255
|
+
const backlogTasks = tasks.tasks || [];
|
|
256
|
+
if (backlogTasks.length === 0) {
|
|
257
|
+
console.log(" No tasks in backlog");
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
backlogTasks.forEach((task, i) => {
|
|
261
|
+
console.log(` ${i + 1}. ${task.title} (${task.id})`);
|
|
262
|
+
console.log(` Priority: ${task.priority} | Assignee: ${task.assignee}`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// VM status
|
|
266
|
+
console.log("\nš„ļø VM Fleet Status:");
|
|
267
|
+
const runningWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "RUNNING");
|
|
268
|
+
const suspendedWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "SUSPENDED");
|
|
269
|
+
const terminatedWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "TERMINATED");
|
|
270
|
+
console.log(` Running workers: ${runningWorkers.length}`);
|
|
271
|
+
console.log(` Suspended workers: ${suspendedWorkers.length}`);
|
|
272
|
+
console.log(` Terminated workers: ${terminatedWorkers.length}`);
|
|
273
|
+
if (vms.find((vm) => vm.name.includes("supervisor"))) {
|
|
274
|
+
const supervisorVm = vms.find((vm) => vm.name.includes("supervisor"));
|
|
275
|
+
console.log(` Supervisor VM: ${supervisorVm.status}`);
|
|
276
|
+
}
|
|
277
|
+
// Agent status
|
|
278
|
+
console.log("\nš¤ Registered Agents:");
|
|
279
|
+
const agentList = agents.agents || [];
|
|
280
|
+
const onlineAgents = agentList.filter((agent) => agent.status === "online");
|
|
281
|
+
const busyAgents = agentList.filter((agent) => agent.status === "busy");
|
|
282
|
+
const offlineAgents = agentList.filter((agent) => agent.status === "offline");
|
|
283
|
+
console.log(` Online: ${onlineAgents.length}, Busy: ${busyAgents.length}, Offline: ${offlineAgents.length}`);
|
|
284
|
+
// Next actions
|
|
285
|
+
console.log("\nšÆ Suggested Actions:");
|
|
286
|
+
if (backlogTasks.length > 0 && runningWorkers.length === 0) {
|
|
287
|
+
console.log(" ⢠Start worker VM(s) to handle task backlog");
|
|
288
|
+
}
|
|
289
|
+
if (terminatedWorkers.length > 0 && backlogTasks.length > 0) {
|
|
290
|
+
console.log(" ⢠Resume terminated worker VMs");
|
|
291
|
+
}
|
|
292
|
+
if (backlogTasks.length > 0) {
|
|
293
|
+
console.log(" ⢠Assign tasks to available workers");
|
|
294
|
+
}
|
|
295
|
+
if (offlineAgents.length > 0) {
|
|
296
|
+
console.log(" ⢠Check on offline agents");
|
|
297
|
+
}
|
|
298
|
+
if (backlogTasks.length === 0 && runningWorkers.length > 0) {
|
|
299
|
+
console.log(" ⢠Suspend idle worker VMs to save costs");
|
|
300
|
+
}
|
|
301
|
+
console.log("\nQuick commands:");
|
|
302
|
+
console.log(" husky supervisor start - Start supervisor");
|
|
303
|
+
console.log(" husky supervisor attach - Attach to supervisor");
|
|
304
|
+
console.log(" husk supervisor status - Check status");
|
|
305
|
+
console.log("");
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error("Error getting workflow status:", error);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
// supervisor cycle
|
|
313
|
+
supervisorCommand
|
|
314
|
+
.command("cycle")
|
|
315
|
+
.description("Run the main supervisor loop (for automation)")
|
|
316
|
+
.option("--interval <seconds>", "Loop interval in seconds", "300") // 5 minutes default
|
|
317
|
+
.option("--once", "Run once and exit")
|
|
318
|
+
.action(async (options) => {
|
|
319
|
+
const config = getConfig();
|
|
320
|
+
if (!config.apiUrl) {
|
|
321
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
const interval = parseInt(options.interval) * 1000;
|
|
325
|
+
console.log(`š Starting supervisor cycle${options.once ? ' (once)' : ` (every ${options.interval}s)`}...`);
|
|
326
|
+
const runCycle = async () => {
|
|
327
|
+
try {
|
|
328
|
+
console.log(`\n[${new Date().toLocaleTimeString()}] Running supervisor cycle...`);
|
|
329
|
+
// Step 1: Check for new tasks
|
|
330
|
+
const tasksRes = await fetch(`${config.apiUrl}/api/tasks?status=backlog&assignee=llm&limit=5`, {
|
|
331
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
332
|
+
});
|
|
333
|
+
if (tasksRes.ok) {
|
|
334
|
+
const tasksData = await tasksRes.json();
|
|
335
|
+
const tasks = tasksData.tasks || [];
|
|
336
|
+
if (tasks.length > 0) {
|
|
337
|
+
console.log(` Found ${tasks.length} tasks in backlog`);
|
|
338
|
+
// Step 2: Check available worker VMs
|
|
339
|
+
const { stdout: vmOutput } = await execAsync("gcloud compute instances list --filter='name~husky-worker' --format='json(name,status,zone)'");
|
|
340
|
+
const vms = JSON.parse(vmOutput || "[]");
|
|
341
|
+
const runningWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "RUNNING");
|
|
342
|
+
if (runningWorkers.length === 0) {
|
|
343
|
+
console.log(" No worker VMs running, starting one...");
|
|
344
|
+
// This would need to be implemented based on your GCP setup
|
|
345
|
+
console.log(" TODO: Implement VM startup logic");
|
|
346
|
+
}
|
|
347
|
+
// Step 3: Assign tasks to workers
|
|
348
|
+
tasks.forEach((task) => {
|
|
349
|
+
console.log(` Task ${task.id}: ${task.title}`);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
console.log(" No tasks in backlog");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
console.log(`[${new Date().toLocaleTimeString()}] Cycle completed`);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
console.error(`[${new Date().toLocaleTimeString()}] Cycle error:`, error);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
if (options.once) {
|
|
363
|
+
await runCycle();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
await runCycle();
|
|
367
|
+
setInterval(runCycle, interval);
|
|
368
|
+
console.log(`\nSupervisor cycle running. Press Ctrl+C to stop.`);
|
|
369
|
+
// Keep process alive
|
|
370
|
+
process.on("SIGINT", () => {
|
|
371
|
+
console.log("\nš Stopping supervisor cycle...");
|
|
372
|
+
process.exit(0);
|
|
373
|
+
});
|
|
374
|
+
await new Promise(() => { });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
function checkGitHubPR(branch) {
|
|
378
|
+
try {
|
|
379
|
+
const result = execSync(`gh pr view "${branch}" --json url,mergedAt,state -q '{"url":.url,"mergedAt":.mergedAt,"state":.state}'`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
380
|
+
const data = JSON.parse(result.trim());
|
|
381
|
+
return {
|
|
382
|
+
exists: true,
|
|
383
|
+
merged: data.mergedAt !== null && data.mergedAt !== "",
|
|
384
|
+
url: data.url,
|
|
385
|
+
state: data.state,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return { exists: false, merged: false };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
supervisorCommand
|
|
393
|
+
.command("auto-track")
|
|
394
|
+
.description("Automatic task status tracking based on GitHub PR state")
|
|
395
|
+
.option("--interval <minutes>", "Check interval in minutes", "5")
|
|
396
|
+
.option("--dry-run", "Show what would be updated without making changes")
|
|
397
|
+
.option("--once", "Run once and exit")
|
|
398
|
+
.option("--stale-hours <hours>", "Hours before task is considered stale", "24")
|
|
399
|
+
.option("--alert", "Send alerts for stale tasks to Google Chat")
|
|
400
|
+
.action(async (options) => {
|
|
401
|
+
const config = getConfig();
|
|
402
|
+
if (!config.apiUrl) {
|
|
403
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const intervalMs = parseInt(options.interval) * 60 * 1000;
|
|
407
|
+
const staleMs = parseInt(options.staleHours) * 60 * 60 * 1000;
|
|
408
|
+
const headers = {};
|
|
409
|
+
if (config.apiKey)
|
|
410
|
+
headers["x-api-key"] = config.apiKey;
|
|
411
|
+
console.log(`š Starting auto-tracking loop (interval: ${options.interval}m, stale: ${options.staleHours}h)`);
|
|
412
|
+
if (options.dryRun)
|
|
413
|
+
console.log(" DRY RUN MODE - no changes will be made\n");
|
|
414
|
+
async function checkAndUpdate() {
|
|
415
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
416
|
+
console.log(`\n[${timestamp}] Checking task statuses...`);
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch(`${config.apiUrl}/api/tasks?status=in_progress,review&limit=50`, { headers });
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
console.error(` ā Failed to fetch tasks: ${res.status}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const data = await res.json();
|
|
424
|
+
const tasks = data.tasks || [];
|
|
425
|
+
if (tasks.length === 0) {
|
|
426
|
+
console.log(" No in_progress or review tasks found");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
console.log(` Found ${tasks.length} active tasks`);
|
|
430
|
+
const staleTasks = [];
|
|
431
|
+
const now = Date.now();
|
|
432
|
+
for (const task of tasks) {
|
|
433
|
+
const taskAge = now - new Date(task.updatedAt).getTime();
|
|
434
|
+
const isStale = taskAge > staleMs;
|
|
435
|
+
if (isStale) {
|
|
436
|
+
staleTasks.push(task);
|
|
437
|
+
}
|
|
438
|
+
if (!task.worktreeBranch) {
|
|
439
|
+
console.log(` š ${task.id}: No branch linked (${task.status})${isStale ? " ā ļø STALE" : ""}`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const prStatus = checkGitHubPR(task.worktreeBranch);
|
|
443
|
+
if (prStatus.merged && task.status !== "done") {
|
|
444
|
+
console.log(` ā
${task.id}: PR merged ā marking done`);
|
|
445
|
+
if (!options.dryRun) {
|
|
446
|
+
const updateRes = await fetch(`${config.apiUrl}/api/tasks/${task.id}`, {
|
|
447
|
+
method: "PATCH",
|
|
448
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
449
|
+
body: JSON.stringify({
|
|
450
|
+
status: "done",
|
|
451
|
+
statusMessage: "PR merged - auto-tracked by supervisor",
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
if (!updateRes.ok) {
|
|
455
|
+
console.error(` ā Failed to update task: ${updateRes.status}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else if (prStatus.exists && task.status === "in_progress") {
|
|
460
|
+
console.log(` š ${task.id}: PR exists (${prStatus.state}) ā marking review`);
|
|
461
|
+
if (!options.dryRun) {
|
|
462
|
+
const updateRes = await fetch(`${config.apiUrl}/api/tasks/${task.id}`, {
|
|
463
|
+
method: "PATCH",
|
|
464
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
465
|
+
body: JSON.stringify({
|
|
466
|
+
status: "review",
|
|
467
|
+
prUrl: prStatus.url,
|
|
468
|
+
statusMessage: "PR opened - auto-tracked by supervisor",
|
|
469
|
+
}),
|
|
470
|
+
});
|
|
471
|
+
if (!updateRes.ok) {
|
|
472
|
+
console.error(` ā Failed to update task: ${updateRes.status}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (!prStatus.exists && task.status === "in_progress") {
|
|
477
|
+
console.log(` šØ ${task.id}: In progress, no PR yet${isStale ? " ā ļø STALE" : ""}`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
console.log(` ā¹ļø ${task.id}: ${task.status} (PR: ${prStatus.exists ? prStatus.state : "none"})${isStale ? " ā ļø STALE" : ""}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (staleTasks.length > 0) {
|
|
484
|
+
console.log(`\n ā ļø Found ${staleTasks.length} stale tasks (>${options.staleHours}h inactive):`);
|
|
485
|
+
staleTasks.forEach((t) => {
|
|
486
|
+
const hours = Math.round((now - new Date(t.updatedAt).getTime()) / (1000 * 60 * 60));
|
|
487
|
+
console.log(` - ${t.title} (${t.id}) - ${hours}h inactive`);
|
|
488
|
+
});
|
|
489
|
+
if (options.alert && !options.dryRun) {
|
|
490
|
+
const alertMessage = `ā ļø **Stale Task Alert**\n\n${staleTasks.length} tasks inactive for >${options.staleHours}h:\n\n${staleTasks.map((t) => `- ${t.title} (${t.id})`).join("\n")}\n\nPlease review and update status.`;
|
|
491
|
+
try {
|
|
492
|
+
const chatRes = await fetch(`${config.apiUrl}/api/google-chat/send`, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
text: alertMessage,
|
|
497
|
+
spaceName: process.env.GOOGLE_CHAT_DM_SPACE,
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
if (chatRes.ok) {
|
|
501
|
+
console.log(" š£ Stale task alert sent to Google Chat");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
console.error(" ā Failed to send alert:", error);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(`[${new Date().toLocaleTimeString()}] Check completed`);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
console.error(`[${timestamp}] Error:`, error);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (options.once) {
|
|
516
|
+
await checkAndUpdate();
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
await checkAndUpdate();
|
|
520
|
+
setInterval(checkAndUpdate, intervalMs);
|
|
521
|
+
console.log(`\nAuto-tracking running. Press Ctrl+C to stop.`);
|
|
522
|
+
process.on("SIGINT", () => {
|
|
523
|
+
console.log("\nš Stopping auto-tracking...");
|
|
524
|
+
process.exit(0);
|
|
525
|
+
});
|
|
526
|
+
await new Promise(() => { });
|
|
527
|
+
}
|
|
528
|
+
});
|
package/dist/commands/task.js
CHANGED
|
@@ -154,7 +154,8 @@ taskCommand
|
|
|
154
154
|
// Note: We don't pass projectId to API to avoid Firestore index requirement
|
|
155
155
|
// Instead, we filter client-side which is fine for reasonable task counts
|
|
156
156
|
const api = getApiClient();
|
|
157
|
-
|
|
157
|
+
const response = await api.get(url.pathname + url.search);
|
|
158
|
+
let tasks = response.tasks;
|
|
158
159
|
// Client-side filtering by projectId (avoids Firestore composite index)
|
|
159
160
|
if (filterProjectId) {
|
|
160
161
|
tasks = tasks.filter(t => t.projectId === filterProjectId);
|
package/dist/commands/youtube.js
CHANGED
|
@@ -2,24 +2,47 @@ import { Command } from "commander";
|
|
|
2
2
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
3
3
|
import { getConfig } from "./config.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { apiRequest } from "../lib/api-client.js";
|
|
5
6
|
// Input validation schemas
|
|
6
7
|
const YouTubeUrlSchema = z.string().min(1, "YouTube URL cannot be empty");
|
|
7
8
|
const YouTubeOptionsSchema = z.object({
|
|
8
9
|
json: z.boolean().optional(),
|
|
9
|
-
prompt: z.string().max(10000, "Custom prompt too long (max 10000 characters)").optional()
|
|
10
|
+
prompt: z.string().max(10000, "Custom prompt too long (max 10000 characters)").optional(),
|
|
11
|
+
language: z.enum(["en", "de"]).optional(),
|
|
12
|
+
useProxy: z.boolean().optional(),
|
|
10
13
|
});
|
|
11
14
|
export const youtubeCommand = new Command("youtube")
|
|
12
15
|
.description("YouTube video summarization using Gemini AI")
|
|
13
16
|
.argument("<url>", "YouTube video URL")
|
|
14
17
|
.option("--json", "Output as JSON")
|
|
15
18
|
.option("--prompt <prompt>", "Custom summarization prompt")
|
|
19
|
+
.option("--language <lang>", "Summary language (en, de)", "de")
|
|
20
|
+
.option("--no-proxy", "Skip API proxy, use direct Gemini API")
|
|
16
21
|
.action(async (url, options) => {
|
|
17
22
|
try {
|
|
18
23
|
// Validate URL
|
|
19
24
|
const validatedUrl = YouTubeUrlSchema.parse(url);
|
|
20
25
|
// Validate options
|
|
21
26
|
const validatedOptions = YouTubeOptionsSchema.parse(options);
|
|
22
|
-
|
|
27
|
+
// Try proxy first (if not disabled)
|
|
28
|
+
if (validatedOptions.useProxy !== false) {
|
|
29
|
+
try {
|
|
30
|
+
await summarizeViaProxy(validatedUrl, validatedOptions);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const err = error;
|
|
35
|
+
// Only fall back on auth/access errors, not on video errors
|
|
36
|
+
if (!err.message.includes("403") && !err.message.includes("401") && !err.message.includes("Forbidden")) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
if (!validatedOptions.json) {
|
|
40
|
+
console.log("ā ļø Proxy unavailable, falling back to direct API...\n");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Fall back to direct API
|
|
45
|
+
await summarizeVideoDirect(validatedUrl, validatedOptions);
|
|
23
46
|
}
|
|
24
47
|
catch (error) {
|
|
25
48
|
if (error instanceof z.ZodError) {
|
|
@@ -58,13 +81,50 @@ function extractVideoId(url) {
|
|
|
58
81
|
throw new Error(`Invalid YouTube URL: ${url}`);
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
|
-
* Summarize
|
|
84
|
+
* Summarize via API proxy (uses server-side credentials)
|
|
62
85
|
*/
|
|
63
|
-
async function
|
|
86
|
+
async function summarizeViaProxy(url, options) {
|
|
64
87
|
const videoId = extractVideoId(url);
|
|
65
88
|
const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
if (!options.json) {
|
|
90
|
+
console.log(`š¹ Video: ${videoId}`);
|
|
91
|
+
console.log(`š URL: ${fullUrl}`);
|
|
92
|
+
console.log(`š Using API proxy...\n`);
|
|
93
|
+
}
|
|
94
|
+
const result = await apiRequest("/api/proxy/youtube/summarize", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: {
|
|
97
|
+
url: fullUrl,
|
|
98
|
+
prompt: options.prompt,
|
|
99
|
+
language: options.language || "de",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify({
|
|
104
|
+
videoId: result.videoId,
|
|
105
|
+
url: result.url,
|
|
106
|
+
summary: result.summary,
|
|
107
|
+
language: result.language,
|
|
108
|
+
}, null, 2));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log('š Zusammenfassung:');
|
|
112
|
+
console.log('='.repeat(70));
|
|
113
|
+
console.log(result.summary);
|
|
114
|
+
console.log('='.repeat(70));
|
|
115
|
+
console.log(`\nā URL: ${result.url}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Summarize YouTube video using direct Gemini API (fallback)
|
|
120
|
+
*/
|
|
121
|
+
async function summarizeVideoDirect(url, options) {
|
|
122
|
+
const videoId = extractVideoId(url);
|
|
123
|
+
const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
124
|
+
if (!options.json) {
|
|
125
|
+
console.log(`š¹ Video: ${videoId}`);
|
|
126
|
+
console.log(`š URL: ${fullUrl}\n`);
|
|
127
|
+
}
|
|
68
128
|
// Get Gemini API key
|
|
69
129
|
// Priority: Standard Gemini API (has 3.0) > Vertex AI (has 2.5 Pro) > Config
|
|
70
130
|
const config = getConfig();
|
|
@@ -92,12 +152,29 @@ async function summarizeVideo(url, options) {
|
|
|
92
152
|
// Use Gemini 3.0 for standard API, 2.5 Pro for Vertex AI
|
|
93
153
|
const modelName = isVertex ? "gemini-2.5-pro" : "gemini-3.0-flash";
|
|
94
154
|
const modelLabel = isVertex ? "Gemini 2.5 Pro (Vertex AI)" : "Gemini 3.0 Flash";
|
|
95
|
-
|
|
155
|
+
if (!options.json) {
|
|
156
|
+
console.log(`š¤ Analyzing with ${modelLabel}...\n`);
|
|
157
|
+
}
|
|
96
158
|
// Initialize Gemini
|
|
97
159
|
const genAI = new GoogleGenerativeAI(geminiApiKey);
|
|
98
160
|
const model = genAI.getGenerativeModel({ model: modelName });
|
|
99
161
|
// Custom prompt or default
|
|
100
|
-
const prompt = options.prompt || `
|
|
162
|
+
const prompt = options.prompt || (options.language === "en" ? `
|
|
163
|
+
Create a structured summary of this YouTube video.
|
|
164
|
+
|
|
165
|
+
Format:
|
|
166
|
+
## Main Topics
|
|
167
|
+
[3-5 key points]
|
|
168
|
+
|
|
169
|
+
## Key Insights
|
|
170
|
+
[Most important learnings]
|
|
171
|
+
|
|
172
|
+
## Summary
|
|
173
|
+
[1-2 paragraph overview]
|
|
174
|
+
|
|
175
|
+
## Keywords
|
|
176
|
+
[Relevant keywords, comma-separated]
|
|
177
|
+
`.trim() : `
|
|
101
178
|
Erstelle eine strukturierte Zusammenfassung dieses YouTube Videos.
|
|
102
179
|
|
|
103
180
|
Format:
|
|
@@ -112,7 +189,7 @@ Format:
|
|
|
112
189
|
|
|
113
190
|
## Keywords
|
|
114
191
|
[Relevante Keywords kommagetrennt]
|
|
115
|
-
`.trim();
|
|
192
|
+
`.trim());
|
|
116
193
|
// Gemini can directly analyze YouTube URLs
|
|
117
194
|
let summary;
|
|
118
195
|
try {
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,8 @@ import { authCommand } from "./commands/auth.js";
|
|
|
38
38
|
import { businessCommand } from "./commands/business.js";
|
|
39
39
|
import { planCommand } from "./commands/plan.js";
|
|
40
40
|
import { diagramsCommand } from "./commands/diagrams.js";
|
|
41
|
+
import { supervisorCommand } from "./commands/supervisor.js";
|
|
42
|
+
import { checkVersion } from "./lib/version-check.js";
|
|
41
43
|
// Read version from package.json
|
|
42
44
|
const require = createRequire(import.meta.url);
|
|
43
45
|
const packageJson = require("../package.json");
|
|
@@ -83,16 +85,23 @@ program.addCommand(authCommand);
|
|
|
83
85
|
program.addCommand(businessCommand);
|
|
84
86
|
program.addCommand(planCommand);
|
|
85
87
|
program.addCommand(diagramsCommand);
|
|
88
|
+
program.addCommand(supervisorCommand);
|
|
86
89
|
// Handle --llm flag specially
|
|
87
90
|
if (process.argv.includes("--llm")) {
|
|
88
91
|
printLLMContext();
|
|
89
92
|
process.exit(0);
|
|
90
93
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const skipVersionCheck = ["--version", "-V", "--help", "-h", "completion"].some((flag) => process.argv.includes(flag));
|
|
95
|
+
async function main() {
|
|
96
|
+
if (!skipVersionCheck) {
|
|
97
|
+
await checkVersion({ silent: process.env.HUSKY_SKIP_VERSION_CHECK === "1" });
|
|
98
|
+
}
|
|
99
|
+
if (process.argv.length <= 2) {
|
|
100
|
+
runInteractiveMode();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
program.parse();
|
|
104
|
+
}
|
|
97
105
|
}
|
|
106
|
+
main();
|
|
98
107
|
// trigger CI
|
package/dist/lib/biz/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { BillbeeClient } from './billbee.js';
|
|
5
5
|
export { ZendeskClient } from './zendesk.js';
|
|
6
|
+
export { ZendeskProxyClient, getZendeskProxyClient, tryZendeskProxy } from './zendesk-proxy.js';
|
|
6
7
|
export { SeaTableClient } from './seatable.js';
|
|
7
8
|
export { QdrantClient } from './qdrant.js';
|
|
8
9
|
export { EmbeddingService, EMBEDDING_MODELS } from './embeddings.js';
|
package/dist/lib/biz/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { BillbeeClient } from './billbee.js';
|
|
5
5
|
export { ZendeskClient } from './zendesk.js';
|
|
6
|
+
export { ZendeskProxyClient, getZendeskProxyClient, tryZendeskProxy } from './zendesk-proxy.js';
|
|
6
7
|
export { SeaTableClient } from './seatable.js';
|
|
7
8
|
export { QdrantClient } from './qdrant.js';
|
|
8
9
|
export { EmbeddingService, EMBEDDING_MODELS } from './embeddings.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zendesk Proxy Client
|
|
3
|
+
*
|
|
4
|
+
* Wraps API proxy calls for Zendesk operations.
|
|
5
|
+
* This client uses server-side credentials via the Husky API proxy.
|
|
6
|
+
*/
|
|
7
|
+
import type { ZendeskTicket, ZendeskUser, TicketComment, ZendeskMacro } from './zendesk-types.js';
|
|
8
|
+
export declare class ZendeskProxyClient {
|
|
9
|
+
private available;
|
|
10
|
+
/**
|
|
11
|
+
* Check if proxy is available (has permission)
|
|
12
|
+
*/
|
|
13
|
+
isAvailable(): Promise<boolean>;
|
|
14
|
+
listTickets(params?: {
|
|
15
|
+
status?: string;
|
|
16
|
+
per_page?: number;
|
|
17
|
+
page?: number;
|
|
18
|
+
}): Promise<ZendeskTicket[]>;
|
|
19
|
+
getTicket(ticketId: number): Promise<{
|
|
20
|
+
ticket: ZendeskTicket;
|
|
21
|
+
comments: TicketComment[];
|
|
22
|
+
}>;
|
|
23
|
+
createTicket(data: {
|
|
24
|
+
subject: string;
|
|
25
|
+
comment: {
|
|
26
|
+
body: string;
|
|
27
|
+
};
|
|
28
|
+
requester?: {
|
|
29
|
+
email: string;
|
|
30
|
+
};
|
|
31
|
+
priority?: string;
|
|
32
|
+
tags?: string[];
|
|
33
|
+
}): Promise<ZendeskTicket>;
|
|
34
|
+
updateTicket(ticketId: number, data: {
|
|
35
|
+
status?: string;
|
|
36
|
+
priority?: string;
|
|
37
|
+
assignee_id?: number;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
custom_fields?: Array<{
|
|
40
|
+
id: number;
|
|
41
|
+
value: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
}): Promise<ZendeskTicket>;
|
|
44
|
+
replyToTicket(ticketId: number, body: string, isPublic?: boolean): Promise<ZendeskTicket>;
|
|
45
|
+
searchTickets(query: string): Promise<ZendeskTicket[]>;
|
|
46
|
+
listUsers(): Promise<ZendeskUser[]>;
|
|
47
|
+
listMacros(): Promise<ZendeskMacro[]>;
|
|
48
|
+
}
|
|
49
|
+
export declare function getZendeskProxyClient(): ZendeskProxyClient;
|
|
50
|
+
/**
|
|
51
|
+
* Try to use proxy, return null if unavailable.
|
|
52
|
+
* Useful for checking if proxy should be used.
|
|
53
|
+
*/
|
|
54
|
+
export declare function tryZendeskProxy(): Promise<ZendeskProxyClient | null>;
|
|
55
|
+
export default ZendeskProxyClient;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zendesk Proxy Client
|
|
3
|
+
*
|
|
4
|
+
* Wraps API proxy calls for Zendesk operations.
|
|
5
|
+
* This client uses server-side credentials via the Husky API proxy.
|
|
6
|
+
*/
|
|
7
|
+
import { apiRequest } from '../api-client.js';
|
|
8
|
+
export class ZendeskProxyClient {
|
|
9
|
+
available = null;
|
|
10
|
+
/**
|
|
11
|
+
* Check if proxy is available (has permission)
|
|
12
|
+
*/
|
|
13
|
+
async isAvailable() {
|
|
14
|
+
if (this.available !== null) {
|
|
15
|
+
return this.available;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await apiRequest('/api/proxy/zendesk/users', {
|
|
19
|
+
method: 'GET',
|
|
20
|
+
});
|
|
21
|
+
this.available = true;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const err = error;
|
|
26
|
+
if (err.message.includes('403') || err.message.includes('401') || err.message.includes('Forbidden')) {
|
|
27
|
+
this.available = false;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Network error - treat as unavailable
|
|
31
|
+
this.available = false;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// =========================================================================
|
|
36
|
+
// TICKETS
|
|
37
|
+
// =========================================================================
|
|
38
|
+
async listTickets(params) {
|
|
39
|
+
const query = new URLSearchParams();
|
|
40
|
+
if (params?.status)
|
|
41
|
+
query.set('status', params.status);
|
|
42
|
+
if (params?.per_page)
|
|
43
|
+
query.set('per_page', String(params.per_page));
|
|
44
|
+
if (params?.page)
|
|
45
|
+
query.set('page', String(params.page));
|
|
46
|
+
const queryString = query.toString();
|
|
47
|
+
const endpoint = `/api/proxy/zendesk/tickets${queryString ? `?${queryString}` : ''}`;
|
|
48
|
+
const response = await apiRequest(endpoint);
|
|
49
|
+
return response.tickets;
|
|
50
|
+
}
|
|
51
|
+
async getTicket(ticketId) {
|
|
52
|
+
const response = await apiRequest(`/api/proxy/zendesk/tickets/${ticketId}`);
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
async createTicket(data) {
|
|
56
|
+
const response = await apiRequest('/api/proxy/zendesk/tickets', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: data,
|
|
59
|
+
});
|
|
60
|
+
return response.ticket;
|
|
61
|
+
}
|
|
62
|
+
async updateTicket(ticketId, data) {
|
|
63
|
+
const response = await apiRequest(`/api/proxy/zendesk/tickets/${ticketId}`, {
|
|
64
|
+
method: 'PATCH',
|
|
65
|
+
body: data,
|
|
66
|
+
});
|
|
67
|
+
return response.ticket;
|
|
68
|
+
}
|
|
69
|
+
async replyToTicket(ticketId, body, isPublic = true) {
|
|
70
|
+
const response = await apiRequest(`/api/proxy/zendesk/tickets/${ticketId}/reply`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: { body, public: isPublic },
|
|
73
|
+
});
|
|
74
|
+
return response.ticket;
|
|
75
|
+
}
|
|
76
|
+
async searchTickets(query) {
|
|
77
|
+
const params = new URLSearchParams({ query });
|
|
78
|
+
const response = await apiRequest(`/api/proxy/zendesk/search?${params}`);
|
|
79
|
+
return response.tickets;
|
|
80
|
+
}
|
|
81
|
+
// =========================================================================
|
|
82
|
+
// USERS
|
|
83
|
+
// =========================================================================
|
|
84
|
+
async listUsers() {
|
|
85
|
+
const response = await apiRequest('/api/proxy/zendesk/users');
|
|
86
|
+
return response.users;
|
|
87
|
+
}
|
|
88
|
+
// =========================================================================
|
|
89
|
+
// MACROS
|
|
90
|
+
// =========================================================================
|
|
91
|
+
async listMacros() {
|
|
92
|
+
const response = await apiRequest('/api/proxy/zendesk/macros');
|
|
93
|
+
return response.macros;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Singleton instance
|
|
97
|
+
let proxyClient = null;
|
|
98
|
+
export function getZendeskProxyClient() {
|
|
99
|
+
if (!proxyClient) {
|
|
100
|
+
proxyClient = new ZendeskProxyClient();
|
|
101
|
+
}
|
|
102
|
+
return proxyClient;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Try to use proxy, return null if unavailable.
|
|
106
|
+
* Useful for checking if proxy should be used.
|
|
107
|
+
*/
|
|
108
|
+
export async function tryZendeskProxy() {
|
|
109
|
+
const client = getZendeskProxyClient();
|
|
110
|
+
const available = await client.isAvailable();
|
|
111
|
+
return available ? client : null;
|
|
112
|
+
}
|
|
113
|
+
export default ZendeskProxyClient;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { getConfig } from "../commands/config.js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const packageJson = require("../../package.json");
|
|
8
|
+
const CACHE_FILE = path.join(os.homedir(), ".husky", "version-cache.json");
|
|
9
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
function readCache() {
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(CACHE_FILE))
|
|
13
|
+
return null;
|
|
14
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
15
|
+
const checkedAt = new Date(data.checkedAt).getTime();
|
|
16
|
+
if (Date.now() - checkedAt > CACHE_TTL_MS)
|
|
17
|
+
return null;
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeCache(cache) {
|
|
25
|
+
try {
|
|
26
|
+
const dir = path.dirname(CACHE_FILE);
|
|
27
|
+
if (!fs.existsSync(dir))
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore cache write errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function fetchLatestVersion() {
|
|
36
|
+
try {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
39
|
+
const res = await fetch("https://registry.npmjs.org/@simonfestl/husky-cli/latest", { signal: controller.signal });
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return null;
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data.version || null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function fetchMinApiVersion() {
|
|
51
|
+
try {
|
|
52
|
+
const config = getConfig();
|
|
53
|
+
if (!config.apiUrl)
|
|
54
|
+
return null;
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
57
|
+
const res = await fetch(`${config.apiUrl}/api/health`, {
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
const minVersion = res.headers.get("X-Min-CLI-Version");
|
|
62
|
+
return minVersion || null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function compareVersions(a, b) {
|
|
69
|
+
const partsA = a.split(".").map(Number);
|
|
70
|
+
const partsB = b.split(".").map(Number);
|
|
71
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
72
|
+
const numA = partsA[i] || 0;
|
|
73
|
+
const numB = partsB[i] || 0;
|
|
74
|
+
if (numA < numB)
|
|
75
|
+
return -1;
|
|
76
|
+
if (numA > numB)
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
export async function checkVersion(options = {}) {
|
|
82
|
+
const currentVersion = packageJson.version;
|
|
83
|
+
let cache = readCache();
|
|
84
|
+
if (!cache) {
|
|
85
|
+
const [latestVersion, minApiVersion] = await Promise.all([
|
|
86
|
+
fetchLatestVersion(),
|
|
87
|
+
fetchMinApiVersion(),
|
|
88
|
+
]);
|
|
89
|
+
if (latestVersion) {
|
|
90
|
+
cache = {
|
|
91
|
+
latestVersion,
|
|
92
|
+
checkedAt: new Date().toISOString(),
|
|
93
|
+
minApiVersion: minApiVersion || undefined,
|
|
94
|
+
};
|
|
95
|
+
writeCache(cache);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!cache)
|
|
99
|
+
return;
|
|
100
|
+
if (cache.minApiVersion && compareVersions(currentVersion, cache.minApiVersion) < 0) {
|
|
101
|
+
console.error(`\nā CLI version ${currentVersion} is below minimum required (${cache.minApiVersion})`);
|
|
102
|
+
console.error(` Run: sudo npm install -g @simonfestl/husky-cli@latest\n`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (!options.silent && compareVersions(currentVersion, cache.latestVersion) < 0) {
|
|
106
|
+
console.warn(`\nā ļø CLI outdated: ${currentVersion} ā ${cache.latestVersion}`);
|
|
107
|
+
console.warn(` Run: sudo npm install -g @simonfestl/husky-cli@latest\n`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function getCurrentVersion() {
|
|
111
|
+
return packageJson.version;
|
|
112
|
+
}
|