@simonfestl/husky-cli 1.16.0 → 1.18.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.
@@ -1,10 +1,28 @@
1
1
  import { Command } from "commander";
2
- import { getConfig, setSessionConfig, clearSessionConfig, getSessionConfig } from "./config.js";
2
+ import { getConfig, setSessionConfig, clearSessionConfig, getSessionConfig, fetchAndCacheRole } 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",
6
6
  "purchasing", "ops", "e2e_agent", "pr_agent"
7
7
  ];
8
+ /**
9
+ * Sanitize error messages to prevent sensitive data leakage.
10
+ * Truncates long messages and removes potential secrets.
11
+ */
12
+ function sanitizeErrorMessage(message, maxLength = 200) {
13
+ if (!message)
14
+ return "Unknown error";
15
+ // Remove potential secrets (patterns like API keys, tokens, etc.)
16
+ let sanitized = message
17
+ .replace(/[a-zA-Z0-9_-]{32,}/g, "[REDACTED]") // Long alphanumeric strings
18
+ .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]") // Bearer tokens
19
+ .replace(/key[=:]\s*[^\s,}]+/gi, "key=[REDACTED]"); // key=value patterns
20
+ // Truncate if too long
21
+ if (sanitized.length > maxLength) {
22
+ sanitized = sanitized.substring(0, maxLength) + "...";
23
+ }
24
+ return sanitized;
25
+ }
8
26
  async function apiRequest(path, options = {}) {
9
27
  const config = getConfig();
10
28
  if (!config.apiUrl || !config.apiKey) {
@@ -21,7 +39,8 @@ async function apiRequest(path, options = {}) {
21
39
  });
22
40
  if (!res.ok) {
23
41
  const error = await res.json().catch(() => ({ error: res.statusText }));
24
- throw new Error(error.message || error.error || `HTTP ${res.status}`);
42
+ const rawMessage = error.message || error.error || `HTTP ${res.status}`;
43
+ throw new Error(sanitizeErrorMessage(rawMessage));
25
44
  }
26
45
  return res.json();
27
46
  }
@@ -293,6 +312,8 @@ authCommand
293
312
  }
294
313
  const session = await res.json();
295
314
  setSessionConfig(session);
315
+ // Fetch and cache permissions for the new session role
316
+ await fetchAndCacheRole();
296
317
  if (options.json) {
297
318
  console.log(JSON.stringify({
298
319
  success: true,
@@ -418,6 +439,8 @@ authCommand
418
439
  }
419
440
  const session = await res.json();
420
441
  setSessionConfig(session);
442
+ // Refresh permissions for the session role
443
+ await fetchAndCacheRole();
421
444
  if (options.json) {
422
445
  console.log(JSON.stringify({
423
446
  success: true,
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
- type AgentRole = "supervisor" | "worker" | "reviewer" | "e2e_agent" | "pr_agent" | "support";
2
+ declare const VALID_ROLES: readonly ["admin", "supervisor", "worker", "reviewer", "e2e_agent", "pr_agent", "support", "devops", "purchasing", "ops"];
3
+ type AgentRole = typeof VALID_ROLES[number];
3
4
  interface Config {
4
5
  apiUrl?: string;
5
6
  apiKey?: string;
@@ -47,7 +48,8 @@ export declare function getConfig(): Config;
47
48
  export declare function saveConfig(config: Config): void;
48
49
  /**
49
50
  * Fetch role and permissions from /api/auth/whoami
50
- * Caches the result in config for 1 hour
51
+ * Uses session token (Bearer) if available, otherwise falls back to API key.
52
+ * Caches the result in config for 1 hour.
51
53
  */
52
54
  export declare function fetchAndCacheRole(): Promise<{
53
55
  role?: AgentRole;
@@ -58,7 +60,9 @@ export declare function fetchAndCacheRole(): Promise<{
58
60
  */
59
61
  export declare function hasPermission(permission: string): boolean;
60
62
  /**
61
- * Get current role from config (may be undefined if not fetched)
63
+ * Get current role from config.
64
+ * Prefers sessionRole (from auth login) over role (from API key) when session is active.
65
+ * Validates that the role is a known valid role before returning.
62
66
  */
63
67
  export declare function getRole(): AgentRole | undefined;
64
68
  /**
@@ -72,8 +76,30 @@ export declare function setSessionConfig(session: {
72
76
  expiresAt: string;
73
77
  agent: string;
74
78
  role: string;
79
+ permissions?: string[];
75
80
  }): void;
76
81
  export declare function clearSessionConfig(): void;
82
+ /**
83
+ * Check if there's an active (non-expired) session
84
+ */
85
+ export declare function isSessionActive(): boolean;
86
+ /**
87
+ * Check if the API is properly configured for making requests.
88
+ * Returns true if we have either an active session or an API key.
89
+ */
90
+ export declare function isApiConfigured(): boolean;
91
+ /**
92
+ * Get authentication headers for API requests.
93
+ * Returns Bearer token if session is active, otherwise x-api-key.
94
+ * Use this for all API calls to ensure consistent auth.
95
+ */
96
+ export declare function getAuthHeaders(): Record<string, string>;
97
+ /**
98
+ * Ensure the session is valid, refreshing if needed.
99
+ * Call this before long-running operations (like watch modes).
100
+ * Returns true if session is valid (or was refreshed), false if no session/refresh failed.
101
+ */
102
+ export declare function ensureValidSession(): Promise<boolean>;
77
103
  export declare function getSessionConfig(): {
78
104
  token?: string;
79
105
  expiresAt?: string;
@@ -1,10 +1,18 @@
1
1
  import { Command } from "commander";
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
6
+ // Valid agent roles - used for runtime validation
7
+ const VALID_ROLES = ["admin", "supervisor", "worker", "reviewer", "e2e_agent", "pr_agent", "support", "devops", "purchasing", "ops"];
6
8
  const CONFIG_DIR = join(homedir(), ".husky");
7
9
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
10
+ /**
11
+ * Validate if a string is a valid AgentRole
12
+ */
13
+ function isValidRole(role) {
14
+ return VALID_ROLES.includes(role);
15
+ }
8
16
  // API Key validation - must be at least 16 characters, alphanumeric + common key chars (base64, JWT, etc.)
9
17
  function validateApiKey(key) {
10
18
  if (key.length < 16) {
@@ -43,25 +51,67 @@ export function getConfig() {
43
51
  }
44
52
  export function saveConfig(config) {
45
53
  if (!existsSync(CONFIG_DIR)) {
46
- mkdirSync(CONFIG_DIR, { recursive: true });
54
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); // rwx------ for directory
55
+ }
56
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 }); // rw------- for file
57
+ // Ensure permissions are set even if file existed
58
+ try {
59
+ chmodSync(CONFIG_FILE, 0o600);
60
+ }
61
+ catch {
62
+ // Ignore chmod errors on Windows
47
63
  }
48
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
49
64
  }
50
65
  /**
51
66
  * Fetch role and permissions from /api/auth/whoami
52
- * Caches the result in config for 1 hour
67
+ * Uses session token (Bearer) if available, otherwise falls back to API key.
68
+ * Caches the result in config for 1 hour.
53
69
  */
54
70
  export async function fetchAndCacheRole() {
55
71
  const config = getConfig();
56
- // Check if we have cached role that's less than 1 hour old
72
+ // Check if there's an active session - if so, use session role directly
73
+ // Session roles are already validated by the server, no need to re-fetch
74
+ if (config.sessionToken && config.sessionRole && config.sessionExpiresAt) {
75
+ const expiresAt = new Date(config.sessionExpiresAt);
76
+ if (expiresAt > new Date()) {
77
+ // Session is active - fetch permissions for this session role if needed
78
+ // Check if we have fresh permissions for this session
79
+ const needsPermissionsFetch = !config.roleLastChecked || !config.permissions;
80
+ if (needsPermissionsFetch && config.apiUrl) {
81
+ try {
82
+ const url = new URL("/api/auth/whoami", config.apiUrl);
83
+ const res = await fetch(url.toString(), {
84
+ headers: { Authorization: `Bearer ${config.sessionToken}` },
85
+ });
86
+ if (res.ok) {
87
+ const data = await res.json();
88
+ // Update permissions cache (keep sessionRole as source of truth for role)
89
+ config.permissions = data.permissions;
90
+ config.roleLastChecked = new Date().toISOString();
91
+ saveConfig(config);
92
+ return { role: config.sessionRole, permissions: data.permissions };
93
+ }
94
+ }
95
+ catch {
96
+ // Fall through to use cached permissions
97
+ }
98
+ }
99
+ // Return session role with cached permissions
100
+ return { role: config.sessionRole, permissions: config.permissions };
101
+ }
102
+ }
103
+ // No active session - use API key auth
104
+ // Check if we have cached role that's less than 5 minutes old
105
+ // Short TTL ensures revoked permissions are detected quickly
106
+ const PERMISSION_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
57
107
  if (config.role && config.roleLastChecked) {
58
108
  const lastChecked = new Date(config.roleLastChecked);
59
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
60
- if (lastChecked > oneHourAgo) {
109
+ const cacheExpiry = new Date(Date.now() - PERMISSION_CACHE_TTL_MS);
110
+ if (lastChecked > cacheExpiry) {
61
111
  return { role: config.role, permissions: config.permissions };
62
112
  }
63
113
  }
64
- // Fetch fresh role/permissions
114
+ // Fetch fresh role/permissions using API key
65
115
  if (!config.apiUrl || !config.apiKey) {
66
116
  return {};
67
117
  }
@@ -102,10 +152,30 @@ export function hasPermission(permission) {
102
152
  return false;
103
153
  }
104
154
  /**
105
- * Get current role from config (may be undefined if not fetched)
155
+ * Get current role from config.
156
+ * Prefers sessionRole (from auth login) over role (from API key) when session is active.
157
+ * Validates that the role is a known valid role before returning.
106
158
  */
107
159
  export function getRole() {
108
- return getConfig().role;
160
+ const config = getConfig();
161
+ // Check if there's an active (non-expired) session
162
+ if (config.sessionToken && config.sessionRole && config.sessionExpiresAt) {
163
+ const expiresAt = new Date(config.sessionExpiresAt);
164
+ if (expiresAt > new Date()) {
165
+ // Session is active, validate and use session role
166
+ if (isValidRole(config.sessionRole)) {
167
+ return config.sessionRole;
168
+ }
169
+ // Invalid role in session - treat as no session
170
+ console.error(`Warning: Invalid session role '${config.sessionRole}' in config`);
171
+ return undefined;
172
+ }
173
+ }
174
+ // Fall back to API key role (also validate)
175
+ if (config.role && isValidRole(config.role)) {
176
+ return config.role;
177
+ }
178
+ return undefined;
109
179
  }
110
180
  /**
111
181
  * Clear the role cache to force a refresh on next fetchAndCacheRole call
@@ -133,6 +203,17 @@ export function setSessionConfig(session) {
133
203
  config.sessionExpiresAt = session.expiresAt;
134
204
  config.sessionAgent = session.agent;
135
205
  config.sessionRole = session.role;
206
+ // Clear old permissions and role cache to force re-fetch for new session role
207
+ delete config.roleLastChecked;
208
+ if (session.permissions) {
209
+ // If permissions provided, use them directly
210
+ config.permissions = session.permissions;
211
+ config.roleLastChecked = new Date().toISOString();
212
+ }
213
+ else {
214
+ // Clear permissions to force re-fetch
215
+ delete config.permissions;
216
+ }
136
217
  saveConfig(config);
137
218
  }
138
219
  export function clearSessionConfig() {
@@ -143,6 +224,91 @@ export function clearSessionConfig() {
143
224
  delete config.sessionRole;
144
225
  saveConfig(config);
145
226
  }
227
+ /**
228
+ * Check if there's an active (non-expired) session
229
+ */
230
+ export function isSessionActive() {
231
+ const config = getConfig();
232
+ if (!config.sessionToken || !config.sessionExpiresAt) {
233
+ return false;
234
+ }
235
+ const expiresAt = new Date(config.sessionExpiresAt);
236
+ return expiresAt > new Date();
237
+ }
238
+ /**
239
+ * Check if the API is properly configured for making requests.
240
+ * Returns true if we have either an active session or an API key.
241
+ */
242
+ export function isApiConfigured() {
243
+ const config = getConfig();
244
+ return Boolean(config.apiUrl && (config.apiKey || isSessionActive()));
245
+ }
246
+ /**
247
+ * Get authentication headers for API requests.
248
+ * Returns Bearer token if session is active, otherwise x-api-key.
249
+ * Use this for all API calls to ensure consistent auth.
250
+ */
251
+ export function getAuthHeaders() {
252
+ const config = getConfig();
253
+ // Check if there's an active (non-expired) session
254
+ if (config.sessionToken && config.sessionExpiresAt) {
255
+ const expiresAt = new Date(config.sessionExpiresAt);
256
+ if (expiresAt > new Date()) {
257
+ // Session is active - use Bearer token
258
+ return { Authorization: `Bearer ${config.sessionToken}` };
259
+ }
260
+ }
261
+ // Fall back to API key
262
+ if (config.apiKey) {
263
+ return { "x-api-key": config.apiKey };
264
+ }
265
+ return {};
266
+ }
267
+ /**
268
+ * Ensure the session is valid, refreshing if needed.
269
+ * Call this before long-running operations (like watch modes).
270
+ * Returns true if session is valid (or was refreshed), false if no session/refresh failed.
271
+ */
272
+ export async function ensureValidSession() {
273
+ const config = getConfig();
274
+ if (!config.sessionToken || !config.sessionExpiresAt) {
275
+ return false; // No session, will use API key
276
+ }
277
+ const expiresAt = new Date(config.sessionExpiresAt);
278
+ const now = new Date();
279
+ const fiveMinutes = 5 * 60 * 1000;
280
+ // Refresh if expiring within 5 minutes
281
+ if (expiresAt.getTime() - now.getTime() < fiveMinutes) {
282
+ if (!config.apiUrl || !config.apiKey || !config.sessionAgent) {
283
+ return false; // Can't refresh without these
284
+ }
285
+ try {
286
+ const url = new URL("/api/auth/session", config.apiUrl);
287
+ const res = await fetch(url.toString(), {
288
+ method: "POST",
289
+ headers: {
290
+ "x-api-key": config.apiKey,
291
+ "Content-Type": "application/json",
292
+ },
293
+ body: JSON.stringify({ agent: config.sessionAgent }),
294
+ });
295
+ if (res.ok) {
296
+ const data = await res.json();
297
+ setSessionConfig({
298
+ token: data.token,
299
+ expiresAt: data.expiresAt,
300
+ agent: data.agent,
301
+ role: data.role,
302
+ });
303
+ return true;
304
+ }
305
+ }
306
+ catch {
307
+ // Refresh failed, continue with current token if not expired
308
+ }
309
+ }
310
+ return expiresAt > now;
311
+ }
146
312
  export function getSessionConfig() {
147
313
  const config = getConfig();
148
314
  if (!config.sessionToken)
@@ -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 } from "../config.js";
7
+ import { getConfig, setConfig, setSessionConfig, clearSessionConfig, getAuthHeaders } from "../config.js";
8
8
  import { pressEnterToContinue } from "./utils.js";
9
9
  export async function authMenu() {
10
10
  console.log("\n AUTH");
@@ -65,7 +65,7 @@ async function login() {
65
65
  method: "POST",
66
66
  headers: {
67
67
  "Content-Type": "application/json",
68
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
68
+ ...getAuthHeaders(),
69
69
  },
70
70
  body: JSON.stringify({ agent: agentName }),
71
71
  });
@@ -4,6 +4,7 @@
4
4
  * Provides menu-based agent memory and learning management.
5
5
  */
6
6
  import { select, input, confirm } from "@inquirer/prompts";
7
+ import { getAuthHeaders } from "../config.js";
7
8
  import { ensureConfig, pressEnterToContinue, truncate } from "./utils.js";
8
9
  export async function brainMenu() {
9
10
  const config = ensureConfig();
@@ -66,7 +67,7 @@ async function listMemories(config) {
66
67
  default: "20",
67
68
  });
68
69
  const res = await fetch(`${config.apiUrl}/api/brain/memories?limit=${limit}`, {
69
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
70
+ headers: getAuthHeaders(),
70
71
  });
71
72
  if (!res.ok) {
72
73
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -103,7 +104,7 @@ async function recallMemory(config) {
103
104
  validate: (v) => (v.length > 0 ? true : "Query is required"),
104
105
  });
105
106
  const res = await fetch(`${config.apiUrl}/api/brain/recall?query=${encodeURIComponent(query)}`, {
106
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
107
+ headers: getAuthHeaders(),
107
108
  });
108
109
  if (!res.ok) {
109
110
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -147,7 +148,7 @@ async function rememberContent(config) {
147
148
  method: "POST",
148
149
  headers: {
149
150
  "Content-Type": "application/json",
150
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
151
+ ...(getAuthHeaders()),
151
152
  },
152
153
  body: JSON.stringify({ content, tags }),
153
154
  });
@@ -183,7 +184,7 @@ async function publishMemory(config) {
183
184
  method: "POST",
184
185
  headers: {
185
186
  "Content-Type": "application/json",
186
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
187
+ ...(getAuthHeaders()),
187
188
  },
188
189
  body: JSON.stringify({ visibility }),
189
190
  });
@@ -204,7 +205,7 @@ async function publishMemory(config) {
204
205
  async function sharedMemories(config) {
205
206
  try {
206
207
  const res = await fetch(`${config.apiUrl}/api/brain/shared`, {
207
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
208
+ headers: getAuthHeaders(),
208
209
  });
209
210
  if (!res.ok) {
210
211
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -242,7 +243,7 @@ async function boostMemory(config) {
242
243
  });
243
244
  const res = await fetch(`${config.apiUrl}/api/brain/boost/${id}`, {
244
245
  method: "POST",
245
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
246
+ headers: getAuthHeaders(),
246
247
  });
247
248
  if (!res.ok) {
248
249
  const error = await res.text();
@@ -275,7 +276,7 @@ async function archiveMemory(config) {
275
276
  }
276
277
  const res = await fetch(`${config.apiUrl}/api/brain/archive/${id}`, {
277
278
  method: "POST",
278
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
279
+ headers: getAuthHeaders(),
279
280
  });
280
281
  if (!res.ok) {
281
282
  const error = await res.text();
@@ -308,7 +309,7 @@ async function deleteMemory(config) {
308
309
  }
309
310
  const res = await fetch(`${config.apiUrl}/api/brain/memories/${id}`, {
310
311
  method: "DELETE",
311
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
312
+ headers: getAuthHeaders(),
312
313
  });
313
314
  if (!res.ok) {
314
315
  const error = await res.text();
@@ -337,7 +338,7 @@ async function cleanupMemories(config) {
337
338
  }
338
339
  const res = await fetch(`${config.apiUrl}/api/brain/cleanup`, {
339
340
  method: "POST",
340
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
341
+ headers: getAuthHeaders(),
341
342
  });
342
343
  if (!res.ok) {
343
344
  const error = await res.text();
@@ -1,5 +1,6 @@
1
1
  import { select, input, confirm } from "@inquirer/prompts";
2
2
  import { execSync } from "child_process";
3
+ import { getAuthHeaders } from "../config.js";
3
4
  import { ensureConfig, pressEnterToContinue, truncate, formatDate } from "./utils.js";
4
5
  const TYPE_LABELS = {
5
6
  feature: "New Features",
@@ -55,7 +56,7 @@ async function fetchChangelogs(config, projectId) {
55
56
  url.searchParams.set("projectId", projectId);
56
57
  }
57
58
  const res = await fetch(url.toString(), {
58
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
59
+ headers: getAuthHeaders(),
59
60
  });
60
61
  if (!res.ok)
61
62
  throw new Error(`API returned ${res.status}`);
@@ -63,7 +64,7 @@ async function fetchChangelogs(config, projectId) {
63
64
  }
64
65
  async function fetchProjects(config) {
65
66
  const res = await fetch(`${config.apiUrl}/api/projects`, {
66
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
67
+ headers: getAuthHeaders(),
67
68
  });
68
69
  if (!res.ok)
69
70
  throw new Error(`API returned ${res.status}`);
@@ -86,7 +87,7 @@ async function selectChangelog(config, message) {
86
87
  return null;
87
88
  // Fetch full changelog
88
89
  const res = await fetch(`${config.apiUrl}/api/changelogs/${changelogId}`, {
89
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
90
+ headers: getAuthHeaders(),
90
91
  });
91
92
  if (!res.ok)
92
93
  return null;
@@ -208,7 +209,7 @@ async function generateChangelog(config) {
208
209
  method: "POST",
209
210
  headers: {
210
211
  "Content-Type": "application/json",
211
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
212
+ ...(getAuthHeaders()),
212
213
  },
213
214
  body: JSON.stringify({
214
215
  projectId,
@@ -279,7 +280,7 @@ async function publishChangelog(config) {
279
280
  method: "PATCH",
280
281
  headers: {
281
282
  "Content-Type": "application/json",
282
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
283
+ ...(getAuthHeaders()),
283
284
  },
284
285
  body: JSON.stringify({
285
286
  status: "published",
@@ -315,7 +316,7 @@ async function deleteChangelog(config) {
315
316
  }
316
317
  const res = await fetch(`${config.apiUrl}/api/changelogs/${changelog.id}`, {
317
318
  method: "DELETE",
318
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
319
+ headers: getAuthHeaders(),
319
320
  });
320
321
  if (!res.ok) {
321
322
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { select, input, confirm } from "@inquirer/prompts";
7
7
  import { ensureConfig, pressEnterToContinue, truncate, formatDate } from "./utils.js";
8
+ import { getAuthHeaders, ensureValidSession } from "../config.js";
8
9
  export async function chatMenu() {
9
10
  const config = ensureConfig();
10
11
  console.log("\n CHAT");
@@ -67,7 +68,7 @@ async function showInbox(config) {
67
68
  });
68
69
  const url = `${config.apiUrl}/api/supervisor/inbox${unreadOnly ? "?unread=true" : ""}`;
69
70
  const res = await fetch(url, {
70
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
71
+ headers: getAuthHeaders(),
71
72
  });
72
73
  if (!res.ok) {
73
74
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -102,7 +103,7 @@ async function showInbox(config) {
102
103
  async function showPending(config) {
103
104
  try {
104
105
  const res = await fetch(`${config.apiUrl}/api/supervisor/pending`, {
105
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
106
+ headers: getAuthHeaders(),
106
107
  });
107
108
  if (!res.ok) {
108
109
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -142,7 +143,7 @@ async function sendMessage(config) {
142
143
  method: "POST",
143
144
  headers: {
144
145
  "Content-Type": "application/json",
145
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
146
+ ...getAuthHeaders(),
146
147
  },
147
148
  body: JSON.stringify({ message }),
148
149
  });
@@ -174,7 +175,7 @@ async function replyToMessage(config) {
174
175
  method: "POST",
175
176
  headers: {
176
177
  "Content-Type": "application/json",
177
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
178
+ ...getAuthHeaders(),
178
179
  },
179
180
  body: JSON.stringify({ response }),
180
181
  });
@@ -196,12 +197,15 @@ async function watchMessages(config) {
196
197
  console.log("\n WATCH MODE");
197
198
  console.log(" " + "-".repeat(50));
198
199
  console.log(" Watching for new messages...");
200
+ console.log(" (Session token will auto-refresh if needed)");
199
201
  console.log("");
200
202
  let lastCheck = new Date().toISOString();
201
203
  const checkMessages = async () => {
202
204
  try {
205
+ // Ensure session is valid before each poll (auto-refreshes if needed)
206
+ await ensureValidSession();
203
207
  const res = await fetch(`${config.apiUrl}/api/supervisor/inbox?since=${encodeURIComponent(lastCheck)}`, {
204
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
208
+ headers: getAuthHeaders(),
205
209
  });
206
210
  if (res.ok) {
207
211
  const messages = await res.json();
@@ -211,6 +215,9 @@ async function watchMessages(config) {
211
215
  console.log("");
212
216
  }
213
217
  }
218
+ else if (res.status === 401) {
219
+ console.log(` [${new Date().toLocaleTimeString()}] Auth error - will retry with refreshed token`);
220
+ }
214
221
  lastCheck = new Date().toISOString();
215
222
  }
216
223
  catch {
@@ -229,7 +236,7 @@ async function watchMessages(config) {
229
236
  async function showConversations(config) {
230
237
  try {
231
238
  const res = await fetch(`${config.apiUrl}/api/agent-conversations`, {
232
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
239
+ headers: getAuthHeaders(),
233
240
  });
234
241
  if (!res.ok) {
235
242
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -260,7 +267,7 @@ async function showConversations(config) {
260
267
  async function showSpaces(config) {
261
268
  try {
262
269
  const res = await fetch(`${config.apiUrl}/api/chat/spaces`, {
263
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
270
+ headers: getAuthHeaders(),
264
271
  });
265
272
  if (!res.ok) {
266
273
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -303,7 +310,7 @@ async function askQuestion(config) {
303
310
  method: "POST",
304
311
  headers: {
305
312
  "Content-Type": "application/json",
306
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
313
+ ...getAuthHeaders(),
307
314
  },
308
315
  body: JSON.stringify({ question, wait: waitForAnswer }),
309
316
  });
@@ -342,7 +349,7 @@ async function requestReview(config) {
342
349
  method: "POST",
343
350
  headers: {
344
351
  "Content-Type": "application/json",
345
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
352
+ ...getAuthHeaders(),
346
353
  },
347
354
  body: JSON.stringify({ question }),
348
355
  });
@@ -1,4 +1,5 @@
1
1
  import { select, input, confirm } from "@inquirer/prompts";
2
+ import { getAuthHeaders } from "../config.js";
2
3
  import { ensureConfig, pressEnterToContinue, truncate } from "./utils.js";
3
4
  export async function departmentsMenu() {
4
5
  const config = ensureConfig();
@@ -36,7 +37,7 @@ export async function departmentsMenu() {
36
37
  }
37
38
  async function fetchDepartments(config) {
38
39
  const res = await fetch(`${config.apiUrl}/api/departments`, {
39
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
40
+ headers: getAuthHeaders(),
40
41
  });
41
42
  if (!res.ok)
42
43
  throw new Error(`API returned ${res.status}`);
@@ -87,7 +88,7 @@ async function viewDepartment(config) {
87
88
  if (!dept)
88
89
  return;
89
90
  const res = await fetch(`${config.apiUrl}/api/departments/${dept.id}`, {
90
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
91
+ headers: getAuthHeaders(),
91
92
  });
92
93
  if (!res.ok) {
93
94
  console.error(`\n Error: API returned ${res.status}\n`);
@@ -128,7 +129,7 @@ async function createDepartment(config) {
128
129
  method: "POST",
129
130
  headers: {
130
131
  "Content-Type": "application/json",
131
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
132
+ ...(getAuthHeaders()),
132
133
  },
133
134
  body: JSON.stringify({
134
135
  name,
@@ -192,7 +193,7 @@ async function updateDepartment(config) {
192
193
  method: "PATCH",
193
194
  headers: {
194
195
  "Content-Type": "application/json",
195
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
196
+ ...(getAuthHeaders()),
196
197
  },
197
198
  body: JSON.stringify(updateData),
198
199
  });
@@ -225,7 +226,7 @@ async function deleteDepartment(config) {
225
226
  }
226
227
  const res = await fetch(`${config.apiUrl}/api/departments/${dept.id}`, {
227
228
  method: "DELETE",
228
- headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
229
+ headers: getAuthHeaders(),
229
230
  });
230
231
  if (!res.ok) {
231
232
  console.error(`\n Error: API returned ${res.status}\n`);