postgresai 0.14.0-beta.4 → 0.14.0-beta.6
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 +31 -3
- package/bin/postgres-ai.ts +226 -16
- package/bunfig.toml +11 -3
- package/dist/bin/postgres-ai.js +726 -95
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.permissions.sql +37 -0
- package/dist/sql/sql/03.optional_rds.sql +6 -0
- package/dist/sql/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/sql/05.helpers.sql +439 -0
- package/lib/checkup.ts +3 -0
- package/lib/config.ts +4 -4
- package/lib/init.ts +9 -3
- package/lib/issues.ts +318 -0
- package/lib/mcp-server.ts +207 -73
- package/lib/metrics-embedded.ts +2 -2
- package/package.json +4 -2
- package/sql/05.helpers.sql +31 -7
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +46 -0
- package/test/checkup.test.ts +3 -2
- package/test/init.integration.test.ts +98 -0
- package/test/init.test.ts +72 -0
- package/test/issues.cli.test.ts +314 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +988 -0
- package/test/schema-validation.test.ts +1 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-beta.
|
|
13067
|
+
version: "0.14.0-beta.6",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -13090,12 +13090,14 @@ var package_default = {
|
|
|
13090
13090
|
},
|
|
13091
13091
|
scripts: {
|
|
13092
13092
|
"embed-metrics": "bun run scripts/embed-metrics.ts",
|
|
13093
|
-
build: `bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))"`,
|
|
13093
|
+
build: `bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql`,
|
|
13094
13094
|
prepublishOnly: "npm run build",
|
|
13095
13095
|
start: "bun ./bin/postgres-ai.ts --help",
|
|
13096
13096
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
13097
13097
|
dev: "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
|
|
13098
13098
|
test: "bun run embed-metrics && bun test",
|
|
13099
|
+
"test:fast": "bun run embed-metrics && bun test --coverage=false",
|
|
13100
|
+
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
13099
13101
|
typecheck: "bun run embed-metrics && bunx tsc --noEmit"
|
|
13100
13102
|
},
|
|
13101
13103
|
dependencies: {
|
|
@@ -13143,10 +13145,10 @@ function readConfig() {
|
|
|
13143
13145
|
try {
|
|
13144
13146
|
const content = fs.readFileSync(userConfigPath, "utf8");
|
|
13145
13147
|
const parsed = JSON.parse(content);
|
|
13146
|
-
config.apiKey = parsed.apiKey
|
|
13147
|
-
config.baseUrl = parsed.baseUrl
|
|
13148
|
-
config.orgId = parsed.orgId
|
|
13149
|
-
config.defaultProject = parsed.defaultProject
|
|
13148
|
+
config.apiKey = parsed.apiKey ?? null;
|
|
13149
|
+
config.baseUrl = parsed.baseUrl ?? null;
|
|
13150
|
+
config.orgId = parsed.orgId ?? null;
|
|
13151
|
+
config.defaultProject = parsed.defaultProject ?? null;
|
|
13150
13152
|
return config;
|
|
13151
13153
|
} catch (err) {
|
|
13152
13154
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -15885,7 +15887,7 @@ var Result = import_lib.default.Result;
|
|
|
15885
15887
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15886
15888
|
var defaults = import_lib.default.defaults;
|
|
15887
15889
|
// package.json
|
|
15888
|
-
var version = "0.14.0-beta.
|
|
15890
|
+
var version = "0.14.0-beta.6";
|
|
15889
15891
|
var package_default2 = {
|
|
15890
15892
|
name: "postgresai",
|
|
15891
15893
|
version,
|
|
@@ -15914,12 +15916,14 @@ var package_default2 = {
|
|
|
15914
15916
|
},
|
|
15915
15917
|
scripts: {
|
|
15916
15918
|
"embed-metrics": "bun run scripts/embed-metrics.ts",
|
|
15917
|
-
build: `bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))"`,
|
|
15919
|
+
build: `bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql`,
|
|
15918
15920
|
prepublishOnly: "npm run build",
|
|
15919
15921
|
start: "bun ./bin/postgres-ai.ts --help",
|
|
15920
15922
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
15921
15923
|
dev: "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
|
|
15922
15924
|
test: "bun run embed-metrics && bun test",
|
|
15925
|
+
"test:fast": "bun run embed-metrics && bun test --coverage=false",
|
|
15926
|
+
"test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
15923
15927
|
typecheck: "bun run embed-metrics && bunx tsc --noEmit"
|
|
15924
15928
|
},
|
|
15925
15929
|
dependencies: {
|
|
@@ -15967,10 +15971,10 @@ function readConfig2() {
|
|
|
15967
15971
|
try {
|
|
15968
15972
|
const content = fs2.readFileSync(userConfigPath, "utf8");
|
|
15969
15973
|
const parsed = JSON.parse(content);
|
|
15970
|
-
config.apiKey = parsed.apiKey
|
|
15971
|
-
config.baseUrl = parsed.baseUrl
|
|
15972
|
-
config.orgId = parsed.orgId
|
|
15973
|
-
config.defaultProject = parsed.defaultProject
|
|
15974
|
+
config.apiKey = parsed.apiKey ?? null;
|
|
15975
|
+
config.baseUrl = parsed.baseUrl ?? null;
|
|
15976
|
+
config.orgId = parsed.orgId ?? null;
|
|
15977
|
+
config.defaultProject = parsed.defaultProject ?? null;
|
|
15974
15978
|
return config;
|
|
15975
15979
|
} catch (err) {
|
|
15976
15980
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16085,7 +16089,8 @@ async function fetchIssues(params) {
|
|
|
16085
16089
|
const headers = {
|
|
16086
16090
|
"access-token": apiKey,
|
|
16087
16091
|
Prefer: "return=representation",
|
|
16088
|
-
"Content-Type": "application/json"
|
|
16092
|
+
"Content-Type": "application/json",
|
|
16093
|
+
Connection: "close"
|
|
16089
16094
|
};
|
|
16090
16095
|
if (debug) {
|
|
16091
16096
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -16126,7 +16131,8 @@ async function fetchIssueComments(params) {
|
|
|
16126
16131
|
const headers = {
|
|
16127
16132
|
"access-token": apiKey,
|
|
16128
16133
|
Prefer: "return=representation",
|
|
16129
|
-
"Content-Type": "application/json"
|
|
16134
|
+
"Content-Type": "application/json",
|
|
16135
|
+
Connection: "close"
|
|
16130
16136
|
};
|
|
16131
16137
|
if (debug) {
|
|
16132
16138
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -16170,7 +16176,8 @@ async function fetchIssue(params) {
|
|
|
16170
16176
|
const headers = {
|
|
16171
16177
|
"access-token": apiKey,
|
|
16172
16178
|
Prefer: "return=representation",
|
|
16173
|
-
"Content-Type": "application/json"
|
|
16179
|
+
"Content-Type": "application/json",
|
|
16180
|
+
Connection: "close"
|
|
16174
16181
|
};
|
|
16175
16182
|
if (debug) {
|
|
16176
16183
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -16203,6 +16210,67 @@ async function fetchIssue(params) {
|
|
|
16203
16210
|
throw new Error(formatHttpError("Failed to fetch issue", response.status, data));
|
|
16204
16211
|
}
|
|
16205
16212
|
}
|
|
16213
|
+
async function createIssue(params) {
|
|
16214
|
+
const { apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug } = params;
|
|
16215
|
+
if (!apiKey) {
|
|
16216
|
+
throw new Error("API key is required");
|
|
16217
|
+
}
|
|
16218
|
+
if (!title) {
|
|
16219
|
+
throw new Error("title is required");
|
|
16220
|
+
}
|
|
16221
|
+
if (typeof orgId !== "number") {
|
|
16222
|
+
throw new Error("orgId is required");
|
|
16223
|
+
}
|
|
16224
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16225
|
+
const url = new URL(`${base}/rpc/issue_create`);
|
|
16226
|
+
const bodyObj = {
|
|
16227
|
+
title,
|
|
16228
|
+
org_id: orgId
|
|
16229
|
+
};
|
|
16230
|
+
if (description !== undefined) {
|
|
16231
|
+
bodyObj.description = description;
|
|
16232
|
+
}
|
|
16233
|
+
if (projectId !== undefined) {
|
|
16234
|
+
bodyObj.project_id = projectId;
|
|
16235
|
+
}
|
|
16236
|
+
if (labels && labels.length > 0) {
|
|
16237
|
+
bodyObj.labels = labels;
|
|
16238
|
+
}
|
|
16239
|
+
const body = JSON.stringify(bodyObj);
|
|
16240
|
+
const headers = {
|
|
16241
|
+
"access-token": apiKey,
|
|
16242
|
+
Prefer: "return=representation",
|
|
16243
|
+
"Content-Type": "application/json",
|
|
16244
|
+
Connection: "close"
|
|
16245
|
+
};
|
|
16246
|
+
if (debug) {
|
|
16247
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16248
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16249
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
16250
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16251
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16252
|
+
console.log(`Debug: Request body: ${body}`);
|
|
16253
|
+
}
|
|
16254
|
+
const response = await fetch(url.toString(), {
|
|
16255
|
+
method: "POST",
|
|
16256
|
+
headers,
|
|
16257
|
+
body
|
|
16258
|
+
});
|
|
16259
|
+
if (debug) {
|
|
16260
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16261
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16262
|
+
}
|
|
16263
|
+
const data = await response.text();
|
|
16264
|
+
if (response.ok) {
|
|
16265
|
+
try {
|
|
16266
|
+
return JSON.parse(data);
|
|
16267
|
+
} catch {
|
|
16268
|
+
throw new Error(`Failed to parse create issue response: ${data}`);
|
|
16269
|
+
}
|
|
16270
|
+
} else {
|
|
16271
|
+
throw new Error(formatHttpError("Failed to create issue", response.status, data));
|
|
16272
|
+
}
|
|
16273
|
+
}
|
|
16206
16274
|
async function createIssueComment(params) {
|
|
16207
16275
|
const { apiKey, apiBaseUrl, issueId, content, parentCommentId, debug } = params;
|
|
16208
16276
|
if (!apiKey) {
|
|
@@ -16227,7 +16295,8 @@ async function createIssueComment(params) {
|
|
|
16227
16295
|
const headers = {
|
|
16228
16296
|
"access-token": apiKey,
|
|
16229
16297
|
Prefer: "return=representation",
|
|
16230
|
-
"Content-Type": "application/json"
|
|
16298
|
+
"Content-Type": "application/json",
|
|
16299
|
+
Connection: "close"
|
|
16231
16300
|
};
|
|
16232
16301
|
if (debug) {
|
|
16233
16302
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -16257,6 +16326,121 @@ async function createIssueComment(params) {
|
|
|
16257
16326
|
throw new Error(formatHttpError("Failed to create issue comment", response.status, data));
|
|
16258
16327
|
}
|
|
16259
16328
|
}
|
|
16329
|
+
async function updateIssue(params) {
|
|
16330
|
+
const { apiKey, apiBaseUrl, issueId, title, description, status, labels, debug } = params;
|
|
16331
|
+
if (!apiKey) {
|
|
16332
|
+
throw new Error("API key is required");
|
|
16333
|
+
}
|
|
16334
|
+
if (!issueId) {
|
|
16335
|
+
throw new Error("issueId is required");
|
|
16336
|
+
}
|
|
16337
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
16338
|
+
throw new Error("At least one field to update is required (title, description, status, or labels)");
|
|
16339
|
+
}
|
|
16340
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16341
|
+
const url = new URL(`${base}/rpc/issue_update`);
|
|
16342
|
+
const bodyObj = {
|
|
16343
|
+
p_id: issueId
|
|
16344
|
+
};
|
|
16345
|
+
if (title !== undefined) {
|
|
16346
|
+
bodyObj.p_title = title;
|
|
16347
|
+
}
|
|
16348
|
+
if (description !== undefined) {
|
|
16349
|
+
bodyObj.p_description = description;
|
|
16350
|
+
}
|
|
16351
|
+
if (status !== undefined) {
|
|
16352
|
+
bodyObj.p_status = status;
|
|
16353
|
+
}
|
|
16354
|
+
if (labels !== undefined) {
|
|
16355
|
+
bodyObj.p_labels = labels;
|
|
16356
|
+
}
|
|
16357
|
+
const body = JSON.stringify(bodyObj);
|
|
16358
|
+
const headers = {
|
|
16359
|
+
"access-token": apiKey,
|
|
16360
|
+
Prefer: "return=representation",
|
|
16361
|
+
"Content-Type": "application/json",
|
|
16362
|
+
Connection: "close"
|
|
16363
|
+
};
|
|
16364
|
+
if (debug) {
|
|
16365
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16366
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16367
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
16368
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16369
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16370
|
+
console.log(`Debug: Request body: ${body}`);
|
|
16371
|
+
}
|
|
16372
|
+
const response = await fetch(url.toString(), {
|
|
16373
|
+
method: "POST",
|
|
16374
|
+
headers,
|
|
16375
|
+
body
|
|
16376
|
+
});
|
|
16377
|
+
if (debug) {
|
|
16378
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16379
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16380
|
+
}
|
|
16381
|
+
const data = await response.text();
|
|
16382
|
+
if (response.ok) {
|
|
16383
|
+
try {
|
|
16384
|
+
return JSON.parse(data);
|
|
16385
|
+
} catch {
|
|
16386
|
+
throw new Error(`Failed to parse update issue response: ${data}`);
|
|
16387
|
+
}
|
|
16388
|
+
} else {
|
|
16389
|
+
throw new Error(formatHttpError("Failed to update issue", response.status, data));
|
|
16390
|
+
}
|
|
16391
|
+
}
|
|
16392
|
+
async function updateIssueComment(params) {
|
|
16393
|
+
const { apiKey, apiBaseUrl, commentId, content, debug } = params;
|
|
16394
|
+
if (!apiKey) {
|
|
16395
|
+
throw new Error("API key is required");
|
|
16396
|
+
}
|
|
16397
|
+
if (!commentId) {
|
|
16398
|
+
throw new Error("commentId is required");
|
|
16399
|
+
}
|
|
16400
|
+
if (!content) {
|
|
16401
|
+
throw new Error("content is required");
|
|
16402
|
+
}
|
|
16403
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
16404
|
+
const url = new URL(`${base}/rpc/issue_comment_update`);
|
|
16405
|
+
const bodyObj = {
|
|
16406
|
+
p_id: commentId,
|
|
16407
|
+
p_content: content
|
|
16408
|
+
};
|
|
16409
|
+
const body = JSON.stringify(bodyObj);
|
|
16410
|
+
const headers = {
|
|
16411
|
+
"access-token": apiKey,
|
|
16412
|
+
Prefer: "return=representation",
|
|
16413
|
+
"Content-Type": "application/json",
|
|
16414
|
+
Connection: "close"
|
|
16415
|
+
};
|
|
16416
|
+
if (debug) {
|
|
16417
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
16418
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
16419
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
16420
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
16421
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
16422
|
+
console.log(`Debug: Request body: ${body}`);
|
|
16423
|
+
}
|
|
16424
|
+
const response = await fetch(url.toString(), {
|
|
16425
|
+
method: "POST",
|
|
16426
|
+
headers,
|
|
16427
|
+
body
|
|
16428
|
+
});
|
|
16429
|
+
if (debug) {
|
|
16430
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
16431
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
16432
|
+
}
|
|
16433
|
+
const data = await response.text();
|
|
16434
|
+
if (response.ok) {
|
|
16435
|
+
try {
|
|
16436
|
+
return JSON.parse(data);
|
|
16437
|
+
} catch {
|
|
16438
|
+
throw new Error(`Failed to parse update comment response: ${data}`);
|
|
16439
|
+
}
|
|
16440
|
+
} else {
|
|
16441
|
+
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
16442
|
+
}
|
|
16443
|
+
}
|
|
16260
16444
|
|
|
16261
16445
|
// node_modules/zod/v4/core/core.js
|
|
16262
16446
|
var NEVER = Object.freeze({
|
|
@@ -23144,10 +23328,116 @@ class StdioServerTransport {
|
|
|
23144
23328
|
}
|
|
23145
23329
|
|
|
23146
23330
|
// lib/mcp-server.ts
|
|
23331
|
+
var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
23332
|
+
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
23333
|
+
async function handleToolCall(req, rootOpts, extra) {
|
|
23334
|
+
const toolName = req.params.name;
|
|
23335
|
+
const args = req.params.arguments || {};
|
|
23336
|
+
const cfg = readConfig2();
|
|
23337
|
+
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
23338
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
23339
|
+
const debug = Boolean(args.debug ?? extra?.debug);
|
|
23340
|
+
if (!apiKey) {
|
|
23341
|
+
return {
|
|
23342
|
+
content: [
|
|
23343
|
+
{
|
|
23344
|
+
type: "text",
|
|
23345
|
+
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
|
|
23346
|
+
}
|
|
23347
|
+
],
|
|
23348
|
+
isError: true
|
|
23349
|
+
};
|
|
23350
|
+
}
|
|
23351
|
+
try {
|
|
23352
|
+
if (toolName === "list_issues") {
|
|
23353
|
+
const issues = await fetchIssues({ apiKey, apiBaseUrl, debug });
|
|
23354
|
+
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
23355
|
+
}
|
|
23356
|
+
if (toolName === "view_issue") {
|
|
23357
|
+
const issueId = String(args.issue_id || "").trim();
|
|
23358
|
+
if (!issueId) {
|
|
23359
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23360
|
+
}
|
|
23361
|
+
const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
23362
|
+
if (!issue2) {
|
|
23363
|
+
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
23364
|
+
}
|
|
23365
|
+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
|
|
23366
|
+
const combined = { issue: issue2, comments };
|
|
23367
|
+
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
23368
|
+
}
|
|
23369
|
+
if (toolName === "post_issue_comment") {
|
|
23370
|
+
const issueId = String(args.issue_id || "").trim();
|
|
23371
|
+
const rawContent = String(args.content || "");
|
|
23372
|
+
const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
|
|
23373
|
+
if (!issueId) {
|
|
23374
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23375
|
+
}
|
|
23376
|
+
if (!rawContent) {
|
|
23377
|
+
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
23378
|
+
}
|
|
23379
|
+
const content = interpretEscapes(rawContent);
|
|
23380
|
+
const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
|
|
23381
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23382
|
+
}
|
|
23383
|
+
if (toolName === "create_issue") {
|
|
23384
|
+
const rawTitle = String(args.title || "").trim();
|
|
23385
|
+
if (!rawTitle) {
|
|
23386
|
+
return { content: [{ type: "text", text: "title is required" }], isError: true };
|
|
23387
|
+
}
|
|
23388
|
+
const title = interpretEscapes(rawTitle);
|
|
23389
|
+
const rawDescription = args.description ? String(args.description) : undefined;
|
|
23390
|
+
const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
23391
|
+
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
|
|
23392
|
+
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
23393
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId;
|
|
23394
|
+
if (orgId === undefined || orgId === null || Number.isNaN(orgId)) {
|
|
23395
|
+
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 };
|
|
23396
|
+
}
|
|
23397
|
+
const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug });
|
|
23398
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23399
|
+
}
|
|
23400
|
+
if (toolName === "update_issue") {
|
|
23401
|
+
const issueId = String(args.issue_id || "").trim();
|
|
23402
|
+
if (!issueId) {
|
|
23403
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23404
|
+
}
|
|
23405
|
+
const rawTitle = args.title !== undefined ? String(args.title) : undefined;
|
|
23406
|
+
const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
|
|
23407
|
+
const rawDescription = args.description !== undefined ? String(args.description) : undefined;
|
|
23408
|
+
const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
23409
|
+
const status = args.status !== undefined ? Number(args.status) : undefined;
|
|
23410
|
+
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
23411
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
23412
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, or labels)" }], isError: true };
|
|
23413
|
+
}
|
|
23414
|
+
if (status !== undefined && (Number.isNaN(status) || status !== 0 && status !== 1)) {
|
|
23415
|
+
return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
|
|
23416
|
+
}
|
|
23417
|
+
const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, debug });
|
|
23418
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23419
|
+
}
|
|
23420
|
+
if (toolName === "update_issue_comment") {
|
|
23421
|
+
const commentId = String(args.comment_id || "").trim();
|
|
23422
|
+
const rawContent = String(args.content || "");
|
|
23423
|
+
if (!commentId) {
|
|
23424
|
+
return { content: [{ type: "text", text: "comment_id is required" }], isError: true };
|
|
23425
|
+
}
|
|
23426
|
+
if (!rawContent.trim()) {
|
|
23427
|
+
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
23428
|
+
}
|
|
23429
|
+
const content = interpretEscapes(rawContent);
|
|
23430
|
+
const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
|
|
23431
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23432
|
+
}
|
|
23433
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
23434
|
+
} catch (err) {
|
|
23435
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23436
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
23437
|
+
}
|
|
23438
|
+
}
|
|
23147
23439
|
async function startMcpServer(rootOpts, extra) {
|
|
23148
23440
|
const server = new Server({ name: "postgresai-mcp", version: package_default2.version }, { capabilities: { tools: {} } });
|
|
23149
|
-
const interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
|
|
23150
|
-
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
23151
23441
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
23152
23442
|
return {
|
|
23153
23443
|
tools: [
|
|
@@ -23189,65 +23479,68 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
23189
23479
|
required: ["issue_id", "content"],
|
|
23190
23480
|
additionalProperties: false
|
|
23191
23481
|
}
|
|
23482
|
+
},
|
|
23483
|
+
{
|
|
23484
|
+
name: "create_issue",
|
|
23485
|
+
description: "Create a new issue in PostgresAI",
|
|
23486
|
+
inputSchema: {
|
|
23487
|
+
type: "object",
|
|
23488
|
+
properties: {
|
|
23489
|
+
title: { type: "string", description: "Issue title (required)" },
|
|
23490
|
+
description: { type: "string", description: "Issue description (supports \\n as newline)" },
|
|
23491
|
+
org_id: { type: "number", description: "Organization ID (uses config value if not provided)" },
|
|
23492
|
+
project_id: { type: "number", description: "Project ID to associate the issue with" },
|
|
23493
|
+
labels: {
|
|
23494
|
+
type: "array",
|
|
23495
|
+
items: { type: "string" },
|
|
23496
|
+
description: "Labels to apply to the issue"
|
|
23497
|
+
},
|
|
23498
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23499
|
+
},
|
|
23500
|
+
required: ["title"],
|
|
23501
|
+
additionalProperties: false
|
|
23502
|
+
}
|
|
23503
|
+
},
|
|
23504
|
+
{
|
|
23505
|
+
name: "update_issue",
|
|
23506
|
+
description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen.",
|
|
23507
|
+
inputSchema: {
|
|
23508
|
+
type: "object",
|
|
23509
|
+
properties: {
|
|
23510
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
23511
|
+
title: { type: "string", description: "New title (supports \\n as newline)" },
|
|
23512
|
+
description: { type: "string", description: "New description (supports \\n as newline)" },
|
|
23513
|
+
status: { type: "number", description: "Status: 0=open, 1=closed" },
|
|
23514
|
+
labels: {
|
|
23515
|
+
type: "array",
|
|
23516
|
+
items: { type: "string" },
|
|
23517
|
+
description: "Labels to set on the issue"
|
|
23518
|
+
},
|
|
23519
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23520
|
+
},
|
|
23521
|
+
required: ["issue_id"],
|
|
23522
|
+
additionalProperties: false
|
|
23523
|
+
}
|
|
23524
|
+
},
|
|
23525
|
+
{
|
|
23526
|
+
name: "update_issue_comment",
|
|
23527
|
+
description: "Update an existing issue comment",
|
|
23528
|
+
inputSchema: {
|
|
23529
|
+
type: "object",
|
|
23530
|
+
properties: {
|
|
23531
|
+
comment_id: { type: "string", description: "Comment ID (UUID)" },
|
|
23532
|
+
content: { type: "string", description: "New comment text (supports \\n as newline)" },
|
|
23533
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
23534
|
+
},
|
|
23535
|
+
required: ["comment_id", "content"],
|
|
23536
|
+
additionalProperties: false
|
|
23537
|
+
}
|
|
23192
23538
|
}
|
|
23193
23539
|
]
|
|
23194
23540
|
};
|
|
23195
23541
|
});
|
|
23196
23542
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
23197
|
-
|
|
23198
|
-
const args = req.params.arguments || {};
|
|
23199
|
-
const cfg = readConfig2();
|
|
23200
|
-
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
23201
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
23202
|
-
const debug = Boolean(args.debug ?? extra?.debug);
|
|
23203
|
-
if (!apiKey) {
|
|
23204
|
-
return {
|
|
23205
|
-
content: [
|
|
23206
|
-
{
|
|
23207
|
-
type: "text",
|
|
23208
|
-
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
|
|
23209
|
-
}
|
|
23210
|
-
],
|
|
23211
|
-
isError: true
|
|
23212
|
-
};
|
|
23213
|
-
}
|
|
23214
|
-
try {
|
|
23215
|
-
if (toolName === "list_issues") {
|
|
23216
|
-
const issues = await fetchIssues({ apiKey, apiBaseUrl, debug });
|
|
23217
|
-
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
23218
|
-
}
|
|
23219
|
-
if (toolName === "view_issue") {
|
|
23220
|
-
const issueId = String(args.issue_id || "").trim();
|
|
23221
|
-
if (!issueId) {
|
|
23222
|
-
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23223
|
-
}
|
|
23224
|
-
const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
23225
|
-
if (!issue2) {
|
|
23226
|
-
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
23227
|
-
}
|
|
23228
|
-
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
|
|
23229
|
-
const combined = { issue: issue2, comments };
|
|
23230
|
-
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
23231
|
-
}
|
|
23232
|
-
if (toolName === "post_issue_comment") {
|
|
23233
|
-
const issueId = String(args.issue_id || "").trim();
|
|
23234
|
-
const rawContent = String(args.content || "");
|
|
23235
|
-
const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
|
|
23236
|
-
if (!issueId) {
|
|
23237
|
-
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
23238
|
-
}
|
|
23239
|
-
if (!rawContent) {
|
|
23240
|
-
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
23241
|
-
}
|
|
23242
|
-
const content = interpretEscapes(rawContent);
|
|
23243
|
-
const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
|
|
23244
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
23245
|
-
}
|
|
23246
|
-
throw new Error(`Unknown tool: ${toolName}`);
|
|
23247
|
-
} catch (err) {
|
|
23248
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
23249
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
23250
|
-
}
|
|
23543
|
+
return handleToolCall(req, rootOpts, extra);
|
|
23251
23544
|
});
|
|
23252
23545
|
const transport = new StdioServerTransport;
|
|
23253
23546
|
await server.connect(transport);
|
|
@@ -23265,7 +23558,8 @@ async function fetchIssues2(params) {
|
|
|
23265
23558
|
const headers = {
|
|
23266
23559
|
"access-token": apiKey,
|
|
23267
23560
|
Prefer: "return=representation",
|
|
23268
|
-
"Content-Type": "application/json"
|
|
23561
|
+
"Content-Type": "application/json",
|
|
23562
|
+
Connection: "close"
|
|
23269
23563
|
};
|
|
23270
23564
|
if (debug) {
|
|
23271
23565
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -23306,7 +23600,8 @@ async function fetchIssueComments2(params) {
|
|
|
23306
23600
|
const headers = {
|
|
23307
23601
|
"access-token": apiKey,
|
|
23308
23602
|
Prefer: "return=representation",
|
|
23309
|
-
"Content-Type": "application/json"
|
|
23603
|
+
"Content-Type": "application/json",
|
|
23604
|
+
Connection: "close"
|
|
23310
23605
|
};
|
|
23311
23606
|
if (debug) {
|
|
23312
23607
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -23350,7 +23645,8 @@ async function fetchIssue2(params) {
|
|
|
23350
23645
|
const headers = {
|
|
23351
23646
|
"access-token": apiKey,
|
|
23352
23647
|
Prefer: "return=representation",
|
|
23353
|
-
"Content-Type": "application/json"
|
|
23648
|
+
"Content-Type": "application/json",
|
|
23649
|
+
Connection: "close"
|
|
23354
23650
|
};
|
|
23355
23651
|
if (debug) {
|
|
23356
23652
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -23383,6 +23679,67 @@ async function fetchIssue2(params) {
|
|
|
23383
23679
|
throw new Error(formatHttpError("Failed to fetch issue", response.status, data));
|
|
23384
23680
|
}
|
|
23385
23681
|
}
|
|
23682
|
+
async function createIssue2(params) {
|
|
23683
|
+
const { apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug } = params;
|
|
23684
|
+
if (!apiKey) {
|
|
23685
|
+
throw new Error("API key is required");
|
|
23686
|
+
}
|
|
23687
|
+
if (!title) {
|
|
23688
|
+
throw new Error("title is required");
|
|
23689
|
+
}
|
|
23690
|
+
if (typeof orgId !== "number") {
|
|
23691
|
+
throw new Error("orgId is required");
|
|
23692
|
+
}
|
|
23693
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
23694
|
+
const url = new URL(`${base}/rpc/issue_create`);
|
|
23695
|
+
const bodyObj = {
|
|
23696
|
+
title,
|
|
23697
|
+
org_id: orgId
|
|
23698
|
+
};
|
|
23699
|
+
if (description !== undefined) {
|
|
23700
|
+
bodyObj.description = description;
|
|
23701
|
+
}
|
|
23702
|
+
if (projectId !== undefined) {
|
|
23703
|
+
bodyObj.project_id = projectId;
|
|
23704
|
+
}
|
|
23705
|
+
if (labels && labels.length > 0) {
|
|
23706
|
+
bodyObj.labels = labels;
|
|
23707
|
+
}
|
|
23708
|
+
const body = JSON.stringify(bodyObj);
|
|
23709
|
+
const headers = {
|
|
23710
|
+
"access-token": apiKey,
|
|
23711
|
+
Prefer: "return=representation",
|
|
23712
|
+
"Content-Type": "application/json",
|
|
23713
|
+
Connection: "close"
|
|
23714
|
+
};
|
|
23715
|
+
if (debug) {
|
|
23716
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
23717
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
23718
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
23719
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
23720
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
23721
|
+
console.log(`Debug: Request body: ${body}`);
|
|
23722
|
+
}
|
|
23723
|
+
const response = await fetch(url.toString(), {
|
|
23724
|
+
method: "POST",
|
|
23725
|
+
headers,
|
|
23726
|
+
body
|
|
23727
|
+
});
|
|
23728
|
+
if (debug) {
|
|
23729
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
23730
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
23731
|
+
}
|
|
23732
|
+
const data = await response.text();
|
|
23733
|
+
if (response.ok) {
|
|
23734
|
+
try {
|
|
23735
|
+
return JSON.parse(data);
|
|
23736
|
+
} catch {
|
|
23737
|
+
throw new Error(`Failed to parse create issue response: ${data}`);
|
|
23738
|
+
}
|
|
23739
|
+
} else {
|
|
23740
|
+
throw new Error(formatHttpError("Failed to create issue", response.status, data));
|
|
23741
|
+
}
|
|
23742
|
+
}
|
|
23386
23743
|
async function createIssueComment2(params) {
|
|
23387
23744
|
const { apiKey, apiBaseUrl, issueId, content, parentCommentId, debug } = params;
|
|
23388
23745
|
if (!apiKey) {
|
|
@@ -23407,7 +23764,8 @@ async function createIssueComment2(params) {
|
|
|
23407
23764
|
const headers = {
|
|
23408
23765
|
"access-token": apiKey,
|
|
23409
23766
|
Prefer: "return=representation",
|
|
23410
|
-
"Content-Type": "application/json"
|
|
23767
|
+
"Content-Type": "application/json",
|
|
23768
|
+
Connection: "close"
|
|
23411
23769
|
};
|
|
23412
23770
|
if (debug) {
|
|
23413
23771
|
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
@@ -23437,6 +23795,121 @@ async function createIssueComment2(params) {
|
|
|
23437
23795
|
throw new Error(formatHttpError("Failed to create issue comment", response.status, data));
|
|
23438
23796
|
}
|
|
23439
23797
|
}
|
|
23798
|
+
async function updateIssue2(params) {
|
|
23799
|
+
const { apiKey, apiBaseUrl, issueId, title, description, status, labels, debug } = params;
|
|
23800
|
+
if (!apiKey) {
|
|
23801
|
+
throw new Error("API key is required");
|
|
23802
|
+
}
|
|
23803
|
+
if (!issueId) {
|
|
23804
|
+
throw new Error("issueId is required");
|
|
23805
|
+
}
|
|
23806
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
23807
|
+
throw new Error("At least one field to update is required (title, description, status, or labels)");
|
|
23808
|
+
}
|
|
23809
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
23810
|
+
const url = new URL(`${base}/rpc/issue_update`);
|
|
23811
|
+
const bodyObj = {
|
|
23812
|
+
p_id: issueId
|
|
23813
|
+
};
|
|
23814
|
+
if (title !== undefined) {
|
|
23815
|
+
bodyObj.p_title = title;
|
|
23816
|
+
}
|
|
23817
|
+
if (description !== undefined) {
|
|
23818
|
+
bodyObj.p_description = description;
|
|
23819
|
+
}
|
|
23820
|
+
if (status !== undefined) {
|
|
23821
|
+
bodyObj.p_status = status;
|
|
23822
|
+
}
|
|
23823
|
+
if (labels !== undefined) {
|
|
23824
|
+
bodyObj.p_labels = labels;
|
|
23825
|
+
}
|
|
23826
|
+
const body = JSON.stringify(bodyObj);
|
|
23827
|
+
const headers = {
|
|
23828
|
+
"access-token": apiKey,
|
|
23829
|
+
Prefer: "return=representation",
|
|
23830
|
+
"Content-Type": "application/json",
|
|
23831
|
+
Connection: "close"
|
|
23832
|
+
};
|
|
23833
|
+
if (debug) {
|
|
23834
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
23835
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
23836
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
23837
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
23838
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
23839
|
+
console.log(`Debug: Request body: ${body}`);
|
|
23840
|
+
}
|
|
23841
|
+
const response = await fetch(url.toString(), {
|
|
23842
|
+
method: "POST",
|
|
23843
|
+
headers,
|
|
23844
|
+
body
|
|
23845
|
+
});
|
|
23846
|
+
if (debug) {
|
|
23847
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
23848
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
23849
|
+
}
|
|
23850
|
+
const data = await response.text();
|
|
23851
|
+
if (response.ok) {
|
|
23852
|
+
try {
|
|
23853
|
+
return JSON.parse(data);
|
|
23854
|
+
} catch {
|
|
23855
|
+
throw new Error(`Failed to parse update issue response: ${data}`);
|
|
23856
|
+
}
|
|
23857
|
+
} else {
|
|
23858
|
+
throw new Error(formatHttpError("Failed to update issue", response.status, data));
|
|
23859
|
+
}
|
|
23860
|
+
}
|
|
23861
|
+
async function updateIssueComment2(params) {
|
|
23862
|
+
const { apiKey, apiBaseUrl, commentId, content, debug } = params;
|
|
23863
|
+
if (!apiKey) {
|
|
23864
|
+
throw new Error("API key is required");
|
|
23865
|
+
}
|
|
23866
|
+
if (!commentId) {
|
|
23867
|
+
throw new Error("commentId is required");
|
|
23868
|
+
}
|
|
23869
|
+
if (!content) {
|
|
23870
|
+
throw new Error("content is required");
|
|
23871
|
+
}
|
|
23872
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
23873
|
+
const url = new URL(`${base}/rpc/issue_comment_update`);
|
|
23874
|
+
const bodyObj = {
|
|
23875
|
+
p_id: commentId,
|
|
23876
|
+
p_content: content
|
|
23877
|
+
};
|
|
23878
|
+
const body = JSON.stringify(bodyObj);
|
|
23879
|
+
const headers = {
|
|
23880
|
+
"access-token": apiKey,
|
|
23881
|
+
Prefer: "return=representation",
|
|
23882
|
+
"Content-Type": "application/json",
|
|
23883
|
+
Connection: "close"
|
|
23884
|
+
};
|
|
23885
|
+
if (debug) {
|
|
23886
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
23887
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
23888
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
23889
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
23890
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
23891
|
+
console.log(`Debug: Request body: ${body}`);
|
|
23892
|
+
}
|
|
23893
|
+
const response = await fetch(url.toString(), {
|
|
23894
|
+
method: "POST",
|
|
23895
|
+
headers,
|
|
23896
|
+
body
|
|
23897
|
+
});
|
|
23898
|
+
if (debug) {
|
|
23899
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
23900
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
23901
|
+
}
|
|
23902
|
+
const data = await response.text();
|
|
23903
|
+
if (response.ok) {
|
|
23904
|
+
try {
|
|
23905
|
+
return JSON.parse(data);
|
|
23906
|
+
} catch {
|
|
23907
|
+
throw new Error(`Failed to parse update comment response: ${data}`);
|
|
23908
|
+
}
|
|
23909
|
+
} else {
|
|
23910
|
+
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
23911
|
+
}
|
|
23912
|
+
}
|
|
23440
23913
|
|
|
23441
23914
|
// lib/util.ts
|
|
23442
23915
|
function maskSecret2(secret) {
|
|
@@ -23470,10 +23943,9 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
23470
23943
|
|
|
23471
23944
|
// lib/init.ts
|
|
23472
23945
|
import { randomBytes } from "crypto";
|
|
23473
|
-
import { URL as URL2 } from "url";
|
|
23946
|
+
import { URL as URL2, fileURLToPath } from "url";
|
|
23474
23947
|
import * as fs3 from "fs";
|
|
23475
23948
|
import * as path3 from "path";
|
|
23476
|
-
var __dirname = "/builds/postgres-ai/postgres_ai/cli/lib";
|
|
23477
23949
|
var DEFAULT_MONITORING_USER = "postgres_ai_mon";
|
|
23478
23950
|
function sslModeToConfig(mode) {
|
|
23479
23951
|
if (mode.toLowerCase() === "disable")
|
|
@@ -23554,9 +24026,11 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
|
|
|
23554
24026
|
}
|
|
23555
24027
|
}
|
|
23556
24028
|
function sqlDir() {
|
|
24029
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
24030
|
+
const currentDir = path3.dirname(currentFile);
|
|
23557
24031
|
const candidates = [
|
|
23558
|
-
path3.resolve(
|
|
23559
|
-
path3.resolve(
|
|
24032
|
+
path3.resolve(currentDir, "..", "sql"),
|
|
24033
|
+
path3.resolve(currentDir, "..", "..", "sql")
|
|
23560
24034
|
];
|
|
23561
24035
|
for (const candidate of candidates) {
|
|
23562
24036
|
if (fs3.existsSync(candidate)) {
|
|
@@ -24513,6 +24987,7 @@ where
|
|
|
24513
24987
|
quote_ident(pci.relname) as tag_index_name,
|
|
24514
24988
|
quote_ident(pct.relname) as tag_table_name,
|
|
24515
24989
|
coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
|
|
24990
|
+
pg_get_indexdef(pidx.indexrelid) as index_definition,
|
|
24516
24991
|
pg_relation_size(pidx.indexrelid) index_size_bytes,
|
|
24517
24992
|
((
|
|
24518
24993
|
select count(1)
|
|
@@ -25150,6 +25625,7 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
|
|
|
25150
25625
|
relation_name: String(transformed.relation_name || ""),
|
|
25151
25626
|
index_size_bytes: indexSizeBytes,
|
|
25152
25627
|
index_size_pretty: formatBytes(indexSizeBytes),
|
|
25628
|
+
index_definition: String(transformed.index_definition || ""),
|
|
25153
25629
|
supports_fk: toBool(transformed.supports_fk)
|
|
25154
25630
|
};
|
|
25155
25631
|
});
|
|
@@ -25903,7 +26379,7 @@ async function execPromise(command) {
|
|
|
25903
26379
|
childProcess.exec(command, (error2, stdout, stderr) => {
|
|
25904
26380
|
if (error2) {
|
|
25905
26381
|
const err = error2;
|
|
25906
|
-
err.code = error2.code
|
|
26382
|
+
err.code = typeof error2.code === "number" ? error2.code : 1;
|
|
25907
26383
|
reject(err);
|
|
25908
26384
|
} else {
|
|
25909
26385
|
resolve6({ stdout, stderr });
|
|
@@ -25916,7 +26392,7 @@ async function execFilePromise(file, args) {
|
|
|
25916
26392
|
childProcess.execFile(file, args, (error2, stdout, stderr) => {
|
|
25917
26393
|
if (error2) {
|
|
25918
26394
|
const err = error2;
|
|
25919
|
-
err.code = error2.code
|
|
26395
|
+
err.code = typeof error2.code === "number" ? error2.code : 1;
|
|
25920
26396
|
reject(err);
|
|
25921
26397
|
} else {
|
|
25922
26398
|
resolve6({ stdout, stderr });
|
|
@@ -26712,6 +27188,8 @@ program2.command("help", { isDefault: true }).description("show help").action(()
|
|
|
26712
27188
|
});
|
|
26713
27189
|
var mon = program2.command("mon").description("monitoring services management");
|
|
26714
27190
|
mon.command("local-install").description("install local monitoring stack (generate config, start services)").option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
27191
|
+
const globalOpts = program2.opts();
|
|
27192
|
+
const apiKey = opts.apiKey || globalOpts.apiKey;
|
|
26715
27193
|
console.log(`
|
|
26716
27194
|
=================================`);
|
|
26717
27195
|
console.log(" PostgresAI monitoring local install");
|
|
@@ -26723,14 +27201,28 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
26723
27201
|
console.log(`Project directory: ${projectDir}
|
|
26724
27202
|
`);
|
|
26725
27203
|
const envFile = path5.resolve(projectDir, ".env");
|
|
26726
|
-
|
|
26727
|
-
|
|
27204
|
+
let existingTag = null;
|
|
27205
|
+
let existingRegistry = null;
|
|
27206
|
+
let existingPassword = null;
|
|
26728
27207
|
if (fs5.existsSync(envFile)) {
|
|
26729
27208
|
const existingEnv = fs5.readFileSync(envFile, "utf8");
|
|
27209
|
+
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
27210
|
+
if (tagMatch)
|
|
27211
|
+
existingTag = tagMatch[1].trim();
|
|
27212
|
+
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
27213
|
+
if (registryMatch)
|
|
27214
|
+
existingRegistry = registryMatch[1].trim();
|
|
26730
27215
|
const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
|
|
26731
|
-
if (pwdMatch)
|
|
26732
|
-
|
|
26733
|
-
|
|
27216
|
+
if (pwdMatch)
|
|
27217
|
+
existingPassword = pwdMatch[1].trim();
|
|
27218
|
+
}
|
|
27219
|
+
const imageTag = opts.tag || existingTag || package_default.version;
|
|
27220
|
+
const envLines = [`PGAI_TAG=${imageTag}`];
|
|
27221
|
+
if (existingRegistry) {
|
|
27222
|
+
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
27223
|
+
}
|
|
27224
|
+
if (existingPassword) {
|
|
27225
|
+
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
26734
27226
|
}
|
|
26735
27227
|
fs5.writeFileSync(envFile, envLines.join(`
|
|
26736
27228
|
`) + `
|
|
@@ -26745,7 +27237,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
26745
27237
|
`);
|
|
26746
27238
|
opts.dbUrl = undefined;
|
|
26747
27239
|
}
|
|
26748
|
-
if (opts.demo &&
|
|
27240
|
+
if (opts.demo && apiKey) {
|
|
26749
27241
|
console.error("\u2717 Cannot use --api-key with --demo mode");
|
|
26750
27242
|
console.error("\u2717 Demo mode is for testing only and does not support API key integration");
|
|
26751
27243
|
console.error(`
|
|
@@ -26765,10 +27257,10 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
26765
27257
|
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
26766
27258
|
console.log(`An API key enables automatic upload of PostgreSQL reports to Postgres AI
|
|
26767
27259
|
`);
|
|
26768
|
-
if (
|
|
27260
|
+
if (apiKey) {
|
|
26769
27261
|
console.log("Using API key provided via --api-key parameter");
|
|
26770
|
-
writeConfig({ apiKey
|
|
26771
|
-
fs5.writeFileSync(path5.resolve(projectDir, ".pgwatch-config"), `api_key=${
|
|
27262
|
+
writeConfig({ apiKey });
|
|
27263
|
+
fs5.writeFileSync(path5.resolve(projectDir, ".pgwatch-config"), `api_key=${apiKey}
|
|
26772
27264
|
`, {
|
|
26773
27265
|
encoding: "utf8",
|
|
26774
27266
|
mode: 384
|
|
@@ -27535,7 +28027,7 @@ Please verify the --api-base-url parameter.`);
|
|
|
27535
28027
|
process.exit(1);
|
|
27536
28028
|
return;
|
|
27537
28029
|
}
|
|
27538
|
-
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
28030
|
+
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
|
|
27539
28031
|
if (opts.debug) {
|
|
27540
28032
|
console.log(`Debug: Auth URL: ${authUrl}`);
|
|
27541
28033
|
}
|
|
@@ -27780,7 +28272,7 @@ Grafana credentials:`);
|
|
|
27780
28272
|
console.log(` Password: ${password}`);
|
|
27781
28273
|
console.log("");
|
|
27782
28274
|
});
|
|
27783
|
-
function
|
|
28275
|
+
function interpretEscapes2(str2) {
|
|
27784
28276
|
return str2.replace(/\\\\/g, "\x00").replace(/\\n/g, `
|
|
27785
28277
|
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\x00/g, "\\");
|
|
27786
28278
|
}
|
|
@@ -27836,12 +28328,12 @@ issues.command("view <issueId>").description("view issue details and comments").
|
|
|
27836
28328
|
process.exitCode = 1;
|
|
27837
28329
|
}
|
|
27838
28330
|
});
|
|
27839
|
-
issues.command("
|
|
28331
|
+
issues.command("post-comment <issueId> <content>").description("post a new comment to an issue").option("--parent <uuid>", "parent comment id").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, content, opts) => {
|
|
27840
28332
|
try {
|
|
27841
28333
|
if (opts.debug) {
|
|
27842
28334
|
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
27843
28335
|
}
|
|
27844
|
-
content =
|
|
28336
|
+
content = interpretEscapes2(content);
|
|
27845
28337
|
if (opts.debug) {
|
|
27846
28338
|
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
27847
28339
|
}
|
|
@@ -27869,6 +28361,145 @@ issues.command("post_comment <issueId> <content>").description("post a new comme
|
|
|
27869
28361
|
process.exitCode = 1;
|
|
27870
28362
|
}
|
|
27871
28363
|
});
|
|
28364
|
+
issues.command("create <title>").description("create a new issue").option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10)).option("--project-id <id>", "project id", (v) => parseInt(v, 10)).option("--description <text>", "issue description (supports \\\\n)").option("--label <label>", "issue label (repeatable)", (value, previous) => {
|
|
28365
|
+
previous.push(value);
|
|
28366
|
+
return previous;
|
|
28367
|
+
}, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
|
|
28368
|
+
try {
|
|
28369
|
+
const rootOpts = program2.opts();
|
|
28370
|
+
const cfg = readConfig();
|
|
28371
|
+
const { apiKey } = getConfig(rootOpts);
|
|
28372
|
+
if (!apiKey) {
|
|
28373
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28374
|
+
process.exitCode = 1;
|
|
28375
|
+
return;
|
|
28376
|
+
}
|
|
28377
|
+
const title = interpretEscapes2(String(rawTitle || "").trim());
|
|
28378
|
+
if (!title) {
|
|
28379
|
+
console.error("title is required");
|
|
28380
|
+
process.exitCode = 1;
|
|
28381
|
+
return;
|
|
28382
|
+
}
|
|
28383
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
28384
|
+
if (typeof orgId !== "number") {
|
|
28385
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
28386
|
+
process.exitCode = 1;
|
|
28387
|
+
return;
|
|
28388
|
+
}
|
|
28389
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
28390
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
28391
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
28392
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28393
|
+
const result = await createIssue2({
|
|
28394
|
+
apiKey,
|
|
28395
|
+
apiBaseUrl,
|
|
28396
|
+
title,
|
|
28397
|
+
orgId,
|
|
28398
|
+
description,
|
|
28399
|
+
projectId,
|
|
28400
|
+
labels,
|
|
28401
|
+
debug: !!opts.debug
|
|
28402
|
+
});
|
|
28403
|
+
printResult(result, opts.json);
|
|
28404
|
+
} catch (err) {
|
|
28405
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28406
|
+
console.error(message);
|
|
28407
|
+
process.exitCode = 1;
|
|
28408
|
+
}
|
|
28409
|
+
});
|
|
28410
|
+
issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (supports \\\\n)").option("--description <text>", "new description (supports \\\\n)").option("--status <value>", "status: open|closed|0|1").option("--label <label>", "set labels (repeatable). If provided, replaces existing labels.", (value, previous) => {
|
|
28411
|
+
previous.push(value);
|
|
28412
|
+
return previous;
|
|
28413
|
+
}, []).option("--clear-labels", "set labels to an empty list").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
|
|
28414
|
+
try {
|
|
28415
|
+
const rootOpts = program2.opts();
|
|
28416
|
+
const cfg = readConfig();
|
|
28417
|
+
const { apiKey } = getConfig(rootOpts);
|
|
28418
|
+
if (!apiKey) {
|
|
28419
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28420
|
+
process.exitCode = 1;
|
|
28421
|
+
return;
|
|
28422
|
+
}
|
|
28423
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28424
|
+
const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
|
|
28425
|
+
const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
|
|
28426
|
+
let status = undefined;
|
|
28427
|
+
if (opts.status !== undefined) {
|
|
28428
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
28429
|
+
if (raw === "open")
|
|
28430
|
+
status = 0;
|
|
28431
|
+
else if (raw === "closed")
|
|
28432
|
+
status = 1;
|
|
28433
|
+
else {
|
|
28434
|
+
const n = Number(raw);
|
|
28435
|
+
if (!Number.isFinite(n)) {
|
|
28436
|
+
console.error("status must be open|closed|0|1");
|
|
28437
|
+
process.exitCode = 1;
|
|
28438
|
+
return;
|
|
28439
|
+
}
|
|
28440
|
+
status = n;
|
|
28441
|
+
}
|
|
28442
|
+
if (status !== 0 && status !== 1) {
|
|
28443
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
28444
|
+
process.exitCode = 1;
|
|
28445
|
+
return;
|
|
28446
|
+
}
|
|
28447
|
+
}
|
|
28448
|
+
let labels = undefined;
|
|
28449
|
+
if (opts.clearLabels) {
|
|
28450
|
+
labels = [];
|
|
28451
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
28452
|
+
labels = opts.label.map(String);
|
|
28453
|
+
}
|
|
28454
|
+
const result = await updateIssue2({
|
|
28455
|
+
apiKey,
|
|
28456
|
+
apiBaseUrl,
|
|
28457
|
+
issueId,
|
|
28458
|
+
title,
|
|
28459
|
+
description,
|
|
28460
|
+
status,
|
|
28461
|
+
labels,
|
|
28462
|
+
debug: !!opts.debug
|
|
28463
|
+
});
|
|
28464
|
+
printResult(result, opts.json);
|
|
28465
|
+
} catch (err) {
|
|
28466
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28467
|
+
console.error(message);
|
|
28468
|
+
process.exitCode = 1;
|
|
28469
|
+
}
|
|
28470
|
+
});
|
|
28471
|
+
issues.command("update-comment <commentId> <content>").description("update an existing issue comment").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commentId, content, opts) => {
|
|
28472
|
+
try {
|
|
28473
|
+
if (opts.debug) {
|
|
28474
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
28475
|
+
}
|
|
28476
|
+
content = interpretEscapes2(content);
|
|
28477
|
+
if (opts.debug) {
|
|
28478
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
28479
|
+
}
|
|
28480
|
+
const rootOpts = program2.opts();
|
|
28481
|
+
const cfg = readConfig();
|
|
28482
|
+
const { apiKey } = getConfig(rootOpts);
|
|
28483
|
+
if (!apiKey) {
|
|
28484
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
28485
|
+
process.exitCode = 1;
|
|
28486
|
+
return;
|
|
28487
|
+
}
|
|
28488
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
28489
|
+
const result = await updateIssueComment2({
|
|
28490
|
+
apiKey,
|
|
28491
|
+
apiBaseUrl,
|
|
28492
|
+
commentId,
|
|
28493
|
+
content,
|
|
28494
|
+
debug: !!opts.debug
|
|
28495
|
+
});
|
|
28496
|
+
printResult(result, opts.json);
|
|
28497
|
+
} catch (err) {
|
|
28498
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28499
|
+
console.error(message);
|
|
28500
|
+
process.exitCode = 1;
|
|
28501
|
+
}
|
|
28502
|
+
});
|
|
27872
28503
|
var mcp = program2.command("mcp").description("MCP server integration");
|
|
27873
28504
|
mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
|
|
27874
28505
|
const rootOpts = program2.opts();
|