@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.
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +262 -0
- package/dist/commands/brain.js +130 -7
- package/dist/lib/biz/agent-brain.d.ts +29 -1
- package/dist/lib/biz/agent-brain.js +72 -1
- package/dist/lib/biz/api-brain.d.ts +73 -0
- package/dist/lib/biz/api-brain.js +196 -0
- package/dist/lib/permissions-cache.d.ts +18 -0
- package/dist/lib/permissions-cache.js +104 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
package/dist/commands/brain.js
CHANGED
|
@@ -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
|
|
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 ["
|
|
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 = ['
|
|
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
|
+
}
|