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