@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.
@@ -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 = ZendeskClient.fromConfig();
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
- console.log(`\n šŸŽ« Tickets (${tickets.length})\n`);
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 = ZendeskClient.fromConfig();
79
+ const client = await getClient(options);
57
80
  const ticketId = parseInt(id, 10);
58
- const [ticket, comments] = await Promise.all([
59
- client.getTicket(ticketId),
60
- client.getTicketComments(ticketId),
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 = ZendeskClient.fromConfig();
101
- const ticket = await client.addComment(parseInt(id, 10), options.message, true);
102
- console.log(`āœ“ Public reply added to ticket #${ticket.id}`);
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 = ZendeskClient.fromConfig();
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
- const ticket = await client.createTicket(ticketData);
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 = ZendeskClient.fromConfig();
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));
@@ -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
+ }
@@ -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
- let tasks = await api.get(url.pathname + url.search);
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);
@@ -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
- await summarizeVideo(validatedUrl, validatedOptions);
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 YouTube video using Gemini (directly with URL)
84
+ * Summarize via API proxy (uses server-side credentials)
62
85
  */
63
- async function summarizeVideo(url, options) {
86
+ async function summarizeViaProxy(url, options) {
64
87
  const videoId = extractVideoId(url);
65
88
  const fullUrl = `https://www.youtube.com/watch?v=${videoId}`;
66
- console.log(`šŸ“¹ Video: ${videoId}`);
67
- console.log(`šŸ”— URL: ${fullUrl}\n`);
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
- console.log(`šŸ¤– Analyzing with ${modelLabel}...\n`);
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
- // Check if no command was provided - run interactive mode
92
- if (process.argv.length <= 2) {
93
- runInteractiveMode();
94
- }
95
- else {
96
- program.parse();
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
@@ -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';
@@ -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,4 @@
1
+ export declare function checkVersion(options?: {
2
+ silent?: boolean;
3
+ }): Promise<void>;
4
+ export declare function getCurrentVersion(): string;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.32.0",
3
+ "version": "1.33.1",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {