postgresai 0.12.0-beta.6 → 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 CHANGED
@@ -55,7 +55,19 @@ Start monitoring with demo database:
55
55
  postgres-ai mon quickstart --demo
56
56
  ```
57
57
 
58
+ Start monitoring with your own database:
59
+ ```bash
60
+ postgres-ai mon quickstart --db-url postgresql://user:pass@host:5432/db
61
+ ```
62
+
63
+ Complete automated setup with API key and database:
64
+ ```bash
65
+ postgres-ai mon quickstart --api-key your_key --db-url postgresql://user:pass@host:5432/db -y
66
+ ```
67
+
58
68
  This will:
69
+ - Configure API key for automated report uploads (if provided)
70
+ - Add PostgreSQL instance to monitor (if provided)
59
71
  - Generate secure Grafana password
60
72
  - Start all monitoring services
61
73
  - Open Grafana at http://localhost:3000
@@ -66,7 +78,15 @@ This will:
66
78
 
67
79
  #### Service lifecycle
68
80
  ```bash
69
- postgres-ai mon quickstart [--demo] # Complete setup (generate config, start services)
81
+ # Complete setup with various options
82
+ postgres-ai mon quickstart # Interactive setup for production
83
+ postgres-ai mon quickstart --demo # Demo mode with sample database
84
+ postgres-ai mon quickstart --api-key <key> # Setup with API key
85
+ postgres-ai mon quickstart --db-url <url> # Setup with database URL
86
+ postgres-ai mon quickstart --api-key <key> --db-url <url> # Complete automated setup
87
+ postgres-ai mon quickstart -y # Auto-accept all defaults
88
+
89
+ # Service management
70
90
  postgres-ai mon start # Start monitoring services
71
91
  postgres-ai mon stop # Stop monitoring services
72
92
  postgres-ai mon restart [service] # Restart all or specific monitoring service
@@ -74,6 +94,12 @@ postgres-ai mon status # Show monitoring services status
74
94
  postgres-ai mon health [--wait <sec>] # Check monitoring services health
75
95
  ```
76
96
 
97
+ ##### Quickstart options
98
+ - `--demo` - Demo mode with sample database (testing only, cannot use with --api-key)
99
+ - `--api-key <key>` - Postgres AI API key for automated report uploads
100
+ - `--db-url <url>` - PostgreSQL connection URL to monitor (format: `postgresql://user:pass@host:port/db`)
101
+ - `-y, --yes` - Accept all defaults and skip interactive prompts
102
+
77
103
  #### Monitoring target databases (`mon targets` subgroup)
78
104
  ```bash
79
105
  postgres-ai mon targets list # List databases to monitor
@@ -96,7 +122,7 @@ postgres-ai mon shell <service> # Open shell to monitoring servic
96
122
  ### MCP server (`mcp` group)
97
123
 
98
124
  ```bash
99
- pgai mcp start # Start MCP stdio server exposing list_issues tool
125
+ pgai mcp start # Start MCP stdio server exposing tools
100
126
  ```
101
127
 
102
128
  Cursor configuration example (Settings → MCP):
@@ -117,6 +143,36 @@ Cursor configuration example (Settings → MCP):
117
143
 
118
144
  Tools exposed:
119
145
  - list_issues: returns the same JSON as `pgai issues list`.
146
+ - view_issue: view a single issue with its comments (args: { issue_id, debug? })
147
+ - post_issue_comment: post a comment (args: { issue_id, content, parent_comment_id?, debug? })
148
+
149
+ ### Issues management (`issues` group)
150
+
151
+ ```bash
152
+ pgai issues list # List issues (shows: id, title, status, created_at)
153
+ pgai issues view <issueId> # View issue details and comments
154
+ pgai issues post_comment <issueId> <content> # Post a comment to an issue
155
+ # Options:
156
+ # --parent <uuid> Parent comment ID (for replies)
157
+ # --debug Enable debug output
158
+ # --json Output raw JSON (overrides default YAML)
159
+ ```
160
+
161
+ #### Output format for issues commands
162
+
163
+ By default, issues commands print human-friendly YAML when writing to a terminal. For scripting, you can:
164
+
165
+ - Use `--json` to force JSON output:
166
+
167
+ ```bash
168
+ pgai issues list --json | jq '.[] | {id, title}'
169
+ ```
170
+
171
+ - Rely on auto-detection: when stdout is not a TTY (e.g., piped or redirected), output is JSON automatically:
172
+
173
+ ```bash
174
+ pgai issues view <issueId> > issue.json
175
+ ```
120
176
 
121
177
  #### Grafana management
122
178
  ```bash
@@ -13,7 +13,7 @@ import * as readline from "readline";
13
13
  import * as http from "https";
14
14
  import { URL } from "url";
15
15
  import { startMcpServer } from "../lib/mcp-server";
16
- import { fetchIssues } from "../lib/issues";
16
+ import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
17
17
  import { resolveBaseUrls } from "../lib/util";
18
18
 
19
19
  const execPromise = promisify(exec);
@@ -59,14 +59,6 @@ interface PathResolution {
59
59
  instancesFile: string;
60
60
  }
61
61
 
62
- /**
63
- * Health check service
64
- */
65
- interface HealthService {
66
- name: string;
67
- url: string;
68
- }
69
-
70
62
  /**
71
63
  * Get configuration from various sources
72
64
  * @param opts - Command line options
@@ -90,6 +82,24 @@ function getConfig(opts: CliOptions): ConfigResult {
90
82
  return { apiKey };
91
83
  }
92
84
 
85
+ // Human-friendly output helper: YAML for TTY by default, JSON when --json or non-TTY
86
+ function printResult(result: unknown, json?: boolean): void {
87
+ if (typeof result === "string") {
88
+ process.stdout.write(result);
89
+ if (!/\n$/.test(result)) console.log();
90
+ return;
91
+ }
92
+ if (json || !process.stdout.isTTY) {
93
+ console.log(JSON.stringify(result, null, 2));
94
+ } else {
95
+ let text = yaml.dump(result as any);
96
+ if (Array.isArray(result)) {
97
+ text = text.replace(/\n- /g, "\n\n- ");
98
+ }
99
+ console.log(text);
100
+ }
101
+ }
102
+
93
103
  const program = new Command();
94
104
 
95
105
  program
@@ -249,23 +259,308 @@ const mon = program.command("mon").description("monitoring services management")
249
259
  mon
250
260
  .command("quickstart")
251
261
  .description("complete setup (generate config, start monitoring services)")
252
- .option("--demo", "demo mode", false)
253
- .action(async () => {
262
+ .option("--demo", "demo mode with sample database", false)
263
+ .option("--api-key <key>", "Postgres AI API key for automated report uploads")
264
+ .option("--db-url <url>", "PostgreSQL connection URL to monitor")
265
+ .option("-y, --yes", "accept all defaults and skip interactive prompts", false)
266
+ .action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; yes: boolean }) => {
267
+ console.log("\n=================================");
268
+ console.log(" PostgresAI Monitoring Quickstart");
269
+ console.log("=================================\n");
270
+ console.log("This will install, configure, and start the monitoring system\n");
271
+
272
+ // Validate conflicting options
273
+ if (opts.demo && opts.dbUrl) {
274
+ console.log("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
275
+ console.log("⚠ The --db-url will be ignored in demo mode.\n");
276
+ opts.dbUrl = undefined;
277
+ }
278
+
279
+ if (opts.demo && opts.apiKey) {
280
+ console.error("✗ Cannot use --api-key with --demo mode");
281
+ console.error("✗ Demo mode is for testing only and does not support API key integration");
282
+ console.error("\nUse demo mode without API key: postgres-ai mon quickstart --demo");
283
+ console.error("Or use production mode with API key: postgres-ai mon quickstart --api-key=your_key");
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+
254
288
  // Check if containers are already running
255
289
  const { running, containers } = checkRunningContainers();
256
290
  if (running) {
257
- console.log(`Monitoring services are already running: ${containers.join(", ")}`);
258
- console.log("Use 'postgres-ai mon restart' to restart them");
291
+ console.log(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
292
+ console.log("Use 'postgres-ai mon restart' to restart them\n");
259
293
  return;
260
294
  }
261
295
 
296
+ // Step 1: API key configuration (only in production mode)
297
+ if (!opts.demo) {
298
+ console.log("Step 1: Postgres AI API Configuration (Optional)");
299
+ console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
300
+
301
+ if (opts.apiKey) {
302
+ console.log("Using API key provided via --api-key parameter");
303
+ config.writeConfig({ apiKey: opts.apiKey });
304
+ console.log("✓ API key saved\n");
305
+ } else if (opts.yes) {
306
+ // Auto-yes mode without API key - skip API key setup
307
+ console.log("Auto-yes mode: no API key provided, skipping API key setup");
308
+ console.log("⚠ Reports will be generated locally only");
309
+ console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
310
+ } else {
311
+ const rl = readline.createInterface({
312
+ input: process.stdin,
313
+ output: process.stdout
314
+ });
315
+
316
+ const question = (prompt: string): Promise<string> =>
317
+ new Promise((resolve) => rl.question(prompt, resolve));
318
+
319
+ try {
320
+ const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
321
+ const proceedWithApiKey = !answer || answer.toLowerCase() === "y";
322
+
323
+ if (proceedWithApiKey) {
324
+ while (true) {
325
+ const inputApiKey = await question("Enter your Postgres AI API key: ");
326
+ const trimmedKey = inputApiKey.trim();
327
+
328
+ if (trimmedKey) {
329
+ config.writeConfig({ apiKey: trimmedKey });
330
+ console.log("✓ API key saved\n");
331
+ break;
332
+ }
333
+
334
+ console.log("⚠ API key cannot be empty");
335
+ const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
336
+ if (retry.toLowerCase() === "n") {
337
+ console.log("⚠ Skipping API key setup - reports will be generated locally only");
338
+ console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
339
+ break;
340
+ }
341
+ }
342
+ } else {
343
+ console.log("⚠ Skipping API key setup - reports will be generated locally only");
344
+ console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
345
+ }
346
+ } finally {
347
+ rl.close();
348
+ }
349
+ }
350
+ } else {
351
+ console.log("Step 1: Demo mode - API key configuration skipped");
352
+ console.log("Demo mode is for testing only and does not support API key integration\n");
353
+ }
354
+
355
+ // Step 2: Add PostgreSQL instance (if not demo mode)
356
+ if (!opts.demo) {
357
+ console.log("Step 2: Add PostgreSQL Instance to Monitor\n");
358
+
359
+ // Clear instances.yml in production mode (start fresh)
360
+ const instancesPath = path.resolve(process.cwd(), "instances.yml");
361
+ const emptyInstancesContent = "# PostgreSQL instances to monitor\n# Add your instances using: postgres-ai mon targets add\n\n";
362
+ fs.writeFileSync(instancesPath, emptyInstancesContent, "utf8");
363
+
364
+ if (opts.dbUrl) {
365
+ console.log("Using database URL provided via --db-url parameter");
366
+ console.log(`Adding PostgreSQL instance from: ${opts.dbUrl}\n`);
367
+
368
+ const match = opts.dbUrl.match(/^postgresql:\/\/[^@]+@([^:/]+)/);
369
+ const autoInstanceName = match ? match[1] : "db-instance";
370
+
371
+ const connStr = opts.dbUrl;
372
+ const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
373
+
374
+ if (!m) {
375
+ console.error("✗ Invalid connection string format");
376
+ process.exitCode = 1;
377
+ return;
378
+ }
379
+
380
+ const host = m[3];
381
+ const db = m[5];
382
+ const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
383
+
384
+ 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`;
385
+ fs.appendFileSync(instancesPath, body, "utf8");
386
+ console.log(`✓ Monitoring target '${instanceName}' added\n`);
387
+
388
+ // Test connection
389
+ console.log("Testing connection to the added instance...");
390
+ try {
391
+ const { Client } = require("pg");
392
+ const client = new Client({ connectionString: connStr });
393
+ await client.connect();
394
+ const result = await client.query("select version();");
395
+ console.log("✓ Connection successful");
396
+ console.log(`${result.rows[0].version}\n`);
397
+ await client.end();
398
+ } catch (error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ console.error(`✗ Connection failed: ${message}\n`);
401
+ }
402
+ } else if (opts.yes) {
403
+ // Auto-yes mode without database URL - skip database setup
404
+ console.log("Auto-yes mode: no database URL provided, skipping database setup");
405
+ console.log("⚠ No PostgreSQL instance added");
406
+ console.log("You can add one later with: postgres-ai mon targets add\n");
407
+ } else {
408
+ const rl = readline.createInterface({
409
+ input: process.stdin,
410
+ output: process.stdout
411
+ });
412
+
413
+ const question = (prompt: string): Promise<string> =>
414
+ new Promise((resolve) => rl.question(prompt, resolve));
415
+
416
+ try {
417
+ console.log("You need to add at least one PostgreSQL instance to monitor");
418
+ const answer = await question("Do you want to add a PostgreSQL instance now? (Y/n): ");
419
+ const proceedWithInstance = !answer || answer.toLowerCase() === "y";
420
+
421
+ if (proceedWithInstance) {
422
+ console.log("\nYou can provide either:");
423
+ console.log(" 1. A full connection string: postgresql://user:pass@host:port/database");
424
+ console.log(" 2. Press Enter to skip for now\n");
425
+
426
+ const connStr = await question("Enter connection string (or press Enter to skip): ");
427
+
428
+ if (connStr.trim()) {
429
+ const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
430
+ if (!m) {
431
+ console.error("✗ Invalid connection string format");
432
+ console.log("⚠ Continuing without adding instance\n");
433
+ } else {
434
+ const host = m[3];
435
+ const db = m[5];
436
+ const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
437
+
438
+ 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`;
439
+ fs.appendFileSync(instancesPath, body, "utf8");
440
+ console.log(`✓ Monitoring target '${instanceName}' added\n`);
441
+
442
+ // Test connection
443
+ console.log("Testing connection to the added instance...");
444
+ try {
445
+ const { Client } = require("pg");
446
+ const client = new Client({ connectionString: connStr });
447
+ await client.connect();
448
+ const result = await client.query("select version();");
449
+ console.log("✓ Connection successful");
450
+ console.log(`${result.rows[0].version}\n`);
451
+ await client.end();
452
+ } catch (error) {
453
+ const message = error instanceof Error ? error.message : String(error);
454
+ console.error(`✗ Connection failed: ${message}\n`);
455
+ }
456
+ }
457
+ } else {
458
+ console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
459
+ }
460
+ } else {
461
+ console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
462
+ }
463
+ } finally {
464
+ rl.close();
465
+ }
466
+ }
467
+ } else {
468
+ console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database\n");
469
+ }
470
+
471
+ // Step 3: Update configuration
472
+ console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
262
473
  const code1 = await runCompose(["run", "--rm", "sources-generator"]);
263
474
  if (code1 !== 0) {
264
475
  process.exitCode = code1;
265
476
  return;
266
477
  }
267
- const code2 = await runCompose(["up", "-d"]);
268
- if (code2 !== 0) process.exitCode = code2;
478
+ console.log(" Configuration updated\n");
479
+
480
+ // Step 4: Ensure Grafana password is configured
481
+ console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
482
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
483
+ let grafanaPassword = "";
484
+
485
+ try {
486
+ if (fs.existsSync(cfgPath)) {
487
+ const stats = fs.statSync(cfgPath);
488
+ if (!stats.isDirectory()) {
489
+ const content = fs.readFileSync(cfgPath, "utf8");
490
+ const match = content.match(/^grafana_password=([^\r\n]+)/m);
491
+ if (match) {
492
+ grafanaPassword = match[1].trim();
493
+ }
494
+ }
495
+ }
496
+
497
+ if (!grafanaPassword) {
498
+ console.log("Generating secure Grafana password...");
499
+ const { stdout: password } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
500
+ grafanaPassword = password.trim();
501
+
502
+ let configContent = "";
503
+ if (fs.existsSync(cfgPath)) {
504
+ const stats = fs.statSync(cfgPath);
505
+ if (!stats.isDirectory()) {
506
+ configContent = fs.readFileSync(cfgPath, "utf8");
507
+ }
508
+ }
509
+
510
+ const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
511
+ lines.push(`grafana_password=${grafanaPassword}`);
512
+ fs.writeFileSync(cfgPath, lines.filter(Boolean).join("\n") + "\n", "utf8");
513
+ }
514
+
515
+ console.log("✓ Grafana password configured\n");
516
+ } catch (error) {
517
+ console.log("⚠ Could not generate Grafana password automatically");
518
+ console.log("Using default password: demo\n");
519
+ grafanaPassword = "demo";
520
+ }
521
+
522
+ // Step 5: Start services
523
+ console.log(opts.demo ? "Step 5: Starting monitoring services..." : "Step 5: Starting monitoring services...");
524
+ const code2 = await runCompose(["up", "-d", "--force-recreate"]);
525
+ if (code2 !== 0) {
526
+ process.exitCode = code2;
527
+ return;
528
+ }
529
+ console.log("✓ Services started\n");
530
+
531
+ // Final summary
532
+ console.log("=================================");
533
+ console.log(" 🎉 Quickstart setup completed!");
534
+ console.log("=================================\n");
535
+
536
+ console.log("What's running:");
537
+ if (opts.demo) {
538
+ console.log(" ✅ Demo PostgreSQL database (monitoring target)");
539
+ }
540
+ console.log(" ✅ PostgreSQL monitoring infrastructure");
541
+ console.log(" ✅ Grafana dashboards (with secure password)");
542
+ console.log(" ✅ Prometheus metrics storage");
543
+ console.log(" ✅ Flask API backend");
544
+ console.log(" ✅ Automated report generation (every 24h)");
545
+ console.log(" ✅ Host stats monitoring (CPU, memory, disk, I/O)\n");
546
+
547
+ if (!opts.demo) {
548
+ console.log("Next steps:");
549
+ console.log(" • Add more PostgreSQL instances: postgres-ai mon targets add");
550
+ console.log(" • View configured instances: postgres-ai mon targets list");
551
+ console.log(" • Check service health: postgres-ai mon health\n");
552
+ } else {
553
+ console.log("Demo mode next steps:");
554
+ console.log(" • Explore Grafana dashboards at http://localhost:3000");
555
+ console.log(" • Connect to demo database: postgresql://postgres:postgres@localhost:55432/target_database");
556
+ console.log(" • Generate some load on the demo database to see metrics\n");
557
+ }
558
+
559
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
560
+ console.log("🚀 MAIN ACCESS POINT - Start here:");
561
+ console.log(" Grafana Dashboard: http://localhost:3000");
562
+ console.log(` Login: monitor / ${grafanaPassword}`);
563
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
269
564
  });
270
565
 
271
566
  mon
@@ -328,11 +623,13 @@ mon
328
623
  .description("health check for monitoring services")
329
624
  .option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0)
330
625
  .action(async (opts: { wait: number }) => {
331
- const services: HealthService[] = [
332
- { name: "Grafana", url: "http://localhost:3000/api/health" },
333
- { name: "Prometheus", url: "http://localhost:59090/-/healthy" },
334
- { name: "PGWatch (Postgres)", url: "http://localhost:58080/health" },
335
- { name: "PGWatch (Prometheus)", url: "http://localhost:58089/health" },
626
+ const services = [
627
+ { name: "Grafana", container: "grafana-with-datasources" },
628
+ { name: "Prometheus", container: "sink-prometheus" },
629
+ { name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
630
+ { name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
631
+ { name: "Target DB", container: "target-db" },
632
+ { name: "Sink Postgres", container: "sink-postgres" },
336
633
  ];
337
634
 
338
635
  const waitTime = opts.wait || 0;
@@ -350,20 +647,16 @@ mon
350
647
  allHealthy = true;
351
648
  for (const service of services) {
352
649
  try {
353
- // Use native fetch instead of requiring curl to be installed
354
- const controller = new AbortController();
355
- const timeoutId = setTimeout(() => controller.abort(), 5000);
356
-
357
- const response = await fetch(service.url, {
358
- signal: controller.signal,
359
- method: 'GET',
360
- });
361
- clearTimeout(timeoutId);
650
+ const { execSync } = require("child_process");
651
+ const status = execSync(`docker inspect -f '{{.State.Status}}' ${service.container} 2>/dev/null`, {
652
+ encoding: 'utf8',
653
+ stdio: ['pipe', 'pipe', 'pipe']
654
+ }).trim();
362
655
 
363
- if (response.status === 200) {
656
+ if (status === 'running') {
364
657
  console.log(`✓ ${service.name}: healthy`);
365
658
  } else {
366
- console.log(`✗ ${service.name}: unhealthy (HTTP ${response.status})`);
659
+ console.log(`✗ ${service.name}: unhealthy (status: ${status})`);
367
660
  allHealthy = false;
368
661
  }
369
662
  } catch (error) {
@@ -1167,6 +1460,24 @@ mon
1167
1460
  console.log("");
1168
1461
  });
1169
1462
 
1463
+ /**
1464
+ * Interpret escape sequences in a string (e.g., \n -> newline)
1465
+ * Note: In regex, to match literal backslash-n, we need \\n in the pattern
1466
+ * which requires \\\\n in the JavaScript string literal
1467
+ */
1468
+ function interpretEscapes(str: string): string {
1469
+ // First handle double backslashes by temporarily replacing them
1470
+ // Then handle other escapes, then restore double backslashes as single
1471
+ return str
1472
+ .replace(/\\\\/g, '\x00') // Temporarily mark double backslashes
1473
+ .replace(/\\n/g, '\n') // Match literal backslash-n (\\\\n in JS string -> \\n in regex -> matches \n)
1474
+ .replace(/\\t/g, '\t')
1475
+ .replace(/\\r/g, '\r')
1476
+ .replace(/\\"/g, '"')
1477
+ .replace(/\\'/g, "'")
1478
+ .replace(/\x00/g, '\\'); // Restore double backslashes as single
1479
+ }
1480
+
1170
1481
  // Issues management
1171
1482
  const issues = program.command("issues").description("issues management");
1172
1483
 
@@ -1174,7 +1485,8 @@ issues
1174
1485
  .command("list")
1175
1486
  .description("list issues")
1176
1487
  .option("--debug", "enable debug output")
1177
- .action(async (opts: { debug?: boolean }) => {
1488
+ .option("--json", "output raw JSON")
1489
+ .action(async (opts: { debug?: boolean; json?: boolean }) => {
1178
1490
  try {
1179
1491
  const rootOpts = program.opts<CliOptions>();
1180
1492
  const cfg = config.readConfig();
@@ -1188,12 +1500,96 @@ issues
1188
1500
  const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
1189
1501
 
1190
1502
  const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
1191
- if (typeof result === "string") {
1192
- process.stdout.write(result);
1193
- if (!/\n$/.test(result)) console.log();
1194
- } else {
1195
- console.log(JSON.stringify(result, null, 2));
1503
+ const trimmed = Array.isArray(result)
1504
+ ? (result as any[]).map((r) => ({
1505
+ id: (r as any).id,
1506
+ title: (r as any).title,
1507
+ status: (r as any).status,
1508
+ created_at: (r as any).created_at,
1509
+ }))
1510
+ : result;
1511
+ printResult(trimmed, opts.json);
1512
+ } catch (err) {
1513
+ const message = err instanceof Error ? err.message : String(err);
1514
+ console.error(message);
1515
+ process.exitCode = 1;
1516
+ }
1517
+ });
1518
+
1519
+ issues
1520
+ .command("view <issueId>")
1521
+ .description("view issue details and comments")
1522
+ .option("--debug", "enable debug output")
1523
+ .option("--json", "output raw JSON")
1524
+ .action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
1525
+ try {
1526
+ const rootOpts = program.opts<CliOptions>();
1527
+ const cfg = config.readConfig();
1528
+ const { apiKey } = getConfig(rootOpts);
1529
+ if (!apiKey) {
1530
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
1531
+ process.exitCode = 1;
1532
+ return;
1533
+ }
1534
+
1535
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
1536
+
1537
+ const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
1538
+ if (!issue) {
1539
+ console.error("Issue not found");
1540
+ process.exitCode = 1;
1541
+ return;
1196
1542
  }
1543
+
1544
+ const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
1545
+ const combined = { issue, comments };
1546
+ printResult(combined, opts.json);
1547
+ } catch (err) {
1548
+ const message = err instanceof Error ? err.message : String(err);
1549
+ console.error(message);
1550
+ process.exitCode = 1;
1551
+ }
1552
+ });
1553
+
1554
+ issues
1555
+ .command("post_comment <issueId> <content>")
1556
+ .description("post a new comment to an issue")
1557
+ .option("--parent <uuid>", "parent comment id")
1558
+ .option("--debug", "enable debug output")
1559
+ .option("--json", "output raw JSON")
1560
+ .action(async (issueId: string, content: string, opts: { parent?: string; debug?: boolean; json?: boolean }) => {
1561
+ try {
1562
+ // Interpret escape sequences in content (e.g., \n -> newline)
1563
+ if (opts.debug) {
1564
+ // eslint-disable-next-line no-console
1565
+ console.log(`Debug: Original content: ${JSON.stringify(content)}`);
1566
+ }
1567
+ content = interpretEscapes(content);
1568
+ if (opts.debug) {
1569
+ // eslint-disable-next-line no-console
1570
+ console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
1571
+ }
1572
+
1573
+ const rootOpts = program.opts<CliOptions>();
1574
+ const cfg = config.readConfig();
1575
+ const { apiKey } = getConfig(rootOpts);
1576
+ if (!apiKey) {
1577
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
1578
+ process.exitCode = 1;
1579
+ return;
1580
+ }
1581
+
1582
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
1583
+
1584
+ const result = await createIssueComment({
1585
+ apiKey,
1586
+ apiBaseUrl,
1587
+ issueId,
1588
+ content,
1589
+ parentCommentId: opts.parent,
1590
+ debug: !!opts.debug,
1591
+ });
1592
+ printResult(result, opts.json);
1197
1593
  } catch (err) {
1198
1594
  const message = err instanceof Error ? err.message : String(err);
1199
1595
  console.error(message);