postgresai 0.11.0-alpha.9 → 0.12.0-alpha.14

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.
@@ -12,6 +12,9 @@ import { promisify } from "util";
12
12
  import * as readline from "readline";
13
13
  import * as http from "https";
14
14
  import { URL } from "url";
15
+ import { startMcpServer } from "../lib/mcp-server";
16
+ import { fetchIssues } from "../lib/issues";
17
+ import { resolveBaseUrls } from "../lib/util";
15
18
 
16
19
  const execPromise = promisify(exec);
17
20
  const execFilePromise = promisify(execFile);
@@ -116,10 +119,24 @@ const stub = (name: string) => async (): Promise<void> => {
116
119
  * Resolve project paths
117
120
  */
118
121
  function resolvePaths(): PathResolution {
119
- const projectDir = process.cwd();
120
- const composeFile = path.resolve(projectDir, "docker-compose.yml");
121
- const instancesFile = path.resolve(projectDir, "instances.yml");
122
- return { fs, path, projectDir, composeFile, instancesFile };
122
+ const startDir = process.cwd();
123
+ let currentDir = startDir;
124
+
125
+ while (true) {
126
+ const composeFile = path.resolve(currentDir, "docker-compose.yml");
127
+ if (fs.existsSync(composeFile)) {
128
+ const instancesFile = path.resolve(currentDir, "instances.yml");
129
+ return { fs, path, projectDir: currentDir, composeFile, instancesFile };
130
+ }
131
+
132
+ const parentDir = path.dirname(currentDir);
133
+ if (parentDir === currentDir) break;
134
+ currentDir = parentDir;
135
+ }
136
+
137
+ throw new Error(
138
+ `docker-compose.yml not found. Run monitoring commands from the PostgresAI project directory or one of its subdirectories (starting search from ${startDir}).`
139
+ );
123
140
  }
124
141
 
125
142
  /**
@@ -137,7 +154,15 @@ function getComposeCmd(): string[] | null {
137
154
  * Run docker compose command
138
155
  */
139
156
  async function runCompose(args: string[]): Promise<number> {
140
- const { composeFile } = resolvePaths();
157
+ let composeFile: string;
158
+ try {
159
+ ({ composeFile } = resolvePaths());
160
+ } catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ console.error(message);
163
+ process.exitCode = 1;
164
+ return 1;
165
+ }
141
166
  const cmd = getComposeCmd();
142
167
  if (!cmd) {
143
168
  console.error("docker compose not found (need docker-compose or docker compose)");
@@ -154,10 +179,12 @@ program.command("help", { isDefault: true }).description("show help").action(()
154
179
  program.outputHelp();
155
180
  });
156
181
 
157
- // Service lifecycle
158
- program
182
+ // Monitoring services management
183
+ const mon = program.command("mon").description("monitoring services management");
184
+
185
+ mon
159
186
  .command("quickstart")
160
- .description("complete setup (generate config, start services)")
187
+ .description("complete setup (generate config, start monitoring services)")
161
188
  .option("--demo", "demo mode", false)
162
189
  .action(async () => {
163
190
  const code1 = await runCompose(["run", "--rm", "sources-generator"]);
@@ -168,47 +195,46 @@ program
168
195
  const code2 = await runCompose(["up", "-d"]);
169
196
  if (code2 !== 0) process.exitCode = code2;
170
197
  });
171
- program
172
- .command("install")
173
- .description("prepare project (no-op in repo checkout)")
174
- .action(async () => {
175
- console.log("Project files present; nothing to install.");
176
- });
177
- program
198
+
199
+ mon
178
200
  .command("start")
179
- .description("start services")
201
+ .description("start monitoring services")
180
202
  .action(async () => {
181
203
  const code = await runCompose(["up", "-d"]);
182
204
  if (code !== 0) process.exitCode = code;
183
205
  });
184
- program
206
+
207
+ mon
185
208
  .command("stop")
186
- .description("stop services")
209
+ .description("stop monitoring services")
187
210
  .action(async () => {
188
211
  const code = await runCompose(["down"]);
189
212
  if (code !== 0) process.exitCode = code;
190
213
  });
191
- program
214
+
215
+ mon
192
216
  .command("restart [service]")
193
- .description("restart all services or specific service")
217
+ .description("restart all monitoring services or specific service")
194
218
  .action(async (service?: string) => {
195
219
  const args = ["restart"];
196
220
  if (service) args.push(service);
197
221
  const code = await runCompose(args);
198
222
  if (code !== 0) process.exitCode = code;
199
223
  });
200
- program
224
+
225
+ mon
201
226
  .command("status")
202
- .description("show service status")
227
+ .description("show monitoring services status")
203
228
  .action(async () => {
204
229
  const code = await runCompose(["ps"]);
205
230
  if (code !== 0) process.exitCode = code;
206
231
  });
207
- program
232
+
233
+ mon
208
234
  .command("logs [service]")
209
235
  .option("-f, --follow", "follow logs", false)
210
236
  .option("--tail <lines>", "number of lines to show from the end of logs", "all")
211
- .description("show logs for all or specific service")
237
+ .description("show logs for all or specific monitoring service")
212
238
  .action(async (service: string | undefined, opts: { follow: boolean; tail: string }) => {
213
239
  const args: string[] = ["logs"];
214
240
  if (opts.follow) args.push("-f");
@@ -217,9 +243,9 @@ program
217
243
  const code = await runCompose(args);
218
244
  if (code !== 0) process.exitCode = code;
219
245
  });
220
- program
246
+ mon
221
247
  .command("health")
222
- .description("health check")
248
+ .description("health check for monitoring services")
223
249
  .option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
224
250
  .action(async (opts: { wait: number }) => {
225
251
  const services: HealthService[] = [
@@ -244,15 +270,20 @@ program
244
270
  allHealthy = true;
245
271
  for (const service of services) {
246
272
  try {
247
- const { stdout } = await execPromise(
248
- `curl -sf -o /dev/null -w "%{http_code}" ${service.url}`,
249
- { timeout: 5000 }
250
- );
251
- const code = stdout.trim();
252
- if (code === "200") {
273
+ // Use native fetch instead of requiring curl to be installed
274
+ const controller = new AbortController();
275
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
276
+
277
+ const response = await fetch(service.url, {
278
+ signal: controller.signal,
279
+ method: 'GET',
280
+ });
281
+ clearTimeout(timeoutId);
282
+
283
+ if (response.status === 200) {
253
284
  console.log(`✓ ${service.name}: healthy`);
254
285
  } else {
255
- console.log(`✗ ${service.name}: unhealthy (HTTP ${code})`);
286
+ console.log(`✗ ${service.name}: unhealthy (HTTP ${response.status})`);
256
287
  allHealthy = false;
257
288
  }
258
289
  } catch (error) {
@@ -274,11 +305,21 @@ program
274
305
  process.exitCode = 1;
275
306
  }
276
307
  });
277
- program
308
+ mon
278
309
  .command("config")
279
- .description("show configuration")
310
+ .description("show monitoring services configuration")
280
311
  .action(async () => {
281
- const { fs, projectDir, composeFile, instancesFile } = resolvePaths();
312
+ let projectDir: string;
313
+ let composeFile: string;
314
+ let instancesFile: string;
315
+ try {
316
+ ({ projectDir, composeFile, instancesFile } = resolvePaths());
317
+ } catch (error) {
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ console.error(message);
320
+ process.exitCode = 1;
321
+ return;
322
+ }
282
323
  console.log(`Project Directory: ${projectDir}`);
283
324
  console.log(`Docker Compose File: ${composeFile}`);
284
325
  console.log(`Instances File: ${instancesFile}`);
@@ -289,16 +330,16 @@ program
289
330
  if (!/\n$/.test(text)) console.log();
290
331
  }
291
332
  });
292
- program
333
+ mon
293
334
  .command("update-config")
294
- .description("apply configuration (generate sources)")
335
+ .description("apply monitoring services configuration (generate sources)")
295
336
  .action(async () => {
296
337
  const code = await runCompose(["run", "--rm", "sources-generator"]);
297
338
  if (code !== 0) process.exitCode = code;
298
339
  });
299
- program
340
+ mon
300
341
  .command("update")
301
- .description("update project")
342
+ .description("update monitoring stack")
302
343
  .action(async () => {
303
344
  console.log("Updating PostgresAI monitoring stack...\n");
304
345
 
@@ -331,8 +372,8 @@ program
331
372
 
332
373
  if (code === 0) {
333
374
  console.log("\n✓ Update completed successfully");
334
- console.log("\nTo apply updates, restart services:");
335
- console.log(" postgres-ai restart");
375
+ console.log("\nTo apply updates, restart monitoring services:");
376
+ console.log(" postgres-ai mon restart");
336
377
  } else {
337
378
  console.error("\n✗ Docker image update failed");
338
379
  process.exitCode = 1;
@@ -343,9 +384,9 @@ program
343
384
  process.exitCode = 1;
344
385
  }
345
386
  });
346
- program
387
+ mon
347
388
  .command("reset [service]")
348
- .description("reset all or specific service")
389
+ .description("reset all or specific monitoring service")
349
390
  .action(async (service?: string) => {
350
391
  const rl = readline.createInterface({
351
392
  input: process.stdin,
@@ -414,9 +455,9 @@ program
414
455
  process.exitCode = 1;
415
456
  }
416
457
  });
417
- program
458
+ mon
418
459
  .command("clean")
419
- .description("cleanup artifacts")
460
+ .description("cleanup monitoring services artifacts")
420
461
  .action(async () => {
421
462
  console.log("Cleaning up Docker resources...\n");
422
463
 
@@ -450,25 +491,27 @@ program
450
491
  process.exitCode = 1;
451
492
  }
452
493
  });
453
- program
494
+ mon
454
495
  .command("shell <service>")
455
- .description("open service shell")
496
+ .description("open shell to monitoring service")
456
497
  .action(async (service: string) => {
457
498
  const code = await runCompose(["exec", service, "/bin/sh"]);
458
499
  if (code !== 0) process.exitCode = code;
459
500
  });
460
- program
501
+ mon
461
502
  .command("check")
462
- .description("system readiness check")
503
+ .description("monitoring services system readiness check")
463
504
  .action(async () => {
464
505
  const code = await runCompose(["ps"]);
465
506
  if (code !== 0) process.exitCode = code;
466
507
  });
467
508
 
468
- // Instance management
469
- program
470
- .command("list-instances")
471
- .description("list instances")
509
+ // Monitoring targets (databases to monitor)
510
+ const targets = mon.command("targets").description("manage databases to monitor");
511
+
512
+ targets
513
+ .command("list")
514
+ .description("list monitoring target databases")
472
515
  .action(async () => {
473
516
  const instancesPath = path.resolve(process.cwd(), "instances.yml");
474
517
  if (!fs.existsSync(instancesPath)) {
@@ -482,32 +525,32 @@ program
482
525
  const instances = yaml.load(content) as Instance[] | null;
483
526
 
484
527
  if (!instances || !Array.isArray(instances) || instances.length === 0) {
485
- console.log("No instances configured");
528
+ console.log("No monitoring targets configured");
486
529
  console.log("");
487
- console.log("To add an instance:");
488
- console.log(" postgres-ai add-instance <connection-string> <name>");
530
+ console.log("To add a monitoring target:");
531
+ console.log(" postgres-ai mon targets add <connection-string> <name>");
489
532
  console.log("");
490
533
  console.log("Example:");
491
- console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
534
+ console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
492
535
  return;
493
536
  }
494
537
 
495
- // Filter out demo placeholder
496
- const filtered = instances.filter((inst) => inst.name && inst.name !== "target-database");
538
+ // Filter out disabled instances (e.g., demo placeholders)
539
+ const filtered = instances.filter((inst) => inst.name && inst.is_enabled !== false);
497
540
 
498
541
  if (filtered.length === 0) {
499
- console.log("No instances configured");
542
+ console.log("No monitoring targets configured");
500
543
  console.log("");
501
- console.log("To add an instance:");
502
- console.log(" postgres-ai add-instance <connection-string> <name>");
544
+ console.log("To add a monitoring target:");
545
+ console.log(" postgres-ai mon targets add <connection-string> <name>");
503
546
  console.log("");
504
547
  console.log("Example:");
505
- console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
548
+ console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
506
549
  return;
507
550
  }
508
551
 
509
552
  for (const inst of filtered) {
510
- console.log(`Instance: ${inst.name}`);
553
+ console.log(`Target: ${inst.name}`);
511
554
  }
512
555
  } catch (err) {
513
556
  const message = err instanceof Error ? err.message : String(err);
@@ -515,9 +558,9 @@ program
515
558
  process.exitCode = 1;
516
559
  }
517
560
  });
518
- program
519
- .command("add-instance [connStr] [name]")
520
- .description("add instance")
561
+ targets
562
+ .command("add [connStr] [name]")
563
+ .description("add monitoring target database")
521
564
  .action(async (connStr?: string, name?: string) => {
522
565
  const file = path.resolve(process.cwd(), "instances.yml");
523
566
  if (!connStr) {
@@ -543,7 +586,7 @@ program
543
586
  if (Array.isArray(instances)) {
544
587
  const exists = instances.some((inst) => inst.name === instanceName);
545
588
  if (exists) {
546
- console.error(`Instance '${instanceName}' already exists`);
589
+ console.error(`Monitoring target '${instanceName}' already exists`);
547
590
  process.exitCode = 1;
548
591
  return;
549
592
  }
@@ -553,7 +596,7 @@ program
553
596
  // If YAML parsing fails, fall back to simple check
554
597
  const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
555
598
  if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
556
- console.error(`Instance '${instanceName}' already exists`);
599
+ console.error(`Monitoring target '${instanceName}' already exists`);
557
600
  process.exitCode = 1;
558
601
  return;
559
602
  }
@@ -563,11 +606,11 @@ program
563
606
  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`;
564
607
  const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
565
608
  fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
566
- console.log(`Instance '${instanceName}' added`);
609
+ console.log(`Monitoring target '${instanceName}' added`);
567
610
  });
568
- program
569
- .command("remove-instance <name>")
570
- .description("remove instance")
611
+ targets
612
+ .command("remove <name>")
613
+ .description("remove monitoring target database")
571
614
  .action(async (name: string) => {
572
615
  const file = path.resolve(process.cwd(), "instances.yml");
573
616
  if (!fs.existsSync(file)) {
@@ -589,22 +632,22 @@ program
589
632
  const filtered = instances.filter((inst) => inst.name !== name);
590
633
 
591
634
  if (filtered.length === instances.length) {
592
- console.error(`Instance '${name}' not found`);
635
+ console.error(`Monitoring target '${name}' not found`);
593
636
  process.exitCode = 1;
594
637
  return;
595
638
  }
596
639
 
597
640
  fs.writeFileSync(file, yaml.dump(filtered), "utf8");
598
- console.log(`Instance '${name}' removed`);
641
+ console.log(`Monitoring target '${name}' removed`);
599
642
  } catch (err) {
600
643
  const message = err instanceof Error ? err.message : String(err);
601
644
  console.error(`Error processing instances.yml: ${message}`);
602
645
  process.exitCode = 1;
603
646
  }
604
647
  });
605
- program
606
- .command("test-instance <name>")
607
- .description("test instance connectivity")
648
+ targets
649
+ .command("test <name>")
650
+ .description("test monitoring target database connectivity")
608
651
  .action(async (name: string) => {
609
652
  const instancesPath = path.resolve(process.cwd(), "instances.yml");
610
653
  if (!fs.existsSync(instancesPath)) {
@@ -626,26 +669,31 @@ program
626
669
  const instance = instances.find((inst) => inst.name === name);
627
670
 
628
671
  if (!instance) {
629
- console.error(`Instance '${name}' not found`);
672
+ console.error(`Monitoring target '${name}' not found`);
630
673
  process.exitCode = 1;
631
674
  return;
632
675
  }
633
676
 
634
677
  if (!instance.conn_str) {
635
- console.error(`Connection string not found for instance '${name}'`);
678
+ console.error(`Connection string not found for monitoring target '${name}'`);
636
679
  process.exitCode = 1;
637
680
  return;
638
681
  }
639
682
 
640
- console.log(`Testing connection to '${name}'...`);
683
+ console.log(`Testing connection to monitoring target '${name}'...`);
641
684
 
642
- const { stdout, stderr } = await execFilePromise(
643
- "psql",
644
- [instance.conn_str, "-c", "SELECT version();", "--no-psqlrc"],
645
- { timeout: 10000, env: { ...process.env, PAGER: 'cat' } }
646
- );
647
- console.log(`✓ Connection successful`);
648
- console.log(stdout.trim());
685
+ // Use native pg client instead of requiring psql to be installed
686
+ const { Client } = require('pg');
687
+ const client = new Client({ connectionString: instance.conn_str });
688
+
689
+ try {
690
+ await client.connect();
691
+ const result = await client.query('select version();');
692
+ console.log(`✓ Connection successful`);
693
+ console.log(result.rows[0].version);
694
+ } finally {
695
+ await client.end();
696
+ }
649
697
  } catch (error) {
650
698
  const message = error instanceof Error ? error.message : String(error);
651
699
  console.error(`✗ Connection failed: ${message}`);
@@ -669,9 +717,8 @@ program
669
717
  const params = pkce.generatePKCEParams();
670
718
 
671
719
  const rootOpts = program.opts<CliOptions>();
672
-
673
- const apiBaseUrl = (rootOpts.apiBaseUrl || process.env.PGAI_API_BASE_URL || "https://postgres.ai/api/general/").replace(/\/$/, "");
674
- const uiBaseUrl = (rootOpts.uiBaseUrl || process.env.PGAI_UI_BASE_URL || "https://console.postgres.ai").replace(/\/$/, "");
720
+ const cfg = config.readConfig();
721
+ const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
675
722
 
676
723
  if (opts.debug) {
677
724
  console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
@@ -682,7 +729,7 @@ program
682
729
  // Step 1: Start local callback server FIRST to get actual port
683
730
  console.log("Starting local callback server...");
684
731
  const requestedPort = opts.port || 0; // 0 = OS assigns available port
685
- const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 300000);
732
+ const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
686
733
 
687
734
  // Wait a bit for server to start and get port
688
735
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -724,10 +771,20 @@ program
724
771
  res.on("end", async () => {
725
772
  if (res.statusCode !== 200) {
726
773
  console.error(`Failed to initialize auth session: ${res.statusCode}`);
727
- console.error(data);
774
+
775
+ // Check if response is HTML (common for 404 pages)
776
+ if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
777
+ console.error("Error: Received HTML response instead of JSON. This usually means:");
778
+ console.error(" 1. The API endpoint URL is incorrect");
779
+ console.error(" 2. The endpoint does not exist (404)");
780
+ console.error(`\nAPI URL attempted: ${initUrl.toString()}`);
781
+ console.error("\nPlease verify the --api-base-url parameter.");
782
+ } else {
783
+ console.error(data);
784
+ }
785
+
728
786
  callbackServer.server.close();
729
- process.exitCode = 1;
730
- return;
787
+ process.exit(1);
731
788
  }
732
789
 
733
790
  // Step 3: Open browser
@@ -748,9 +805,22 @@ program
748
805
 
749
806
  // Step 4: Wait for callback
750
807
  console.log("Waiting for authorization...");
808
+ console.log("(Press Ctrl+C to cancel)\n");
809
+
810
+ // Handle Ctrl+C gracefully
811
+ const cancelHandler = () => {
812
+ console.log("\n\nAuthentication cancelled by user.");
813
+ callbackServer.server.close();
814
+ process.exit(130); // Standard exit code for SIGINT
815
+ };
816
+ process.on("SIGINT", cancelHandler);
817
+
751
818
  try {
752
819
  const { code } = await callbackServer.promise;
753
820
 
821
+ // Remove the cancel handler after successful auth
822
+ process.off("SIGINT", cancelHandler);
823
+
754
824
  // Step 5: Exchange code for token
755
825
  console.log("\nExchanging authorization code for API token...");
756
826
  const exchangeData = JSON.stringify({
@@ -758,7 +828,6 @@ program
758
828
  code_verifier: params.codeVerifier,
759
829
  state: params.state,
760
830
  });
761
-
762
831
  const exchangeUrl = new URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
763
832
  const exchangeReq = http.request(
764
833
  exchangeUrl,
@@ -770,20 +839,31 @@ program
770
839
  },
771
840
  },
772
841
  (exchangeRes) => {
773
- let exchangeData = "";
774
- exchangeRes.on("data", (chunk) => (exchangeData += chunk));
842
+ let exchangeBody = "";
843
+ exchangeRes.on("data", (chunk) => (exchangeBody += chunk));
775
844
  exchangeRes.on("end", () => {
776
845
  if (exchangeRes.statusCode !== 200) {
777
846
  console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
778
- console.error(exchangeData);
779
- process.exitCode = 1;
847
+
848
+ // Check if response is HTML (common for 404 pages)
849
+ if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
850
+ console.error("Error: Received HTML response instead of JSON. This usually means:");
851
+ console.error(" 1. The API endpoint URL is incorrect");
852
+ console.error(" 2. The endpoint does not exist (404)");
853
+ console.error(`\nAPI URL attempted: ${exchangeUrl.toString()}`);
854
+ console.error("\nPlease verify the --api-base-url parameter.");
855
+ } else {
856
+ console.error(exchangeBody);
857
+ }
858
+
859
+ process.exit(1);
780
860
  return;
781
861
  }
782
862
 
783
863
  try {
784
- const result = JSON.parse(exchangeData);
785
- const apiToken = result.api_token;
786
- const orgId = result.org_id;
864
+ const result = JSON.parse(exchangeBody);
865
+ const apiToken = result.api_token || result?.[0]?.result?.api_token; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
866
+ const orgId = result.org_id || result?.[0]?.result?.org_id; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
787
867
 
788
868
  // Step 6: Save token to config
789
869
  config.writeConfig({
@@ -796,10 +876,11 @@ program
796
876
  console.log(`API key saved to: ${config.getConfigPath()}`);
797
877
  console.log(`Organization ID: ${orgId}`);
798
878
  console.log(`\nYou can now use the CLI without specifying an API key.`);
879
+ process.exit(0);
799
880
  } catch (err) {
800
881
  const message = err instanceof Error ? err.message : String(err);
801
882
  console.error(`Failed to parse response: ${message}`);
802
- process.exitCode = 1;
883
+ process.exit(1);
803
884
  }
804
885
  });
805
886
  }
@@ -807,16 +888,28 @@ program
807
888
 
808
889
  exchangeReq.on("error", (err: Error) => {
809
890
  console.error(`Exchange request failed: ${err.message}`);
810
- process.exitCode = 1;
891
+ process.exit(1);
811
892
  });
812
893
 
813
894
  exchangeReq.write(exchangeData);
814
895
  exchangeReq.end();
815
896
 
816
897
  } catch (err) {
898
+ // Remove the cancel handler in error case too
899
+ process.off("SIGINT", cancelHandler);
900
+
817
901
  const message = err instanceof Error ? err.message : String(err);
818
- console.error(`\nAuthentication failed: ${message}`);
819
- process.exitCode = 1;
902
+
903
+ // Provide more helpful error messages
904
+ if (message.includes("timeout")) {
905
+ console.error(`\nAuthentication timed out.`);
906
+ console.error(`This usually means you closed the browser window without completing authentication.`);
907
+ console.error(`Please try again and complete the authentication flow.`);
908
+ } else {
909
+ console.error(`\nAuthentication failed: ${message}`);
910
+ }
911
+
912
+ process.exit(1);
820
913
  }
821
914
  });
822
915
  }
@@ -825,7 +918,7 @@ program
825
918
  initReq.on("error", (err: Error) => {
826
919
  console.error(`Failed to connect to API: ${err.message}`);
827
920
  callbackServer.server.close();
828
- process.exitCode = 1;
921
+ process.exit(1);
829
922
  });
830
923
 
831
924
  initReq.write(initData);
@@ -834,7 +927,7 @@ program
834
927
  } catch (err) {
835
928
  const message = err instanceof Error ? err.message : String(err);
836
929
  console.error(`Authentication error: ${message}`);
837
- process.exitCode = 1;
930
+ process.exit(1);
838
931
  }
839
932
  });
840
933
 
@@ -856,13 +949,8 @@ program
856
949
  console.log(`\nTo authenticate, run: pgai auth`);
857
950
  return;
858
951
  }
859
- const mask = (k: string): string => {
860
- if (k.length <= 8) return "****";
861
- if (k.length <= 16) return `${k.slice(0, 4)}${"*".repeat(k.length - 8)}${k.slice(-4)}`;
862
- // For longer keys, show more of the beginning to help identify them
863
- return `${k.slice(0, Math.min(12, k.length - 8))}${"*".repeat(Math.max(4, k.length - 16))}${k.slice(-4)}`;
864
- };
865
- console.log(`Current API key: ${mask(cfg.apiKey)}`);
952
+ const { maskSecret } = require("../lib/util");
953
+ console.log(`Current API key: ${maskSecret(cfg.apiKey)}`);
866
954
  if (cfg.orgId) {
867
955
  console.log(`Organization ID: ${cfg.orgId}`);
868
956
  }
@@ -908,9 +996,9 @@ program
908
996
  console.log("API key removed");
909
997
  console.log(`\nTo authenticate again, run: pgai auth`);
910
998
  });
911
- program
999
+ mon
912
1000
  .command("generate-grafana-password")
913
- .description("generate Grafana password")
1001
+ .description("generate Grafana password for monitoring services")
914
1002
  .action(async () => {
915
1003
  const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
916
1004
 
@@ -950,8 +1038,8 @@ program
950
1038
  console.log(" URL: http://localhost:3000");
951
1039
  console.log(" Username: monitor");
952
1040
  console.log(` Password: ${newPassword}`);
953
- console.log("\nRestart Grafana to apply:");
954
- console.log(" postgres-ai restart grafana");
1041
+ console.log("\nReset Grafana to apply new password:");
1042
+ console.log(" postgres-ai mon reset grafana");
955
1043
  } catch (error) {
956
1044
  const message = error instanceof Error ? error.message : String(error);
957
1045
  console.error(`Failed to generate password: ${message}`);
@@ -959,13 +1047,13 @@ program
959
1047
  process.exitCode = 1;
960
1048
  }
961
1049
  });
962
- program
1050
+ mon
963
1051
  .command("show-grafana-credentials")
964
- .description("show Grafana credentials")
1052
+ .description("show Grafana credentials for monitoring services")
965
1053
  .action(async () => {
966
1054
  const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
967
1055
  if (!fs.existsSync(cfgPath)) {
968
- console.error("Configuration file not found. Run 'quickstart' first.");
1056
+ console.error("Configuration file not found. Run 'postgres-ai mon quickstart' first.");
969
1057
  process.exitCode = 1;
970
1058
  return;
971
1059
  }
@@ -999,5 +1087,51 @@ program
999
1087
  console.log("");
1000
1088
  });
1001
1089
 
1090
+ // Issues management
1091
+ const issues = program.command("issues").description("issues management");
1092
+
1093
+ issues
1094
+ .command("list")
1095
+ .description("list issues")
1096
+ .option("--debug", "enable debug output")
1097
+ .action(async (opts: { debug?: boolean }) => {
1098
+ try {
1099
+ const rootOpts = program.opts<CliOptions>();
1100
+ const cfg = config.readConfig();
1101
+ const { apiKey } = getConfig(rootOpts);
1102
+ if (!apiKey) {
1103
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
1104
+ process.exitCode = 1;
1105
+ return;
1106
+ }
1107
+
1108
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
1109
+
1110
+ const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
1111
+ if (typeof result === "string") {
1112
+ process.stdout.write(result);
1113
+ if (!/\n$/.test(result)) console.log();
1114
+ } else {
1115
+ console.log(JSON.stringify(result, null, 2));
1116
+ }
1117
+ } catch (err) {
1118
+ const message = err instanceof Error ? err.message : String(err);
1119
+ console.error(message);
1120
+ process.exitCode = 1;
1121
+ }
1122
+ });
1123
+
1124
+ // MCP server
1125
+ const mcp = program.command("mcp").description("MCP server integration");
1126
+
1127
+ mcp
1128
+ .command("start")
1129
+ .description("start MCP stdio server")
1130
+ .option("--debug", "enable debug output")
1131
+ .action(async (opts: { debug?: boolean }) => {
1132
+ const rootOpts = program.opts<CliOptions>();
1133
+ await startMcpServer(rootOpts, { debug: !!opts.debug });
1134
+ });
1135
+
1002
1136
  program.parseAsync(process.argv);
1003
1137