@prajwolkc/stk 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/brain.js +36 -3
- package/dist/mcp/server.js +14 -1553
- package/dist/mcp/tools/brain.d.ts +2 -0
- package/dist/mcp/tools/brain.js +557 -0
- package/dist/mcp/tools/data.d.ts +2 -0
- package/dist/mcp/tools/data.js +385 -0
- package/dist/mcp/tools/github.d.ts +2 -0
- package/dist/mcp/tools/github.js +95 -0
- package/dist/mcp/tools/infra.d.ts +2 -0
- package/dist/mcp/tools/infra.js +263 -0
- package/dist/mcp/tools/ops.d.ts +2 -0
- package/dist/mcp/tools/ops.js +411 -0
- package/dist/mcp/tools/security.d.ts +2 -0
- package/dist/mcp/tools/security.js +25 -0
- package/dist/mcp/types.d.ts +2 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/brain-cloud.d.ts +13 -0
- package/dist/services/brain-cloud.js +131 -0
- package/dist/services/brain-extract.d.ts +14 -0
- package/dist/services/brain-extract.js +253 -0
- package/dist/services/brain-search.d.ts +33 -0
- package/dist/services/brain-search.js +153 -0
- package/dist/services/brain-store.d.ts +25 -0
- package/dist/services/brain-store.js +42 -0
- package/dist/services/brain.d.ts +19 -68
- package/dist/services/brain.js +18 -542
- package/dist/services/metrics.d.ts +35 -0
- package/dist/services/metrics.js +78 -0
- package/dist/services/security.d.ts +19 -0
- package/dist/services/security.js +194 -0
- package/package.json +2 -1
- package/src/data/seed-patterns.json +1802 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { recordMetric, getMetrics, compareToBaseline, getDeployFrequency, getErrorRate, getUptime } from "../../services/metrics.js";
|
|
3
|
+
export function registerDataTools(server) {
|
|
4
|
+
// ──────────────────────────────────────────
|
|
5
|
+
// Tool: stk_db
|
|
6
|
+
// ──────────────────────────────────────────
|
|
7
|
+
server.tool("stk_db", "Query your Supabase database directly. Run SELECT queries, check row counts, inspect table data — all from chat. Only read operations are allowed for safety.", {
|
|
8
|
+
query: z.string().optional().describe("SQL query to run (SELECT only for safety)"),
|
|
9
|
+
table: z.string().optional().describe("Shorthand: just provide a table name to SELECT * with a limit"),
|
|
10
|
+
limit: z.number().optional().describe("Max rows to return (default 20)"),
|
|
11
|
+
}, async ({ query, table, limit: rawLimit }) => {
|
|
12
|
+
const limit = rawLimit ?? 20;
|
|
13
|
+
const url = process.env.SUPABASE_URL;
|
|
14
|
+
const key = process.env.SUPABASE_SERVICE_KEY;
|
|
15
|
+
if (!url || !key) {
|
|
16
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "SUPABASE_URL or SUPABASE_SERVICE_KEY not set" }) }] };
|
|
17
|
+
}
|
|
18
|
+
// If table shorthand is used, build a simple query
|
|
19
|
+
let sql = query ?? "";
|
|
20
|
+
if (table && !query) {
|
|
21
|
+
sql = `SELECT * FROM ${table} ORDER BY created_at DESC LIMIT ${limit}`;
|
|
22
|
+
}
|
|
23
|
+
if (!sql && !table) {
|
|
24
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Provide either a 'query' or 'table' parameter" }) }] };
|
|
25
|
+
}
|
|
26
|
+
// Safety: only allow read operations
|
|
27
|
+
const normalized = sql.trim().toUpperCase();
|
|
28
|
+
if (!normalized.startsWith("SELECT") && !normalized.startsWith("WITH")) {
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Only SELECT/WITH queries are allowed for safety. Use Supabase dashboard for mutations." }) }] };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${url}/rest/v1/rpc/`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
apikey: key,
|
|
36
|
+
Authorization: `Bearer ${key}`,
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Prefer: "return=representation",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({}),
|
|
41
|
+
});
|
|
42
|
+
// Use PostgREST query instead of RPC for better compatibility
|
|
43
|
+
// Parse table name from SQL for simple queries
|
|
44
|
+
const tableMatch = sql.match(/FROM\s+["']?(\w+)["']?/i);
|
|
45
|
+
const targetTable = table ?? tableMatch?.[1];
|
|
46
|
+
if (targetTable) {
|
|
47
|
+
const restRes = await fetch(`${url}/rest/v1/${targetTable}?select=*&limit=${limit}`, {
|
|
48
|
+
headers: {
|
|
49
|
+
apikey: key,
|
|
50
|
+
Authorization: `Bearer ${key}`,
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
Prefer: "count=exact",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!restRes.ok) {
|
|
56
|
+
const errText = await restRes.text();
|
|
57
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Query failed: ${errText}` }) }] };
|
|
58
|
+
}
|
|
59
|
+
const contentRange = restRes.headers.get("content-range");
|
|
60
|
+
const totalCount = contentRange ? contentRange.split("/")[1] : "unknown";
|
|
61
|
+
const data = await restRes.json();
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify({
|
|
66
|
+
table: targetTable,
|
|
67
|
+
totalRows: totalCount,
|
|
68
|
+
returned: Array.isArray(data) ? data.length : 0,
|
|
69
|
+
data,
|
|
70
|
+
}, null, 2),
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Could not parse table name from query. Use the 'table' parameter instead." }) }] };
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }] };
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// ──────────────────────────────────────────
|
|
81
|
+
// Tool: stk_analytics
|
|
82
|
+
// ──────────────────────────────────────────
|
|
83
|
+
server.tool("stk_analytics", "Get live app analytics: total users, posts, payments, revenue, and recent activity. Pulls data from Supabase and Stripe in one call.", {}, async () => {
|
|
84
|
+
const results = {};
|
|
85
|
+
// Supabase stats
|
|
86
|
+
const url = process.env.SUPABASE_URL;
|
|
87
|
+
const key = process.env.SUPABASE_SERVICE_KEY;
|
|
88
|
+
if (url && key) {
|
|
89
|
+
const headers = {
|
|
90
|
+
apikey: key,
|
|
91
|
+
Authorization: `Bearer ${key}`,
|
|
92
|
+
Prefer: "count=exact",
|
|
93
|
+
};
|
|
94
|
+
// Get table counts in parallel
|
|
95
|
+
const [postsRes, usersRes, paymentsRes] = await Promise.all([
|
|
96
|
+
fetch(`${url}/rest/v1/posts?select=id&limit=0`, { headers }).catch(() => null),
|
|
97
|
+
fetch(`${url}/rest/v1/users?select=id&limit=0`, { headers }).catch(() => null),
|
|
98
|
+
fetch(`${url}/rest/v1/payments?select=id&limit=0`, { headers }).catch(() => null),
|
|
99
|
+
]);
|
|
100
|
+
const getCount = (res) => {
|
|
101
|
+
if (!res?.ok)
|
|
102
|
+
return null;
|
|
103
|
+
const range = res.headers.get("content-range");
|
|
104
|
+
return range ? parseInt(range.split("/")[1]) || 0 : null;
|
|
105
|
+
};
|
|
106
|
+
results.supabase = {
|
|
107
|
+
totalPosts: getCount(postsRes),
|
|
108
|
+
totalUsers: getCount(usersRes),
|
|
109
|
+
totalPayments: getCount(paymentsRes),
|
|
110
|
+
};
|
|
111
|
+
// Recent posts (last 24h)
|
|
112
|
+
const oneDayAgo = Math.floor(Date.now() / 1000) - 86400;
|
|
113
|
+
const recentRes = await fetch(`${url}/rest/v1/posts?select=id×tamp=gte.${oneDayAgo}&limit=0`, { headers: { ...headers, Prefer: "count=exact" } }).catch(() => null);
|
|
114
|
+
results.supabase.postsLast24h = getCount(recentRes);
|
|
115
|
+
// Recent users with paid posts
|
|
116
|
+
const paidUsersRes = await fetch(`${url}/rest/v1/users?select=id&paid_posts_available=gt.0&limit=0`, { headers: { ...headers, Prefer: "count=exact" } }).catch(() => null);
|
|
117
|
+
results.supabase.usersWithPaidPosts = getCount(paidUsersRes);
|
|
118
|
+
}
|
|
119
|
+
// Stripe stats
|
|
120
|
+
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
121
|
+
if (stripeKey) {
|
|
122
|
+
try {
|
|
123
|
+
// Balance
|
|
124
|
+
const balRes = await fetch("https://api.stripe.com/v1/balance", {
|
|
125
|
+
headers: { Authorization: `Bearer ${stripeKey}` },
|
|
126
|
+
});
|
|
127
|
+
const balData = await balRes.json();
|
|
128
|
+
// Recent charges
|
|
129
|
+
const chargesRes = await fetch("https://api.stripe.com/v1/charges?limit=100", {
|
|
130
|
+
headers: { Authorization: `Bearer ${stripeKey}` },
|
|
131
|
+
});
|
|
132
|
+
const chargesData = await chargesRes.json();
|
|
133
|
+
const charges = chargesData.data ?? [];
|
|
134
|
+
const totalRevenue = charges.reduce((sum, c) => sum + (c.status === "succeeded" ? c.amount : 0), 0);
|
|
135
|
+
results.stripe = {
|
|
136
|
+
balance: balData.available?.map((b) => `${(b.amount / 100).toFixed(2)} ${b.currency.toUpperCase()}`) ?? [],
|
|
137
|
+
totalCharges: charges.length,
|
|
138
|
+
successfulCharges: charges.filter((c) => c.status === "succeeded").length,
|
|
139
|
+
totalRevenue: `${(totalRevenue / 100).toFixed(2)}`,
|
|
140
|
+
mode: stripeKey.startsWith("sk_live") ? "live" : "test",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
results.stripe = { error: err instanceof Error ? err.message : String(err) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: JSON.stringify({ analytics: results }, null, 2) }],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
// ──────────────────────────────────────────
|
|
152
|
+
// Tool: stk_perf
|
|
153
|
+
// ──────────────────────────────────────────
|
|
154
|
+
server.tool("stk_perf", "Check performance across your stack: Supabase query latency, table sizes, Vercel deploy times, and API response times.", {
|
|
155
|
+
tables: z.array(z.string()).optional().describe("Specific Supabase tables to benchmark (defaults to all detected)"),
|
|
156
|
+
}, async ({ tables }) => {
|
|
157
|
+
const perf = {};
|
|
158
|
+
// Supabase performance
|
|
159
|
+
const url = process.env.SUPABASE_URL;
|
|
160
|
+
const key = process.env.SUPABASE_SERVICE_KEY;
|
|
161
|
+
if (url && key) {
|
|
162
|
+
const headers = {
|
|
163
|
+
apikey: key,
|
|
164
|
+
Authorization: `Bearer ${key}`,
|
|
165
|
+
Prefer: "count=exact",
|
|
166
|
+
};
|
|
167
|
+
// Auto-detect tables or use provided list
|
|
168
|
+
const tablesToCheck = tables ?? ["posts", "users", "payments"];
|
|
169
|
+
const tableStats = [];
|
|
170
|
+
for (const table of tablesToCheck) {
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`${url}/rest/v1/${table}?select=id&limit=0`, {
|
|
174
|
+
headers,
|
|
175
|
+
});
|
|
176
|
+
const latency = Date.now() - start;
|
|
177
|
+
const range = res.headers.get("content-range");
|
|
178
|
+
const count = range ? parseInt(range.split("/")[1]) || 0 : null;
|
|
179
|
+
let status = "fast";
|
|
180
|
+
if (latency > 1000)
|
|
181
|
+
status = "slow";
|
|
182
|
+
else if (latency > 500)
|
|
183
|
+
status = "moderate";
|
|
184
|
+
tableStats.push({ table, rowCount: count, queryLatency: latency, status });
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
tableStats.push({ table, rowCount: null, queryLatency: Date.now() - start, status: "error" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// API latency test
|
|
191
|
+
const apiStart = Date.now();
|
|
192
|
+
await fetch(`${url}/rest/v1/`, { headers }).catch(() => null);
|
|
193
|
+
const apiLatency = Date.now() - apiStart;
|
|
194
|
+
perf.supabase = {
|
|
195
|
+
apiLatency,
|
|
196
|
+
tables: tableStats,
|
|
197
|
+
recommendation: tableStats.some((t) => t.status === "slow")
|
|
198
|
+
? "Some queries are slow. Consider adding database indexes."
|
|
199
|
+
: tableStats.some((t) => (t.rowCount ?? 0) > 10000)
|
|
200
|
+
? "Large tables detected. Ensure you have indexes on frequently queried columns."
|
|
201
|
+
: "Performance looks good.",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Vercel deploy performance
|
|
205
|
+
if (process.env.VERCEL_TOKEN) {
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch("https://api.vercel.com/v6/deployments?limit=5", {
|
|
208
|
+
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
|
209
|
+
});
|
|
210
|
+
const data = await res.json();
|
|
211
|
+
const deploys = (data.deployments ?? []).map((d) => {
|
|
212
|
+
const buildDuration = d.buildingAt && d.ready
|
|
213
|
+
? Math.round((d.ready - d.buildingAt) / 1000)
|
|
214
|
+
: null;
|
|
215
|
+
return {
|
|
216
|
+
id: d.uid,
|
|
217
|
+
state: d.readyState ?? d.state,
|
|
218
|
+
buildDuration: buildDuration ? `${buildDuration}s` : "unknown",
|
|
219
|
+
created: new Date(d.created).toISOString(),
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
perf.vercel = {
|
|
223
|
+
recentDeploys: deploys,
|
|
224
|
+
avgBuildTime: deploys.filter((d) => d.buildDuration !== "unknown").length > 0
|
|
225
|
+
? "see individual deploys"
|
|
226
|
+
: "no build data available",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch { /* skip */ }
|
|
230
|
+
}
|
|
231
|
+
// Stripe API latency
|
|
232
|
+
if (process.env.STRIPE_SECRET_KEY) {
|
|
233
|
+
const start = Date.now();
|
|
234
|
+
await fetch("https://api.stripe.com/v1/balance", {
|
|
235
|
+
headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
|
|
236
|
+
}).catch(() => null);
|
|
237
|
+
perf.stripe = { apiLatency: Date.now() - start };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: JSON.stringify({ performance: perf }, null, 2) }],
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
// ──────────────────────────────────────────
|
|
244
|
+
// Tool: stk_cost
|
|
245
|
+
// ──────────────────────────────────────────
|
|
246
|
+
server.tool("stk_cost", "Track costs across your stack: Stripe fees, Vercel usage, Supabase plan details. Get a unified view of what you're spending.", {}, async () => {
|
|
247
|
+
const costs = {};
|
|
248
|
+
// Stripe revenue & fees
|
|
249
|
+
if (process.env.STRIPE_SECRET_KEY) {
|
|
250
|
+
try {
|
|
251
|
+
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
252
|
+
// Balance
|
|
253
|
+
const balRes = await fetch("https://api.stripe.com/v1/balance", {
|
|
254
|
+
headers: { Authorization: `Bearer ${stripeKey}` },
|
|
255
|
+
});
|
|
256
|
+
const balData = await balRes.json();
|
|
257
|
+
// Recent balance transactions for fee tracking
|
|
258
|
+
const txRes = await fetch("https://api.stripe.com/v1/balance_transactions?limit=100", {
|
|
259
|
+
headers: { Authorization: `Bearer ${stripeKey}` },
|
|
260
|
+
});
|
|
261
|
+
const txData = await txRes.json();
|
|
262
|
+
const transactions = txData.data ?? [];
|
|
263
|
+
const totalFees = transactions.reduce((sum, t) => sum + (t.fee || 0), 0);
|
|
264
|
+
const totalGross = transactions.reduce((sum, t) => sum + (t.amount > 0 ? t.amount : 0), 0);
|
|
265
|
+
const totalNet = transactions.reduce((sum, t) => sum + (t.net || 0), 0);
|
|
266
|
+
costs.stripe = {
|
|
267
|
+
mode: stripeKey.startsWith("sk_live") ? "live" : "test",
|
|
268
|
+
balance: balData.available?.map((b) => ({
|
|
269
|
+
amount: (b.amount / 100).toFixed(2),
|
|
270
|
+
currency: b.currency.toUpperCase(),
|
|
271
|
+
})) ?? [],
|
|
272
|
+
recentTransactions: transactions.length,
|
|
273
|
+
totalGross: (totalGross / 100).toFixed(2),
|
|
274
|
+
totalFees: (totalFees / 100).toFixed(2),
|
|
275
|
+
totalNet: (totalNet / 100).toFixed(2),
|
|
276
|
+
feePercentage: totalGross > 0 ? ((totalFees / totalGross) * 100).toFixed(1) + "%" : "N/A",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
costs.stripe = { error: err instanceof Error ? err.message : String(err) };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Vercel usage
|
|
284
|
+
if (process.env.VERCEL_TOKEN) {
|
|
285
|
+
try {
|
|
286
|
+
// Get team/user info for billing context
|
|
287
|
+
const userRes = await fetch("https://api.vercel.com/v2/user", {
|
|
288
|
+
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
|
289
|
+
});
|
|
290
|
+
const userData = await userRes.json();
|
|
291
|
+
// Count deployments
|
|
292
|
+
const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=100", {
|
|
293
|
+
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
|
|
294
|
+
});
|
|
295
|
+
const depData = await depRes.json();
|
|
296
|
+
const deployments = depData.deployments ?? [];
|
|
297
|
+
// Deployments this month
|
|
298
|
+
const monthStart = new Date();
|
|
299
|
+
monthStart.setDate(1);
|
|
300
|
+
monthStart.setHours(0, 0, 0, 0);
|
|
301
|
+
const thisMonth = deployments.filter((d) => new Date(d.created) >= monthStart);
|
|
302
|
+
costs.vercel = {
|
|
303
|
+
plan: userData.user?.billing?.plan ?? "unknown",
|
|
304
|
+
deploymentsThisMonth: thisMonth.length,
|
|
305
|
+
totalDeployments: deployments.length,
|
|
306
|
+
note: "Vercel free tier includes 100 deployments/day. Check vercel.com/dashboard for detailed billing.",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
catch { /* skip */ }
|
|
310
|
+
}
|
|
311
|
+
// Supabase usage estimate
|
|
312
|
+
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY) {
|
|
313
|
+
const url = process.env.SUPABASE_URL;
|
|
314
|
+
const key = process.env.SUPABASE_SERVICE_KEY;
|
|
315
|
+
const headers = {
|
|
316
|
+
apikey: key,
|
|
317
|
+
Authorization: `Bearer ${key}`,
|
|
318
|
+
Prefer: "count=exact",
|
|
319
|
+
};
|
|
320
|
+
const tableCounts = {};
|
|
321
|
+
for (const table of ["posts", "users", "payments"]) {
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(`${url}/rest/v1/${table}?select=id&limit=0`, { headers });
|
|
324
|
+
const range = res.headers.get("content-range");
|
|
325
|
+
tableCounts[table] = range ? parseInt(range.split("/")[1]) || 0 : null;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
tableCounts[table] = null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const totalRows = Object.values(tableCounts).reduce((s, v) => s + (v ?? 0), 0);
|
|
332
|
+
costs.supabase = {
|
|
333
|
+
tables: tableCounts,
|
|
334
|
+
totalRows,
|
|
335
|
+
note: "Supabase free tier: 500MB database, 1GB storage, 50k monthly active users. Check supabase.com/dashboard for detailed usage.",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: JSON.stringify({ costs }, null, 2) }],
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
// ──────────────────────────────────────────
|
|
343
|
+
// Tool: stk_metrics
|
|
344
|
+
// ──────────────────────────────────────────
|
|
345
|
+
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.", {
|
|
346
|
+
action: z.enum(["view", "record", "compare"]).optional().default("view"),
|
|
347
|
+
type: z.enum(["deploy", "health_check", "error", "response_time"]).optional(),
|
|
348
|
+
value: z.number().optional(),
|
|
349
|
+
metadata: z.record(z.string(), z.string()).optional(),
|
|
350
|
+
days: z.number().optional().default(7),
|
|
351
|
+
}, async ({ action, type, value, metadata, days }) => {
|
|
352
|
+
if (action === "record" && type && value !== undefined) {
|
|
353
|
+
recordMetric(type, value, metadata ?? {});
|
|
354
|
+
return { content: [{ type: "text", text: JSON.stringify({ recorded: true, type, value }) }] };
|
|
355
|
+
}
|
|
356
|
+
if (action === "compare") {
|
|
357
|
+
const types = ["deploy", "health_check", "error", "response_time"];
|
|
358
|
+
const comparisons = types.map(t => ({ type: t, ...compareToBaseline(t) }));
|
|
359
|
+
const degraded = comparisons.filter(c => c.status === "degraded");
|
|
360
|
+
return {
|
|
361
|
+
content: [{
|
|
362
|
+
type: "text",
|
|
363
|
+
text: JSON.stringify({
|
|
364
|
+
comparisons,
|
|
365
|
+
degraded: degraded.length > 0 ? degraded : "none",
|
|
366
|
+
summary: `${degraded.length} metric(s) degraded`,
|
|
367
|
+
}, null, 2),
|
|
368
|
+
}],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Default: view
|
|
372
|
+
return {
|
|
373
|
+
content: [{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: JSON.stringify({
|
|
376
|
+
period: `${days} days`,
|
|
377
|
+
deploys: getDeployFrequency(days),
|
|
378
|
+
errors: getErrorRate(days),
|
|
379
|
+
uptime: getUptime(days),
|
|
380
|
+
recentMetrics: getMetrics(type ?? undefined, days, 20),
|
|
381
|
+
}, null, 2),
|
|
382
|
+
}],
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
} // end registerDataTools
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadConfig } from "../../lib/config.js";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
function detectGitHubRepo() {
|
|
5
|
+
try {
|
|
6
|
+
const url = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7
|
+
const match = url.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
8
|
+
return match?.[1] ?? null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function registerGithubTools(server) {
|
|
15
|
+
// ──────────────────────────────────────────
|
|
16
|
+
// Tool: stk_todo_list
|
|
17
|
+
// ──────────────────────────────────────────
|
|
18
|
+
server.tool("stk_todo_list", "List open GitHub issues for this project. Helps understand what needs to be worked on.", {
|
|
19
|
+
label: z.string().optional().describe("Filter by label"),
|
|
20
|
+
limit: z.number().optional().default(15).describe("Max issues to return"),
|
|
21
|
+
}, async ({ label, limit }) => {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const repo = config.github?.repo ?? process.env.GITHUB_REPO ?? detectGitHubRepo();
|
|
24
|
+
const token = process.env.GITHUB_TOKEN;
|
|
25
|
+
if (!repo) {
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Could not detect GitHub repo. Set GITHUB_REPO or add github.repo to stk.config.json" }) }] };
|
|
27
|
+
}
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
state: "open",
|
|
30
|
+
per_page: String(limit),
|
|
31
|
+
sort: "updated",
|
|
32
|
+
direction: "desc",
|
|
33
|
+
});
|
|
34
|
+
if (label)
|
|
35
|
+
params.set("labels", label);
|
|
36
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
37
|
+
if (token)
|
|
38
|
+
headers.Authorization = `Bearer ${token}`;
|
|
39
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/issues?${params}`, { headers });
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `GitHub API: ${res.status}` }) }] };
|
|
42
|
+
}
|
|
43
|
+
const issues = (await res.json());
|
|
44
|
+
const filtered = issues
|
|
45
|
+
.filter((i) => !i.pull_request)
|
|
46
|
+
.map((i) => ({
|
|
47
|
+
number: i.number,
|
|
48
|
+
title: i.title,
|
|
49
|
+
labels: i.labels.map((l) => l.name),
|
|
50
|
+
assignee: i.assignee?.login ?? null,
|
|
51
|
+
created: i.created_at,
|
|
52
|
+
url: i.html_url,
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify({ repo, issues: filtered }, null, 2) }],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// ──────────────────────────────────────────
|
|
59
|
+
// Tool: stk_todo_add
|
|
60
|
+
// ──────────────────────────────────────────
|
|
61
|
+
server.tool("stk_todo_add", "Create a new GitHub issue for this project.", {
|
|
62
|
+
title: z.string().describe("Issue title"),
|
|
63
|
+
body: z.string().optional().describe("Issue body/description"),
|
|
64
|
+
labels: z.array(z.string()).optional().describe("Labels to add"),
|
|
65
|
+
}, async ({ title, body, labels }) => {
|
|
66
|
+
const config = loadConfig();
|
|
67
|
+
const repo = config.github?.repo ?? process.env.GITHUB_REPO ?? detectGitHubRepo();
|
|
68
|
+
const token = process.env.GITHUB_TOKEN;
|
|
69
|
+
if (!repo || !token) {
|
|
70
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Need GITHUB_TOKEN and repo to create issues" }) }] };
|
|
71
|
+
}
|
|
72
|
+
const payload = { title };
|
|
73
|
+
if (body)
|
|
74
|
+
payload.body = body;
|
|
75
|
+
if (labels)
|
|
76
|
+
payload.labels = labels;
|
|
77
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/issues`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
Accept: "application/vnd.github+json",
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(payload),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const data = (await res.json());
|
|
88
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: data.message ?? `HTTP ${res.status}` }) }] };
|
|
89
|
+
}
|
|
90
|
+
const issue = (await res.json());
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: JSON.stringify({ created: true, number: issue.number, url: issue.html_url }, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
} // end registerGithubTools
|