@simonfestl/husky-cli 0.3.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.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # Husky CLI
2
+
3
+ CLI for Huskyv0 Task Orchestration with Claude Agent SDK integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # From GitHub (recommended for VMs)
9
+ npm install -g github:simon-sfxecom/husky-cli
10
+
11
+ # Local development
12
+ cd packages/cli
13
+ npm install
14
+ npm run build
15
+ npm link
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ ### Task Management
21
+
22
+ ```bash
23
+ # List tasks
24
+ husky task list
25
+ husky task list --status in_progress
26
+
27
+ # Create task
28
+ husky task create "Fix login bug" --priority high --project abc123
29
+
30
+ # Start/complete tasks
31
+ husky task start <task-id>
32
+ husky task done <task-id> --pr https://github.com/...
33
+ ```
34
+
35
+ ### Configuration
36
+
37
+ ```bash
38
+ # Set API URL and key
39
+ husky config set api-url https://your-husky-dashboard.run.app
40
+ husky config set api-key your-api-key
41
+
42
+ # View config
43
+ husky config list
44
+ ```
45
+
46
+ ### Agent Commands (VM Execution)
47
+
48
+ ```bash
49
+ # Generate execution plan
50
+ husky agent plan \
51
+ --session-id=<session-id> \
52
+ --prompt="Fix all TypeScript errors" \
53
+ --api-url=https://husky.example.com \
54
+ --api-key=<api-key> \
55
+ --anthropic-key=<anthropic-key> \
56
+ --workdir=/workspace
57
+
58
+ # Wait for user approval
59
+ husky agent wait-approval \
60
+ --session-id=<session-id> \
61
+ --api-url=https://husky.example.com \
62
+ --api-key=<api-key> \
63
+ --timeout=1800
64
+
65
+ # Execute approved plan
66
+ husky agent execute \
67
+ --session-id=<session-id> \
68
+ --api-url=https://husky.example.com \
69
+ --api-key=<api-key> \
70
+ --anthropic-key=<anthropic-key> \
71
+ --github-token=<github-token>
72
+ ```
73
+
74
+ ## Environment Variables
75
+
76
+ | Variable | Description |
77
+ |----------|-------------|
78
+ | `ANTHROPIC_API_KEY` | Anthropic API key for Claude |
79
+ | `HUSKY_API_URL` | Husky Dashboard URL |
80
+ | `HUSKY_API_KEY` | Husky API key |
81
+ | `GITHUB_TOKEN` | GitHub token for commits (optional) |
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ # Install dependencies
87
+ npm install
88
+
89
+ # Build
90
+ npm run build
91
+
92
+ # Watch mode
93
+ npm run dev
94
+ ```
95
+
96
+ ## Publishing / Release
97
+
98
+ Die CLI wird automatisch via GitHub Actions auf npm veroeffentlicht.
99
+
100
+ ### Neue Version veroeffentlichen
101
+
102
+ 1. **Version in `package.json` aktualisieren:**
103
+ ```bash
104
+ npm version patch # oder minor/major
105
+ ```
106
+
107
+ 2. **GitHub Release erstellen:**
108
+ - Gehe zu GitHub > Releases > "Create a new release"
109
+ - Tag muss zur Version passen (z.B. `v0.3.0` fuer Version `0.3.0`)
110
+ - Release Notes hinzufuegen
111
+ - "Publish release" klicken
112
+
113
+ 3. **Automatische Veroeffentlichung:**
114
+ - Der Workflow (`.github/workflows/publish.yml`) wird getriggert
115
+ - Package wird mit npm provenance auf npm veroeffentlicht
116
+
117
+ ### Manueller Trigger
118
+
119
+ Falls noetig, kann der Workflow auch manuell ausgeloest werden:
120
+ - GitHub > Actions > "Publish CLI to npm" > "Run workflow"
121
+
122
+ ### Voraussetzungen
123
+
124
+ | Secret | Beschreibung |
125
+ |--------|--------------|
126
+ | `NPM_TOKEN` | npm Access Token mit publish Berechtigung. Muss in GitHub Repo Settings > Secrets > Actions hinterlegt sein. |
127
+
128
+ ## Changelog
129
+
130
+ ### v0.2.0 (2025-01-05)
131
+ - Added `agent` commands (plan, execute, wait-approval)
132
+ - Added StreamClient with batching for efficient API calls
133
+ - Integration with Claude Agent SDK
134
+ - Support for planning phase before execution
135
+
136
+ ### v0.1.0 (2025-01-04)
137
+ - Initial release
138
+ - Task management commands (list, create, start, done)
139
+ - Configuration management
140
+ - API key authentication
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const agentCommand: Command;
@@ -0,0 +1,279 @@
1
+ import { Command } from "commander";
2
+ import { spawn } from "child_process";
3
+ import { StreamClient, updateSessionStatus, submitPlan, waitForApproval, } from "../lib/streaming.js";
4
+ // ============================================
5
+ // DEPRECATION NOTICE
6
+ // ============================================
7
+ // The 'husky agent' commands are DEPRECATED and will be removed in a future version.
8
+ //
9
+ // The new architecture has Claude Code (or any AI agent) as the main process,
10
+ // and uses 'husky task' commands as Bash tools for communication.
11
+ //
12
+ // Migration Guide:
13
+ // ----------------
14
+ // OLD (deprecated):
15
+ // husky agent plan --session-id xyz --prompt "..."
16
+ // husky agent wait-approval --session-id xyz
17
+ // husky agent execute --session-id xyz
18
+ //
19
+ // NEW (recommended):
20
+ // export HUSKY_TASK_ID="xyz"
21
+ // husky task status "Working on task..."
22
+ // husky task plan --summary "Plan description" --steps "step1,step2"
23
+ // husky task wait-approval --timeout 1800
24
+ // husky task complete --output "Done" --pr "https://..."
25
+ //
26
+ // The new approach is agent-agnostic - works with Claude Code, Gemini, Codex, etc.
27
+ // ============================================
28
+ function showDeprecationWarning(command) {
29
+ console.warn("\n" + "=".repeat(60));
30
+ console.warn("DEPRECATION WARNING");
31
+ console.warn("=".repeat(60));
32
+ console.warn(`The 'husky agent ${command}' command is deprecated.`);
33
+ console.warn("");
34
+ console.warn("Please migrate to the new 'husky task' commands:");
35
+ console.warn(" husky task status <message> - Report progress");
36
+ console.warn(" husky task plan --summary ... - Submit plan");
37
+ console.warn(" husky task wait-approval - Wait for approval");
38
+ console.warn(" husky task complete --output - Mark complete");
39
+ console.warn("");
40
+ console.warn("Set HUSKY_TASK_ID environment variable instead of --session-id");
41
+ console.warn("=".repeat(60) + "\n");
42
+ }
43
+ export const agentCommand = new Command("agent").description("[DEPRECATED] Run Claude Agent for automated code tasks. Use 'husky task' commands instead.");
44
+ // husky agent plan
45
+ agentCommand
46
+ .command("plan")
47
+ .description("Generate an execution plan using Claude")
48
+ .requiredOption("--session-id <id>", "VM Session ID")
49
+ .requiredOption("--prompt <prompt>", "Task prompt")
50
+ .requiredOption("--api-url <url>", "Husky API URL")
51
+ .requiredOption("--api-key <key>", "Husky API Key")
52
+ .requiredOption("--anthropic-key <key>", "Anthropic API Key")
53
+ .option("--workdir <path>", "Working directory", process.cwd())
54
+ .option("--max-budget <usd>", "Max budget in USD", "2.0")
55
+ .action(async (options) => {
56
+ showDeprecationWarning("plan");
57
+ const streamClient = new StreamClient(options.apiUrl, options.sessionId, options.apiKey);
58
+ try {
59
+ await streamClient.system("Starting plan generation...");
60
+ await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "planning");
61
+ // Use Claude Code CLI in plan mode
62
+ const planPrompt = `You are in PLAN MODE. Analyze the following task and create a detailed execution plan. Do NOT execute any changes yet.
63
+
64
+ TASK: ${options.prompt}
65
+
66
+ Create a structured plan with:
67
+ 1. Step-by-step actions needed
68
+ 2. Files that will be modified
69
+ 3. Risk assessment (low/medium/high) for each step
70
+ 4. Estimated time for execution
71
+
72
+ Output your plan in a clear, numbered format. After planning, use the ExitPlanMode tool to indicate completion.`;
73
+ await streamClient.system("Invoking Claude for planning...");
74
+ // Run Claude Code in print mode for planning
75
+ const result = await runClaudeCode(planPrompt, options.workdir, options.anthropicKey, streamClient, parseFloat(options.maxBudget));
76
+ // Parse the plan from Claude's output
77
+ const plan = parsePlanFromOutput(result.output);
78
+ // Submit plan to Husky
79
+ await submitPlan(options.apiUrl, options.sessionId, options.apiKey, plan);
80
+ await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "awaiting_approval");
81
+ await streamClient.system("Plan submitted. Waiting for approval...");
82
+ console.log("Plan generated successfully");
83
+ process.exit(0);
84
+ }
85
+ catch (error) {
86
+ await streamClient.stderr(`Plan generation failed: ${error}`);
87
+ await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "failed", { lastError: String(error) });
88
+ console.error("Plan generation failed:", error);
89
+ process.exit(1);
90
+ }
91
+ });
92
+ // husky agent execute
93
+ agentCommand
94
+ .command("execute")
95
+ .description("Execute the approved plan")
96
+ .requiredOption("--session-id <id>", "VM Session ID")
97
+ .requiredOption("--api-url <url>", "Husky API URL")
98
+ .requiredOption("--api-key <key>", "Husky API Key")
99
+ .requiredOption("--anthropic-key <key>", "Anthropic API Key")
100
+ .option("--workdir <path>", "Working directory", process.cwd())
101
+ .option("--github-token <token>", "GitHub token for commits")
102
+ .option("--max-budget <usd>", "Max budget in USD", "5.0")
103
+ .action(async (options) => {
104
+ showDeprecationWarning("execute");
105
+ const streamClient = new StreamClient(options.apiUrl, options.sessionId, options.apiKey);
106
+ try {
107
+ await streamClient.system("Starting execution...");
108
+ await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "running");
109
+ // Set GitHub token if provided
110
+ if (options.githubToken) {
111
+ process.env.GITHUB_TOKEN = options.githubToken;
112
+ }
113
+ // Fetch the original prompt from the session
114
+ const sessionResponse = await fetch(`${options.apiUrl}/api/vm-sessions/${options.sessionId}`, {
115
+ headers: { "X-API-Key": options.apiKey },
116
+ });
117
+ if (!sessionResponse.ok) {
118
+ throw new Error("Failed to fetch session details");
119
+ }
120
+ const session = await sessionResponse.json();
121
+ const prompt = session.prompt;
122
+ await streamClient.system(`Executing task: ${prompt}`);
123
+ // Run Claude Code to execute the task
124
+ const result = await runClaudeCode(prompt, options.workdir, options.anthropicKey, streamClient, parseFloat(options.maxBudget));
125
+ await streamClient.system(`Execution completed with exit code: ${result.exitCode}`);
126
+ // Report completion
127
+ await fetch(`${options.apiUrl}/api/webhooks/vm/completion`, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ "X-API-Key": options.apiKey,
132
+ },
133
+ body: JSON.stringify({
134
+ sessionId: options.sessionId,
135
+ exitCode: result.exitCode,
136
+ output: result.output,
137
+ }),
138
+ });
139
+ console.log("Execution completed");
140
+ process.exit(result.exitCode);
141
+ }
142
+ catch (error) {
143
+ await streamClient.stderr(`Execution failed: ${error}`);
144
+ await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "failed", { lastError: String(error) });
145
+ console.error("Execution failed:", error);
146
+ process.exit(1);
147
+ }
148
+ });
149
+ // husky agent wait-approval
150
+ agentCommand
151
+ .command("wait-approval")
152
+ .description("Wait for plan approval")
153
+ .requiredOption("--session-id <id>", "VM Session ID")
154
+ .requiredOption("--api-url <url>", "Husky API URL")
155
+ .requiredOption("--api-key <key>", "Husky API Key")
156
+ .option("--timeout <seconds>", "Timeout in seconds", "1800")
157
+ .action(async (options) => {
158
+ showDeprecationWarning("wait-approval");
159
+ const timeoutMs = parseInt(options.timeout) * 1000;
160
+ console.error(`Waiting for approval (timeout: ${options.timeout}s)...`);
161
+ const result = await waitForApproval(options.apiUrl, options.sessionId, options.apiKey, timeoutMs);
162
+ // Output result to stdout for shell script to capture
163
+ console.log(result);
164
+ process.exit(result === "approved" ? 0 : 1);
165
+ });
166
+ /**
167
+ * Run Claude Code CLI and stream output
168
+ */
169
+ async function runClaudeCode(prompt, workdir, anthropicKey, streamClient, maxBudgetUsd) {
170
+ return new Promise((resolve, reject) => {
171
+ const outputLines = [];
172
+ // Spawn Claude Code process
173
+ const claude = spawn("claude", [
174
+ "-p",
175
+ prompt,
176
+ "--output-format",
177
+ "stream-json",
178
+ "--max-turns",
179
+ "50",
180
+ ], {
181
+ cwd: workdir,
182
+ env: {
183
+ ...process.env,
184
+ ANTHROPIC_API_KEY: anthropicKey,
185
+ },
186
+ stdio: ["pipe", "pipe", "pipe"],
187
+ });
188
+ claude.stdout.on("data", async (data) => {
189
+ const text = data.toString();
190
+ outputLines.push(text);
191
+ // Try to parse JSON messages
192
+ const lines = text.split("\n").filter(Boolean);
193
+ for (const line of lines) {
194
+ try {
195
+ const msg = JSON.parse(line);
196
+ if (msg.type === "assistant" && msg.message?.content) {
197
+ for (const block of msg.message.content) {
198
+ if (block.type === "text") {
199
+ await streamClient.stdout(block.text);
200
+ }
201
+ else if (block.type === "tool_use") {
202
+ await streamClient.system(`Using tool: ${block.name}`);
203
+ }
204
+ }
205
+ }
206
+ else if (msg.type === "result") {
207
+ await streamClient.system(`Cost: $${msg.cost_usd?.toFixed(4) || "unknown"}`);
208
+ }
209
+ }
210
+ catch {
211
+ // Not JSON, send as plain text
212
+ await streamClient.stdout(line);
213
+ }
214
+ }
215
+ });
216
+ claude.stderr.on("data", async (data) => {
217
+ const text = data.toString();
218
+ outputLines.push(text);
219
+ await streamClient.stderr(text);
220
+ });
221
+ claude.on("error", (error) => {
222
+ reject(error);
223
+ });
224
+ claude.on("close", (code) => {
225
+ resolve({
226
+ exitCode: code ?? 1,
227
+ output: outputLines.join("\n").slice(0, 500000), // Limit to 500KB
228
+ });
229
+ });
230
+ });
231
+ }
232
+ /**
233
+ * Parse plan from Claude's output
234
+ */
235
+ function parsePlanFromOutput(output) {
236
+ // Simple parsing - extract numbered steps
237
+ const steps = [];
238
+ const lines = output.split("\n");
239
+ let currentStep = 0;
240
+ for (const line of lines) {
241
+ // Match numbered steps like "1." or "Step 1:"
242
+ const stepMatch = line.match(/^(?:Step\s+)?(\d+)[.):]\s*(.+)/i);
243
+ if (stepMatch) {
244
+ currentStep = parseInt(stepMatch[1]);
245
+ const description = stepMatch[2].trim();
246
+ // Extract file paths mentioned
247
+ const fileMatches = description.match(/`([^`]+\.[a-z]+)`/g) || [];
248
+ const files = fileMatches.map((f) => f.replace(/`/g, ""));
249
+ // Determine risk level based on keywords
250
+ let risk = "low";
251
+ if (/delete|remove|drop|danger/i.test(description)) {
252
+ risk = "high";
253
+ }
254
+ else if (/modify|change|update|refactor/i.test(description)) {
255
+ risk = "medium";
256
+ }
257
+ steps.push({
258
+ order: currentStep,
259
+ description,
260
+ files,
261
+ risk,
262
+ });
263
+ }
264
+ }
265
+ // If no steps found, create a generic one
266
+ if (steps.length === 0) {
267
+ steps.push({
268
+ order: 1,
269
+ description: "Execute task as specified",
270
+ files: [],
271
+ risk: "medium",
272
+ });
273
+ }
274
+ return {
275
+ steps,
276
+ estimatedCost: 0.5, // Placeholder
277
+ estimatedRuntime: 5, // 5 minutes placeholder
278
+ };
279
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ interface Config {
3
+ apiUrl?: string;
4
+ apiKey?: string;
5
+ }
6
+ export declare function getConfig(): Config;
7
+ export declare const configCommand: Command;
8
+ export {};
@@ -0,0 +1,73 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ const CONFIG_DIR = join(homedir(), ".husky");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ export function getConfig() {
8
+ try {
9
+ if (!existsSync(CONFIG_FILE)) {
10
+ return {};
11
+ }
12
+ const content = readFileSync(CONFIG_FILE, "utf-8");
13
+ return JSON.parse(content);
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ function saveConfig(config) {
20
+ if (!existsSync(CONFIG_DIR)) {
21
+ mkdirSync(CONFIG_DIR, { recursive: true });
22
+ }
23
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+ export const configCommand = new Command("config")
26
+ .description("Manage CLI configuration");
27
+ // husky config set <key> <value>
28
+ configCommand
29
+ .command("set <key> <value>")
30
+ .description("Set a configuration value")
31
+ .action((key, value) => {
32
+ const config = getConfig();
33
+ if (key === "api-url") {
34
+ config.apiUrl = value;
35
+ }
36
+ else if (key === "api-key") {
37
+ config.apiKey = value;
38
+ }
39
+ else {
40
+ console.error(`Unknown config key: ${key}`);
41
+ console.log("Available keys: api-url, api-key");
42
+ process.exit(1);
43
+ }
44
+ saveConfig(config);
45
+ console.log(`✓ Set ${key} = ${key === "api-key" ? "***" : value}`);
46
+ });
47
+ // husky config get <key>
48
+ configCommand
49
+ .command("get <key>")
50
+ .description("Get a configuration value")
51
+ .action((key) => {
52
+ const config = getConfig();
53
+ if (key === "api-url") {
54
+ console.log(config.apiUrl || "(not set)");
55
+ }
56
+ else if (key === "api-key") {
57
+ console.log(config.apiKey ? "***" : "(not set)");
58
+ }
59
+ else {
60
+ console.error(`Unknown config key: ${key}`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ // husky config list
65
+ configCommand
66
+ .command("list")
67
+ .description("List all configuration")
68
+ .action(() => {
69
+ const config = getConfig();
70
+ console.log("Configuration:");
71
+ console.log(` api-url: ${config.apiUrl || "(not set)"}`);
72
+ console.log(` api-key: ${config.apiKey ? "***" : "(not set)"}`);
73
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const roadmapCommand: Command;