@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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { resolve, basename } from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { loadConfig } from "../lib/config.js";
|
|
5
|
+
// ──────────────────────────────────────────
|
|
6
|
+
// Extractors
|
|
7
|
+
// ──────────────────────────────────────────
|
|
8
|
+
function makeEntry(title, content, category, source, tags) {
|
|
9
|
+
return { id: randomUUID(), title, content, category, source, tags, created_at: new Date().toISOString() };
|
|
10
|
+
}
|
|
11
|
+
export function extractFromClaudeMd(filePath, projectName) {
|
|
12
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
13
|
+
const entries = [];
|
|
14
|
+
const source = `project:${projectName}`;
|
|
15
|
+
const sections = raw.split(/^## /m).slice(1);
|
|
16
|
+
const categoryMap = {
|
|
17
|
+
architecture: "architecture", commands: "deployment", "key paths": "architecture",
|
|
18
|
+
"code rules": "architecture", "theming": "architecture", "backend patterns": "architecture",
|
|
19
|
+
"auth": "auth", "permissions": "auth", "frontend patterns": "architecture",
|
|
20
|
+
"testing": "testing", "environment": "deployment", "cache": "performance",
|
|
21
|
+
"queue": "architecture", "database": "database", "deploy": "deployment",
|
|
22
|
+
"data": "database", "api": "api", "route": "api", "security": "security",
|
|
23
|
+
};
|
|
24
|
+
for (const section of sections) {
|
|
25
|
+
const lines = section.split("\n");
|
|
26
|
+
const heading = lines[0]?.trim() ?? "";
|
|
27
|
+
const body = lines.slice(1).join("\n").trim();
|
|
28
|
+
if (!heading || body.length < 20)
|
|
29
|
+
continue;
|
|
30
|
+
const headingLower = heading.toLowerCase();
|
|
31
|
+
let category = "general";
|
|
32
|
+
for (const [keyword, cat] of Object.entries(categoryMap)) {
|
|
33
|
+
if (headingLower.includes(keyword)) {
|
|
34
|
+
category = cat;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const truncated = body.length > 2000 ? body.slice(0, 2000) + "\n..." : body;
|
|
39
|
+
entries.push(makeEntry(heading, truncated, category, source, [heading.toLowerCase().replace(/[^a-z0-9]+/g, "-")]));
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
export function extractFromPackageJson(filePath, projectName) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
46
|
+
const pkg = JSON.parse(raw);
|
|
47
|
+
const source = `project:${projectName}`;
|
|
48
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
49
|
+
const depNames = Object.keys(deps);
|
|
50
|
+
const groups = { framework: [], orm: [], auth: [], billing: [], testing: [], ui: [], build: [], other: [] };
|
|
51
|
+
const classify = {
|
|
52
|
+
react: "framework", next: "framework", express: "framework", fastify: "framework", vue: "framework", angular: "framework",
|
|
53
|
+
prisma: "orm", typeorm: "orm", drizzle: "orm", sequelize: "orm", mongoose: "orm",
|
|
54
|
+
jsonwebtoken: "auth", passport: "auth", "next-auth": "auth", bcrypt: "auth",
|
|
55
|
+
stripe: "billing", "@stripe/stripe-js": "billing",
|
|
56
|
+
jest: "testing", vitest: "testing", mocha: "testing", supertest: "testing",
|
|
57
|
+
tailwindcss: "ui", "@radix-ui": "ui", "framer-motion": "ui", shadcn: "ui",
|
|
58
|
+
vite: "build", webpack: "build", esbuild: "build", tsx: "build", typescript: "build",
|
|
59
|
+
};
|
|
60
|
+
for (const dep of depNames) {
|
|
61
|
+
let grouped = false;
|
|
62
|
+
for (const [key, group] of Object.entries(classify)) {
|
|
63
|
+
if (dep.includes(key)) {
|
|
64
|
+
groups[group].push(dep);
|
|
65
|
+
grouped = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!grouped)
|
|
70
|
+
groups.other.push(dep);
|
|
71
|
+
}
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (pkg.name)
|
|
74
|
+
parts.push(`Package: ${pkg.name}`);
|
|
75
|
+
if (pkg.scripts)
|
|
76
|
+
parts.push(`Scripts: ${Object.keys(pkg.scripts).join(", ")}`);
|
|
77
|
+
for (const [group, deps] of Object.entries(groups)) {
|
|
78
|
+
if (deps.length > 0 && group !== "other")
|
|
79
|
+
parts.push(`${group}: ${deps.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
const label = filePath.includes("node-backend") ? "Backend" : filePath.includes("frontend") ? "Frontend" : "Root";
|
|
82
|
+
return [makeEntry(`${label} Dependencies & Scripts`, parts.join("\n"), "stack", source, ["dependencies", label.toLowerCase(), ...depNames.slice(0, 10)])];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function extractFromPrismaSchema(filePath, projectName) {
|
|
89
|
+
try {
|
|
90
|
+
const content = readFileSync(filePath, "utf-8");
|
|
91
|
+
const source = `project:${projectName}`;
|
|
92
|
+
const models = content.match(/^model \w+/gm) ?? [];
|
|
93
|
+
const modelNames = models.map(m => m.replace("model ", ""));
|
|
94
|
+
const enums = content.match(/^enum \w+/gm) ?? [];
|
|
95
|
+
const enumNames = enums.map(e => e.replace("enum ", ""));
|
|
96
|
+
const hasOrgId = content.includes("organizationId");
|
|
97
|
+
const hasSoftDelete = content.includes("deletedAt");
|
|
98
|
+
const hasTimestamps = content.includes("@updatedAt");
|
|
99
|
+
const hasRelations = content.match(/@relation/g)?.length ?? 0;
|
|
100
|
+
const parts = [`${models.length} models: ${modelNames.join(", ")}`];
|
|
101
|
+
if (enums.length)
|
|
102
|
+
parts.push(`${enums.length} enums: ${enumNames.join(", ")}`);
|
|
103
|
+
if (hasOrgId)
|
|
104
|
+
parts.push("Multi-tenant: organizationId on entities");
|
|
105
|
+
if (hasSoftDelete)
|
|
106
|
+
parts.push("Soft deletes: deletedAt field");
|
|
107
|
+
if (hasTimestamps)
|
|
108
|
+
parts.push("Auto timestamps: createdAt/updatedAt");
|
|
109
|
+
if (hasRelations)
|
|
110
|
+
parts.push(`${hasRelations} relations defined`);
|
|
111
|
+
return [makeEntry("Database Schema Overview", parts.join("\n"), "database", source, ["prisma", "schema", "database", ...modelNames.slice(0, 15)])];
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function extractFromDockerfile(filePath, projectName) {
|
|
118
|
+
try {
|
|
119
|
+
const content = readFileSync(filePath, "utf-8");
|
|
120
|
+
const source = `project:${projectName}`;
|
|
121
|
+
const parts = [];
|
|
122
|
+
const baseImages = content.match(/^FROM\s+\S+/gm) ?? [];
|
|
123
|
+
if (baseImages.length)
|
|
124
|
+
parts.push(`Base images: ${baseImages.map(b => b.replace("FROM ", "")).join(" → ")}`);
|
|
125
|
+
if (baseImages.length > 1)
|
|
126
|
+
parts.push("Multi-stage build");
|
|
127
|
+
if (content.includes("HEALTHCHECK"))
|
|
128
|
+
parts.push("Has healthcheck");
|
|
129
|
+
if (content.includes("USER") && !content.includes("USER root"))
|
|
130
|
+
parts.push("Non-root user");
|
|
131
|
+
if (content.includes("tini"))
|
|
132
|
+
parts.push("Uses tini init");
|
|
133
|
+
const ports = content.match(/EXPOSE\s+(\d+)/g);
|
|
134
|
+
if (ports)
|
|
135
|
+
parts.push(`Ports: ${ports.map(p => p.replace("EXPOSE ", "")).join(", ")}`);
|
|
136
|
+
return parts.length > 0 ? [makeEntry("Docker Configuration", parts.join("\n"), "deployment", source, ["docker", "container"])] : [];
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function extractFromCIConfig(filePath, projectName) {
|
|
143
|
+
try {
|
|
144
|
+
const content = readFileSync(filePath, "utf-8");
|
|
145
|
+
const source = `project:${projectName}`;
|
|
146
|
+
const parts = [];
|
|
147
|
+
if (filePath.includes(".github"))
|
|
148
|
+
parts.push("CI: GitHub Actions");
|
|
149
|
+
else if (filePath.includes(".gitlab"))
|
|
150
|
+
parts.push("CI: GitLab CI");
|
|
151
|
+
else if (filePath.includes("circle"))
|
|
152
|
+
parts.push("CI: CircleCI");
|
|
153
|
+
const jobs = content.match(/^\s{2}\w[\w-]*:/gm);
|
|
154
|
+
if (jobs)
|
|
155
|
+
parts.push(`Jobs: ${jobs.map(j => j.trim().replace(":", "")).join(", ")}`);
|
|
156
|
+
if (content.includes("tsc"))
|
|
157
|
+
parts.push("Type checking step");
|
|
158
|
+
if (content.includes("test"))
|
|
159
|
+
parts.push("Test step");
|
|
160
|
+
if (content.includes("docker"))
|
|
161
|
+
parts.push("Docker build step");
|
|
162
|
+
if (content.includes("audit"))
|
|
163
|
+
parts.push("Security audit step");
|
|
164
|
+
const triggers = content.match(/on:\s*\n([\s\S]*?)(?=\n\w)/);
|
|
165
|
+
if (triggers) {
|
|
166
|
+
if (content.includes("push:"))
|
|
167
|
+
parts.push("Triggers on push");
|
|
168
|
+
if (content.includes("pull_request:"))
|
|
169
|
+
parts.push("Triggers on PR");
|
|
170
|
+
}
|
|
171
|
+
return parts.length > 0 ? [makeEntry("CI/CD Pipeline", parts.join("\n"), "deployment", source, ["ci", "pipeline"])] : [];
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function extractFromRoutes(routeDir, projectName) {
|
|
178
|
+
try {
|
|
179
|
+
const source = `project:${projectName}`;
|
|
180
|
+
const files = readdirSync(routeDir).filter(f => f.endsWith(".ts") || f.endsWith(".js"));
|
|
181
|
+
const routeNames = files.map(f => f.replace(/\.(ts|js)$/, ""));
|
|
182
|
+
return [makeEntry("API Routes", `${files.length} route files: ${routeNames.join(", ")}`, "api", source, ["routes", "api", ...routeNames.slice(0, 15)])];
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function extractFromStkConfig(filePath, projectName) {
|
|
189
|
+
try {
|
|
190
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
191
|
+
const config = JSON.parse(raw);
|
|
192
|
+
const source = `project:${projectName}`;
|
|
193
|
+
const services = Object.entries(config.services ?? {})
|
|
194
|
+
.filter(([, v]) => v === true || (typeof v === "object" && v.enabled !== false))
|
|
195
|
+
.map(([k]) => k);
|
|
196
|
+
return services.length > 0
|
|
197
|
+
? [makeEntry("Infrastructure Services", `Configured services: ${services.join(", ")}`, "architecture", source, ["infrastructure", ...services])]
|
|
198
|
+
: [];
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function ingestProject(projectPath) {
|
|
205
|
+
const config = loadConfig();
|
|
206
|
+
const projectName = config.name ?? basename(projectPath);
|
|
207
|
+
const entries = [];
|
|
208
|
+
const filesScanned = [];
|
|
209
|
+
const fileExtractors = [
|
|
210
|
+
{ path: "CLAUDE.md", extractor: extractFromClaudeMd },
|
|
211
|
+
{ path: "package.json", extractor: extractFromPackageJson },
|
|
212
|
+
{ path: "node-backend/package.json", extractor: extractFromPackageJson },
|
|
213
|
+
{ path: "frontend/package.json", extractor: extractFromPackageJson },
|
|
214
|
+
{ path: "node-backend/prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
215
|
+
{ path: "Dockerfile", extractor: extractFromDockerfile },
|
|
216
|
+
{ path: ".github/workflows/ci.yml", extractor: extractFromCIConfig },
|
|
217
|
+
{ path: "stk.config.json", extractor: extractFromStkConfig },
|
|
218
|
+
];
|
|
219
|
+
const altPaths = [
|
|
220
|
+
{ path: "prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
221
|
+
{ path: "src/prisma/schema.prisma", extractor: extractFromPrismaSchema },
|
|
222
|
+
{ path: ".github/workflows/main.yml", extractor: extractFromCIConfig },
|
|
223
|
+
{ path: ".github/workflows/deploy.yml", extractor: extractFromCIConfig },
|
|
224
|
+
{ path: ".gitlab-ci.yml", extractor: extractFromCIConfig },
|
|
225
|
+
{ path: "docker-compose.yml", extractor: extractFromDockerfile },
|
|
226
|
+
{ path: "backend/package.json", extractor: extractFromPackageJson },
|
|
227
|
+
{ path: "server/package.json", extractor: extractFromPackageJson },
|
|
228
|
+
{ path: "api/package.json", extractor: extractFromPackageJson },
|
|
229
|
+
];
|
|
230
|
+
const allPaths = [...fileExtractors, ...altPaths];
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
for (const { path, extractor } of allPaths) {
|
|
233
|
+
const fullPath = resolve(projectPath, path);
|
|
234
|
+
if (seen.has(fullPath) || !existsSync(fullPath))
|
|
235
|
+
continue;
|
|
236
|
+
seen.add(fullPath);
|
|
237
|
+
const extracted = extractor(fullPath, projectName);
|
|
238
|
+
if (extracted.length > 0) {
|
|
239
|
+
entries.push(...extracted);
|
|
240
|
+
filesScanned.push(path);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const routeDirs = ["node-backend/src/routes", "src/routes", "backend/src/routes", "server/src/routes", "api/src/routes"];
|
|
244
|
+
for (const dir of routeDirs) {
|
|
245
|
+
const fullDir = resolve(projectPath, dir);
|
|
246
|
+
if (existsSync(fullDir)) {
|
|
247
|
+
entries.push(...extractFromRoutes(fullDir, projectName));
|
|
248
|
+
filesScanned.push(dir);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { projectName, entries, filesScanned };
|
|
253
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { KnowledgeEntry } from "./brain-store.js";
|
|
2
|
+
export interface ScoredEntry {
|
|
3
|
+
entry: KnowledgeEntry;
|
|
4
|
+
score: number;
|
|
5
|
+
matchedTerms: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Search brain with relevance scoring — returns entries ranked by how many terms match */
|
|
8
|
+
export declare function smartSearch(terms: string[], category?: string): ScoredEntry[];
|
|
9
|
+
/** Extract relevant terms from a task description */
|
|
10
|
+
export declare function extractTerms(description: string): string[];
|
|
11
|
+
/** Proactive check — find gotchas relevant to a task before coding */
|
|
12
|
+
export declare function brainCheck(taskDescription: string): ScoredEntry[];
|
|
13
|
+
/** Diagnose an error — find matching patterns from past issues */
|
|
14
|
+
export declare function brainDiagnose(error: string): ScoredEntry[];
|
|
15
|
+
/** Seed brain with curated patterns (idempotent) */
|
|
16
|
+
export declare function seedBrain(patterns: KnowledgeEntry[], force?: boolean): {
|
|
17
|
+
added: number;
|
|
18
|
+
skipped: number;
|
|
19
|
+
};
|
|
20
|
+
/** Review a diff against brain knowledge — find gotchas per file */
|
|
21
|
+
export interface ReviewWarning {
|
|
22
|
+
file: string;
|
|
23
|
+
linesChanged: number;
|
|
24
|
+
warnings: Array<{
|
|
25
|
+
title: string;
|
|
26
|
+
content: string;
|
|
27
|
+
relevance: number;
|
|
28
|
+
source: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function reviewDiff(diff: string): ReviewWarning[];
|
|
32
|
+
/** Get contributor name from git config */
|
|
33
|
+
export declare function getContributor(): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { loadBrainStore, getAllEntries, saveBrainStore } from "./brain-store.js";
|
|
3
|
+
/** Search brain with relevance scoring — returns entries ranked by how many terms match */
|
|
4
|
+
export function smartSearch(terms, category) {
|
|
5
|
+
const store = loadBrainStore();
|
|
6
|
+
let entries = getAllEntries(store);
|
|
7
|
+
if (category) {
|
|
8
|
+
entries = entries.filter(e => e.category === category);
|
|
9
|
+
}
|
|
10
|
+
const normalizedTerms = terms.map(t => t.toLowerCase());
|
|
11
|
+
const scored = [];
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const searchText = `${entry.title} ${entry.content} ${entry.tags.join(" ")}`.toLowerCase();
|
|
14
|
+
const matchedTerms = [];
|
|
15
|
+
let score = 0;
|
|
16
|
+
for (const term of normalizedTerms) {
|
|
17
|
+
if (searchText.includes(term)) {
|
|
18
|
+
matchedTerms.push(term);
|
|
19
|
+
if (entry.title.toLowerCase().includes(term))
|
|
20
|
+
score += 3;
|
|
21
|
+
else if (entry.tags.some(t => t.toLowerCase().includes(term)))
|
|
22
|
+
score += 2;
|
|
23
|
+
else
|
|
24
|
+
score += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (entry.tags.some(t => ["gotcha", "debugging", "bug", "fix", "issue"].includes(t))) {
|
|
28
|
+
score *= 1.5;
|
|
29
|
+
}
|
|
30
|
+
if (score > 0) {
|
|
31
|
+
scored.push({ entry, score, matchedTerms });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
35
|
+
}
|
|
36
|
+
/** Extract relevant terms from a task description */
|
|
37
|
+
export function extractTerms(description) {
|
|
38
|
+
const stopWords = new Set([
|
|
39
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
40
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
41
|
+
"should", "may", "might", "shall", "can", "need", "must", "to", "of",
|
|
42
|
+
"in", "for", "on", "with", "at", "by", "from", "as", "into", "about",
|
|
43
|
+
"like", "through", "after", "before", "between", "under", "above",
|
|
44
|
+
"and", "but", "or", "not", "no", "so", "if", "then", "than", "too",
|
|
45
|
+
"very", "just", "also", "how", "what", "when", "where", "why", "which",
|
|
46
|
+
"that", "this", "these", "those", "it", "its", "i", "we", "you", "they",
|
|
47
|
+
"me", "us", "my", "our", "your", "add", "implement", "create", "build",
|
|
48
|
+
"make", "want", "get", "set", "use", "new", "update", "change",
|
|
49
|
+
]);
|
|
50
|
+
const words = description.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
54
|
+
const phrases = [];
|
|
55
|
+
const desc = description.toLowerCase();
|
|
56
|
+
const commonPhrases = [
|
|
57
|
+
"email verification", "password reset", "auth state", "user select",
|
|
58
|
+
"prisma select", "react context", "protected route", "refresh token",
|
|
59
|
+
"rate limit", "soft delete", "multi-tenant", "org scoping",
|
|
60
|
+
"file upload", "webhook", "cron job", "background job",
|
|
61
|
+
"api key", "role based", "permission", "middleware",
|
|
62
|
+
];
|
|
63
|
+
for (const phrase of commonPhrases) {
|
|
64
|
+
if (desc.includes(phrase))
|
|
65
|
+
phrases.push(phrase);
|
|
66
|
+
}
|
|
67
|
+
return [...new Set([...words, ...phrases])];
|
|
68
|
+
}
|
|
69
|
+
/** Proactive check — find gotchas relevant to a task before coding */
|
|
70
|
+
export function brainCheck(taskDescription) {
|
|
71
|
+
const terms = extractTerms(taskDescription);
|
|
72
|
+
return smartSearch(terms);
|
|
73
|
+
}
|
|
74
|
+
/** Diagnose an error — find matching patterns from past issues */
|
|
75
|
+
export function brainDiagnose(error) {
|
|
76
|
+
const terms = extractTerms(error);
|
|
77
|
+
const errorTerms = error.toLowerCase()
|
|
78
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
79
|
+
.split(/\s+/)
|
|
80
|
+
.filter(w => w.length > 3);
|
|
81
|
+
const allTerms = [...new Set([...terms, ...errorTerms])];
|
|
82
|
+
return smartSearch(allTerms);
|
|
83
|
+
}
|
|
84
|
+
/** Seed brain with curated patterns (idempotent) */
|
|
85
|
+
export function seedBrain(patterns, force = false) {
|
|
86
|
+
const store = loadBrainStore();
|
|
87
|
+
if (force) {
|
|
88
|
+
store.global = store.global.filter(e => !e.source.startsWith("seed:"));
|
|
89
|
+
}
|
|
90
|
+
const existingIds = new Set(getAllEntries(store).map(e => e.id));
|
|
91
|
+
let added = 0;
|
|
92
|
+
let skipped = 0;
|
|
93
|
+
for (const entry of patterns) {
|
|
94
|
+
if (existingIds.has(entry.id)) {
|
|
95
|
+
skipped++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
store.global.push(entry);
|
|
99
|
+
added++;
|
|
100
|
+
}
|
|
101
|
+
if (added > 0)
|
|
102
|
+
saveBrainStore(store);
|
|
103
|
+
return { added, skipped };
|
|
104
|
+
}
|
|
105
|
+
export function reviewDiff(diff) {
|
|
106
|
+
const results = [];
|
|
107
|
+
const fileChunks = diff.split(/^diff --git /m).slice(1);
|
|
108
|
+
for (const chunk of fileChunks) {
|
|
109
|
+
const pathMatch = chunk.match(/b\/(.+?)[\s\n]/);
|
|
110
|
+
if (!pathMatch)
|
|
111
|
+
continue;
|
|
112
|
+
const filePath = pathMatch[1];
|
|
113
|
+
const addedLines = (chunk.match(/^\+[^+]/gm) ?? []).length;
|
|
114
|
+
const removedLines = (chunk.match(/^-[^-]/gm) ?? []).length;
|
|
115
|
+
const linesChanged = addedLines + removedLines;
|
|
116
|
+
const addedContent = (chunk.match(/^\+(.+)$/gm) ?? []).map(l => l.slice(1)).join(" ");
|
|
117
|
+
const searchText = `${filePath} ${addedContent}`;
|
|
118
|
+
const terms = extractTerms(searchText);
|
|
119
|
+
if (filePath.endsWith(".prisma"))
|
|
120
|
+
terms.push("prisma", "schema", "database");
|
|
121
|
+
if (filePath.includes("auth"))
|
|
122
|
+
terms.push("auth", "authentication");
|
|
123
|
+
if (filePath.includes("route"))
|
|
124
|
+
terms.push("route", "api", "endpoint");
|
|
125
|
+
if (filePath.includes("middleware"))
|
|
126
|
+
terms.push("middleware");
|
|
127
|
+
if (filePath.includes("docker") || filePath.includes("Dockerfile"))
|
|
128
|
+
terms.push("docker", "container");
|
|
129
|
+
const matches = smartSearch([...new Set(terms)]);
|
|
130
|
+
if (matches.length > 0) {
|
|
131
|
+
results.push({
|
|
132
|
+
file: filePath,
|
|
133
|
+
linesChanged,
|
|
134
|
+
warnings: matches.slice(0, 3).map(m => ({
|
|
135
|
+
title: m.entry.title,
|
|
136
|
+
content: m.entry.content.slice(0, 300),
|
|
137
|
+
relevance: m.score,
|
|
138
|
+
source: m.entry.source,
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
/** Get contributor name from git config */
|
|
146
|
+
export function getContributor() {
|
|
147
|
+
try {
|
|
148
|
+
return execSync("git config user.name", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return "unknown";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface KnowledgeEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
content: string;
|
|
5
|
+
category: string;
|
|
6
|
+
source: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
created_at: string;
|
|
9
|
+
contributor?: string;
|
|
10
|
+
updated_at?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectBrain {
|
|
13
|
+
ingestedAt: string;
|
|
14
|
+
projectPath: string;
|
|
15
|
+
entries: KnowledgeEntry[];
|
|
16
|
+
}
|
|
17
|
+
export interface BrainStore {
|
|
18
|
+
version: 1;
|
|
19
|
+
projects: Record<string, ProjectBrain>;
|
|
20
|
+
global: KnowledgeEntry[];
|
|
21
|
+
}
|
|
22
|
+
export declare function loadBrainStore(): BrainStore;
|
|
23
|
+
export declare function saveBrainStore(store: BrainStore): void;
|
|
24
|
+
/** Get all entries — optionally scoped to a project */
|
|
25
|
+
export declare function getAllEntries(store: BrainStore, projectName?: string): KnowledgeEntry[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
// ──────────────────────────────────────────
|
|
5
|
+
// Storage
|
|
6
|
+
// ──────────────────────────────────────────
|
|
7
|
+
const STK_DIR = join(homedir(), ".stk");
|
|
8
|
+
const BRAIN_PATH = join(STK_DIR, "brain.json");
|
|
9
|
+
function ensureStkDir() {
|
|
10
|
+
if (!existsSync(STK_DIR))
|
|
11
|
+
mkdirSync(STK_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
export function loadBrainStore() {
|
|
14
|
+
ensureStkDir();
|
|
15
|
+
if (!existsSync(BRAIN_PATH)) {
|
|
16
|
+
return { version: 1, projects: {}, global: [] };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(BRAIN_PATH, "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { version: 1, projects: {}, global: [] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function saveBrainStore(store) {
|
|
27
|
+
ensureStkDir();
|
|
28
|
+
writeFileSync(BRAIN_PATH, JSON.stringify(store, null, 2));
|
|
29
|
+
}
|
|
30
|
+
/** Get all entries — optionally scoped to a project */
|
|
31
|
+
export function getAllEntries(store, projectName) {
|
|
32
|
+
const entries = [...store.global];
|
|
33
|
+
if (projectName && store.projects[projectName]) {
|
|
34
|
+
entries.push(...store.projects[projectName].entries);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
for (const proj of Object.values(store.projects)) {
|
|
38
|
+
entries.push(...proj.entries);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
package/dist/services/brain.d.ts
CHANGED
|
@@ -1,46 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
export declare function loadBrainStore(): BrainStore;
|
|
21
|
-
export declare function saveBrainStore(store: BrainStore): void;
|
|
22
|
-
/** Get all entries — optionally scoped to a project */
|
|
23
|
-
export declare function getAllEntries(store: BrainStore, projectName?: string): KnowledgeEntry[];
|
|
24
|
-
/** Extract knowledge from CLAUDE.md sections */
|
|
25
|
-
export declare function extractFromClaudeMd(filePath: string, projectName: string): KnowledgeEntry[];
|
|
26
|
-
/** Extract knowledge from package.json */
|
|
27
|
-
export declare function extractFromPackageJson(filePath: string, projectName: string): KnowledgeEntry[];
|
|
28
|
-
/** Extract knowledge from Prisma schema */
|
|
29
|
-
export declare function extractFromPrismaSchema(filePath: string, projectName: string): KnowledgeEntry[];
|
|
30
|
-
/** Extract knowledge from Dockerfile */
|
|
31
|
-
export declare function extractFromDockerfile(filePath: string, projectName: string): KnowledgeEntry[];
|
|
32
|
-
/** Extract knowledge from CI config */
|
|
33
|
-
export declare function extractFromCIConfig(filePath: string, projectName: string): KnowledgeEntry[];
|
|
34
|
-
/** Extract knowledge from route files directory */
|
|
35
|
-
export declare function extractFromRoutes(routeDir: string, projectName: string): KnowledgeEntry[];
|
|
36
|
-
/** Extract knowledge from stk.config.json */
|
|
37
|
-
export declare function extractFromStkConfig(filePath: string, projectName: string): KnowledgeEntry[];
|
|
38
|
-
interface IngestResult {
|
|
39
|
-
projectName: string;
|
|
40
|
-
entries: KnowledgeEntry[];
|
|
41
|
-
filesScanned: string[];
|
|
42
|
-
}
|
|
43
|
-
export declare function ingestProject(projectPath: string): IngestResult;
|
|
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
|
+
*/
|
|
10
|
+
export type { KnowledgeEntry, ProjectBrain, BrainStore } from "./brain-store.js";
|
|
11
|
+
export { loadBrainStore, saveBrainStore, getAllEntries } from "./brain-store.js";
|
|
12
|
+
export { extractFromClaudeMd, extractFromPackageJson, extractFromPrismaSchema, extractFromDockerfile, extractFromCIConfig, extractFromRoutes, extractFromStkConfig, ingestProject } from "./brain-extract.js";
|
|
13
|
+
export type { IngestResult } from "./brain-extract.js";
|
|
14
|
+
export type { ScoredEntry } from "./brain-search.js";
|
|
15
|
+
export { smartSearch, extractTerms, brainCheck, brainDiagnose, seedBrain, reviewDiff, getContributor } from "./brain-search.js";
|
|
16
|
+
export type { ReviewWarning } from "./brain-search.js";
|
|
17
|
+
export type { SyncResult } from "./brain-cloud.js";
|
|
18
|
+
export { pushToCloud, pullFromCloud, syncBrain, cloudInsert } from "./brain-cloud.js";
|
|
19
|
+
import type { KnowledgeEntry } from "./brain-store.js";
|
|
44
20
|
export declare function getLocalBrainClient(): {
|
|
45
21
|
query(_table: string, params?: Record<string, string>): Promise<{
|
|
46
22
|
data: KnowledgeEntry[];
|
|
@@ -52,28 +28,3 @@ export declare function getLocalBrainClient(): {
|
|
|
52
28
|
ok: boolean;
|
|
53
29
|
}>;
|
|
54
30
|
};
|
|
55
|
-
export interface SyncResult {
|
|
56
|
-
pushed: number;
|
|
57
|
-
pulled: number;
|
|
58
|
-
errors: string[];
|
|
59
|
-
}
|
|
60
|
-
/** Push all local entries to cloud */
|
|
61
|
-
export declare function pushToCloud(): Promise<SyncResult>;
|
|
62
|
-
/** Pull cloud entries to local */
|
|
63
|
-
export declare function pullFromCloud(): Promise<SyncResult>;
|
|
64
|
-
/** Full sync: push local → cloud, then pull cloud → local */
|
|
65
|
-
export declare function syncBrain(): Promise<SyncResult>;
|
|
66
|
-
interface ScoredEntry {
|
|
67
|
-
entry: KnowledgeEntry;
|
|
68
|
-
score: number;
|
|
69
|
-
matchedTerms: string[];
|
|
70
|
-
}
|
|
71
|
-
/** Search brain with relevance scoring — returns entries ranked by how many terms match */
|
|
72
|
-
export declare function smartSearch(terms: string[], category?: string): ScoredEntry[];
|
|
73
|
-
/** Extract relevant terms from a task description */
|
|
74
|
-
export declare function extractTerms(description: string): string[];
|
|
75
|
-
/** Proactive check — find gotchas relevant to a task before coding */
|
|
76
|
-
export declare function brainCheck(taskDescription: string): ScoredEntry[];
|
|
77
|
-
/** Diagnose an error — find matching patterns from past issues */
|
|
78
|
-
export declare function brainDiagnose(error: string): ScoredEntry[];
|
|
79
|
-
export {};
|