postgresai 0.15.0-dev.1 → 0.15.0-dev.11
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/README.md +82 -9
- package/bin/postgres-ai.ts +813 -233
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +6193 -1059
- package/instances.demo.yml +14 -0
- package/lib/checkup-api.ts +25 -6
- package/lib/checkup-dictionary.ts +0 -11
- package/lib/checkup.ts +255 -24
- package/lib/config.ts +3 -0
- package/lib/init.ts +197 -5
- package/lib/instances.ts +245 -0
- package/lib/issues.ts +72 -72
- package/lib/mcp-server.ts +229 -18
- package/lib/metrics-loader.ts +6 -4
- package/lib/reports.ts +373 -0
- package/lib/storage.ts +367 -0
- package/lib/supabase.ts +8 -1
- package/lib/util.ts +7 -1
- package/package.json +4 -4
- package/scripts/embed-checkup-dictionary.ts +9 -0
- package/scripts/embed-metrics.ts +2 -0
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +139 -0
- package/test/checkup.test.ts +1316 -2
- package/test/compose-cmd.test.ts +120 -0
- package/test/config-consistency.test.ts +321 -5
- package/test/init.integration.test.ts +27 -28
- package/test/init.test.ts +534 -6
- package/test/issues.cli.test.ts +625 -1
- package/test/mcp-server.test.ts +944 -2
- package/test/monitoring.test.ts +355 -0
- package/test/permission-check-sql.test.ts +116 -0
- package/test/reports.cli.test.ts +793 -0
- package/test/reports.test.ts +977 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/storage.test.ts +935 -0
- package/test/test-utils.ts +51 -2
- package/test/upgrade.test.ts +422 -0
package/lib/mcp-server.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
updateActionItem,
|
|
15
15
|
type ConfigChange,
|
|
16
16
|
} from "./issues";
|
|
17
|
+
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, parseFlexibleDate } from "./reports";
|
|
18
|
+
import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "./storage";
|
|
17
19
|
import { resolveBaseUrls } from "./util";
|
|
18
20
|
|
|
19
21
|
// MCP SDK imports - Bun handles these directly
|
|
@@ -106,13 +108,19 @@ export async function handleToolCall(
|
|
|
106
108
|
const issueId = String(args.issue_id || "").trim();
|
|
107
109
|
const rawContent = String(args.content || "");
|
|
108
110
|
const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
|
|
111
|
+
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
109
112
|
if (!issueId) {
|
|
110
113
|
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
111
114
|
}
|
|
112
|
-
if (!rawContent) {
|
|
113
|
-
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
115
|
+
if (!rawContent && attachments.length === 0) {
|
|
116
|
+
return { content: [{ type: "text", text: "content or attachments is required" }], isError: true };
|
|
117
|
+
}
|
|
118
|
+
let content = interpretEscapes(rawContent);
|
|
119
|
+
if (attachments.length > 0) {
|
|
120
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
121
|
+
const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
122
|
+
content = appendAttachmentsToContent(content, uploaded);
|
|
114
123
|
}
|
|
115
|
-
const content = interpretEscapes(rawContent);
|
|
116
124
|
const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
|
|
117
125
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
118
126
|
}
|
|
@@ -124,15 +132,21 @@ export async function handleToolCall(
|
|
|
124
132
|
}
|
|
125
133
|
const title = interpretEscapes(rawTitle);
|
|
126
134
|
const rawDescription = args.description ? String(args.description) : undefined;
|
|
127
|
-
|
|
135
|
+
let description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
128
136
|
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
|
|
129
137
|
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
138
|
+
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
130
139
|
// Get orgId from args or fall back to config
|
|
131
140
|
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId;
|
|
132
141
|
// Note: orgId=0 is technically valid (though unlikely), so don't use falsy check
|
|
133
142
|
if (orgId === undefined || orgId === null || Number.isNaN(orgId)) {
|
|
134
143
|
return { content: [{ type: "text", text: "org_id is required. Either provide it as a parameter or run 'pgai auth' to set it in config." }], isError: true };
|
|
135
144
|
}
|
|
145
|
+
if (attachments.length > 0) {
|
|
146
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
147
|
+
const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
148
|
+
description = appendAttachmentsToContent(description ?? "", uploaded);
|
|
149
|
+
}
|
|
136
150
|
const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug });
|
|
137
151
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
138
152
|
}
|
|
@@ -145,17 +159,33 @@ export async function handleToolCall(
|
|
|
145
159
|
const rawTitle = args.title !== undefined ? String(args.title) : undefined;
|
|
146
160
|
const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
|
|
147
161
|
const rawDescription = args.description !== undefined ? String(args.description) : undefined;
|
|
148
|
-
|
|
162
|
+
let description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
149
163
|
const status = args.status !== undefined ? Number(args.status) : undefined;
|
|
150
164
|
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
166
|
+
// Validate that at least one update field is provided (attachments alone counts)
|
|
167
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined && attachments.length === 0) {
|
|
168
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, or attachments)" }], isError: true };
|
|
154
169
|
}
|
|
155
170
|
// Validate status value if provided (check for NaN and valid values)
|
|
156
171
|
if (status !== undefined && (Number.isNaN(status) || (status !== 0 && status !== 1))) {
|
|
157
172
|
return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
|
|
158
173
|
}
|
|
174
|
+
if (attachments.length > 0) {
|
|
175
|
+
// If the caller did not supply a new description, fetch the existing one
|
|
176
|
+
// and append to it so "add a screenshot to issue X" is one round-trip
|
|
177
|
+
// for the agent. Same race-window tradeoff as the CLI flag.
|
|
178
|
+
if (description === undefined) {
|
|
179
|
+
const existing = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
180
|
+
if (!existing) {
|
|
181
|
+
return { content: [{ type: "text", text: `Issue not found: ${issueId}` }], isError: true };
|
|
182
|
+
}
|
|
183
|
+
description = (existing as { description?: string | null }).description ?? "";
|
|
184
|
+
}
|
|
185
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
186
|
+
const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
187
|
+
description = appendAttachmentsToContent(description, uploaded);
|
|
188
|
+
}
|
|
159
189
|
const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, debug });
|
|
160
190
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
161
191
|
}
|
|
@@ -163,17 +193,62 @@ export async function handleToolCall(
|
|
|
163
193
|
if (toolName === "update_issue_comment") {
|
|
164
194
|
const commentId = String(args.comment_id || "").trim();
|
|
165
195
|
const rawContent = String(args.content || "");
|
|
196
|
+
const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
|
|
166
197
|
if (!commentId) {
|
|
167
198
|
return { content: [{ type: "text", text: "comment_id is required" }], isError: true };
|
|
168
199
|
}
|
|
169
|
-
if (!rawContent.trim()) {
|
|
170
|
-
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
200
|
+
if (!rawContent.trim() && attachments.length === 0) {
|
|
201
|
+
return { content: [{ type: "text", text: "content or attachments is required" }], isError: true };
|
|
202
|
+
}
|
|
203
|
+
let content = interpretEscapes(rawContent);
|
|
204
|
+
if (attachments.length > 0) {
|
|
205
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
206
|
+
const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
|
|
207
|
+
content = appendAttachmentsToContent(content, uploaded);
|
|
171
208
|
}
|
|
172
|
-
const content = interpretEscapes(rawContent);
|
|
173
209
|
const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
|
|
174
210
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
175
211
|
}
|
|
176
212
|
|
|
213
|
+
if (toolName === "upload_file") {
|
|
214
|
+
const filePath = String(args.path || "").trim();
|
|
215
|
+
if (!filePath) {
|
|
216
|
+
return { content: [{ type: "text", text: "path is required" }], isError: true };
|
|
217
|
+
}
|
|
218
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
219
|
+
const result = await uploadFile({ apiKey, storageBaseUrl, filePath, debug });
|
|
220
|
+
const markdown = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify(
|
|
226
|
+
{
|
|
227
|
+
success: result.success,
|
|
228
|
+
url: result.url,
|
|
229
|
+
markdown,
|
|
230
|
+
metadata: result.metadata,
|
|
231
|
+
requestId: result.requestId,
|
|
232
|
+
},
|
|
233
|
+
null,
|
|
234
|
+
2
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (toolName === "download_file") {
|
|
242
|
+
const fileUrl = String(args.url || "").trim();
|
|
243
|
+
const outputPath = args.output_path !== undefined ? String(args.output_path) : undefined;
|
|
244
|
+
if (!fileUrl) {
|
|
245
|
+
return { content: [{ type: "text", text: "url is required" }], isError: true };
|
|
246
|
+
}
|
|
247
|
+
const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
248
|
+
const result = await downloadFile({ apiKey, storageBaseUrl, fileUrl, outputPath, debug });
|
|
249
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
250
|
+
}
|
|
251
|
+
|
|
177
252
|
// Action Items Tools
|
|
178
253
|
if (toolName === "view_action_item") {
|
|
179
254
|
// Support both single ID and array of IDs
|
|
@@ -250,6 +325,50 @@ export async function handleToolCall(
|
|
|
250
325
|
return { content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }] };
|
|
251
326
|
}
|
|
252
327
|
|
|
328
|
+
// Reports Tools
|
|
329
|
+
if (toolName === "list_reports") {
|
|
330
|
+
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
|
|
331
|
+
const status = args.status ? String(args.status) : undefined;
|
|
332
|
+
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
|
|
333
|
+
const beforeDate = args.before_date ? parseFlexibleDate(String(args.before_date)) : undefined;
|
|
334
|
+
const all = args.all === true;
|
|
335
|
+
let reports;
|
|
336
|
+
if (all) {
|
|
337
|
+
reports = await fetchAllReports({ apiKey, apiBaseUrl, projectId, status, limit, debug });
|
|
338
|
+
} else {
|
|
339
|
+
reports = await fetchReports({ apiKey, apiBaseUrl, projectId, status, limit, beforeDate, debug });
|
|
340
|
+
}
|
|
341
|
+
return { content: [{ type: "text", text: JSON.stringify(reports, null, 2) }] };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (toolName === "list_report_files") {
|
|
345
|
+
const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
|
|
346
|
+
if (reportId !== undefined && isNaN(reportId)) {
|
|
347
|
+
return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
|
|
348
|
+
}
|
|
349
|
+
const type = args.type ? String(args.type) as "json" | "md" : undefined;
|
|
350
|
+
const checkId = args.check_id ? String(args.check_id) : undefined;
|
|
351
|
+
if (reportId === undefined && !checkId) {
|
|
352
|
+
return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
|
|
353
|
+
}
|
|
354
|
+
const files = await fetchReportFiles({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
|
|
355
|
+
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (toolName === "get_report_data") {
|
|
359
|
+
const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
|
|
360
|
+
if (reportId !== undefined && isNaN(reportId)) {
|
|
361
|
+
return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
const type = args.type ? String(args.type) as "json" | "md" : undefined;
|
|
364
|
+
const checkId = args.check_id ? String(args.check_id) : undefined;
|
|
365
|
+
if (reportId === undefined && !checkId) {
|
|
366
|
+
return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
|
|
367
|
+
}
|
|
368
|
+
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
|
|
369
|
+
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
370
|
+
}
|
|
371
|
+
|
|
253
372
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
254
373
|
} catch (err) {
|
|
255
374
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -300,22 +419,27 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
300
419
|
},
|
|
301
420
|
{
|
|
302
421
|
name: "post_issue_comment",
|
|
303
|
-
description: "Post a new comment to an issue (optionally as a reply)",
|
|
422
|
+
description: "Post a new comment to an issue (optionally as a reply). Local files passed via 'attachments' are uploaded to PostgresAI storage and the resulting markdown links are appended to the comment body (image extensions render inline).",
|
|
304
423
|
inputSchema: {
|
|
305
424
|
type: "object",
|
|
306
425
|
properties: {
|
|
307
426
|
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
308
427
|
content: { type: "string", description: "Comment text (supports \\n as newline)" },
|
|
309
428
|
parent_comment_id: { type: "string", description: "Parent comment ID (UUID) for replies" },
|
|
429
|
+
attachments: {
|
|
430
|
+
type: "array",
|
|
431
|
+
items: { type: "string" },
|
|
432
|
+
description: "Local file paths to upload and append as markdown links (images render inline). Either 'content' or 'attachments' must be non-empty.",
|
|
433
|
+
},
|
|
310
434
|
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
311
435
|
},
|
|
312
|
-
required: ["issue_id"
|
|
436
|
+
required: ["issue_id"],
|
|
313
437
|
additionalProperties: false,
|
|
314
438
|
},
|
|
315
439
|
},
|
|
316
440
|
{
|
|
317
441
|
name: "create_issue",
|
|
318
|
-
description: "Create a new issue in PostgresAI",
|
|
442
|
+
description: "Create a new issue in PostgresAI. Local files passed via 'attachments' are uploaded to PostgresAI storage and the resulting markdown links are appended to the issue description.",
|
|
319
443
|
inputSchema: {
|
|
320
444
|
type: "object",
|
|
321
445
|
properties: {
|
|
@@ -328,6 +452,11 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
328
452
|
items: { type: "string" },
|
|
329
453
|
description: "Labels to apply to the issue",
|
|
330
454
|
},
|
|
455
|
+
attachments: {
|
|
456
|
+
type: "array",
|
|
457
|
+
items: { type: "string" },
|
|
458
|
+
description: "Local file paths to upload and append as markdown links to the description (images render inline)",
|
|
459
|
+
},
|
|
331
460
|
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
332
461
|
},
|
|
333
462
|
required: ["title"],
|
|
@@ -336,7 +465,7 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
336
465
|
},
|
|
337
466
|
{
|
|
338
467
|
name: "update_issue",
|
|
339
|
-
description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen.",
|
|
468
|
+
description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
|
|
340
469
|
inputSchema: {
|
|
341
470
|
type: "object",
|
|
342
471
|
properties: {
|
|
@@ -349,6 +478,11 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
349
478
|
items: { type: "string" },
|
|
350
479
|
description: "Labels to set on the issue",
|
|
351
480
|
},
|
|
481
|
+
attachments: {
|
|
482
|
+
type: "array",
|
|
483
|
+
items: { type: "string" },
|
|
484
|
+
description: "Local file paths to upload and append as markdown links (images render inline). When provided without 'description', the existing description is fetched and appended to.",
|
|
485
|
+
},
|
|
352
486
|
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
353
487
|
},
|
|
354
488
|
required: ["issue_id"],
|
|
@@ -357,15 +491,47 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
357
491
|
},
|
|
358
492
|
{
|
|
359
493
|
name: "update_issue_comment",
|
|
360
|
-
description: "Update an existing issue comment",
|
|
494
|
+
description: "Update an existing issue comment. Local files passed via 'attachments' are uploaded and appended to 'content' as markdown links.",
|
|
361
495
|
inputSchema: {
|
|
362
496
|
type: "object",
|
|
363
497
|
properties: {
|
|
364
498
|
comment_id: { type: "string", description: "Comment ID (UUID)" },
|
|
365
|
-
content: { type: "string", description: "New comment text (supports \\n as newline)" },
|
|
499
|
+
content: { type: "string", description: "New comment text (supports \\n as newline). Either 'content' or 'attachments' must be non-empty." },
|
|
500
|
+
attachments: {
|
|
501
|
+
type: "array",
|
|
502
|
+
items: { type: "string" },
|
|
503
|
+
description: "Local file paths to upload and append as markdown links (images render inline)",
|
|
504
|
+
},
|
|
505
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
506
|
+
},
|
|
507
|
+
required: ["comment_id"],
|
|
508
|
+
additionalProperties: false,
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "upload_file",
|
|
513
|
+
description: "Upload a local file to PostgresAI storage. Returns the storage URL and a ready-to-paste markdown link (image extensions get the inline `` form, others get `[](url)`). For posting attachments alongside an issue or comment, prefer the 'attachments' parameter on the issue/comment tools — this tool is for ad-hoc uploads or when you need the URL out of band.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: "object",
|
|
516
|
+
properties: {
|
|
517
|
+
path: { type: "string", description: "Local file path to upload (absolute or relative to CWD)" },
|
|
518
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
519
|
+
},
|
|
520
|
+
required: ["path"],
|
|
521
|
+
additionalProperties: false,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "download_file",
|
|
526
|
+
description: "Download a file from PostgresAI storage by URL (full URL under the storage base, or a relative path like /files/123/foo.png).",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
url: { type: "string", description: "Full URL (must be under the configured storage base) or relative storage path (e.g. /files/123/foo.png)" },
|
|
531
|
+
output_path: { type: "string", description: "Local destination path (default: derive filename from URL, save in CWD). When omitted, the path-traversal guard restricts the destination to CWD." },
|
|
366
532
|
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
367
533
|
},
|
|
368
|
-
required: ["
|
|
534
|
+
required: ["url"],
|
|
369
535
|
additionalProperties: false,
|
|
370
536
|
},
|
|
371
537
|
},
|
|
@@ -442,6 +608,51 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
442
608
|
additionalProperties: false,
|
|
443
609
|
},
|
|
444
610
|
},
|
|
611
|
+
// Reports Tools
|
|
612
|
+
{
|
|
613
|
+
name: "list_reports",
|
|
614
|
+
description: "List checkup reports. Returns report metadata: id, project, status, timestamps. Use get_report_data to fetch actual report content. Supports date-based filtering with before_date.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
properties: {
|
|
618
|
+
project_id: { type: "number", description: "Filter by project ID" },
|
|
619
|
+
status: { type: "string", description: "Filter by status (e.g., 'completed')" },
|
|
620
|
+
limit: { type: "number", description: "Max number of reports to return (default: 20)" },
|
|
621
|
+
before_date: { type: "string", description: "Show reports created before this date (YYYY-MM-DD, DD.MM.YYYY, YYYY-MM-DD HH:mm, etc.)" },
|
|
622
|
+
all: { type: "boolean", description: "Fetch all reports (paginated automatically)" },
|
|
623
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
624
|
+
},
|
|
625
|
+
additionalProperties: false,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "list_report_files",
|
|
630
|
+
description: "List files in a checkup report (metadata only, no content). Each report contains json (raw data) and md (markdown analysis) files per check. Either report_id or check_id must be provided.",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
|
|
635
|
+
type: { type: "string", description: "Filter by file type: 'json' or 'md'" },
|
|
636
|
+
check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
|
|
637
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
638
|
+
},
|
|
639
|
+
additionalProperties: false,
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "get_report_data",
|
|
644
|
+
description: "Get checkup report file content. Returns files with a 'data' field containing the actual content: markdown analysis or JSON raw data. Use type='md' for human-readable analysis with recommendations, type='json' for raw check data. Either report_id or check_id must be provided.",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: "object",
|
|
647
|
+
properties: {
|
|
648
|
+
report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
|
|
649
|
+
type: { type: "string", description: "Filter by file type: 'json' for raw data, 'md' for markdown analysis" },
|
|
650
|
+
check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
|
|
651
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
652
|
+
},
|
|
653
|
+
additionalProperties: false,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
445
656
|
],
|
|
446
657
|
};
|
|
447
658
|
});
|
package/lib/metrics-loader.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Metrics loader for express checkup reports
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Loads SQL queries from embedded metrics data (generated from metrics.yml at build time).
|
|
5
5
|
* Provides version-aware query selection and row transformation utilities.
|
|
6
6
|
*/
|
|
@@ -9,7 +9,7 @@ import { METRICS, MetricDefinition } from "./metrics-embedded";
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Get SQL query for a specific metric, selecting the appropriate version.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @param metricName - Name of the metric (e.g., "settings", "db_stats")
|
|
14
14
|
* @param pgMajorVersion - PostgreSQL major version (default: 16)
|
|
15
15
|
* @returns SQL query string
|
|
@@ -41,7 +41,7 @@ export function getMetricSql(metricName: string, pgMajorVersion: number = 16): s
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Get metric definition including all metadata.
|
|
44
|
-
*
|
|
44
|
+
*
|
|
45
45
|
* @param metricName - Name of the metric
|
|
46
46
|
* @returns MetricDefinition or undefined if not found
|
|
47
47
|
*/
|
|
@@ -63,7 +63,7 @@ export function listMetricNames(): string[] {
|
|
|
63
63
|
export const METRIC_NAMES = {
|
|
64
64
|
// Index health checks
|
|
65
65
|
H001: "pg_invalid_indexes",
|
|
66
|
-
H002: "unused_indexes",
|
|
66
|
+
H002: "unused_indexes",
|
|
67
67
|
H004: "redundant_indexes",
|
|
68
68
|
// Bloat estimation
|
|
69
69
|
F004: "pg_table_bloat",
|
|
@@ -75,6 +75,8 @@ export const METRIC_NAMES = {
|
|
|
75
75
|
dbSize: "db_size",
|
|
76
76
|
// Stats reset info (H002)
|
|
77
77
|
statsReset: "stats_reset",
|
|
78
|
+
// I/O statistics (I001) - PostgreSQL 16+
|
|
79
|
+
I001: "pg_stat_io",
|
|
78
80
|
} as const;
|
|
79
81
|
|
|
80
82
|
/**
|