@simonfestl/husky-cli 1.8.2 → 1.9.2

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`, {
@@ -1,4 +1,4 @@
1
- export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker"];
1
+ export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker", "reviewer", "e2e_agent", "pr_agent"];
2
2
  export type AgentType = typeof AGENT_TYPES[number];
3
3
  export type MemoryVisibility = 'private' | 'team' | 'public';
4
4
  export interface Memory {
@@ -42,7 +42,9 @@ export declare class AgentBrain {
42
42
  getDatabaseInfo(): {
43
43
  agentType?: AgentType;
44
44
  databaseName: string;
45
+ collectionName: string;
45
46
  };
47
+ private getCollectionName;
46
48
  private ensureCollection;
47
49
  remember(content: string, tags?: string[], metadata?: Record<string, unknown>, visibility?: MemoryVisibility, allowPii?: boolean): Promise<string>;
48
50
  recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
@@ -3,9 +3,9 @@ import { EmbeddingService } from './embeddings.js';
3
3
  import { getConfig } from '../../commands/config.js';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { sanitizeForEmbedding } from './pii-filter.js';
6
- const MEMORIES_COLLECTION = 'agent-memories';
6
+ const DEFAULT_COLLECTION = 'agent-memories';
7
7
  const VECTOR_SIZE = 768;
8
- export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker'];
8
+ export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker', 'reviewer', 'e2e_agent', 'pr_agent'];
9
9
  export function isValidAgentType(value) {
10
10
  return value !== undefined && AGENT_TYPES.includes(value);
11
11
  }
@@ -45,17 +45,25 @@ export class AgentBrain {
45
45
  });
46
46
  }
47
47
  getDatabaseInfo() {
48
+ const collectionName = this.getCollectionName();
48
49
  return {
49
50
  agentType: this.agentType,
50
- databaseName: `qdrant:${MEMORIES_COLLECTION}`,
51
+ databaseName: `qdrant:${collectionName}`,
52
+ collectionName,
51
53
  };
52
54
  }
55
+ getCollectionName() {
56
+ if (!this.agentType)
57
+ return DEFAULT_COLLECTION;
58
+ return `${this.agentType}-memories`;
59
+ }
53
60
  async ensureCollection() {
61
+ const collection = this.getCollectionName();
54
62
  try {
55
- await this.qdrant.getCollection(MEMORIES_COLLECTION);
63
+ await this.qdrant.getCollection(collection);
56
64
  }
57
65
  catch {
58
- await this.qdrant.createCollection(MEMORIES_COLLECTION, VECTOR_SIZE);
66
+ await this.qdrant.createCollection(collection, VECTOR_SIZE);
59
67
  }
60
68
  }
61
69
  async remember(content, tags = [], metadata, visibility = 'private', allowPii = false) {
@@ -69,7 +77,7 @@ export class AgentBrain {
69
77
  const embedding = await this.embeddings.embed(sanitizeResult.sanitized);
70
78
  const id = randomUUID();
71
79
  const now = new Date().toISOString();
72
- await this.qdrant.upsertOne(MEMORIES_COLLECTION, id, embedding, {
80
+ await this.qdrant.upsertOne(this.getCollectionName(), id, embedding, {
73
81
  agent: this.agentId,
74
82
  agentType: this.agentType || 'default',
75
83
  content: sanitizeResult.sanitized, // Store sanitized content
@@ -108,7 +116,7 @@ export class AgentBrain {
108
116
  if (this.agentType) {
109
117
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
110
118
  }
111
- const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit, {
119
+ const results = await this.qdrant.search(this.getCollectionName(), queryEmbedding, limit, {
112
120
  filter,
113
121
  scoreThreshold: minScore,
114
122
  });
@@ -142,7 +150,7 @@ export class AgentBrain {
142
150
  }
143
151
  ]
144
152
  };
145
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
153
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
146
154
  filter,
147
155
  limit,
148
156
  with_payload: true,
@@ -162,7 +170,7 @@ export class AgentBrain {
162
170
  }
163
171
  async forget(memoryId) {
164
172
  await this.ensureCollection();
165
- await this.qdrant.deletePoints(MEMORIES_COLLECTION, [memoryId]);
173
+ await this.qdrant.deletePoints(this.getCollectionName(), [memoryId]);
166
174
  }
167
175
  async listMemories(limit = 20) {
168
176
  await this.ensureCollection();
@@ -174,7 +182,7 @@ export class AgentBrain {
174
182
  if (this.agentType) {
175
183
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
176
184
  }
177
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
185
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
178
186
  filter,
179
187
  limit,
180
188
  with_payload: true,
@@ -202,7 +210,7 @@ export class AgentBrain {
202
210
  if (this.agentType) {
203
211
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
204
212
  }
205
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
213
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
206
214
  filter,
207
215
  limit: 1000,
208
216
  with_payload: true,
@@ -228,7 +236,7 @@ export class AgentBrain {
228
236
  async publish(memoryId, visibility) {
229
237
  await this.ensureCollection();
230
238
  const now = new Date().toISOString();
231
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
239
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
232
240
  visibility,
233
241
  publishedBy: this.agentId,
234
242
  publishedAt: now,
@@ -241,7 +249,7 @@ export class AgentBrain {
241
249
  async unpublish(memoryId) {
242
250
  await this.ensureCollection();
243
251
  const now = new Date().toISOString();
244
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
252
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
245
253
  visibility: 'private',
246
254
  publishedBy: undefined,
247
255
  publishedAt: undefined,
@@ -269,7 +277,7 @@ export class AgentBrain {
269
277
  { key: 'status', match: { value: 'active' } },
270
278
  ],
271
279
  };
272
- const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit * 3, {
280
+ const results = await this.qdrant.search(this.getCollectionName(), queryEmbedding, limit * 3, {
273
281
  filter,
274
282
  scoreThreshold: minScore,
275
283
  });
@@ -312,7 +320,7 @@ export class AgentBrain {
312
320
  { key: 'status', match: { value: 'active' } },
313
321
  ],
314
322
  };
315
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
323
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
316
324
  filter,
317
325
  limit,
318
326
  with_payload: true,
@@ -340,13 +348,13 @@ export class AgentBrain {
340
348
  */
341
349
  async boost(memoryId) {
342
350
  await this.ensureCollection();
343
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
351
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
344
352
  if (!point)
345
353
  throw new Error('Memory not found');
346
354
  const boostCount = Number(point.payload?.boostCount || 0) + 1;
347
355
  const downvoteCount = Number(point.payload?.downvoteCount || 0);
348
356
  const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
349
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
357
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
350
358
  boostCount,
351
359
  qualityScore,
352
360
  updatedAt: new Date().toISOString(),
@@ -357,13 +365,13 @@ export class AgentBrain {
357
365
  */
358
366
  async downvote(memoryId) {
359
367
  await this.ensureCollection();
360
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
368
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
361
369
  if (!point)
362
370
  throw new Error('Memory not found');
363
371
  const boostCount = Number(point.payload?.boostCount || 0);
364
372
  const downvoteCount = Number(point.payload?.downvoteCount || 0) + 1;
365
373
  const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
366
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
374
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
367
375
  downvoteCount,
368
376
  qualityScore,
369
377
  updatedAt: new Date().toISOString(),
@@ -374,7 +382,7 @@ export class AgentBrain {
374
382
  */
375
383
  async getQuality(memoryId) {
376
384
  await this.ensureCollection();
377
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
385
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
378
386
  if (!point)
379
387
  throw new Error('Memory not found');
380
388
  return {
@@ -434,7 +442,7 @@ export class AgentBrain {
434
442
  }))
435
443
  });
436
444
  }
437
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
445
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
438
446
  filter,
439
447
  limit: 1000,
440
448
  with_payload: true,
@@ -468,7 +476,7 @@ export class AgentBrain {
468
476
  boostCount,
469
477
  });
470
478
  if (!dryRun) {
471
- await this.qdrant.setPayload(MEMORIES_COLLECTION, String(r.id), {
479
+ await this.qdrant.setPayload(this.getCollectionName(), String(r.id), {
472
480
  status: 'archived',
473
481
  updatedAt: new Date().toISOString(),
474
482
  });
@@ -488,7 +496,7 @@ export class AgentBrain {
488
496
  { key: 'status', match: { value: 'archived' } },
489
497
  ],
490
498
  };
491
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
499
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
492
500
  filter,
493
501
  limit: 1000,
494
502
  with_payload: true,
@@ -503,7 +511,7 @@ export class AgentBrain {
503
511
  }
504
512
  }
505
513
  if (toPurge.length > 0) {
506
- await this.qdrant.deletePoints(MEMORIES_COLLECTION, toPurge);
514
+ await this.qdrant.deletePoints(this.getCollectionName(), toPurge);
507
515
  }
508
516
  return toPurge.length;
509
517
  }
@@ -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.2",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {