@simonfestl/husky-cli 1.25.2 → 1.25.4

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.
@@ -7,7 +7,10 @@ function toDate(value) {
7
7
  return value instanceof Date ? value : new Date(value);
8
8
  }
9
9
  function createBrain(agentId, agentType, options) {
10
- if (options?.useApi || (shouldUseApi() && options?.kb)) {
10
+ // Use API if:
11
+ // 1. Explicitly requested via --use-api
12
+ // 2. shouldUseApi() returns true (no Qdrant configured or session token available)
13
+ if (options?.useApi || shouldUseApi()) {
11
14
  return new ApiBrain({
12
15
  agentId,
13
16
  agentType: isValidAgentType(agentType) ? agentType : undefined,
@@ -5,10 +5,13 @@
5
5
  import { select, input } from "@inquirer/prompts";
6
6
  import { pressEnterToContinue } from "./utils.js";
7
7
  import { ZendeskClient, BillbeeClient, SeaTableClient, QdrantClient, EmbeddingService } from "../../lib/biz/index.js";
8
+ import { requireAnyPermission } from "../../lib/permissions.js";
8
9
  // ============================================
9
10
  // MAIN BUSINESS MENU
10
11
  // ============================================
11
12
  export async function businessMenu() {
13
+ // RBAC: Require biz permissions
14
+ requireAnyPermission(["biz:tickets", "biz:orders", "biz:customers", "biz:*"]);
12
15
  const menuItems = [
13
16
  { name: "🎫 Tickets (Zendesk)", value: "tickets" },
14
17
  { name: "👤 Customers (Billbee)", value: "customers" },
@@ -6,6 +6,7 @@
6
6
  import { select, input, confirm } from "@inquirer/prompts";
7
7
  import { getAuthHeaders } from "../config.js";
8
8
  import { ensureConfig, pressEnterToContinue, truncate, formatDate } from "./utils.js";
9
+ import { requireAnyPermission } from "../../lib/permissions.js";
9
10
  export async function infraMenu() {
10
11
  const config = ensureConfig();
11
12
  console.log("\n INFRASTRUCTURE");
@@ -209,6 +210,8 @@ async function showAlerts(config) {
209
210
  }
210
211
  }
211
212
  async function restartService(config) {
213
+ // RBAC: Require infra update permission for destructive operations
214
+ requireAnyPermission(["infra:update", "infra:*"]);
212
215
  try {
213
216
  const serviceName = await input({
214
217
  message: "Service name to restart:",
@@ -6,6 +6,7 @@
6
6
  import { select, input, confirm } from "@inquirer/prompts";
7
7
  import { pressEnterToContinue, truncate } from "./utils.js";
8
8
  import { spawnSync } from "child_process";
9
+ import { requireAnyPermission } from "../../lib/permissions.js";
9
10
  /**
10
11
  * Safe command execution using spawnSync
11
12
  */
@@ -158,6 +159,8 @@ async function checkStatus() {
158
159
  }
159
160
  }
160
161
  async function createPR() {
162
+ // RBAC: Require PR create permission
163
+ requireAnyPermission(["pr:create", "pr:*"]);
161
164
  try {
162
165
  const title = await input({
163
166
  message: "PR title:",
@@ -196,6 +199,8 @@ async function createPR() {
196
199
  }
197
200
  }
198
201
  async function mergePR() {
202
+ // RBAC: Require PR merge permission
203
+ requireAnyPermission(["pr:merge", "pr:*"]);
199
204
  try {
200
205
  const prNumber = await input({
201
206
  message: "PR number to merge:",
@@ -2,6 +2,7 @@ import { select, input, confirm } from "@inquirer/prompts";
2
2
  import { getAuthHeaders } from "../config.js";
3
3
  import { ensureConfig, pressEnterToContinue, truncate } from "./utils.js";
4
4
  import { resolveProject } from "../../lib/project-resolver.js";
5
+ import { requirePermission } from "../../lib/permissions.js";
5
6
  export async function tasksMenu() {
6
7
  const config = ensureConfig();
7
8
  const menuItems = [
@@ -386,6 +387,8 @@ async function updateTask(config) {
386
387
  }
387
388
  }
388
389
  async function startTask(config) {
390
+ // RBAC: Require task:start permission
391
+ requirePermission("task:start");
389
392
  try {
390
393
  const task = await selectTask(config, "Select task to start:");
391
394
  if (!task)
@@ -408,6 +411,8 @@ async function startTask(config) {
408
411
  }
409
412
  }
410
413
  async function markTaskDone(config) {
414
+ // RBAC: Require task:done permission
415
+ requirePermission("task:done");
411
416
  try {
412
417
  const task = await selectTask(config, "Select task to mark as done:");
413
418
  if (!task)
@@ -1,6 +1,7 @@
1
1
  import { select, input, confirm } from "@inquirer/prompts";
2
2
  import { getAuthHeaders } from "../config.js";
3
3
  import { ensureConfig, pressEnterToContinue, truncate } from "./utils.js";
4
+ import { requireAnyPermission } from "../../lib/permissions.js";
4
5
  export async function vmSessionsMenu() {
5
6
  const config = ensureConfig();
6
7
  const menuItems = [
@@ -127,6 +128,8 @@ async function viewVMSession(config) {
127
128
  }
128
129
  }
129
130
  async function createVMSession(config) {
131
+ // RBAC: Require VM create permission
132
+ requireAnyPermission(["vm:create", "vm:*"]);
130
133
  try {
131
134
  const name = await input({
132
135
  message: "Session name:",
@@ -179,6 +182,8 @@ async function createVMSession(config) {
179
182
  }
180
183
  }
181
184
  async function startVM(config) {
185
+ // RBAC: Require VM manage permission
186
+ requireAnyPermission(["vm:manage", "vm:*"]);
182
187
  try {
183
188
  const session = await selectVMSession(config, "Select session to start:");
184
189
  if (!session)
@@ -202,6 +207,8 @@ async function startVM(config) {
202
207
  }
203
208
  }
204
209
  async function stopVM(config) {
210
+ // RBAC: Require VM manage permission
211
+ requireAnyPermission(["vm:manage", "vm:*"]);
205
212
  try {
206
213
  const session = await selectVMSession(config, "Select session to stop:");
207
214
  if (!session)
@@ -288,6 +295,8 @@ async function approvePlan(config) {
288
295
  }
289
296
  }
290
297
  async function deleteVMSession(config) {
298
+ // RBAC: Require VM manage permission
299
+ requireAnyPermission(["vm:manage", "vm:*"]);
291
300
  try {
292
301
  const session = await selectVMSession(config, "Select session to delete:");
293
302
  if (!session)
@@ -2,20 +2,45 @@ import { getConfig } from "../../commands/config.js";
2
2
  import { canAccessKnowledgeBase as checkKbAccess } from "../permissions-cache.js";
3
3
  async function apiRequest(path, options = {}) {
4
4
  const config = getConfig();
5
- if (!config.apiUrl || !config.apiKey) {
6
- throw new Error("API not configured. Run: husky config set api-url <url> && husky config set api-key <key>");
5
+ if (!config.apiUrl) {
6
+ throw new Error("API not configured. Run: husky config set api-url <url>");
7
+ }
8
+ // Prefer session token (JWT), fall back to API key for backwards compatibility
9
+ const headers = {
10
+ "Content-Type": "application/json",
11
+ };
12
+ if (config.sessionToken) {
13
+ // Check if session is expired
14
+ if (config.sessionExpiresAt) {
15
+ const expiresAt = new Date(config.sessionExpiresAt);
16
+ if (expiresAt < new Date()) {
17
+ throw new Error("Session expired. Run: husky auth login --agent <name>");
18
+ }
19
+ }
20
+ headers["Authorization"] = `Bearer ${config.sessionToken}`;
21
+ }
22
+ else if (config.apiKey) {
23
+ // Legacy fallback - will be rejected by new API but kept for error message
24
+ headers["x-api-key"] = config.apiKey;
25
+ }
26
+ else {
27
+ throw new Error("Not authenticated. Run: husky auth login --agent <name>");
7
28
  }
8
29
  const url = new URL(`/api/brain${path}`, config.apiUrl);
9
30
  const res = await fetch(url.toString(), {
10
31
  method: options.method || "POST",
11
- headers: {
12
- "x-api-key": config.apiKey,
13
- "Content-Type": "application/json",
14
- },
32
+ headers,
15
33
  body: options.body ? JSON.stringify(options.body) : undefined,
16
34
  });
17
35
  if (!res.ok) {
18
36
  const error = await res.json().catch(() => ({ error: res.statusText }));
37
+ if (res.status === 401) {
38
+ // Check if it's the new API rejecting API key auth
39
+ if (error.upgrade) {
40
+ throw new Error("Session required. Run: husky auth login --agent <name>");
41
+ }
42
+ throw new Error(error.message || "Authentication failed");
43
+ }
19
44
  if (res.status === 403) {
20
45
  throw new Error(`Access denied: ${error.error || 'Permission denied to this resource'}`);
21
46
  }
@@ -191,6 +216,13 @@ export class ApiBrain {
191
216
  }
192
217
  export function shouldUseApi() {
193
218
  const config = getConfig();
194
- return Boolean(config.apiUrl && config.apiKey);
219
+ // Use API if:
220
+ // 1. No Qdrant URL configured (can't use direct access)
221
+ // 2. Or session token is available (preferred auth method)
222
+ if (!config.qdrantUrl) {
223
+ return Boolean(config.apiUrl);
224
+ }
225
+ // If Qdrant is configured, still prefer API if session token exists
226
+ return Boolean(config.apiUrl && config.sessionToken);
195
227
  }
196
228
  export default ApiBrain;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.25.2",
3
+ "version": "1.25.4",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {