@simonfestl/husky-cli 1.32.0 ā 1.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/biz/tickets.d.ts +7 -1
- package/dist/commands/biz/tickets.js +62 -15
- package/dist/commands/project.js +177 -1
- package/dist/commands/task.js +2 -1
- package/dist/commands/youtube.js +86 -9
- package/dist/index.js +13 -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));
|
package/dist/commands/project.js
CHANGED
|
@@ -261,6 +261,154 @@ projectCommand
|
|
|
261
261
|
process.exit(1);
|
|
262
262
|
}
|
|
263
263
|
});
|
|
264
|
+
projectCommand
|
|
265
|
+
.command("list-configs")
|
|
266
|
+
.description("List all project configurations")
|
|
267
|
+
.option("--json", "Output as JSON")
|
|
268
|
+
.action(async (options) => {
|
|
269
|
+
const config = ensureConfig();
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch(`${config.apiUrl}/api/project-configs`, {
|
|
272
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
273
|
+
});
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
throw new Error(`API error: ${res.status}`);
|
|
276
|
+
}
|
|
277
|
+
const configs = await res.json();
|
|
278
|
+
if (options.json) {
|
|
279
|
+
console.log(JSON.stringify(configs, null, 2));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
printProjectConfigs(configs);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error("Error fetching project configs:", error);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
projectCommand
|
|
291
|
+
.command("show-config <repo>")
|
|
292
|
+
.description("Show configuration for a repository (owner/repo)")
|
|
293
|
+
.option("--json", "Output as JSON")
|
|
294
|
+
.action(async (repo, options) => {
|
|
295
|
+
const config = ensureConfig();
|
|
296
|
+
try {
|
|
297
|
+
const url = new URL("/api/project-configs", config.apiUrl);
|
|
298
|
+
url.searchParams.set("repo", repo);
|
|
299
|
+
const res = await fetch(url.toString(), {
|
|
300
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
301
|
+
});
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
if (res.status === 404) {
|
|
304
|
+
console.error(`Error: No configuration found for repository ${repo}`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.error(`Error: API returned ${res.status}`);
|
|
308
|
+
}
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
const projectConfig = await res.json();
|
|
312
|
+
if (options.json) {
|
|
313
|
+
console.log(JSON.stringify(projectConfig, null, 2));
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
printProjectConfigDetail(projectConfig);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
console.error("Error fetching project config:", error);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
projectCommand
|
|
325
|
+
.command("enable-pr-agent <repo>")
|
|
326
|
+
.description("Enable PR Agent for a repository (owner/repo)")
|
|
327
|
+
.option("--json", "Output as JSON")
|
|
328
|
+
.action(async (repo, options) => {
|
|
329
|
+
const config = ensureConfig();
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(`${config.apiUrl}/api/project-configs`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: {
|
|
334
|
+
"Content-Type": "application/json",
|
|
335
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
repo,
|
|
339
|
+
prAgentEnabled: true,
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
if (!res.ok) {
|
|
343
|
+
const errorData = await res.json().catch(() => ({}));
|
|
344
|
+
throw new Error(errorData.error || `API error: ${res.status}`);
|
|
345
|
+
}
|
|
346
|
+
const projectConfig = await res.json();
|
|
347
|
+
if (options.json) {
|
|
348
|
+
console.log(JSON.stringify(projectConfig, null, 2));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.log(`ā PR Agent enabled for ${repo}`);
|
|
352
|
+
console.log(` Repository: ${projectConfig.repo}`);
|
|
353
|
+
console.log(` PR Agent: ${projectConfig.prAgentEnabled ? "Enabled" : "Disabled"}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error("Error enabling PR Agent:", error);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
projectCommand
|
|
362
|
+
.command("disable-pr-agent <repo>")
|
|
363
|
+
.description("Disable PR Agent for a repository (owner/repo)")
|
|
364
|
+
.option("--json", "Output as JSON")
|
|
365
|
+
.action(async (repo, options) => {
|
|
366
|
+
const config = ensureConfig();
|
|
367
|
+
try {
|
|
368
|
+
const url = new URL("/api/project-configs", config.apiUrl);
|
|
369
|
+
url.searchParams.set("repo", repo);
|
|
370
|
+
const getRes = await fetch(url.toString(), {
|
|
371
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
372
|
+
});
|
|
373
|
+
if (!getRes.ok) {
|
|
374
|
+
if (getRes.status === 404) {
|
|
375
|
+
console.error(`Error: No configuration found for repository ${repo}`);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.error(`Error: API returned ${getRes.status}`);
|
|
379
|
+
}
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const projectConfig = await getRes.json();
|
|
383
|
+
const updateRes = await fetch(`${config.apiUrl}/api/project-configs/${projectConfig.id}`, {
|
|
384
|
+
method: "PUT",
|
|
385
|
+
headers: {
|
|
386
|
+
"Content-Type": "application/json",
|
|
387
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
prAgentEnabled: false,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
if (!updateRes.ok) {
|
|
394
|
+
const errorData = await updateRes.json().catch(() => ({}));
|
|
395
|
+
throw new Error(errorData.error || `API error: ${updateRes.status}`);
|
|
396
|
+
}
|
|
397
|
+
const updatedConfig = await updateRes.json();
|
|
398
|
+
if (options.json) {
|
|
399
|
+
console.log(JSON.stringify(updatedConfig, null, 2));
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log(`ā PR Agent disabled for ${repo}`);
|
|
403
|
+
console.log(` Repository: ${updatedConfig.repo}`);
|
|
404
|
+
console.log(` PR Agent: ${updatedConfig.prAgentEnabled ? "Enabled" : "Disabled"}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.error("Error disabling PR Agent:", error);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
264
412
|
// ============================================
|
|
265
413
|
// KNOWLEDGE MANAGEMENT COMMANDS
|
|
266
414
|
// ============================================
|
|
@@ -459,7 +607,6 @@ function printKnowledgeList(projectId, knowledge) {
|
|
|
459
607
|
const truncatedTitle = entry.title.length > 33 ? entry.title.substring(0, 30) + "..." : entry.title;
|
|
460
608
|
console.log(` ${entry.id.padEnd(24)} [${categoryConfig.icon}] ${categoryConfig.label.padEnd(10)} ${truncatedTitle}`);
|
|
461
609
|
}
|
|
462
|
-
// Summary by category
|
|
463
610
|
const byCategory = {};
|
|
464
611
|
for (const entry of knowledge) {
|
|
465
612
|
byCategory[entry.category] = (byCategory[entry.category] || 0) + 1;
|
|
@@ -471,3 +618,32 @@ function printKnowledgeList(projectId, knowledge) {
|
|
|
471
618
|
.join(", ");
|
|
472
619
|
console.log(` By Category: ${categoryStr}\n`);
|
|
473
620
|
}
|
|
621
|
+
function printProjectConfigs(configs) {
|
|
622
|
+
if (configs.length === 0) {
|
|
623
|
+
console.log("\n No project configurations found.");
|
|
624
|
+
console.log(" Enable PR Agent with: husky project enable-pr-agent <owner/repo>\n");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
console.log("\n PROJECT CONFIGURATIONS");
|
|
628
|
+
console.log(" " + "-".repeat(80));
|
|
629
|
+
console.log(` ${"REPOSITORY".padEnd(40)} ${"PR AGENT".padEnd(15)} ${"UPDATED".padEnd(20)}`);
|
|
630
|
+
console.log(" " + "-".repeat(80));
|
|
631
|
+
for (const config of configs) {
|
|
632
|
+
const status = config.prAgentEnabled ? "ā Enabled" : "ā Disabled";
|
|
633
|
+
const updated = new Date(config.updatedAt).toLocaleDateString();
|
|
634
|
+
const truncatedRepo = config.repo.length > 38 ? config.repo.substring(0, 35) + "..." : config.repo;
|
|
635
|
+
console.log(` ${truncatedRepo.padEnd(40)} ${status.padEnd(15)} ${updated}`);
|
|
636
|
+
}
|
|
637
|
+
console.log(" " + "-".repeat(80));
|
|
638
|
+
console.log(` Total: ${configs.length} configuration(s)\n`);
|
|
639
|
+
}
|
|
640
|
+
function printProjectConfigDetail(config) {
|
|
641
|
+
console.log(`\n Project Configuration: ${config.repo}`);
|
|
642
|
+
console.log(" " + "=".repeat(60));
|
|
643
|
+
console.log(` ID: ${config.id}`);
|
|
644
|
+
console.log(` Repository: ${config.repo}`);
|
|
645
|
+
console.log(` PR Agent: ${config.prAgentEnabled ? "ā Enabled" : "ā Disabled"}`);
|
|
646
|
+
console.log(` Created: ${new Date(config.createdAt).toLocaleString()}`);
|
|
647
|
+
console.log(` Updated: ${new Date(config.updatedAt).toLocaleString()}`);
|
|
648
|
+
console.log("");
|
|
649
|
+
}
|
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,7 @@ 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 { checkVersion } from "./lib/version-check.js";
|
|
41
42
|
// Read version from package.json
|
|
42
43
|
const require = createRequire(import.meta.url);
|
|
43
44
|
const packageJson = require("../package.json");
|
|
@@ -88,11 +89,17 @@ if (process.argv.includes("--llm")) {
|
|
|
88
89
|
printLLMContext();
|
|
89
90
|
process.exit(0);
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
const skipVersionCheck = ["--version", "-V", "--help", "-h", "completion"].some((flag) => process.argv.includes(flag));
|
|
93
|
+
async function main() {
|
|
94
|
+
if (!skipVersionCheck) {
|
|
95
|
+
await checkVersion({ silent: process.env.HUSKY_SKIP_VERSION_CHECK === "1" });
|
|
96
|
+
}
|
|
97
|
+
if (process.argv.length <= 2) {
|
|
98
|
+
runInteractiveMode();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
program.parse();
|
|
102
|
+
}
|
|
97
103
|
}
|
|
104
|
+
main();
|
|
98
105
|
// 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
|
+
}
|