postgresai 0.15.0-rc.1 → 0.15.0-rc.2
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 +79 -8
- package/bin/postgres-ai.ts +132 -17
- package/dist/bin/postgres-ai.js +692 -254
- package/lib/mcp-server.ts +139 -18
- package/lib/storage.ts +77 -1
- package/package.json +1 -1
- package/test/issues.cli.test.ts +395 -0
- package/test/mcp-server.test.ts +485 -2
- package/test/storage.test.ts +175 -1
package/README.md
CHANGED
|
@@ -249,22 +249,93 @@ Cursor configuration example (Settings → MCP):
|
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
Tools exposed:
|
|
252
|
-
- list_issues
|
|
253
|
-
- view_issue
|
|
254
|
-
-
|
|
252
|
+
- `list_issues`: returns the same JSON as `postgresai issues list`.
|
|
253
|
+
- `view_issue`: view a single issue with its comments (args: `{ issue_id, debug? }`).
|
|
254
|
+
- `create_issue`: create a new issue (args: `{ title, description?, org_id, attachments?, debug? }`).
|
|
255
|
+
- `update_issue`: update title/description/status/labels (args: `{ issue_id, title?, description?, status?, labels?, attachments?, debug? }`).
|
|
256
|
+
- `post_issue_comment`: post a comment (args: `{ issue_id, content?, parent_comment_id?, attachments?, debug? }`).
|
|
257
|
+
- `update_issue_comment`: update an existing comment (args: `{ comment_id, content?, attachments?, debug? }`).
|
|
258
|
+
- `upload_file`: upload a local file and return the storage URL plus a ready-to-paste markdown link (args: `{ path, debug? }`).
|
|
259
|
+
- `download_file`: download a file from storage (args: `{ url, output_path?, debug? }`).
|
|
260
|
+
|
|
261
|
+
#### `attachments` parameter (issue/comment tools)
|
|
262
|
+
|
|
263
|
+
`create_issue`, `update_issue`, `post_issue_comment`, and `update_issue_comment` accept an
|
|
264
|
+
optional `attachments: string[]` of local file paths. Each file is uploaded to PostgresAI
|
|
265
|
+
storage and the resulting markdown link is appended to the comment body or issue
|
|
266
|
+
description (image extensions — `.png .jpg .jpeg .gif .webp .svg .bmp .ico` — render
|
|
267
|
+
inline as ``; everything else as `[](url)`).
|
|
268
|
+
|
|
269
|
+
For `post_issue_comment` and `update_issue_comment`, either `content` or `attachments`
|
|
270
|
+
must be non-empty (attachments alone are allowed). For `update_issue` with `attachments`
|
|
271
|
+
but no `description`, the existing description is fetched first and the new links are
|
|
272
|
+
appended to it.
|
|
273
|
+
|
|
274
|
+
#### Threat model
|
|
275
|
+
|
|
276
|
+
The MCP server runs in your local user account with your PostgresAI API key. It
|
|
277
|
+
treats the connected MCP client (the LLM agent) as **trusted** — the same way the
|
|
278
|
+
CLI treats you when you type a command. In particular:
|
|
279
|
+
|
|
280
|
+
- `upload_file` and the `attachments: string[]` parameter on the issue/comment tools
|
|
281
|
+
read **any local file the CLI process can read**, including secrets like
|
|
282
|
+
`~/.ssh/id_rsa`, `~/.aws/credentials`, or `~/.config/postgresai/config.json` (which
|
|
283
|
+
contains your own API key). The file's bytes are uploaded to PostgresAI storage
|
|
284
|
+
and the resulting URL becomes visible to anyone with read access to the issue or
|
|
285
|
+
comment it ends up in.
|
|
286
|
+
- `download_file` writes to **any path the CLI process can write to** when
|
|
287
|
+
`output_path` is supplied (`~/.ssh/authorized_keys`, `~/.bashrc`, etc. are all
|
|
288
|
+
fair game). When `output_path` is omitted, downloads are restricted to the
|
|
289
|
+
current working directory.
|
|
290
|
+
|
|
291
|
+
This is fine when the agent and the upstream context the agent is reading are
|
|
292
|
+
trusted. It is **not** safe to run this MCP server against an agent that is
|
|
293
|
+
processing untrusted text (issue bodies, comments, web pages, third-party docs)
|
|
294
|
+
without additional sandboxing — a prompt-injection in any input the agent reads
|
|
295
|
+
could be used to exfiltrate local secrets or write arbitrary files. If you need
|
|
296
|
+
to expose this MCP server to such an agent, run the agent (and this server) in a
|
|
297
|
+
container or restricted user account that doesn't have access to anything
|
|
298
|
+
sensitive.
|
|
255
299
|
|
|
256
300
|
### Issues management (`issues` group)
|
|
257
301
|
|
|
258
302
|
```bash
|
|
259
|
-
postgresai issues list
|
|
260
|
-
postgresai issues view <issueId>
|
|
261
|
-
postgresai issues
|
|
262
|
-
#
|
|
263
|
-
|
|
303
|
+
postgresai issues list # List issues (shows: id, title, status, created_at)
|
|
304
|
+
postgresai issues view <issueId> # View issue details and comments
|
|
305
|
+
postgresai issues create --org-id <id> --title <t> # Create a new issue
|
|
306
|
+
postgresai issues update <issueId> [--title ... --status ...]# Update an existing issue
|
|
307
|
+
postgresai issues post-comment <issueId> <content> # Post a comment to an issue
|
|
308
|
+
postgresai issues update-comment <commentId> <content> # Update an existing comment
|
|
309
|
+
postgresai issues files upload <path> # Upload a file, print URL + markdown
|
|
310
|
+
postgresai issues files download <url> [-o <path>] # Download a file
|
|
311
|
+
# Common options:
|
|
312
|
+
# --parent <uuid> Parent comment ID (for replies on post-comment)
|
|
264
313
|
# --debug Enable debug output
|
|
265
314
|
# --json Output raw JSON (overrides default YAML)
|
|
266
315
|
```
|
|
267
316
|
|
|
317
|
+
#### Attaching files to issues and comments (`--attach`)
|
|
318
|
+
|
|
319
|
+
`create`, `update`, `post-comment`, and `update-comment` accept a repeatable
|
|
320
|
+
`--attach <path>` flag. Each file is uploaded to PostgresAI storage and a
|
|
321
|
+
markdown link is appended to the comment body (or issue description). Image
|
|
322
|
+
extensions — `.png .jpg .jpeg .gif .webp .svg .bmp .ico` — render inline as
|
|
323
|
+
``; everything else as `[](url)`. Multiple `--attach` flags preserve
|
|
324
|
+
order; each link goes on its own line.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Attach a screenshot to a new comment
|
|
328
|
+
postgresai issues post-comment <issueId> "Saw this in prod" --attach screenshot.png
|
|
329
|
+
|
|
330
|
+
# Attach multiple files to a new issue
|
|
331
|
+
postgresai issues create --org-id 4 --title "Slow query" \
|
|
332
|
+
--description "Plan attached" --attach plan.txt --attach flame.svg
|
|
333
|
+
|
|
334
|
+
# Attach a file to an existing issue without changing the description.
|
|
335
|
+
# The current description is fetched and the link is appended to it.
|
|
336
|
+
postgresai issues update <issueId> --attach trace.log
|
|
337
|
+
```
|
|
338
|
+
|
|
268
339
|
#### Output format for issues commands
|
|
269
340
|
|
|
270
341
|
By default, issues commands print human-friendly YAML when writing to a terminal. For scripting, you can:
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { startMcpServer } from "../lib/mcp-server";
|
|
|
14
14
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
|
|
15
15
|
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
|
|
16
16
|
import { resolveBaseUrls } from "../lib/util";
|
|
17
|
-
import { uploadFile, downloadFile, buildMarkdownLink } from "../lib/storage";
|
|
17
|
+
import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "../lib/storage";
|
|
18
18
|
import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, checkCurrentUserPermissions, connectWithSslFallback, DEFAULT_MONITORING_USER, formatPermissionCheckMessages, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
|
|
19
19
|
import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
|
|
20
20
|
import * as pkce from "../lib/pkce";
|
|
@@ -3915,9 +3915,18 @@ issues
|
|
|
3915
3915
|
.command("post-comment <issueId> <content>")
|
|
3916
3916
|
.description("post a new comment to an issue")
|
|
3917
3917
|
.option("--parent <uuid>", "parent comment id")
|
|
3918
|
+
.option(
|
|
3919
|
+
"--attach <path>",
|
|
3920
|
+
"attach a file (uploads to storage and appends a markdown link; repeatable)",
|
|
3921
|
+
(value: string, previous: string[]) => {
|
|
3922
|
+
previous.push(value);
|
|
3923
|
+
return previous;
|
|
3924
|
+
},
|
|
3925
|
+
[] as string[]
|
|
3926
|
+
)
|
|
3918
3927
|
.option("--debug", "enable debug output")
|
|
3919
3928
|
.option("--json", "output raw JSON")
|
|
3920
|
-
.action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
|
|
3929
|
+
.action(async (issueId: string, content: string, opts: { parent?: string; attach?: string[]; debug?: boolean; json?: boolean }) => {
|
|
3921
3930
|
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
3922
3931
|
if (opts.debug) {
|
|
3923
3932
|
// eslint-disable-next-line no-console
|
|
@@ -3929,7 +3938,11 @@ issues
|
|
|
3929
3938
|
console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
3930
3939
|
}
|
|
3931
3940
|
|
|
3932
|
-
const
|
|
3941
|
+
const attachPaths = Array.isArray(opts.attach) ? opts.attach : [];
|
|
3942
|
+
const spinner = createTtySpinner(
|
|
3943
|
+
process.stdout.isTTY ?? false,
|
|
3944
|
+
attachPaths.length > 0 ? "Uploading attachments..." : "Posting comment..."
|
|
3945
|
+
);
|
|
3933
3946
|
try {
|
|
3934
3947
|
const rootOpts = program.opts<CliOptions>();
|
|
3935
3948
|
const cfg = config.readConfig();
|
|
@@ -3941,13 +3954,25 @@ issues
|
|
|
3941
3954
|
return;
|
|
3942
3955
|
}
|
|
3943
3956
|
|
|
3944
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3957
|
+
const { apiBaseUrl, storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
3958
|
+
|
|
3959
|
+
let augmentedContent = content;
|
|
3960
|
+
if (attachPaths.length > 0) {
|
|
3961
|
+
const uploaded = await uploadAttachments({
|
|
3962
|
+
apiKey,
|
|
3963
|
+
storageBaseUrl,
|
|
3964
|
+
attachmentPaths: attachPaths,
|
|
3965
|
+
debug: !!opts.debug,
|
|
3966
|
+
});
|
|
3967
|
+
augmentedContent = appendAttachmentsToContent(content, uploaded);
|
|
3968
|
+
spinner.update("Posting comment...");
|
|
3969
|
+
}
|
|
3945
3970
|
|
|
3946
3971
|
const result = await createIssueComment({
|
|
3947
3972
|
apiKey,
|
|
3948
3973
|
apiBaseUrl,
|
|
3949
3974
|
issueId,
|
|
3950
|
-
content,
|
|
3975
|
+
content: augmentedContent,
|
|
3951
3976
|
parentCommentId: opts.parent,
|
|
3952
3977
|
debug: !!opts.debug,
|
|
3953
3978
|
});
|
|
@@ -3976,9 +4001,18 @@ issues
|
|
|
3976
4001
|
},
|
|
3977
4002
|
[] as string[]
|
|
3978
4003
|
)
|
|
4004
|
+
.option(
|
|
4005
|
+
"--attach <path>",
|
|
4006
|
+
"attach a file (uploads to storage and appends a markdown link to the description; repeatable)",
|
|
4007
|
+
(value: string, previous: string[]) => {
|
|
4008
|
+
previous.push(value);
|
|
4009
|
+
return previous;
|
|
4010
|
+
},
|
|
4011
|
+
[] as string[]
|
|
4012
|
+
)
|
|
3979
4013
|
.option("--debug", "enable debug output")
|
|
3980
4014
|
.option("--json", "output raw JSON")
|
|
3981
|
-
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
|
|
4015
|
+
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; attach?: string[]; debug?: boolean; json?: boolean }) => {
|
|
3982
4016
|
const rootOpts = program.opts<CliOptions>();
|
|
3983
4017
|
const cfg = config.readConfig();
|
|
3984
4018
|
const { apiKey } = getConfig(rootOpts);
|
|
@@ -4005,16 +4039,33 @@ issues
|
|
|
4005
4039
|
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
4006
4040
|
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
4007
4041
|
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
4042
|
+
const attachPaths = Array.isArray(opts.attach) ? opts.attach : [];
|
|
4008
4043
|
|
|
4009
|
-
const spinner = createTtySpinner(
|
|
4044
|
+
const spinner = createTtySpinner(
|
|
4045
|
+
process.stdout.isTTY ?? false,
|
|
4046
|
+
attachPaths.length > 0 ? "Uploading attachments..." : "Creating issue..."
|
|
4047
|
+
);
|
|
4010
4048
|
try {
|
|
4011
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4049
|
+
const { apiBaseUrl, storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4050
|
+
|
|
4051
|
+
let augmentedDescription = description;
|
|
4052
|
+
if (attachPaths.length > 0) {
|
|
4053
|
+
const uploaded = await uploadAttachments({
|
|
4054
|
+
apiKey,
|
|
4055
|
+
storageBaseUrl,
|
|
4056
|
+
attachmentPaths: attachPaths,
|
|
4057
|
+
debug: !!opts.debug,
|
|
4058
|
+
});
|
|
4059
|
+
augmentedDescription = appendAttachmentsToContent(description ?? "", uploaded);
|
|
4060
|
+
spinner.update("Creating issue...");
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4012
4063
|
const result = await createIssue({
|
|
4013
4064
|
apiKey,
|
|
4014
4065
|
apiBaseUrl,
|
|
4015
4066
|
title,
|
|
4016
4067
|
orgId,
|
|
4017
|
-
description,
|
|
4068
|
+
description: augmentedDescription,
|
|
4018
4069
|
projectId,
|
|
4019
4070
|
labels,
|
|
4020
4071
|
debug: !!opts.debug,
|
|
@@ -4045,9 +4096,18 @@ issues
|
|
|
4045
4096
|
[] as string[]
|
|
4046
4097
|
)
|
|
4047
4098
|
.option("--clear-labels", "set labels to an empty list")
|
|
4099
|
+
.option(
|
|
4100
|
+
"--attach <path>",
|
|
4101
|
+
"attach a file (uploads and appends a markdown link to --description; if --description is omitted the existing description is fetched and appended to; repeatable)",
|
|
4102
|
+
(value: string, previous: string[]) => {
|
|
4103
|
+
previous.push(value);
|
|
4104
|
+
return previous;
|
|
4105
|
+
},
|
|
4106
|
+
[] as string[]
|
|
4107
|
+
)
|
|
4048
4108
|
.option("--debug", "enable debug output")
|
|
4049
4109
|
.option("--json", "output raw JSON")
|
|
4050
|
-
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
|
|
4110
|
+
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; attach?: string[]; debug?: boolean; json?: boolean }) => {
|
|
4051
4111
|
const rootOpts = program.opts<CliOptions>();
|
|
4052
4112
|
const cfg = config.readConfig();
|
|
4053
4113
|
const { apiKey } = getConfig(rootOpts);
|
|
@@ -4057,10 +4117,10 @@ issues
|
|
|
4057
4117
|
return;
|
|
4058
4118
|
}
|
|
4059
4119
|
|
|
4060
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4120
|
+
const { apiBaseUrl, storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4061
4121
|
|
|
4062
4122
|
const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
|
|
4063
|
-
|
|
4123
|
+
let description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
4064
4124
|
|
|
4065
4125
|
let status: number | undefined = undefined;
|
|
4066
4126
|
if (opts.status !== undefined) {
|
|
@@ -4090,8 +4150,38 @@ issues
|
|
|
4090
4150
|
labels = opts.label.map(String);
|
|
4091
4151
|
}
|
|
4092
4152
|
|
|
4093
|
-
const
|
|
4153
|
+
const attachPaths = Array.isArray(opts.attach) ? opts.attach : [];
|
|
4154
|
+
const spinner = createTtySpinner(
|
|
4155
|
+
process.stdout.isTTY ?? false,
|
|
4156
|
+
attachPaths.length > 0 ? "Uploading attachments..." : "Updating issue..."
|
|
4157
|
+
);
|
|
4094
4158
|
try {
|
|
4159
|
+
if (attachPaths.length > 0) {
|
|
4160
|
+
// If the caller did not supply a new description, fetch the existing one
|
|
4161
|
+
// and append to it. This makes "add a screenshot to issue X" a one-step
|
|
4162
|
+
// operation rather than forcing the caller to copy-paste the existing
|
|
4163
|
+
// description first. Small race window if someone else updates
|
|
4164
|
+
// concurrently, which is acceptable for an interactive CLI / agent.
|
|
4165
|
+
if (description === undefined) {
|
|
4166
|
+
const existing = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
4167
|
+
if (!existing) {
|
|
4168
|
+
spinner.stop();
|
|
4169
|
+
console.error(`Issue not found: ${issueId}`);
|
|
4170
|
+
process.exitCode = 1;
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
description = (existing as { description?: string | null }).description ?? "";
|
|
4174
|
+
}
|
|
4175
|
+
const uploaded = await uploadAttachments({
|
|
4176
|
+
apiKey,
|
|
4177
|
+
storageBaseUrl,
|
|
4178
|
+
attachmentPaths: attachPaths,
|
|
4179
|
+
debug: !!opts.debug,
|
|
4180
|
+
});
|
|
4181
|
+
description = appendAttachmentsToContent(description ?? "", uploaded);
|
|
4182
|
+
spinner.update("Updating issue...");
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4095
4185
|
const result = await updateIssue({
|
|
4096
4186
|
apiKey,
|
|
4097
4187
|
apiBaseUrl,
|
|
@@ -4115,9 +4205,18 @@ issues
|
|
|
4115
4205
|
issues
|
|
4116
4206
|
.command("update-comment <commentId> <content>")
|
|
4117
4207
|
.description("update an existing issue comment")
|
|
4208
|
+
.option(
|
|
4209
|
+
"--attach <path>",
|
|
4210
|
+
"attach a file (uploads and appends a markdown link to <content>; repeatable)",
|
|
4211
|
+
(value: string, previous: string[]) => {
|
|
4212
|
+
previous.push(value);
|
|
4213
|
+
return previous;
|
|
4214
|
+
},
|
|
4215
|
+
[] as string[]
|
|
4216
|
+
)
|
|
4118
4217
|
.option("--debug", "enable debug output")
|
|
4119
4218
|
.option("--json", "output raw JSON")
|
|
4120
|
-
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
4219
|
+
.action(async (commentId: string, content: string, opts: { attach?: string[]; debug?: boolean; json?: boolean }) => {
|
|
4121
4220
|
if (opts.debug) {
|
|
4122
4221
|
// eslint-disable-next-line no-console
|
|
4123
4222
|
console.error(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
@@ -4137,15 +4236,31 @@ issues
|
|
|
4137
4236
|
return;
|
|
4138
4237
|
}
|
|
4139
4238
|
|
|
4140
|
-
const
|
|
4239
|
+
const attachPaths = Array.isArray(opts.attach) ? opts.attach : [];
|
|
4240
|
+
const spinner = createTtySpinner(
|
|
4241
|
+
process.stdout.isTTY ?? false,
|
|
4242
|
+
attachPaths.length > 0 ? "Uploading attachments..." : "Updating comment..."
|
|
4243
|
+
);
|
|
4141
4244
|
try {
|
|
4142
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4245
|
+
const { apiBaseUrl, storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4246
|
+
|
|
4247
|
+
let augmentedContent = content;
|
|
4248
|
+
if (attachPaths.length > 0) {
|
|
4249
|
+
const uploaded = await uploadAttachments({
|
|
4250
|
+
apiKey,
|
|
4251
|
+
storageBaseUrl,
|
|
4252
|
+
attachmentPaths: attachPaths,
|
|
4253
|
+
debug: !!opts.debug,
|
|
4254
|
+
});
|
|
4255
|
+
augmentedContent = appendAttachmentsToContent(content, uploaded);
|
|
4256
|
+
spinner.update("Updating comment...");
|
|
4257
|
+
}
|
|
4143
4258
|
|
|
4144
4259
|
const result = await updateIssueComment({
|
|
4145
4260
|
apiKey,
|
|
4146
4261
|
apiBaseUrl,
|
|
4147
4262
|
commentId,
|
|
4148
|
-
content,
|
|
4263
|
+
content: augmentedContent,
|
|
4149
4264
|
debug: !!opts.debug,
|
|
4150
4265
|
});
|
|
4151
4266
|
spinner.stop();
|