@simonfestl/husky-cli 1.10.0 → 1.12.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 +126 -0
- package/dist/commands/auth.js +176 -1
- package/dist/commands/config.d.ts +21 -0
- package/dist/commands/config.js +34 -2
- package/dist/commands/task.js +4 -2
- package/dist/index.js +2 -0
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +117 -0
- package/dist/lib/biz/sop.js +1 -14
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -143,6 +143,76 @@ husky e2e list --task <id> # List artifacts
|
|
|
143
143
|
husky e2e clean --older-than 7 # Clean old artifacts
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
### PR Management (PR Agent)
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
husky pr list # List open PRs
|
|
150
|
+
husky pr get <pr-number> # Get PR details
|
|
151
|
+
husky pr review <pr-number> # Start review
|
|
152
|
+
husky pr approve <pr-number> # Approve PR
|
|
153
|
+
husky pr request-changes <pr-number> --comment "..."
|
|
154
|
+
husky pr merge <pr-number> # Merge PR
|
|
155
|
+
husky pr close <pr-number> # Close PR
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Infrastructure (DevOps)
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
husky infra status # Overall infra status
|
|
162
|
+
husky infra vms # List all VMs
|
|
163
|
+
husky infra services # Cloud Run services
|
|
164
|
+
husky infra logs <service> # Service logs
|
|
165
|
+
husky infra metrics # Resource metrics
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### YouTube Summarization
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
husky youtube <url> # Summarize video with Gemini AI
|
|
172
|
+
husky youtube <url> --remember # Also store in Second Brain
|
|
173
|
+
husky youtube <url> --json # JSON output
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Image Generation
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
husky image "a futuristic city" # Generate image with Imagen 3
|
|
180
|
+
husky image "..." --output ./image.png # Save to file
|
|
181
|
+
husky image "..." --aspect 16:9 # Aspect ratio
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Mermaid Diagrams
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
husky mermaid validate <file> # Validate Mermaid syntax
|
|
188
|
+
husky mermaid validate --stdin # Validate from stdin
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Service Accounts
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
husky sa list # List service accounts
|
|
195
|
+
husky sa create <name> --role worker # Create service account
|
|
196
|
+
husky sa get <id> # Get details
|
|
197
|
+
husky sa delete <id> # Delete service account
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Agent Messaging
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
husky agent-msg send <to> "message" # Send to another agent
|
|
204
|
+
husky agent-msg inbox # Check inbox
|
|
205
|
+
husky agent-msg read <id> # Read message
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Preview Deployments
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
husky preview list # List PR previews
|
|
212
|
+
husky preview get <pr-number> # Get preview URL
|
|
213
|
+
husky preview logs <pr-number> # Preview logs
|
|
214
|
+
```
|
|
215
|
+
|
|
146
216
|
### Business Strategy
|
|
147
217
|
|
|
148
218
|
```bash
|
|
@@ -249,6 +319,41 @@ husky config list
|
|
|
249
319
|
husky config test
|
|
250
320
|
```
|
|
251
321
|
|
|
322
|
+
### Authentication (Session Tokens)
|
|
323
|
+
|
|
324
|
+
Session tokens provide short-lived JWT authentication for agents. They are created using `HUSKY_API_KEY` and auto-refresh when expired.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Login (creates 1-hour session token)
|
|
328
|
+
husky auth login --agent supervisor
|
|
329
|
+
husky auth login --agent husky-worker-1
|
|
330
|
+
|
|
331
|
+
# Check session status
|
|
332
|
+
husky auth session
|
|
333
|
+
husky auth session --json
|
|
334
|
+
|
|
335
|
+
# Refresh token manually
|
|
336
|
+
husky auth refresh
|
|
337
|
+
husky auth refresh --agent supervisor
|
|
338
|
+
|
|
339
|
+
# Logout (clear session)
|
|
340
|
+
husky auth logout
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**VM Startup Pattern:**
|
|
344
|
+
```bash
|
|
345
|
+
#!/bin/bash
|
|
346
|
+
VM_NAME=$(hostname)
|
|
347
|
+
husky auth login --agent "$VM_NAME"
|
|
348
|
+
# All subsequent commands use Bearer token
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**How it works:**
|
|
352
|
+
1. `HUSKY_API_KEY` is used once to create a session token
|
|
353
|
+
2. All subsequent API calls use `Authorization: Bearer <token>`
|
|
354
|
+
3. Token auto-refreshes when expired or within 5 minutes of expiry
|
|
355
|
+
4. Falls back to `x-api-key` if refresh fails
|
|
356
|
+
|
|
252
357
|
### Help & Documentation
|
|
253
358
|
|
|
254
359
|
```bash
|
|
@@ -328,6 +433,27 @@ husky --version
|
|
|
328
433
|
|
|
329
434
|
## Changelog
|
|
330
435
|
|
|
436
|
+
### v1.12.0 (2026-01-12) - Session Token Authentication
|
|
437
|
+
|
|
438
|
+
**New Features:**
|
|
439
|
+
- `husky auth login --agent <name>` - Create session token from HUSKY_API_KEY
|
|
440
|
+
- `husky auth logout` - Clear session token
|
|
441
|
+
- `husky auth session` - Show session status (agent, role, expiry)
|
|
442
|
+
- `husky auth refresh` - Manually refresh token
|
|
443
|
+
|
|
444
|
+
**Improvements:**
|
|
445
|
+
- All API calls now use Bearer token authentication (auto-refresh)
|
|
446
|
+
- Token auto-refreshes when expired or within 5 minutes of expiry
|
|
447
|
+
- Falls back to x-api-key for backwards compatibility
|
|
448
|
+
- JWT_SECRET now required in production (fail-fast)
|
|
449
|
+
|
|
450
|
+
**Documentation:**
|
|
451
|
+
- Added missing command sections: pr, infra, youtube, image, mermaid, sa, agent-msg, preview
|
|
452
|
+
- Updated architecture docs with session token flow
|
|
453
|
+
|
|
454
|
+
**Code Quality:**
|
|
455
|
+
- Removed `as any` type suppression in sop.ts
|
|
456
|
+
|
|
331
457
|
### v1.7.0 (2026-01-11) - E2E Agent Production Ready
|
|
332
458
|
|
|
333
459
|
**New Features:**
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { getConfig } from "./config.js";
|
|
2
|
+
import { getConfig, setSessionConfig, clearSessionConfig, getSessionConfig } from "./config.js";
|
|
3
3
|
import { getPermissions, clearPermissionsCache, getCacheStatus, hasPermission, canAccessKnowledgeBase } from "../lib/permissions-cache.js";
|
|
4
4
|
const API_KEY_ROLES = [
|
|
5
5
|
"admin", "supervisor", "worker", "reviewer", "support",
|
|
@@ -260,3 +260,178 @@ authCommand
|
|
|
260
260
|
process.exit(1);
|
|
261
261
|
}
|
|
262
262
|
});
|
|
263
|
+
authCommand
|
|
264
|
+
.command("login")
|
|
265
|
+
.description("Create a session token for this agent")
|
|
266
|
+
.requiredOption("--agent <name>", "Agent name (must be registered in Firestore)")
|
|
267
|
+
.option("--json", "Output as JSON")
|
|
268
|
+
.action(async (options) => {
|
|
269
|
+
try {
|
|
270
|
+
const config = getConfig();
|
|
271
|
+
if (!config.apiUrl || !config.apiKey) {
|
|
272
|
+
console.error("API not configured. Run: husky config set api-url <url> && husky config set api-key <key>");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
const url = new URL("/api/auth/session", config.apiUrl);
|
|
276
|
+
const res = await fetch(url.toString(), {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"x-api-key": config.apiKey,
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ agent: options.agent }),
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
const error = await res.json().catch(() => ({ error: res.statusText }));
|
|
286
|
+
if (res.status === 404) {
|
|
287
|
+
console.error(`Agent '${options.agent}' not found. Register the agent first.`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.error(`Login failed: ${error.message || error.error || `HTTP ${res.status}`}`);
|
|
291
|
+
}
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const session = await res.json();
|
|
295
|
+
setSessionConfig(session);
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(JSON.stringify({
|
|
298
|
+
success: true,
|
|
299
|
+
agent: session.agent,
|
|
300
|
+
role: session.role,
|
|
301
|
+
expiresAt: session.expiresAt,
|
|
302
|
+
}, null, 2));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const expiresAt = new Date(session.expiresAt);
|
|
306
|
+
console.log("\n✅ Session created");
|
|
307
|
+
console.log("─".repeat(40));
|
|
308
|
+
console.log(`Agent: ${session.agent}`);
|
|
309
|
+
console.log(`Role: ${session.role}`);
|
|
310
|
+
console.log(`Expires: ${expiresAt.toLocaleString()}`);
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log("All API calls will now use this session token.");
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
authCommand
|
|
320
|
+
.command("logout")
|
|
321
|
+
.description("Clear the current session token")
|
|
322
|
+
.option("--json", "Output as JSON")
|
|
323
|
+
.action(async (options) => {
|
|
324
|
+
const session = getSessionConfig();
|
|
325
|
+
if (!session) {
|
|
326
|
+
if (options.json) {
|
|
327
|
+
console.log(JSON.stringify({ success: false, message: "No active session" }));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
console.log("No active session to clear.");
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
clearSessionConfig();
|
|
335
|
+
if (options.json) {
|
|
336
|
+
console.log(JSON.stringify({ success: true, agent: session.agent }));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
console.log(`✅ Session cleared for agent '${session.agent}'`);
|
|
340
|
+
});
|
|
341
|
+
authCommand
|
|
342
|
+
.command("session")
|
|
343
|
+
.description("Show current session status")
|
|
344
|
+
.option("--json", "Output as JSON")
|
|
345
|
+
.action(async (options) => {
|
|
346
|
+
const session = getSessionConfig();
|
|
347
|
+
if (!session || !session.token) {
|
|
348
|
+
if (options.json) {
|
|
349
|
+
console.log(JSON.stringify({ active: false }));
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.log("No active session. Run: husky auth login --agent <name>");
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const expiresAt = session.expiresAt ? new Date(session.expiresAt) : null;
|
|
357
|
+
const now = new Date();
|
|
358
|
+
const isExpired = expiresAt ? expiresAt < now : true;
|
|
359
|
+
const expiresInMs = expiresAt ? expiresAt.getTime() - now.getTime() : 0;
|
|
360
|
+
const expiresInMinutes = Math.max(0, Math.floor(expiresInMs / 60000));
|
|
361
|
+
if (options.json) {
|
|
362
|
+
console.log(JSON.stringify({
|
|
363
|
+
active: !isExpired,
|
|
364
|
+
agent: session.agent,
|
|
365
|
+
role: session.role,
|
|
366
|
+
expiresAt: session.expiresAt,
|
|
367
|
+
expired: isExpired,
|
|
368
|
+
expiresInMinutes,
|
|
369
|
+
}, null, 2));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log("\n🔐 Session Status");
|
|
373
|
+
console.log("─".repeat(40));
|
|
374
|
+
console.log(`Agent: ${session.agent || "(unknown)"}`);
|
|
375
|
+
console.log(`Role: ${session.role || "(unknown)"}`);
|
|
376
|
+
if (isExpired) {
|
|
377
|
+
console.log(`Status: 🔴 EXPIRED`);
|
|
378
|
+
console.log(`Expired: ${expiresAt?.toLocaleString() || "(unknown)"}`);
|
|
379
|
+
console.log("");
|
|
380
|
+
console.log("Run: husky auth refresh --agent <name>");
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
console.log(`Status: 🟢 ACTIVE`);
|
|
384
|
+
console.log(`Expires: ${expiresAt?.toLocaleString()} (${expiresInMinutes} minutes)`);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
authCommand
|
|
388
|
+
.command("refresh")
|
|
389
|
+
.description("Refresh the session token")
|
|
390
|
+
.option("--agent <name>", "Agent name (uses current session agent if not specified)")
|
|
391
|
+
.option("--json", "Output as JSON")
|
|
392
|
+
.action(async (options) => {
|
|
393
|
+
try {
|
|
394
|
+
const config = getConfig();
|
|
395
|
+
if (!config.apiUrl || !config.apiKey) {
|
|
396
|
+
console.error("API not configured. Run: husky config set api-url <url> && husky config set api-key <key>");
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
const currentSession = getSessionConfig();
|
|
400
|
+
const agentName = options.agent || currentSession?.agent;
|
|
401
|
+
if (!agentName) {
|
|
402
|
+
console.error("No agent specified and no active session. Use: husky auth refresh --agent <name>");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const url = new URL("/api/auth/session", config.apiUrl);
|
|
406
|
+
const res = await fetch(url.toString(), {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: {
|
|
409
|
+
"x-api-key": config.apiKey,
|
|
410
|
+
"Content-Type": "application/json",
|
|
411
|
+
},
|
|
412
|
+
body: JSON.stringify({ agent: agentName }),
|
|
413
|
+
});
|
|
414
|
+
if (!res.ok) {
|
|
415
|
+
const error = await res.json().catch(() => ({ error: res.statusText }));
|
|
416
|
+
console.error(`Refresh failed: ${error.message || error.error || `HTTP ${res.status}`}`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
const session = await res.json();
|
|
420
|
+
setSessionConfig(session);
|
|
421
|
+
if (options.json) {
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
success: true,
|
|
424
|
+
agent: session.agent,
|
|
425
|
+
role: session.role,
|
|
426
|
+
expiresAt: session.expiresAt,
|
|
427
|
+
}, null, 2));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const expiresAt = new Date(session.expiresAt);
|
|
431
|
+
console.log(`✅ Session refreshed for '${session.agent}' (expires: ${expiresAt.toLocaleString()})`);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
@@ -3,6 +3,10 @@ type AgentRole = "supervisor" | "worker" | "reviewer" | "e2e_agent" | "pr_agent"
|
|
|
3
3
|
interface Config {
|
|
4
4
|
apiUrl?: string;
|
|
5
5
|
apiKey?: string;
|
|
6
|
+
sessionToken?: string;
|
|
7
|
+
sessionExpiresAt?: string;
|
|
8
|
+
sessionAgent?: string;
|
|
9
|
+
sessionRole?: string;
|
|
6
10
|
workerId?: string;
|
|
7
11
|
workerName?: string;
|
|
8
12
|
role?: AgentRole;
|
|
@@ -28,8 +32,12 @@ interface Config {
|
|
|
28
32
|
nocodbApiToken?: string;
|
|
29
33
|
nocodbBaseUrl?: string;
|
|
30
34
|
nocodbWorkspaceId?: string;
|
|
35
|
+
skuterzoneUsername?: string;
|
|
36
|
+
skuterzonePassword?: string;
|
|
37
|
+
skuterzoneBaseUrl?: string;
|
|
31
38
|
}
|
|
32
39
|
export declare function getConfig(): Config;
|
|
40
|
+
export declare function saveConfig(config: Config): void;
|
|
33
41
|
/**
|
|
34
42
|
* Fetch role and permissions from /api/auth/whoami
|
|
35
43
|
* Caches the result in config for 1 hour
|
|
@@ -52,5 +60,18 @@ export declare function getRole(): AgentRole | undefined;
|
|
|
52
60
|
export declare function clearRoleCache(): void;
|
|
53
61
|
export declare function setConfig(key: "apiUrl" | "apiKey" | "workerId" | "workerName", value: string): void;
|
|
54
62
|
export declare function setGotessConfig(token: string, bookId: string): void;
|
|
63
|
+
export declare function setSessionConfig(session: {
|
|
64
|
+
token: string;
|
|
65
|
+
expiresAt: string;
|
|
66
|
+
agent: string;
|
|
67
|
+
role: string;
|
|
68
|
+
}): void;
|
|
69
|
+
export declare function clearSessionConfig(): void;
|
|
70
|
+
export declare function getSessionConfig(): {
|
|
71
|
+
token?: string;
|
|
72
|
+
expiresAt?: string;
|
|
73
|
+
agent?: string;
|
|
74
|
+
role?: string;
|
|
75
|
+
} | null;
|
|
55
76
|
export declare const configCommand: Command;
|
|
56
77
|
export {};
|
package/dist/commands/config.js
CHANGED
|
@@ -41,7 +41,7 @@ export function getConfig() {
|
|
|
41
41
|
return {};
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
function saveConfig(config) {
|
|
44
|
+
export function saveConfig(config) {
|
|
45
45
|
if (!existsSync(CONFIG_DIR)) {
|
|
46
46
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
47
47
|
}
|
|
@@ -127,6 +127,33 @@ export function setGotessConfig(token, bookId) {
|
|
|
127
127
|
config.gotessBookId = bookId;
|
|
128
128
|
saveConfig(config);
|
|
129
129
|
}
|
|
130
|
+
export function setSessionConfig(session) {
|
|
131
|
+
const config = getConfig();
|
|
132
|
+
config.sessionToken = session.token;
|
|
133
|
+
config.sessionExpiresAt = session.expiresAt;
|
|
134
|
+
config.sessionAgent = session.agent;
|
|
135
|
+
config.sessionRole = session.role;
|
|
136
|
+
saveConfig(config);
|
|
137
|
+
}
|
|
138
|
+
export function clearSessionConfig() {
|
|
139
|
+
const config = getConfig();
|
|
140
|
+
delete config.sessionToken;
|
|
141
|
+
delete config.sessionExpiresAt;
|
|
142
|
+
delete config.sessionAgent;
|
|
143
|
+
delete config.sessionRole;
|
|
144
|
+
saveConfig(config);
|
|
145
|
+
}
|
|
146
|
+
export function getSessionConfig() {
|
|
147
|
+
const config = getConfig();
|
|
148
|
+
if (!config.sessionToken)
|
|
149
|
+
return null;
|
|
150
|
+
return {
|
|
151
|
+
token: config.sessionToken,
|
|
152
|
+
expiresAt: config.sessionExpiresAt,
|
|
153
|
+
agent: config.sessionAgent,
|
|
154
|
+
role: config.sessionRole,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
130
157
|
export const configCommand = new Command("config")
|
|
131
158
|
.description("Manage CLI configuration");
|
|
132
159
|
// husky config set <key> <value>
|
|
@@ -166,6 +193,10 @@ configCommand
|
|
|
166
193
|
"nocodb-api-token": "nocodbApiToken",
|
|
167
194
|
"nocodb-base-url": "nocodbBaseUrl",
|
|
168
195
|
"nocodb-workspace-id": "nocodbWorkspaceId",
|
|
196
|
+
// Skuterzone
|
|
197
|
+
"skuterzone-username": "skuterzoneUsername",
|
|
198
|
+
"skuterzone-password": "skuterzonePassword",
|
|
199
|
+
"skuterzone-base-url": "skuterzoneBaseUrl",
|
|
169
200
|
};
|
|
170
201
|
const configKey = keyMappings[key];
|
|
171
202
|
if (!configKey) {
|
|
@@ -180,6 +211,7 @@ configCommand
|
|
|
180
211
|
console.log(" Gotess: gotess-token, gotess-book-id");
|
|
181
212
|
console.log(" Gemini: gemini-api-key");
|
|
182
213
|
console.log(" NocoDB: nocodb-api-token, nocodb-base-url, nocodb-workspace-id");
|
|
214
|
+
console.log(" Skuterzone: skuterzone-username, skuterzone-password, skuterzone-base-url");
|
|
183
215
|
console.log(" Brain: agent-type");
|
|
184
216
|
console.error("\n💡 For configuration help: husky explain config");
|
|
185
217
|
process.exit(1);
|
|
@@ -201,7 +233,7 @@ configCommand
|
|
|
201
233
|
config[configKey] = value;
|
|
202
234
|
saveConfig(config);
|
|
203
235
|
// Mask sensitive values in output
|
|
204
|
-
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token"];
|
|
236
|
+
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token", "skuterzone-username", "skuterzone-password"];
|
|
205
237
|
const displayValue = sensitiveKeys.includes(key) ? "***" : value;
|
|
206
238
|
console.log(`✓ Set ${key} = ${displayValue}`);
|
|
207
239
|
});
|
package/dist/commands/task.js
CHANGED
|
@@ -829,12 +829,14 @@ taskCommand
|
|
|
829
829
|
throw new Error(`API error: ${res.status}`);
|
|
830
830
|
}
|
|
831
831
|
const data = await res.json();
|
|
832
|
-
if (data.
|
|
832
|
+
if (data.approved === true && data.pending === false) {
|
|
833
833
|
console.log("✓ Plan approved!");
|
|
834
834
|
process.exit(0);
|
|
835
835
|
}
|
|
836
|
-
else if (data.
|
|
836
|
+
else if (data.approved === false && data.rejected === true) {
|
|
837
837
|
console.log("✗ Plan rejected");
|
|
838
|
+
if (data.reason)
|
|
839
|
+
console.log(`Reason: ${data.reason}`);
|
|
838
840
|
process.exit(1);
|
|
839
841
|
}
|
|
840
842
|
// Still pending, wait and poll again
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { e2eCommand } from "./commands/e2e.js";
|
|
|
34
34
|
import { prCommand } from "./commands/pr.js";
|
|
35
35
|
import { youtubeCommand } from "./commands/youtube.js";
|
|
36
36
|
import { imageCommand } from "./commands/image.js";
|
|
37
|
+
import { authCommand } from "./commands/auth.js";
|
|
37
38
|
// Read version from package.json
|
|
38
39
|
const require = createRequire(import.meta.url);
|
|
39
40
|
const packageJson = require("../package.json");
|
|
@@ -75,6 +76,7 @@ program.addCommand(e2eCommand);
|
|
|
75
76
|
program.addCommand(prCommand);
|
|
76
77
|
program.addCommand(youtubeCommand);
|
|
77
78
|
program.addCommand(imageCommand);
|
|
79
|
+
program.addCommand(authCommand);
|
|
78
80
|
// Handle --llm flag specially
|
|
79
81
|
if (process.argv.includes("--llm")) {
|
|
80
82
|
printLLMContext();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ApiRequestOptions {
|
|
2
|
+
method?: string;
|
|
3
|
+
body?: unknown;
|
|
4
|
+
skipAuth?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function apiRequest<T>(path: string, options?: ApiRequestOptions): Promise<T>;
|
|
7
|
+
export declare function getApiClient(): {
|
|
8
|
+
get: <T>(path: string) => Promise<T>;
|
|
9
|
+
post: <T>(path: string, body?: unknown) => Promise<T>;
|
|
10
|
+
put: <T>(path: string, body?: unknown) => Promise<T>;
|
|
11
|
+
patch: <T>(path: string, body?: unknown) => Promise<T>;
|
|
12
|
+
delete: <T>(path: string) => Promise<T>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getConfig, getSessionConfig, setSessionConfig, clearSessionConfig } from "../commands/config.js";
|
|
2
|
+
const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
|
|
3
|
+
let refreshInProgress = null;
|
|
4
|
+
function isSessionExpired(expiresAt) {
|
|
5
|
+
if (!expiresAt)
|
|
6
|
+
return true;
|
|
7
|
+
return new Date(expiresAt).getTime() < Date.now();
|
|
8
|
+
}
|
|
9
|
+
function isSessionExpiringSoon(expiresAt) {
|
|
10
|
+
if (!expiresAt)
|
|
11
|
+
return true;
|
|
12
|
+
return new Date(expiresAt).getTime() - Date.now() < REFRESH_THRESHOLD_MS;
|
|
13
|
+
}
|
|
14
|
+
async function doRefresh(agentName) {
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
if (!config.apiUrl || !config.apiKey)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL("/api/auth/session", config.apiUrl);
|
|
20
|
+
const res = await fetch(url.toString(), {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
"x-api-key": config.apiKey,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({ agent: agentName }),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return null;
|
|
30
|
+
const session = await res.json();
|
|
31
|
+
setSessionConfig(session);
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function refreshSession(agentName) {
|
|
39
|
+
if (refreshInProgress) {
|
|
40
|
+
return refreshInProgress;
|
|
41
|
+
}
|
|
42
|
+
refreshInProgress = doRefresh(agentName).finally(() => {
|
|
43
|
+
refreshInProgress = null;
|
|
44
|
+
});
|
|
45
|
+
return refreshInProgress;
|
|
46
|
+
}
|
|
47
|
+
async function doFetch(url, method, authHeader, body) {
|
|
48
|
+
return fetch(url.toString(), {
|
|
49
|
+
method,
|
|
50
|
+
headers: {
|
|
51
|
+
...authHeader,
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
},
|
|
54
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function getAuthHeader(session, apiKey) {
|
|
58
|
+
if (session?.token && session.expiresAt) {
|
|
59
|
+
if (isSessionExpired(session.expiresAt)) {
|
|
60
|
+
if (session.agent) {
|
|
61
|
+
const newSession = await refreshSession(session.agent);
|
|
62
|
+
if (newSession) {
|
|
63
|
+
return { "Authorization": `Bearer ${newSession.token}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
clearSessionConfig();
|
|
67
|
+
if (apiKey) {
|
|
68
|
+
return { "x-api-key": apiKey };
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Session expired and no API key available for refresh");
|
|
71
|
+
}
|
|
72
|
+
if (isSessionExpiringSoon(session.expiresAt) && session.agent) {
|
|
73
|
+
refreshSession(session.agent).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
return { "Authorization": `Bearer ${session.token}` };
|
|
76
|
+
}
|
|
77
|
+
if (apiKey) {
|
|
78
|
+
return { "x-api-key": apiKey };
|
|
79
|
+
}
|
|
80
|
+
throw new Error("No authentication configured. Run: husky auth login --agent <name> or husky config set api-key <key>");
|
|
81
|
+
}
|
|
82
|
+
export async function apiRequest(path, options = {}) {
|
|
83
|
+
const config = getConfig();
|
|
84
|
+
if (!config.apiUrl) {
|
|
85
|
+
throw new Error("API URL not configured. Run: husky config set api-url <url>");
|
|
86
|
+
}
|
|
87
|
+
const session = getSessionConfig();
|
|
88
|
+
const method = options.method || "GET";
|
|
89
|
+
const url = new URL(path, config.apiUrl);
|
|
90
|
+
const authHeader = options.skipAuth
|
|
91
|
+
? {}
|
|
92
|
+
: await getAuthHeader(session, config.apiKey);
|
|
93
|
+
const res = await doFetch(url, method, authHeader, options.body);
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
if (res.status === 401 && session?.token && session.agent) {
|
|
96
|
+
const newSession = await refreshSession(session.agent);
|
|
97
|
+
if (newSession) {
|
|
98
|
+
const retryRes = await doFetch(url, method, { "Authorization": `Bearer ${newSession.token}` }, options.body);
|
|
99
|
+
if (retryRes.ok) {
|
|
100
|
+
return retryRes.json();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const error = await res.json().catch(() => ({ error: res.statusText }));
|
|
105
|
+
throw new Error(error.message || error.error || `HTTP ${res.status}`);
|
|
106
|
+
}
|
|
107
|
+
return res.json();
|
|
108
|
+
}
|
|
109
|
+
export function getApiClient() {
|
|
110
|
+
return {
|
|
111
|
+
get: (path) => apiRequest(path),
|
|
112
|
+
post: (path, body) => apiRequest(path, { method: "POST", body }),
|
|
113
|
+
put: (path, body) => apiRequest(path, { method: "PUT", body }),
|
|
114
|
+
patch: (path, body) => apiRequest(path, { method: "PATCH", body }),
|
|
115
|
+
delete: (path) => apiRequest(path, { method: "DELETE" }),
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/lib/biz/sop.js
CHANGED
|
@@ -282,21 +282,8 @@ export class SOPService {
|
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
async scrollPoints(collection, options) {
|
|
285
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
286
|
-
const qdrantAny = this.qdrant;
|
|
287
285
|
try {
|
|
288
|
-
|
|
289
|
-
method: 'POST',
|
|
290
|
-
body: JSON.stringify({
|
|
291
|
-
filter: options.filter,
|
|
292
|
-
limit: options.limit || 50,
|
|
293
|
-
with_payload: options.with_payload ?? true,
|
|
294
|
-
}),
|
|
295
|
-
});
|
|
296
|
-
return response.result.points.map((p) => ({
|
|
297
|
-
id: p.id,
|
|
298
|
-
payload: p.payload || {},
|
|
299
|
-
}));
|
|
286
|
+
return await this.qdrant.scroll(collection, options);
|
|
300
287
|
}
|
|
301
288
|
catch (error) {
|
|
302
289
|
console.error('Scroll failed:', error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonfestl/husky-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@inquirer/prompts": "^8.1.0",
|
|
26
26
|
"commander": "^12.1.0",
|
|
27
27
|
"firebase-admin": "^13.6.0",
|
|
28
|
+
"playwright": "^1.57.0",
|
|
28
29
|
"sharp": "^0.34.5",
|
|
29
30
|
"youtube-transcript": "^1.2.1",
|
|
30
31
|
"zod": "^4.3.5"
|