postgresai 0.15.0-dev.3 → 0.15.0-dev.4

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.
@@ -13,6 +13,7 @@ import { startMcpServer } from "../lib/mcp-server";
13
13
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
14
14
  import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
15
15
  import { resolveBaseUrls } from "../lib/util";
16
+ import { uploadFile, downloadFile, buildMarkdownLink } from "../lib/storage";
16
17
  import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
17
18
  import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
18
19
  import * as pkce from "../lib/pkce";
@@ -412,6 +413,7 @@ interface CliOptions {
412
413
  apiKey?: string;
413
414
  apiBaseUrl?: string;
414
415
  uiBaseUrl?: string;
416
+ storageBaseUrl?: string;
415
417
  }
416
418
 
417
419
  /**
@@ -580,6 +582,10 @@ program
580
582
  .option(
581
583
  "--ui-base-url <url>",
582
584
  "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
585
+ )
586
+ .option(
587
+ "--storage-base-url <url>",
588
+ "Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)"
583
589
  );
584
590
 
585
591
  program
@@ -596,6 +602,27 @@ program
596
602
  console.log(`Default project saved: ${value}`);
597
603
  });
598
604
 
605
+ program
606
+ .command("set-storage-url <url>")
607
+ .description("store storage base URL for file uploads")
608
+ .action(async (url: string) => {
609
+ const value = (url || "").trim();
610
+ if (!value) {
611
+ console.error("Error: url is required");
612
+ process.exitCode = 1;
613
+ return;
614
+ }
615
+ try {
616
+ const { normalizeBaseUrl } = await import("../lib/util");
617
+ const normalized = normalizeBaseUrl(value);
618
+ config.writeConfig({ storageBaseUrl: normalized });
619
+ console.log(`Storage URL saved: ${normalized}`);
620
+ } catch {
621
+ console.error(`Error: invalid URL: ${value}`);
622
+ process.exitCode = 1;
623
+ }
624
+ });
625
+
599
626
  program
600
627
  .command("prepare-db [conn]")
601
628
  .description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
@@ -841,8 +868,8 @@ program
841
868
  } else {
842
869
  console.log("✓ prepare-db verify: OK");
843
870
  if (v.missingOptional.length > 0) {
844
- console.log("⚠ Optional items missing:");
845
- for (const m of v.missingOptional) console.log(`- ${m}`);
871
+ console.error("⚠ Optional items missing:");
872
+ for (const m of v.missingOptional) console.error(`- ${m}`);
846
873
  }
847
874
  }
848
875
  return;
@@ -973,8 +1000,8 @@ program
973
1000
  } else {
974
1001
  console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
975
1002
  if (skippedOptional.length > 0) {
976
- console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
977
- for (const s of skippedOptional) console.log(`- ${s}`);
1003
+ console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
1004
+ for (const s of skippedOptional) console.error(`- ${s}`);
978
1005
  }
979
1006
  if (process.stdout.isTTY) {
980
1007
  console.log(`Applied ${applied.length} steps`);
@@ -1155,8 +1182,8 @@ program
1155
1182
  } else {
1156
1183
  console.log(`✓ prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
1157
1184
  if (v.missingOptional.length > 0) {
1158
- console.log("⚠ Optional items missing:");
1159
- for (const m of v.missingOptional) console.log(`- ${m}`);
1185
+ console.error("⚠ Optional items missing:");
1186
+ for (const m of v.missingOptional) console.error(`- ${m}`);
1160
1187
  }
1161
1188
  }
1162
1189
  return;
@@ -1283,8 +1310,8 @@ program
1283
1310
  } else {
1284
1311
  console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
1285
1312
  if (skippedOptional.length > 0) {
1286
- console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
1287
- for (const s of skippedOptional) console.log(`- ${s}`);
1313
+ console.error("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
1314
+ for (const s of skippedOptional) console.error(`- ${s}`);
1288
1315
  }
1289
1316
  // Keep output compact but still useful
1290
1317
  if (process.stdout.isTTY) {
@@ -1600,11 +1627,11 @@ program
1600
1627
  console.log("✓ unprepare-db completed");
1601
1628
  console.log(`Applied ${applied.length} steps`);
1602
1629
  } else {
1603
- console.log("⚠ unprepare-db completed with errors");
1630
+ console.error("⚠ unprepare-db completed with errors");
1604
1631
  console.log(`Applied ${applied.length} steps`);
1605
- console.log("Errors:");
1632
+ console.error("Errors:");
1606
1633
  for (const err of errors) {
1607
- console.log(` - ${err}`);
1634
+ console.error(` - ${err}`);
1608
1635
  }
1609
1636
  process.exitCode = 1;
1610
1637
  }
@@ -2089,9 +2116,9 @@ function registerMonitoringInstance(
2089
2116
  const debug = opts?.debug;
2090
2117
 
2091
2118
  if (debug) {
2092
- console.log(`\nDebug: Registering monitoring instance...`);
2093
- console.log(`Debug: POST ${url}`);
2094
- console.log(`Debug: project_name=${projectName}`);
2119
+ console.error(`\nDebug: Registering monitoring instance...`);
2120
+ console.error(`Debug: POST ${url}`);
2121
+ console.error(`Debug: project_name=${projectName}`);
2095
2122
  }
2096
2123
 
2097
2124
  // Fire and forget - don't block the main flow
@@ -2109,18 +2136,18 @@ function registerMonitoringInstance(
2109
2136
  const body = await res.text().catch(() => "");
2110
2137
  if (!res.ok) {
2111
2138
  if (debug) {
2112
- console.log(`Debug: Monitoring registration failed: HTTP ${res.status}`);
2113
- console.log(`Debug: Response: ${body}`);
2139
+ console.error(`Debug: Monitoring registration failed: HTTP ${res.status}`);
2140
+ console.error(`Debug: Response: ${body}`);
2114
2141
  }
2115
2142
  return;
2116
2143
  }
2117
2144
  if (debug) {
2118
- console.log(`Debug: Monitoring registration response: ${body}`);
2145
+ console.error(`Debug: Monitoring registration response: ${body}`);
2119
2146
  }
2120
2147
  })
2121
2148
  .catch((err) => {
2122
2149
  if (debug) {
2123
- console.log(`Debug: Monitoring registration error: ${err.message}`);
2150
+ console.error(`Debug: Monitoring registration error: ${err.message}`);
2124
2151
  }
2125
2152
  });
2126
2153
  }
@@ -2300,8 +2327,8 @@ mon
2300
2327
 
2301
2328
  // Validate conflicting options
2302
2329
  if (opts.demo && opts.dbUrl) {
2303
- console.log("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
2304
- console.log("⚠ The --db-url will be ignored in demo mode.\n");
2330
+ console.error("⚠ Both --demo and --db-url provided. Demo mode includes its own database.");
2331
+ console.error("⚠ The --db-url will be ignored in demo mode.\n");
2305
2332
  opts.dbUrl = undefined;
2306
2333
  }
2307
2334
 
@@ -2317,7 +2344,7 @@ mon
2317
2344
  // Check if containers are already running
2318
2345
  const { running, containers } = checkRunningContainers();
2319
2346
  if (running) {
2320
- console.log(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
2347
+ console.error(`⚠ Monitoring services are already running: ${containers.join(", ")}`);
2321
2348
  console.log("Use 'postgres-ai mon restart' to restart them\n");
2322
2349
  return;
2323
2350
  }
@@ -2336,7 +2363,7 @@ mon
2336
2363
  } else if (opts.yes) {
2337
2364
  // Auto-yes mode without API key - skip API key setup
2338
2365
  console.log("Auto-yes mode: no API key provided, skipping API key setup");
2339
- console.log("⚠ Reports will be generated locally only");
2366
+ console.error("⚠ Reports will be generated locally only");
2340
2367
  console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
2341
2368
  } else {
2342
2369
  const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
@@ -2356,16 +2383,16 @@ mon
2356
2383
  break;
2357
2384
  }
2358
2385
 
2359
- console.log("⚠ API key cannot be empty");
2386
+ console.error("⚠ API key cannot be empty");
2360
2387
  const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
2361
2388
  if (retry.toLowerCase() === "n") {
2362
- console.log("⚠ Skipping API key setup - reports will be generated locally only");
2389
+ console.error("⚠ Skipping API key setup - reports will be generated locally only");
2363
2390
  console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
2364
2391
  break;
2365
2392
  }
2366
2393
  }
2367
2394
  } else {
2368
- console.log("⚠ Skipping API key setup - reports will be generated locally only");
2395
+ console.error("⚠ Skipping API key setup - reports will be generated locally only");
2369
2396
  console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
2370
2397
  }
2371
2398
  }
@@ -2425,7 +2452,7 @@ mon
2425
2452
  } else if (opts.yes) {
2426
2453
  // Auto-yes mode without database URL - skip database setup
2427
2454
  console.log("Auto-yes mode: no database URL provided, skipping database setup");
2428
- console.log("⚠ No PostgreSQL instance added");
2455
+ console.error("⚠ No PostgreSQL instance added");
2429
2456
  console.log("You can add one later with: postgres-ai mon targets add\n");
2430
2457
  } else {
2431
2458
  console.log("You need to add at least one PostgreSQL instance to monitor");
@@ -2443,7 +2470,7 @@ mon
2443
2470
  const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
2444
2471
  if (!m) {
2445
2472
  console.error("✗ Invalid connection string format");
2446
- console.log("⚠ Continuing without adding instance\n");
2473
+ console.error("⚠ Continuing without adding instance\n");
2447
2474
  } else {
2448
2475
  const host = m[3];
2449
2476
  const db = m[5];
@@ -2468,10 +2495,10 @@ mon
2468
2495
  }
2469
2496
  }
2470
2497
  } else {
2471
- console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
2498
+ console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
2472
2499
  }
2473
2500
  } else {
2474
- console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
2501
+ console.error("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
2475
2502
  }
2476
2503
  }
2477
2504
  } else {
@@ -2524,7 +2551,7 @@ mon
2524
2551
 
2525
2552
  console.log("✓ Grafana password configured\n");
2526
2553
  } catch (error) {
2527
- console.log("⚠ Could not generate Grafana password automatically");
2554
+ console.error("⚠ Could not generate Grafana password automatically");
2528
2555
  console.log("Using default password: demo\n");
2529
2556
  grafanaPassword = "demo";
2530
2557
  }
@@ -2916,7 +2943,7 @@ mon
2916
2943
  if (downCode === 0) {
2917
2944
  console.log("✓ Monitoring services stopped and removed");
2918
2945
  } else {
2919
- console.log("⚠ Could not stop services (may not be running)");
2946
+ console.error("⚠ Could not stop services (may not be running)");
2920
2947
  }
2921
2948
 
2922
2949
  // Remove any orphaned containers that docker compose down missed
@@ -3229,8 +3256,8 @@ auth
3229
3256
  const { apiBaseUrl, uiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
3230
3257
 
3231
3258
  if (opts.debug) {
3232
- console.log(`Debug: Resolved API base URL: ${apiBaseUrl}`);
3233
- console.log(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
3259
+ console.error(`Debug: Resolved API base URL: ${apiBaseUrl}`);
3260
+ console.error(`Debug: Resolved UI base URL: ${uiBaseUrl}`);
3234
3261
  }
3235
3262
 
3236
3263
  try {
@@ -3260,8 +3287,8 @@ auth
3260
3287
  const initUrl = new URL(`${apiBaseUrl}/rpc/oauth_init`);
3261
3288
 
3262
3289
  if (opts.debug) {
3263
- console.log(`Debug: Trying to POST to: ${initUrl.toString()}`);
3264
- console.log(`Debug: Request data: ${initData}`);
3290
+ console.error(`Debug: Trying to POST to: ${initUrl.toString()}`);
3291
+ console.error(`Debug: Request data: ${initData}`);
3265
3292
  }
3266
3293
 
3267
3294
  // Step 2: Initialize OAuth session on backend using fetch
@@ -3307,7 +3334,7 @@ auth
3307
3334
  const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
3308
3335
 
3309
3336
  if (opts.debug) {
3310
- console.log(`Debug: Auth URL: ${authUrl}`);
3337
+ console.error(`Debug: Auth URL: ${authUrl}`);
3311
3338
  }
3312
3339
 
3313
3340
  console.log(`\nOpening browser for authentication...`);
@@ -3731,12 +3758,12 @@ issues
3731
3758
  // Interpret escape sequences in content (e.g., \n -> newline)
3732
3759
  if (opts.debug) {
3733
3760
  // eslint-disable-next-line no-console
3734
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3761
+ console.error(`Debug: Original content: ${JSON.stringify(content)}`);
3735
3762
  }
3736
3763
  content = interpretEscapes(content);
3737
3764
  if (opts.debug) {
3738
3765
  // eslint-disable-next-line no-console
3739
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
3766
+ console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
3740
3767
  }
3741
3768
 
3742
3769
  const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
@@ -3930,12 +3957,12 @@ issues
3930
3957
  .action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
3931
3958
  if (opts.debug) {
3932
3959
  // eslint-disable-next-line no-console
3933
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
3960
+ console.error(`Debug: Original content: ${JSON.stringify(content)}`);
3934
3961
  }
3935
3962
  content = interpretEscapes(content);
3936
3963
  if (opts.debug) {
3937
3964
  // eslint-disable-next-line no-console
3938
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
3965
+ console.error(`Debug: Interpreted content: ${JSON.stringify(content)}`);
3939
3966
  }
3940
3967
 
3941
3968
  const rootOpts = program.opts<CliOptions>();
@@ -3968,6 +3995,93 @@ issues
3968
3995
  }
3969
3996
  });
3970
3997
 
3998
+ // File upload/download (subcommands of issues)
3999
+ const issueFiles = issues.command("files").description("upload and download files for issues");
4000
+
4001
+ issueFiles
4002
+ .command("upload <path>")
4003
+ .description("upload a file to storage and get a markdown link")
4004
+ .option("--debug", "enable debug output")
4005
+ .option("--json", "output raw JSON")
4006
+ .action(async (filePath: string, opts: { debug?: boolean; json?: boolean }) => {
4007
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Uploading file...");
4008
+ try {
4009
+ const rootOpts = program.opts<CliOptions>();
4010
+ const cfg = config.readConfig();
4011
+ const { apiKey } = getConfig(rootOpts);
4012
+ if (!apiKey) {
4013
+ spinner.stop();
4014
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
4015
+ process.exitCode = 1;
4016
+ return;
4017
+ }
4018
+
4019
+ const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
4020
+
4021
+ const result = await uploadFile({
4022
+ apiKey,
4023
+ storageBaseUrl,
4024
+ filePath,
4025
+ debug: !!opts.debug,
4026
+ });
4027
+ spinner.stop();
4028
+
4029
+ if (opts.json) {
4030
+ printResult(result, true);
4031
+ } else {
4032
+ const md = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
4033
+ const displayUrl = result.url.startsWith("/") ? `${storageBaseUrl}${result.url}` : `${storageBaseUrl}/${result.url}`;
4034
+ console.log(`URL: ${displayUrl}`);
4035
+ console.log(`File: ${result.metadata.originalName}`);
4036
+ console.log(`Size: ${result.metadata.size} bytes`);
4037
+ console.log(`Type: ${result.metadata.mimeType}`);
4038
+ console.log(`Markdown: ${md}`);
4039
+ }
4040
+ } catch (err) {
4041
+ spinner.stop();
4042
+ const message = err instanceof Error ? err.message : String(err);
4043
+ console.error(message);
4044
+ process.exitCode = 1;
4045
+ }
4046
+ });
4047
+
4048
+ issueFiles
4049
+ .command("download <url>")
4050
+ .description("download a file from storage")
4051
+ .option("-o, --output <path>", "output file path (default: derive from URL)")
4052
+ .option("--debug", "enable debug output")
4053
+ .action(async (fileUrl: string, opts: { output?: string; debug?: boolean }) => {
4054
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Downloading file...");
4055
+ try {
4056
+ const rootOpts = program.opts<CliOptions>();
4057
+ const cfg = config.readConfig();
4058
+ const { apiKey } = getConfig(rootOpts);
4059
+ if (!apiKey) {
4060
+ spinner.stop();
4061
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
4062
+ process.exitCode = 1;
4063
+ return;
4064
+ }
4065
+
4066
+ const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
4067
+
4068
+ const result = await downloadFile({
4069
+ apiKey,
4070
+ storageBaseUrl,
4071
+ fileUrl,
4072
+ outputPath: opts.output,
4073
+ debug: !!opts.debug,
4074
+ });
4075
+ spinner.stop();
4076
+ console.log(`Saved: ${result.savedTo}`);
4077
+ } catch (err) {
4078
+ spinner.stop();
4079
+ const message = err instanceof Error ? err.message : String(err);
4080
+ console.error(message);
4081
+ process.exitCode = 1;
4082
+ }
4083
+ });
4084
+
3971
4085
  // Action Items management (subcommands of issues)
3972
4086
  issues
3973
4087
  .command("action-items <issueId>")