@simonfestl/husky-cli 1.38.4 ā 1.38.6
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/agent-msg.js +9 -4
- package/dist/commands/biz/gotess.js +10 -4
- package/dist/commands/chat.js +33 -37
- package/dist/commands/config.d.ts +9 -6
- package/dist/commands/config.js +103 -127
- package/dist/commands/interactive/auth.js +7 -2
- package/dist/commands/research.d.ts +2 -0
- package/dist/commands/research.js +774 -0
- package/dist/commands/supervisor.js +0 -1
- package/dist/index.js +46 -1
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/api-client.js +16 -83
- package/dist/lib/biz/api-brain.js +6 -23
- package/dist/lib/biz/emove-playwright.js +0 -1
- package/dist/lib/biz/index.d.ts +2 -0
- package/dist/lib/biz/index.js +2 -0
- package/dist/lib/biz/reddit.d.ts +83 -0
- package/dist/lib/biz/reddit.js +168 -0
- package/dist/lib/biz/skuterzone-playwright.js +0 -1
- package/dist/lib/biz/x-client.d.ts +27 -0
- package/dist/lib/biz/x-client.js +123 -0
- package/dist/lib/biz/youtube-monitor.d.ts +72 -0
- package/dist/lib/biz/youtube-monitor.js +180 -0
- package/dist/lib/permissions-cache.js +9 -35
- package/package.json +1 -1
package/dist/commands/config.js
CHANGED
|
@@ -74,79 +74,49 @@ export function saveConfig(config) {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Fetch role and permissions from /api/auth/whoami
|
|
77
|
-
* Uses session token (Bearer)
|
|
77
|
+
* Uses session token (Bearer) only.
|
|
78
78
|
* Caches the result in config for 1 hour.
|
|
79
79
|
*/
|
|
80
80
|
export async function fetchAndCacheRole() {
|
|
81
81
|
const config = getConfig();
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// First try the role-specific permissions endpoint
|
|
93
|
-
let url = new URL(`/api/auth/permissions/${encodeURIComponent(config.sessionRole)}`, config.apiUrl);
|
|
94
|
-
let res = await fetch(url.toString(), {
|
|
95
|
-
headers: { "x-api-key": config.apiKey },
|
|
96
|
-
});
|
|
97
|
-
// If role-specific endpoint fails, fall back to whoami with session token
|
|
98
|
-
if (!res.ok && config.sessionToken) {
|
|
99
|
-
url = new URL("/api/auth/whoami", config.apiUrl);
|
|
100
|
-
res = await fetch(url.toString(), {
|
|
101
|
-
headers: { "Authorization": `Bearer ${config.sessionToken}` },
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (res.ok) {
|
|
105
|
-
const data = await res.json();
|
|
106
|
-
// Update permissions cache (keep sessionRole as source of truth for role)
|
|
107
|
-
config.permissions = data.permissions;
|
|
108
|
-
config.roleLastChecked = new Date().toISOString();
|
|
109
|
-
saveConfig(config);
|
|
110
|
-
return { role: config.sessionRole, permissions: data.permissions };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// Fall through to use cached permissions
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Return session role with cached permissions
|
|
118
|
-
return { role: config.sessionRole, permissions: config.permissions };
|
|
119
|
-
}
|
|
82
|
+
// Session token is the only supported auth mode for runtime requests.
|
|
83
|
+
if (!config.sessionToken || !config.sessionRole || !config.sessionExpiresAt) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
if (!isValidRole(config.sessionRole)) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const expiresAt = new Date(config.sessionExpiresAt);
|
|
90
|
+
if (expiresAt <= new Date()) {
|
|
91
|
+
return {};
|
|
120
92
|
}
|
|
121
|
-
// No active session - use API key auth
|
|
122
93
|
// Check if we have cached role that's less than 5 minutes old
|
|
123
94
|
// Short TTL ensures revoked permissions are detected quickly
|
|
124
95
|
const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
125
|
-
if (config.
|
|
96
|
+
if (config.permissions && config.roleLastChecked) {
|
|
126
97
|
const lastChecked = new Date(config.roleLastChecked);
|
|
127
98
|
const cacheExpiry = new Date(Date.now() - PERMISSION_CACHE_TTL_MS);
|
|
128
99
|
if (lastChecked > cacheExpiry) {
|
|
129
|
-
return { role: config.
|
|
100
|
+
return { role: config.sessionRole, permissions: config.permissions };
|
|
130
101
|
}
|
|
131
102
|
}
|
|
132
|
-
//
|
|
133
|
-
if (!config.apiUrl
|
|
134
|
-
return {};
|
|
103
|
+
// If API URL is unavailable, return the session role with cached permissions.
|
|
104
|
+
if (!config.apiUrl) {
|
|
105
|
+
return { role: config.sessionRole, permissions: config.permissions };
|
|
135
106
|
}
|
|
136
107
|
try {
|
|
137
108
|
const api = getApiClient();
|
|
138
109
|
const data = await api.get("/api/auth/whoami");
|
|
139
|
-
//
|
|
140
|
-
config.role = data.role;
|
|
110
|
+
// Keep sessionRole as source of truth; refresh permissions from whoami.
|
|
141
111
|
config.permissions = data.permissions;
|
|
142
112
|
config.roleLastChecked = new Date().toISOString();
|
|
143
113
|
saveConfig(config);
|
|
144
|
-
return { role:
|
|
114
|
+
return { role: config.sessionRole, permissions: data.permissions };
|
|
145
115
|
}
|
|
146
116
|
catch {
|
|
147
117
|
// Ignore fetch errors, return cached or empty
|
|
148
118
|
}
|
|
149
|
-
return { role: config.
|
|
119
|
+
return { role: config.sessionRole, permissions: config.permissions };
|
|
150
120
|
}
|
|
151
121
|
/**
|
|
152
122
|
* Check if current config has a specific permission
|
|
@@ -166,7 +136,7 @@ export function hasPermission(permission) {
|
|
|
166
136
|
}
|
|
167
137
|
/**
|
|
168
138
|
* Get current role from config.
|
|
169
|
-
*
|
|
139
|
+
* Uses sessionRole (from auth login) when session is active.
|
|
170
140
|
* Validates that the role is a known valid role before returning.
|
|
171
141
|
*/
|
|
172
142
|
export function getRole() {
|
|
@@ -184,10 +154,6 @@ export function getRole() {
|
|
|
184
154
|
return undefined;
|
|
185
155
|
}
|
|
186
156
|
}
|
|
187
|
-
// Fall back to API key role (also validate)
|
|
188
|
-
if (config.role && isValidRole(config.role)) {
|
|
189
|
-
return config.role;
|
|
190
|
-
}
|
|
191
157
|
return undefined;
|
|
192
158
|
}
|
|
193
159
|
/**
|
|
@@ -250,15 +216,15 @@ export function isSessionActive() {
|
|
|
250
216
|
}
|
|
251
217
|
/**
|
|
252
218
|
* Check if the API is properly configured for making requests.
|
|
253
|
-
* Returns true if we have
|
|
219
|
+
* Returns true if we have an API URL and an active session.
|
|
254
220
|
*/
|
|
255
221
|
export function isApiConfigured() {
|
|
256
222
|
const config = getConfig();
|
|
257
|
-
return Boolean(config.apiUrl &&
|
|
223
|
+
return Boolean(config.apiUrl && isSessionActive());
|
|
258
224
|
}
|
|
259
225
|
/**
|
|
260
226
|
* Get authentication headers for API requests.
|
|
261
|
-
* Returns Bearer token if session is active
|
|
227
|
+
* Returns Bearer token if session is active.
|
|
262
228
|
* Use this for all API calls to ensure consistent auth.
|
|
263
229
|
*/
|
|
264
230
|
export function getAuthHeaders() {
|
|
@@ -271,56 +237,70 @@ export function getAuthHeaders() {
|
|
|
271
237
|
return { Authorization: `Bearer ${config.sessionToken}` };
|
|
272
238
|
}
|
|
273
239
|
}
|
|
274
|
-
// Fall back to API key
|
|
275
|
-
if (config.apiKey) {
|
|
276
|
-
return { "x-api-key": config.apiKey };
|
|
277
|
-
}
|
|
278
240
|
return {};
|
|
279
241
|
}
|
|
280
242
|
/**
|
|
281
243
|
* Ensure the session is valid, refreshing if needed.
|
|
282
|
-
*
|
|
283
|
-
* Returns true if session is valid (or was refreshed), false if no session/refresh failed.
|
|
244
|
+
* Returns true if session is active, false otherwise.
|
|
284
245
|
*/
|
|
285
246
|
export async function ensureValidSession() {
|
|
286
247
|
const config = getConfig();
|
|
248
|
+
// No session at all
|
|
287
249
|
if (!config.sessionToken || !config.sessionExpiresAt) {
|
|
288
|
-
return false;
|
|
250
|
+
return false;
|
|
289
251
|
}
|
|
290
252
|
const expiresAt = new Date(config.sessionExpiresAt);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
253
|
+
const msLeft = expiresAt.getTime() - Date.now();
|
|
254
|
+
// Refresh if expiring soon (5 min) to avoid 401 mid-command.
|
|
255
|
+
const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
|
|
256
|
+
const shouldRefresh = msLeft <= REFRESH_THRESHOLD_MS;
|
|
257
|
+
if (!shouldRefresh) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (!config.apiUrl) {
|
|
261
|
+
// Can't refresh without API URL. If the token is already expired, fail.
|
|
262
|
+
if (msLeft <= 0) {
|
|
263
|
+
clearSessionConfig();
|
|
264
|
+
return false;
|
|
297
265
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const data = await res.json();
|
|
310
|
-
setSessionConfig({
|
|
311
|
-
token: data.token,
|
|
312
|
-
expiresAt: data.expiresAt,
|
|
313
|
-
agent: data.agent,
|
|
314
|
-
role: data.role,
|
|
315
|
-
});
|
|
316
|
-
return true;
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch(new URL("/api/auth/refresh", config.apiUrl).toString(), {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { Authorization: `Bearer ${config.sessionToken}` },
|
|
272
|
+
});
|
|
273
|
+
if (!res.ok) {
|
|
274
|
+
if (msLeft <= 0) {
|
|
275
|
+
clearSessionConfig();
|
|
276
|
+
return false;
|
|
317
277
|
}
|
|
278
|
+
// Keep the current token (it is still valid but close to expiry).
|
|
279
|
+
return true;
|
|
318
280
|
}
|
|
319
|
-
|
|
320
|
-
|
|
281
|
+
const data = await res.json();
|
|
282
|
+
if (!data.token || !data.expiresAt) {
|
|
283
|
+
if (msLeft <= 0) {
|
|
284
|
+
clearSessionConfig();
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
321
288
|
}
|
|
289
|
+
setSessionConfig({
|
|
290
|
+
token: data.token,
|
|
291
|
+
expiresAt: data.expiresAt,
|
|
292
|
+
agent: data.agent?.id || config.sessionAgent || "unknown",
|
|
293
|
+
role: data.role || config.sessionRole || "worker",
|
|
294
|
+
});
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
if (msLeft <= 0) {
|
|
299
|
+
clearSessionConfig();
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
322
303
|
}
|
|
323
|
-
return expiresAt > now;
|
|
324
304
|
}
|
|
325
305
|
export function getSessionConfig() {
|
|
326
306
|
const config = getConfig();
|
|
@@ -386,6 +366,10 @@ configCommand
|
|
|
386
366
|
"wattiz-base-url": "wattizBaseUrl",
|
|
387
367
|
"wattiz-language": "wattizLanguage",
|
|
388
368
|
"gtasks-subject": "gtasksSubject",
|
|
369
|
+
"reddit-client-id": "redditClientId",
|
|
370
|
+
"reddit-client-secret": "redditClientSecret",
|
|
371
|
+
"youtube-api-key": "youtubeApiKey",
|
|
372
|
+
"x-bearer-token": "xBearerToken",
|
|
389
373
|
};
|
|
390
374
|
const configKey = keyMappings[key];
|
|
391
375
|
if (!configKey) {
|
|
@@ -404,6 +388,7 @@ configCommand
|
|
|
404
388
|
console.log(" Emove: emove-username, emove-password, emove-base-url");
|
|
405
389
|
console.log(" Wattiz: wattiz-username, wattiz-password, wattiz-base-url, wattiz-language");
|
|
406
390
|
console.log(" GTasks: gtasks-subject");
|
|
391
|
+
console.log(" Research: reddit-client-id, reddit-client-secret, youtube-api-key, x-bearer-token");
|
|
407
392
|
console.log(" Brain: agent-type");
|
|
408
393
|
console.error("\nš” For configuration help: husky explain config");
|
|
409
394
|
process.exit(1);
|
|
@@ -425,7 +410,7 @@ configCommand
|
|
|
425
410
|
config[configKey] = value;
|
|
426
411
|
saveConfig(config);
|
|
427
412
|
// Mask sensitive values in output
|
|
428
|
-
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", "emove-username", "emove-password", "wattiz-username", "wattiz-password"];
|
|
413
|
+
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", "emove-username", "emove-password", "wattiz-username", "wattiz-password", "reddit-client-secret", "youtube-api-key", "x-bearer-token"];
|
|
429
414
|
const displayValue = sensitiveKeys.includes(key) ? "***" : value;
|
|
430
415
|
console.log(`ā Set ${key} = ${displayValue}`);
|
|
431
416
|
});
|
|
@@ -466,8 +451,9 @@ configCommand
|
|
|
466
451
|
if (!config.apiUrl) {
|
|
467
452
|
ErrorHelpers.missingApiUrl();
|
|
468
453
|
}
|
|
469
|
-
if (!
|
|
470
|
-
|
|
454
|
+
if (!isSessionActive()) {
|
|
455
|
+
console.error("No active session. Run: husky auth login --agent <name>");
|
|
456
|
+
process.exit(1);
|
|
471
457
|
}
|
|
472
458
|
console.log("Testing API connection...");
|
|
473
459
|
try {
|
|
@@ -475,51 +461,41 @@ configCommand
|
|
|
475
461
|
// First test basic connectivity with /api/tasks
|
|
476
462
|
await api.get("/api/tasks");
|
|
477
463
|
console.log(`API connection successful (API URL: ${config.apiUrl})`);
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const updatedConfig = getConfig();
|
|
494
|
-
updatedConfig.role = data.role;
|
|
495
|
-
updatedConfig.permissions = data.permissions;
|
|
496
|
-
updatedConfig.roleLastChecked = new Date().toISOString();
|
|
497
|
-
saveConfig(updatedConfig);
|
|
498
|
-
console.log(`\nRBAC Info (API Key):`);
|
|
499
|
-
console.log(` Role: ${data.role || "(not assigned)"}`);
|
|
500
|
-
if (data.permissions && data.permissions.length > 0) {
|
|
501
|
-
console.log(` Permissions: ${data.permissions.join(", ")}`);
|
|
502
|
-
}
|
|
503
|
-
if (data.agentId) {
|
|
504
|
-
console.log(` Agent ID: ${data.agentId}`);
|
|
505
|
-
}
|
|
464
|
+
// Show session info and refresh permission cache from whoami.
|
|
465
|
+
console.log(`\nSession Info:`);
|
|
466
|
+
console.log(` Agent: ${config.sessionAgent || "(unknown)"}`);
|
|
467
|
+
console.log(` Role: ${config.sessionRole || "(unknown)"}`);
|
|
468
|
+
console.log(` Expires: ${config.sessionExpiresAt ? new Date(config.sessionExpiresAt).toLocaleString() : "(unknown)"}`);
|
|
469
|
+
try {
|
|
470
|
+
const data = await api.get("/api/auth/whoami");
|
|
471
|
+
const updatedConfig = getConfig();
|
|
472
|
+
updatedConfig.permissions = data.permissions;
|
|
473
|
+
updatedConfig.roleLastChecked = new Date().toISOString();
|
|
474
|
+
saveConfig(updatedConfig);
|
|
475
|
+
console.log(`\nRBAC Info (Session):`);
|
|
476
|
+
console.log(` Role: ${data.role || config.sessionRole || "(unknown)"}`);
|
|
477
|
+
if (data.permissions && data.permissions.length > 0) {
|
|
478
|
+
console.log(` Permissions: ${data.permissions.join(", ")}`);
|
|
506
479
|
}
|
|
507
|
-
|
|
508
|
-
|
|
480
|
+
if (data.agentId) {
|
|
481
|
+
console.log(` Agent ID: ${data.agentId}`);
|
|
509
482
|
}
|
|
510
483
|
}
|
|
484
|
+
catch {
|
|
485
|
+
// whoami failed, but tasks worked - connection is fine
|
|
486
|
+
}
|
|
511
487
|
}
|
|
512
488
|
catch (error) {
|
|
513
489
|
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
514
490
|
if (errorMsg.includes("401")) {
|
|
515
491
|
console.error(`API connection failed: Unauthorized (HTTP 401)`);
|
|
516
|
-
console.error("
|
|
492
|
+
console.error(" Session is missing or expired. Run: husky auth login --agent <name>");
|
|
517
493
|
console.error("\n For configuration help: husky explain config");
|
|
518
494
|
process.exit(1);
|
|
519
495
|
}
|
|
520
496
|
else if (errorMsg.includes("403")) {
|
|
521
497
|
console.error(`API connection failed: Forbidden (HTTP 403)`);
|
|
522
|
-
console.error(" Your
|
|
498
|
+
console.error(" Your session role may not have the required permissions");
|
|
523
499
|
console.error("\n For configuration help: husky explain config");
|
|
524
500
|
process.exit(1);
|
|
525
501
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides menu-based authentication and API key management.
|
|
5
5
|
*/
|
|
6
6
|
import { select, input, confirm } from "@inquirer/prompts";
|
|
7
|
-
import { getConfig, setConfig, setSessionConfig, clearSessionConfig
|
|
7
|
+
import { getConfig, setConfig, setSessionConfig, clearSessionConfig } from "../config.js";
|
|
8
8
|
import { pressEnterToContinue } from "./utils.js";
|
|
9
9
|
export async function authMenu() {
|
|
10
10
|
console.log("\n AUTH");
|
|
@@ -55,6 +55,11 @@ async function login() {
|
|
|
55
55
|
await pressEnterToContinue();
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
+
if (!config.apiKey) {
|
|
59
|
+
console.error("\n Error: API key not configured. Run 'husky config set api-key <key>' first.\n");
|
|
60
|
+
await pressEnterToContinue();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
58
63
|
const agentName = await input({
|
|
59
64
|
message: "Agent name:",
|
|
60
65
|
default: "interactive-cli",
|
|
@@ -65,7 +70,7 @@ async function login() {
|
|
|
65
70
|
method: "POST",
|
|
66
71
|
headers: {
|
|
67
72
|
"Content-Type": "application/json",
|
|
68
|
-
|
|
73
|
+
"x-api-key": config.apiKey,
|
|
69
74
|
},
|
|
70
75
|
body: JSON.stringify({ agent: agentName }),
|
|
71
76
|
});
|