@simonfestl/husky-cli 1.9.2 → 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", "reviewer", "e2e_agent", "pr_agent"];
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,6 +45,7 @@ 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;
@@ -106,4 +114,24 @@ export declare class AgentBrain {
106
114
  */
107
115
  purge(retentionDays?: number): Promise<number>;
108
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
+ }
109
137
  export default AgentBrain;
@@ -5,7 +5,32 @@ import { randomUUID } from 'crypto';
5
5
  import { sanitizeForEmbedding } from './pii-filter.js';
6
6
  const DEFAULT_COLLECTION = 'agent-memories';
7
7
  const VECTOR_SIZE = 768;
8
- export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker', 'reviewer', 'e2e_agent', 'pr_agent'];
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({
@@ -53,6 +80,8 @@ export class AgentBrain {
53
80
  };
54
81
  }
55
82
  getCollectionName() {
83
+ if (this.collectionNameOverride)
84
+ return this.collectionNameOverride;
56
85
  if (!this.agentType)
57
86
  return DEFAULT_COLLECTION;
58
87
  return `${this.agentType}-memories`;
@@ -516,4 +545,46 @@ export class AgentBrain {
516
545
  return toPurge.length;
517
546
  }
518
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
+ }
519
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.2",
3
+ "version": "1.10.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {