@prajwolkc/stk 0.6.1 → 0.7.1
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/brain.js +36 -3
- package/dist/mcp/server.js +14 -1553
- package/dist/mcp/tools/brain.d.ts +2 -0
- package/dist/mcp/tools/brain.js +557 -0
- package/dist/mcp/tools/data.d.ts +2 -0
- package/dist/mcp/tools/data.js +385 -0
- package/dist/mcp/tools/github.d.ts +2 -0
- package/dist/mcp/tools/github.js +95 -0
- package/dist/mcp/tools/infra.d.ts +2 -0
- package/dist/mcp/tools/infra.js +263 -0
- package/dist/mcp/tools/ops.d.ts +2 -0
- package/dist/mcp/tools/ops.js +411 -0
- package/dist/mcp/tools/security.d.ts +2 -0
- package/dist/mcp/tools/security.js +25 -0
- package/dist/mcp/types.d.ts +2 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/brain-cloud.d.ts +13 -0
- package/dist/services/brain-cloud.js +131 -0
- package/dist/services/brain-extract.d.ts +14 -0
- package/dist/services/brain-extract.js +253 -0
- package/dist/services/brain-search.d.ts +33 -0
- package/dist/services/brain-search.js +153 -0
- package/dist/services/brain-store.d.ts +25 -0
- package/dist/services/brain-store.js +42 -0
- package/dist/services/brain.d.ts +19 -68
- package/dist/services/brain.js +18 -542
- package/dist/services/metrics.d.ts +35 -0
- package/dist/services/metrics.js +78 -0
- package/dist/services/security.d.ts +19 -0
- package/dist/services/security.js +194 -0
- package/package.json +2 -1
- package/src/data/seed-patterns.json +1802 -0
package/dist/services/brain.js
CHANGED
|
@@ -1,326 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Brain module — re-exports from split modules.
|
|
3
|
+
*
|
|
4
|
+
* Split into:
|
|
5
|
+
* brain-store.ts — Types, storage, persistence
|
|
6
|
+
* brain-extract.ts — File extractors, project ingestion
|
|
7
|
+
* brain-search.ts — Smart search, check, diagnose, seed, review
|
|
8
|
+
* brain-cloud.ts — Supabase cloud sync
|
|
9
|
+
*/
|
|
4
10
|
import { randomUUID } from "crypto";
|
|
5
11
|
import { loadConfig } from "../lib/config.js";
|
|
12
|
+
export { loadBrainStore, saveBrainStore, getAllEntries } from "./brain-store.js";
|
|
13
|
+
export { extractFromClaudeMd, extractFromPackageJson, extractFromPrismaSchema, extractFromDockerfile, extractFromCIConfig, extractFromRoutes, extractFromStkConfig, ingestProject } from "./brain-extract.js";
|
|
14
|
+
export { smartSearch, extractTerms, brainCheck, brainDiagnose, seedBrain, reviewDiff, getContributor } from "./brain-search.js";
|
|
15
|
+
export { pushToCloud, pullFromCloud, syncBrain, cloudInsert } from "./brain-cloud.js";
|
|
6
16
|
// ──────────────────────────────────────────
|
|
7
|
-
//
|
|
8
|
-
// ──────────────────────────────────────────
|
|
9
|
-
const STK_DIR = join(homedir(), ".stk");
|
|
10
|
-
const BRAIN_PATH = join(STK_DIR, "brain.json");
|
|
11
|
-
function ensureStkDir() {
|
|
12
|
-
if (!existsSync(STK_DIR))
|
|
13
|
-
mkdirSync(STK_DIR, { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
export function loadBrainStore() {
|
|
16
|
-
ensureStkDir();
|
|
17
|
-
if (!existsSync(BRAIN_PATH)) {
|
|
18
|
-
return { version: 1, projects: {}, global: [] };
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
const raw = readFileSync(BRAIN_PATH, "utf-8");
|
|
22
|
-
return JSON.parse(raw);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return { version: 1, projects: {}, global: [] };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export function saveBrainStore(store) {
|
|
29
|
-
ensureStkDir();
|
|
30
|
-
writeFileSync(BRAIN_PATH, JSON.stringify(store, null, 2));
|
|
31
|
-
}
|
|
32
|
-
/** Get all entries — optionally scoped to a project */
|
|
33
|
-
export function getAllEntries(store, projectName) {
|
|
34
|
-
const entries = [...store.global];
|
|
35
|
-
if (projectName && store.projects[projectName]) {
|
|
36
|
-
entries.push(...store.projects[projectName].entries);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
for (const proj of Object.values(store.projects)) {
|
|
40
|
-
entries.push(...proj.entries);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return entries;
|
|
44
|
-
}
|
|
45
|
-
// ──────────────────────────────────────────
|
|
46
|
-
// Extractors
|
|
47
|
-
// ──────────────────────────────────────────
|
|
48
|
-
function makeEntry(title, content, category, source, tags) {
|
|
49
|
-
return { id: randomUUID(), title, content, category, source, tags, created_at: new Date().toISOString() };
|
|
50
|
-
}
|
|
51
|
-
/** Extract knowledge from CLAUDE.md sections */
|
|
52
|
-
export function extractFromClaudeMd(filePath, projectName) {
|
|
53
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
54
|
-
const entries = [];
|
|
55
|
-
const source = `project:${projectName}`;
|
|
56
|
-
// Split by ## headings
|
|
57
|
-
const sections = raw.split(/^## /m).slice(1);
|
|
58
|
-
const categoryMap = {
|
|
59
|
-
architecture: "architecture", commands: "deployment", "key paths": "architecture",
|
|
60
|
-
"code rules": "architecture", "theming": "architecture", "backend patterns": "architecture",
|
|
61
|
-
"auth": "auth", "permissions": "auth", "frontend patterns": "architecture",
|
|
62
|
-
"testing": "testing", "environment": "deployment", "cache": "performance",
|
|
63
|
-
"queue": "architecture", "database": "database", "deploy": "deployment",
|
|
64
|
-
"data": "database", "api": "api", "route": "api", "security": "security",
|
|
65
|
-
};
|
|
66
|
-
for (const section of sections) {
|
|
67
|
-
const lines = section.split("\n");
|
|
68
|
-
const heading = lines[0]?.trim() ?? "";
|
|
69
|
-
const body = lines.slice(1).join("\n").trim();
|
|
70
|
-
if (!heading || body.length < 20)
|
|
71
|
-
continue;
|
|
72
|
-
// Infer category from heading
|
|
73
|
-
const headingLower = heading.toLowerCase();
|
|
74
|
-
let category = "general";
|
|
75
|
-
for (const [keyword, cat] of Object.entries(categoryMap)) {
|
|
76
|
-
if (headingLower.includes(keyword)) {
|
|
77
|
-
category = cat;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Truncate very long sections
|
|
82
|
-
const truncated = body.length > 2000 ? body.slice(0, 2000) + "\n..." : body;
|
|
83
|
-
entries.push(makeEntry(heading, truncated, category, source, [heading.toLowerCase().replace(/[^a-z0-9]+/g, "-")]));
|
|
84
|
-
}
|
|
85
|
-
return entries;
|
|
86
|
-
}
|
|
87
|
-
/** Extract knowledge from package.json */
|
|
88
|
-
export function extractFromPackageJson(filePath, projectName) {
|
|
89
|
-
try {
|
|
90
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
91
|
-
const pkg = JSON.parse(raw);
|
|
92
|
-
const source = `project:${projectName}`;
|
|
93
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
94
|
-
const depNames = Object.keys(deps);
|
|
95
|
-
const groups = {
|
|
96
|
-
framework: [], orm: [], auth: [], billing: [], testing: [], ui: [], build: [], other: [],
|
|
97
|
-
};
|
|
98
|
-
const classify = {
|
|
99
|
-
react: "framework", next: "framework", express: "framework", fastify: "framework", "vue": "framework", angular: "framework",
|
|
100
|
-
prisma: "orm", typeorm: "orm", drizzle: "orm", sequelize: "orm", mongoose: "orm",
|
|
101
|
-
jsonwebtoken: "auth", passport: "auth", "next-auth": "auth", bcrypt: "auth",
|
|
102
|
-
stripe: "billing", "@stripe/stripe-js": "billing",
|
|
103
|
-
jest: "testing", vitest: "testing", mocha: "testing", supertest: "testing",
|
|
104
|
-
tailwindcss: "ui", "@radix-ui": "ui", "framer-motion": "ui", "shadcn": "ui",
|
|
105
|
-
vite: "build", webpack: "build", esbuild: "build", tsx: "build", typescript: "build",
|
|
106
|
-
};
|
|
107
|
-
for (const dep of depNames) {
|
|
108
|
-
let grouped = false;
|
|
109
|
-
for (const [key, group] of Object.entries(classify)) {
|
|
110
|
-
if (dep.includes(key)) {
|
|
111
|
-
groups[group].push(dep);
|
|
112
|
-
grouped = true;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (!grouped)
|
|
117
|
-
groups.other.push(dep);
|
|
118
|
-
}
|
|
119
|
-
const parts = [];
|
|
120
|
-
if (pkg.name)
|
|
121
|
-
parts.push(`Package: ${pkg.name}`);
|
|
122
|
-
if (pkg.scripts)
|
|
123
|
-
parts.push(`Scripts: ${Object.keys(pkg.scripts).join(", ")}`);
|
|
124
|
-
for (const [group, deps] of Object.entries(groups)) {
|
|
125
|
-
if (deps.length > 0 && group !== "other")
|
|
126
|
-
parts.push(`${group}: ${deps.join(", ")}`);
|
|
127
|
-
}
|
|
128
|
-
const label = filePath.includes("node-backend") ? "Backend" :
|
|
129
|
-
filePath.includes("frontend") ? "Frontend" : "Root";
|
|
130
|
-
return [makeEntry(`${label} Dependencies & Scripts`, parts.join("\n"), "stack", source, ["dependencies", label.toLowerCase(), ...depNames.slice(0, 10)])];
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/** Extract knowledge from Prisma schema */
|
|
137
|
-
export function extractFromPrismaSchema(filePath, projectName) {
|
|
138
|
-
try {
|
|
139
|
-
const content = readFileSync(filePath, "utf-8");
|
|
140
|
-
const source = `project:${projectName}`;
|
|
141
|
-
const models = content.match(/^model \w+/gm) ?? [];
|
|
142
|
-
const modelNames = models.map(m => m.replace("model ", ""));
|
|
143
|
-
const enums = content.match(/^enum \w+/gm) ?? [];
|
|
144
|
-
const enumNames = enums.map(e => e.replace("enum ", ""));
|
|
145
|
-
const hasOrgId = content.includes("organizationId");
|
|
146
|
-
const hasSoftDelete = content.includes("deletedAt");
|
|
147
|
-
const hasTimestamps = content.includes("@updatedAt");
|
|
148
|
-
const hasRelations = content.match(/@relation/g)?.length ?? 0;
|
|
149
|
-
const parts = [
|
|
150
|
-
`${models.length} models: ${modelNames.join(", ")}`,
|
|
151
|
-
];
|
|
152
|
-
if (enums.length)
|
|
153
|
-
parts.push(`${enums.length} enums: ${enumNames.join(", ")}`);
|
|
154
|
-
if (hasOrgId)
|
|
155
|
-
parts.push("Multi-tenant: organizationId on entities");
|
|
156
|
-
if (hasSoftDelete)
|
|
157
|
-
parts.push("Soft deletes: deletedAt field");
|
|
158
|
-
if (hasTimestamps)
|
|
159
|
-
parts.push("Auto timestamps: createdAt/updatedAt");
|
|
160
|
-
if (hasRelations)
|
|
161
|
-
parts.push(`${hasRelations} relations defined`);
|
|
162
|
-
return [makeEntry("Database Schema Overview", parts.join("\n"), "database", source, ["prisma", "schema", "database", ...modelNames.slice(0, 15)])];
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/** Extract knowledge from Dockerfile */
|
|
169
|
-
export function extractFromDockerfile(filePath, projectName) {
|
|
170
|
-
try {
|
|
171
|
-
const content = readFileSync(filePath, "utf-8");
|
|
172
|
-
const source = `project:${projectName}`;
|
|
173
|
-
const parts = [];
|
|
174
|
-
const baseImages = content.match(/^FROM\s+\S+/gm) ?? [];
|
|
175
|
-
if (baseImages.length)
|
|
176
|
-
parts.push(`Base images: ${baseImages.map(b => b.replace("FROM ", "")).join(" → ")}`);
|
|
177
|
-
if (baseImages.length > 1)
|
|
178
|
-
parts.push("Multi-stage build");
|
|
179
|
-
if (content.includes("HEALTHCHECK"))
|
|
180
|
-
parts.push("Has healthcheck");
|
|
181
|
-
if (content.includes("USER") && !content.includes("USER root"))
|
|
182
|
-
parts.push("Non-root user");
|
|
183
|
-
if (content.includes("tini"))
|
|
184
|
-
parts.push("Uses tini init");
|
|
185
|
-
const ports = content.match(/EXPOSE\s+(\d+)/g);
|
|
186
|
-
if (ports)
|
|
187
|
-
parts.push(`Ports: ${ports.map(p => p.replace("EXPOSE ", "")).join(", ")}`);
|
|
188
|
-
return parts.length > 0
|
|
189
|
-
? [makeEntry("Docker Configuration", parts.join("\n"), "deployment", source, ["docker", "container"])]
|
|
190
|
-
: [];
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/** Extract knowledge from CI config */
|
|
197
|
-
export function extractFromCIConfig(filePath, projectName) {
|
|
198
|
-
try {
|
|
199
|
-
const content = readFileSync(filePath, "utf-8");
|
|
200
|
-
const source = `project:${projectName}`;
|
|
201
|
-
const parts = [];
|
|
202
|
-
if (filePath.includes(".github"))
|
|
203
|
-
parts.push("CI: GitHub Actions");
|
|
204
|
-
else if (filePath.includes(".gitlab"))
|
|
205
|
-
parts.push("CI: GitLab CI");
|
|
206
|
-
else if (filePath.includes("circle"))
|
|
207
|
-
parts.push("CI: CircleCI");
|
|
208
|
-
const jobs = content.match(/^\s{2}\w[\w-]*:/gm);
|
|
209
|
-
if (jobs)
|
|
210
|
-
parts.push(`Jobs: ${jobs.map(j => j.trim().replace(":", "")).join(", ")}`);
|
|
211
|
-
if (content.includes("tsc"))
|
|
212
|
-
parts.push("Type checking step");
|
|
213
|
-
if (content.includes("test"))
|
|
214
|
-
parts.push("Test step");
|
|
215
|
-
if (content.includes("docker"))
|
|
216
|
-
parts.push("Docker build step");
|
|
217
|
-
if (content.includes("audit"))
|
|
218
|
-
parts.push("Security audit step");
|
|
219
|
-
const triggers = content.match(/on:\s*\n([\s\S]*?)(?=\n\w)/);
|
|
220
|
-
if (triggers) {
|
|
221
|
-
if (content.includes("push:"))
|
|
222
|
-
parts.push("Triggers on push");
|
|
223
|
-
if (content.includes("pull_request:"))
|
|
224
|
-
parts.push("Triggers on PR");
|
|
225
|
-
}
|
|
226
|
-
return parts.length > 0
|
|
227
|
-
? [makeEntry("CI/CD Pipeline", parts.join("\n"), "deployment", source, ["ci", "pipeline"])]
|
|
228
|
-
: [];
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/** Extract knowledge from route files directory */
|
|
235
|
-
export function extractFromRoutes(routeDir, projectName) {
|
|
236
|
-
try {
|
|
237
|
-
const source = `project:${projectName}`;
|
|
238
|
-
const files = readdirSync(routeDir).filter(f => f.endsWith(".ts") || f.endsWith(".js"));
|
|
239
|
-
const routeNames = files.map(f => f.replace(/\.(ts|js)$/, ""));
|
|
240
|
-
return [makeEntry("API Routes", `${files.length} route files: ${routeNames.join(", ")}`, "api", source, ["routes", "api", ...routeNames.slice(0, 15)])];
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return [];
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/** Extract knowledge from stk.config.json */
|
|
247
|
-
export function extractFromStkConfig(filePath, projectName) {
|
|
248
|
-
try {
|
|
249
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
250
|
-
const config = JSON.parse(raw);
|
|
251
|
-
const source = `project:${projectName}`;
|
|
252
|
-
const services = Object.entries(config.services ?? {})
|
|
253
|
-
.filter(([, v]) => v === true || (typeof v === "object" && v.enabled !== false))
|
|
254
|
-
.map(([k]) => k);
|
|
255
|
-
return services.length > 0
|
|
256
|
-
? [makeEntry("Infrastructure Services", `Configured services: ${services.join(", ")}`, "architecture", source, ["infrastructure", ...services])]
|
|
257
|
-
: [];
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
export function ingestProject(projectPath) {
|
|
264
|
-
const config = loadConfig();
|
|
265
|
-
const projectName = config.name ?? basename(projectPath);
|
|
266
|
-
const entries = [];
|
|
267
|
-
const filesScanned = [];
|
|
268
|
-
const fileExtractors = [
|
|
269
|
-
{ path: "CLAUDE.md", extractor: extractFromClaudeMd },
|
|
270
|
-
{ path: "package.json", extractor: extractFromPackageJson },
|
|
271
|
-
{ path: "node-backend/package.json", extractor: extractFromPackageJson },
|
|
272
|
-
{ path: "frontend/package.json", extractor: extractFromPackageJson },
|
|
273
|
-
{ path: "node-backend/prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
274
|
-
{ path: "Dockerfile", extractor: extractFromDockerfile },
|
|
275
|
-
{ path: ".github/workflows/ci.yml", extractor: extractFromCIConfig },
|
|
276
|
-
{ path: "stk.config.json", extractor: extractFromStkConfig },
|
|
277
|
-
];
|
|
278
|
-
// Also try common alternative locations
|
|
279
|
-
const altPaths = [
|
|
280
|
-
{ path: "prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
281
|
-
{ path: "src/prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
282
|
-
{ path: ".github/workflows/main.yml", extractor: extractFromCIConfig },
|
|
283
|
-
{ path: ".github/workflows/deploy.yml", extractor: extractFromCIConfig },
|
|
284
|
-
{ path: ".gitlab-ci.yml", extractor: extractFromCIConfig },
|
|
285
|
-
{ path: "docker-compose.yml", extractor: extractFromDockerfile },
|
|
286
|
-
{ path: "backend/package.json", extractor: extractFromPackageJson },
|
|
287
|
-
{ path: "server/package.json", extractor: extractFromPackageJson },
|
|
288
|
-
{ path: "api/package.json", extractor: extractFromPackageJson },
|
|
289
|
-
];
|
|
290
|
-
const allPaths = [...fileExtractors, ...altPaths];
|
|
291
|
-
const seen = new Set();
|
|
292
|
-
for (const { path, extractor } of allPaths) {
|
|
293
|
-
const fullPath = resolve(projectPath, path);
|
|
294
|
-
if (seen.has(fullPath) || !existsSync(fullPath))
|
|
295
|
-
continue;
|
|
296
|
-
seen.add(fullPath);
|
|
297
|
-
const extracted = extractor(fullPath, projectName);
|
|
298
|
-
if (extracted.length > 0) {
|
|
299
|
-
entries.push(...extracted);
|
|
300
|
-
filesScanned.push(path);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Route directories
|
|
304
|
-
const routeDirs = [
|
|
305
|
-
"node-backend/src/routes",
|
|
306
|
-
"src/routes",
|
|
307
|
-
"backend/src/routes",
|
|
308
|
-
"server/src/routes",
|
|
309
|
-
"api/src/routes",
|
|
310
|
-
];
|
|
311
|
-
for (const dir of routeDirs) {
|
|
312
|
-
const fullDir = resolve(projectPath, dir);
|
|
313
|
-
if (existsSync(fullDir)) {
|
|
314
|
-
entries.push(...extractFromRoutes(fullDir, projectName));
|
|
315
|
-
filesScanned.push(dir);
|
|
316
|
-
break; // only one route dir
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return { projectName, entries, filesScanned };
|
|
320
|
-
}
|
|
321
|
-
// ──────────────────────────────────────────
|
|
322
|
-
// Local brain client (replaces getBrainClient)
|
|
17
|
+
// Local brain client (stays here as the MCP interface)
|
|
323
18
|
// ──────────────────────────────────────────
|
|
19
|
+
import { loadBrainStore, saveBrainStore, getAllEntries } from "./brain-store.js";
|
|
20
|
+
import { ingestProject } from "./brain-extract.js";
|
|
21
|
+
import { cloudInsert } from "./brain-cloud.js";
|
|
324
22
|
export function getLocalBrainClient() {
|
|
325
23
|
const store = loadBrainStore();
|
|
326
24
|
const config = loadConfig();
|
|
@@ -328,7 +26,7 @@ export function getLocalBrainClient() {
|
|
|
328
26
|
// Auto-ingest if this project hasn't been scanned yet
|
|
329
27
|
if (projectName && projectName !== "my-app" && !store.projects[projectName]) {
|
|
330
28
|
try {
|
|
331
|
-
const { entries
|
|
29
|
+
const { entries } = ingestProject(process.cwd());
|
|
332
30
|
if (entries.length > 0) {
|
|
333
31
|
store.projects[projectName] = {
|
|
334
32
|
ingestedAt: new Date().toISOString(),
|
|
@@ -346,7 +44,6 @@ export function getLocalBrainClient() {
|
|
|
346
44
|
async query(_table, params = {}) {
|
|
347
45
|
const currentStore = loadBrainStore();
|
|
348
46
|
let entries = getAllEntries(currentStore);
|
|
349
|
-
// Handle ilike search (Supabase PostgREST style)
|
|
350
47
|
if (params.or) {
|
|
351
48
|
const matches = params.or.match(/ilike\.%(.+?)%/g);
|
|
352
49
|
if (matches) {
|
|
@@ -356,13 +53,10 @@ export function getLocalBrainClient() {
|
|
|
356
53
|
e.tags.some(t => t.toLowerCase().includes(term))));
|
|
357
54
|
}
|
|
358
55
|
}
|
|
359
|
-
// Handle category filter
|
|
360
56
|
if (params.category) {
|
|
361
57
|
const cat = params.category.replace("eq.", "");
|
|
362
58
|
entries = entries.filter(e => e.category === cat);
|
|
363
59
|
}
|
|
364
|
-
// Handle select (for stats — just return all fields)
|
|
365
|
-
// Handle order (best-effort)
|
|
366
60
|
if (params.order) {
|
|
367
61
|
const field = params.order;
|
|
368
62
|
entries.sort((a, b) => String(a[field] ?? "").localeCompare(String(b[field] ?? "")));
|
|
@@ -393,221 +87,3 @@ export function getLocalBrainClient() {
|
|
|
393
87
|
},
|
|
394
88
|
};
|
|
395
89
|
}
|
|
396
|
-
// ──────────────────────────────────────────
|
|
397
|
-
// Cloud sync (Supabase-backed)
|
|
398
|
-
// ──────────────────────────────────────────
|
|
399
|
-
function getCloudConfig() {
|
|
400
|
-
const url = process.env.SUPABASE_URL;
|
|
401
|
-
const key = process.env.SUPABASE_SERVICE_KEY;
|
|
402
|
-
if (!url || !key)
|
|
403
|
-
return null;
|
|
404
|
-
return { url, key };
|
|
405
|
-
}
|
|
406
|
-
async function cloudInsert(entry) {
|
|
407
|
-
const cloud = getCloudConfig();
|
|
408
|
-
if (!cloud)
|
|
409
|
-
return false;
|
|
410
|
-
const res = await fetch(`${cloud.url}/rest/v1/knowledge`, {
|
|
411
|
-
method: "POST",
|
|
412
|
-
headers: {
|
|
413
|
-
apikey: cloud.key,
|
|
414
|
-
Authorization: `Bearer ${cloud.key}`,
|
|
415
|
-
"Content-Type": "application/json",
|
|
416
|
-
Prefer: "resolution=ignore-duplicates",
|
|
417
|
-
},
|
|
418
|
-
body: JSON.stringify({
|
|
419
|
-
id: entry.id,
|
|
420
|
-
title: entry.title,
|
|
421
|
-
content: entry.content,
|
|
422
|
-
category: entry.category,
|
|
423
|
-
source: entry.source,
|
|
424
|
-
tags: entry.tags,
|
|
425
|
-
created_at: entry.created_at,
|
|
426
|
-
}),
|
|
427
|
-
});
|
|
428
|
-
return res.ok;
|
|
429
|
-
}
|
|
430
|
-
/** Push all local entries to cloud */
|
|
431
|
-
export async function pushToCloud() {
|
|
432
|
-
const cloud = getCloudConfig();
|
|
433
|
-
if (!cloud)
|
|
434
|
-
return { pushed: 0, pulled: 0, errors: ["SUPABASE_URL or SUPABASE_SERVICE_KEY not set"] };
|
|
435
|
-
const store = loadBrainStore();
|
|
436
|
-
const allLocal = getAllEntries(store);
|
|
437
|
-
let pushed = 0;
|
|
438
|
-
const errors = [];
|
|
439
|
-
// Get existing cloud IDs to avoid duplicates
|
|
440
|
-
const existingRes = await fetch(`${cloud.url}/rest/v1/knowledge?select=id&limit=10000`, {
|
|
441
|
-
headers: { apikey: cloud.key, Authorization: `Bearer ${cloud.key}` },
|
|
442
|
-
});
|
|
443
|
-
const existingData = existingRes.ok ? await existingRes.json() : [];
|
|
444
|
-
const existingIds = new Set(existingData.map((r) => r.id));
|
|
445
|
-
// Push entries that don't exist in cloud
|
|
446
|
-
const toInsert = allLocal.filter(e => !existingIds.has(e.id));
|
|
447
|
-
// Batch insert in chunks of 50
|
|
448
|
-
for (let i = 0; i < toInsert.length; i += 50) {
|
|
449
|
-
const batch = toInsert.slice(i, i + 50);
|
|
450
|
-
const res = await fetch(`${cloud.url}/rest/v1/knowledge`, {
|
|
451
|
-
method: "POST",
|
|
452
|
-
headers: {
|
|
453
|
-
apikey: cloud.key,
|
|
454
|
-
Authorization: `Bearer ${cloud.key}`,
|
|
455
|
-
"Content-Type": "application/json",
|
|
456
|
-
Prefer: "resolution=ignore-duplicates",
|
|
457
|
-
},
|
|
458
|
-
body: JSON.stringify(batch),
|
|
459
|
-
});
|
|
460
|
-
if (res.ok) {
|
|
461
|
-
pushed += batch.length;
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
const err = await res.text();
|
|
465
|
-
errors.push(`Batch insert failed: ${err}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return { pushed, pulled: 0, errors };
|
|
469
|
-
}
|
|
470
|
-
/** Pull cloud entries to local */
|
|
471
|
-
export async function pullFromCloud() {
|
|
472
|
-
const cloud = getCloudConfig();
|
|
473
|
-
if (!cloud)
|
|
474
|
-
return { pushed: 0, pulled: 0, errors: ["SUPABASE_URL or SUPABASE_SERVICE_KEY not set"] };
|
|
475
|
-
const store = loadBrainStore();
|
|
476
|
-
const localIds = new Set(getAllEntries(store).map(e => e.id));
|
|
477
|
-
let pulled = 0;
|
|
478
|
-
const errors = [];
|
|
479
|
-
// Fetch all cloud entries
|
|
480
|
-
const res = await fetch(`${cloud.url}/rest/v1/knowledge?select=*&limit=10000&order=created_at.desc`, {
|
|
481
|
-
headers: {
|
|
482
|
-
apikey: cloud.key,
|
|
483
|
-
Authorization: `Bearer ${cloud.key}`,
|
|
484
|
-
"Content-Type": "application/json",
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
if (!res.ok) {
|
|
488
|
-
const err = await res.text();
|
|
489
|
-
return { pushed: 0, pulled: 0, errors: [`Cloud fetch failed: ${err}`] };
|
|
490
|
-
}
|
|
491
|
-
const cloudEntries = await res.json();
|
|
492
|
-
for (const entry of cloudEntries) {
|
|
493
|
-
if (localIds.has(entry.id))
|
|
494
|
-
continue;
|
|
495
|
-
// Determine where to put it — if source matches a project, add to that project
|
|
496
|
-
const projectMatch = entry.source.match(/^project:(.+)$/);
|
|
497
|
-
if (projectMatch) {
|
|
498
|
-
const projName = projectMatch[1];
|
|
499
|
-
if (!store.projects[projName]) {
|
|
500
|
-
store.projects[projName] = {
|
|
501
|
-
ingestedAt: entry.created_at,
|
|
502
|
-
projectPath: "",
|
|
503
|
-
entries: [],
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
store.projects[projName].entries.push(entry);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
store.global.push(entry);
|
|
510
|
-
}
|
|
511
|
-
pulled++;
|
|
512
|
-
}
|
|
513
|
-
if (pulled > 0)
|
|
514
|
-
saveBrainStore(store);
|
|
515
|
-
return { pushed: 0, pulled, errors };
|
|
516
|
-
}
|
|
517
|
-
/** Full sync: push local → cloud, then pull cloud → local */
|
|
518
|
-
export async function syncBrain() {
|
|
519
|
-
const pushResult = await pushToCloud();
|
|
520
|
-
const pullResult = await pullFromCloud();
|
|
521
|
-
return {
|
|
522
|
-
pushed: pushResult.pushed,
|
|
523
|
-
pulled: pullResult.pulled,
|
|
524
|
-
errors: [...pushResult.errors, ...pullResult.errors],
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
/** Search brain with relevance scoring — returns entries ranked by how many terms match */
|
|
528
|
-
export function smartSearch(terms, category) {
|
|
529
|
-
const store = loadBrainStore();
|
|
530
|
-
let entries = getAllEntries(store);
|
|
531
|
-
if (category) {
|
|
532
|
-
entries = entries.filter(e => e.category === category);
|
|
533
|
-
}
|
|
534
|
-
const normalizedTerms = terms.map(t => t.toLowerCase());
|
|
535
|
-
const scored = [];
|
|
536
|
-
for (const entry of entries) {
|
|
537
|
-
const searchText = `${entry.title} ${entry.content} ${entry.tags.join(" ")}`.toLowerCase();
|
|
538
|
-
const matchedTerms = [];
|
|
539
|
-
let score = 0;
|
|
540
|
-
for (const term of normalizedTerms) {
|
|
541
|
-
if (searchText.includes(term)) {
|
|
542
|
-
matchedTerms.push(term);
|
|
543
|
-
// Title match scores higher
|
|
544
|
-
if (entry.title.toLowerCase().includes(term))
|
|
545
|
-
score += 3;
|
|
546
|
-
// Tag match scores high
|
|
547
|
-
else if (entry.tags.some(t => t.toLowerCase().includes(term)))
|
|
548
|
-
score += 2;
|
|
549
|
-
// Content match
|
|
550
|
-
else
|
|
551
|
-
score += 1;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
// Boost gotcha/debugging entries
|
|
555
|
-
if (entry.tags.some(t => ["gotcha", "debugging", "bug", "fix", "issue"].includes(t))) {
|
|
556
|
-
score *= 1.5;
|
|
557
|
-
}
|
|
558
|
-
if (score > 0) {
|
|
559
|
-
scored.push({ entry, score, matchedTerms });
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return scored.sort((a, b) => b.score - a.score);
|
|
563
|
-
}
|
|
564
|
-
/** Extract relevant terms from a task description */
|
|
565
|
-
export function extractTerms(description) {
|
|
566
|
-
const stopWords = new Set([
|
|
567
|
-
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
568
|
-
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
569
|
-
"should", "may", "might", "shall", "can", "need", "must", "to", "of",
|
|
570
|
-
"in", "for", "on", "with", "at", "by", "from", "as", "into", "about",
|
|
571
|
-
"like", "through", "after", "before", "between", "under", "above",
|
|
572
|
-
"and", "but", "or", "not", "no", "so", "if", "then", "than", "too",
|
|
573
|
-
"very", "just", "also", "how", "what", "when", "where", "why", "which",
|
|
574
|
-
"that", "this", "these", "those", "it", "its", "i", "we", "you", "they",
|
|
575
|
-
"me", "us", "my", "our", "your", "add", "implement", "create", "build",
|
|
576
|
-
"make", "want", "get", "set", "use", "new", "update", "change",
|
|
577
|
-
]);
|
|
578
|
-
const words = description.toLowerCase()
|
|
579
|
-
.replace(/[^a-z0-9\s-]/g, " ")
|
|
580
|
-
.split(/\s+/)
|
|
581
|
-
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
582
|
-
// Also extract multi-word phrases
|
|
583
|
-
const phrases = [];
|
|
584
|
-
const desc = description.toLowerCase();
|
|
585
|
-
const commonPhrases = [
|
|
586
|
-
"email verification", "password reset", "auth state", "user select",
|
|
587
|
-
"prisma select", "react context", "protected route", "refresh token",
|
|
588
|
-
"rate limit", "soft delete", "multi-tenant", "org scoping",
|
|
589
|
-
"file upload", "webhook", "cron job", "background job",
|
|
590
|
-
"api key", "role based", "permission", "middleware",
|
|
591
|
-
];
|
|
592
|
-
for (const phrase of commonPhrases) {
|
|
593
|
-
if (desc.includes(phrase))
|
|
594
|
-
phrases.push(phrase);
|
|
595
|
-
}
|
|
596
|
-
return [...new Set([...words, ...phrases])];
|
|
597
|
-
}
|
|
598
|
-
/** Proactive check — find gotchas relevant to a task before coding */
|
|
599
|
-
export function brainCheck(taskDescription) {
|
|
600
|
-
const terms = extractTerms(taskDescription);
|
|
601
|
-
return smartSearch(terms);
|
|
602
|
-
}
|
|
603
|
-
/** Diagnose an error — find matching patterns from past issues */
|
|
604
|
-
export function brainDiagnose(error) {
|
|
605
|
-
const terms = extractTerms(error);
|
|
606
|
-
// Add error-specific terms
|
|
607
|
-
const errorTerms = error.toLowerCase()
|
|
608
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
609
|
-
.split(/\s+/)
|
|
610
|
-
.filter(w => w.length > 3);
|
|
611
|
-
const allTerms = [...new Set([...terms, ...errorTerms])];
|
|
612
|
-
return smartSearch(allTerms);
|
|
613
|
-
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface MetricEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
type: "deploy" | "health_check" | "error" | "response_time";
|
|
4
|
+
value: number;
|
|
5
|
+
metadata: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
interface MetricsStore {
|
|
8
|
+
version: 1;
|
|
9
|
+
entries: MetricEntry[];
|
|
10
|
+
}
|
|
11
|
+
export declare function loadMetrics(): MetricsStore;
|
|
12
|
+
export declare function saveMetrics(store: MetricsStore): void;
|
|
13
|
+
export declare function recordMetric(type: MetricEntry["type"], value: number, metadata?: Record<string, string>): void;
|
|
14
|
+
export declare function getMetrics(type?: MetricEntry["type"], days?: number, limit?: number): MetricEntry[];
|
|
15
|
+
export declare function getBaseline(type: MetricEntry["type"]): number;
|
|
16
|
+
export declare function compareToBaseline(type: MetricEntry["type"]): {
|
|
17
|
+
current: number;
|
|
18
|
+
baseline: number;
|
|
19
|
+
changePct: number;
|
|
20
|
+
status: "degraded" | "stable" | "improved";
|
|
21
|
+
};
|
|
22
|
+
export declare function getDeployFrequency(days?: number): {
|
|
23
|
+
total: number;
|
|
24
|
+
perDay: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function getErrorRate(days?: number): {
|
|
27
|
+
total: number;
|
|
28
|
+
perDay: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function getUptime(days?: number): {
|
|
31
|
+
checks: number;
|
|
32
|
+
healthy: number;
|
|
33
|
+
pct: number;
|
|
34
|
+
};
|
|
35
|
+
export {};
|