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

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);
@@ -154,10 +157,12 @@ program.command("help", { isDefault: true }).description("show help").action(()
154
157
  program.outputHelp();
155
158
  });
156
159
 
157
- // Service lifecycle
158
- program
160
+ // Monitoring services management
161
+ const mon = program.command("mon").description("monitoring services management");
162
+
163
+ mon
159
164
  .command("quickstart")
160
- .description("complete setup (generate config, start services)")
165
+ .description("complete setup (generate config, start monitoring services)")
161
166
  .option("--demo", "demo mode", false)
162
167
  .action(async () => {
163
168
  const code1 = await runCompose(["run", "--rm", "sources-generator"]);
@@ -168,47 +173,46 @@ program
168
173
  const code2 = await runCompose(["up", "-d"]);
169
174
  if (code2 !== 0) process.exitCode = code2;
170
175
  });
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
176
+
177
+ mon
178
178
  .command("start")
179
- .description("start services")
179
+ .description("start monitoring services")
180
180
  .action(async () => {
181
181
  const code = await runCompose(["up", "-d"]);
182
182
  if (code !== 0) process.exitCode = code;
183
183
  });
184
- program
184
+
185
+ mon
185
186
  .command("stop")
186
- .description("stop services")
187
+ .description("stop monitoring services")
187
188
  .action(async () => {
188
189
  const code = await runCompose(["down"]);
189
190
  if (code !== 0) process.exitCode = code;
190
191
  });
191
- program
192
+
193
+ mon
192
194
  .command("restart [service]")
193
- .description("restart all services or specific service")
195
+ .description("restart all monitoring services or specific service")
194
196
  .action(async (service?: string) => {
195
197
  const args = ["restart"];
196
198
  if (service) args.push(service);
197
199
  const code = await runCompose(args);
198
200
  if (code !== 0) process.exitCode = code;
199
201
  });
200
- program
202
+
203
+ mon
201
204
  .command("status")
202
- .description("show service status")
205
+ .description("show monitoring services status")
203
206
  .action(async () => {
204
207
  const code = await runCompose(["ps"]);
205
208
  if (code !== 0) process.exitCode = code;
206
209
  });
207
- program
210
+
211
+ mon
208
212
  .command("logs [service]")
209
213
  .option("-f, --follow", "follow logs", false)
210
214
  .option("--tail <lines>", "number of lines to show from the end of logs", "all")
211
- .description("show logs for all or specific service")
215
+ .description("show logs for all or specific monitoring service")
212
216
  .action(async (service: string | undefined, opts: { follow: boolean; tail: string }) => {
213
217
  const args: string[] = ["logs"];
214
218
  if (opts.follow) args.push("-f");
@@ -217,9 +221,9 @@ program
217
221
  const code = await runCompose(args);
218
222
  if (code !== 0) process.exitCode = code;
219
223
  });
220
- program
224
+ mon
221
225
  .command("health")
222
- .description("health check")
226
+ .description("health check for monitoring services")
223
227
  .option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
224
228
  .action(async (opts: { wait: number }) => {
225
229
  const services: HealthService[] = [
@@ -274,9 +278,9 @@ program
274
278
  process.exitCode = 1;
275
279
  }
276
280
  });
277
- program
281
+ mon
278
282
  .command("config")
279
- .description("show configuration")
283
+ .description("show monitoring services configuration")
280
284
  .action(async () => {
281
285
  const { fs, projectDir, composeFile, instancesFile } = resolvePaths();
282
286
  console.log(`Project Directory: ${projectDir}`);
@@ -289,16 +293,16 @@ program
289
293
  if (!/\n$/.test(text)) console.log();
290
294
  }
291
295
  });
292
- program
296
+ mon
293
297
  .command("update-config")
294
- .description("apply configuration (generate sources)")
298
+ .description("apply monitoring services configuration (generate sources)")
295
299
  .action(async () => {
296
300
  const code = await runCompose(["run", "--rm", "sources-generator"]);
297
301
  if (code !== 0) process.exitCode = code;
298
302
  });
299
- program
303
+ mon
300
304
  .command("update")
301
- .description("update project")
305
+ .description("update monitoring stack")
302
306
  .action(async () => {
303
307
  console.log("Updating PostgresAI monitoring stack...\n");
304
308
 
@@ -331,8 +335,8 @@ program
331
335
 
332
336
  if (code === 0) {
333
337
  console.log("\n✓ Update completed successfully");
334
- console.log("\nTo apply updates, restart services:");
335
- console.log(" postgres-ai restart");
338
+ console.log("\nTo apply updates, restart monitoring services:");
339
+ console.log(" postgres-ai mon restart");
336
340
  } else {
337
341
  console.error("\n✗ Docker image update failed");
338
342
  process.exitCode = 1;
@@ -343,9 +347,9 @@ program
343
347
  process.exitCode = 1;
344
348
  }
345
349
  });
346
- program
350
+ mon
347
351
  .command("reset [service]")
348
- .description("reset all or specific service")
352
+ .description("reset all or specific monitoring service")
349
353
  .action(async (service?: string) => {
350
354
  const rl = readline.createInterface({
351
355
  input: process.stdin,
@@ -414,9 +418,9 @@ program
414
418
  process.exitCode = 1;
415
419
  }
416
420
  });
417
- program
421
+ mon
418
422
  .command("clean")
419
- .description("cleanup artifacts")
423
+ .description("cleanup monitoring services artifacts")
420
424
  .action(async () => {
421
425
  console.log("Cleaning up Docker resources...\n");
422
426
 
@@ -450,25 +454,27 @@ program
450
454
  process.exitCode = 1;
451
455
  }
452
456
  });
453
- program
457
+ mon
454
458
  .command("shell <service>")
455
- .description("open service shell")
459
+ .description("open shell to monitoring service")
456
460
  .action(async (service: string) => {
457
461
  const code = await runCompose(["exec", service, "/bin/sh"]);
458
462
  if (code !== 0) process.exitCode = code;
459
463
  });
460
- program
464
+ mon
461
465
  .command("check")
462
- .description("system readiness check")
466
+ .description("monitoring services system readiness check")
463
467
  .action(async () => {
464
468
  const code = await runCompose(["ps"]);
465
469
  if (code !== 0) process.exitCode = code;
466
470
  });
467
471
 
468
- // Instance management
469
- program
470
- .command("list-instances")
471
- .description("list instances")
472
+ // Monitoring targets (databases to monitor)
473
+ const targets = mon.command("targets").description("manage databases to monitor");
474
+
475
+ targets
476
+ .command("list")
477
+ .description("list monitoring target databases")
472
478
  .action(async () => {
473
479
  const instancesPath = path.resolve(process.cwd(), "instances.yml");
474
480
  if (!fs.existsSync(instancesPath)) {
@@ -482,13 +488,13 @@ program
482
488
  const instances = yaml.load(content) as Instance[] | null;
483
489
 
484
490
  if (!instances || !Array.isArray(instances) || instances.length === 0) {
485
- console.log("No instances configured");
491
+ console.log("No monitoring targets configured");
486
492
  console.log("");
487
- console.log("To add an instance:");
488
- console.log(" postgres-ai add-instance <connection-string> <name>");
493
+ console.log("To add a monitoring target:");
494
+ console.log(" postgres-ai mon targets add <connection-string> <name>");
489
495
  console.log("");
490
496
  console.log("Example:");
491
- console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
497
+ console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
492
498
  return;
493
499
  }
494
500
 
@@ -496,18 +502,18 @@ program
496
502
  const filtered = instances.filter((inst) => inst.name && inst.name !== "target-database");
497
503
 
498
504
  if (filtered.length === 0) {
499
- console.log("No instances configured");
505
+ console.log("No monitoring targets configured");
500
506
  console.log("");
501
- console.log("To add an instance:");
502
- console.log(" postgres-ai add-instance <connection-string> <name>");
507
+ console.log("To add a monitoring target:");
508
+ console.log(" postgres-ai mon targets add <connection-string> <name>");
503
509
  console.log("");
504
510
  console.log("Example:");
505
- console.log(" postgres-ai add-instance 'postgresql://user:pass@host:5432/db' my-db");
511
+ console.log(" postgres-ai mon targets add 'postgresql://user:pass@host:5432/db' my-db");
506
512
  return;
507
513
  }
508
514
 
509
515
  for (const inst of filtered) {
510
- console.log(`Instance: ${inst.name}`);
516
+ console.log(`Target: ${inst.name}`);
511
517
  }
512
518
  } catch (err) {
513
519
  const message = err instanceof Error ? err.message : String(err);
@@ -515,9 +521,9 @@ program
515
521
  process.exitCode = 1;
516
522
  }
517
523
  });
518
- program
519
- .command("add-instance [connStr] [name]")
520
- .description("add instance")
524
+ targets
525
+ .command("add [connStr] [name]")
526
+ .description("add monitoring target database")
521
527
  .action(async (connStr?: string, name?: string) => {
522
528
  const file = path.resolve(process.cwd(), "instances.yml");
523
529
  if (!connStr) {
@@ -543,7 +549,7 @@ program
543
549
  if (Array.isArray(instances)) {
544
550
  const exists = instances.some((inst) => inst.name === instanceName);
545
551
  if (exists) {
546
- console.error(`Instance '${instanceName}' already exists`);
552
+ console.error(`Monitoring target '${instanceName}' already exists`);
547
553
  process.exitCode = 1;
548
554
  return;
549
555
  }
@@ -553,7 +559,7 @@ program
553
559
  // If YAML parsing fails, fall back to simple check
554
560
  const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
555
561
  if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
556
- console.error(`Instance '${instanceName}' already exists`);
562
+ console.error(`Monitoring target '${instanceName}' already exists`);
557
563
  process.exitCode = 1;
558
564
  return;
559
565
  }
@@ -563,11 +569,11 @@ program
563
569
  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
570
  const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
565
571
  fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
566
- console.log(`Instance '${instanceName}' added`);
572
+ console.log(`Monitoring target '${instanceName}' added`);
567
573
  });
568
- program
569
- .command("remove-instance <name>")
570
- .description("remove instance")
574
+ targets
575
+ .command("remove <name>")
576
+ .description("remove monitoring target database")
571
577
  .action(async (name: string) => {
572
578
  const file = path.resolve(process.cwd(), "instances.yml");
573
579
  if (!fs.existsSync(file)) {
@@ -589,22 +595,22 @@ program
589
595
  const filtered = instances.filter((inst) => inst.name !== name);
590
596
 
591
597
  if (filtered.length === instances.length) {
592
- console.error(`Instance '${name}' not found`);
598
+ console.error(`Monitoring target '${name}' not found`);
593
599
  process.exitCode = 1;
594
600
  return;
595
601
  }
596
602
 
597
603
  fs.writeFileSync(file, yaml.dump(filtered), "utf8");
598
- console.log(`Instance '${name}' removed`);
604
+ console.log(`Monitoring target '${name}' removed`);
599
605
  } catch (err) {
600
606
  const message = err instanceof Error ? err.message : String(err);
601
607
  console.error(`Error processing instances.yml: ${message}`);
602
608
  process.exitCode = 1;
603
609
  }
604
610
  });
605
- program
606
- .command("test-instance <name>")
607
- .description("test instance connectivity")
611
+ targets
612
+ .command("test <name>")
613
+ .description("test monitoring target database connectivity")
608
614
  .action(async (name: string) => {
609
615
  const instancesPath = path.resolve(process.cwd(), "instances.yml");
610
616
  if (!fs.existsSync(instancesPath)) {
@@ -626,18 +632,18 @@ program
626
632
  const instance = instances.find((inst) => inst.name === name);
627
633
 
628
634
  if (!instance) {
629
- console.error(`Instance '${name}' not found`);
635
+ console.error(`Monitoring target '${name}' not found`);
630
636
  process.exitCode = 1;
631
637
  return;
632
638
  }
633
639
 
634
640
  if (!instance.conn_str) {
635
- console.error(`Connection string not found for instance '${name}'`);
641
+ console.error(`Connection string not found for monitoring target '${name}'`);
636
642
  process.exitCode = 1;
637
643
  return;
638
644
  }
639
645
 
640
- console.log(`Testing connection to '${name}'...`);
646
+ console.log(`Testing connection to monitoring target '${name}'...`);
641
647
 
642
648
  const { stdout, stderr } = await execFilePromise(
643
649
  "psql",
@@ -669,9 +675,8 @@ program
669
675
  const params = pkce.generatePKCEParams();
670
676
 
671
677
  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(/\/$/, "");
678
+ const cfg = config.readConfig();
679
+ const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
675
680
 
676
681
  if (opts.debug) {
677
682
  console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
@@ -682,7 +687,7 @@ program
682
687
  // Step 1: Start local callback server FIRST to get actual port
683
688
  console.log("Starting local callback server...");
684
689
  const requestedPort = opts.port || 0; // 0 = OS assigns available port
685
- const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 300000);
690
+ const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
686
691
 
687
692
  // Wait a bit for server to start and get port
688
693
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -724,10 +729,20 @@ program
724
729
  res.on("end", async () => {
725
730
  if (res.statusCode !== 200) {
726
731
  console.error(`Failed to initialize auth session: ${res.statusCode}`);
727
- console.error(data);
732
+
733
+ // Check if response is HTML (common for 404 pages)
734
+ if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
735
+ console.error("Error: Received HTML response instead of JSON. This usually means:");
736
+ console.error(" 1. The API endpoint URL is incorrect");
737
+ console.error(" 2. The endpoint does not exist (404)");
738
+ console.error(`\nAPI URL attempted: ${initUrl.toString()}`);
739
+ console.error("\nPlease verify the --api-base-url parameter.");
740
+ } else {
741
+ console.error(data);
742
+ }
743
+
728
744
  callbackServer.server.close();
729
- process.exitCode = 1;
730
- return;
745
+ process.exit(1);
731
746
  }
732
747
 
733
748
  // Step 3: Open browser
@@ -748,9 +763,22 @@ program
748
763
 
749
764
  // Step 4: Wait for callback
750
765
  console.log("Waiting for authorization...");
766
+ console.log("(Press Ctrl+C to cancel)\n");
767
+
768
+ // Handle Ctrl+C gracefully
769
+ const cancelHandler = () => {
770
+ console.log("\n\nAuthentication cancelled by user.");
771
+ callbackServer.server.close();
772
+ process.exit(130); // Standard exit code for SIGINT
773
+ };
774
+ process.on("SIGINT", cancelHandler);
775
+
751
776
  try {
752
777
  const { code } = await callbackServer.promise;
753
778
 
779
+ // Remove the cancel handler after successful auth
780
+ process.off("SIGINT", cancelHandler);
781
+
754
782
  // Step 5: Exchange code for token
755
783
  console.log("\nExchanging authorization code for API token...");
756
784
  const exchangeData = JSON.stringify({
@@ -758,7 +786,6 @@ program
758
786
  code_verifier: params.codeVerifier,
759
787
  state: params.state,
760
788
  });
761
-
762
789
  const exchangeUrl = new URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
763
790
  const exchangeReq = http.request(
764
791
  exchangeUrl,
@@ -770,20 +797,31 @@ program
770
797
  },
771
798
  },
772
799
  (exchangeRes) => {
773
- let exchangeData = "";
774
- exchangeRes.on("data", (chunk) => (exchangeData += chunk));
800
+ let exchangeBody = "";
801
+ exchangeRes.on("data", (chunk) => (exchangeBody += chunk));
775
802
  exchangeRes.on("end", () => {
776
803
  if (exchangeRes.statusCode !== 200) {
777
804
  console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
778
- console.error(exchangeData);
779
- process.exitCode = 1;
805
+
806
+ // Check if response is HTML (common for 404 pages)
807
+ if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
808
+ console.error("Error: Received HTML response instead of JSON. This usually means:");
809
+ console.error(" 1. The API endpoint URL is incorrect");
810
+ console.error(" 2. The endpoint does not exist (404)");
811
+ console.error(`\nAPI URL attempted: ${exchangeUrl.toString()}`);
812
+ console.error("\nPlease verify the --api-base-url parameter.");
813
+ } else {
814
+ console.error(exchangeBody);
815
+ }
816
+
817
+ process.exit(1);
780
818
  return;
781
819
  }
782
820
 
783
821
  try {
784
- const result = JSON.parse(exchangeData);
785
- const apiToken = result.api_token;
786
- const orgId = result.org_id;
822
+ const result = JSON.parse(exchangeBody);
823
+ 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.
824
+ 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
825
 
788
826
  // Step 6: Save token to config
789
827
  config.writeConfig({
@@ -796,10 +834,11 @@ program
796
834
  console.log(`API key saved to: ${config.getConfigPath()}`);
797
835
  console.log(`Organization ID: ${orgId}`);
798
836
  console.log(`\nYou can now use the CLI without specifying an API key.`);
837
+ process.exit(0);
799
838
  } catch (err) {
800
839
  const message = err instanceof Error ? err.message : String(err);
801
840
  console.error(`Failed to parse response: ${message}`);
802
- process.exitCode = 1;
841
+ process.exit(1);
803
842
  }
804
843
  });
805
844
  }
@@ -807,16 +846,28 @@ program
807
846
 
808
847
  exchangeReq.on("error", (err: Error) => {
809
848
  console.error(`Exchange request failed: ${err.message}`);
810
- process.exitCode = 1;
849
+ process.exit(1);
811
850
  });
812
851
 
813
852
  exchangeReq.write(exchangeData);
814
853
  exchangeReq.end();
815
854
 
816
855
  } catch (err) {
856
+ // Remove the cancel handler in error case too
857
+ process.off("SIGINT", cancelHandler);
858
+
817
859
  const message = err instanceof Error ? err.message : String(err);
818
- console.error(`\nAuthentication failed: ${message}`);
819
- process.exitCode = 1;
860
+
861
+ // Provide more helpful error messages
862
+ if (message.includes("timeout")) {
863
+ console.error(`\nAuthentication timed out.`);
864
+ console.error(`This usually means you closed the browser window without completing authentication.`);
865
+ console.error(`Please try again and complete the authentication flow.`);
866
+ } else {
867
+ console.error(`\nAuthentication failed: ${message}`);
868
+ }
869
+
870
+ process.exit(1);
820
871
  }
821
872
  });
822
873
  }
@@ -825,7 +876,7 @@ program
825
876
  initReq.on("error", (err: Error) => {
826
877
  console.error(`Failed to connect to API: ${err.message}`);
827
878
  callbackServer.server.close();
828
- process.exitCode = 1;
879
+ process.exit(1);
829
880
  });
830
881
 
831
882
  initReq.write(initData);
@@ -834,7 +885,7 @@ program
834
885
  } catch (err) {
835
886
  const message = err instanceof Error ? err.message : String(err);
836
887
  console.error(`Authentication error: ${message}`);
837
- process.exitCode = 1;
888
+ process.exit(1);
838
889
  }
839
890
  });
840
891
 
@@ -856,13 +907,8 @@ program
856
907
  console.log(`\nTo authenticate, run: pgai auth`);
857
908
  return;
858
909
  }
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)}`);
910
+ const { maskSecret } = require("../lib/util");
911
+ console.log(`Current API key: ${maskSecret(cfg.apiKey)}`);
866
912
  if (cfg.orgId) {
867
913
  console.log(`Organization ID: ${cfg.orgId}`);
868
914
  }
@@ -908,9 +954,9 @@ program
908
954
  console.log("API key removed");
909
955
  console.log(`\nTo authenticate again, run: pgai auth`);
910
956
  });
911
- program
957
+ mon
912
958
  .command("generate-grafana-password")
913
- .description("generate Grafana password")
959
+ .description("generate Grafana password for monitoring services")
914
960
  .action(async () => {
915
961
  const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
916
962
 
@@ -950,8 +996,8 @@ program
950
996
  console.log(" URL: http://localhost:3000");
951
997
  console.log(" Username: monitor");
952
998
  console.log(` Password: ${newPassword}`);
953
- console.log("\nRestart Grafana to apply:");
954
- console.log(" postgres-ai restart grafana");
999
+ console.log("\nReset Grafana to apply new password:");
1000
+ console.log(" postgres-ai mon reset grafana");
955
1001
  } catch (error) {
956
1002
  const message = error instanceof Error ? error.message : String(error);
957
1003
  console.error(`Failed to generate password: ${message}`);
@@ -959,13 +1005,13 @@ program
959
1005
  process.exitCode = 1;
960
1006
  }
961
1007
  });
962
- program
1008
+ mon
963
1009
  .command("show-grafana-credentials")
964
- .description("show Grafana credentials")
1010
+ .description("show Grafana credentials for monitoring services")
965
1011
  .action(async () => {
966
1012
  const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
967
1013
  if (!fs.existsSync(cfgPath)) {
968
- console.error("Configuration file not found. Run 'quickstart' first.");
1014
+ console.error("Configuration file not found. Run 'postgres-ai mon quickstart' first.");
969
1015
  process.exitCode = 1;
970
1016
  return;
971
1017
  }
@@ -999,5 +1045,51 @@ program
999
1045
  console.log("");
1000
1046
  });
1001
1047
 
1048
+ // Issues management
1049
+ const issues = program.command("issues").description("issues management");
1050
+
1051
+ issues
1052
+ .command("list")
1053
+ .description("list issues")
1054
+ .option("--debug", "enable debug output")
1055
+ .action(async (opts: { debug?: boolean }) => {
1056
+ try {
1057
+ const rootOpts = program.opts<CliOptions>();
1058
+ const cfg = config.readConfig();
1059
+ const { apiKey } = getConfig(rootOpts);
1060
+ if (!apiKey) {
1061
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
1062
+ process.exitCode = 1;
1063
+ return;
1064
+ }
1065
+
1066
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
1067
+
1068
+ const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
1069
+ if (typeof result === "string") {
1070
+ process.stdout.write(result);
1071
+ if (!/\n$/.test(result)) console.log();
1072
+ } else {
1073
+ console.log(JSON.stringify(result, null, 2));
1074
+ }
1075
+ } catch (err) {
1076
+ const message = err instanceof Error ? err.message : String(err);
1077
+ console.error(message);
1078
+ process.exitCode = 1;
1079
+ }
1080
+ });
1081
+
1082
+ // MCP server
1083
+ const mcp = program.command("mcp").description("MCP server integration");
1084
+
1085
+ mcp
1086
+ .command("start")
1087
+ .description("start MCP stdio server")
1088
+ .option("--debug", "enable debug output")
1089
+ .action(async (opts: { debug?: boolean }) => {
1090
+ const rootOpts = program.opts<CliOptions>();
1091
+ await startMcpServer(rootOpts, { debug: !!opts.debug });
1092
+ });
1093
+
1002
1094
  program.parseAsync(process.argv);
1003
1095