@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.
@@ -1,327 +1,24 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
2
- import { join, resolve, basename } from "path";
3
- import { homedir } from "os";
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
- // Storage
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, filesScanned } = ingestProject(process.cwd());
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.7.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",