@simonfestl/husky-cli 1.9.1 → 1.10.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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const authCommand: Command;
@@ -0,0 +1,262 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ import { getPermissions, clearPermissionsCache, getCacheStatus, hasPermission, canAccessKnowledgeBase } from "../lib/permissions-cache.js";
4
+ const API_KEY_ROLES = [
5
+ "admin", "supervisor", "worker", "reviewer", "support",
6
+ "purchasing", "ops", "e2e_agent", "pr_agent"
7
+ ];
8
+ async function apiRequest(path, options = {}) {
9
+ const config = getConfig();
10
+ if (!config.apiUrl || !config.apiKey) {
11
+ throw new Error("API not configured. Run: husky config set api-url <url> && husky config set api-key <key>");
12
+ }
13
+ const url = new URL(path, config.apiUrl);
14
+ const res = await fetch(url.toString(), {
15
+ method: options.method || "GET",
16
+ headers: {
17
+ "x-api-key": config.apiKey,
18
+ "Content-Type": "application/json",
19
+ },
20
+ body: options.body ? JSON.stringify(options.body) : undefined,
21
+ });
22
+ if (!res.ok) {
23
+ const error = await res.json().catch(() => ({ error: res.statusText }));
24
+ throw new Error(error.message || error.error || `HTTP ${res.status}`);
25
+ }
26
+ return res.json();
27
+ }
28
+ export const authCommand = new Command("auth")
29
+ .description("Manage API keys and authentication");
30
+ authCommand
31
+ .command("keys")
32
+ .description("List all API keys")
33
+ .option("--include-revoked", "Include revoked keys")
34
+ .option("--json", "Output as JSON")
35
+ .action(async (options) => {
36
+ try {
37
+ const query = options.includeRevoked ? "?includeRevoked=true" : "";
38
+ const data = await apiRequest(`/api/auth/keys${query}`);
39
+ if (options.json) {
40
+ console.log(JSON.stringify(data.keys, null, 2));
41
+ return;
42
+ }
43
+ if (data.keys.length === 0) {
44
+ console.log("No API keys found.");
45
+ return;
46
+ }
47
+ console.log("\nAPI Keys:");
48
+ console.log("─".repeat(80));
49
+ for (const key of data.keys) {
50
+ const status = key.revoked ? "šŸ”“ REVOKED" : "🟢 ACTIVE";
51
+ const expires = key.expiresAt ? new Date(key.expiresAt).toLocaleDateString() : "Never";
52
+ const lastUsed = key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleDateString() : "Never";
53
+ console.log(`${status} ${key.keyPrefix}... ${key.name}`);
54
+ console.log(` Role: ${key.role} | Expires: ${expires} | Last used: ${lastUsed}`);
55
+ console.log(` ID: ${key.id}`);
56
+ console.log("");
57
+ }
58
+ }
59
+ catch (error) {
60
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ authCommand
65
+ .command("create-key")
66
+ .description("Create a new API key")
67
+ .requiredOption("--name <name>", "Human-readable name for the key")
68
+ .requiredOption("--role <role>", `Role: ${API_KEY_ROLES.join(", ")}`)
69
+ .option("--scopes <scopes>", "Comma-separated additional scopes")
70
+ .option("--expires-in-days <days>", "Expiration in days (1-365)")
71
+ .option("--json", "Output as JSON")
72
+ .action(async (options) => {
73
+ try {
74
+ if (!API_KEY_ROLES.includes(options.role)) {
75
+ console.error(`Invalid role: ${options.role}`);
76
+ console.error(`Valid roles: ${API_KEY_ROLES.join(", ")}`);
77
+ process.exit(1);
78
+ }
79
+ const body = {
80
+ name: options.name,
81
+ role: options.role,
82
+ };
83
+ if (options.scopes) {
84
+ body.scopes = options.scopes.split(",").map((s) => s.trim());
85
+ }
86
+ if (options.expiresInDays) {
87
+ const days = parseInt(options.expiresInDays, 10);
88
+ if (isNaN(days) || days < 1 || days > 365) {
89
+ console.error("--expires-in-days must be between 1 and 365");
90
+ process.exit(1);
91
+ }
92
+ body.expiresInDays = days;
93
+ }
94
+ const result = await apiRequest("/api/auth/keys", {
95
+ method: "POST",
96
+ body,
97
+ });
98
+ if (options.json) {
99
+ console.log(JSON.stringify(result, null, 2));
100
+ return;
101
+ }
102
+ console.log("\nāœ… API Key Created Successfully");
103
+ console.log("─".repeat(60));
104
+ console.log(`Name: ${result.name}`);
105
+ console.log(`Role: ${result.role}`);
106
+ console.log(`Key ID: ${result.id}`);
107
+ console.log(`Prefix: ${result.keyPrefix}`);
108
+ if (result.expiresAt) {
109
+ console.log(`Expires: ${new Date(result.expiresAt).toLocaleDateString()}`);
110
+ }
111
+ console.log("");
112
+ console.log("šŸ”‘ API KEY (store securely - shown only once):");
113
+ console.log("");
114
+ console.log(` ${result.plainTextKey}`);
115
+ console.log("");
116
+ console.log("āš ļø " + result.warning);
117
+ }
118
+ catch (error) {
119
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
120
+ process.exit(1);
121
+ }
122
+ });
123
+ authCommand
124
+ .command("revoke-key <id>")
125
+ .description("Revoke an API key")
126
+ .option("--json", "Output as JSON")
127
+ .action(async (id, options) => {
128
+ try {
129
+ const result = await apiRequest(`/api/auth/keys/${id}`, { method: "DELETE" });
130
+ if (options.json) {
131
+ console.log(JSON.stringify(result, null, 2));
132
+ return;
133
+ }
134
+ console.log(`\nāœ… API Key Revoked: ${result.keyPrefix}...`);
135
+ console.log(` Revoked at: ${new Date(result.revokedAt).toLocaleString()}`);
136
+ }
137
+ catch (error) {
138
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
139
+ process.exit(1);
140
+ }
141
+ });
142
+ authCommand
143
+ .command("whoami")
144
+ .description("Show current authentication info")
145
+ .option("--json", "Output as JSON")
146
+ .action(async (options) => {
147
+ try {
148
+ const data = await apiRequest("/api/auth/whoami");
149
+ if (options.json) {
150
+ console.log(JSON.stringify(data, null, 2));
151
+ return;
152
+ }
153
+ console.log("\nšŸ” Authentication Info");
154
+ console.log("─".repeat(40));
155
+ console.log(`Role: ${data.role}`);
156
+ console.log(`Key ID: ${data.keyId}`);
157
+ console.log(`Source: ${data.source}`);
158
+ if (data.scopes && data.scopes.length > 0) {
159
+ console.log(`Scopes: ${data.scopes.join(", ")}`);
160
+ }
161
+ console.log("");
162
+ console.log("Permissions:");
163
+ for (const perm of data.permissions) {
164
+ console.log(` • ${perm}`);
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
169
+ process.exit(1);
170
+ }
171
+ });
172
+ authCommand
173
+ .command("permissions")
174
+ .description("Show cached permissions (5-min cache)")
175
+ .option("--refresh", "Force refresh from API")
176
+ .option("--json", "Output as JSON")
177
+ .action(async (options) => {
178
+ try {
179
+ if (options.refresh) {
180
+ clearPermissionsCache();
181
+ }
182
+ const perms = await getPermissions();
183
+ const cacheStatus = getCacheStatus();
184
+ if (options.json) {
185
+ console.log(JSON.stringify({
186
+ ...perms,
187
+ cache: cacheStatus,
188
+ }, null, 2));
189
+ return;
190
+ }
191
+ const cacheAge = cacheStatus.age ? Math.round(cacheStatus.age / 1000) : 0;
192
+ const expiresIn = cacheStatus.expiresIn ? Math.round(cacheStatus.expiresIn / 1000) : 0;
193
+ console.log("\nšŸ”‘ Cached Permissions");
194
+ console.log("─".repeat(50));
195
+ console.log(`Role: ${perms.role}`);
196
+ console.log(`Cache age: ${cacheAge}s (expires in ${expiresIn}s)`);
197
+ console.log("");
198
+ console.log("Permissions:");
199
+ for (const perm of perms.permissions) {
200
+ console.log(` • ${perm}`);
201
+ }
202
+ if (perms.knowledgeBases.length > 0) {
203
+ console.log("");
204
+ console.log("Knowledge Bases:");
205
+ for (const kb of perms.knowledgeBases) {
206
+ console.log(` • ${kb}`);
207
+ }
208
+ }
209
+ }
210
+ catch (error) {
211
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
212
+ process.exit(1);
213
+ }
214
+ });
215
+ authCommand
216
+ .command("can <permission>")
217
+ .description("Check if current key has a specific permission")
218
+ .option("--json", "Output as JSON")
219
+ .action(async (permission, options) => {
220
+ try {
221
+ const allowed = await hasPermission(permission);
222
+ if (options.json) {
223
+ console.log(JSON.stringify({ permission, allowed }));
224
+ return;
225
+ }
226
+ if (allowed) {
227
+ console.log(`āœ… Permission granted: ${permission}`);
228
+ }
229
+ else {
230
+ console.log(`āŒ Permission denied: ${permission}`);
231
+ process.exit(1);
232
+ }
233
+ }
234
+ catch (error) {
235
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
236
+ process.exit(1);
237
+ }
238
+ });
239
+ authCommand
240
+ .command("can-access-kb <kb>")
241
+ .description("Check if current key can access a knowledge base")
242
+ .option("--json", "Output as JSON")
243
+ .action(async (kb, options) => {
244
+ try {
245
+ const allowed = await canAccessKnowledgeBase(kb);
246
+ if (options.json) {
247
+ console.log(JSON.stringify({ knowledgeBase: kb, allowed }));
248
+ return;
249
+ }
250
+ if (allowed) {
251
+ console.log(`āœ… Access granted to KB: ${kb}`);
252
+ }
253
+ else {
254
+ console.log(`āŒ Access denied to KB: ${kb}`);
255
+ process.exit(1);
256
+ }
257
+ }
258
+ catch (error) {
259
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
260
+ process.exit(1);
261
+ }
262
+ });
@@ -1,11 +1,32 @@
1
1
  import { Command } from "commander";
2
- import { AgentBrain, AGENT_TYPES, isValidAgentType } from "../lib/biz/agent-brain.js";
2
+ import { AgentBrain, AGENT_TYPES, isValidAgentType, KNOWLEDGE_BASES, isValidKnowledgeBase, KnowledgeBaseBrain, getAccessibleKnowledgeBases, getAgentType } from "../lib/biz/agent-brain.js";
3
3
  import { generateSOP, formatSOPAsMarkdown } from "../lib/biz/sop-generator.js";
4
+ import { ApiBrain, shouldUseApi } from "../lib/biz/api-brain.js";
4
5
  const DEFAULT_AGENT = process.env.HUSKY_AGENT_ID || 'default';
5
- function createBrain(agentId, agentType) {
6
+ function toDate(value) {
7
+ return value instanceof Date ? value : new Date(value);
8
+ }
9
+ function createBrain(agentId, agentType, options) {
10
+ if (options?.useApi || (shouldUseApi() && options?.kb)) {
11
+ return new ApiBrain({
12
+ agentId,
13
+ agentType: isValidAgentType(agentType) ? agentType : undefined,
14
+ knowledgeBase: options?.kb,
15
+ });
16
+ }
6
17
  const validAgentType = isValidAgentType(agentType) ? agentType : undefined;
7
18
  return new AgentBrain({ agentId, agentType: validAgentType });
8
19
  }
20
+ function createKBBrain(kb, agentType, agentId = DEFAULT_AGENT) {
21
+ const resolvedAgentType = isValidAgentType(agentType) ? agentType : getAgentType();
22
+ if (!resolvedAgentType) {
23
+ throw new Error(`Agent type required for knowledge base access. Set HUSKY_AGENT_TYPE or use --agent-type`);
24
+ }
25
+ if (!isValidKnowledgeBase(kb)) {
26
+ throw new Error(`Invalid knowledge base '${kb}'. Available: ${KNOWLEDGE_BASES.join(', ')}`);
27
+ }
28
+ return new KnowledgeBaseBrain(resolvedAgentType, kb, agentId);
29
+ }
9
30
  export const brainCommand = new Command("brain")
10
31
  .description("Agent memory and knowledge management");
11
32
  brainCommand
@@ -14,13 +35,27 @@ brainCommand
14
35
  .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
15
36
  .option("-t, --tags <tags>", "Comma-separated tags")
16
37
  .option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
38
+ .option("--kb <name>", `Knowledge base to use (${KNOWLEDGE_BASES.join(", ")})`)
17
39
  .option("--visibility <level>", "Visibility level (private, team, public)", "private")
18
40
  .option("--allow-pii", "Skip PII filtering (use only for technical/internal content)")
19
41
  .option("--json", "Output as JSON")
20
42
  .action(async (content, options) => {
21
43
  try {
22
- const brain = createBrain(options.agent, options.agentType);
23
44
  const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
45
+ if (options.kb) {
46
+ const kbBrain = createKBBrain(options.kb, options.agentType, options.agent);
47
+ const info = kbBrain.getInfo();
48
+ console.log(` Storing in knowledge base: ${info.knowledgeBase} (${info.collectionName})...`);
49
+ const id = await kbBrain.remember(content, tags);
50
+ if (options.json) {
51
+ console.log(JSON.stringify({ success: true, id, knowledgeBase: info.knowledgeBase, collection: info.collectionName }));
52
+ }
53
+ else {
54
+ console.log(` āœ“ Stored in ${info.knowledgeBase}: ${id}`);
55
+ }
56
+ return;
57
+ }
58
+ const brain = createBrain(options.agent, options.agentType);
24
59
  const visibility = options.visibility;
25
60
  const dbInfo = brain.getDatabaseInfo();
26
61
  if (!["private", "team", "public"].includes(visibility)) {
@@ -48,19 +83,39 @@ brainCommand
48
83
  .option("-l, --limit <num>", "Max results", "5")
49
84
  .option("-m, --min-score <score>", "Minimum similarity score (0-1)", "0.5")
50
85
  .option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
86
+ .option("--kb <name>", `Knowledge base to search (${KNOWLEDGE_BASES.join(", ")})`)
51
87
  .option("--shared", "Search shared memories from other agents")
52
88
  .option("--public-only", "Search only public memories (requires --shared)")
53
89
  .option("--json", "Output as JSON")
54
90
  .action(async (query, options) => {
55
91
  try {
92
+ if (options.kb) {
93
+ const kbBrain = createKBBrain(options.kb, options.agentType, options.agent);
94
+ const info = kbBrain.getInfo();
95
+ console.log(` Searching knowledge base: ${info.knowledgeBase}...`);
96
+ const results = await kbBrain.recall(query, parseInt(options.limit, 10), parseFloat(options.minScore));
97
+ if (options.json) {
98
+ console.log(JSON.stringify({ success: true, query, knowledgeBase: info.knowledgeBase, results }));
99
+ return;
100
+ }
101
+ console.log(`\n šŸ“š Knowledge Base: ${info.knowledgeBase} (${results.length} found)\n`);
102
+ if (results.length === 0) {
103
+ console.log(` No results found.`);
104
+ return;
105
+ }
106
+ for (const r of results) {
107
+ const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
108
+ console.log(` [${(r.score * 100).toFixed(1)}%] ${r.memory.content.slice(0, 80)}${tags}`);
109
+ }
110
+ console.log("");
111
+ return;
112
+ }
56
113
  const brain = createBrain(options.agent, options.agentType);
57
114
  let results;
58
115
  if (options.shared) {
59
- // Search shared memories
60
116
  results = await brain.recallShared(query, parseInt(options.limit, 10), parseFloat(options.minScore), options.publicOnly);
61
117
  }
62
118
  else {
63
- // Search personal memories
64
119
  const dbInfo = brain.getDatabaseInfo();
65
120
  console.log(` Searching memories for: "${query}" (db: ${dbInfo.databaseName})...`);
66
121
  results = await brain.recall(query, parseInt(options.limit, 10), parseFloat(options.minScore));
@@ -111,7 +166,7 @@ brainCommand
111
166
  return;
112
167
  }
113
168
  for (const m of memories) {
114
- const date = m.createdAt.toLocaleDateString("de-DE");
169
+ const date = toDate(m.createdAt).toLocaleDateString("de-DE");
115
170
  const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
116
171
  console.log(` ${date} │ ${m.content.slice(0, 60)}...${tags}`);
117
172
  }
@@ -398,7 +453,7 @@ brainCommand
398
453
  if (toArchive.length > 0) {
399
454
  console.log(`\n Memories:`);
400
455
  for (const m of toArchive.slice(0, 10)) {
401
- const age = Math.floor((Date.now() - m.createdAt.getTime()) / (1000 * 60 * 60 * 24));
456
+ const age = Math.floor((Date.now() - toDate(m.createdAt).getTime()) / (1000 * 60 * 60 * 24));
402
457
  console.log(` ${m.id.slice(0, 8)} │ ${m.content.slice(0, 50)}... (${age}d old, Q: ${m.qualityScore?.toFixed(2)})`);
403
458
  }
404
459
  if (toArchive.length > 10) {
@@ -483,4 +538,72 @@ brainCommand
483
538
  process.exit(1);
484
539
  }
485
540
  });
541
+ // ============================================================================
542
+ // Knowledge Base Commands
543
+ // ============================================================================
544
+ brainCommand
545
+ .command("kb-list")
546
+ .description("List available knowledge bases and your access")
547
+ .option("--agent-type <type>", `Agent type (${AGENT_TYPES.join(", ")})`)
548
+ .option("--json", "Output as JSON")
549
+ .action(async (options) => {
550
+ try {
551
+ const agentType = isValidAgentType(options.agentType) ? options.agentType : getAgentType();
552
+ const accessible = agentType ? getAccessibleKnowledgeBases(agentType) : [];
553
+ if (options.json) {
554
+ console.log(JSON.stringify({
555
+ agentType: agentType || null,
556
+ knowledgeBases: KNOWLEDGE_BASES,
557
+ accessible,
558
+ }));
559
+ return;
560
+ }
561
+ console.log(`\n šŸ“š Knowledge Bases`);
562
+ console.log(` ────────────────────────────────`);
563
+ console.log(` Your role: ${agentType || '(not set)'}\n`);
564
+ for (const kb of KNOWLEDGE_BASES) {
565
+ const hasAccess = accessible.includes(kb);
566
+ const icon = hasAccess ? 'āœ“' : 'āœ—';
567
+ const color = hasAccess ? '' : ' (no access)';
568
+ console.log(` ${icon} ${kb}${color}`);
569
+ }
570
+ console.log("");
571
+ }
572
+ catch (error) {
573
+ console.error("Error:", error.message);
574
+ process.exit(1);
575
+ }
576
+ });
577
+ brainCommand
578
+ .command("kb-stats <kb>")
579
+ .description("Show statistics for a knowledge base")
580
+ .option("--agent-type <type>", `Agent type (${AGENT_TYPES.join(", ")})`)
581
+ .option("--json", "Output as JSON")
582
+ .action(async (kb, options) => {
583
+ try {
584
+ const kbBrain = createKBBrain(kb, options.agentType);
585
+ const info = kbBrain.getInfo();
586
+ const stats = await kbBrain.stats();
587
+ if (options.json) {
588
+ console.log(JSON.stringify({ success: true, ...info, ...stats }));
589
+ return;
590
+ }
591
+ console.log(`\n šŸ“š Knowledge Base: ${info.knowledgeBase}`);
592
+ console.log(` ────────────────────────────────`);
593
+ console.log(` Collection: ${info.collectionName}`);
594
+ console.log(` Total entries: ${stats.count}`);
595
+ if (Object.keys(stats.tags).length > 0) {
596
+ console.log(`\n Tags:`);
597
+ const sortedTags = Object.entries(stats.tags).sort((a, b) => b[1] - a[1]);
598
+ for (const [tag, count] of sortedTags.slice(0, 10)) {
599
+ console.log(` ${tag}: ${count}`);
600
+ }
601
+ }
602
+ console.log("");
603
+ }
604
+ catch (error) {
605
+ console.error("Error:", error.message);
606
+ process.exit(1);
607
+ }
608
+ });
486
609
  export default brainCommand;
@@ -1,5 +1,11 @@
1
- export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker"];
1
+ export declare const AGENT_TYPES: readonly ["admin", "supervisor", "support", "worker", "reviewer", "e2e_agent", "pr_agent", "purchasing", "ops"];
2
2
  export type AgentType = typeof AGENT_TYPES[number];
3
+ export declare const KNOWLEDGE_BASES: readonly ["secondbrain", "supplier-products", "customer-insights", "process-sops"];
4
+ export type KnowledgeBase = typeof KNOWLEDGE_BASES[number];
5
+ export declare const ROLE_KB_ACCESS: Record<AgentType, readonly KnowledgeBase[]>;
6
+ export declare function isValidKnowledgeBase(value: string | undefined): value is KnowledgeBase;
7
+ export declare function canAccessKnowledgeBase(agentType: AgentType | undefined, kb: KnowledgeBase): boolean;
8
+ export declare function getAccessibleKnowledgeBases(agentType: AgentType | undefined): readonly KnowledgeBase[];
3
9
  export type MemoryVisibility = 'private' | 'team' | 'public';
4
10
  export interface Memory {
5
11
  id: string;
@@ -30,6 +36,7 @@ export interface AgentBrainOptions {
30
36
  agentId: string;
31
37
  agentType?: AgentType;
32
38
  projectId?: string;
39
+ collectionName?: string;
33
40
  }
34
41
  export declare function isValidAgentType(value: string | undefined): value is AgentType;
35
42
  export declare function getAgentType(): AgentType | undefined;
@@ -38,11 +45,14 @@ export declare class AgentBrain {
38
45
  private embeddings;
39
46
  private agentId;
40
47
  private agentType?;
48
+ private collectionNameOverride?;
41
49
  constructor(agentIdOrOptions: string | AgentBrainOptions, projectId?: string);
42
50
  getDatabaseInfo(): {
43
51
  agentType?: AgentType;
44
52
  databaseName: string;
53
+ collectionName: string;
45
54
  };
55
+ private getCollectionName;
46
56
  private ensureCollection;
47
57
  remember(content: string, tags?: string[], metadata?: Record<string, unknown>, visibility?: MemoryVisibility, allowPii?: boolean): Promise<string>;
48
58
  recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
@@ -104,4 +114,24 @@ export declare class AgentBrain {
104
114
  */
105
115
  purge(retentionDays?: number): Promise<number>;
106
116
  }
117
+ export declare class KnowledgeBaseBrain {
118
+ private brain;
119
+ private kb;
120
+ private agentType;
121
+ constructor(agentType: AgentType, kb: KnowledgeBase, agentId?: string);
122
+ getCollectionName(): string;
123
+ remember(content: string, tags?: string[], metadata?: Record<string, unknown>): Promise<string>;
124
+ recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
125
+ list(limit?: number): Promise<Memory[]>;
126
+ forget(memoryId: string): Promise<void>;
127
+ stats(): Promise<{
128
+ count: number;
129
+ tags: Record<string, number>;
130
+ }>;
131
+ getInfo(): {
132
+ agentType: AgentType;
133
+ knowledgeBase: KnowledgeBase;
134
+ collectionName: string;
135
+ };
136
+ }
107
137
  export default AgentBrain;
@@ -3,9 +3,34 @@ import { EmbeddingService } from './embeddings.js';
3
3
  import { getConfig } from '../../commands/config.js';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { sanitizeForEmbedding } from './pii-filter.js';
6
- const MEMORIES_COLLECTION = 'agent-memories';
6
+ const DEFAULT_COLLECTION = 'agent-memories';
7
7
  const VECTOR_SIZE = 768;
8
- export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker'];
8
+ export const AGENT_TYPES = ['admin', 'supervisor', 'support', 'worker', 'reviewer', 'e2e_agent', 'pr_agent', 'purchasing', 'ops'];
9
+ export const KNOWLEDGE_BASES = ['secondbrain', 'supplier-products', 'customer-insights', 'process-sops'];
10
+ export const ROLE_KB_ACCESS = {
11
+ admin: ['secondbrain', 'supplier-products', 'customer-insights', 'process-sops'],
12
+ supervisor: ['secondbrain', 'supplier-products', 'customer-insights', 'process-sops'],
13
+ support: ['customer-insights', 'supplier-products', 'process-sops'],
14
+ purchasing: ['supplier-products', 'process-sops'],
15
+ ops: ['supplier-products', 'process-sops'],
16
+ worker: [],
17
+ reviewer: [],
18
+ e2e_agent: [],
19
+ pr_agent: [],
20
+ };
21
+ export function isValidKnowledgeBase(value) {
22
+ return value !== undefined && KNOWLEDGE_BASES.includes(value);
23
+ }
24
+ export function canAccessKnowledgeBase(agentType, kb) {
25
+ if (!agentType)
26
+ return false;
27
+ return ROLE_KB_ACCESS[agentType]?.includes(kb) ?? false;
28
+ }
29
+ export function getAccessibleKnowledgeBases(agentType) {
30
+ if (!agentType)
31
+ return [];
32
+ return ROLE_KB_ACCESS[agentType] ?? [];
33
+ }
9
34
  export function isValidAgentType(value) {
10
35
  return value !== undefined && AGENT_TYPES.includes(value);
11
36
  }
@@ -26,6 +51,7 @@ export class AgentBrain {
26
51
  embeddings;
27
52
  agentId;
28
53
  agentType;
54
+ collectionNameOverride;
29
55
  constructor(agentIdOrOptions, projectId) {
30
56
  let options;
31
57
  if (typeof agentIdOrOptions === 'string') {
@@ -37,6 +63,7 @@ export class AgentBrain {
37
63
  const config = getConfig();
38
64
  this.agentId = options.agentId;
39
65
  this.agentType = options.agentType || getAgentType();
66
+ this.collectionNameOverride = options.collectionName;
40
67
  this.qdrant = QdrantClient.fromConfig();
41
68
  const gcpProject = options.projectId || config.gcpProjectId || process.env.GOOGLE_CLOUD_PROJECT || 'tigerv0';
42
69
  this.embeddings = new EmbeddingService({
@@ -45,17 +72,27 @@ export class AgentBrain {
45
72
  });
46
73
  }
47
74
  getDatabaseInfo() {
75
+ const collectionName = this.getCollectionName();
48
76
  return {
49
77
  agentType: this.agentType,
50
- databaseName: `qdrant:${MEMORIES_COLLECTION}`,
78
+ databaseName: `qdrant:${collectionName}`,
79
+ collectionName,
51
80
  };
52
81
  }
82
+ getCollectionName() {
83
+ if (this.collectionNameOverride)
84
+ return this.collectionNameOverride;
85
+ if (!this.agentType)
86
+ return DEFAULT_COLLECTION;
87
+ return `${this.agentType}-memories`;
88
+ }
53
89
  async ensureCollection() {
90
+ const collection = this.getCollectionName();
54
91
  try {
55
- await this.qdrant.getCollection(MEMORIES_COLLECTION);
92
+ await this.qdrant.getCollection(collection);
56
93
  }
57
94
  catch {
58
- await this.qdrant.createCollection(MEMORIES_COLLECTION, VECTOR_SIZE);
95
+ await this.qdrant.createCollection(collection, VECTOR_SIZE);
59
96
  }
60
97
  }
61
98
  async remember(content, tags = [], metadata, visibility = 'private', allowPii = false) {
@@ -69,7 +106,7 @@ export class AgentBrain {
69
106
  const embedding = await this.embeddings.embed(sanitizeResult.sanitized);
70
107
  const id = randomUUID();
71
108
  const now = new Date().toISOString();
72
- await this.qdrant.upsertOne(MEMORIES_COLLECTION, id, embedding, {
109
+ await this.qdrant.upsertOne(this.getCollectionName(), id, embedding, {
73
110
  agent: this.agentId,
74
111
  agentType: this.agentType || 'default',
75
112
  content: sanitizeResult.sanitized, // Store sanitized content
@@ -108,7 +145,7 @@ export class AgentBrain {
108
145
  if (this.agentType) {
109
146
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
110
147
  }
111
- const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit, {
148
+ const results = await this.qdrant.search(this.getCollectionName(), queryEmbedding, limit, {
112
149
  filter,
113
150
  scoreThreshold: minScore,
114
151
  });
@@ -142,7 +179,7 @@ export class AgentBrain {
142
179
  }
143
180
  ]
144
181
  };
145
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
182
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
146
183
  filter,
147
184
  limit,
148
185
  with_payload: true,
@@ -162,7 +199,7 @@ export class AgentBrain {
162
199
  }
163
200
  async forget(memoryId) {
164
201
  await this.ensureCollection();
165
- await this.qdrant.deletePoints(MEMORIES_COLLECTION, [memoryId]);
202
+ await this.qdrant.deletePoints(this.getCollectionName(), [memoryId]);
166
203
  }
167
204
  async listMemories(limit = 20) {
168
205
  await this.ensureCollection();
@@ -174,7 +211,7 @@ export class AgentBrain {
174
211
  if (this.agentType) {
175
212
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
176
213
  }
177
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
214
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
178
215
  filter,
179
216
  limit,
180
217
  with_payload: true,
@@ -202,7 +239,7 @@ export class AgentBrain {
202
239
  if (this.agentType) {
203
240
  filter.must.push({ key: 'agentType', match: { value: this.agentType } });
204
241
  }
205
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
242
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
206
243
  filter,
207
244
  limit: 1000,
208
245
  with_payload: true,
@@ -228,7 +265,7 @@ export class AgentBrain {
228
265
  async publish(memoryId, visibility) {
229
266
  await this.ensureCollection();
230
267
  const now = new Date().toISOString();
231
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
268
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
232
269
  visibility,
233
270
  publishedBy: this.agentId,
234
271
  publishedAt: now,
@@ -241,7 +278,7 @@ export class AgentBrain {
241
278
  async unpublish(memoryId) {
242
279
  await this.ensureCollection();
243
280
  const now = new Date().toISOString();
244
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
281
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
245
282
  visibility: 'private',
246
283
  publishedBy: undefined,
247
284
  publishedAt: undefined,
@@ -269,7 +306,7 @@ export class AgentBrain {
269
306
  { key: 'status', match: { value: 'active' } },
270
307
  ],
271
308
  };
272
- const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit * 3, {
309
+ const results = await this.qdrant.search(this.getCollectionName(), queryEmbedding, limit * 3, {
273
310
  filter,
274
311
  scoreThreshold: minScore,
275
312
  });
@@ -312,7 +349,7 @@ export class AgentBrain {
312
349
  { key: 'status', match: { value: 'active' } },
313
350
  ],
314
351
  };
315
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
352
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
316
353
  filter,
317
354
  limit,
318
355
  with_payload: true,
@@ -340,13 +377,13 @@ export class AgentBrain {
340
377
  */
341
378
  async boost(memoryId) {
342
379
  await this.ensureCollection();
343
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
380
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
344
381
  if (!point)
345
382
  throw new Error('Memory not found');
346
383
  const boostCount = Number(point.payload?.boostCount || 0) + 1;
347
384
  const downvoteCount = Number(point.payload?.downvoteCount || 0);
348
385
  const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
349
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
386
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
350
387
  boostCount,
351
388
  qualityScore,
352
389
  updatedAt: new Date().toISOString(),
@@ -357,13 +394,13 @@ export class AgentBrain {
357
394
  */
358
395
  async downvote(memoryId) {
359
396
  await this.ensureCollection();
360
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
397
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
361
398
  if (!point)
362
399
  throw new Error('Memory not found');
363
400
  const boostCount = Number(point.payload?.boostCount || 0);
364
401
  const downvoteCount = Number(point.payload?.downvoteCount || 0) + 1;
365
402
  const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
366
- await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
403
+ await this.qdrant.setPayload(this.getCollectionName(), memoryId, {
367
404
  downvoteCount,
368
405
  qualityScore,
369
406
  updatedAt: new Date().toISOString(),
@@ -374,7 +411,7 @@ export class AgentBrain {
374
411
  */
375
412
  async getQuality(memoryId) {
376
413
  await this.ensureCollection();
377
- const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
414
+ const point = await this.qdrant.getPoint(this.getCollectionName(), memoryId);
378
415
  if (!point)
379
416
  throw new Error('Memory not found');
380
417
  return {
@@ -434,7 +471,7 @@ export class AgentBrain {
434
471
  }))
435
472
  });
436
473
  }
437
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
474
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
438
475
  filter,
439
476
  limit: 1000,
440
477
  with_payload: true,
@@ -468,7 +505,7 @@ export class AgentBrain {
468
505
  boostCount,
469
506
  });
470
507
  if (!dryRun) {
471
- await this.qdrant.setPayload(MEMORIES_COLLECTION, String(r.id), {
508
+ await this.qdrant.setPayload(this.getCollectionName(), String(r.id), {
472
509
  status: 'archived',
473
510
  updatedAt: new Date().toISOString(),
474
511
  });
@@ -488,7 +525,7 @@ export class AgentBrain {
488
525
  { key: 'status', match: { value: 'archived' } },
489
526
  ],
490
527
  };
491
- const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
528
+ const results = await this.qdrant.scroll(this.getCollectionName(), {
492
529
  filter,
493
530
  limit: 1000,
494
531
  with_payload: true,
@@ -503,9 +540,51 @@ export class AgentBrain {
503
540
  }
504
541
  }
505
542
  if (toPurge.length > 0) {
506
- await this.qdrant.deletePoints(MEMORIES_COLLECTION, toPurge);
543
+ await this.qdrant.deletePoints(this.getCollectionName(), toPurge);
507
544
  }
508
545
  return toPurge.length;
509
546
  }
510
547
  }
548
+ export class KnowledgeBaseBrain {
549
+ brain;
550
+ kb;
551
+ agentType;
552
+ constructor(agentType, kb, agentId = 'default') {
553
+ if (!canAccessKnowledgeBase(agentType, kb)) {
554
+ throw new Error(`Access denied to knowledge base '${kb}'`);
555
+ }
556
+ this.agentType = agentType;
557
+ this.kb = kb;
558
+ this.brain = new AgentBrain({
559
+ agentId,
560
+ agentType,
561
+ collectionName: `${kb}-kb`
562
+ });
563
+ }
564
+ getCollectionName() {
565
+ return `${this.kb}-kb`;
566
+ }
567
+ async remember(content, tags = [], metadata) {
568
+ return this.brain.remember(content, tags, { ...metadata, knowledgeBase: this.kb, sourceAgent: this.agentType }, 'team', false);
569
+ }
570
+ async recall(query, limit = 5, minScore = 0.5) {
571
+ return this.brain.recall(query, limit, minScore);
572
+ }
573
+ async list(limit = 20) {
574
+ return this.brain.listMemories(limit);
575
+ }
576
+ async forget(memoryId) {
577
+ return this.brain.forget(memoryId);
578
+ }
579
+ async stats() {
580
+ return this.brain.stats();
581
+ }
582
+ getInfo() {
583
+ return {
584
+ agentType: this.agentType,
585
+ knowledgeBase: this.kb,
586
+ collectionName: this.getCollectionName(),
587
+ };
588
+ }
589
+ }
511
590
  export default AgentBrain;
@@ -0,0 +1,73 @@
1
+ export interface Memory {
2
+ id: string;
3
+ agent: string;
4
+ agentType?: string;
5
+ content: string;
6
+ tags: string[];
7
+ createdAt: string;
8
+ updatedAt: string;
9
+ metadata?: Record<string, unknown>;
10
+ visibility?: string;
11
+ publishedBy?: string;
12
+ publishedAt?: string;
13
+ useCount?: number;
14
+ endorsements?: number;
15
+ recallCount?: number;
16
+ qualityScore?: number;
17
+ status?: string;
18
+ }
19
+ export interface RecallResult {
20
+ memory: Memory;
21
+ score: number;
22
+ }
23
+ export interface BrainStats {
24
+ count: number;
25
+ tags: Record<string, number>;
26
+ }
27
+ export declare class ApiBrain {
28
+ private agentId;
29
+ private agentType?;
30
+ private knowledgeBase?;
31
+ constructor(options: {
32
+ agentId: string;
33
+ agentType?: string;
34
+ knowledgeBase?: string;
35
+ });
36
+ private validateKbAccess;
37
+ remember(content: string, tags?: string[], metadata?: Record<string, unknown>, visibility?: string): Promise<string>;
38
+ recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
39
+ list(limit?: number): Promise<Memory[]>;
40
+ forget(memoryId: string): Promise<void>;
41
+ stats(): Promise<BrainStats>;
42
+ tags(tagList: string[], limit?: number): Promise<Memory[]>;
43
+ publish(memoryId: string, visibility?: string): Promise<void>;
44
+ unpublish(memoryId: string): Promise<void>;
45
+ endorse(memoryId: string): Promise<void>;
46
+ getDatabaseInfo(): {
47
+ agentType?: string;
48
+ databaseName: string;
49
+ collectionName: string;
50
+ };
51
+ listMemories(limit?: number): Promise<Memory[]>;
52
+ recallByTags(tagList: string[], limit?: number): Promise<Memory[]>;
53
+ recallShared(query: string, limit?: number, minScore?: number, publicOnly?: boolean): Promise<RecallResult[]>;
54
+ listShared(limit?: number, publicOnly?: boolean): Promise<Memory[]>;
55
+ boost(_memoryId: string): Promise<void>;
56
+ downvote(_memoryId: string): Promise<void>;
57
+ getQuality(_memoryId: string): Promise<{
58
+ recallCount: number;
59
+ boostCount: number;
60
+ downvoteCount: number;
61
+ qualityScore: number;
62
+ status: string;
63
+ }>;
64
+ cleanup(_dryRun?: boolean, _threshold?: number, _minAgeDays?: number, _tags?: string[]): Promise<Memory[]>;
65
+ purge(_retentionDays?: number): Promise<number>;
66
+ static getInfo(): Promise<{
67
+ knowledgeBases: string[];
68
+ accessibleKnowledgeBases: string[];
69
+ role?: string;
70
+ }>;
71
+ }
72
+ export declare function shouldUseApi(): boolean;
73
+ export default ApiBrain;
@@ -0,0 +1,196 @@
1
+ import { getConfig } from "../../commands/config.js";
2
+ import { canAccessKnowledgeBase as checkKbAccess } from "../permissions-cache.js";
3
+ async function apiRequest(path, options = {}) {
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>");
7
+ }
8
+ const url = new URL(`/api/brain${path}`, config.apiUrl);
9
+ const res = await fetch(url.toString(), {
10
+ method: options.method || "POST",
11
+ headers: {
12
+ "x-api-key": config.apiKey,
13
+ "Content-Type": "application/json",
14
+ },
15
+ body: options.body ? JSON.stringify(options.body) : undefined,
16
+ });
17
+ if (!res.ok) {
18
+ const error = await res.json().catch(() => ({ error: res.statusText }));
19
+ if (res.status === 403) {
20
+ throw new Error(`Access denied: ${error.error || 'Permission denied to this resource'}`);
21
+ }
22
+ throw new Error(error.message || error.error || `HTTP ${res.status}`);
23
+ }
24
+ return res.json();
25
+ }
26
+ export class ApiBrain {
27
+ agentId;
28
+ agentType;
29
+ knowledgeBase;
30
+ constructor(options) {
31
+ this.agentId = options.agentId;
32
+ this.agentType = options.agentType;
33
+ this.knowledgeBase = options.knowledgeBase;
34
+ }
35
+ async validateKbAccess() {
36
+ if (!this.knowledgeBase)
37
+ return;
38
+ const hasAccess = await checkKbAccess(this.knowledgeBase);
39
+ if (!hasAccess) {
40
+ throw new Error(`Access denied to knowledge base '${this.knowledgeBase}'. Check your role permissions.`);
41
+ }
42
+ }
43
+ async remember(content, tags = [], metadata, visibility = "private") {
44
+ await this.validateKbAccess();
45
+ const result = await apiRequest("/remember", {
46
+ body: {
47
+ content,
48
+ tags,
49
+ agentId: this.agentId,
50
+ agentType: this.agentType,
51
+ metadata,
52
+ visibility,
53
+ knowledgeBase: this.knowledgeBase,
54
+ },
55
+ });
56
+ return result.id;
57
+ }
58
+ async recall(query, limit = 5, minScore = 0.5) {
59
+ await this.validateKbAccess();
60
+ const result = await apiRequest("/recall", {
61
+ body: {
62
+ query,
63
+ limit,
64
+ minScore,
65
+ agentId: this.agentId,
66
+ agentType: this.agentType,
67
+ knowledgeBase: this.knowledgeBase,
68
+ },
69
+ });
70
+ return result.results;
71
+ }
72
+ async list(limit = 20) {
73
+ await this.validateKbAccess();
74
+ const result = await apiRequest("/list", {
75
+ body: {
76
+ limit,
77
+ agentId: this.agentId,
78
+ agentType: this.agentType,
79
+ knowledgeBase: this.knowledgeBase,
80
+ },
81
+ });
82
+ return result.memories;
83
+ }
84
+ async forget(memoryId) {
85
+ await apiRequest("/forget", {
86
+ body: {
87
+ memoryId,
88
+ knowledgeBase: this.knowledgeBase,
89
+ },
90
+ });
91
+ }
92
+ async stats() {
93
+ const result = await apiRequest("/stats", {
94
+ body: {
95
+ agentId: this.agentId,
96
+ agentType: this.agentType,
97
+ knowledgeBase: this.knowledgeBase,
98
+ },
99
+ });
100
+ return { count: result.count, tags: result.tags };
101
+ }
102
+ async tags(tagList, limit = 10) {
103
+ const result = await apiRequest("/tags", {
104
+ body: {
105
+ tags: tagList,
106
+ limit,
107
+ agentId: this.agentId,
108
+ agentType: this.agentType,
109
+ knowledgeBase: this.knowledgeBase,
110
+ },
111
+ });
112
+ return result.memories;
113
+ }
114
+ async publish(memoryId, visibility = "team") {
115
+ await apiRequest("/publish", {
116
+ body: { memoryId, visibility },
117
+ });
118
+ }
119
+ async unpublish(memoryId) {
120
+ await apiRequest("/unpublish", {
121
+ body: { memoryId },
122
+ });
123
+ }
124
+ async endorse(memoryId) {
125
+ await apiRequest("/endorse", {
126
+ body: { memoryId },
127
+ });
128
+ }
129
+ getDatabaseInfo() {
130
+ return {
131
+ agentType: this.agentType,
132
+ databaseName: `api:${this.knowledgeBase || 'agent-memories'}`,
133
+ collectionName: this.knowledgeBase ? `${this.knowledgeBase}-kb` : 'agent-memories',
134
+ };
135
+ }
136
+ async listMemories(limit = 20) {
137
+ return this.list(limit);
138
+ }
139
+ async recallByTags(tagList, limit = 10) {
140
+ const result = await apiRequest("/tags", {
141
+ body: {
142
+ tags: tagList,
143
+ limit,
144
+ agentId: this.agentId,
145
+ agentType: this.agentType,
146
+ knowledgeBase: this.knowledgeBase,
147
+ },
148
+ });
149
+ return result.memories;
150
+ }
151
+ async recallShared(query, limit = 5, minScore = 0.5, publicOnly = false) {
152
+ const result = await apiRequest("/recall-shared", {
153
+ body: {
154
+ query,
155
+ limit,
156
+ minScore,
157
+ agentType: this.agentType,
158
+ visibility: publicOnly ? 'public' : 'team',
159
+ },
160
+ });
161
+ return result.results;
162
+ }
163
+ async listShared(limit = 20, publicOnly = false) {
164
+ const result = await apiRequest("/shared", {
165
+ body: {
166
+ limit,
167
+ agentType: this.agentType,
168
+ visibility: publicOnly ? 'public' : undefined,
169
+ },
170
+ });
171
+ return result.memories;
172
+ }
173
+ async boost(_memoryId) {
174
+ throw new Error("boost() is not supported via API. Use direct Qdrant access for quality operations.");
175
+ }
176
+ async downvote(_memoryId) {
177
+ throw new Error("downvote() is not supported via API. Use direct Qdrant access for quality operations.");
178
+ }
179
+ async getQuality(_memoryId) {
180
+ throw new Error("getQuality() is not supported via API. Use direct Qdrant access for quality operations.");
181
+ }
182
+ async cleanup(_dryRun = true, _threshold = 0.1, _minAgeDays = 90, _tags) {
183
+ throw new Error("cleanup() is not supported via API. Use direct Qdrant access for admin operations.");
184
+ }
185
+ async purge(_retentionDays = 365) {
186
+ throw new Error("purge() is not supported via API. Use direct Qdrant access for admin operations.");
187
+ }
188
+ static async getInfo() {
189
+ return apiRequest("/info", { method: "GET" });
190
+ }
191
+ }
192
+ export function shouldUseApi() {
193
+ const config = getConfig();
194
+ return Boolean(config.apiUrl && config.apiKey);
195
+ }
196
+ export default ApiBrain;
@@ -0,0 +1,18 @@
1
+ interface CachedPermissions {
2
+ role: string;
3
+ permissions: string[];
4
+ knowledgeBases: string[];
5
+ fetchedAt: number;
6
+ }
7
+ export declare function getPermissions(): Promise<CachedPermissions>;
8
+ export declare function clearPermissionsCache(): void;
9
+ export declare function hasPermission(permission: string): Promise<boolean>;
10
+ export declare function canAccessKnowledgeBase(kb: string): Promise<boolean>;
11
+ export declare function getAccessibleKnowledgeBases(): Promise<string[]>;
12
+ export declare function getCurrentRole(): Promise<string | null>;
13
+ export declare function getCacheStatus(): {
14
+ cached: boolean;
15
+ age: number | null;
16
+ expiresIn: number | null;
17
+ };
18
+ export {};
@@ -0,0 +1,104 @@
1
+ import { getConfig } from "../commands/config.js";
2
+ const CACHE_TTL_MS = 5 * 60 * 1000;
3
+ let cache = null;
4
+ let fetchPromise = null;
5
+ async function fetchPermissions() {
6
+ const config = getConfig();
7
+ if (!config.apiUrl || !config.apiKey) {
8
+ throw new Error("API not configured");
9
+ }
10
+ const url = new URL("/api/auth/whoami", config.apiUrl);
11
+ const res = await fetch(url.toString(), {
12
+ method: "GET",
13
+ headers: {
14
+ "x-api-key": config.apiKey,
15
+ "Content-Type": "application/json",
16
+ },
17
+ });
18
+ if (!res.ok) {
19
+ const error = await res.json().catch(() => ({ error: res.statusText }));
20
+ throw new Error(error.message || error.error || `HTTP ${res.status}`);
21
+ }
22
+ const data = await res.json();
23
+ const kbPermissions = data.permissions
24
+ .filter((p) => p.startsWith("kb:"))
25
+ .map((p) => p.replace("kb:", ""));
26
+ return {
27
+ role: data.role,
28
+ permissions: data.permissions,
29
+ knowledgeBases: kbPermissions,
30
+ fetchedAt: Date.now(),
31
+ };
32
+ }
33
+ export async function getPermissions() {
34
+ const now = Date.now();
35
+ if (cache && now - cache.fetchedAt < CACHE_TTL_MS) {
36
+ return cache;
37
+ }
38
+ if (!fetchPromise) {
39
+ fetchPromise = fetchPermissions().then(result => {
40
+ cache = result;
41
+ fetchPromise = null;
42
+ return result;
43
+ }).catch(err => {
44
+ fetchPromise = null;
45
+ throw err;
46
+ });
47
+ }
48
+ return fetchPromise;
49
+ }
50
+ export function clearPermissionsCache() {
51
+ cache = null;
52
+ fetchPromise = null;
53
+ }
54
+ export async function hasPermission(permission) {
55
+ try {
56
+ const perms = await getPermissions();
57
+ if (perms.permissions.includes("*"))
58
+ return true;
59
+ if (perms.permissions.includes(permission))
60
+ return true;
61
+ const [scope] = permission.split(":");
62
+ if (perms.permissions.includes(`${scope}:*`))
63
+ return true;
64
+ return false;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ export async function canAccessKnowledgeBase(kb) {
71
+ try {
72
+ const perms = await getPermissions();
73
+ return perms.knowledgeBases.includes(kb) || perms.permissions.includes("kb:*");
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ export async function getAccessibleKnowledgeBases() {
80
+ try {
81
+ const perms = await getPermissions();
82
+ return perms.knowledgeBases;
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ }
88
+ export async function getCurrentRole() {
89
+ try {
90
+ const perms = await getPermissions();
91
+ return perms.role;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ export function getCacheStatus() {
98
+ if (!cache) {
99
+ return { cached: false, age: null, expiresIn: null };
100
+ }
101
+ const age = Date.now() - cache.fetchedAt;
102
+ const expiresIn = Math.max(0, CACHE_TTL_MS - age);
103
+ return { cached: true, age, expiresIn };
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {