@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.
- package/dist/commands/biz/tickets.js +2 -2
- package/dist/commands/config.js +14 -12
- package/dist/commands/task.js +19 -19
- package/dist/lib/biz/agent-brain.d.ts +3 -1
- package/dist/lib/biz/agent-brain.js +32 -24
- package/dist/lib/biz/qdrant.d.ts +1 -4
- package/dist/lib/biz/qdrant.js +9 -12
- package/dist/lib/error-hints.d.ts +69 -0
- package/dist/lib/error-hints.js +164 -0
- package/dist/lib/permissions.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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) {
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
process.exit(1);
|
|
243
|
+
ErrorHelpers.missingApiUrl();
|
|
245
244
|
}
|
|
246
245
|
if (!config.apiKey) {
|
|
247
|
-
|
|
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":
|
|
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
|
-
|
|
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":
|
|
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
|
}
|
package/dist/commands/task.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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(
|
|
238
|
+
const workerId = await ensureWorkerRegistered(apiUrl, apiKey);
|
|
236
239
|
const sessionId = generateSessionId();
|
|
237
|
-
await registerSession(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:${
|
|
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(
|
|
63
|
+
await this.qdrant.getCollection(collection);
|
|
56
64
|
}
|
|
57
65
|
catch {
|
|
58
|
-
await this.qdrant.createCollection(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
514
|
+
await this.qdrant.deletePoints(this.getCollectionName(), toPurge);
|
|
507
515
|
}
|
|
508
516
|
return toPurge.length;
|
|
509
517
|
}
|
package/dist/lib/biz/qdrant.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/biz/qdrant.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/permissions.js
CHANGED
|
@@ -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
|
}
|