@simonfestl/husky-cli 1.8.2 → 1.9.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.
@@ -8,6 +8,7 @@ import { ZendeskClient } from "../../lib/biz/index.js";
8
8
  import { AgentBrain } from "../../lib/biz/agent-brain.js";
9
9
  import * as fs from "fs";
10
10
  import * as path from "path";
11
+ import { errorWithAutoHint } from "../../lib/error-hints.js";
11
12
  export const ticketsCommand = new Command("tickets")
12
13
  .description("Manage support tickets (Zendesk)");
13
14
  // husky biz tickets list
@@ -159,8 +160,7 @@ ticketsCommand
159
160
  });
160
161
  }
161
162
  if (Object.keys(updates).length === 0) {
162
- console.error("Error: Provide --status, --priority, or --field");
163
- process.exit(1);
163
+ errorWithAutoHint("Provide --status, --priority, or --field. Use --help for options.");
164
164
  }
165
165
  const ticket = await client.updateTicket(parseInt(id, 10), updates);
166
166
  if (options.json) {
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
+ import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
5
6
  const CONFIG_DIR = join(homedir(), ".husky");
6
7
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
8
  // API Key validation - must be at least 16 characters, alphanumeric + common key chars (base64, JWT, etc.)
@@ -180,21 +181,20 @@ configCommand
180
181
  console.log(" Gemini: gemini-api-key");
181
182
  console.log(" NocoDB: nocodb-api-token, nocodb-base-url, nocodb-workspace-id");
182
183
  console.log(" Brain: agent-type");
184
+ console.error("\nšŸ’” For configuration help: husky explain config");
183
185
  process.exit(1);
184
186
  }
185
187
  // Validation for specific keys
186
188
  if (key === "api-url" || key === "billbee-base-url") {
187
189
  const validation = validateApiUrl(value);
188
190
  if (!validation.valid) {
189
- console.error(`Error: ${validation.error}`);
190
- process.exit(1);
191
+ errorWithHint(validation.error || "Invalid URL", ExplainTopic.CONFIG, "Learn about proper URL format");
191
192
  }
192
193
  }
193
194
  if (key === "agent-type") {
194
195
  const validTypes = ["support", "claude", "gotess", "supervisor", "worker"];
195
196
  if (!validTypes.includes(value)) {
196
- console.error(`Error: Invalid agent type. Must be one of: ${validTypes.join(", ")}`);
197
- process.exit(1);
197
+ errorWithHint(`Invalid agent type. Must be one of: ${validTypes.join(", ")}`, ExplainTopic.CONFIG, "See available configuration options");
198
198
  }
199
199
  }
200
200
  // Set the value
@@ -240,41 +240,41 @@ configCommand
240
240
  const config = getConfig();
241
241
  // Check if configuration is complete
242
242
  if (!config.apiUrl) {
243
- console.error("Error: API URL not configured. Run: husky config set api-url <url>");
244
- process.exit(1);
243
+ ErrorHelpers.missingApiUrl();
245
244
  }
246
245
  if (!config.apiKey) {
247
- console.error("Error: API key not configured. Run: husky config set api-key <key>");
248
- process.exit(1);
246
+ ErrorHelpers.missingApiKey();
249
247
  }
250
248
  console.log("Testing API connection...");
251
249
  try {
252
250
  // First test basic connectivity with /api/tasks
253
251
  const tasksUrl = new URL("/api/tasks", config.apiUrl);
252
+ const apiKey = config.apiKey; // We already checked it's defined above
254
253
  const tasksRes = await fetch(tasksUrl.toString(), {
255
- headers: { "x-api-key": config.apiKey },
254
+ headers: { "x-api-key": apiKey },
256
255
  });
257
256
  if (!tasksRes.ok) {
258
257
  if (tasksRes.status === 401) {
259
258
  console.error(`API connection failed: Unauthorized (HTTP 401)`);
260
259
  console.error(" Check your API key with: husky config set api-key <key>");
260
+ console.error("\nšŸ’” For configuration help: husky explain config");
261
261
  process.exit(1);
262
262
  }
263
263
  else if (tasksRes.status === 403) {
264
264
  console.error(`API connection failed: Forbidden (HTTP 403)`);
265
265
  console.error(" Your API key may not have the required permissions");
266
+ console.error("\nšŸ’” For configuration help: husky explain config");
266
267
  process.exit(1);
267
268
  }
268
269
  else {
269
- console.error(`API connection failed: HTTP ${tasksRes.status}`);
270
- process.exit(1);
270
+ errorWithHint(`API connection failed: HTTP ${tasksRes.status}`, ExplainTopic.CONFIG, "Check your API configuration");
271
271
  }
272
272
  }
273
273
  console.log(`API connection successful (API URL: ${config.apiUrl})`);
274
274
  // Now fetch role/permissions from whoami
275
275
  const whoamiUrl = new URL("/api/auth/whoami", config.apiUrl);
276
276
  const whoamiRes = await fetch(whoamiUrl.toString(), {
277
- headers: { "x-api-key": config.apiKey },
277
+ headers: { "x-api-key": apiKey },
278
278
  });
279
279
  if (whoamiRes.ok) {
280
280
  const data = await whoamiRes.json();
@@ -298,9 +298,11 @@ configCommand
298
298
  if (error instanceof TypeError && error.message.includes("fetch")) {
299
299
  console.error(`API connection failed: Could not connect to ${config.apiUrl}`);
300
300
  console.error(" Check your API URL with: husky config set api-url <url>");
301
+ console.error("\nšŸ’” For configuration help: husky explain config");
301
302
  }
302
303
  else {
303
304
  console.error(`API connection failed: ${error instanceof Error ? error.message : "Unknown error"}`);
305
+ console.error("\nšŸ’” For configuration help: husky explain config");
304
306
  }
305
307
  process.exit(1);
306
308
  }
@@ -8,14 +8,16 @@ import { WorktreeManager } from "../lib/worktree.js";
8
8
  import { execSync } from "child_process";
9
9
  import { resolveProject, fetchProjects, formatProjectList } from "../lib/project-resolver.js";
10
10
  import { requirePermission } from "../lib/permissions.js";
11
+ import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
11
12
  export const taskCommand = new Command("task")
12
13
  .description("Manage tasks");
13
14
  // Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
14
15
  function getTaskId(options) {
15
16
  const id = options.id || process.env.HUSKY_TASK_ID;
16
17
  if (!id) {
17
- console.error("Error: Task ID required. Use --id or set HUSKY_TASK_ID environment variable.");
18
- process.exit(1);
18
+ ErrorHelpers.missingTaskId();
19
+ // TypeScript doesn't know that ErrorHelpers.missingTaskId() never returns
20
+ throw new Error("unreachable");
19
21
  }
20
22
  return id;
21
23
  }
@@ -23,8 +25,9 @@ function getTaskId(options) {
23
25
  function ensureConfig() {
24
26
  const config = getConfig();
25
27
  if (!config.apiUrl) {
26
- console.error("Error: API URL not configured. Run: husky config set api-url <url>");
27
- process.exit(1);
28
+ ErrorHelpers.missingApiUrl();
29
+ // TypeScript doesn't know that ErrorHelpers.missingApiUrl() never returns
30
+ throw new Error("unreachable");
28
31
  }
29
32
  return config;
30
33
  }
@@ -128,8 +131,7 @@ taskCommand
128
131
  .action(async (options) => {
129
132
  const config = getConfig();
130
133
  if (!config.apiUrl) {
131
- console.error("Error: API URL not configured. Run: husky config set api-url <url>");
132
- process.exit(1);
134
+ ErrorHelpers.missingApiUrl();
133
135
  }
134
136
  try {
135
137
  const url = new URL("/api/tasks", config.apiUrl);
@@ -141,7 +143,7 @@ taskCommand
141
143
  let filterProjectId = null;
142
144
  if (!options.all && !options.project) {
143
145
  const repoIdentifier = getGitRepoIdentifier();
144
- if (repoIdentifier) {
146
+ if (repoIdentifier && config.apiUrl) {
145
147
  autoDetectedProject = await findProjectByRepo(config.apiUrl, config.apiKey, repoIdentifier);
146
148
  if (autoDetectedProject) {
147
149
  filterProjectId = autoDetectedProject.id;
@@ -215,8 +217,7 @@ taskCommand
215
217
  printTasks(tasks);
216
218
  }
217
219
  catch (error) {
218
- console.error("Error fetching tasks:", error);
219
- process.exit(1);
220
+ errorWithHint(`Error fetching tasks: ${error instanceof Error ? error.message : error}`, ExplainTopic.TASK, "Learn about task listing and management");
220
221
  }
221
222
  });
222
223
  // husky task start <id>
@@ -227,14 +228,16 @@ taskCommand
227
228
  .action(async (id, options) => {
228
229
  const config = getConfig();
229
230
  if (!config.apiUrl) {
230
- console.error("Error: API URL not configured.");
231
- process.exit(1);
231
+ ErrorHelpers.missingApiUrl();
232
+ throw new Error("unreachable");
232
233
  }
234
+ const apiUrl = config.apiUrl; // Type narrowing
235
+ const apiKey = config.apiKey || "";
233
236
  try {
234
237
  // Ensure worker is registered and create a session
235
- const workerId = await ensureWorkerRegistered(config.apiUrl, config.apiKey || "");
238
+ const workerId = await ensureWorkerRegistered(apiUrl, apiKey);
236
239
  const sessionId = generateSessionId();
237
- await registerSession(config.apiUrl, config.apiKey || "", workerId, sessionId);
240
+ await registerSession(apiUrl, apiKey, workerId, sessionId);
238
241
  // Create worktree for isolation (unless --no-worktree)
239
242
  let worktreeInfo = null;
240
243
  if (options.worktree !== false) {
@@ -515,8 +518,7 @@ taskCommand
515
518
  updates.projectId = resolved.projectId;
516
519
  }
517
520
  if (Object.keys(updates).length === 0) {
518
- console.error("Error: No update options provided. Use --help for available options.");
519
- process.exit(1);
521
+ errorWithHint("No update options provided. Use --help for available options.", ExplainTopic.TASK, "See all available update options");
520
522
  }
521
523
  // Auto-create worktree when starting a task (unless --no-worktree)
522
524
  let worktreeInfo = null;
@@ -715,12 +717,10 @@ taskCommand
715
717
  const taskId = idArg || options.id || process.env.HUSKY_TASK_ID;
716
718
  const message = messageArg || options.message;
717
719
  if (!taskId) {
718
- console.error("Error: Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID");
719
- process.exit(1);
720
+ errorWithHint("Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID", ExplainTopic.TASK, "Learn about task ID usage");
720
721
  }
721
722
  if (!message) {
722
- console.error("Error: Message required. Use positional arg or -m/--message");
723
- process.exit(1);
723
+ errorWithHint("Message required. Use positional arg or -m/--message", ExplainTopic.TASK, "See message command examples");
724
724
  }
725
725
  try {
726
726
  const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
@@ -48,6 +48,7 @@ export declare class QdrantClient {
48
48
  upsertOne(collectionName: string, id: string | number, vector: number[], payload?: Record<string, unknown>): Promise<void>;
49
49
  getPoint(collectionName: string, id: string | number): Promise<Point | null>;
50
50
  deletePoints(collectionName: string, ids: (string | number)[]): Promise<void>;
51
+ setPayload(collectionName: string, pointId: string | number, payload: Record<string, unknown>): Promise<void>;
51
52
  count(collectionName: string): Promise<number>;
52
53
  scroll(collectionName: string, options?: {
53
54
  filter?: Record<string, unknown>;
@@ -57,9 +58,5 @@ export declare class QdrantClient {
57
58
  id: string | number;
58
59
  payload: Record<string, unknown>;
59
60
  }>>;
60
- /**
61
- * Update payload for a specific point (for quality/visibility updates)
62
- */
63
- setPayload(collectionName: string, id: string | number, payload: Record<string, unknown>): Promise<void>;
64
61
  }
65
62
  export default QdrantClient;
@@ -155,6 +155,15 @@ export class QdrantClient {
155
155
  body: JSON.stringify({ points: ids }),
156
156
  });
157
157
  }
158
+ async setPayload(collectionName, pointId, payload) {
159
+ await this.request(`/collections/${collectionName}/points/payload?wait=true`, {
160
+ method: 'POST',
161
+ body: JSON.stringify({
162
+ points: [pointId],
163
+ payload,
164
+ }),
165
+ });
166
+ }
158
167
  async count(collectionName) {
159
168
  const info = await this.getCollection(collectionName);
160
169
  return info.pointsCount;
@@ -173,17 +182,5 @@ export class QdrantClient {
173
182
  payload: p.payload || {},
174
183
  }));
175
184
  }
176
- /**
177
- * Update payload for a specific point (for quality/visibility updates)
178
- */
179
- async setPayload(collectionName, id, payload) {
180
- await this.request(`/collections/${collectionName}/points/payload?wait=true`, {
181
- method: 'POST',
182
- body: JSON.stringify({
183
- points: [id],
184
- payload,
185
- }),
186
- });
187
- }
188
185
  }
189
186
  export default QdrantClient;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Error Hints System
3
+ *
4
+ * Provides helpful hints and references to `husky explain` when users encounter errors.
5
+ * This makes the CLI more user-friendly by guiding users to relevant documentation.
6
+ */
7
+ /**
8
+ * Map of error categories to their corresponding `husky explain` topics
9
+ */
10
+ export declare enum ExplainTopic {
11
+ TASK = "task",
12
+ CONFIG = "config",
13
+ ROADMAP = "roadmap",
14
+ CHANGELOG = "changelog",
15
+ AGENT = "agent"
16
+ }
17
+ /**
18
+ * Print an error message with an automatic hint based on content
19
+ */
20
+ export declare function errorWithAutoHint(message: string, exitCode?: number): never;
21
+ /**
22
+ * Print an error message with a specific hint topic
23
+ */
24
+ export declare function errorWithHint(message: string, topic: ExplainTopic, customHint?: string, exitCode?: number): never;
25
+ /**
26
+ * Print an error message with no hint (for errors where no help is available)
27
+ */
28
+ export declare function errorWithoutHint(message: string, exitCode?: number): never;
29
+ /**
30
+ * Predefined error helpers for common scenarios
31
+ */
32
+ export declare const ErrorHelpers: {
33
+ /**
34
+ * Error: Task ID not provided
35
+ */
36
+ missingTaskId: () => never;
37
+ /**
38
+ * Error: API URL not configured
39
+ */
40
+ missingApiUrl: () => never;
41
+ /**
42
+ * Error: API key not configured
43
+ */
44
+ missingApiKey: () => never;
45
+ /**
46
+ * Error: Both API URL and key not configured
47
+ */
48
+ missingConfig: () => never;
49
+ /**
50
+ * Error: Permission denied
51
+ */
52
+ permissionDenied: (operation: string) => never;
53
+ /**
54
+ * Error: Invalid task status
55
+ */
56
+ invalidTaskStatus: (status: string) => never;
57
+ /**
58
+ * Error: Task operation failed
59
+ */
60
+ taskOperationFailed: (operation: string, reason: string) => never;
61
+ /**
62
+ * Error: API request failed
63
+ */
64
+ apiRequestFailed: (status: number, message: string) => never;
65
+ };
66
+ /**
67
+ * Format a warning message with a hint (non-fatal)
68
+ */
69
+ export declare function warningWithHint(message: string, topic: ExplainTopic, customHint?: string): void;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Error Hints System
3
+ *
4
+ * Provides helpful hints and references to `husky explain` when users encounter errors.
5
+ * This makes the CLI more user-friendly by guiding users to relevant documentation.
6
+ */
7
+ /**
8
+ * Map of error categories to their corresponding `husky explain` topics
9
+ */
10
+ export var ExplainTopic;
11
+ (function (ExplainTopic) {
12
+ ExplainTopic["TASK"] = "task";
13
+ ExplainTopic["CONFIG"] = "config";
14
+ ExplainTopic["ROADMAP"] = "roadmap";
15
+ ExplainTopic["CHANGELOG"] = "changelog";
16
+ ExplainTopic["AGENT"] = "agent";
17
+ })(ExplainTopic || (ExplainTopic = {}));
18
+ const ERROR_PATTERNS = [
19
+ {
20
+ keywords: ["task id", "HUSKY_TASK_ID", "task status", "task complete", "task start"],
21
+ topic: ExplainTopic.TASK,
22
+ },
23
+ {
24
+ keywords: ["api url", "api key", "not configured", "config set", "authentication"],
25
+ topic: ExplainTopic.CONFIG,
26
+ },
27
+ {
28
+ keywords: ["roadmap", "phase", "feature"],
29
+ topic: ExplainTopic.ROADMAP,
30
+ },
31
+ {
32
+ keywords: ["changelog", "version", "commits"],
33
+ topic: ExplainTopic.CHANGELOG,
34
+ },
35
+ {
36
+ keywords: ["workflow", "agent", "session"],
37
+ topic: ExplainTopic.AGENT,
38
+ },
39
+ ];
40
+ /**
41
+ * Detect which explain topic is most relevant based on error message
42
+ */
43
+ function detectExplainTopic(message) {
44
+ const lowerMessage = message.toLowerCase();
45
+ for (const pattern of ERROR_PATTERNS) {
46
+ if (pattern.keywords.some(keyword => lowerMessage.includes(keyword))) {
47
+ return pattern.topic;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * Format a hint message for a specific explain topic
54
+ */
55
+ function formatHint(topic, customHint) {
56
+ if (customHint) {
57
+ return `\nšŸ’” Hint: ${customHint}\n Run: husky explain ${topic}`;
58
+ }
59
+ const hints = {
60
+ [ExplainTopic.TASK]: "For task workflow help",
61
+ [ExplainTopic.CONFIG]: "For configuration help",
62
+ [ExplainTopic.ROADMAP]: "For roadmap commands help",
63
+ [ExplainTopic.CHANGELOG]: "For changelog commands help",
64
+ [ExplainTopic.AGENT]: "For agent workflow examples",
65
+ };
66
+ return `\nšŸ’” ${hints[topic]}: husky explain ${topic}`;
67
+ }
68
+ /**
69
+ * Print an error message with an automatic hint based on content
70
+ */
71
+ export function errorWithAutoHint(message, exitCode = 1) {
72
+ console.error(`Error: ${message}`);
73
+ const topic = detectExplainTopic(message);
74
+ if (topic) {
75
+ console.error(formatHint(topic));
76
+ }
77
+ process.exit(exitCode);
78
+ }
79
+ /**
80
+ * Print an error message with a specific hint topic
81
+ */
82
+ export function errorWithHint(message, topic, customHint, exitCode = 1) {
83
+ console.error(`Error: ${message}`);
84
+ console.error(formatHint(topic, customHint));
85
+ process.exit(exitCode);
86
+ }
87
+ /**
88
+ * Print an error message with no hint (for errors where no help is available)
89
+ */
90
+ export function errorWithoutHint(message, exitCode = 1) {
91
+ console.error(`Error: ${message}`);
92
+ process.exit(exitCode);
93
+ }
94
+ /**
95
+ * Predefined error helpers for common scenarios
96
+ */
97
+ export const ErrorHelpers = {
98
+ /**
99
+ * Error: Task ID not provided
100
+ */
101
+ missingTaskId: () => {
102
+ errorWithHint("Task ID required. Use --id or set HUSKY_TASK_ID environment variable.", ExplainTopic.TASK, "Learn about task ID usage and environment variables");
103
+ },
104
+ /**
105
+ * Error: API URL not configured
106
+ */
107
+ missingApiUrl: () => {
108
+ errorWithHint("API URL not configured. Run: husky config set api-url <url>", ExplainTopic.CONFIG, "Learn how to configure the CLI");
109
+ },
110
+ /**
111
+ * Error: API key not configured
112
+ */
113
+ missingApiKey: () => {
114
+ errorWithHint("API key not configured. Run: husky config set api-key <key>", ExplainTopic.CONFIG, "Learn how to configure authentication");
115
+ },
116
+ /**
117
+ * Error: Both API URL and key not configured
118
+ */
119
+ missingConfig: () => {
120
+ errorWithHint("API URL and key required. Run: husky config test", ExplainTopic.CONFIG, "Learn about CLI configuration");
121
+ },
122
+ /**
123
+ * Error: Permission denied
124
+ */
125
+ permissionDenied: (operation) => {
126
+ errorWithAutoHint(`Permission denied: ${operation}`);
127
+ },
128
+ /**
129
+ * Error: Invalid task status
130
+ */
131
+ invalidTaskStatus: (status) => {
132
+ errorWithHint(`Invalid status: ${status}. Valid statuses: backlog, in_progress, review, done`, ExplainTopic.TASK, "See all available task statuses");
133
+ },
134
+ /**
135
+ * Error: Task operation failed
136
+ */
137
+ taskOperationFailed: (operation, reason) => {
138
+ errorWithHint(`Failed to ${operation}: ${reason}`, ExplainTopic.TASK, "Learn about task management");
139
+ },
140
+ /**
141
+ * Error: API request failed
142
+ */
143
+ apiRequestFailed: (status, message) => {
144
+ if (status === 401) {
145
+ errorWithHint("Authentication failed. Check your API key.", ExplainTopic.CONFIG, "Learn how to configure authentication");
146
+ }
147
+ else if (status === 403) {
148
+ errorWithAutoHint(`Permission denied: ${message}`);
149
+ }
150
+ else if (status === 404) {
151
+ errorWithoutHint(`Resource not found: ${message}`);
152
+ }
153
+ else {
154
+ errorWithAutoHint(`API error (${status}): ${message}`);
155
+ }
156
+ },
157
+ };
158
+ /**
159
+ * Format a warning message with a hint (non-fatal)
160
+ */
161
+ export function warningWithHint(message, topic, customHint) {
162
+ console.warn(`⚠ Warning: ${message}`);
163
+ console.warn(formatHint(topic, customHint).replace("šŸ’”", "ā„¹ļø"));
164
+ }
@@ -5,6 +5,7 @@
5
5
  * Permissions are fetched from the API and cached locally.
6
6
  */
7
7
  import { getConfig, hasPermission, getRole, fetchAndCacheRole, clearRoleCache } from "../commands/config.js";
8
+ import { ExplainTopic } from "./error-hints.js";
8
9
  /**
9
10
  * Check if current user has a specific permission.
10
11
  * Uses cached permissions from config.
@@ -27,6 +28,7 @@ export function requirePermission(permission) {
27
28
  else {
28
29
  console.error("Run 'husky config test' to refresh your role and permissions.");
29
30
  }
31
+ console.error(`\nšŸ’” For configuration help: husky explain ${ExplainTopic.CONFIG}`);
30
32
  process.exit(1);
31
33
  }
32
34
  }
@@ -43,6 +45,7 @@ export function requireAnyPermission(permissions) {
43
45
  if (config.role) {
44
46
  console.error(`Your role (${config.role}) does not have these permissions.`);
45
47
  }
48
+ console.error(`\nšŸ’” For configuration help: husky explain ${ExplainTopic.CONFIG}`);
46
49
  process.exit(1);
47
50
  }
48
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.8.2",
3
+ "version": "1.9.1",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {