@simonfestl/husky-cli 0.8.1 → 0.8.3

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.
@@ -49,6 +49,22 @@ function isGitRepo() {
49
49
  return false;
50
50
  }
51
51
  }
52
+ // Helper: Get current Git remote URL (e.g., "simon-sfxecom/huskyv0")
53
+ function getGitRepoIdentifier() {
54
+ if (!isGitRepo())
55
+ return null;
56
+ try {
57
+ const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
58
+ // Extract owner/repo from various URL formats:
59
+ // https://github.com/owner/repo.git
60
+ // git@github.com:owner/repo.git
61
+ const match = remoteUrl.match(/[/:]([\w-]+\/[\w-]+?)(\.git)?$/);
62
+ return match ? match[1] : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
52
68
  // Helper: Create worktree for task when starting (auto-isolation)
53
69
  function createWorktreeForTask(taskId) {
54
70
  if (!isGitRepo()) {
@@ -66,11 +82,43 @@ function createWorktreeForTask(taskId) {
66
82
  return null;
67
83
  }
68
84
  }
85
+ // Helper: Find project ID by GitHub repo identifier
86
+ async function findProjectByRepo(apiUrl, apiKey, repoIdentifier) {
87
+ try {
88
+ const res = await fetch(`${apiUrl}/api/projects`, {
89
+ headers: apiKey ? { "x-api-key": apiKey } : {},
90
+ });
91
+ if (!res.ok)
92
+ return null;
93
+ const projects = await res.json();
94
+ // Extract repo name (e.g., "huskyv0" from "simon-sfxecom/huskyv0")
95
+ const repoName = repoIdentifier.split("/").pop()?.toLowerCase() || "";
96
+ // Try to match by:
97
+ // 1. githubRepo field (exact or partial match)
98
+ // 2. Project name similarity to repo name
99
+ const project = projects.find((p) => {
100
+ // Check githubRepo field first
101
+ if (p.githubRepo?.toLowerCase().includes(repoIdentifier.toLowerCase())) {
102
+ return true;
103
+ }
104
+ // Fallback: Check if project name matches repo name (case-insensitive)
105
+ const projectNameLower = p.name.toLowerCase().replace(/[^a-z0-9]/g, "");
106
+ const repoNameClean = repoName.replace(/[^a-z0-9]/g, "");
107
+ return projectNameLower === repoNameClean || projectNameLower.includes(repoNameClean) || repoNameClean.includes(projectNameLower);
108
+ });
109
+ return project ? { id: project.id, name: project.name } : null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
69
115
  // husky task list
70
116
  taskCommand
71
117
  .command("list")
72
118
  .description("List all tasks")
73
119
  .option("-s, --status <status>", "Filter by status")
120
+ .option("-a, --all", "Show all tasks (ignore current repo filter)")
121
+ .option("--project <id>", "Filter by project ID")
74
122
  .option("-p, --page <num>", "Page number (starts at 1)", "1")
75
123
  .option("-n, --per-page <num>", "Items per page (default: all)")
76
124
  .option("-i, --interactive", "Interactive pagination with arrow keys")
@@ -86,13 +134,39 @@ taskCommand
86
134
  if (options.status) {
87
135
  url.searchParams.set("status", options.status);
88
136
  }
137
+ // Auto-detect project from current repo (unless --all or --project specified)
138
+ let autoDetectedProject = null;
139
+ let filterProjectId = null;
140
+ if (!options.all && !options.project) {
141
+ const repoIdentifier = getGitRepoIdentifier();
142
+ if (repoIdentifier) {
143
+ autoDetectedProject = await findProjectByRepo(config.apiUrl, config.apiKey, repoIdentifier);
144
+ if (autoDetectedProject) {
145
+ filterProjectId = autoDetectedProject.id;
146
+ }
147
+ }
148
+ }
149
+ else if (options.project) {
150
+ filterProjectId = options.project;
151
+ }
152
+ // Note: We don't pass projectId to API to avoid Firestore index requirement
153
+ // Instead, we filter client-side which is fine for reasonable task counts
89
154
  const res = await fetch(url.toString(), {
90
155
  headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
91
156
  });
92
157
  if (!res.ok) {
93
158
  throw new Error(`API error: ${res.status}`);
94
159
  }
95
- const tasks = await res.json();
160
+ let tasks = await res.json();
161
+ // Client-side filtering by projectId (avoids Firestore composite index)
162
+ if (filterProjectId) {
163
+ tasks = tasks.filter(t => t.projectId === filterProjectId);
164
+ }
165
+ // Show filter info if auto-detected project
166
+ if (autoDetectedProject && !options.json) {
167
+ console.log(`\n 📁 Filtering by project: ${autoDetectedProject.name}`);
168
+ console.log(` Use --all to see all tasks\n`);
169
+ }
96
170
  // JSON output
97
171
  if (options.json) {
98
172
  console.log(JSON.stringify(tasks, null, 2));
@@ -103,12 +177,12 @@ taskCommand
103
177
  await paginateList({
104
178
  items: tasks,
105
179
  pageSize: 10,
106
- title: "Tasks",
180
+ title: autoDetectedProject ? `Tasks (${autoDetectedProject.name})` : "Tasks",
107
181
  emptyMessage: "No tasks found.",
108
182
  renderItem: (task) => {
109
183
  const statusIcon = task.status === "done" ? "✓" : task.status === "in_progress" ? "▶" : "○";
110
184
  const priorityIcon = task.priority === "urgent" ? "🔴" : task.priority === "high" ? "🟠" : "";
111
- return ` ${statusIcon} ${task.id.slice(0, 8)} │ ${task.title.slice(0, 40).padEnd(40)} ${priorityIcon}`;
185
+ return ` ${statusIcon} ${task.id.padEnd(20)} │ ${task.title.slice(0, 40).padEnd(40)} ${priorityIcon}`;
112
186
  },
113
187
  selectableItems: true,
114
188
  onSelect: async (task) => {
@@ -131,7 +205,7 @@ taskCommand
131
205
  const pageSize = parseInt(options.perPage, 10);
132
206
  printPaginated(tasks, pageNum, pageSize, (task) => {
133
207
  const statusIcon = task.status === "done" ? "✓" : task.status === "in_progress" ? "▶" : "○";
134
- return ` ${statusIcon} ${task.id.slice(0, 8)} │ ${task.title}`;
208
+ return ` ${statusIcon} ${task.id.padEnd(20)} │ ${task.title}`;
135
209
  }, "Tasks");
136
210
  return;
137
211
  }
@@ -1005,12 +1079,15 @@ function printTasks(tasks) {
1005
1079
  if (statusTasks.length === 0)
1006
1080
  continue;
1007
1081
  console.log(`\n ${label}`);
1008
- console.log(" " + "─".repeat(50));
1082
+ console.log(" " + "─".repeat(95));
1083
+ console.log(` ${"ID".padEnd(20)} ${"Title".padEnd(30)} ${"Project".padEnd(15)} ${"Project ID".padEnd(12)} ${"Priority".padEnd(6)}`);
1084
+ console.log(" " + "─".repeat(95));
1009
1085
  for (const task of statusTasks) {
1010
1086
  const agentStr = task.agent ? ` (${task.agent})` : "";
1011
1087
  const doneStr = status === "done" ? " ✓" : "";
1012
- const projectStr = (task.projectName || task.projectId)?.slice(0, 15).padEnd(15) || "—".padEnd(15);
1013
- console.log(` ${task.id.slice(0, 10).padEnd(10)} ${task.title.slice(0, 25).padEnd(25)} ${projectStr} ${task.priority.padEnd(6)}${agentStr}${doneStr}`);
1088
+ const projectName = task.projectName?.slice(0, 15).padEnd(15) || "—".padEnd(15);
1089
+ const projectId = task.projectId?.slice(0, 12).padEnd(12) || "—".padEnd(12);
1090
+ console.log(` ${task.id.padEnd(20)} ${task.title.slice(0, 30).padEnd(30)} ${projectName} ${projectId} ${task.priority.padEnd(6)}${agentStr}${doneStr}`);
1014
1091
  }
1015
1092
  }
1016
1093
  console.log("");
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const workerCommand: Command;
@@ -0,0 +1,210 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ import { getWorkerIdentity, ensureWorkerRegistered } from "../lib/worker.js";
4
+ export const workerCommand = new Command("worker")
5
+ .description("Manage workers and sessions");
6
+ // husky worker whoami - Show current worker identity
7
+ workerCommand
8
+ .command("whoami")
9
+ .description("Show current worker identity")
10
+ .option("--json", "Output as JSON")
11
+ .action(async (options) => {
12
+ const identity = getWorkerIdentity();
13
+ if (options.json) {
14
+ console.log(JSON.stringify(identity, null, 2));
15
+ }
16
+ else {
17
+ console.log("\n Worker Identity");
18
+ console.log(" " + "─".repeat(40));
19
+ console.log(` ID: ${identity.workerId}`);
20
+ console.log(` Name: ${identity.workerName}`);
21
+ console.log(` Host: ${identity.hostname}`);
22
+ console.log(` User: ${identity.username}`);
23
+ console.log(` Platform: ${identity.platform}`);
24
+ console.log(` Version: ${identity.agentVersion}`);
25
+ console.log("");
26
+ }
27
+ });
28
+ // husky worker register - Register worker with API
29
+ workerCommand
30
+ .command("register")
31
+ .description("Register this worker with the API")
32
+ .action(async () => {
33
+ const config = getConfig();
34
+ if (!config.apiUrl) {
35
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
36
+ process.exit(1);
37
+ }
38
+ try {
39
+ const workerId = await ensureWorkerRegistered(config.apiUrl, config.apiKey || "");
40
+ const identity = getWorkerIdentity();
41
+ console.log(`✓ Worker registered: ${workerId}`);
42
+ console.log(` Name: ${identity.workerName}`);
43
+ }
44
+ catch (error) {
45
+ console.error("Error registering worker:", error);
46
+ process.exit(1);
47
+ }
48
+ });
49
+ // husky worker list - List all registered workers
50
+ workerCommand
51
+ .command("list")
52
+ .description("List all registered workers")
53
+ .option("--json", "Output as JSON")
54
+ .action(async (options) => {
55
+ const config = getConfig();
56
+ if (!config.apiUrl) {
57
+ console.error("Error: API URL not configured.");
58
+ process.exit(1);
59
+ }
60
+ try {
61
+ const res = await fetch(`${config.apiUrl}/api/workers`, {
62
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
63
+ });
64
+ if (!res.ok) {
65
+ throw new Error(`API error: ${res.status}`);
66
+ }
67
+ const workers = await res.json();
68
+ if (options.json) {
69
+ console.log(JSON.stringify(workers, null, 2));
70
+ return;
71
+ }
72
+ if (workers.length === 0) {
73
+ console.log("No workers registered.");
74
+ return;
75
+ }
76
+ console.log("\n Registered Workers");
77
+ console.log(" " + "─".repeat(60));
78
+ for (const worker of workers) {
79
+ const statusIcon = worker.lastSeen && isRecent(worker.lastSeen) ? "🟢" : "⚪";
80
+ const typeIcon = worker.type === "claude-code" ? "🤖" : "👤";
81
+ console.log(` ${statusIcon} ${typeIcon} ${worker.id.slice(0, 12).padEnd(12)} │ ${worker.name?.slice(0, 30).padEnd(30)} │ ${worker.hostname || "—"}`);
82
+ }
83
+ console.log("");
84
+ }
85
+ catch (error) {
86
+ console.error("Error fetching workers:", error);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ // husky worker sessions - List active sessions
91
+ workerCommand
92
+ .command("sessions")
93
+ .description("List active worker sessions")
94
+ .option("--all", "Show all sessions (not just active)")
95
+ .option("--json", "Output as JSON")
96
+ .action(async (options) => {
97
+ const config = getConfig();
98
+ if (!config.apiUrl) {
99
+ console.error("Error: API URL not configured.");
100
+ process.exit(1);
101
+ }
102
+ try {
103
+ const endpoint = options.all
104
+ ? `${config.apiUrl}/api/workers/sessions`
105
+ : `${config.apiUrl}/api/workers/sessions/active`;
106
+ const res = await fetch(endpoint, {
107
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
108
+ });
109
+ if (!res.ok) {
110
+ throw new Error(`API error: ${res.status}`);
111
+ }
112
+ const sessions = await res.json();
113
+ if (options.json) {
114
+ console.log(JSON.stringify(sessions, null, 2));
115
+ return;
116
+ }
117
+ if (sessions.length === 0) {
118
+ console.log(options.all ? "No sessions found." : "No active sessions.");
119
+ return;
120
+ }
121
+ console.log(`\n ${options.all ? "All" : "Active"} Sessions`);
122
+ console.log(" " + "─".repeat(70));
123
+ for (const session of sessions) {
124
+ const statusIcon = session.status === "active" ? "🟢" : "⚪";
125
+ const taskInfo = session.currentTaskId ? `Task: ${session.currentTaskId.slice(0, 8)}` : "No task";
126
+ const timeAgo = session.lastHeartbeat ? formatTimeAgo(session.lastHeartbeat) : "—";
127
+ console.log(` ${statusIcon} ${session.id.slice(0, 12).padEnd(12)} │ ${session.workerId.slice(0, 12).padEnd(12)} │ ${taskInfo.padEnd(20)} │ ${timeAgo}`);
128
+ }
129
+ console.log("");
130
+ }
131
+ catch (error) {
132
+ console.error("Error fetching sessions:", error);
133
+ process.exit(1);
134
+ }
135
+ });
136
+ // husky worker activity - Show who is working on what
137
+ workerCommand
138
+ .command("activity")
139
+ .description("Show current activity (who is working on what)")
140
+ .option("--json", "Output as JSON")
141
+ .action(async (options) => {
142
+ const config = getConfig();
143
+ if (!config.apiUrl) {
144
+ console.error("Error: API URL not configured.");
145
+ process.exit(1);
146
+ }
147
+ try {
148
+ // Fetch active sessions and in-progress tasks in parallel
149
+ const [sessionsRes, tasksRes] = await Promise.all([
150
+ fetch(`${config.apiUrl}/api/workers/sessions/active`, {
151
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
152
+ }),
153
+ fetch(`${config.apiUrl}/api/tasks?status=in_progress`, {
154
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
155
+ }),
156
+ ]);
157
+ if (!sessionsRes.ok || !tasksRes.ok) {
158
+ throw new Error("API error fetching activity");
159
+ }
160
+ const sessions = await sessionsRes.json();
161
+ const tasks = await tasksRes.json();
162
+ if (options.json) {
163
+ console.log(JSON.stringify({ sessions, tasks }, null, 2));
164
+ return;
165
+ }
166
+ console.log("\n Current Activity");
167
+ console.log(" " + "═".repeat(70));
168
+ if (tasks.length === 0) {
169
+ console.log(" No tasks currently in progress.");
170
+ }
171
+ else {
172
+ for (const task of tasks) {
173
+ const agentIcon = task.agent?.includes("claude") ? "🤖" : task.agent ? "👤" : "❓";
174
+ const workerInfo = task.workerId ? `Worker: ${task.workerId.slice(0, 8)}` : "No worker";
175
+ console.log(`\n ${agentIcon} ${task.title.slice(0, 50)}`);
176
+ console.log(` ID: ${task.id.slice(0, 12)} │ ${task.agent || "unassigned"} │ ${workerInfo}`);
177
+ if (task.projectName) {
178
+ console.log(` Project: ${task.projectName}`);
179
+ }
180
+ }
181
+ }
182
+ console.log("\n " + "─".repeat(70));
183
+ console.log(` Active Sessions: ${sessions.length}`);
184
+ console.log(` Tasks In Progress: ${tasks.length}`);
185
+ console.log("");
186
+ }
187
+ catch (error) {
188
+ console.error("Error fetching activity:", error);
189
+ process.exit(1);
190
+ }
191
+ });
192
+ // Helper: Check if timestamp is recent (within 5 minutes)
193
+ function isRecent(timestamp) {
194
+ const diff = Date.now() - new Date(timestamp).getTime();
195
+ return diff < 5 * 60 * 1000; // 5 minutes
196
+ }
197
+ // Helper: Format time ago
198
+ function formatTimeAgo(timestamp) {
199
+ const diff = Date.now() - new Date(timestamp).getTime();
200
+ const minutes = Math.floor(diff / 60000);
201
+ const hours = Math.floor(diff / 3600000);
202
+ const days = Math.floor(diff / 86400000);
203
+ if (minutes < 1)
204
+ return "just now";
205
+ if (minutes < 60)
206
+ return `${minutes}m ago`;
207
+ if (hours < 24)
208
+ return `${hours}h ago`;
209
+ return `${days}d ago`;
210
+ }
package/dist/index.js CHANGED
@@ -13,17 +13,17 @@ import { workflowCommand } from "./commands/workflow.js";
13
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
- import { processCommand } from "./commands/process.js";
17
16
  import { settingsCommand } from "./commands/settings.js";
18
17
  import { strategyCommand } from "./commands/strategy.js";
19
18
  import { completionCommand } from "./commands/completion.js";
20
19
  import { worktreeCommand } from "./commands/worktree.js";
20
+ import { workerCommand } from "./commands/worker.js";
21
21
  import { runInteractiveMode } from "./commands/interactive.js";
22
22
  const program = new Command();
23
23
  program
24
24
  .name("husky")
25
25
  .description("CLI for Huskyv0 Task Orchestration with Claude Agent")
26
- .version("0.7.1");
26
+ .version("0.8.1");
27
27
  program.addCommand(taskCommand);
28
28
  program.addCommand(configCommand);
29
29
  program.addCommand(agentCommand);
@@ -37,11 +37,11 @@ program.addCommand(workflowCommand);
37
37
  program.addCommand(julesCommand);
38
38
  program.addCommand(vmCommand);
39
39
  program.addCommand(vmConfigCommand);
40
- program.addCommand(processCommand);
41
40
  program.addCommand(settingsCommand);
42
41
  program.addCommand(strategyCommand);
43
42
  program.addCommand(completionCommand);
44
43
  program.addCommand(worktreeCommand);
44
+ program.addCommand(workerCommand);
45
45
  // Check if no command was provided - run interactive mode
46
46
  if (process.argv.length <= 2) {
47
47
  runInteractiveMode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,4 +42,4 @@
42
42
  "bugs": {
43
43
  "url": "https://github.com/simon-sfxecom/huskyv0/issues"
44
44
  }
45
- }
45
+ }