@prajwolkc/stk 0.7.1 → 0.9.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.
@@ -0,0 +1,1877 @@
1
+ import { z } from "zod";
2
+ import { loadConfig, enabledServices } from "../../lib/config.js";
3
+ import { getChecker, loadPluginCheckers } from "../../services/registry.js";
4
+ import { brainCheck, brainDiagnose, smartSearch, extractTerms, reviewDiff, getContributor } from "../../services/brain-search.js";
5
+ import { loadBrainStore, saveBrainStore, getAllEntries } from "../../services/brain-store.js";
6
+ import { cloudInsert } from "../../services/brain-cloud.js";
7
+ import { recordMetric, compareToBaseline, getDeployFrequency, getErrorRate, getUptime } from "../../services/metrics.js";
8
+ import { checkSecrets, runAllChecks } from "../../services/security.js";
9
+ import { pushToCloud, pullFromCloud } from "../../services/brain-cloud.js";
10
+ import { ingestProject } from "../../services/brain-extract.js";
11
+ import { execSync } from "child_process";
12
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
13
+ // ──────────────────────────────────────────
14
+ // Helpers
15
+ // ──────────────────────────────────────────
16
+ function detectCategory(filePaths) {
17
+ const joined = filePaths.join(" ").toLowerCase();
18
+ if (/auth|login|session|jwt|oauth/.test(joined))
19
+ return "auth";
20
+ if (/prisma|migration|schema|database|db|mongo|redis/.test(joined))
21
+ return "database";
22
+ if (/route|api|endpoint|controller/.test(joined))
23
+ return "api";
24
+ if (/docker|deploy|ci|cd|railway|vercel|fly|render|\.github/.test(joined))
25
+ return "deployment";
26
+ if (/test|spec|__test__/.test(joined))
27
+ return "testing";
28
+ if (/security|cors|rate|csrf|helmet/.test(joined))
29
+ return "security";
30
+ if (/perf|cache|optim|lazy|bundle/.test(joined))
31
+ return "performance";
32
+ if (/stripe|payment|billing|subscription/.test(joined))
33
+ return "payments";
34
+ return "general";
35
+ }
36
+ function parseChangedFiles(diff) {
37
+ const files = [];
38
+ const matches = diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm);
39
+ for (const m of matches)
40
+ files.push(m[1]);
41
+ return files;
42
+ }
43
+ async function fetchLogs(provider, lines = 30) {
44
+ const logs = [];
45
+ if ((provider === "railway" || !provider) && process.env.RAILWAY_API_TOKEN) {
46
+ try {
47
+ const token = process.env.RAILWAY_API_TOKEN;
48
+ const projectId = process.env.RAILWAY_PROJECT_ID;
49
+ const serviceId = process.env.RAILWAY_SERVICE_ID;
50
+ if (token && projectId && serviceId) {
51
+ const envId = process.env.RAILWAY_ENVIRONMENT_ID;
52
+ const gql = `query { deployments(first:1, input:{projectId:"${projectId}",serviceId:"${serviceId}"${envId ? `,environmentId:"${envId}"` : ""}}) { edges { node { id } } } }`;
53
+ const depRes = await fetch("https://backboard.railway.app/graphql/v2", {
54
+ method: "POST",
55
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
56
+ body: JSON.stringify({ query: gql }),
57
+ signal: AbortSignal.timeout(10000),
58
+ });
59
+ const depData = await depRes.json();
60
+ const depId = depData?.data?.deployments?.edges?.[0]?.node?.id;
61
+ if (depId) {
62
+ const logRes = await fetch("https://backboard.railway.app/graphql/v2", {
63
+ method: "POST",
64
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
65
+ body: JSON.stringify({ query: `query { deploymentLogs(deploymentId:"${depId}",limit:${lines}) { timestamp message severity } }` }),
66
+ signal: AbortSignal.timeout(10000),
67
+ });
68
+ const logData = await logRes.json();
69
+ logs.push(...(logData?.data?.deploymentLogs ?? []));
70
+ }
71
+ }
72
+ }
73
+ catch { /* timeout or network error */ }
74
+ }
75
+ if (logs.length === 0 && (provider === "vercel" || !provider) && process.env.VERCEL_TOKEN) {
76
+ try {
77
+ const token = process.env.VERCEL_TOKEN;
78
+ const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
79
+ headers: { Authorization: `Bearer ${token}` },
80
+ signal: AbortSignal.timeout(10000),
81
+ });
82
+ const depData = await depRes.json();
83
+ const dep = depData.deployments?.[0];
84
+ if (dep) {
85
+ const logRes = await fetch(`https://api.vercel.com/v2/deployments/${dep.uid}/events`, {
86
+ headers: { Authorization: `Bearer ${token}` },
87
+ signal: AbortSignal.timeout(10000),
88
+ });
89
+ const events = await logRes.json();
90
+ if (Array.isArray(events)) {
91
+ for (const e of events.filter((ev) => ev.type === "stdout" || ev.type === "stderr").slice(-lines)) {
92
+ logs.push({
93
+ timestamp: new Date(e.created).toISOString(),
94
+ message: e.payload?.text ?? e.text ?? "",
95
+ severity: e.type === "stderr" ? "ERROR" : "INFO",
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ catch { /* timeout or network error */ }
102
+ }
103
+ return logs;
104
+ }
105
+ export function registerAutomationTools(server) {
106
+ // ──────────────────────────────────────────
107
+ // Tool: stk_workflow (MASTER INSTRUCTION SET)
108
+ // ──────────────────────────────────────────
109
+ server.tool("stk_workflow", `IMPORTANT: READ THIS FIRST. This tool returns the stk automation playbook — the exact sequence of tools to call and WHEN. You MUST call this tool at the start of every conversation where stk is available. It tells you how to use all stk tools automatically without the user having to ask.`, {}, async () => {
110
+ return {
111
+ content: [{
112
+ type: "text",
113
+ text: JSON.stringify({
114
+ instructions: "Follow these rules automatically. Do NOT wait for the user to ask.",
115
+ workflow: {
116
+ "ON_CONVERSATION_START": {
117
+ action: "Call stk_context to understand the project immediately",
118
+ tool: "stk_context",
119
+ when: "First message of any conversation",
120
+ },
121
+ "BEFORE_IMPLEMENTING": {
122
+ action: "Call stk_autopilot with the task description before writing ANY code",
123
+ tool: "stk_autopilot",
124
+ when: "User asks to build, add, implement, create, or fix something",
125
+ why: "Catches gotchas BEFORE you waste time coding the wrong approach",
126
+ },
127
+ "ON_ERROR": {
128
+ action: "Call stk_auto_diagnose with the error message",
129
+ tool: "stk_auto_diagnose",
130
+ when: "An error occurs during development, testing, or in production logs",
131
+ why: "The brain may already have the solution from another project",
132
+ },
133
+ "BEFORE_COMMITTING": {
134
+ action: "Call stk_precommit to scan for secrets and gotchas",
135
+ tool: "stk_precommit",
136
+ when: "About to commit code changes",
137
+ why: "Prevents committing secrets or known anti-patterns",
138
+ },
139
+ "AFTER_FIXING_BUG": {
140
+ action: "Call stk_autolearn to save the pattern for future use",
141
+ tool: "stk_autolearn",
142
+ when: "After successfully fixing a bug or implementing a non-trivial feature",
143
+ why: "Shares the knowledge with all stk users via cloud brain",
144
+ },
145
+ "AFTER_DEPLOYING": {
146
+ action: "Call stk_postdeploy to verify everything is healthy",
147
+ tool: "stk_postdeploy",
148
+ when: "After pushing to production or running stk_smart_deploy",
149
+ why: "Catches post-deploy issues before users report them",
150
+ },
151
+ "ON_SOMETHING_BROKE": {
152
+ action: "Call stk_incident to capture full state and create GitHub issue",
153
+ tool: "stk_incident",
154
+ when: "User reports production is broken, down, or has critical errors",
155
+ why: "Captures the full picture for incident response",
156
+ },
157
+ "ADDING_DEPENDENCY": {
158
+ action: "Call stk_dep_intel to check before adding or updating a package",
159
+ tool: "stk_dep_intel",
160
+ when: "About to add, remove, or update an npm package",
161
+ why: "Prevents version conflicts and known dependency issues",
162
+ },
163
+ "CODE_REVIEW": {
164
+ action: "Call stk_diff_explain to understand and review changes",
165
+ tool: "stk_diff_explain",
166
+ when: "User asks to review a PR, commit, or set of changes",
167
+ why: "Enriches review with brain knowledge and risk detection",
168
+ },
169
+ },
170
+ note: "These tools run instantly and cost nothing — they use local brain + your configured services. Always prefer calling them over manual investigation.",
171
+ }, null, 2),
172
+ }],
173
+ };
174
+ });
175
+ // ──────────────────────────────────────────
176
+ // Tool: stk_autopilot
177
+ // ──────────────────────────────────────────
178
+ server.tool("stk_autopilot", "REQUIRED: ALWAYS call this BEFORE writing any code. Returns brain gotchas, relevant patterns, service health, deploy status, and open issues for your task. Never skip this — it prevents wasted effort by catching problems before you code.", {
179
+ task: z.string().describe("What you're about to work on (e.g., 'add email verification', 'fix payment webhook')"),
180
+ }, async ({ task }) => {
181
+ const config = loadConfig();
182
+ const [brainResult, patternsResult, healthResult, deployResult, issuesResult] = await Promise.allSettled([
183
+ // 1. Brain check for gotchas
184
+ Promise.resolve(brainCheck(task)),
185
+ // 2. Relevant patterns
186
+ Promise.resolve(smartSearch(extractTerms(task))),
187
+ // 3. Health check
188
+ (async () => {
189
+ await loadPluginCheckers();
190
+ const serviceList = enabledServices(config);
191
+ if (serviceList.length === 0)
192
+ return { services: [], summary: { healthy: 0, down: 0, total: 0 } };
193
+ const checks = serviceList.map(async (name) => {
194
+ const checker = getChecker(name);
195
+ if (!checker)
196
+ return { name, status: "skipped", detail: "unknown" };
197
+ return checker();
198
+ });
199
+ const results = await Promise.all(checks);
200
+ return {
201
+ services: results.map(r => ({ name: r.name, status: r.status })),
202
+ summary: {
203
+ healthy: results.filter(r => r.status === "healthy").length,
204
+ down: results.filter(r => r.status === "down").length,
205
+ total: results.length,
206
+ },
207
+ };
208
+ })(),
209
+ // 4. Latest deploy
210
+ (async () => {
211
+ if (!process.env.VERCEL_TOKEN)
212
+ return null;
213
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
214
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
215
+ signal: AbortSignal.timeout(10000),
216
+ });
217
+ const data = await res.json();
218
+ const dep = data.deployments?.[0];
219
+ if (!dep)
220
+ return null;
221
+ return { provider: "vercel", state: dep.readyState ?? dep.state, url: dep.url, created: dep.created };
222
+ })(),
223
+ // 5. Open issues
224
+ (async () => {
225
+ const token = process.env.GITHUB_TOKEN;
226
+ if (!token)
227
+ return [];
228
+ let repo = process.env.GITHUB_REPO ?? "";
229
+ if (!repo) {
230
+ try {
231
+ repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
232
+ }
233
+ catch {
234
+ return [];
235
+ }
236
+ }
237
+ const res = await fetch(`https://api.github.com/repos/${repo}/issues?state=open&per_page=5`, {
238
+ headers: { Authorization: `Bearer ${token}` },
239
+ signal: AbortSignal.timeout(10000),
240
+ });
241
+ if (!res.ok)
242
+ return [];
243
+ const issues = await res.json();
244
+ return issues.map(i => ({ number: i.number, title: i.title, labels: (i.labels ?? []).map((l) => l.name) }));
245
+ })(),
246
+ ]);
247
+ const warnings = brainResult.status === "fulfilled" ? brainResult.value.slice(0, 5).map(r => ({
248
+ title: r.entry.title, warning: r.entry.content, relevance: r.score, category: r.entry.category,
249
+ })) : [];
250
+ const patterns = patternsResult.status === "fulfilled" ? patternsResult.value.slice(0, 5).map(r => ({
251
+ title: r.entry.title, content: r.entry.content.slice(0, 200), source: r.entry.source,
252
+ })) : [];
253
+ const health = healthResult.status === "fulfilled" ? healthResult.value : null;
254
+ const lastDeploy = deployResult.status === "fulfilled" ? deployResult.value : null;
255
+ const openIssues = issuesResult.status === "fulfilled" ? issuesResult.value : [];
256
+ const hasDownServices = health?.summary?.down ?? 0 > 0;
257
+ const hasDeployError = lastDeploy?.state === "ERROR";
258
+ const readyToCode = !hasDownServices && !hasDeployError;
259
+ return {
260
+ content: [{
261
+ type: "text",
262
+ text: JSON.stringify({
263
+ task,
264
+ brainWarnings: warnings,
265
+ relevantPatterns: patterns,
266
+ health,
267
+ lastDeploy,
268
+ openIssues,
269
+ readyToCode,
270
+ summary: `${warnings.length} warnings, ${patterns.length} patterns, ${health?.summary?.down ?? 0} services down, ${openIssues.length} open issues`,
271
+ }, null, 2),
272
+ }],
273
+ };
274
+ });
275
+ // ──────────────────────────────────────────
276
+ // Tool: stk_autolearn
277
+ // ──────────────────────────────────────────
278
+ server.tool("stk_autolearn", "ALWAYS call this after fixing a bug or implementing a feature. Extracts the pattern from git diff and saves to brain + cloud so all stk users learn from it. Never skip — this is how the collective brain grows.", {
279
+ description: z.string().describe("What was fixed or built (e.g., 'Fixed N+1 query in user endpoint')"),
280
+ category: z.string().optional().describe("Category (auto-detected from files if omitted)"),
281
+ tags: z.array(z.string()).optional().describe("Tags for searchability"),
282
+ }, async ({ description, category, tags }) => {
283
+ // Get diff
284
+ let diff = "";
285
+ let commitMsg = "";
286
+ try {
287
+ diff = execSync("git diff HEAD~1", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
288
+ }
289
+ catch { /* no commits */ }
290
+ try {
291
+ commitMsg = execSync('git log -1 --format="%s"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
292
+ }
293
+ catch { /* */ }
294
+ if (!diff) {
295
+ try {
296
+ diff = execSync("git diff", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
297
+ }
298
+ catch { /* */ }
299
+ }
300
+ const changedFiles = parseChangedFiles(diff);
301
+ const autoCategory = category ?? detectCategory(changedFiles);
302
+ const contributor = getContributor();
303
+ // Build entry
304
+ const id = `learn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
305
+ const filesNote = changedFiles.length > 0 ? `\nFiles: ${changedFiles.slice(0, 10).join(", ")}` : "";
306
+ const commitNote = commitMsg ? `\nCommit: ${commitMsg}` : "";
307
+ const entry = {
308
+ id,
309
+ title: description,
310
+ content: `${description}${filesNote}${commitNote}`.slice(0, 2000),
311
+ category: autoCategory,
312
+ source: `project:${loadConfig().name ?? "unknown"}`,
313
+ tags: tags ?? [...new Set(changedFiles.map(f => f.split("/").pop()?.split(".")[0] ?? "").filter(Boolean).slice(0, 5))],
314
+ created_at: new Date().toISOString(),
315
+ };
316
+ // Save locally
317
+ const store = loadBrainStore();
318
+ store.global.push(entry);
319
+ saveBrainStore(store);
320
+ // Push to cloud
321
+ const cloudOk = await cloudInsert(entry).catch(() => false);
322
+ return {
323
+ content: [{
324
+ type: "text",
325
+ text: JSON.stringify({
326
+ learned: true,
327
+ entry: { id: entry.id, title: entry.title, category: entry.category, tags: entry.tags },
328
+ contributor,
329
+ filesChanged: changedFiles.length,
330
+ savedToCloud: cloudOk,
331
+ message: `Pattern saved. ${cloudOk ? "Shared with all stk users via cloud." : "Saved locally (cloud sync failed)."}`,
332
+ }, null, 2),
333
+ }],
334
+ };
335
+ });
336
+ // ──────────────────────────────────────────
337
+ // Tool: stk_auto_diagnose
338
+ // ──────────────────────────────────────────
339
+ server.tool("stk_auto_diagnose", "ALWAYS call this when ANY error occurs — during dev, testing, or production. Searches brain for solutions, checks logs, health, and metrics. Call this BEFORE debugging manually — the answer may already exist in the brain.", {
340
+ error: z.string().describe("The error message or description of the problem"),
341
+ provider: z.enum(["railway", "vercel"]).optional().describe("Log source (auto-detects if omitted)"),
342
+ }, async ({ error, provider }) => {
343
+ const config = loadConfig();
344
+ const [brainResult, logsResult, healthResult, metricsResult] = await Promise.allSettled([
345
+ // 1. Brain diagnosis
346
+ Promise.resolve(brainDiagnose(error)),
347
+ // 2. Recent logs with error filtering
348
+ (async () => {
349
+ const logs = await fetchLogs(provider, 50);
350
+ const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|ReferenceError|rejected|FATAL/i;
351
+ return logs.filter(l => l.severity === "ERROR" || errorPattern.test(l.message)).slice(-10);
352
+ })(),
353
+ // 3. Health checks
354
+ (async () => {
355
+ await loadPluginCheckers();
356
+ const serviceList = enabledServices(config);
357
+ if (serviceList.length === 0)
358
+ return [];
359
+ const checks = serviceList.map(async (name) => {
360
+ const checker = getChecker(name);
361
+ if (!checker)
362
+ return null;
363
+ return checker();
364
+ });
365
+ const results = (await Promise.all(checks)).filter(Boolean);
366
+ return results.filter(r => r.status === "down" || r.status === "degraded");
367
+ })(),
368
+ // 4. Metric anomalies
369
+ Promise.resolve(compareToBaseline("error")),
370
+ ]);
371
+ const brainMatches = brainResult.status === "fulfilled"
372
+ ? brainResult.value.slice(0, 5).map(r => ({ title: r.entry.title, solution: r.entry.content, relevance: r.score, source: r.entry.source }))
373
+ : [];
374
+ const relatedLogErrors = logsResult.status === "fulfilled" ? logsResult.value : [];
375
+ const downServices = healthResult.status === "fulfilled" ? healthResult.value : [];
376
+ const errorTrend = metricsResult.status === "fulfilled" ? metricsResult.value : null;
377
+ // Build suggested actions
378
+ const suggestedActions = [];
379
+ if (brainMatches.length > 0)
380
+ suggestedActions.push(`Apply solution from: "${brainMatches[0].title}"`);
381
+ for (const svc of downServices)
382
+ suggestedActions.push(`Service "${svc.name}" is ${svc.status} — may be root cause`);
383
+ if (errorTrend && errorTrend.status === "degraded")
384
+ suggestedActions.push(`Error rate increased by ${errorTrend.changePct}% — check recent deploys`);
385
+ if (brainMatches.length === 0 && downServices.length === 0)
386
+ suggestedActions.push("New issue — debug it, fix it, then use stk_autolearn to save the solution");
387
+ return {
388
+ content: [{
389
+ type: "text",
390
+ text: JSON.stringify({
391
+ error,
392
+ brainMatches,
393
+ relatedLogErrors,
394
+ downServices: downServices.map(s => ({ name: s.name, status: s.status, detail: s.detail })),
395
+ errorTrend,
396
+ suggestedActions,
397
+ diagnosis: `Found ${brainMatches.length} brain matches, ${downServices.length} services down, ${relatedLogErrors.length} related log errors`,
398
+ }, null, 2),
399
+ }],
400
+ };
401
+ });
402
+ // ──────────────────────────────────────────
403
+ // Tool: stk_postdeploy
404
+ // ──────────────────────────────────────────
405
+ server.tool("stk_postdeploy", "ALWAYS call this after ANY deploy to production. Runs health checks, records metrics, checks for error spikes. Never skip post-deploy verification.", {
406
+ provider: z.enum(["railway", "vercel"]).optional().describe("Deploy provider (auto-detects if omitted)"),
407
+ }, async ({ provider }) => {
408
+ const config = loadConfig();
409
+ const detected = provider ?? (enabledServices(config).includes("railway") ? "railway" : enabledServices(config).includes("vercel") ? "vercel" : undefined);
410
+ const [healthResult, logsResult, deployResult] = await Promise.allSettled([
411
+ // 1. Health checks
412
+ (async () => {
413
+ await loadPluginCheckers();
414
+ const serviceList = enabledServices(config);
415
+ const checks = serviceList.map(async (name) => {
416
+ const checker = getChecker(name);
417
+ if (!checker)
418
+ return { name, status: "skipped", detail: "unknown" };
419
+ return checker();
420
+ });
421
+ return Promise.all(checks);
422
+ })(),
423
+ // 2. Recent logs — check for errors
424
+ (async () => {
425
+ const logs = await fetchLogs(detected, 30);
426
+ const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|FATAL/i;
427
+ return logs.filter(l => l.severity === "ERROR" || errorPattern.test(l.message));
428
+ })(),
429
+ // 3. Deploy status
430
+ (async () => {
431
+ if (!process.env.VERCEL_TOKEN)
432
+ return null;
433
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
434
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
435
+ signal: AbortSignal.timeout(10000),
436
+ });
437
+ const data = await res.json();
438
+ const dep = data.deployments?.[0];
439
+ if (!dep)
440
+ return null;
441
+ return { provider: "vercel", state: dep.readyState ?? dep.state, url: dep.url };
442
+ })(),
443
+ ]);
444
+ const healthResults = healthResult.status === "fulfilled" ? healthResult.value : [];
445
+ const errorLogs = logsResult.status === "fulfilled" ? logsResult.value : [];
446
+ const deploy = deployResult.status === "fulfilled" ? deployResult.value : null;
447
+ // Record metrics
448
+ recordMetric("deploy", 1, { provider: detected ?? "unknown" });
449
+ const healthyCount = healthResults.filter(r => r.status === "healthy").length;
450
+ recordMetric("health_check", healthyCount > 0 ? healthyCount : 0);
451
+ const errorTrend = compareToBaseline("error");
452
+ // Determine status
453
+ const anyDown = healthResults.some(r => r.status === "down");
454
+ const deployError = deploy?.state === "ERROR";
455
+ const errorSpike = errorLogs.length > 5;
456
+ let status = "green";
457
+ const issues = [];
458
+ if (anyDown || deployError) {
459
+ status = "red";
460
+ if (anyDown)
461
+ issues.push(`Services down: ${healthResults.filter(r => r.status === "down").map(r => r.name).join(", ")}`);
462
+ if (deployError)
463
+ issues.push("Deploy in ERROR state");
464
+ }
465
+ else if (errorSpike || errorTrend.status === "degraded" || healthResults.some(r => r.status === "degraded")) {
466
+ status = "yellow";
467
+ if (errorSpike)
468
+ issues.push(`${errorLogs.length} errors in recent logs`);
469
+ if (errorTrend.status === "degraded")
470
+ issues.push(`Error rate up ${errorTrend.changePct}%`);
471
+ }
472
+ if (issues.length === 0)
473
+ issues.push("All clear");
474
+ return {
475
+ content: [{
476
+ type: "text",
477
+ text: JSON.stringify({
478
+ deploy,
479
+ health: {
480
+ services: healthResults.map(r => ({ name: r.name, status: r.status })),
481
+ summary: {
482
+ healthy: healthyCount,
483
+ down: healthResults.filter(r => r.status === "down").length,
484
+ total: healthResults.length,
485
+ },
486
+ },
487
+ metrics: { deployRecorded: true, errorTrend },
488
+ logErrors: { count: errorLogs.length, recentErrors: errorLogs.slice(0, 5) },
489
+ status,
490
+ issues,
491
+ }, null, 2),
492
+ }],
493
+ };
494
+ });
495
+ // ──────────────────────────────────────────
496
+ // Tool: stk_precommit
497
+ // ──────────────────────────────────────────
498
+ server.tool("stk_precommit", "ALWAYS call this BEFORE committing code. Scans for exposed secrets, reviews against brain knowledge for gotchas. If this reports issues, fix them before committing. Never commit without checking.", {
499
+ diff: z.string().optional().describe("Raw git diff (auto-detects staged changes if omitted)"),
500
+ }, async ({ diff }) => {
501
+ // Get diff
502
+ let diffContent = diff ?? "";
503
+ if (!diffContent) {
504
+ try {
505
+ diffContent = execSync("git diff --staged", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
506
+ }
507
+ catch { /* */ }
508
+ }
509
+ if (!diffContent) {
510
+ try {
511
+ diffContent = execSync("git diff", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
512
+ }
513
+ catch { /* */ }
514
+ }
515
+ if (!diffContent) {
516
+ return {
517
+ content: [{
518
+ type: "text",
519
+ text: JSON.stringify({ filesChanged: [], safeToCommit: true, summary: "No changes to check" }),
520
+ }],
521
+ };
522
+ }
523
+ const [secretsResult, reviewResult, diffSecretsResult] = await Promise.allSettled([
524
+ // 1. Check for exposed secrets in .env files
525
+ Promise.resolve(checkSecrets()),
526
+ // 2. Brain review of diff
527
+ Promise.resolve(reviewDiff(diffContent)),
528
+ // 3. Scan diff content for inline secrets
529
+ Promise.resolve((() => {
530
+ const findings = [];
531
+ const secretPatterns = [
532
+ { pattern: /sk_live_[a-zA-Z0-9]+/, reason: "Stripe live secret key" },
533
+ { pattern: /AKIA[A-Z0-9]{16}/, reason: "AWS access key" },
534
+ { pattern: /-----BEGIN (RSA |EC )?PRIVATE KEY-----/, reason: "Private key" },
535
+ { pattern: /ghp_[a-zA-Z0-9]{36}/, reason: "GitHub personal access token" },
536
+ { pattern: /eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}/, reason: "JWT token (possible hardcoded secret)" },
537
+ ];
538
+ const addedLines = diffContent.match(/^\+[^+](.*)$/gm) ?? [];
539
+ for (const line of addedLines) {
540
+ for (const { pattern, reason } of secretPatterns) {
541
+ if (pattern.test(line)) {
542
+ findings.push({ line: line.slice(0, 100), reason });
543
+ }
544
+ }
545
+ }
546
+ return findings;
547
+ })()),
548
+ ]);
549
+ const secretsFindings = secretsResult.status === "fulfilled" ? secretsResult.value : [];
550
+ const brainReview = reviewResult.status === "fulfilled" ? reviewResult.value : [];
551
+ const inlineSecrets = diffSecretsResult.status === "fulfilled" ? diffSecretsResult.value : [];
552
+ const filesChanged = parseChangedFiles(diffContent);
553
+ const filesWithWarnings = brainReview.filter(r => r.warnings.length > 0);
554
+ const criticalSecrets = secretsFindings.filter(f => f.level === "critical");
555
+ const safeToCommit = criticalSecrets.length === 0 && inlineSecrets.length === 0;
556
+ return {
557
+ content: [{
558
+ type: "text",
559
+ text: JSON.stringify({
560
+ filesChanged,
561
+ securityFindings: secretsFindings,
562
+ inlineSecrets,
563
+ brainReview: {
564
+ filesReviewed: brainReview.length,
565
+ filesWithWarnings: filesWithWarnings.length,
566
+ warnings: filesWithWarnings.slice(0, 10),
567
+ },
568
+ safeToCommit,
569
+ summary: `${filesChanged.length} files changed, ${criticalSecrets.length + inlineSecrets.length} security issues, ${filesWithWarnings.reduce((s, r) => s + r.warnings.length, 0)} brain warnings`,
570
+ }, null, 2),
571
+ }],
572
+ };
573
+ });
574
+ // ══════════════════════════════════════════
575
+ // SUPERPOWER TOOLS
576
+ // ══════════════════════════════════════════
577
+ // ──────────────────────────────────────────
578
+ // Tool: stk_mission_control
579
+ // ──────────────────────────────────────────
580
+ server.tool("stk_mission_control", "GOD MODE. Complete stack intelligence in one call: git state, all service health with latency, deploy history, error trends, security posture, brain stats, open issues, metrics overview, and uptime. The ultimate context dump for understanding your entire infrastructure.", {}, async () => {
581
+ const config = loadConfig();
582
+ const [gitResult, healthResult, deployResult, issuesResult, metricsResult, securityResult, brainResult] = await Promise.allSettled([
583
+ // 1. Git state
584
+ Promise.resolve((() => {
585
+ try {
586
+ return {
587
+ branch: execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
588
+ dirty: execSync("git status --porcelain", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean).length,
589
+ lastCommit: execSync('git log -1 --format="%s"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
590
+ lastCommitAge: execSync('git log -1 --format="%cr"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
591
+ totalCommits: parseInt(execSync("git rev-list --count HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(), 10),
592
+ };
593
+ }
594
+ catch {
595
+ return null;
596
+ }
597
+ })()),
598
+ // 2. Full health with latency
599
+ (async () => {
600
+ await loadPluginCheckers();
601
+ const serviceList = enabledServices(config);
602
+ const checks = serviceList.map(async (name) => {
603
+ const checker = getChecker(name);
604
+ if (!checker)
605
+ return { name, status: "skipped", detail: "unknown" };
606
+ const start = Date.now();
607
+ const result = await checker();
608
+ return { ...result, latency: Date.now() - start };
609
+ });
610
+ return Promise.all(checks);
611
+ })(),
612
+ // 3. Deploy history (last 5)
613
+ (async () => {
614
+ if (!process.env.VERCEL_TOKEN)
615
+ return [];
616
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=5", {
617
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
618
+ signal: AbortSignal.timeout(10000),
619
+ });
620
+ const data = await res.json();
621
+ return (data.deployments ?? []).map((d) => ({
622
+ state: d.readyState ?? d.state, url: d.url, created: new Date(d.created).toISOString(), target: d.target ?? "preview",
623
+ }));
624
+ })(),
625
+ // 4. Open issues
626
+ (async () => {
627
+ const token = process.env.GITHUB_TOKEN;
628
+ if (!token)
629
+ return { total: 0, issues: [], note: "GITHUB_TOKEN not set" };
630
+ let repo = process.env.GITHUB_REPO ?? "";
631
+ if (!repo) {
632
+ try {
633
+ repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
634
+ }
635
+ catch {
636
+ return { total: 0, issues: [] };
637
+ }
638
+ }
639
+ const res = await fetch(`https://api.github.com/repos/${repo}/issues?state=open&per_page=10&sort=updated`, {
640
+ headers: { Authorization: `Bearer ${token}` },
641
+ signal: AbortSignal.timeout(10000),
642
+ });
643
+ if (!res.ok)
644
+ return { total: 0, issues: [] };
645
+ const issues = await res.json();
646
+ const filtered = issues.filter(i => !i.pull_request);
647
+ return { total: filtered.length, issues: filtered.map(i => ({ number: i.number, title: i.title, labels: (i.labels ?? []).map((l) => l.name) })) };
648
+ })(),
649
+ // 5. Metrics overview
650
+ Promise.resolve((() => {
651
+ const deployFreq = getDeployFrequency(7);
652
+ const errorRate = getErrorRate(7);
653
+ const uptime = getUptime(7);
654
+ const errorTrend = compareToBaseline("error");
655
+ return { deployFrequency: deployFreq, errorRate, uptime, errorTrend };
656
+ })()),
657
+ // 6. Security posture (fast checks only)
658
+ Promise.resolve(runAllChecks(["secrets", "cors", "auth"])),
659
+ // 7. Brain stats
660
+ Promise.resolve((() => {
661
+ const store = loadBrainStore();
662
+ const allEntries = getAllEntries(store);
663
+ const categories = {};
664
+ for (const e of allEntries)
665
+ categories[e.category] = (categories[e.category] || 0) + 1;
666
+ return { total: allEntries.length, categories, projects: Object.keys(store.projects).length };
667
+ })()),
668
+ ]);
669
+ const health = healthResult.status === "fulfilled" ? healthResult.value : [];
670
+ const healthSummary = {
671
+ services: health.map((r) => ({ name: r.name, status: r.status, latency: r.latency ?? null })),
672
+ healthy: health.filter((r) => r.status === "healthy").length,
673
+ down: health.filter((r) => r.status === "down").length,
674
+ total: health.length,
675
+ };
676
+ const security = securityResult.status === "fulfilled" ? securityResult.value : [];
677
+ const securitySummary = {
678
+ critical: security.filter(f => f.level === "critical").length,
679
+ warnings: security.filter(f => f.level === "warning").length,
680
+ status: security.some(f => f.level === "critical") ? "CRITICAL" : security.some(f => f.level === "warning") ? "WARNING" : "CLEAN",
681
+ };
682
+ // Overall system status
683
+ const anyDown = healthSummary.down > 0;
684
+ const hasCriticalSecurity = securitySummary.critical > 0;
685
+ const metrics = metricsResult.status === "fulfilled" ? metricsResult.value : null;
686
+ const errorDegraded = metrics?.errorTrend?.status === "degraded";
687
+ let systemStatus = "GREEN";
688
+ if (anyDown || hasCriticalSecurity)
689
+ systemStatus = "RED";
690
+ else if (errorDegraded || securitySummary.warnings > 0)
691
+ systemStatus = "YELLOW";
692
+ return {
693
+ content: [{
694
+ type: "text",
695
+ text: JSON.stringify({
696
+ systemStatus,
697
+ project: config.name,
698
+ git: gitResult.status === "fulfilled" ? gitResult.value : null,
699
+ health: healthSummary,
700
+ deploys: deployResult.status === "fulfilled" ? deployResult.value : [],
701
+ issues: issuesResult.status === "fulfilled" ? issuesResult.value : null,
702
+ metrics,
703
+ security: { ...securitySummary, findings: security },
704
+ brain: brainResult.status === "fulfilled" ? brainResult.value : null,
705
+ }, null, 2),
706
+ }],
707
+ };
708
+ });
709
+ // ──────────────────────────────────────────
710
+ // Tool: stk_incident
711
+ // ──────────────────────────────────────────
712
+ server.tool("stk_incident", "ALWAYS call this when the user reports something is BROKEN in production. Captures full state snapshot (logs, health, metrics, git, deploys), diagnoses from brain, creates GitHub issue. This is the first thing to do in any outage.", {
713
+ title: z.string().describe("Short incident description (e.g., 'API returning 500s', 'Payment webhook failing')"),
714
+ createIssue: z.boolean().optional().default(true).describe("Create a GitHub issue with full incident context"),
715
+ provider: z.enum(["railway", "vercel"]).optional(),
716
+ }, async ({ title, createIssue, provider }) => {
717
+ const config = loadConfig();
718
+ const timestamp = new Date().toISOString();
719
+ const [healthResult, logsResult, brainResult, deployResult, metricsResult] = await Promise.allSettled([
720
+ // 1. Health snapshot
721
+ (async () => {
722
+ await loadPluginCheckers();
723
+ const serviceList = enabledServices(config);
724
+ const checks = serviceList.map(async (name) => {
725
+ const checker = getChecker(name);
726
+ if (!checker)
727
+ return { name, status: "skipped" };
728
+ return checker();
729
+ });
730
+ return Promise.all(checks);
731
+ })(),
732
+ // 2. Recent logs
733
+ fetchLogs(provider, 50),
734
+ // 3. Brain diagnosis
735
+ Promise.resolve(brainDiagnose(title)),
736
+ // 4. Recent deploys
737
+ (async () => {
738
+ if (!process.env.VERCEL_TOKEN)
739
+ return [];
740
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=3", {
741
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
742
+ signal: AbortSignal.timeout(10000),
743
+ });
744
+ const data = await res.json();
745
+ return (data.deployments ?? []).map((d) => ({
746
+ state: d.readyState ?? d.state, url: d.url, created: new Date(d.created).toISOString(),
747
+ }));
748
+ })(),
749
+ // 5. Error metrics
750
+ Promise.resolve(compareToBaseline("error")),
751
+ ]);
752
+ const health = healthResult.status === "fulfilled" ? healthResult.value : [];
753
+ const logs = logsResult.status === "fulfilled" ? logsResult.value : [];
754
+ const brainMatches = brainResult.status === "fulfilled" ? brainResult.value.slice(0, 3) : [];
755
+ const deploys = deployResult.status === "fulfilled" ? deployResult.value : [];
756
+ const errorTrend = metricsResult.status === "fulfilled" ? metricsResult.value : null;
757
+ // Filter error logs
758
+ const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|FATAL/i;
759
+ const errorLogs = logs.filter(l => l.severity === "ERROR" || errorPattern.test(l.message)).slice(-15);
760
+ const downServices = health.filter(r => r.status === "down" || r.status === "degraded");
761
+ // Record incident metric
762
+ recordMetric("error", 1, { incident: title });
763
+ // Git state
764
+ let gitInfo = "";
765
+ try {
766
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
767
+ const commit = execSync('git log -1 --format="%h %s"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
768
+ gitInfo = `Branch: ${branch}, Last commit: ${commit}`;
769
+ }
770
+ catch { /* */ }
771
+ // Create GitHub issue
772
+ let issueUrl = null;
773
+ if (createIssue) {
774
+ const token = process.env.GITHUB_TOKEN;
775
+ let repo = process.env.GITHUB_REPO ?? "";
776
+ if (!repo) {
777
+ try {
778
+ repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
779
+ }
780
+ catch { /* */ }
781
+ }
782
+ if (token && repo) {
783
+ const body = [
784
+ `## Incident: ${title}`,
785
+ `**Time:** ${timestamp}`,
786
+ `**Git:** ${gitInfo}`,
787
+ "",
788
+ "### Services Down",
789
+ downServices.length > 0 ? downServices.map(s => `- ${s.name}: ${s.status} — ${s.detail ?? "unreachable"}`).join("\n") : "None",
790
+ "",
791
+ "### Error Logs (last 15)",
792
+ errorLogs.length > 0 ? "```\n" + errorLogs.map(l => `[${l.severity}] ${l.message}`).join("\n").slice(0, 3000) + "\n```" : "No error logs captured",
793
+ "",
794
+ "### Recent Deploys",
795
+ deploys.length > 0 ? deploys.map((d) => `- ${d.state} at ${d.created} — ${d.url}`).join("\n") : "No deploy info",
796
+ "",
797
+ "### Brain Suggestions",
798
+ brainMatches.length > 0 ? brainMatches.map(m => `- **${m.entry.title}**: ${m.entry.content.slice(0, 200)}`).join("\n") : "No matching patterns found",
799
+ "",
800
+ "### Error Trend",
801
+ errorTrend ? `Status: ${errorTrend.status}, Change: ${errorTrend.changePct}%` : "No baseline data",
802
+ "",
803
+ "---",
804
+ "*Auto-generated by stk incident response*",
805
+ ].join("\n");
806
+ try {
807
+ const res = await fetch(`https://api.github.com/repos/${repo}/issues`, {
808
+ method: "POST",
809
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
810
+ body: JSON.stringify({ title: `[INCIDENT] ${title}`, body, labels: ["incident", "bug"] }),
811
+ signal: AbortSignal.timeout(10000),
812
+ });
813
+ if (res.ok) {
814
+ const issue = await res.json();
815
+ issueUrl = issue.html_url;
816
+ }
817
+ }
818
+ catch { /* */ }
819
+ }
820
+ }
821
+ return {
822
+ content: [{
823
+ type: "text",
824
+ text: JSON.stringify({
825
+ incident: title,
826
+ timestamp,
827
+ downServices: downServices.map(s => ({ name: s.name, status: s.status, detail: s.detail })),
828
+ errorLogs: errorLogs.slice(0, 10),
829
+ recentDeploys: deploys,
830
+ brainSuggestions: brainMatches.map(m => ({ title: m.entry.title, solution: m.entry.content.slice(0, 300) })),
831
+ errorTrend,
832
+ gitInfo,
833
+ githubIssue: issueUrl,
834
+ suggestedActions: [
835
+ ...(downServices.length > 0 ? [`Check down services: ${downServices.map(s => s.name).join(", ")}`] : []),
836
+ ...(brainMatches.length > 0 ? [`Try: ${brainMatches[0].entry.title}`] : []),
837
+ ...(deploys.length > 0 && deploys[0].state === "ERROR" ? ["Latest deploy failed — consider rollback with stk_rollback"] : []),
838
+ "After fixing, use stk_autolearn to save the solution",
839
+ ],
840
+ }, null, 2),
841
+ }],
842
+ };
843
+ });
844
+ // ──────────────────────────────────────────
845
+ // Tool: stk_guardian
846
+ // ──────────────────────────────────────────
847
+ server.tool("stk_guardian", "FULL SECURITY + RELIABILITY AUDIT. Runs all security checks, verifies service health, checks for env var drift, validates deploy state, analyzes error trends, and reviews brain knowledge coverage. Returns a comprehensive risk assessment.", {}, async () => {
848
+ const config = loadConfig();
849
+ const [securityResult, healthResult, envResult, metricsResult, brainResult] = await Promise.allSettled([
850
+ // 1. Full security scan
851
+ Promise.resolve(runAllChecks()),
852
+ // 2. Health checks
853
+ (async () => {
854
+ await loadPluginCheckers();
855
+ const serviceList = enabledServices(config);
856
+ const checks = serviceList.map(async (name) => {
857
+ const checker = getChecker(name);
858
+ if (!checker)
859
+ return { name, status: "skipped" };
860
+ return checker();
861
+ });
862
+ return Promise.all(checks);
863
+ })(),
864
+ // 3. Env var completeness
865
+ Promise.resolve((() => {
866
+ const ENV_REQS = {
867
+ railway: ["RAILWAY_API_TOKEN", "RAILWAY_PROJECT_ID"],
868
+ vercel: ["VERCEL_TOKEN"],
869
+ database: ["DATABASE_URL"],
870
+ redis: ["REDIS_URL"],
871
+ supabase: ["SUPABASE_URL"],
872
+ stripe: ["STRIPE_SECRET_KEY"],
873
+ r2: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"],
874
+ };
875
+ const enabled = enabledServices(config);
876
+ const missing = [];
877
+ for (const svc of enabled) {
878
+ for (const v of ENV_REQS[svc] ?? []) {
879
+ if (!process.env[v])
880
+ missing.push(`${svc}: ${v}`);
881
+ }
882
+ }
883
+ return { configured: enabled.length, missingVars: missing };
884
+ })()),
885
+ // 4. Metrics health
886
+ Promise.resolve((() => {
887
+ const errorTrend = compareToBaseline("error");
888
+ const uptime = getUptime(7);
889
+ const deployFreq = getDeployFrequency(7);
890
+ return { errorTrend, uptime, deployFrequency: deployFreq };
891
+ })()),
892
+ // 5. Brain coverage
893
+ Promise.resolve((() => {
894
+ const store = loadBrainStore();
895
+ const entries = getAllEntries(store);
896
+ const categories = {};
897
+ for (const e of entries)
898
+ categories[e.category] = (categories[e.category] || 0) + 1;
899
+ const weakAreas = ["auth", "security", "database", "deployment", "api"].filter(c => (categories[c] ?? 0) < 5);
900
+ return { total: entries.length, categories, weakAreas };
901
+ })()),
902
+ ]);
903
+ const security = securityResult.status === "fulfilled" ? securityResult.value : [];
904
+ const health = healthResult.status === "fulfilled" ? healthResult.value : [];
905
+ const env = envResult.status === "fulfilled" ? envResult.value : { configured: 0, missingVars: [] };
906
+ const metrics = metricsResult.status === "fulfilled" ? metricsResult.value : null;
907
+ const brain = brainResult.status === "fulfilled" ? brainResult.value : null;
908
+ // Risk score (0-100, lower is better)
909
+ let riskScore = 0;
910
+ const risks = [];
911
+ const criticalFindings = security.filter(f => f.level === "critical").length;
912
+ const warningFindings = security.filter(f => f.level === "warning").length;
913
+ riskScore += criticalFindings * 20;
914
+ riskScore += warningFindings * 5;
915
+ if (criticalFindings > 0)
916
+ risks.push(`${criticalFindings} critical security findings`);
917
+ const downCount = health.filter(r => r.status === "down").length;
918
+ riskScore += downCount * 15;
919
+ if (downCount > 0)
920
+ risks.push(`${downCount} services down`);
921
+ riskScore += env.missingVars.length * 5;
922
+ if (env.missingVars.length > 0)
923
+ risks.push(`${env.missingVars.length} missing env vars`);
924
+ if (metrics?.errorTrend?.status === "degraded") {
925
+ riskScore += 10;
926
+ risks.push("Error rate trending up");
927
+ }
928
+ if (metrics?.uptime && metrics.uptime.pct < 99) {
929
+ riskScore += 10;
930
+ risks.push(`Uptime at ${metrics.uptime.pct}%`);
931
+ }
932
+ if (brain?.weakAreas && brain.weakAreas.length > 2) {
933
+ riskScore += 5;
934
+ risks.push(`Brain has weak coverage in: ${brain.weakAreas.join(", ")}`);
935
+ }
936
+ riskScore = Math.min(100, riskScore);
937
+ const riskLevel = riskScore >= 50 ? "HIGH" : riskScore >= 20 ? "MEDIUM" : "LOW";
938
+ return {
939
+ content: [{
940
+ type: "text",
941
+ text: JSON.stringify({
942
+ riskScore,
943
+ riskLevel,
944
+ risks,
945
+ security: {
946
+ findings: security,
947
+ summary: { critical: criticalFindings, warnings: warningFindings },
948
+ },
949
+ health: {
950
+ services: health.map(r => ({ name: r.name, status: r.status })),
951
+ down: downCount,
952
+ },
953
+ environment: env,
954
+ metrics,
955
+ brainCoverage: brain,
956
+ recommendations: [
957
+ ...(criticalFindings > 0 ? ["FIX NOW: Resolve critical security findings"] : []),
958
+ ...(downCount > 0 ? ["URGENT: Investigate down services"] : []),
959
+ ...(env.missingVars.length > 0 ? [`Configure missing env vars: ${env.missingVars.join(", ")}`] : []),
960
+ ...(brain?.weakAreas && brain.weakAreas.length > 0 ? [`Strengthen brain knowledge in: ${brain.weakAreas.join(", ")}`] : []),
961
+ ],
962
+ }, null, 2),
963
+ }],
964
+ };
965
+ });
966
+ // ──────────────────────────────────────────
967
+ // Tool: stk_smart_deploy
968
+ // ──────────────────────────────────────────
969
+ server.tool("stk_smart_deploy", "INTELLIGENT DEPLOY PIPELINE. Runs pre-commit checks (security + brain review) → pushes code → verifies health post-deploy → auto-reports issues. If problems are detected pre-deploy, it blocks and tells you why. The safe way to ship.", {
970
+ branch: z.string().optional().describe("Branch to push (default: from config or 'main')"),
971
+ skipChecks: z.boolean().optional().default(false).describe("Skip pre-deploy checks (not recommended)"),
972
+ }, async ({ branch, skipChecks }) => {
973
+ const config = loadConfig();
974
+ const deployBranch = branch ?? config.deploy?.branch ?? "main";
975
+ const steps = [];
976
+ // Step 1: Pre-deploy checks
977
+ if (!skipChecks) {
978
+ // Security scan
979
+ const secretFindings = checkSecrets();
980
+ const critical = secretFindings.filter(f => f.level === "critical");
981
+ if (critical.length > 0) {
982
+ steps.push({ step: "security_scan", status: "fail", detail: `${critical.length} critical: ${critical.map(f => f.message).join("; ")}` });
983
+ return {
984
+ content: [{
985
+ type: "text",
986
+ text: JSON.stringify({
987
+ deployed: false,
988
+ blocked: true,
989
+ reason: "Critical security issues found",
990
+ steps,
991
+ fix: critical.map(f => f.fix).filter(Boolean),
992
+ }, null, 2),
993
+ }],
994
+ };
995
+ }
996
+ steps.push({ step: "security_scan", status: secretFindings.length > 0 ? "warn" : "pass", detail: `${secretFindings.length} findings` });
997
+ // Brain review
998
+ let diff = "";
999
+ try {
1000
+ diff = execSync("git diff --staged", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
1001
+ }
1002
+ catch { /* */ }
1003
+ if (!diff) {
1004
+ try {
1005
+ diff = execSync("git diff", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
1006
+ }
1007
+ catch { /* */ }
1008
+ }
1009
+ if (diff) {
1010
+ const review = reviewDiff(diff);
1011
+ const warnings = review.filter(r => r.warnings.length > 0);
1012
+ steps.push({ step: "brain_review", status: warnings.length > 0 ? "warn" : "pass", detail: `${warnings.length} files with warnings` });
1013
+ }
1014
+ else {
1015
+ steps.push({ step: "brain_review", status: "skip", detail: "No diff to review" });
1016
+ }
1017
+ // Health pre-check
1018
+ await loadPluginCheckers();
1019
+ const serviceList = enabledServices(config);
1020
+ if (serviceList.length > 0) {
1021
+ const checks = serviceList.map(async (name) => {
1022
+ const checker = getChecker(name);
1023
+ if (!checker)
1024
+ return { name, status: "skipped" };
1025
+ return checker();
1026
+ });
1027
+ const results = await Promise.all(checks);
1028
+ const down = results.filter(r => r.status === "down");
1029
+ if (down.length > 0) {
1030
+ steps.push({ step: "pre_health_check", status: "warn", detail: `${down.length} services down: ${down.map(r => r.name).join(", ")}` });
1031
+ }
1032
+ else {
1033
+ steps.push({ step: "pre_health_check", status: "pass", detail: `${results.length} services healthy` });
1034
+ }
1035
+ }
1036
+ }
1037
+ else {
1038
+ steps.push({ step: "pre_checks", status: "skip", detail: "Skipped by user" });
1039
+ }
1040
+ // Step 2: Push
1041
+ try {
1042
+ const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1043
+ if (currentBranch !== deployBranch) {
1044
+ steps.push({ step: "git_push", status: "fail", detail: `On '${currentBranch}', not '${deployBranch}'` });
1045
+ return {
1046
+ content: [{
1047
+ type: "text",
1048
+ text: JSON.stringify({ deployed: false, blocked: true, reason: `Wrong branch: ${currentBranch}`, steps }, null, 2),
1049
+ }],
1050
+ };
1051
+ }
1052
+ execSync(`git push origin ${deployBranch}`, { encoding: "utf-8", stdio: "pipe" });
1053
+ steps.push({ step: "git_push", status: "pass", detail: `Pushed to ${deployBranch}` });
1054
+ }
1055
+ catch (err) {
1056
+ steps.push({ step: "git_push", status: "fail", detail: err instanceof Error ? err.message : String(err) });
1057
+ return {
1058
+ content: [{
1059
+ type: "text",
1060
+ text: JSON.stringify({ deployed: false, blocked: true, reason: "Git push failed", steps }, null, 2),
1061
+ }],
1062
+ };
1063
+ }
1064
+ // Step 3: Record deploy metric
1065
+ recordMetric("deploy", 1, { branch: deployBranch });
1066
+ steps.push({ step: "record_metrics", status: "pass", detail: "Deploy metric recorded" });
1067
+ // Step 4: Post-deploy instructions
1068
+ steps.push({ step: "post_verify", status: "pass", detail: "Call stk_postdeploy in 30-60s to verify" });
1069
+ return {
1070
+ content: [{
1071
+ type: "text",
1072
+ text: JSON.stringify({
1073
+ deployed: true,
1074
+ branch: deployBranch,
1075
+ steps,
1076
+ next: "Wait 30-60 seconds for deploy to propagate, then call stk_postdeploy to verify health.",
1077
+ }, null, 2),
1078
+ }],
1079
+ };
1080
+ });
1081
+ // ──────────────────────────────────────────
1082
+ // Tool: stk_changelog
1083
+ // ──────────────────────────────────────────
1084
+ server.tool("stk_changelog", "AUTO-GENERATE CHANGELOG. Analyzes git commits, groups by type (feature, fix, refactor), enriches with brain knowledge, and produces a structured changelog. Perfect for releases and updates.", {
1085
+ since: z.string().optional().describe("Starting point — tag, commit hash, or relative (e.g., 'v0.7.0', 'HEAD~20', '2026-03-01'). Default: last 20 commits."),
1086
+ format: z.enum(["markdown", "json"]).optional().default("markdown"),
1087
+ }, async ({ since, format }) => {
1088
+ // Get commits
1089
+ const sinceArg = since ? (since.match(/^\d{4}-/) ? `--since="${since}"` : `${since}..HEAD`) : "-20";
1090
+ let logOutput = "";
1091
+ try {
1092
+ logOutput = execSync(`git log ${sinceArg} --format="COMMIT_SEP%h|%s|%an|%ai"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 }).trim();
1093
+ }
1094
+ catch {
1095
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Could not read git log" }) }] };
1096
+ }
1097
+ const commits = logOutput.split("COMMIT_SEP").filter(Boolean).map(line => {
1098
+ const [hash, message, author, date] = line.split("|");
1099
+ return { hash, message, author, date };
1100
+ });
1101
+ if (commits.length === 0) {
1102
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No commits found in range" }) }] };
1103
+ }
1104
+ // Categorize commits
1105
+ const categories = {
1106
+ features: [], fixes: [], refactors: [], docs: [], tests: [], chores: [], other: [],
1107
+ };
1108
+ for (const c of commits) {
1109
+ const msg = c.message.toLowerCase();
1110
+ if (msg.startsWith("feat") || msg.includes("add ") || msg.includes("new ") || msg.includes("implement"))
1111
+ categories.features.push(c);
1112
+ else if (msg.startsWith("fix") || msg.includes("fix ") || msg.includes("bug") || msg.includes("patch"))
1113
+ categories.fixes.push(c);
1114
+ else if (msg.startsWith("refactor") || msg.includes("refactor") || msg.includes("split") || msg.includes("modular"))
1115
+ categories.refactors.push(c);
1116
+ else if (msg.startsWith("doc") || msg.includes("readme") || msg.includes("claude.md"))
1117
+ categories.docs.push(c);
1118
+ else if (msg.startsWith("test") || msg.includes("test"))
1119
+ categories.tests.push(c);
1120
+ else if (msg.startsWith("chore") || msg.includes("ci") || msg.includes("build") || msg.includes("bump"))
1121
+ categories.chores.push(c);
1122
+ else
1123
+ categories.other.push(c);
1124
+ }
1125
+ // Enrich with brain patterns learned during this period
1126
+ const brainStore = loadBrainStore();
1127
+ const allEntries = getAllEntries(brainStore);
1128
+ const startDate = commits[commits.length - 1]?.date ?? "";
1129
+ const recentLearnings = allEntries
1130
+ .filter(e => e.created_at >= startDate)
1131
+ .slice(0, 5)
1132
+ .map(e => ({ title: e.title, category: e.category }));
1133
+ if (format === "json") {
1134
+ return {
1135
+ content: [{
1136
+ type: "text",
1137
+ text: JSON.stringify({
1138
+ totalCommits: commits.length,
1139
+ range: { from: since ?? "last 20 commits", to: "HEAD" },
1140
+ categories,
1141
+ brainLearnings: recentLearnings,
1142
+ }, null, 2),
1143
+ }],
1144
+ };
1145
+ }
1146
+ // Markdown format
1147
+ const lines = [];
1148
+ lines.push(`# Changelog\n`);
1149
+ lines.push(`**${commits.length} commits** from ${commits[commits.length - 1]?.date?.split("T")[0] ?? "?"} to ${commits[0]?.date?.split("T")[0] ?? "now"}\n`);
1150
+ const sectionMap = {
1151
+ features: "Features", fixes: "Bug Fixes", refactors: "Refactoring",
1152
+ docs: "Documentation", tests: "Tests", chores: "Chores", other: "Other",
1153
+ };
1154
+ for (const [key, label] of Object.entries(sectionMap)) {
1155
+ const items = categories[key];
1156
+ if (items.length === 0)
1157
+ continue;
1158
+ lines.push(`\n### ${label}\n`);
1159
+ for (const c of items)
1160
+ lines.push(`- ${c.message} (\`${c.hash}\`)`);
1161
+ }
1162
+ if (recentLearnings.length > 0) {
1163
+ lines.push(`\n### Brain Learnings\n`);
1164
+ for (const l of recentLearnings)
1165
+ lines.push(`- **${l.title}** (${l.category})`);
1166
+ }
1167
+ return {
1168
+ content: [{
1169
+ type: "text",
1170
+ text: JSON.stringify({ format: "markdown", content: lines.join("\n") }, null, 2),
1171
+ }],
1172
+ };
1173
+ });
1174
+ // ──────────────────────────────────────────
1175
+ // Tool: stk_sweep
1176
+ // ──────────────────────────────────────────
1177
+ server.tool("stk_sweep", "FULL STACK SWEEP. Runs EVERYTHING: health checks, security audit, brain sync (push + pull), metrics snapshot, and log analysis. Then returns a prioritized action list. Use this for daily/weekly maintenance or before major releases.", {}, async () => {
1178
+ const config = loadConfig();
1179
+ const startTime = Date.now();
1180
+ const [healthResult, securityResult, syncResult, logsResult, metricsResult] = await Promise.allSettled([
1181
+ // 1. Health
1182
+ (async () => {
1183
+ await loadPluginCheckers();
1184
+ const serviceList = enabledServices(config);
1185
+ const checks = serviceList.map(async (name) => {
1186
+ const checker = getChecker(name);
1187
+ if (!checker)
1188
+ return { name, status: "skipped" };
1189
+ return checker();
1190
+ });
1191
+ return Promise.all(checks);
1192
+ })(),
1193
+ // 2. Security
1194
+ Promise.resolve(runAllChecks()),
1195
+ // 3. Brain sync
1196
+ (async () => {
1197
+ const pushResult = await pushToCloud().catch(() => ({ pushed: 0, pulled: 0, errors: ["push failed"] }));
1198
+ const pullResult = await pullFromCloud().catch(() => ({ pushed: 0, pulled: 0, errors: ["pull failed"] }));
1199
+ return { pushed: pushResult.pushed, pulled: pullResult.pulled, errors: [...pushResult.errors, ...pullResult.errors] };
1200
+ })(),
1201
+ // 4. Logs
1202
+ fetchLogs(undefined, 50),
1203
+ // 5. Metrics
1204
+ Promise.resolve((() => {
1205
+ const deployFreq = getDeployFrequency(7);
1206
+ const errorRate = getErrorRate(7);
1207
+ const uptime = getUptime(7);
1208
+ return { deployFreq, errorRate, uptime };
1209
+ })()),
1210
+ ]);
1211
+ const health = healthResult.status === "fulfilled" ? healthResult.value : [];
1212
+ const security = securityResult.status === "fulfilled" ? securityResult.value : [];
1213
+ const sync = syncResult.status === "fulfilled" ? syncResult.value : { pushed: 0, pulled: 0, errors: [] };
1214
+ const logs = logsResult.status === "fulfilled" ? logsResult.value : [];
1215
+ const metrics = metricsResult.status === "fulfilled" ? metricsResult.value : null;
1216
+ const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|FATAL/i;
1217
+ const errorLogs = logs.filter(l => l.severity === "ERROR" || errorPattern.test(l.message));
1218
+ // Build prioritized action list
1219
+ const actions = [];
1220
+ const downServices = health.filter(r => r.status === "down");
1221
+ for (const s of downServices)
1222
+ actions.push({ priority: "P0", action: `Fix down service: ${s.name}` });
1223
+ const criticalSecurity = security.filter(f => f.level === "critical");
1224
+ for (const f of criticalSecurity)
1225
+ actions.push({ priority: "P0", action: `Security: ${f.message}` });
1226
+ const warnSecurity = security.filter(f => f.level === "warning");
1227
+ for (const f of warnSecurity)
1228
+ actions.push({ priority: "P1", action: `Security: ${f.message}` });
1229
+ if (errorLogs.length > 10)
1230
+ actions.push({ priority: "P1", action: `${errorLogs.length} errors in recent logs — investigate` });
1231
+ if (metrics?.uptime && metrics.uptime.pct < 99)
1232
+ actions.push({ priority: "P1", action: `Uptime at ${metrics.uptime.pct}% — investigate` });
1233
+ if (sync.errors.length > 0)
1234
+ actions.push({ priority: "P2", action: "Brain sync had errors — check Supabase connection" });
1235
+ if (sync.pulled > 0)
1236
+ actions.push({ priority: "P2", action: `Pulled ${sync.pulled} new patterns from cloud brain` });
1237
+ if (actions.length === 0)
1238
+ actions.push({ priority: "P2", action: "All clear — nothing urgent" });
1239
+ const durationMs = Date.now() - startTime;
1240
+ return {
1241
+ content: [{
1242
+ type: "text",
1243
+ text: JSON.stringify({
1244
+ sweep: "complete",
1245
+ durationMs,
1246
+ health: {
1247
+ healthy: health.filter(r => r.status === "healthy").length,
1248
+ down: downServices.length,
1249
+ total: health.length,
1250
+ },
1251
+ security: {
1252
+ critical: criticalSecurity.length,
1253
+ warnings: warnSecurity.length,
1254
+ total: security.length,
1255
+ },
1256
+ brainSync: sync,
1257
+ logs: { total: logs.length, errors: errorLogs.length },
1258
+ metrics,
1259
+ actions,
1260
+ }, null, 2),
1261
+ }],
1262
+ };
1263
+ });
1264
+ // ══════════════════════════════════════════
1265
+ // CONTEXT INTELLIGENCE TOOLS
1266
+ // ══════════════════════════════════════════
1267
+ // ──────────────────────────────────────────
1268
+ // Tool: stk_context
1269
+ // ──────────────────────────────────────────
1270
+ server.tool("stk_context", "INSTANT PROJECT CONTEXT. Call this at the start of any new conversation. Returns: tech stack, file structure, code conventions, recent git activity, what's deployed, what's broken, brain knowledge, and open work. Everything Claude needs to write code that fits the project from message one.", {}, async () => {
1271
+ const config = loadConfig();
1272
+ const [stackResult, structureResult, gitResult, conventionsResult, brainResult, healthResult, issuesResult] = await Promise.allSettled([
1273
+ // 1. Tech stack from package.json
1274
+ Promise.resolve((() => {
1275
+ try {
1276
+ const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
1277
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1278
+ const stack = [];
1279
+ const frameworks = {
1280
+ next: "Next.js", react: "React", vue: "Vue", svelte: "Svelte", angular: "Angular",
1281
+ express: "Express", fastify: "Fastify", hono: "Hono", koa: "Koa",
1282
+ "@supabase/supabase-js": "Supabase", stripe: "Stripe",
1283
+ prisma: "Prisma", "@prisma/client": "Prisma", mongoose: "Mongoose",
1284
+ drizzle: "Drizzle", typeorm: "TypeORM",
1285
+ redis: "Redis", ioredis: "Redis", bullmq: "BullMQ",
1286
+ tailwindcss: "Tailwind CSS", typescript: "TypeScript", zod: "Zod",
1287
+ vitest: "Vitest", jest: "Jest", mocha: "Mocha",
1288
+ };
1289
+ for (const [pkg, name] of Object.entries(frameworks)) {
1290
+ if (deps?.[pkg])
1291
+ stack.push(`${name} (${deps[pkg]})`);
1292
+ }
1293
+ return {
1294
+ name: pkg.name, version: pkg.version, description: pkg.description,
1295
+ stack, scripts: Object.keys(pkg.scripts ?? {}),
1296
+ totalDeps: Object.keys(pkg.dependencies ?? {}).length,
1297
+ totalDevDeps: Object.keys(pkg.devDependencies ?? {}).length,
1298
+ };
1299
+ }
1300
+ catch {
1301
+ return null;
1302
+ }
1303
+ })()),
1304
+ // 2. File structure (top-level + src breakdown)
1305
+ Promise.resolve((() => {
1306
+ const structure = {};
1307
+ try {
1308
+ const topLevel = readdirSync(".").filter(f => !f.startsWith(".") && f !== "node_modules" && f !== "dist");
1309
+ structure.root = topLevel;
1310
+ // Scan src/ if exists
1311
+ for (const dir of ["src", "app", "pages", "lib", "components"]) {
1312
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
1313
+ try {
1314
+ structure[dir] = readdirSync(dir).slice(0, 30);
1315
+ }
1316
+ catch { /* */ }
1317
+ }
1318
+ }
1319
+ }
1320
+ catch { /* */ }
1321
+ return structure;
1322
+ })()),
1323
+ // 3. Git activity
1324
+ Promise.resolve((() => {
1325
+ try {
1326
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1327
+ const recentCommits = execSync('git log -10 --format="%h|%s|%an|%cr"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim()
1328
+ .split("\n").filter(Boolean).map(line => {
1329
+ const [hash, message, author, age] = line.split("|");
1330
+ return { hash, message, author, age };
1331
+ });
1332
+ const dirty = execSync("git status --porcelain", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
1333
+ const branches = execSync("git branch --format='%(refname:short)'", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
1334
+ return { branch, recentCommits, dirtyFiles: dirty.length, branches };
1335
+ }
1336
+ catch {
1337
+ return null;
1338
+ }
1339
+ })()),
1340
+ // 4. Code conventions detection
1341
+ Promise.resolve((() => {
1342
+ const conventions = {};
1343
+ // TSConfig
1344
+ if (existsSync("tsconfig.json")) {
1345
+ try {
1346
+ const tsconfig = JSON.parse(readFileSync("tsconfig.json", "utf-8"));
1347
+ conventions.strict = tsconfig.compilerOptions?.strict ? "yes" : "no";
1348
+ conventions.target = tsconfig.compilerOptions?.target ?? "unknown";
1349
+ conventions.module = tsconfig.compilerOptions?.module ?? "unknown";
1350
+ }
1351
+ catch { /* */ }
1352
+ }
1353
+ // Linting
1354
+ if (existsSync(".eslintrc.json") || existsSync(".eslintrc.js") || existsSync("eslint.config.js"))
1355
+ conventions.linter = "ESLint";
1356
+ if (existsSync("biome.json"))
1357
+ conventions.linter = "Biome";
1358
+ if (existsSync(".prettierrc") || existsSync(".prettierrc.json"))
1359
+ conventions.formatter = "Prettier";
1360
+ // Testing
1361
+ if (existsSync("vitest.config.ts") || existsSync("vitest.config.js"))
1362
+ conventions.testing = "Vitest";
1363
+ else if (existsSync("jest.config.ts") || existsSync("jest.config.js"))
1364
+ conventions.testing = "Jest";
1365
+ // Docker
1366
+ if (existsSync("Dockerfile") || existsSync("docker-compose.yml"))
1367
+ conventions.containerized = "yes";
1368
+ // CI
1369
+ if (existsSync(".github/workflows"))
1370
+ conventions.ci = "GitHub Actions";
1371
+ // CLAUDE.md
1372
+ if (existsSync("CLAUDE.md")) {
1373
+ try {
1374
+ conventions.claudeMd = readFileSync("CLAUDE.md", "utf-8").slice(0, 1500);
1375
+ }
1376
+ catch { /* */ }
1377
+ }
1378
+ return conventions;
1379
+ })()),
1380
+ // 5. Brain knowledge summary
1381
+ Promise.resolve((() => {
1382
+ const store = loadBrainStore();
1383
+ const entries = getAllEntries(store);
1384
+ const categories = {};
1385
+ for (const e of entries)
1386
+ categories[e.category] = (categories[e.category] || 0) + 1;
1387
+ // Top 5 most relevant entries (by recency)
1388
+ const recent = entries.slice(-5).reverse().map(e => ({ title: e.title, category: e.category, source: e.source }));
1389
+ return { total: entries.length, categories, projects: Object.keys(store.projects), recentEntries: recent };
1390
+ })()),
1391
+ // 6. Service health (quick)
1392
+ (async () => {
1393
+ await loadPluginCheckers();
1394
+ const serviceList = enabledServices(config);
1395
+ if (serviceList.length === 0)
1396
+ return null;
1397
+ const checks = serviceList.map(async (name) => {
1398
+ const checker = getChecker(name);
1399
+ if (!checker)
1400
+ return { name, status: "skipped" };
1401
+ return checker();
1402
+ });
1403
+ const results = await Promise.all(checks);
1404
+ return results.map(r => ({ name: r.name, status: r.status }));
1405
+ })(),
1406
+ // 7. Open issues
1407
+ (async () => {
1408
+ const token = process.env.GITHUB_TOKEN;
1409
+ if (!token)
1410
+ return null;
1411
+ let repo = process.env.GITHUB_REPO ?? "";
1412
+ if (!repo) {
1413
+ try {
1414
+ repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
1415
+ }
1416
+ catch {
1417
+ return null;
1418
+ }
1419
+ }
1420
+ const res = await fetch(`https://api.github.com/repos/${repo}/issues?state=open&per_page=5&sort=updated`, {
1421
+ headers: { Authorization: `Bearer ${token}` },
1422
+ signal: AbortSignal.timeout(10000),
1423
+ });
1424
+ if (!res.ok)
1425
+ return null;
1426
+ const issues = await res.json();
1427
+ return issues.filter(i => !i.pull_request).map(i => ({ number: i.number, title: i.title, labels: (i.labels ?? []).map((l) => l.name) }));
1428
+ })(),
1429
+ ]);
1430
+ return {
1431
+ content: [{
1432
+ type: "text",
1433
+ text: JSON.stringify({
1434
+ project: config.name,
1435
+ stack: stackResult.status === "fulfilled" ? stackResult.value : null,
1436
+ structure: structureResult.status === "fulfilled" ? structureResult.value : null,
1437
+ git: gitResult.status === "fulfilled" ? gitResult.value : null,
1438
+ conventions: conventionsResult.status === "fulfilled" ? conventionsResult.value : null,
1439
+ brain: brainResult.status === "fulfilled" ? brainResult.value : null,
1440
+ services: healthResult.status === "fulfilled" ? healthResult.value : null,
1441
+ openIssues: issuesResult.status === "fulfilled" ? issuesResult.value : null,
1442
+ }, null, 2),
1443
+ }],
1444
+ };
1445
+ });
1446
+ // ──────────────────────────────────────────
1447
+ // Tool: stk_time_travel
1448
+ // ──────────────────────────────────────────
1449
+ server.tool("stk_time_travel", "RECONSTRUCT WHAT HAPPENED. Given a time window, shows: commits, deploys, error spikes, health changes, and brain entries added. Perfect for 'something broke overnight' investigations and post-mortems.", {
1450
+ hours: z.number().optional().default(24).describe("How many hours to look back (default: 24)"),
1451
+ since: z.string().optional().describe("Exact start time (ISO 8601). Overrides hours if provided."),
1452
+ }, async ({ hours, since }) => {
1453
+ const sinceDate = since ? new Date(since) : new Date(Date.now() - hours * 3600000);
1454
+ const sinceISO = sinceDate.toISOString();
1455
+ const sinceGit = sinceDate.toISOString().split("T")[0];
1456
+ const [commitsResult, deploysResult, logsResult, metricsResult, brainResult] = await Promise.allSettled([
1457
+ // 1. Commits in window
1458
+ Promise.resolve((() => {
1459
+ try {
1460
+ const output = execSync(`git log --since="${sinceGit}" --format="%h|%s|%an|%ai"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 }).trim();
1461
+ if (!output)
1462
+ return [];
1463
+ return output.split("\n").filter(Boolean).map(line => {
1464
+ const [hash, message, author, date] = line.split("|");
1465
+ return { hash, message, author, date };
1466
+ });
1467
+ }
1468
+ catch {
1469
+ return [];
1470
+ }
1471
+ })()),
1472
+ // 2. Deploy history
1473
+ (async () => {
1474
+ if (!process.env.VERCEL_TOKEN)
1475
+ return [];
1476
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=20", {
1477
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
1478
+ signal: AbortSignal.timeout(10000),
1479
+ });
1480
+ const data = await res.json();
1481
+ return (data.deployments ?? [])
1482
+ .filter((d) => new Date(d.created).toISOString() >= sinceISO)
1483
+ .map((d) => ({
1484
+ state: d.readyState ?? d.state, url: d.url, created: new Date(d.created).toISOString(), target: d.target,
1485
+ }));
1486
+ })(),
1487
+ // 3. Logs in window
1488
+ (async () => {
1489
+ const logs = await fetchLogs(undefined, 100);
1490
+ const inWindow = logs.filter(l => l.timestamp >= sinceISO);
1491
+ const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|FATAL/i;
1492
+ const errors = inWindow.filter(l => l.severity === "ERROR" || errorPattern.test(l.message));
1493
+ return { total: inWindow.length, errors: errors.length, topErrors: errors.slice(0, 10) };
1494
+ })(),
1495
+ // 4. Metrics in window
1496
+ Promise.resolve((() => {
1497
+ const daysSince = Math.max(1, Math.ceil(hours / 24));
1498
+ const deploys = getDeployFrequency(daysSince);
1499
+ const errors = getErrorRate(daysSince);
1500
+ const uptime = getUptime(daysSince);
1501
+ const errorTrend = compareToBaseline("error");
1502
+ return { deploys, errors, uptime, errorTrend };
1503
+ })()),
1504
+ // 5. Brain entries added in window
1505
+ Promise.resolve((() => {
1506
+ const store = loadBrainStore();
1507
+ const entries = getAllEntries(store);
1508
+ return entries.filter(e => e.created_at >= sinceISO).map(e => ({ title: e.title, category: e.category, source: e.source, created_at: e.created_at }));
1509
+ })()),
1510
+ ]);
1511
+ const commits = commitsResult.status === "fulfilled" ? commitsResult.value : [];
1512
+ const deploys = deploysResult.status === "fulfilled" ? deploysResult.value : [];
1513
+ const logs = logsResult.status === "fulfilled" ? logsResult.value : { total: 0, errors: 0, topErrors: [] };
1514
+ const metrics = metricsResult.status === "fulfilled" ? metricsResult.value : null;
1515
+ const brainEntries = brainResult.status === "fulfilled" ? brainResult.value : [];
1516
+ // Build timeline
1517
+ const timeline = [];
1518
+ for (const c of commits)
1519
+ timeline.push({ time: c.date, type: "commit", detail: `${c.hash} ${c.message} (${c.author})` });
1520
+ for (const d of deploys)
1521
+ timeline.push({ time: d.created, type: "deploy", detail: `${d.state} → ${d.url}` });
1522
+ for (const e of logs.topErrors)
1523
+ timeline.push({ time: e.timestamp, type: "error", detail: e.message.slice(0, 150) });
1524
+ timeline.sort((a, b) => a.time.localeCompare(b.time));
1525
+ return {
1526
+ content: [{
1527
+ type: "text",
1528
+ text: JSON.stringify({
1529
+ window: { since: sinceISO, hours },
1530
+ summary: {
1531
+ commits: commits.length,
1532
+ deploys: deploys.length,
1533
+ logErrors: logs.errors,
1534
+ brainEntries: brainEntries.length,
1535
+ },
1536
+ timeline: timeline.slice(-30),
1537
+ commits,
1538
+ deploys,
1539
+ logs,
1540
+ metrics,
1541
+ brainEntries,
1542
+ }, null, 2),
1543
+ }],
1544
+ };
1545
+ });
1546
+ // ──────────────────────────────────────────
1547
+ // Tool: stk_onboard
1548
+ // ──────────────────────────────────────────
1549
+ server.tool("stk_onboard", "ZERO TO FULL CONTEXT. First time in a project? This tool: ingests the project into brain, pulls knowledge from cloud, detects full stack, maps architecture, and returns everything needed to start contributing. One call to onboard.", {
1550
+ force: z.boolean().optional().default(false).describe("Re-ingest even if already done"),
1551
+ }, async ({ force }) => {
1552
+ const config = loadConfig();
1553
+ const results = { project: config.name };
1554
+ // 1. Ingest project into brain
1555
+ try {
1556
+ const store = loadBrainStore();
1557
+ const projectName = config.name ?? "unknown";
1558
+ if (!store.projects[projectName] || force) {
1559
+ const { entries, filesScanned } = ingestProject(process.cwd());
1560
+ if (entries.length > 0) {
1561
+ store.projects[projectName] = {
1562
+ ingestedAt: new Date().toISOString(),
1563
+ projectPath: process.cwd(),
1564
+ entries,
1565
+ };
1566
+ saveBrainStore(store);
1567
+ results.ingested = { entries: entries.length, filesScanned };
1568
+ }
1569
+ }
1570
+ else {
1571
+ results.ingested = { alreadyDone: true, entries: store.projects[projectName].entries.length };
1572
+ }
1573
+ }
1574
+ catch (err) {
1575
+ results.ingested = { error: err instanceof Error ? err.message : "failed" };
1576
+ }
1577
+ // 2. Pull from cloud brain
1578
+ try {
1579
+ const pullResult = await pullFromCloud();
1580
+ results.cloudPull = { pulled: pullResult.pulled, errors: pullResult.errors.length > 0 ? pullResult.errors : undefined };
1581
+ }
1582
+ catch {
1583
+ results.cloudPull = { pulled: 0, error: "failed" };
1584
+ }
1585
+ // 3. Full stack detection
1586
+ try {
1587
+ const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
1588
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1589
+ const detect = (name) => deps?.[name] ? `${name}@${deps[name]}` : null;
1590
+ results.stack = {
1591
+ runtime: "Node.js",
1592
+ language: detect("typescript") ? "TypeScript" : "JavaScript",
1593
+ framework: detect("next") ?? detect("react") ?? detect("vue") ?? detect("express") ?? detect("fastify") ?? detect("hono") ?? "unknown",
1594
+ database: detect("@prisma/client") ?? detect("prisma") ?? detect("mongoose") ?? detect("drizzle-orm") ?? detect("typeorm") ?? detect("@supabase/supabase-js") ?? "none detected",
1595
+ cache: detect("redis") ?? detect("ioredis") ?? detect("bullmq") ?? "none",
1596
+ payments: detect("stripe") ?? "none",
1597
+ testing: detect("vitest") ?? detect("jest") ?? detect("mocha") ?? "none",
1598
+ styling: detect("tailwindcss") ?? detect("styled-components") ?? detect("@emotion/react") ?? "none",
1599
+ validation: detect("zod") ?? detect("joi") ?? detect("yup") ?? "none",
1600
+ };
1601
+ }
1602
+ catch {
1603
+ results.stack = null;
1604
+ }
1605
+ // 4. Architecture map
1606
+ try {
1607
+ const dirs = {};
1608
+ const scanDirs = ["src", "app", "pages", "api", "lib", "components", "services", "routes", "middleware", "utils", "hooks", "types", "models", "prisma", "tests", "scripts"];
1609
+ for (const dir of scanDirs) {
1610
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
1611
+ dirs[dir] = readdirSync(dir).filter(f => !f.startsWith(".")).slice(0, 20);
1612
+ }
1613
+ }
1614
+ results.architecture = dirs;
1615
+ }
1616
+ catch {
1617
+ results.architecture = null;
1618
+ }
1619
+ // 5. Config files present
1620
+ const configFiles = [
1621
+ "tsconfig.json", "package.json", "stk.config.json", "CLAUDE.md",
1622
+ ".eslintrc.json", "eslint.config.js", "biome.json", ".prettierrc",
1623
+ "Dockerfile", "docker-compose.yml", "fly.toml", "vercel.json", "railway.json",
1624
+ ".github/workflows", "prisma/schema.prisma",
1625
+ ];
1626
+ results.configFiles = configFiles.filter(f => existsSync(f));
1627
+ // 6. Brain knowledge summary
1628
+ const store = loadBrainStore();
1629
+ const allEntries = getAllEntries(store);
1630
+ const categories = {};
1631
+ for (const e of allEntries)
1632
+ categories[e.category] = (categories[e.category] || 0) + 1;
1633
+ results.brain = { total: allEntries.length, categories, projects: Object.keys(store.projects) };
1634
+ // 7. Services configured
1635
+ results.services = enabledServices(config);
1636
+ return {
1637
+ content: [{
1638
+ type: "text",
1639
+ text: JSON.stringify(results, null, 2),
1640
+ }],
1641
+ };
1642
+ });
1643
+ // ──────────────────────────────────────────
1644
+ // Tool: stk_dep_intel
1645
+ // ──────────────────────────────────────────
1646
+ server.tool("stk_dep_intel", "DEPENDENCY INTELLIGENCE. Before adding or changing a package, check: what's already installed, version conflicts, peer deps, known issues from brain, and what the package is used for in this project. Prevents bad dependency decisions.", {
1647
+ package: z.string().optional().describe("Specific package to analyze (e.g., 'prisma', 'stripe'). If omitted, returns full dependency overview."),
1648
+ }, async ({ package: pkgName }) => {
1649
+ let pkg = {};
1650
+ try {
1651
+ pkg = JSON.parse(readFileSync("package.json", "utf-8"));
1652
+ }
1653
+ catch {
1654
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No package.json found" }) }] };
1655
+ }
1656
+ const deps = pkg.dependencies ?? {};
1657
+ const devDeps = pkg.devDependencies ?? {};
1658
+ const allDeps = { ...deps, ...devDeps };
1659
+ if (!pkgName) {
1660
+ // Full overview
1661
+ const categories = {
1662
+ framework: [], database: [], auth: [], payments: [], testing: [], styling: [], tooling: [], other: [],
1663
+ };
1664
+ const categorize = {
1665
+ next: "framework", react: "framework", vue: "framework", express: "framework", fastify: "framework", hono: "framework",
1666
+ prisma: "database", "@prisma/client": "database", mongoose: "database", "drizzle-orm": "database", typeorm: "database",
1667
+ "@supabase/supabase-js": "database", redis: "database", ioredis: "database",
1668
+ stripe: "payments", "next-auth": "auth", "jsonwebtoken": "auth", bcrypt: "auth", passport: "auth",
1669
+ vitest: "testing", jest: "testing", "@testing-library/react": "testing",
1670
+ tailwindcss: "styling", "styled-components": "styling",
1671
+ typescript: "tooling", eslint: "tooling", prettier: "tooling",
1672
+ };
1673
+ for (const [name, ver] of Object.entries(allDeps)) {
1674
+ const cat = categorize[name] ?? "other";
1675
+ categories[cat].push(`${name}@${ver}`);
1676
+ }
1677
+ return {
1678
+ content: [{
1679
+ type: "text",
1680
+ text: JSON.stringify({
1681
+ total: Object.keys(allDeps).length,
1682
+ production: Object.keys(deps).length,
1683
+ dev: Object.keys(devDeps).length,
1684
+ categories,
1685
+ lockfile: existsSync("package-lock.json") ? "npm" : existsSync("yarn.lock") ? "yarn" : existsSync("pnpm-lock.yaml") ? "pnpm" : existsSync("bun.lockb") ? "bun" : "none",
1686
+ }, null, 2),
1687
+ }],
1688
+ };
1689
+ }
1690
+ // Specific package analysis
1691
+ const installed = allDeps[pkgName];
1692
+ const isDep = !!deps[pkgName];
1693
+ const isDevDep = !!devDeps[pkgName];
1694
+ // Find where it's used in the codebase
1695
+ let usageFiles = [];
1696
+ try {
1697
+ const output = execSync(`grep -rl "${pkgName}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" src/ 2>/dev/null || true`, {
1698
+ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024,
1699
+ }).trim();
1700
+ usageFiles = output ? output.split("\n").filter(Boolean).slice(0, 15) : [];
1701
+ }
1702
+ catch { /* */ }
1703
+ // Check brain for known issues/patterns about this package
1704
+ const brainMatches = smartSearch(extractTerms(pkgName));
1705
+ const brainInsights = brainMatches.slice(0, 5).map(m => ({ title: m.entry.title, content: m.entry.content.slice(0, 200), source: m.entry.source }));
1706
+ // Check for peer dependency info
1707
+ let peerInfo = null;
1708
+ try {
1709
+ const npmInfo = execSync(`npm info ${pkgName} peerDependencies --json 2>/dev/null || echo "{}"`, {
1710
+ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000,
1711
+ }).trim();
1712
+ const peers = JSON.parse(npmInfo);
1713
+ if (Object.keys(peers).length > 0) {
1714
+ const missing = Object.keys(peers).filter(p => !allDeps[p]);
1715
+ peerInfo = missing.length > 0 ? `Missing peer deps: ${missing.join(", ")}` : "All peer deps satisfied";
1716
+ }
1717
+ }
1718
+ catch { /* */ }
1719
+ // Latest version
1720
+ let latestVersion = null;
1721
+ try {
1722
+ latestVersion = execSync(`npm info ${pkgName} version 2>/dev/null`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 }).trim();
1723
+ }
1724
+ catch { /* */ }
1725
+ return {
1726
+ content: [{
1727
+ type: "text",
1728
+ text: JSON.stringify({
1729
+ package: pkgName,
1730
+ installed: installed ?? "NOT INSTALLED",
1731
+ latest: latestVersion,
1732
+ type: isDep ? "production" : isDevDep ? "dev" : "not installed",
1733
+ usedIn: usageFiles,
1734
+ usageCount: usageFiles.length,
1735
+ peerDeps: peerInfo,
1736
+ brainInsights,
1737
+ needsUpdate: installed && latestVersion && installed.replace(/[\^~]/, "") !== latestVersion,
1738
+ }, null, 2),
1739
+ }],
1740
+ };
1741
+ });
1742
+ // ──────────────────────────────────────────
1743
+ // Tool: stk_diff_explain
1744
+ // ──────────────────────────────────────────
1745
+ server.tool("stk_diff_explain", "EXPLAIN WHAT CHANGED AND WHY. Given a commit, PR, or range, breaks down every file change, categorizes by type (feature/fix/refactor), enriches with brain knowledge, and identifies potential risks. Perfect for code reviews and catching up on changes.", {
1746
+ ref: z.string().optional().describe("Commit hash, branch, PR number, or range (e.g., 'abc123', 'HEAD~3..HEAD', 'main..feature'). Default: last commit."),
1747
+ }, async ({ ref }) => {
1748
+ // Determine diff command
1749
+ let diffCmd = "git diff HEAD~1";
1750
+ let logCmd = 'git log -1 --format="%H|%s|%an|%ai|%b"';
1751
+ if (ref) {
1752
+ if (ref.includes("..")) {
1753
+ diffCmd = `git diff ${ref}`;
1754
+ logCmd = `git log ${ref} --format="%H|%s|%an|%ai|%b"`;
1755
+ }
1756
+ else if (/^\d+$/.test(ref)) {
1757
+ // PR number — try to get from GitHub
1758
+ const token = process.env.GITHUB_TOKEN;
1759
+ let repo = process.env.GITHUB_REPO ?? "";
1760
+ if (!repo) {
1761
+ try {
1762
+ repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
1763
+ }
1764
+ catch { /* */ }
1765
+ }
1766
+ if (token && repo) {
1767
+ try {
1768
+ const res = await fetch(`https://api.github.com/repos/${repo}/pulls/${ref}`, {
1769
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github.diff" },
1770
+ signal: AbortSignal.timeout(10000),
1771
+ });
1772
+ if (res.ok) {
1773
+ const prDiff = await res.text();
1774
+ const prInfoRes = await fetch(`https://api.github.com/repos/${repo}/pulls/${ref}`, {
1775
+ headers: { Authorization: `Bearer ${token}` },
1776
+ signal: AbortSignal.timeout(10000),
1777
+ });
1778
+ const prInfo = await prInfoRes.json();
1779
+ const files = parseChangedFiles(prDiff);
1780
+ const review = reviewDiff(prDiff);
1781
+ const category = detectCategory(files);
1782
+ return {
1783
+ content: [{
1784
+ type: "text",
1785
+ text: JSON.stringify({
1786
+ type: "pull_request",
1787
+ pr: { number: ref, title: prInfo.title, author: prInfo.user?.login, state: prInfo.state, body: prInfo.body?.slice(0, 500) },
1788
+ filesChanged: files,
1789
+ changeType: category,
1790
+ brainReview: review.filter(r => r.warnings.length > 0).slice(0, 10),
1791
+ risks: review.filter(r => r.warnings.length > 0).map(r => `${r.file}: ${r.warnings.length} brain warnings`),
1792
+ }, null, 2),
1793
+ }],
1794
+ };
1795
+ }
1796
+ }
1797
+ catch { /* fall through */ }
1798
+ }
1799
+ // Fallback: treat as commit
1800
+ diffCmd = `git diff ${ref}~1 ${ref}`;
1801
+ logCmd = `git log ${ref} -1 --format="%H|%s|%an|%ai|%b"`;
1802
+ }
1803
+ else {
1804
+ diffCmd = `git diff ${ref}~1 ${ref}`;
1805
+ logCmd = `git log ${ref} -1 --format="%H|%s|%an|%ai|%b"`;
1806
+ }
1807
+ }
1808
+ // Get diff
1809
+ let diff = "";
1810
+ try {
1811
+ diff = execSync(diffCmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
1812
+ }
1813
+ catch { /* */ }
1814
+ // Get commit info
1815
+ let commitInfo = {};
1816
+ try {
1817
+ const output = execSync(logCmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 }).trim();
1818
+ const lines = output.split("\n");
1819
+ if (lines[0]) {
1820
+ const [hash, subject, author, date, ...bodyParts] = lines[0].split("|");
1821
+ commitInfo = { hash, subject, author, date, body: bodyParts.join("|").trim() };
1822
+ }
1823
+ }
1824
+ catch { /* */ }
1825
+ if (!diff) {
1826
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No diff found for the given reference" }) }] };
1827
+ }
1828
+ const files = parseChangedFiles(diff);
1829
+ const changeType = detectCategory(files);
1830
+ // Per-file analysis
1831
+ const fileChunks = diff.split(/^diff --git /m).slice(1);
1832
+ const fileAnalysis = fileChunks.slice(0, 20).map(chunk => {
1833
+ const pathMatch = chunk.match(/b\/(.+?)[\s\n]/);
1834
+ const filePath = pathMatch?.[1] ?? "unknown";
1835
+ const added = (chunk.match(/^\+[^+]/gm) ?? []).length;
1836
+ const removed = (chunk.match(/^-[^-]/gm) ?? []).length;
1837
+ return { file: filePath, added, removed, net: added - removed };
1838
+ });
1839
+ // Brain review
1840
+ const brainReview = reviewDiff(diff);
1841
+ const filesWithWarnings = brainReview.filter(r => r.warnings.length > 0);
1842
+ // Classify change type
1843
+ const msg = (commitInfo.subject ?? "").toLowerCase();
1844
+ let intent = "other";
1845
+ if (msg.startsWith("feat") || msg.includes("add "))
1846
+ intent = "feature";
1847
+ else if (msg.startsWith("fix") || msg.includes("fix "))
1848
+ intent = "bugfix";
1849
+ else if (msg.includes("refactor") || msg.includes("split") || msg.includes("clean"))
1850
+ intent = "refactor";
1851
+ else if (msg.includes("test"))
1852
+ intent = "test";
1853
+ else if (msg.includes("doc") || msg.includes("readme"))
1854
+ intent = "docs";
1855
+ const totalAdded = fileAnalysis.reduce((s, f) => s + f.added, 0);
1856
+ const totalRemoved = fileAnalysis.reduce((s, f) => s + f.removed, 0);
1857
+ return {
1858
+ content: [{
1859
+ type: "text",
1860
+ text: JSON.stringify({
1861
+ commit: commitInfo,
1862
+ intent,
1863
+ category: changeType,
1864
+ stats: { files: files.length, added: totalAdded, removed: totalRemoved, net: totalAdded - totalRemoved },
1865
+ fileAnalysis,
1866
+ brainWarnings: filesWithWarnings.slice(0, 10),
1867
+ risks: [
1868
+ ...(filesWithWarnings.length > 0 ? [`${filesWithWarnings.length} files triggered brain warnings`] : []),
1869
+ ...(totalRemoved > 100 ? [`Large deletion: ${totalRemoved} lines removed`] : []),
1870
+ ...(files.some(f => f.includes("migration") || f.includes("schema")) ? ["Database schema change detected — review carefully"] : []),
1871
+ ...(files.some(f => f.includes("auth") || f.includes("security")) ? ["Auth/security file modified — verify no regression"] : []),
1872
+ ],
1873
+ }, null, 2),
1874
+ }],
1875
+ };
1876
+ });
1877
+ } // end registerAutomationTools