postgresai 0.12.0-beta.5 → 0.12.0-beta.7
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 +58 -2
- package/bin/postgres-ai.ts +619 -164
- package/dist/bin/postgres-ai.js +462 -33
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/issues.d.ts +69 -1
- package/dist/lib/issues.d.ts.map +1 -1
- package/dist/lib/issues.js +232 -1
- package/dist/lib/issues.js.map +1 -1
- package/dist/lib/mcp-server.d.ts.map +1 -1
- package/dist/lib/mcp-server.js +69 -15
- package/dist/lib/mcp-server.js.map +1 -1
- package/dist/package.json +1 -1
- package/lib/issues.ts +325 -3
- package/lib/mcp-server.ts +75 -17
- package/package.json +1 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -71,6 +71,25 @@ function getConfig(opts) {
|
|
|
71
71
|
}
|
|
72
72
|
return { apiKey };
|
|
73
73
|
}
|
|
74
|
+
// Human-friendly output helper: YAML for TTY by default, JSON when --json or non-TTY
|
|
75
|
+
function printResult(result, json) {
|
|
76
|
+
if (typeof result === "string") {
|
|
77
|
+
process.stdout.write(result);
|
|
78
|
+
if (!/\n$/.test(result))
|
|
79
|
+
console.log();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (json || !process.stdout.isTTY) {
|
|
83
|
+
console.log(JSON.stringify(result, null, 2));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
let text = yaml.dump(result);
|
|
87
|
+
if (Array.isArray(result)) {
|
|
88
|
+
text = text.replace(/\n- /g, "\n\n- ");
|
|
89
|
+
}
|
|
90
|
+
console.log(text);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
74
93
|
const program = new commander_1.Command();
|
|
75
94
|
program
|
|
76
95
|
.name("postgres-ai")
|
|
@@ -150,8 +169,9 @@ function checkRunningContainers() {
|
|
|
150
169
|
*/
|
|
151
170
|
async function runCompose(args) {
|
|
152
171
|
let composeFile;
|
|
172
|
+
let projectDir;
|
|
153
173
|
try {
|
|
154
|
-
({ composeFile } = resolvePaths());
|
|
174
|
+
({ composeFile, projectDir } = resolvePaths());
|
|
155
175
|
}
|
|
156
176
|
catch (error) {
|
|
157
177
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -171,8 +191,29 @@ async function runCompose(args) {
|
|
|
171
191
|
process.exitCode = 1;
|
|
172
192
|
return 1;
|
|
173
193
|
}
|
|
194
|
+
// Read Grafana password from .pgwatch-config and pass to Docker Compose
|
|
195
|
+
const env = { ...process.env };
|
|
196
|
+
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
197
|
+
if (fs.existsSync(cfgPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const stats = fs.statSync(cfgPath);
|
|
200
|
+
if (!stats.isDirectory()) {
|
|
201
|
+
const content = fs.readFileSync(cfgPath, "utf8");
|
|
202
|
+
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
203
|
+
if (match) {
|
|
204
|
+
env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
// If we can't read the config, continue without setting the password
|
|
210
|
+
}
|
|
211
|
+
}
|
|
174
212
|
return new Promise((resolve) => {
|
|
175
|
-
const child = (0, child_process_1.spawn)(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], {
|
|
213
|
+
const child = (0, child_process_1.spawn)(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], {
|
|
214
|
+
stdio: "inherit",
|
|
215
|
+
env: env
|
|
216
|
+
});
|
|
176
217
|
child.on("close", (code) => resolve(code || 0));
|
|
177
218
|
});
|
|
178
219
|
}
|
|
@@ -184,23 +225,284 @@ const mon = program.command("mon").description("monitoring services management")
|
|
|
184
225
|
mon
|
|
185
226
|
.command("quickstart")
|
|
186
227
|
.description("complete setup (generate config, start monitoring services)")
|
|
187
|
-
.option("--demo", "demo mode", false)
|
|
188
|
-
.
|
|
228
|
+
.option("--demo", "demo mode with sample database", false)
|
|
229
|
+
.option("--api-key <key>", "Postgres AI API key for automated report uploads")
|
|
230
|
+
.option("--db-url <url>", "PostgreSQL connection URL to monitor")
|
|
231
|
+
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
232
|
+
.action(async (opts) => {
|
|
233
|
+
console.log("\n=================================");
|
|
234
|
+
console.log(" PostgresAI Monitoring Quickstart");
|
|
235
|
+
console.log("=================================\n");
|
|
236
|
+
console.log("This will install, configure, and start the monitoring system\n");
|
|
237
|
+
// Validate conflicting options
|
|
238
|
+
if (opts.demo && opts.dbUrl) {
|
|
239
|
+
console.log("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
|
|
240
|
+
console.log("⚠ The --db-url will be ignored in demo mode.\n");
|
|
241
|
+
opts.dbUrl = undefined;
|
|
242
|
+
}
|
|
243
|
+
if (opts.demo && opts.apiKey) {
|
|
244
|
+
console.error("✗ Cannot use --api-key with --demo mode");
|
|
245
|
+
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
246
|
+
console.error("\nUse demo mode without API key: postgres-ai mon quickstart --demo");
|
|
247
|
+
console.error("Or use production mode with API key: postgres-ai mon quickstart --api-key=your_key");
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
189
251
|
// Check if containers are already running
|
|
190
252
|
const { running, containers } = checkRunningContainers();
|
|
191
253
|
if (running) {
|
|
192
|
-
console.log(
|
|
193
|
-
console.log("Use 'postgres-ai mon restart' to restart them");
|
|
254
|
+
console.log(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
|
|
255
|
+
console.log("Use 'postgres-ai mon restart' to restart them\n");
|
|
194
256
|
return;
|
|
195
257
|
}
|
|
258
|
+
// Step 1: API key configuration (only in production mode)
|
|
259
|
+
if (!opts.demo) {
|
|
260
|
+
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
261
|
+
console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
|
|
262
|
+
if (opts.apiKey) {
|
|
263
|
+
console.log("Using API key provided via --api-key parameter");
|
|
264
|
+
config.writeConfig({ apiKey: opts.apiKey });
|
|
265
|
+
console.log("✓ API key saved\n");
|
|
266
|
+
}
|
|
267
|
+
else if (opts.yes) {
|
|
268
|
+
// Auto-yes mode without API key - skip API key setup
|
|
269
|
+
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
270
|
+
console.log("⚠ Reports will be generated locally only");
|
|
271
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const rl = readline.createInterface({
|
|
275
|
+
input: process.stdin,
|
|
276
|
+
output: process.stdout
|
|
277
|
+
});
|
|
278
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
279
|
+
try {
|
|
280
|
+
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
281
|
+
const proceedWithApiKey = !answer || answer.toLowerCase() === "y";
|
|
282
|
+
if (proceedWithApiKey) {
|
|
283
|
+
while (true) {
|
|
284
|
+
const inputApiKey = await question("Enter your Postgres AI API key: ");
|
|
285
|
+
const trimmedKey = inputApiKey.trim();
|
|
286
|
+
if (trimmedKey) {
|
|
287
|
+
config.writeConfig({ apiKey: trimmedKey });
|
|
288
|
+
console.log("✓ API key saved\n");
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
console.log("⚠ API key cannot be empty");
|
|
292
|
+
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
293
|
+
if (retry.toLowerCase() === "n") {
|
|
294
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
295
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
302
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
rl.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.log("Step 1: Demo mode - API key configuration skipped");
|
|
312
|
+
console.log("Demo mode is for testing only and does not support API key integration\n");
|
|
313
|
+
}
|
|
314
|
+
// Step 2: Add PostgreSQL instance (if not demo mode)
|
|
315
|
+
if (!opts.demo) {
|
|
316
|
+
console.log("Step 2: Add PostgreSQL Instance to Monitor\n");
|
|
317
|
+
// Clear instances.yml in production mode (start fresh)
|
|
318
|
+
const instancesPath = path.resolve(process.cwd(), "instances.yml");
|
|
319
|
+
const emptyInstancesContent = "# PostgreSQL instances to monitor\n# Add your instances using: postgres-ai mon targets add\n\n";
|
|
320
|
+
fs.writeFileSync(instancesPath, emptyInstancesContent, "utf8");
|
|
321
|
+
if (opts.dbUrl) {
|
|
322
|
+
console.log("Using database URL provided via --db-url parameter");
|
|
323
|
+
console.log(`Adding PostgreSQL instance from: ${opts.dbUrl}\n`);
|
|
324
|
+
const match = opts.dbUrl.match(/^postgresql:\/\/[^@]+@([^:/]+)/);
|
|
325
|
+
const autoInstanceName = match ? match[1] : "db-instance";
|
|
326
|
+
const connStr = opts.dbUrl;
|
|
327
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
328
|
+
if (!m) {
|
|
329
|
+
console.error("✗ Invalid connection string format");
|
|
330
|
+
process.exitCode = 1;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const host = m[3];
|
|
334
|
+
const db = m[5];
|
|
335
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
336
|
+
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
337
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
338
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
339
|
+
// Test connection
|
|
340
|
+
console.log("Testing connection to the added instance...");
|
|
341
|
+
try {
|
|
342
|
+
const { Client } = require("pg");
|
|
343
|
+
const client = new Client({ connectionString: connStr });
|
|
344
|
+
await client.connect();
|
|
345
|
+
const result = await client.query("select version();");
|
|
346
|
+
console.log("✓ Connection successful");
|
|
347
|
+
console.log(`${result.rows[0].version}\n`);
|
|
348
|
+
await client.end();
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
352
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (opts.yes) {
|
|
356
|
+
// Auto-yes mode without database URL - skip database setup
|
|
357
|
+
console.log("Auto-yes mode: no database URL provided, skipping database setup");
|
|
358
|
+
console.log("⚠ No PostgreSQL instance added");
|
|
359
|
+
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const rl = readline.createInterface({
|
|
363
|
+
input: process.stdin,
|
|
364
|
+
output: process.stdout
|
|
365
|
+
});
|
|
366
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
367
|
+
try {
|
|
368
|
+
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
369
|
+
const answer = await question("Do you want to add a PostgreSQL instance now? (Y/n): ");
|
|
370
|
+
const proceedWithInstance = !answer || answer.toLowerCase() === "y";
|
|
371
|
+
if (proceedWithInstance) {
|
|
372
|
+
console.log("\nYou can provide either:");
|
|
373
|
+
console.log(" 1. A full connection string: postgresql://user:pass@host:port/database");
|
|
374
|
+
console.log(" 2. Press Enter to skip for now\n");
|
|
375
|
+
const connStr = await question("Enter connection string (or press Enter to skip): ");
|
|
376
|
+
if (connStr.trim()) {
|
|
377
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
378
|
+
if (!m) {
|
|
379
|
+
console.error("✗ Invalid connection string format");
|
|
380
|
+
console.log("⚠ Continuing without adding instance\n");
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const host = m[3];
|
|
384
|
+
const db = m[5];
|
|
385
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
386
|
+
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
387
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
388
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
389
|
+
// Test connection
|
|
390
|
+
console.log("Testing connection to the added instance...");
|
|
391
|
+
try {
|
|
392
|
+
const { Client } = require("pg");
|
|
393
|
+
const client = new Client({ connectionString: connStr });
|
|
394
|
+
await client.connect();
|
|
395
|
+
const result = await client.query("select version();");
|
|
396
|
+
console.log("✓ Connection successful");
|
|
397
|
+
console.log(`${result.rows[0].version}\n`);
|
|
398
|
+
await client.end();
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
402
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
rl.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database\n");
|
|
421
|
+
}
|
|
422
|
+
// Step 3: Update configuration
|
|
423
|
+
console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
|
|
196
424
|
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
197
425
|
if (code1 !== 0) {
|
|
198
426
|
process.exitCode = code1;
|
|
199
427
|
return;
|
|
200
428
|
}
|
|
201
|
-
|
|
202
|
-
|
|
429
|
+
console.log("✓ Configuration updated\n");
|
|
430
|
+
// Step 4: Ensure Grafana password is configured
|
|
431
|
+
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
432
|
+
const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
|
|
433
|
+
let grafanaPassword = "";
|
|
434
|
+
try {
|
|
435
|
+
if (fs.existsSync(cfgPath)) {
|
|
436
|
+
const stats = fs.statSync(cfgPath);
|
|
437
|
+
if (!stats.isDirectory()) {
|
|
438
|
+
const content = fs.readFileSync(cfgPath, "utf8");
|
|
439
|
+
const match = content.match(/^grafana_password=([^\r\n]+)/m);
|
|
440
|
+
if (match) {
|
|
441
|
+
grafanaPassword = match[1].trim();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!grafanaPassword) {
|
|
446
|
+
console.log("Generating secure Grafana password...");
|
|
447
|
+
const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
|
|
448
|
+
grafanaPassword = password.trim();
|
|
449
|
+
let configContent = "";
|
|
450
|
+
if (fs.existsSync(cfgPath)) {
|
|
451
|
+
const stats = fs.statSync(cfgPath);
|
|
452
|
+
if (!stats.isDirectory()) {
|
|
453
|
+
configContent = fs.readFileSync(cfgPath, "utf8");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
|
|
457
|
+
lines.push(`grafana_password=${grafanaPassword}`);
|
|
458
|
+
fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
|
|
459
|
+
}
|
|
460
|
+
console.log("✓ Grafana password configured\n");
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.log("⚠ Could not generate Grafana password automatically");
|
|
464
|
+
console.log("Using default password: demo\n");
|
|
465
|
+
grafanaPassword = "demo";
|
|
466
|
+
}
|
|
467
|
+
// Step 5: Start services
|
|
468
|
+
console.log(opts.demo ? "Step 5: Starting monitoring services..." : "Step 5: Starting monitoring services...");
|
|
469
|
+
const code2 = await runCompose(["up", "-d", "--force-recreate"]);
|
|
470
|
+
if (code2 !== 0) {
|
|
203
471
|
process.exitCode = code2;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
console.log("✓ Services started\n");
|
|
475
|
+
// Final summary
|
|
476
|
+
console.log("=================================");
|
|
477
|
+
console.log(" 🎉 Quickstart setup completed!");
|
|
478
|
+
console.log("=================================\n");
|
|
479
|
+
console.log("What's running:");
|
|
480
|
+
if (opts.demo) {
|
|
481
|
+
console.log(" ✅ Demo PostgreSQL database (monitoring target)");
|
|
482
|
+
}
|
|
483
|
+
console.log(" ✅ PostgreSQL monitoring infrastructure");
|
|
484
|
+
console.log(" ✅ Grafana dashboards (with secure password)");
|
|
485
|
+
console.log(" ✅ Prometheus metrics storage");
|
|
486
|
+
console.log(" ✅ Flask API backend");
|
|
487
|
+
console.log(" ✅ Automated report generation (every 24h)");
|
|
488
|
+
console.log(" ✅ Host stats monitoring (CPU, memory, disk, I/O)\n");
|
|
489
|
+
if (!opts.demo) {
|
|
490
|
+
console.log("Next steps:");
|
|
491
|
+
console.log(" • Add more PostgreSQL instances: postgres-ai mon targets add");
|
|
492
|
+
console.log(" • View configured instances: postgres-ai mon targets list");
|
|
493
|
+
console.log(" • Check service health: postgres-ai mon health\n");
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
console.log("Demo mode next steps:");
|
|
497
|
+
console.log(" • Explore Grafana dashboards at http://localhost:3000");
|
|
498
|
+
console.log(" • Connect to demo database: postgresql://postgres:postgres@localhost:55432/target_database");
|
|
499
|
+
console.log(" • Generate some load on the demo database to see metrics\n");
|
|
500
|
+
}
|
|
501
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
502
|
+
console.log("🚀 MAIN ACCESS POINT - Start here:");
|
|
503
|
+
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
504
|
+
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
505
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
204
506
|
});
|
|
205
507
|
mon
|
|
206
508
|
.command("start")
|
|
@@ -267,10 +569,12 @@ mon
|
|
|
267
569
|
.option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
|
|
268
570
|
.action(async (opts) => {
|
|
269
571
|
const services = [
|
|
270
|
-
{ name: "Grafana",
|
|
271
|
-
{ name: "Prometheus",
|
|
272
|
-
{ name: "PGWatch (Postgres)",
|
|
273
|
-
{ name: "PGWatch (Prometheus)",
|
|
572
|
+
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
573
|
+
{ name: "Prometheus", container: "sink-prometheus" },
|
|
574
|
+
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
575
|
+
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
576
|
+
{ name: "Target DB", container: "target-db" },
|
|
577
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
274
578
|
];
|
|
275
579
|
const waitTime = opts.wait || 0;
|
|
276
580
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
@@ -284,19 +588,16 @@ mon
|
|
|
284
588
|
allHealthy = true;
|
|
285
589
|
for (const service of services) {
|
|
286
590
|
try {
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
clearTimeout(timeoutId);
|
|
295
|
-
if (response.status === 200) {
|
|
591
|
+
const { execSync } = require("child_process");
|
|
592
|
+
const status = execSync(`docker inspect -f '{{.State.Status}}' ${service.container} 2>/dev/null`, {
|
|
593
|
+
encoding: 'utf8',
|
|
594
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
595
|
+
}).trim();
|
|
596
|
+
if (status === 'running') {
|
|
296
597
|
console.log(`✓ ${service.name}: healthy`);
|
|
297
598
|
}
|
|
298
599
|
else {
|
|
299
|
-
console.log(`✗ ${service.name}: unhealthy (
|
|
600
|
+
console.log(`✗ ${service.name}: unhealthy (status: ${status})`);
|
|
300
601
|
allHealthy = false;
|
|
301
602
|
}
|
|
302
603
|
}
|
|
@@ -1019,12 +1320,30 @@ mon
|
|
|
1019
1320
|
console.log(` Password: ${password}`);
|
|
1020
1321
|
console.log("");
|
|
1021
1322
|
});
|
|
1323
|
+
/**
|
|
1324
|
+
* Interpret escape sequences in a string (e.g., \n -> newline)
|
|
1325
|
+
* Note: In regex, to match literal backslash-n, we need \\n in the pattern
|
|
1326
|
+
* which requires \\\\n in the JavaScript string literal
|
|
1327
|
+
*/
|
|
1328
|
+
function interpretEscapes(str) {
|
|
1329
|
+
// First handle double backslashes by temporarily replacing them
|
|
1330
|
+
// Then handle other escapes, then restore double backslashes as single
|
|
1331
|
+
return str
|
|
1332
|
+
.replace(/\\\\/g, '\x00') // Temporarily mark double backslashes
|
|
1333
|
+
.replace(/\\n/g, '\n') // Match literal backslash-n (\\\\n in JS string -> \\n in regex -> matches \n)
|
|
1334
|
+
.replace(/\\t/g, '\t')
|
|
1335
|
+
.replace(/\\r/g, '\r')
|
|
1336
|
+
.replace(/\\"/g, '"')
|
|
1337
|
+
.replace(/\\'/g, "'")
|
|
1338
|
+
.replace(/\x00/g, '\\'); // Restore double backslashes as single
|
|
1339
|
+
}
|
|
1022
1340
|
// Issues management
|
|
1023
1341
|
const issues = program.command("issues").description("issues management");
|
|
1024
1342
|
issues
|
|
1025
1343
|
.command("list")
|
|
1026
1344
|
.description("list issues")
|
|
1027
1345
|
.option("--debug", "enable debug output")
|
|
1346
|
+
.option("--json", "output raw JSON")
|
|
1028
1347
|
.action(async (opts) => {
|
|
1029
1348
|
try {
|
|
1030
1349
|
const rootOpts = program.opts();
|
|
@@ -1037,14 +1356,90 @@ issues
|
|
|
1037
1356
|
}
|
|
1038
1357
|
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1039
1358
|
const result = await (0, issues_1.fetchIssues)({ apiKey, apiBaseUrl, debug: !!opts.debug });
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1359
|
+
const trimmed = Array.isArray(result)
|
|
1360
|
+
? result.map((r) => ({
|
|
1361
|
+
id: r.id,
|
|
1362
|
+
title: r.title,
|
|
1363
|
+
status: r.status,
|
|
1364
|
+
created_at: r.created_at,
|
|
1365
|
+
}))
|
|
1366
|
+
: result;
|
|
1367
|
+
printResult(trimmed, opts.json);
|
|
1368
|
+
}
|
|
1369
|
+
catch (err) {
|
|
1370
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1371
|
+
console.error(message);
|
|
1372
|
+
process.exitCode = 1;
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
issues
|
|
1376
|
+
.command("view <issueId>")
|
|
1377
|
+
.description("view issue details and comments")
|
|
1378
|
+
.option("--debug", "enable debug output")
|
|
1379
|
+
.option("--json", "output raw JSON")
|
|
1380
|
+
.action(async (issueId, opts) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const rootOpts = program.opts();
|
|
1383
|
+
const cfg = config.readConfig();
|
|
1384
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1385
|
+
if (!apiKey) {
|
|
1386
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1387
|
+
process.exitCode = 1;
|
|
1388
|
+
return;
|
|
1044
1389
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1390
|
+
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1391
|
+
const issue = await (0, issues_1.fetchIssue)({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1392
|
+
if (!issue) {
|
|
1393
|
+
console.error("Issue not found");
|
|
1394
|
+
process.exitCode = 1;
|
|
1395
|
+
return;
|
|
1047
1396
|
}
|
|
1397
|
+
const comments = await (0, issues_1.fetchIssueComments)({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
1398
|
+
const combined = { issue, comments };
|
|
1399
|
+
printResult(combined, opts.json);
|
|
1400
|
+
}
|
|
1401
|
+
catch (err) {
|
|
1402
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1403
|
+
console.error(message);
|
|
1404
|
+
process.exitCode = 1;
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
issues
|
|
1408
|
+
.command("post_comment <issueId> <content>")
|
|
1409
|
+
.description("post a new comment to an issue")
|
|
1410
|
+
.option("--parent <uuid>", "parent comment id")
|
|
1411
|
+
.option("--debug", "enable debug output")
|
|
1412
|
+
.option("--json", "output raw JSON")
|
|
1413
|
+
.action(async (issueId, content, opts) => {
|
|
1414
|
+
try {
|
|
1415
|
+
// Interpret escape sequences in content (e.g., \n -> newline)
|
|
1416
|
+
if (opts.debug) {
|
|
1417
|
+
// eslint-disable-next-line no-console
|
|
1418
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
1419
|
+
}
|
|
1420
|
+
content = interpretEscapes(content);
|
|
1421
|
+
if (opts.debug) {
|
|
1422
|
+
// eslint-disable-next-line no-console
|
|
1423
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
1424
|
+
}
|
|
1425
|
+
const rootOpts = program.opts();
|
|
1426
|
+
const cfg = config.readConfig();
|
|
1427
|
+
const { apiKey } = getConfig(rootOpts);
|
|
1428
|
+
if (!apiKey) {
|
|
1429
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
1430
|
+
process.exitCode = 1;
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const { apiBaseUrl } = (0, util_2.resolveBaseUrls)(rootOpts, cfg);
|
|
1434
|
+
const result = await (0, issues_1.createIssueComment)({
|
|
1435
|
+
apiKey,
|
|
1436
|
+
apiBaseUrl,
|
|
1437
|
+
issueId,
|
|
1438
|
+
content,
|
|
1439
|
+
parentCommentId: opts.parent,
|
|
1440
|
+
debug: !!opts.debug,
|
|
1441
|
+
});
|
|
1442
|
+
printResult(result, opts.json);
|
|
1048
1443
|
}
|
|
1049
1444
|
catch (err) {
|
|
1050
1445
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1104,6 +1499,44 @@ mcp
|
|
|
1104
1499
|
return;
|
|
1105
1500
|
}
|
|
1106
1501
|
try {
|
|
1502
|
+
// Get the path to the current pgai executable
|
|
1503
|
+
let pgaiPath;
|
|
1504
|
+
try {
|
|
1505
|
+
const execPath = await execPromise("which pgai");
|
|
1506
|
+
pgaiPath = execPath.stdout.trim();
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
// Fallback to just "pgai" if which fails
|
|
1510
|
+
pgaiPath = "pgai";
|
|
1511
|
+
}
|
|
1512
|
+
// Claude Code uses its own CLI to manage MCP servers
|
|
1513
|
+
if (client === "claude-code") {
|
|
1514
|
+
console.log("Installing PostgresAI MCP server for Claude Code...");
|
|
1515
|
+
try {
|
|
1516
|
+
const { stdout, stderr } = await execPromise(`claude mcp add -s user postgresai ${pgaiPath} mcp start`);
|
|
1517
|
+
if (stdout)
|
|
1518
|
+
console.log(stdout);
|
|
1519
|
+
if (stderr)
|
|
1520
|
+
console.error(stderr);
|
|
1521
|
+
console.log("");
|
|
1522
|
+
console.log("Successfully installed PostgresAI MCP server for Claude Code");
|
|
1523
|
+
console.log("");
|
|
1524
|
+
console.log("Next steps:");
|
|
1525
|
+
console.log(" 1. Restart Claude Code to load the new configuration");
|
|
1526
|
+
console.log(" 2. The PostgresAI MCP server will be available as 'postgresai'");
|
|
1527
|
+
}
|
|
1528
|
+
catch (err) {
|
|
1529
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1530
|
+
console.error("Failed to install MCP server using Claude CLI");
|
|
1531
|
+
console.error(message);
|
|
1532
|
+
console.error("");
|
|
1533
|
+
console.error("Make sure the 'claude' CLI tool is installed and in your PATH");
|
|
1534
|
+
console.error("See: https://docs.anthropic.com/en/docs/build-with-claude/mcp");
|
|
1535
|
+
process.exitCode = 1;
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
// For other clients (Cursor, Windsurf, Codex), use JSON config editing
|
|
1107
1540
|
const homeDir = os.homedir();
|
|
1108
1541
|
let configPath;
|
|
1109
1542
|
let configDir;
|
|
@@ -1113,10 +1546,6 @@ mcp
|
|
|
1113
1546
|
configPath = path.join(homeDir, ".cursor", "mcp.json");
|
|
1114
1547
|
configDir = path.dirname(configPath);
|
|
1115
1548
|
break;
|
|
1116
|
-
case "claude-code":
|
|
1117
|
-
configPath = path.join(homeDir, ".claude-code", "mcp.json");
|
|
1118
|
-
configDir = path.dirname(configPath);
|
|
1119
|
-
break;
|
|
1120
1549
|
case "windsurf":
|
|
1121
1550
|
configPath = path.join(homeDir, ".windsurf", "mcp.json");
|
|
1122
1551
|
configDir = path.dirname(configPath);
|
|
@@ -1150,7 +1579,7 @@ mcp
|
|
|
1150
1579
|
}
|
|
1151
1580
|
// Add or update PostgresAI MCP server configuration
|
|
1152
1581
|
config.mcpServers.postgresai = {
|
|
1153
|
-
command:
|
|
1582
|
+
command: pgaiPath,
|
|
1154
1583
|
args: ["mcp", "start"]
|
|
1155
1584
|
};
|
|
1156
1585
|
// Write updated config
|