@simonfestl/husky-cli 0.9.6 → 1.0.0

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.
@@ -6,6 +6,7 @@ import { paginateList, printPaginated } from "../lib/pagination.js";
6
6
  import { ensureWorkerRegistered, generateSessionId, registerSession } from "../lib/worker.js";
7
7
  import { WorktreeManager } from "../lib/worktree.js";
8
8
  import { execSync } from "child_process";
9
+ import { resolveProject, fetchProjects, formatProjectList } from "../lib/project-resolver.js";
9
10
  export const taskCommand = new Command("task")
10
11
  .description("Manage tasks");
11
12
  // Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
@@ -221,7 +222,8 @@ taskCommand
221
222
  taskCommand
222
223
  .command("start <id>")
223
224
  .description("Start working on a task")
224
- .action(async (id) => {
225
+ .option("--no-worktree", "Skip worktree creation")
226
+ .action(async (id, options) => {
225
227
  const config = getConfig();
226
228
  if (!config.apiUrl) {
227
229
  console.error("Error: API URL not configured.");
@@ -232,6 +234,15 @@ taskCommand
232
234
  const workerId = await ensureWorkerRegistered(config.apiUrl, config.apiKey || "");
233
235
  const sessionId = generateSessionId();
234
236
  await registerSession(config.apiUrl, config.apiKey || "", workerId, sessionId);
237
+ // Create worktree for isolation (unless --no-worktree)
238
+ let worktreeInfo = null;
239
+ if (options.worktree !== false) {
240
+ worktreeInfo = createWorktreeForTask(id);
241
+ if (worktreeInfo) {
242
+ console.log(`✓ Created worktree: ${worktreeInfo.path}`);
243
+ console.log(` Branch: ${worktreeInfo.branch}`);
244
+ }
245
+ }
235
246
  const res = await fetch(`${config.apiUrl}/api/tasks/${id}/start`, {
236
247
  method: "POST",
237
248
  headers: {
@@ -242,6 +253,11 @@ taskCommand
242
253
  agent: "claude-code",
243
254
  workerId,
244
255
  sessionId,
256
+ // Include worktree info if created
257
+ ...(worktreeInfo ? {
258
+ worktreePath: worktreeInfo.path,
259
+ worktreeBranch: worktreeInfo.branch,
260
+ } : {}),
245
261
  }),
246
262
  });
247
263
  if (!res.ok) {
@@ -251,6 +267,10 @@ taskCommand
251
267
  console.log(`✓ Started: ${task.title}`);
252
268
  console.log(` Worker: ${workerId}`);
253
269
  console.log(` Session: ${sessionId}`);
270
+ // Show hint to cd into worktree
271
+ if (worktreeInfo) {
272
+ console.log(`\n💡 To work in isolation: cd ${worktreeInfo.path}`);
273
+ }
254
274
  }
255
275
  catch (error) {
256
276
  console.error("Error starting task:", error);
@@ -262,6 +282,7 @@ taskCommand
262
282
  .command("done <id>")
263
283
  .description("Mark task as done")
264
284
  .option("--pr <url>", "Link to PR")
285
+ .option("--skip-qa", "Skip QA review and mark as done directly")
265
286
  .action(async (id, options) => {
266
287
  const config = getConfig();
267
288
  if (!config.apiUrl) {
@@ -275,25 +296,36 @@ taskCommand
275
296
  "Content-Type": "application/json",
276
297
  ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
277
298
  },
278
- body: JSON.stringify({ prUrl: options.pr }),
299
+ body: JSON.stringify({
300
+ prUrl: options.pr,
301
+ skipQA: options.skipQa === true,
302
+ }),
279
303
  });
280
304
  if (!res.ok) {
281
305
  throw new Error(`API error: ${res.status}`);
282
306
  }
283
307
  const task = await res.json();
284
- console.log(`✓ Completed: ${task.title}`);
308
+ if (task.qaTriggered) {
309
+ console.log(`✓ Task moved to review: ${task.title}`);
310
+ console.log(` QA pipeline triggered - awaiting verification`);
311
+ }
312
+ else {
313
+ console.log(`✓ Completed: ${task.title}`);
314
+ }
315
+ if (task.message) {
316
+ console.log(` ${task.message}`);
317
+ }
285
318
  }
286
319
  catch (error) {
287
320
  console.error("Error completing task:", error);
288
321
  process.exit(1);
289
322
  }
290
323
  });
291
- // husky task create <title>
292
324
  taskCommand
293
325
  .command("create <title>")
294
326
  .description("Create a new task")
295
327
  .option("-d, --description <desc>", "Task description")
296
- .option("--project <projectId>", "Project ID")
328
+ .option("--project <project>", "Project name or ID")
297
329
  .option("--path <path>", "Path in project")
298
330
  .option("-p, --priority <priority>", "Priority (low, medium, high)", "medium")
299
331
  .action(async (title, options) => {
@@ -302,6 +334,29 @@ taskCommand
302
334
  console.error("Error: API URL not configured.");
303
335
  process.exit(1);
304
336
  }
337
+ let resolvedProjectId;
338
+ const resolverConfig = { apiUrl: config.apiUrl, apiKey: config.apiKey };
339
+ if (options.project) {
340
+ const resolved = await resolveProject(options.project, resolverConfig);
341
+ if (!resolved) {
342
+ const projects = await fetchProjects(resolverConfig);
343
+ console.error(`\n❌ Project "${options.project}" not found.\n`);
344
+ console.error(formatProjectList(projects));
345
+ process.exit(1);
346
+ }
347
+ if (resolved.resolvedBy === "name-match") {
348
+ console.log(`ℹ️ Resolved "${options.project}" → ${resolved.projectName} (${resolved.projectId})`);
349
+ }
350
+ if (resolved.resolvedBy === "fuzzy-match") {
351
+ const confirmed = await confirm(`Did you mean "${resolved.projectName}"? (${Math.round(resolved.confidence * 100)}% match)`);
352
+ if (!confirmed) {
353
+ console.log("Cancelled.");
354
+ process.exit(0);
355
+ }
356
+ console.log(`ℹ️ Using project: ${resolved.projectName} (${resolved.projectId})`);
357
+ }
358
+ resolvedProjectId = resolved.projectId;
359
+ }
305
360
  try {
306
361
  const res = await fetch(`${config.apiUrl}/api/tasks`, {
307
362
  method: "POST",
@@ -312,13 +367,14 @@ taskCommand
312
367
  body: JSON.stringify({
313
368
  title,
314
369
  description: options.description,
315
- projectId: options.project,
370
+ projectId: resolvedProjectId,
316
371
  linkedPath: options.path,
317
372
  priority: options.priority,
318
373
  }),
319
374
  });
320
375
  if (!res.ok) {
321
- throw new Error(`API error: ${res.status}`);
376
+ const errorData = await res.json().catch(() => ({}));
377
+ throw new Error(errorData.error || `API error: ${res.status}`);
322
378
  }
323
379
  const task = await res.json();
324
380
  console.log(`✓ Created: #${task.id} ${task.title}`);
@@ -328,7 +384,6 @@ taskCommand
328
384
  process.exit(1);
329
385
  }
330
386
  });
331
- // husky task update <id>
332
387
  taskCommand
333
388
  .command("update <id>")
334
389
  .description("Update task properties")
@@ -337,12 +392,12 @@ taskCommand
337
392
  .option("--status <status>", "New status (e.g., backlog, in_progress, review, done, or custom status)")
338
393
  .option("--priority <priority>", "New priority (low, medium, high, urgent)")
339
394
  .option("--assignee <assignee>", "New assignee (human, llm, unassigned)")
340
- .option("--project <projectId>", "Link to project")
395
+ .option("--project <project>", "Link to project (name or ID)")
341
396
  .option("--no-worktree", "Skip automatic worktree creation when starting task")
342
397
  .option("--json", "Output as JSON")
343
398
  .action(async (id, options) => {
344
399
  const config = ensureConfig();
345
- // Build update payload with only changed fields
400
+ const resolverConfig = { apiUrl: config.apiUrl, apiKey: config.apiKey };
346
401
  const updates = {};
347
402
  if (options.title)
348
403
  updates.title = options.title;
@@ -354,8 +409,27 @@ taskCommand
354
409
  updates.priority = options.priority;
355
410
  if (options.assignee)
356
411
  updates.assignee = options.assignee;
357
- if (options.project)
358
- updates.projectId = options.project;
412
+ if (options.project) {
413
+ const resolved = await resolveProject(options.project, resolverConfig);
414
+ if (!resolved) {
415
+ const projects = await fetchProjects(resolverConfig);
416
+ console.error(`\n❌ Project "${options.project}" not found.\n`);
417
+ console.error(formatProjectList(projects));
418
+ process.exit(1);
419
+ }
420
+ if (resolved.resolvedBy === "name-match") {
421
+ console.log(`ℹ️ Resolved "${options.project}" → ${resolved.projectName} (${resolved.projectId})`);
422
+ }
423
+ if (resolved.resolvedBy === "fuzzy-match") {
424
+ const confirmed = await confirm(`Did you mean "${resolved.projectName}"? (${Math.round(resolved.confidence * 100)}% match)`);
425
+ if (!confirmed) {
426
+ console.log("Cancelled.");
427
+ process.exit(0);
428
+ }
429
+ console.log(`ℹ️ Using project: ${resolved.projectName} (${resolved.projectId})`);
430
+ }
431
+ updates.projectId = resolved.projectId;
432
+ }
359
433
  if (Object.keys(updates).length === 0) {
360
434
  console.error("Error: No update options provided. Use --help for available options.");
361
435
  process.exit(1);
@@ -547,11 +621,23 @@ taskCommand
547
621
  });
548
622
  // husky task message <id> "message" - post status message to task
549
623
  taskCommand
550
- .command("message <id> <message>")
624
+ .command("message [id] [message]")
551
625
  .description("Post a status message to a task")
552
- .action(async (id, message) => {
626
+ .option("-m, --message <text>", "Message text (alternative to positional arg)")
627
+ .option("--id <taskId>", "Task ID (alternative to positional arg, or use HUSKY_TASK_ID)")
628
+ .action(async (idArg, messageArg, options) => {
553
629
  const config = ensureConfig();
554
- const taskId = id;
630
+ // Support both: `husky task message <id> <msg>` and `husky task message -m <msg> --id <id>`
631
+ const taskId = idArg || options.id || process.env.HUSKY_TASK_ID;
632
+ const message = messageArg || options.message;
633
+ if (!taskId) {
634
+ console.error("Error: Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID");
635
+ process.exit(1);
636
+ }
637
+ if (!message) {
638
+ console.error("Error: Message required. Use positional arg or -m/--message");
639
+ process.exit(1);
640
+ }
555
641
  try {
556
642
  const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
557
643
  method: "POST",
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { createRequire } from "module";
3
4
  import { taskCommand } from "./commands/task.js";
4
5
  import { configCommand } from "./commands/config.js";
5
6
  import { agentCommand } from "./commands/agent.js";
@@ -10,7 +11,6 @@ import { projectCommand } from "./commands/project.js";
10
11
  import { ideaCommand } from "./commands/idea.js";
11
12
  import { departmentCommand } from "./commands/department.js";
12
13
  import { workflowCommand } from "./commands/workflow.js";
13
- import { julesCommand } from "./commands/jules.js";
14
14
  import { vmCommand } from "./commands/vm.js";
15
15
  import { vmConfigCommand } from "./commands/vm-config.js";
16
16
  import { processCommand } from "./commands/process.js";
@@ -20,15 +20,18 @@ import { completionCommand } from "./commands/completion.js";
20
20
  import { worktreeCommand } from "./commands/worktree.js";
21
21
  import { workerCommand } from "./commands/worker.js";
22
22
  import { bizCommand } from "./commands/biz.js";
23
- import { servicesCommand } from "./commands/services.js";
24
23
  import { printLLMContext, llmCommand } from "./commands/llm-context.js";
25
24
  import { runInteractiveMode } from "./commands/interactive.js";
26
25
  import { serviceAccountCommand } from "./commands/service-account.js";
26
+ import { chatCommand } from "./commands/chat.js";
27
+ // Read version from package.json
28
+ const require = createRequire(import.meta.url);
29
+ const packageJson = require("../package.json");
27
30
  const program = new Command();
28
31
  program
29
32
  .name("husky")
30
33
  .description("CLI for Huskyv0 Task Orchestration with Claude Agent")
31
- .version("0.9.5")
34
+ .version(packageJson.version)
32
35
  .option("--llm", "Output LLM reference documentation (markdown)");
33
36
  program.addCommand(taskCommand);
34
37
  program.addCommand(configCommand);
@@ -40,7 +43,6 @@ program.addCommand(projectCommand);
40
43
  program.addCommand(ideaCommand);
41
44
  program.addCommand(departmentCommand);
42
45
  program.addCommand(workflowCommand);
43
- program.addCommand(julesCommand);
44
46
  program.addCommand(vmCommand);
45
47
  program.addCommand(vmConfigCommand);
46
48
  program.addCommand(processCommand);
@@ -50,8 +52,8 @@ program.addCommand(completionCommand);
50
52
  program.addCommand(worktreeCommand);
51
53
  program.addCommand(workerCommand);
52
54
  program.addCommand(bizCommand);
53
- program.addCommand(servicesCommand);
54
55
  program.addCommand(serviceAccountCommand);
56
+ program.addCommand(chatCommand);
55
57
  program.addCommand(llmCommand);
56
58
  // Handle --llm flag specially
57
59
  if (process.argv.includes("--llm")) {
@@ -0,0 +1,26 @@
1
+ export interface Project {
2
+ id: string;
3
+ name: string;
4
+ }
5
+ export interface ResolveResult {
6
+ projectId: string;
7
+ projectName: string;
8
+ resolvedBy: "exact-id" | "name-match" | "fuzzy-match";
9
+ confidence: number;
10
+ }
11
+ export interface FuzzyMatch {
12
+ project: Project;
13
+ score: number;
14
+ }
15
+ interface Config {
16
+ apiUrl: string;
17
+ apiKey?: string;
18
+ }
19
+ export declare function fetchProjects(config: Config): Promise<Project[]>;
20
+ export declare function resolveProject(input: string, config: Config, fuzzyThreshold?: number): Promise<ResolveResult | null>;
21
+ export declare function formatProjectList(projects: Project[]): string;
22
+ export declare function validateProjectId(projectId: string, config: Config): Promise<{
23
+ valid: boolean;
24
+ project?: Project;
25
+ }>;
26
+ export {};
@@ -0,0 +1,111 @@
1
+ export async function fetchProjects(config) {
2
+ try {
3
+ const res = await fetch(`${config.apiUrl}/api/projects`, {
4
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
5
+ });
6
+ if (!res.ok)
7
+ return [];
8
+ return res.json();
9
+ }
10
+ catch {
11
+ return [];
12
+ }
13
+ }
14
+ function calculateLevenshteinDistance(a, b) {
15
+ const matrix = [];
16
+ for (let i = 0; i <= b.length; i++) {
17
+ matrix[i] = [i];
18
+ }
19
+ for (let j = 0; j <= a.length; j++) {
20
+ matrix[0][j] = j;
21
+ }
22
+ for (let i = 1; i <= b.length; i++) {
23
+ for (let j = 1; j <= a.length; j++) {
24
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
25
+ matrix[i][j] = matrix[i - 1][j - 1];
26
+ }
27
+ else {
28
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
29
+ }
30
+ }
31
+ }
32
+ return matrix[b.length][a.length];
33
+ }
34
+ function similarityScore(a, b) {
35
+ const normalizedA = a.toLowerCase().trim();
36
+ const normalizedB = b.toLowerCase().trim();
37
+ if (normalizedA === normalizedB)
38
+ return 1;
39
+ const maxLen = Math.max(normalizedA.length, normalizedB.length);
40
+ if (maxLen === 0)
41
+ return 1;
42
+ const distance = calculateLevenshteinDistance(normalizedA, normalizedB);
43
+ return 1 - (distance / maxLen);
44
+ }
45
+ function findClosestMatch(input, projects) {
46
+ if (projects.length === 0)
47
+ return null;
48
+ let bestMatch = null;
49
+ for (const project of projects) {
50
+ const nameScore = similarityScore(input, project.name);
51
+ const idScore = similarityScore(input, project.id);
52
+ const score = Math.max(nameScore, idScore);
53
+ if (!bestMatch || score > bestMatch.score) {
54
+ bestMatch = { project, score };
55
+ }
56
+ }
57
+ return bestMatch;
58
+ }
59
+ export async function resolveProject(input, config, fuzzyThreshold = 0.6) {
60
+ const projects = await fetchProjects(config);
61
+ if (projects.length === 0) {
62
+ return null;
63
+ }
64
+ const byId = projects.find((p) => p.id === input);
65
+ if (byId) {
66
+ return {
67
+ projectId: byId.id,
68
+ projectName: byId.name,
69
+ resolvedBy: "exact-id",
70
+ confidence: 1,
71
+ };
72
+ }
73
+ const byName = projects.find((p) => p.name.toLowerCase() === input.toLowerCase());
74
+ if (byName) {
75
+ return {
76
+ projectId: byName.id,
77
+ projectName: byName.name,
78
+ resolvedBy: "name-match",
79
+ confidence: 1,
80
+ };
81
+ }
82
+ const fuzzy = findClosestMatch(input, projects);
83
+ if (fuzzy && fuzzy.score >= fuzzyThreshold) {
84
+ return {
85
+ projectId: fuzzy.project.id,
86
+ projectName: fuzzy.project.name,
87
+ resolvedBy: "fuzzy-match",
88
+ confidence: fuzzy.score,
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+ export function formatProjectList(projects) {
94
+ if (projects.length === 0) {
95
+ return " No projects available.";
96
+ }
97
+ const lines = [];
98
+ lines.push(" Available projects:");
99
+ for (const p of projects) {
100
+ lines.push(` - ${p.name} (ID: ${p.id})`);
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+ export async function validateProjectId(projectId, config) {
105
+ const projects = await fetchProjects(config);
106
+ const project = projects.find((p) => p.id === projectId);
107
+ return {
108
+ valid: !!project,
109
+ project,
110
+ };
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "0.9.6",
3
+ "version": "1.0.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- export declare function julesSessionsMenu(): Promise<void>;