@prajwolkc/stk 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/brain.js +36 -3
- package/dist/mcp/server.js +243 -1
- package/dist/services/brain.d.ts +19 -0
- package/dist/services/brain.js +76 -0
- package/dist/services/metrics.d.ts +35 -0
- package/dist/services/metrics.js +78 -0
- package/dist/services/security.d.ts +19 -0
- package/dist/services/security.js +194 -0
- package/package.json +2 -1
- package/src/data/seed-patterns.json +1802 -0
package/dist/commands/brain.js
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
|
-
import { syncBrain, pushToCloud, pullFromCloud, loadBrainStore, getAllEntries } from "../services/brain.js";
|
|
4
|
+
import { syncBrain, pushToCloud, pullFromCloud, loadBrainStore, getAllEntries, seedBrain } from "../services/brain.js";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
5
8
|
export const brainCommand = new Command("brain")
|
|
6
9
|
.description("Manage the stk knowledge brain — sync, push, pull across machines")
|
|
7
|
-
.argument("[action]", "push | pull | sync | stats (default: sync)")
|
|
8
|
-
.
|
|
10
|
+
.argument("[action]", "push | pull | sync | stats | seed (default: sync)")
|
|
11
|
+
.option("--force", "force re-seed (replace existing seed entries)")
|
|
12
|
+
.action(async (action = "sync", _extra, cmd) => {
|
|
13
|
+
const opts = cmd?.opts?.() ?? {};
|
|
14
|
+
if (action === "seed") {
|
|
15
|
+
const spinner = ora(" Loading seed patterns...").start();
|
|
16
|
+
try {
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
// Data lives in src/data (shipped with npm), not dist/data
|
|
20
|
+
const seedPath = join(__dirname, "..", "..", "src", "data", "seed-patterns.json");
|
|
21
|
+
const patterns = JSON.parse(readFileSync(seedPath, "utf-8"));
|
|
22
|
+
spinner.text = ` Seeding ${patterns.length} patterns...`;
|
|
23
|
+
const { added, skipped } = seedBrain(patterns, opts.force);
|
|
24
|
+
spinner.succeed(` Seeded ${chalk.white(added)} patterns (${skipped} already existed)`);
|
|
25
|
+
console.log();
|
|
26
|
+
const categories = {};
|
|
27
|
+
for (const p of patterns)
|
|
28
|
+
categories[p.category] = (categories[p.category] || 0) + 1;
|
|
29
|
+
console.log(chalk.bold(" Patterns by category:"));
|
|
30
|
+
for (const [cat, count] of Object.entries(categories).sort(([, a], [, b]) => b - a)) {
|
|
31
|
+
console.log(` ${chalk.dim("●")} ${cat}: ${count}`);
|
|
32
|
+
}
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
spinner.fail(" Seed failed");
|
|
37
|
+
console.log(` ${chalk.red(err instanceof Error ? err.message : String(err))}`);
|
|
38
|
+
console.log();
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
9
42
|
if (action === "stats") {
|
|
10
43
|
const store = loadBrainStore();
|
|
11
44
|
const projects = Object.entries(store.projects);
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,7 +11,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { loadConfig, enabledServices } from "../lib/config.js";
|
|
13
13
|
import { getChecker, allCheckerNames, loadPluginCheckers } from "../services/registry.js";
|
|
14
|
-
import { getLocalBrainClient, ingestProject, loadBrainStore, saveBrainStore, syncBrain, pushToCloud, pullFromCloud, brainCheck, brainDiagnose } from "../services/brain.js";
|
|
14
|
+
import { getLocalBrainClient, ingestProject, loadBrainStore, saveBrainStore, syncBrain, pushToCloud, pullFromCloud, brainCheck, brainDiagnose, reviewDiff } from "../services/brain.js";
|
|
15
|
+
import { recordMetric, getMetrics, compareToBaseline, getDeployFrequency, getErrorRate, getUptime } from "../services/metrics.js";
|
|
16
|
+
import { runAllChecks } from "../services/security.js";
|
|
15
17
|
import { execSync } from "child_process";
|
|
16
18
|
const server = new McpServer({
|
|
17
19
|
name: "stk",
|
|
@@ -1563,6 +1565,246 @@ function detectGitHubRepo() {
|
|
|
1563
1565
|
return null;
|
|
1564
1566
|
}
|
|
1565
1567
|
}
|
|
1568
|
+
// ──────────────────────────────────────────
|
|
1569
|
+
// Tool: stk_autofix
|
|
1570
|
+
// ──────────────────────────────────────────
|
|
1571
|
+
server.tool("stk_autofix", "Auto-diagnose errors from production logs. Fetches recent logs, extracts errors, and searches the brain for matching solutions. Returns errors with matched fix patterns and confidence scores.", {
|
|
1572
|
+
provider: z.enum(["railway", "vercel"]).optional(),
|
|
1573
|
+
lines: z.number().optional().default(50),
|
|
1574
|
+
}, async ({ provider, lines }) => {
|
|
1575
|
+
// Determine provider
|
|
1576
|
+
const config = loadConfig();
|
|
1577
|
+
const detected = provider ?? (enabledServices(config).includes("railway") ? "railway" : "vercel");
|
|
1578
|
+
// Fetch logs using existing stk_logs logic (inline)
|
|
1579
|
+
let logs = [];
|
|
1580
|
+
try {
|
|
1581
|
+
if (detected === "railway") {
|
|
1582
|
+
const token = process.env.RAILWAY_API_TOKEN;
|
|
1583
|
+
const projectId = process.env.RAILWAY_PROJECT_ID;
|
|
1584
|
+
const serviceId = process.env.RAILWAY_SERVICE_ID;
|
|
1585
|
+
if (token && projectId && serviceId) {
|
|
1586
|
+
const envId = process.env.RAILWAY_ENVIRONMENT_ID;
|
|
1587
|
+
const gql = `query { deployments(first:1, input:{projectId:"${projectId}",serviceId:"${serviceId}"${envId ? `,environmentId:"${envId}"` : ""}}) { edges { node { id } } } }`;
|
|
1588
|
+
const depRes = await fetch("https://backboard.railway.app/graphql/v2", {
|
|
1589
|
+
method: "POST",
|
|
1590
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
1591
|
+
body: JSON.stringify({ query: gql }),
|
|
1592
|
+
});
|
|
1593
|
+
const depData = await depRes.json();
|
|
1594
|
+
const depId = (depData?.data?.deployments?.edges?.[0]?.node?.id) ?? null;
|
|
1595
|
+
if (depId) {
|
|
1596
|
+
const logGql = `query { deploymentLogs(deploymentId:"${depId}",limit:${lines}) { timestamp message severity } }`;
|
|
1597
|
+
const logRes = await fetch("https://backboard.railway.app/graphql/v2", {
|
|
1598
|
+
method: "POST",
|
|
1599
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
1600
|
+
body: JSON.stringify({ query: logGql }),
|
|
1601
|
+
});
|
|
1602
|
+
const logData = await logRes.json();
|
|
1603
|
+
logs = (logData?.data?.deploymentLogs ?? []);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
catch { /* log fetch failed */ }
|
|
1609
|
+
// Filter for errors
|
|
1610
|
+
const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|ReferenceError|rejected|FATAL/i;
|
|
1611
|
+
const errorLogs = logs.filter(l => l.severity === "error" || (l.message && errorPattern.test(l.message)));
|
|
1612
|
+
// Deduplicate by first 100 chars
|
|
1613
|
+
const grouped = new Map();
|
|
1614
|
+
for (const log of errorLogs) {
|
|
1615
|
+
const key = log.message.slice(0, 100);
|
|
1616
|
+
const existing = grouped.get(key);
|
|
1617
|
+
if (existing) {
|
|
1618
|
+
existing.count++;
|
|
1619
|
+
existing.lastSeen = log.timestamp;
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
grouped.set(key, { message: log.message, count: 1, firstSeen: log.timestamp, lastSeen: log.timestamp });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// Diagnose each unique error
|
|
1626
|
+
const errors = [];
|
|
1627
|
+
let matchedErrors = 0;
|
|
1628
|
+
for (const error of grouped.values()) {
|
|
1629
|
+
const matches = brainDiagnose(error.message);
|
|
1630
|
+
const solutions = matches.slice(0, 3).map(m => ({
|
|
1631
|
+
title: m.entry.title,
|
|
1632
|
+
content: m.entry.content.slice(0, 400),
|
|
1633
|
+
confidence: Math.min(1, m.score / 10),
|
|
1634
|
+
source: m.entry.source,
|
|
1635
|
+
}));
|
|
1636
|
+
if (solutions.length > 0)
|
|
1637
|
+
matchedErrors++;
|
|
1638
|
+
errors.push({ ...error, solutions });
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
content: [{
|
|
1642
|
+
type: "text",
|
|
1643
|
+
text: JSON.stringify({
|
|
1644
|
+
provider: detected,
|
|
1645
|
+
errors,
|
|
1646
|
+
summary: {
|
|
1647
|
+
totalErrors: errorLogs.length,
|
|
1648
|
+
uniqueErrors: grouped.size,
|
|
1649
|
+
matchedErrors,
|
|
1650
|
+
unmatchedErrors: grouped.size - matchedErrors,
|
|
1651
|
+
},
|
|
1652
|
+
}, null, 2),
|
|
1653
|
+
}],
|
|
1654
|
+
};
|
|
1655
|
+
});
|
|
1656
|
+
// ──────────────────────────────────────────
|
|
1657
|
+
// Tool: stk_brain_review
|
|
1658
|
+
// ──────────────────────────────────────────
|
|
1659
|
+
server.tool("stk_brain_review", "Review code changes against the brain's knowledge base. Checks a git diff or PR for known gotchas per file. Use before merging PRs or after making changes.", {
|
|
1660
|
+
diff: z.string().optional().describe("Raw git diff output"),
|
|
1661
|
+
pr: z.number().optional().describe("GitHub PR number to review"),
|
|
1662
|
+
}, async ({ diff, pr }) => {
|
|
1663
|
+
let diffContent = diff ?? "";
|
|
1664
|
+
// If PR number, fetch diff from GitHub
|
|
1665
|
+
if (pr && !diff) {
|
|
1666
|
+
const token = process.env.GITHUB_TOKEN;
|
|
1667
|
+
let repo = process.env.GITHUB_REPO ?? "";
|
|
1668
|
+
if (!repo) {
|
|
1669
|
+
try {
|
|
1670
|
+
repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
|
|
1671
|
+
}
|
|
1672
|
+
catch { /* */ }
|
|
1673
|
+
}
|
|
1674
|
+
if (repo && token) {
|
|
1675
|
+
try {
|
|
1676
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/pulls/${pr}`, {
|
|
1677
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github.diff" },
|
|
1678
|
+
});
|
|
1679
|
+
if (res.ok)
|
|
1680
|
+
diffContent = await res.text();
|
|
1681
|
+
}
|
|
1682
|
+
catch { /* */ }
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
// If no diff provided, get latest commit diff
|
|
1686
|
+
if (!diffContent) {
|
|
1687
|
+
try {
|
|
1688
|
+
diffContent = execSync("git diff HEAD~1", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* */ }
|
|
1691
|
+
}
|
|
1692
|
+
if (!diffContent) {
|
|
1693
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "No diff available. Provide diff text, PR number, or ensure you're in a git repo." }) }] };
|
|
1694
|
+
}
|
|
1695
|
+
const results = reviewDiff(diffContent);
|
|
1696
|
+
const filesWithWarnings = results.filter(r => r.warnings.length > 0);
|
|
1697
|
+
return {
|
|
1698
|
+
content: [{
|
|
1699
|
+
type: "text",
|
|
1700
|
+
text: JSON.stringify({
|
|
1701
|
+
files: results,
|
|
1702
|
+
summary: {
|
|
1703
|
+
filesReviewed: results.length,
|
|
1704
|
+
filesWithWarnings: filesWithWarnings.length,
|
|
1705
|
+
totalWarnings: filesWithWarnings.reduce((s, r) => s + r.warnings.length, 0),
|
|
1706
|
+
},
|
|
1707
|
+
}, null, 2),
|
|
1708
|
+
}],
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
// ──────────────────────────────────────────
|
|
1712
|
+
// Tool: stk_metrics
|
|
1713
|
+
// ──────────────────────────────────────────
|
|
1714
|
+
server.tool("stk_metrics", "Track and analyze infrastructure metrics over time — deploy frequency, error rates, response times, uptime. Compare current performance against historical baselines to detect regressions.", {
|
|
1715
|
+
action: z.enum(["view", "record", "compare"]).optional().default("view"),
|
|
1716
|
+
type: z.enum(["deploy", "health_check", "error", "response_time"]).optional(),
|
|
1717
|
+
value: z.number().optional(),
|
|
1718
|
+
metadata: z.record(z.string(), z.string()).optional(),
|
|
1719
|
+
days: z.number().optional().default(7),
|
|
1720
|
+
}, async ({ action, type, value, metadata, days }) => {
|
|
1721
|
+
if (action === "record" && type && value !== undefined) {
|
|
1722
|
+
recordMetric(type, value, metadata ?? {});
|
|
1723
|
+
return { content: [{ type: "text", text: JSON.stringify({ recorded: true, type, value }) }] };
|
|
1724
|
+
}
|
|
1725
|
+
if (action === "compare") {
|
|
1726
|
+
const types = ["deploy", "health_check", "error", "response_time"];
|
|
1727
|
+
const comparisons = types.map(t => ({ type: t, ...compareToBaseline(t) }));
|
|
1728
|
+
const degraded = comparisons.filter(c => c.status === "degraded");
|
|
1729
|
+
return {
|
|
1730
|
+
content: [{
|
|
1731
|
+
type: "text",
|
|
1732
|
+
text: JSON.stringify({
|
|
1733
|
+
comparisons,
|
|
1734
|
+
degraded: degraded.length > 0 ? degraded : "none",
|
|
1735
|
+
summary: `${degraded.length} metric(s) degraded`,
|
|
1736
|
+
}, null, 2),
|
|
1737
|
+
}],
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
// Default: view
|
|
1741
|
+
return {
|
|
1742
|
+
content: [{
|
|
1743
|
+
type: "text",
|
|
1744
|
+
text: JSON.stringify({
|
|
1745
|
+
period: `${days} days`,
|
|
1746
|
+
deploys: getDeployFrequency(days),
|
|
1747
|
+
errors: getErrorRate(days),
|
|
1748
|
+
uptime: getUptime(days),
|
|
1749
|
+
recentMetrics: getMetrics(type ?? undefined, days, 20),
|
|
1750
|
+
}, null, 2),
|
|
1751
|
+
}],
|
|
1752
|
+
};
|
|
1753
|
+
});
|
|
1754
|
+
// ──────────────────────────────────────────
|
|
1755
|
+
// Tool: stk_secure
|
|
1756
|
+
// ──────────────────────────────────────────
|
|
1757
|
+
server.tool("stk_secure", "Security scan — check for exposed secrets, vulnerable dependencies, missing rate limiting, open CORS, and unprotected routes. Returns findings with severity levels and fix suggestions.", {
|
|
1758
|
+
checks: z.array(z.enum(["secrets", "deps", "rate_limit", "cors", "auth"])).optional().describe("Specific checks to run (default: all)"),
|
|
1759
|
+
}, async ({ checks }) => {
|
|
1760
|
+
const findings = runAllChecks(checks);
|
|
1761
|
+
const critical = findings.filter(f => f.level === "critical").length;
|
|
1762
|
+
const warning = findings.filter(f => f.level === "warning").length;
|
|
1763
|
+
const info = findings.filter(f => f.level === "info").length;
|
|
1764
|
+
return {
|
|
1765
|
+
content: [{
|
|
1766
|
+
type: "text",
|
|
1767
|
+
text: JSON.stringify({
|
|
1768
|
+
findings,
|
|
1769
|
+
summary: { critical, warning, info, total: findings.length },
|
|
1770
|
+
status: critical > 0 ? "CRITICAL" : warning > 0 ? "WARNING" : "CLEAN",
|
|
1771
|
+
}, null, 2),
|
|
1772
|
+
}],
|
|
1773
|
+
};
|
|
1774
|
+
});
|
|
1775
|
+
// ──────────────────────────────────────────
|
|
1776
|
+
// Tool: stk_brain_team
|
|
1777
|
+
// ──────────────────────────────────────────
|
|
1778
|
+
server.tool("stk_brain_team", "Show team brain contributions — who learned what, from which projects, and when. Tracks knowledge sharing across team members.", {}, async () => {
|
|
1779
|
+
const store = loadBrainStore();
|
|
1780
|
+
const allEntries = [...store.global];
|
|
1781
|
+
for (const proj of Object.values(store.projects))
|
|
1782
|
+
allEntries.push(...proj.entries);
|
|
1783
|
+
const contributors = {};
|
|
1784
|
+
for (const entry of allEntries) {
|
|
1785
|
+
const contributor = entry.contributor ?? "auto";
|
|
1786
|
+
if (!contributors[contributor]) {
|
|
1787
|
+
contributors[contributor] = { count: 0, lastActive: entry.created_at, categories: {} };
|
|
1788
|
+
}
|
|
1789
|
+
contributors[contributor].count++;
|
|
1790
|
+
if (entry.created_at > contributors[contributor].lastActive) {
|
|
1791
|
+
contributors[contributor].lastActive = entry.created_at;
|
|
1792
|
+
}
|
|
1793
|
+
contributors[contributor].categories[entry.category] = (contributors[contributor].categories[entry.category] || 0) + 1;
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
content: [{
|
|
1797
|
+
type: "text",
|
|
1798
|
+
text: JSON.stringify({
|
|
1799
|
+
totalEntries: allEntries.length,
|
|
1800
|
+
contributors,
|
|
1801
|
+
projects: Object.entries(store.projects).map(([name, p]) => ({
|
|
1802
|
+
name, entries: p.entries.length, ingestedAt: p.ingestedAt,
|
|
1803
|
+
})),
|
|
1804
|
+
}, null, 2),
|
|
1805
|
+
}],
|
|
1806
|
+
};
|
|
1807
|
+
});
|
|
1566
1808
|
// Start
|
|
1567
1809
|
async function main() {
|
|
1568
1810
|
const transport = new StdioServerTransport();
|
package/dist/services/brain.d.ts
CHANGED
|
@@ -76,4 +76,23 @@ export declare function extractTerms(description: string): string[];
|
|
|
76
76
|
export declare function brainCheck(taskDescription: string): ScoredEntry[];
|
|
77
77
|
/** Diagnose an error — find matching patterns from past issues */
|
|
78
78
|
export declare function brainDiagnose(error: string): ScoredEntry[];
|
|
79
|
+
/** Seed brain with curated patterns (idempotent) */
|
|
80
|
+
export declare function seedBrain(patterns: KnowledgeEntry[], force?: boolean): {
|
|
81
|
+
added: number;
|
|
82
|
+
skipped: number;
|
|
83
|
+
};
|
|
84
|
+
/** Review a diff against brain knowledge — find gotchas per file */
|
|
85
|
+
export interface ReviewWarning {
|
|
86
|
+
file: string;
|
|
87
|
+
linesChanged: number;
|
|
88
|
+
warnings: Array<{
|
|
89
|
+
title: string;
|
|
90
|
+
content: string;
|
|
91
|
+
relevance: number;
|
|
92
|
+
source: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
export declare function reviewDiff(diff: string): ReviewWarning[];
|
|
96
|
+
/** Get contributor name from git config */
|
|
97
|
+
export declare function getContributor(): string;
|
|
79
98
|
export {};
|
package/dist/services/brain.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from
|
|
|
2
2
|
import { join, resolve, basename } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
+
import { execSync } from "child_process";
|
|
5
6
|
import { loadConfig } from "../lib/config.js";
|
|
6
7
|
// ──────────────────────────────────────────
|
|
7
8
|
// Storage
|
|
@@ -611,3 +612,78 @@ export function brainDiagnose(error) {
|
|
|
611
612
|
const allTerms = [...new Set([...terms, ...errorTerms])];
|
|
612
613
|
return smartSearch(allTerms);
|
|
613
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface MetricEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
type: "deploy" | "health_check" | "error" | "response_time";
|
|
4
|
+
value: number;
|
|
5
|
+
metadata: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
interface MetricsStore {
|
|
8
|
+
version: 1;
|
|
9
|
+
entries: MetricEntry[];
|
|
10
|
+
}
|
|
11
|
+
export declare function loadMetrics(): MetricsStore;
|
|
12
|
+
export declare function saveMetrics(store: MetricsStore): void;
|
|
13
|
+
export declare function recordMetric(type: MetricEntry["type"], value: number, metadata?: Record<string, string>): void;
|
|
14
|
+
export declare function getMetrics(type?: MetricEntry["type"], days?: number, limit?: number): MetricEntry[];
|
|
15
|
+
export declare function getBaseline(type: MetricEntry["type"]): number;
|
|
16
|
+
export declare function compareToBaseline(type: MetricEntry["type"]): {
|
|
17
|
+
current: number;
|
|
18
|
+
baseline: number;
|
|
19
|
+
changePct: number;
|
|
20
|
+
status: "degraded" | "stable" | "improved";
|
|
21
|
+
};
|
|
22
|
+
export declare function getDeployFrequency(days?: number): {
|
|
23
|
+
total: number;
|
|
24
|
+
perDay: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function getErrorRate(days?: number): {
|
|
27
|
+
total: number;
|
|
28
|
+
perDay: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function getUptime(days?: number): {
|
|
31
|
+
checks: number;
|
|
32
|
+
healthy: number;
|
|
33
|
+
pct: number;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const STK_DIR = join(homedir(), ".stk");
|
|
5
|
+
const METRICS_PATH = join(STK_DIR, "metrics.json");
|
|
6
|
+
const MAX_ENTRIES = 1000;
|
|
7
|
+
function ensureDir() {
|
|
8
|
+
if (!existsSync(STK_DIR))
|
|
9
|
+
mkdirSync(STK_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
export function loadMetrics() {
|
|
12
|
+
ensureDir();
|
|
13
|
+
if (!existsSync(METRICS_PATH))
|
|
14
|
+
return { version: 1, entries: [] };
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(METRICS_PATH, "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { version: 1, entries: [] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function saveMetrics(store) {
|
|
23
|
+
ensureDir();
|
|
24
|
+
writeFileSync(METRICS_PATH, JSON.stringify(store, null, 2));
|
|
25
|
+
}
|
|
26
|
+
export function recordMetric(type, value, metadata = {}) {
|
|
27
|
+
const store = loadMetrics();
|
|
28
|
+
store.entries.push({ timestamp: new Date().toISOString(), type, value, metadata });
|
|
29
|
+
// Evict oldest if over limit
|
|
30
|
+
if (store.entries.length > MAX_ENTRIES) {
|
|
31
|
+
store.entries = store.entries.slice(-MAX_ENTRIES);
|
|
32
|
+
}
|
|
33
|
+
saveMetrics(store);
|
|
34
|
+
}
|
|
35
|
+
export function getMetrics(type, days = 7, limit = 100) {
|
|
36
|
+
const store = loadMetrics();
|
|
37
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
38
|
+
let entries = store.entries.filter(e => e.timestamp >= since);
|
|
39
|
+
if (type)
|
|
40
|
+
entries = entries.filter(e => e.type === type);
|
|
41
|
+
return entries.slice(-limit);
|
|
42
|
+
}
|
|
43
|
+
export function getBaseline(type) {
|
|
44
|
+
const store = loadMetrics();
|
|
45
|
+
const ofType = store.entries.filter(e => e.type === type).slice(-10);
|
|
46
|
+
if (ofType.length === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return ofType.reduce((sum, e) => sum + e.value, 0) / ofType.length;
|
|
49
|
+
}
|
|
50
|
+
export function compareToBaseline(type) {
|
|
51
|
+
const store = loadMetrics();
|
|
52
|
+
const ofType = store.entries.filter(e => e.type === type);
|
|
53
|
+
if (ofType.length < 2)
|
|
54
|
+
return { current: 0, baseline: 0, changePct: 0, status: "stable" };
|
|
55
|
+
const recent = ofType.slice(-3);
|
|
56
|
+
const older = ofType.slice(-13, -3);
|
|
57
|
+
const current = recent.reduce((s, e) => s + e.value, 0) / recent.length;
|
|
58
|
+
const baseline = older.length > 0 ? older.reduce((s, e) => s + e.value, 0) / older.length : current;
|
|
59
|
+
const changePct = baseline === 0 ? 0 : Math.round(((current - baseline) / baseline) * 100);
|
|
60
|
+
const status = changePct > 20 ? "degraded" : changePct < -20 ? "improved" : "stable";
|
|
61
|
+
return { current: Math.round(current * 100) / 100, baseline: Math.round(baseline * 100) / 100, changePct, status };
|
|
62
|
+
}
|
|
63
|
+
export function getDeployFrequency(days = 7) {
|
|
64
|
+
const entries = getMetrics("deploy", days, 1000);
|
|
65
|
+
const perDay = days > 0 ? Math.round((entries.length / days) * 10) / 10 : 0;
|
|
66
|
+
return { total: entries.length, perDay };
|
|
67
|
+
}
|
|
68
|
+
export function getErrorRate(days = 7) {
|
|
69
|
+
const entries = getMetrics("error", days, 1000);
|
|
70
|
+
const perDay = days > 0 ? Math.round((entries.length / days) * 10) / 10 : 0;
|
|
71
|
+
return { total: entries.length, perDay };
|
|
72
|
+
}
|
|
73
|
+
export function getUptime(days = 7) {
|
|
74
|
+
const entries = getMetrics("health_check", days, 1000);
|
|
75
|
+
const healthy = entries.filter(e => e.value > 0).length;
|
|
76
|
+
const pct = entries.length > 0 ? Math.round((healthy / entries.length) * 1000) / 10 : 100;
|
|
77
|
+
return { checks: entries.length, healthy, pct };
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SecurityFinding {
|
|
2
|
+
check: string;
|
|
3
|
+
level: "critical" | "warning" | "info";
|
|
4
|
+
message: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
fix?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Check for exposed secrets in .env files */
|
|
9
|
+
export declare function checkSecrets(): SecurityFinding[];
|
|
10
|
+
/** Check for outdated/vulnerable dependencies */
|
|
11
|
+
export declare function checkDeps(): SecurityFinding[];
|
|
12
|
+
/** Check for missing rate limiting */
|
|
13
|
+
export declare function checkRateLimit(): SecurityFinding[];
|
|
14
|
+
/** Check for open CORS */
|
|
15
|
+
export declare function checkCors(): SecurityFinding[];
|
|
16
|
+
/** Check for routes without auth middleware */
|
|
17
|
+
export declare function checkAuth(): SecurityFinding[];
|
|
18
|
+
/** Run all security checks */
|
|
19
|
+
export declare function runAllChecks(checks?: string[]): SecurityFinding[];
|