postgresai 0.14.0-beta.12 → 0.14.0-beta.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.
Files changed (42) hide show
  1. package/README.md +32 -0
  2. package/bin/postgres-ai.ts +1234 -170
  3. package/dist/bin/postgres-ai.js +2480 -410
  4. package/dist/sql/02.extensions.sql +8 -0
  5. package/dist/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  6. package/dist/sql/sql/02.extensions.sql +8 -0
  7. package/dist/sql/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  8. package/dist/sql/sql/uninit/01.helpers.sql +5 -0
  9. package/dist/sql/sql/uninit/02.permissions.sql +30 -0
  10. package/dist/sql/sql/uninit/03.role.sql +27 -0
  11. package/dist/sql/uninit/01.helpers.sql +5 -0
  12. package/dist/sql/uninit/02.permissions.sql +30 -0
  13. package/dist/sql/uninit/03.role.sql +27 -0
  14. package/lib/checkup.ts +69 -3
  15. package/lib/init.ts +184 -26
  16. package/lib/issues.ts +453 -7
  17. package/lib/mcp-server.ts +180 -3
  18. package/lib/metrics-embedded.ts +3 -3
  19. package/lib/supabase.ts +824 -0
  20. package/package.json +1 -1
  21. package/sql/02.extensions.sql +8 -0
  22. package/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  23. package/sql/uninit/01.helpers.sql +5 -0
  24. package/sql/uninit/02.permissions.sql +30 -0
  25. package/sql/uninit/03.role.sql +27 -0
  26. package/test/checkup.test.ts +240 -14
  27. package/test/config-consistency.test.ts +36 -0
  28. package/test/init.integration.test.ts +80 -71
  29. package/test/init.test.ts +501 -2
  30. package/test/issues.cli.test.ts +224 -0
  31. package/test/mcp-server.test.ts +551 -12
  32. package/test/supabase.test.ts +568 -0
  33. package/test/test-utils.ts +6 -0
  34. /package/dist/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  35. /package/dist/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  36. /package/dist/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  37. /package/dist/sql/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  38. /package/dist/sql/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  39. /package/dist/sql/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  40. /package/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  41. /package/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  42. /package/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
@@ -13064,7 +13064,7 @@ var {
13064
13064
  // package.json
13065
13065
  var package_default = {
13066
13066
  name: "postgresai",
13067
- version: "0.14.0-beta.12",
13067
+ version: "0.14.0-beta.14",
13068
13068
  description: "postgres_ai CLI",
13069
13069
  license: "Apache-2.0",
13070
13070
  private: false,
@@ -15887,7 +15887,7 @@ var Result = import_lib.default.Result;
15887
15887
  var TypeOverrides = import_lib.default.TypeOverrides;
15888
15888
  var defaults = import_lib.default.defaults;
15889
15889
  // package.json
15890
- var version = "0.14.0-beta.12";
15890
+ var version = "0.14.0-beta.14";
15891
15891
  var package_default2 = {
15892
15892
  name: "postgresai",
15893
15893
  version,
@@ -16079,13 +16079,24 @@ function resolveBaseUrls(opts, cfg, defaults2 = {}) {
16079
16079
 
16080
16080
  // lib/issues.ts
16081
16081
  async function fetchIssues(params) {
16082
- const { apiKey, apiBaseUrl, debug } = params;
16082
+ const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
16083
16083
  if (!apiKey) {
16084
16084
  throw new Error("API key is required");
16085
16085
  }
16086
16086
  const base = normalizeBaseUrl(apiBaseUrl);
16087
16087
  const url = new URL(`${base}/issues`);
16088
16088
  url.searchParams.set("select", "id,title,status,created_at");
16089
+ url.searchParams.set("order", "id.desc");
16090
+ url.searchParams.set("limit", String(limit));
16091
+ url.searchParams.set("offset", String(offset));
16092
+ if (typeof orgId === "number") {
16093
+ url.searchParams.set("org_id", `eq.${orgId}`);
16094
+ }
16095
+ if (status === "open") {
16096
+ url.searchParams.set("status", "eq.0");
16097
+ } else if (status === "closed") {
16098
+ url.searchParams.set("status", "eq.1");
16099
+ }
16089
16100
  const headers = {
16090
16101
  "access-token": apiKey,
16091
16102
  Prefer: "return=representation",
@@ -16170,7 +16181,7 @@ async function fetchIssue(params) {
16170
16181
  }
16171
16182
  const base = normalizeBaseUrl(apiBaseUrl);
16172
16183
  const url = new URL(`${base}/issues`);
16173
- url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
16184
+ url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
16174
16185
  url.searchParams.set("id", `eq.${issueId}`);
16175
16186
  url.searchParams.set("limit", "1");
16176
16187
  const headers = {
@@ -16198,11 +16209,20 @@ async function fetchIssue(params) {
16198
16209
  if (response.ok) {
16199
16210
  try {
16200
16211
  const parsed = JSON.parse(data);
16201
- if (Array.isArray(parsed)) {
16202
- return parsed[0] ?? null;
16203
- } else {
16204
- return parsed;
16212
+ const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
16213
+ if (!rawIssue) {
16214
+ return null;
16205
16215
  }
16216
+ const actionItemsSummary = Array.isArray(rawIssue.action_items) ? rawIssue.action_items.map((item) => ({ id: item.id, title: item.title })) : [];
16217
+ return {
16218
+ id: rawIssue.id,
16219
+ title: rawIssue.title,
16220
+ description: rawIssue.description,
16221
+ status: rawIssue.status,
16222
+ created_at: rawIssue.created_at,
16223
+ author_display_name: rawIssue.author_display_name,
16224
+ action_items: actionItemsSummary
16225
+ };
16206
16226
  } catch {
16207
16227
  throw new Error(`Failed to parse issue response: ${data}`);
16208
16228
  }
@@ -16441,6 +16461,243 @@ async function updateIssueComment(params) {
16441
16461
  throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
16442
16462
  }
16443
16463
  }
16464
+ async function fetchActionItem(params) {
16465
+ const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
16466
+ if (!apiKey) {
16467
+ throw new Error("API key is required");
16468
+ }
16469
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16470
+ const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
16471
+ const validIds = rawIds.filter((id) => id != null && typeof id === "string").map((id) => id.trim()).filter((id) => id.length > 0 && uuidPattern.test(id));
16472
+ if (validIds.length === 0) {
16473
+ throw new Error("actionItemId is required and must be a valid UUID");
16474
+ }
16475
+ const base = normalizeBaseUrl(apiBaseUrl);
16476
+ const url = new URL(`${base}/issue_action_items`);
16477
+ if (validIds.length === 1) {
16478
+ url.searchParams.set("id", `eq.${validIds[0]}`);
16479
+ } else {
16480
+ url.searchParams.set("id", `in.(${validIds.join(",")})`);
16481
+ }
16482
+ const headers = {
16483
+ "access-token": apiKey,
16484
+ Prefer: "return=representation",
16485
+ "Content-Type": "application/json",
16486
+ Connection: "close"
16487
+ };
16488
+ if (debug) {
16489
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
16490
+ console.log(`Debug: Resolved API base URL: ${base}`);
16491
+ console.log(`Debug: GET URL: ${url.toString()}`);
16492
+ console.log(`Debug: Auth scheme: access-token`);
16493
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
16494
+ }
16495
+ const response = await fetch(url.toString(), {
16496
+ method: "GET",
16497
+ headers
16498
+ });
16499
+ if (debug) {
16500
+ console.log(`Debug: Response status: ${response.status}`);
16501
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
16502
+ }
16503
+ const data = await response.text();
16504
+ if (response.ok) {
16505
+ try {
16506
+ const parsed = JSON.parse(data);
16507
+ if (Array.isArray(parsed)) {
16508
+ return parsed;
16509
+ }
16510
+ return parsed ? [parsed] : [];
16511
+ } catch {
16512
+ throw new Error(`Failed to parse action item response: ${data}`);
16513
+ }
16514
+ } else {
16515
+ throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
16516
+ }
16517
+ }
16518
+ async function fetchActionItems(params) {
16519
+ const { apiKey, apiBaseUrl, issueId, debug } = params;
16520
+ if (!apiKey) {
16521
+ throw new Error("API key is required");
16522
+ }
16523
+ if (!issueId) {
16524
+ throw new Error("issueId is required");
16525
+ }
16526
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16527
+ if (!uuidPattern.test(issueId.trim())) {
16528
+ throw new Error("issueId must be a valid UUID");
16529
+ }
16530
+ const base = normalizeBaseUrl(apiBaseUrl);
16531
+ const url = new URL(`${base}/issue_action_items`);
16532
+ url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
16533
+ const headers = {
16534
+ "access-token": apiKey,
16535
+ Prefer: "return=representation",
16536
+ "Content-Type": "application/json",
16537
+ Connection: "close"
16538
+ };
16539
+ if (debug) {
16540
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
16541
+ console.log(`Debug: Resolved API base URL: ${base}`);
16542
+ console.log(`Debug: GET URL: ${url.toString()}`);
16543
+ console.log(`Debug: Auth scheme: access-token`);
16544
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
16545
+ }
16546
+ const response = await fetch(url.toString(), {
16547
+ method: "GET",
16548
+ headers
16549
+ });
16550
+ if (debug) {
16551
+ console.log(`Debug: Response status: ${response.status}`);
16552
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
16553
+ }
16554
+ const data = await response.text();
16555
+ if (response.ok) {
16556
+ try {
16557
+ return JSON.parse(data);
16558
+ } catch {
16559
+ throw new Error(`Failed to parse action items response: ${data}`);
16560
+ }
16561
+ } else {
16562
+ throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
16563
+ }
16564
+ }
16565
+ async function createActionItem(params) {
16566
+ const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
16567
+ if (!apiKey) {
16568
+ throw new Error("API key is required");
16569
+ }
16570
+ if (!issueId) {
16571
+ throw new Error("issueId is required");
16572
+ }
16573
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16574
+ if (!uuidPattern.test(issueId.trim())) {
16575
+ throw new Error("issueId must be a valid UUID");
16576
+ }
16577
+ if (!title) {
16578
+ throw new Error("title is required");
16579
+ }
16580
+ const base = normalizeBaseUrl(apiBaseUrl);
16581
+ const url = new URL(`${base}/rpc/issue_action_item_create`);
16582
+ const bodyObj = {
16583
+ issue_id: issueId,
16584
+ title
16585
+ };
16586
+ if (description !== undefined) {
16587
+ bodyObj.description = description;
16588
+ }
16589
+ if (sqlAction !== undefined) {
16590
+ bodyObj.sql_action = sqlAction;
16591
+ }
16592
+ if (configs !== undefined) {
16593
+ bodyObj.configs = configs;
16594
+ }
16595
+ const body = JSON.stringify(bodyObj);
16596
+ const headers = {
16597
+ "access-token": apiKey,
16598
+ Prefer: "return=representation",
16599
+ "Content-Type": "application/json",
16600
+ Connection: "close"
16601
+ };
16602
+ if (debug) {
16603
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
16604
+ console.log(`Debug: Resolved API base URL: ${base}`);
16605
+ console.log(`Debug: POST URL: ${url.toString()}`);
16606
+ console.log(`Debug: Auth scheme: access-token`);
16607
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
16608
+ console.log(`Debug: Request body: ${body}`);
16609
+ }
16610
+ const response = await fetch(url.toString(), {
16611
+ method: "POST",
16612
+ headers,
16613
+ body
16614
+ });
16615
+ if (debug) {
16616
+ console.log(`Debug: Response status: ${response.status}`);
16617
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
16618
+ }
16619
+ const data = await response.text();
16620
+ if (response.ok) {
16621
+ try {
16622
+ return JSON.parse(data);
16623
+ } catch {
16624
+ throw new Error(`Failed to parse create action item response: ${data}`);
16625
+ }
16626
+ } else {
16627
+ throw new Error(formatHttpError("Failed to create action item", response.status, data));
16628
+ }
16629
+ }
16630
+ async function updateActionItem(params) {
16631
+ const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
16632
+ if (!apiKey) {
16633
+ throw new Error("API key is required");
16634
+ }
16635
+ if (!actionItemId) {
16636
+ throw new Error("actionItemId is required");
16637
+ }
16638
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16639
+ if (!uuidPattern.test(actionItemId.trim())) {
16640
+ throw new Error("actionItemId must be a valid UUID");
16641
+ }
16642
+ const hasUpdateField = title !== undefined || description !== undefined || isDone !== undefined || status !== undefined || statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
16643
+ if (!hasUpdateField) {
16644
+ throw new Error("At least one field to update is required");
16645
+ }
16646
+ const base = normalizeBaseUrl(apiBaseUrl);
16647
+ const url = new URL(`${base}/rpc/issue_action_item_update`);
16648
+ const bodyObj = {
16649
+ action_item_id: actionItemId
16650
+ };
16651
+ if (title !== undefined) {
16652
+ bodyObj.title = title;
16653
+ }
16654
+ if (description !== undefined) {
16655
+ bodyObj.description = description;
16656
+ }
16657
+ if (isDone !== undefined) {
16658
+ bodyObj.is_done = isDone;
16659
+ }
16660
+ if (status !== undefined) {
16661
+ bodyObj.status = status;
16662
+ }
16663
+ if (statusReason !== undefined) {
16664
+ bodyObj.status_reason = statusReason;
16665
+ }
16666
+ if (sqlAction !== undefined) {
16667
+ bodyObj.sql_action = sqlAction;
16668
+ }
16669
+ if (configs !== undefined) {
16670
+ bodyObj.configs = configs;
16671
+ }
16672
+ const body = JSON.stringify(bodyObj);
16673
+ const headers = {
16674
+ "access-token": apiKey,
16675
+ Prefer: "return=representation",
16676
+ "Content-Type": "application/json",
16677
+ Connection: "close"
16678
+ };
16679
+ if (debug) {
16680
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
16681
+ console.log(`Debug: Resolved API base URL: ${base}`);
16682
+ console.log(`Debug: POST URL: ${url.toString()}`);
16683
+ console.log(`Debug: Auth scheme: access-token`);
16684
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
16685
+ console.log(`Debug: Request body: ${body}`);
16686
+ }
16687
+ const response = await fetch(url.toString(), {
16688
+ method: "POST",
16689
+ headers,
16690
+ body
16691
+ });
16692
+ if (debug) {
16693
+ console.log(`Debug: Response status: ${response.status}`);
16694
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
16695
+ }
16696
+ if (!response.ok) {
16697
+ const data = await response.text();
16698
+ throw new Error(formatHttpError("Failed to update action item", response.status, data));
16699
+ }
16700
+ }
16444
16701
 
16445
16702
  // node_modules/zod/v4/core/core.js
16446
16703
  var NEVER = Object.freeze({
@@ -23350,7 +23607,16 @@ async function handleToolCall(req, rootOpts, extra) {
23350
23607
  }
23351
23608
  try {
23352
23609
  if (toolName === "list_issues") {
23353
- const issues = await fetchIssues({ apiKey, apiBaseUrl, debug });
23610
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
23611
+ const statusArg = args.status ? String(args.status) : undefined;
23612
+ let status;
23613
+ if (statusArg === "open")
23614
+ status = "open";
23615
+ else if (statusArg === "closed")
23616
+ status = "closed";
23617
+ const limit = args.limit !== undefined ? Number(args.limit) : undefined;
23618
+ const offset = args.offset !== undefined ? Number(args.offset) : undefined;
23619
+ const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
23354
23620
  return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
23355
23621
  }
23356
23622
  if (toolName === "view_issue") {
@@ -23430,6 +23696,70 @@ async function handleToolCall(req, rootOpts, extra) {
23430
23696
  const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
23431
23697
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
23432
23698
  }
23699
+ if (toolName === "view_action_item") {
23700
+ let actionItemIds;
23701
+ if (Array.isArray(args.action_item_ids)) {
23702
+ actionItemIds = args.action_item_ids.map((id) => String(id).trim()).filter((id) => id);
23703
+ } else if (args.action_item_id) {
23704
+ actionItemIds = [String(args.action_item_id).trim()];
23705
+ } else {
23706
+ actionItemIds = [];
23707
+ }
23708
+ if (actionItemIds.length === 0) {
23709
+ return { content: [{ type: "text", text: "action_item_id or action_item_ids is required" }], isError: true };
23710
+ }
23711
+ const actionItems = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds, debug });
23712
+ if (actionItems.length === 0) {
23713
+ return { content: [{ type: "text", text: "Action item(s) not found" }], isError: true };
23714
+ }
23715
+ return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
23716
+ }
23717
+ if (toolName === "list_action_items") {
23718
+ const issueId = String(args.issue_id || "").trim();
23719
+ if (!issueId) {
23720
+ return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
23721
+ }
23722
+ const actionItems = await fetchActionItems({ apiKey, apiBaseUrl, issueId, debug });
23723
+ return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
23724
+ }
23725
+ if (toolName === "create_action_item") {
23726
+ const issueId = String(args.issue_id || "").trim();
23727
+ const rawTitle = String(args.title || "").trim();
23728
+ if (!issueId) {
23729
+ return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
23730
+ }
23731
+ if (!rawTitle) {
23732
+ return { content: [{ type: "text", text: "title is required" }], isError: true };
23733
+ }
23734
+ const title = interpretEscapes(rawTitle);
23735
+ const rawDescription = args.description ? String(args.description) : undefined;
23736
+ const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
23737
+ const sqlAction = args.sql_action !== undefined ? String(args.sql_action) : undefined;
23738
+ const configs = Array.isArray(args.configs) ? args.configs : undefined;
23739
+ const result = await createActionItem({ apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug });
23740
+ return { content: [{ type: "text", text: JSON.stringify({ id: result }, null, 2) }] };
23741
+ }
23742
+ if (toolName === "update_action_item") {
23743
+ const actionItemId = String(args.action_item_id || "").trim();
23744
+ if (!actionItemId) {
23745
+ return { content: [{ type: "text", text: "action_item_id is required" }], isError: true };
23746
+ }
23747
+ const rawTitle = args.title !== undefined ? String(args.title) : undefined;
23748
+ const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
23749
+ const rawDescription = args.description !== undefined ? String(args.description) : undefined;
23750
+ const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
23751
+ const isDone = args.is_done !== undefined ? Boolean(args.is_done) : undefined;
23752
+ const status = args.status !== undefined ? String(args.status) : undefined;
23753
+ const statusReason = args.status_reason !== undefined ? String(args.status_reason) : undefined;
23754
+ if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined) {
23755
+ return { content: [{ type: "text", text: "At least one field to update is required (title, description, is_done, status, or status_reason)" }], isError: true };
23756
+ }
23757
+ if (status !== undefined && !["waiting_for_approval", "approved", "rejected"].includes(status)) {
23758
+ return { content: [{ type: "text", text: "status must be 'waiting_for_approval', 'approved', or 'rejected'" }], isError: true };
23759
+ }
23760
+ await updateActionItem({ apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, debug });
23761
+ return { content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }] };
23762
+ }
23433
23763
  throw new Error(`Unknown tool: ${toolName}`);
23434
23764
  } catch (err) {
23435
23765
  const message = err instanceof Error ? err.message : String(err);
@@ -23437,7 +23767,11 @@ async function handleToolCall(req, rootOpts, extra) {
23437
23767
  }
23438
23768
  }
23439
23769
  async function startMcpServer(rootOpts, extra) {
23440
- const server = new Server({ name: "postgresai-mcp", version: package_default2.version }, { capabilities: { tools: {} } });
23770
+ const server = new Server({
23771
+ name: "postgresai-mcp",
23772
+ version: package_default2.version,
23773
+ title: "PostgresAI MCP Server"
23774
+ }, { capabilities: { tools: {} } });
23441
23775
  server.setRequestHandler(ListToolsRequestSchema, async () => {
23442
23776
  return {
23443
23777
  tools: [
@@ -23447,6 +23781,10 @@ async function startMcpServer(rootOpts, extra) {
23447
23781
  inputSchema: {
23448
23782
  type: "object",
23449
23783
  properties: {
23784
+ org_id: { type: "number", description: "Organization ID (optional, falls back to config)" },
23785
+ status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
23786
+ limit: { type: "number", description: "Max number of issues to return (default: 20)" },
23787
+ offset: { type: "number", description: "Number of issues to skip (default: 0)" },
23450
23788
  debug: { type: "boolean", description: "Enable verbose debug logs" }
23451
23789
  },
23452
23790
  additionalProperties: false
@@ -23535,47 +23873,130 @@ async function startMcpServer(rootOpts, extra) {
23535
23873
  required: ["comment_id", "content"],
23536
23874
  additionalProperties: false
23537
23875
  }
23538
- }
23539
- ]
23540
- };
23541
- });
23542
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
23543
- return handleToolCall(req, rootOpts, extra);
23544
- });
23545
- const transport = new StdioServerTransport;
23546
- await server.connect(transport);
23547
- }
23548
-
23549
- // lib/issues.ts
23550
- async function fetchIssues2(params) {
23551
- const { apiKey, apiBaseUrl, debug } = params;
23552
- if (!apiKey) {
23553
- throw new Error("API key is required");
23554
- }
23555
- const base = normalizeBaseUrl(apiBaseUrl);
23556
- const url = new URL(`${base}/issues`);
23557
- url.searchParams.set("select", "id,title,status,created_at");
23558
- const headers = {
23559
- "access-token": apiKey,
23560
- Prefer: "return=representation",
23561
- "Content-Type": "application/json",
23562
- Connection: "close"
23563
- };
23564
- if (debug) {
23565
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
23566
- console.log(`Debug: Resolved API base URL: ${base}`);
23567
- console.log(`Debug: GET URL: ${url.toString()}`);
23568
- console.log(`Debug: Auth scheme: access-token`);
23569
- console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
23570
- }
23571
- const response = await fetch(url.toString(), {
23572
- method: "GET",
23573
- headers
23574
- });
23575
- if (debug) {
23576
- console.log(`Debug: Response status: ${response.status}`);
23577
- console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
23578
- }
23876
+ },
23877
+ {
23878
+ name: "view_action_item",
23879
+ description: "View action item(s) with all details. Supports single ID or multiple IDs.",
23880
+ inputSchema: {
23881
+ type: "object",
23882
+ properties: {
23883
+ action_item_id: { type: "string", description: "Single action item ID (UUID)" },
23884
+ action_item_ids: { type: "array", items: { type: "string" }, description: "Multiple action item IDs (UUIDs)" },
23885
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
23886
+ },
23887
+ additionalProperties: false
23888
+ }
23889
+ },
23890
+ {
23891
+ name: "list_action_items",
23892
+ description: "List action items for an issue",
23893
+ inputSchema: {
23894
+ type: "object",
23895
+ properties: {
23896
+ issue_id: { type: "string", description: "Issue ID (UUID)" },
23897
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
23898
+ },
23899
+ required: ["issue_id"],
23900
+ additionalProperties: false
23901
+ }
23902
+ },
23903
+ {
23904
+ name: "create_action_item",
23905
+ description: "Create a new action item for an issue",
23906
+ inputSchema: {
23907
+ type: "object",
23908
+ properties: {
23909
+ issue_id: { type: "string", description: "Issue ID (UUID)" },
23910
+ title: { type: "string", description: "Action item title" },
23911
+ description: { type: "string", description: "Detailed description" },
23912
+ sql_action: { type: "string", description: "SQL command to execute, e.g. 'DROP INDEX CONCURRENTLY idx_unused;'" },
23913
+ configs: {
23914
+ type: "array",
23915
+ items: {
23916
+ type: "object",
23917
+ properties: {
23918
+ parameter: { type: "string" },
23919
+ value: { type: "string" }
23920
+ },
23921
+ required: ["parameter", "value"]
23922
+ },
23923
+ description: "Configuration parameter changes"
23924
+ },
23925
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
23926
+ },
23927
+ required: ["issue_id", "title"],
23928
+ additionalProperties: false
23929
+ }
23930
+ },
23931
+ {
23932
+ name: "update_action_item",
23933
+ description: "Update an action item: mark as done/not done, approve/reject, or edit title/description",
23934
+ inputSchema: {
23935
+ type: "object",
23936
+ properties: {
23937
+ action_item_id: { type: "string", description: "Action item ID (UUID)" },
23938
+ title: { type: "string", description: "New title" },
23939
+ description: { type: "string", description: "New description" },
23940
+ is_done: { type: "boolean", description: "Mark as done (true) or not done (false)" },
23941
+ status: { type: "string", description: "Approval status: 'waiting_for_approval', 'approved', or 'rejected'" },
23942
+ status_reason: { type: "string", description: "Reason for approval/rejection" },
23943
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
23944
+ },
23945
+ required: ["action_item_id"],
23946
+ additionalProperties: false
23947
+ }
23948
+ }
23949
+ ]
23950
+ };
23951
+ });
23952
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
23953
+ return handleToolCall(req, rootOpts, extra);
23954
+ });
23955
+ const transport = new StdioServerTransport;
23956
+ await server.connect(transport);
23957
+ }
23958
+
23959
+ // lib/issues.ts
23960
+ async function fetchIssues2(params) {
23961
+ const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
23962
+ if (!apiKey) {
23963
+ throw new Error("API key is required");
23964
+ }
23965
+ const base = normalizeBaseUrl(apiBaseUrl);
23966
+ const url = new URL(`${base}/issues`);
23967
+ url.searchParams.set("select", "id,title,status,created_at");
23968
+ url.searchParams.set("order", "id.desc");
23969
+ url.searchParams.set("limit", String(limit));
23970
+ url.searchParams.set("offset", String(offset));
23971
+ if (typeof orgId === "number") {
23972
+ url.searchParams.set("org_id", `eq.${orgId}`);
23973
+ }
23974
+ if (status === "open") {
23975
+ url.searchParams.set("status", "eq.0");
23976
+ } else if (status === "closed") {
23977
+ url.searchParams.set("status", "eq.1");
23978
+ }
23979
+ const headers = {
23980
+ "access-token": apiKey,
23981
+ Prefer: "return=representation",
23982
+ "Content-Type": "application/json",
23983
+ Connection: "close"
23984
+ };
23985
+ if (debug) {
23986
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
23987
+ console.log(`Debug: Resolved API base URL: ${base}`);
23988
+ console.log(`Debug: GET URL: ${url.toString()}`);
23989
+ console.log(`Debug: Auth scheme: access-token`);
23990
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
23991
+ }
23992
+ const response = await fetch(url.toString(), {
23993
+ method: "GET",
23994
+ headers
23995
+ });
23996
+ if (debug) {
23997
+ console.log(`Debug: Response status: ${response.status}`);
23998
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
23999
+ }
23579
24000
  const data = await response.text();
23580
24001
  if (response.ok) {
23581
24002
  try {
@@ -23639,7 +24060,7 @@ async function fetchIssue2(params) {
23639
24060
  }
23640
24061
  const base = normalizeBaseUrl(apiBaseUrl);
23641
24062
  const url = new URL(`${base}/issues`);
23642
- url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
24063
+ url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
23643
24064
  url.searchParams.set("id", `eq.${issueId}`);
23644
24065
  url.searchParams.set("limit", "1");
23645
24066
  const headers = {
@@ -23667,11 +24088,20 @@ async function fetchIssue2(params) {
23667
24088
  if (response.ok) {
23668
24089
  try {
23669
24090
  const parsed = JSON.parse(data);
23670
- if (Array.isArray(parsed)) {
23671
- return parsed[0] ?? null;
23672
- } else {
23673
- return parsed;
24091
+ const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
24092
+ if (!rawIssue) {
24093
+ return null;
23674
24094
  }
24095
+ const actionItemsSummary = Array.isArray(rawIssue.action_items) ? rawIssue.action_items.map((item) => ({ id: item.id, title: item.title })) : [];
24096
+ return {
24097
+ id: rawIssue.id,
24098
+ title: rawIssue.title,
24099
+ description: rawIssue.description,
24100
+ status: rawIssue.status,
24101
+ created_at: rawIssue.created_at,
24102
+ author_display_name: rawIssue.author_display_name,
24103
+ action_items: actionItemsSummary
24104
+ };
23675
24105
  } catch {
23676
24106
  throw new Error(`Failed to parse issue response: ${data}`);
23677
24107
  }
@@ -23910,150 +24340,396 @@ async function updateIssueComment2(params) {
23910
24340
  throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
23911
24341
  }
23912
24342
  }
23913
-
23914
- // lib/util.ts
23915
- function maskSecret2(secret) {
23916
- if (!secret)
23917
- return "";
23918
- if (secret.length <= 8)
23919
- return "****";
23920
- if (secret.length <= 16)
23921
- return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
23922
- return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
23923
- }
23924
- function normalizeBaseUrl2(value) {
23925
- const trimmed = (value || "").replace(/\/$/, "");
23926
- try {
23927
- new URL(trimmed);
23928
- } catch {
23929
- throw new Error(`Invalid base URL: ${value}`);
23930
- }
23931
- return trimmed;
23932
- }
23933
- function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
23934
- const defApi = defaults2.apiBaseUrl || "https://postgres.ai/api/general/";
23935
- const defUi = defaults2.uiBaseUrl || "https://console.postgres.ai";
23936
- const apiCandidate = opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi;
23937
- const uiCandidate = opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi;
23938
- return {
23939
- apiBaseUrl: normalizeBaseUrl2(apiCandidate),
23940
- uiBaseUrl: normalizeBaseUrl2(uiCandidate)
23941
- };
23942
- }
23943
-
23944
- // lib/init.ts
23945
- import { randomBytes } from "crypto";
23946
- import { URL as URL2, fileURLToPath } from "url";
23947
- import * as fs3 from "fs";
23948
- import * as path3 from "path";
23949
- var DEFAULT_MONITORING_USER = "postgres_ai_mon";
23950
- function sslModeToConfig(mode) {
23951
- if (mode.toLowerCase() === "disable")
23952
- return false;
23953
- if (mode.toLowerCase() === "verify-full" || mode.toLowerCase() === "verify-ca")
23954
- return true;
23955
- return { rejectUnauthorized: false };
23956
- }
23957
- function extractSslModeFromUri(uri) {
23958
- try {
23959
- return new URL2(uri).searchParams.get("sslmode") ?? undefined;
23960
- } catch {
23961
- return uri.match(/[?&]sslmode=([^&]+)/i)?.[1];
23962
- }
23963
- }
23964
- function stripSslModeFromUri(uri) {
23965
- try {
23966
- const u = new URL2(uri);
23967
- u.searchParams.delete("sslmode");
23968
- return u.toString();
23969
- } catch {
23970
- return uri.replace(/[?&]sslmode=[^&]*/gi, "").replace(/\?&/, "?").replace(/\?$/, "");
24343
+ async function fetchActionItem2(params) {
24344
+ const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
24345
+ if (!apiKey) {
24346
+ throw new Error("API key is required");
23971
24347
  }
23972
- }
23973
- function isSslNegotiationError(err) {
23974
- if (!err || typeof err !== "object")
23975
- return false;
23976
- const e = err;
23977
- const msg = typeof e.message === "string" ? e.message.toLowerCase() : "";
23978
- const code = typeof e.code === "string" ? e.code : "";
23979
- const fallbackPatterns = [
23980
- "the server does not support ssl",
23981
- "ssl off",
23982
- "server does not support ssl connections"
23983
- ];
23984
- for (const pattern of fallbackPatterns) {
23985
- if (msg.includes(pattern))
23986
- return true;
24348
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24349
+ const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
24350
+ const validIds = rawIds.filter((id) => id != null && typeof id === "string").map((id) => id.trim()).filter((id) => id.length > 0 && uuidPattern.test(id));
24351
+ if (validIds.length === 0) {
24352
+ throw new Error("actionItemId is required and must be a valid UUID");
23987
24353
  }
23988
- if (code === "08P01" && (msg.includes("ssl") || msg.includes("unsupported"))) {
23989
- return true;
24354
+ const base = normalizeBaseUrl(apiBaseUrl);
24355
+ const url = new URL(`${base}/issue_action_items`);
24356
+ if (validIds.length === 1) {
24357
+ url.searchParams.set("id", `eq.${validIds[0]}`);
24358
+ } else {
24359
+ url.searchParams.set("id", `in.(${validIds.join(",")})`);
23990
24360
  }
23991
- return false;
23992
- }
23993
- async function connectWithSslFallback(ClientClass, adminConn, verbose) {
23994
- const tryConnect = async (config2) => {
23995
- const client = new ClientClass(config2);
23996
- await client.connect();
23997
- return client;
24361
+ const headers = {
24362
+ "access-token": apiKey,
24363
+ Prefer: "return=representation",
24364
+ "Content-Type": "application/json",
24365
+ Connection: "close"
23998
24366
  };
23999
- if (!adminConn.sslFallbackEnabled) {
24000
- const client = await tryConnect(adminConn.clientConfig);
24001
- return { client, usedSsl: !!adminConn.clientConfig.ssl };
24367
+ if (debug) {
24368
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
24369
+ console.log(`Debug: Resolved API base URL: ${base}`);
24370
+ console.log(`Debug: GET URL: ${url.toString()}`);
24371
+ console.log(`Debug: Auth scheme: access-token`);
24372
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
24002
24373
  }
24003
- try {
24004
- const client = await tryConnect(adminConn.clientConfig);
24005
- return { client, usedSsl: true };
24006
- } catch (sslErr) {
24007
- if (!isSslNegotiationError(sslErr)) {
24008
- throw sslErr;
24009
- }
24010
- if (verbose) {
24011
- console.log("SSL connection failed, retrying without SSL...");
24012
- }
24013
- const noSslConfig = { ...adminConn.clientConfig, ssl: false };
24374
+ const response = await fetch(url.toString(), {
24375
+ method: "GET",
24376
+ headers
24377
+ });
24378
+ if (debug) {
24379
+ console.log(`Debug: Response status: ${response.status}`);
24380
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
24381
+ }
24382
+ const data = await response.text();
24383
+ if (response.ok) {
24014
24384
  try {
24015
- const client = await tryConnect(noSslConfig);
24016
- return { client, usedSsl: false };
24017
- } catch (noSslErr) {
24018
- if (isSslNegotiationError(noSslErr)) {
24019
- const msg = noSslErr?.message || "";
24020
- if (msg.toLowerCase().includes("ssl") && msg.toLowerCase().includes("required")) {
24021
- throw sslErr;
24022
- }
24385
+ const parsed = JSON.parse(data);
24386
+ if (Array.isArray(parsed)) {
24387
+ return parsed;
24023
24388
  }
24024
- throw noSslErr;
24025
- }
24026
- }
24027
- }
24028
- function sqlDir() {
24029
- const currentFile = fileURLToPath(import.meta.url);
24030
- const currentDir = path3.dirname(currentFile);
24031
- const candidates = [
24032
- path3.resolve(currentDir, "..", "sql"),
24033
- path3.resolve(currentDir, "..", "..", "sql")
24034
- ];
24035
- for (const candidate of candidates) {
24036
- if (fs3.existsSync(candidate)) {
24037
- return candidate;
24389
+ return parsed ? [parsed] : [];
24390
+ } catch {
24391
+ throw new Error(`Failed to parse action item response: ${data}`);
24038
24392
  }
24393
+ } else {
24394
+ throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
24039
24395
  }
24040
- throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
24041
- }
24042
- function loadSqlTemplate(filename) {
24043
- const p = path3.join(sqlDir(), filename);
24044
- return fs3.readFileSync(p, "utf8");
24045
- }
24046
- function applyTemplate(sql, vars) {
24047
- return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
24048
- const v = vars[key];
24049
- if (v === undefined)
24050
- throw new Error(`Missing SQL template var: ${key}`);
24051
- return v;
24052
- });
24053
24396
  }
24054
- function quoteIdent(ident) {
24055
- if (ident.includes("\x00")) {
24056
- throw new Error("Identifier cannot contain null bytes");
24397
+ async function fetchActionItems2(params) {
24398
+ const { apiKey, apiBaseUrl, issueId, debug } = params;
24399
+ if (!apiKey) {
24400
+ throw new Error("API key is required");
24401
+ }
24402
+ if (!issueId) {
24403
+ throw new Error("issueId is required");
24404
+ }
24405
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24406
+ if (!uuidPattern.test(issueId.trim())) {
24407
+ throw new Error("issueId must be a valid UUID");
24408
+ }
24409
+ const base = normalizeBaseUrl(apiBaseUrl);
24410
+ const url = new URL(`${base}/issue_action_items`);
24411
+ url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
24412
+ const headers = {
24413
+ "access-token": apiKey,
24414
+ Prefer: "return=representation",
24415
+ "Content-Type": "application/json",
24416
+ Connection: "close"
24417
+ };
24418
+ if (debug) {
24419
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
24420
+ console.log(`Debug: Resolved API base URL: ${base}`);
24421
+ console.log(`Debug: GET URL: ${url.toString()}`);
24422
+ console.log(`Debug: Auth scheme: access-token`);
24423
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
24424
+ }
24425
+ const response = await fetch(url.toString(), {
24426
+ method: "GET",
24427
+ headers
24428
+ });
24429
+ if (debug) {
24430
+ console.log(`Debug: Response status: ${response.status}`);
24431
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
24432
+ }
24433
+ const data = await response.text();
24434
+ if (response.ok) {
24435
+ try {
24436
+ return JSON.parse(data);
24437
+ } catch {
24438
+ throw new Error(`Failed to parse action items response: ${data}`);
24439
+ }
24440
+ } else {
24441
+ throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
24442
+ }
24443
+ }
24444
+ async function createActionItem2(params) {
24445
+ const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
24446
+ if (!apiKey) {
24447
+ throw new Error("API key is required");
24448
+ }
24449
+ if (!issueId) {
24450
+ throw new Error("issueId is required");
24451
+ }
24452
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24453
+ if (!uuidPattern.test(issueId.trim())) {
24454
+ throw new Error("issueId must be a valid UUID");
24455
+ }
24456
+ if (!title) {
24457
+ throw new Error("title is required");
24458
+ }
24459
+ const base = normalizeBaseUrl(apiBaseUrl);
24460
+ const url = new URL(`${base}/rpc/issue_action_item_create`);
24461
+ const bodyObj = {
24462
+ issue_id: issueId,
24463
+ title
24464
+ };
24465
+ if (description !== undefined) {
24466
+ bodyObj.description = description;
24467
+ }
24468
+ if (sqlAction !== undefined) {
24469
+ bodyObj.sql_action = sqlAction;
24470
+ }
24471
+ if (configs !== undefined) {
24472
+ bodyObj.configs = configs;
24473
+ }
24474
+ const body = JSON.stringify(bodyObj);
24475
+ const headers = {
24476
+ "access-token": apiKey,
24477
+ Prefer: "return=representation",
24478
+ "Content-Type": "application/json",
24479
+ Connection: "close"
24480
+ };
24481
+ if (debug) {
24482
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
24483
+ console.log(`Debug: Resolved API base URL: ${base}`);
24484
+ console.log(`Debug: POST URL: ${url.toString()}`);
24485
+ console.log(`Debug: Auth scheme: access-token`);
24486
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
24487
+ console.log(`Debug: Request body: ${body}`);
24488
+ }
24489
+ const response = await fetch(url.toString(), {
24490
+ method: "POST",
24491
+ headers,
24492
+ body
24493
+ });
24494
+ if (debug) {
24495
+ console.log(`Debug: Response status: ${response.status}`);
24496
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
24497
+ }
24498
+ const data = await response.text();
24499
+ if (response.ok) {
24500
+ try {
24501
+ return JSON.parse(data);
24502
+ } catch {
24503
+ throw new Error(`Failed to parse create action item response: ${data}`);
24504
+ }
24505
+ } else {
24506
+ throw new Error(formatHttpError("Failed to create action item", response.status, data));
24507
+ }
24508
+ }
24509
+ async function updateActionItem2(params) {
24510
+ const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
24511
+ if (!apiKey) {
24512
+ throw new Error("API key is required");
24513
+ }
24514
+ if (!actionItemId) {
24515
+ throw new Error("actionItemId is required");
24516
+ }
24517
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24518
+ if (!uuidPattern.test(actionItemId.trim())) {
24519
+ throw new Error("actionItemId must be a valid UUID");
24520
+ }
24521
+ const hasUpdateField = title !== undefined || description !== undefined || isDone !== undefined || status !== undefined || statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
24522
+ if (!hasUpdateField) {
24523
+ throw new Error("At least one field to update is required");
24524
+ }
24525
+ const base = normalizeBaseUrl(apiBaseUrl);
24526
+ const url = new URL(`${base}/rpc/issue_action_item_update`);
24527
+ const bodyObj = {
24528
+ action_item_id: actionItemId
24529
+ };
24530
+ if (title !== undefined) {
24531
+ bodyObj.title = title;
24532
+ }
24533
+ if (description !== undefined) {
24534
+ bodyObj.description = description;
24535
+ }
24536
+ if (isDone !== undefined) {
24537
+ bodyObj.is_done = isDone;
24538
+ }
24539
+ if (status !== undefined) {
24540
+ bodyObj.status = status;
24541
+ }
24542
+ if (statusReason !== undefined) {
24543
+ bodyObj.status_reason = statusReason;
24544
+ }
24545
+ if (sqlAction !== undefined) {
24546
+ bodyObj.sql_action = sqlAction;
24547
+ }
24548
+ if (configs !== undefined) {
24549
+ bodyObj.configs = configs;
24550
+ }
24551
+ const body = JSON.stringify(bodyObj);
24552
+ const headers = {
24553
+ "access-token": apiKey,
24554
+ Prefer: "return=representation",
24555
+ "Content-Type": "application/json",
24556
+ Connection: "close"
24557
+ };
24558
+ if (debug) {
24559
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
24560
+ console.log(`Debug: Resolved API base URL: ${base}`);
24561
+ console.log(`Debug: POST URL: ${url.toString()}`);
24562
+ console.log(`Debug: Auth scheme: access-token`);
24563
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
24564
+ console.log(`Debug: Request body: ${body}`);
24565
+ }
24566
+ const response = await fetch(url.toString(), {
24567
+ method: "POST",
24568
+ headers,
24569
+ body
24570
+ });
24571
+ if (debug) {
24572
+ console.log(`Debug: Response status: ${response.status}`);
24573
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
24574
+ }
24575
+ if (!response.ok) {
24576
+ const data = await response.text();
24577
+ throw new Error(formatHttpError("Failed to update action item", response.status, data));
24578
+ }
24579
+ }
24580
+
24581
+ // lib/util.ts
24582
+ function maskSecret2(secret) {
24583
+ if (!secret)
24584
+ return "";
24585
+ if (secret.length <= 8)
24586
+ return "****";
24587
+ if (secret.length <= 16)
24588
+ return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
24589
+ return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
24590
+ }
24591
+ function normalizeBaseUrl2(value) {
24592
+ const trimmed = (value || "").replace(/\/$/, "");
24593
+ try {
24594
+ new URL(trimmed);
24595
+ } catch {
24596
+ throw new Error(`Invalid base URL: ${value}`);
24597
+ }
24598
+ return trimmed;
24599
+ }
24600
+ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
24601
+ const defApi = defaults2.apiBaseUrl || "https://postgres.ai/api/general/";
24602
+ const defUi = defaults2.uiBaseUrl || "https://console.postgres.ai";
24603
+ const apiCandidate = opts?.apiBaseUrl || process.env.PGAI_API_BASE_URL || cfg?.baseUrl || defApi;
24604
+ const uiCandidate = opts?.uiBaseUrl || process.env.PGAI_UI_BASE_URL || defUi;
24605
+ return {
24606
+ apiBaseUrl: normalizeBaseUrl2(apiCandidate),
24607
+ uiBaseUrl: normalizeBaseUrl2(uiCandidate)
24608
+ };
24609
+ }
24610
+
24611
+ // lib/init.ts
24612
+ import { randomBytes } from "crypto";
24613
+ import { URL as URL2, fileURLToPath } from "url";
24614
+ import * as fs3 from "fs";
24615
+ import * as path3 from "path";
24616
+ var DEFAULT_MONITORING_USER = "postgres_ai_mon";
24617
+ var KNOWN_PROVIDERS = ["self-managed", "supabase"];
24618
+ var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
24619
+ var SKIP_ALTER_USER_PROVIDERS = ["supabase"];
24620
+ var SKIP_SEARCH_PATH_CHECK_PROVIDERS = ["supabase"];
24621
+ function validateProvider(provider) {
24622
+ if (!provider || KNOWN_PROVIDERS.includes(provider))
24623
+ return null;
24624
+ return `Unknown provider "${provider}". Known providers: ${KNOWN_PROVIDERS.join(", ")}. Treating as self-managed.`;
24625
+ }
24626
+ function sslModeToConfig(mode) {
24627
+ if (mode.toLowerCase() === "disable")
24628
+ return false;
24629
+ if (mode.toLowerCase() === "verify-full" || mode.toLowerCase() === "verify-ca")
24630
+ return true;
24631
+ return { rejectUnauthorized: false };
24632
+ }
24633
+ function extractSslModeFromUri(uri) {
24634
+ try {
24635
+ return new URL2(uri).searchParams.get("sslmode") ?? undefined;
24636
+ } catch {
24637
+ return uri.match(/[?&]sslmode=([^&]+)/i)?.[1];
24638
+ }
24639
+ }
24640
+ function stripSslModeFromUri(uri) {
24641
+ try {
24642
+ const u = new URL2(uri);
24643
+ u.searchParams.delete("sslmode");
24644
+ return u.toString();
24645
+ } catch {
24646
+ return uri.replace(/[?&]sslmode=[^&]*/gi, "").replace(/\?&/, "?").replace(/\?$/, "");
24647
+ }
24648
+ }
24649
+ function isSslNegotiationError(err) {
24650
+ if (!err || typeof err !== "object")
24651
+ return false;
24652
+ const e = err;
24653
+ const msg = typeof e.message === "string" ? e.message.toLowerCase() : "";
24654
+ const code = typeof e.code === "string" ? e.code : "";
24655
+ const fallbackPatterns = [
24656
+ "the server does not support ssl",
24657
+ "ssl off",
24658
+ "server does not support ssl connections"
24659
+ ];
24660
+ for (const pattern of fallbackPatterns) {
24661
+ if (msg.includes(pattern))
24662
+ return true;
24663
+ }
24664
+ if (code === "08P01" && (msg.includes("ssl") || msg.includes("unsupported"))) {
24665
+ return true;
24666
+ }
24667
+ return false;
24668
+ }
24669
+ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
24670
+ const tryConnect = async (config2) => {
24671
+ const client = new ClientClass(config2);
24672
+ await client.connect();
24673
+ return client;
24674
+ };
24675
+ if (!adminConn.sslFallbackEnabled) {
24676
+ const client = await tryConnect(adminConn.clientConfig);
24677
+ return { client, usedSsl: !!adminConn.clientConfig.ssl };
24678
+ }
24679
+ try {
24680
+ const client = await tryConnect(adminConn.clientConfig);
24681
+ return { client, usedSsl: true };
24682
+ } catch (sslErr) {
24683
+ if (!isSslNegotiationError(sslErr)) {
24684
+ throw sslErr;
24685
+ }
24686
+ if (verbose) {
24687
+ console.log("SSL connection failed, retrying without SSL...");
24688
+ }
24689
+ const noSslConfig = { ...adminConn.clientConfig, ssl: false };
24690
+ try {
24691
+ const client = await tryConnect(noSslConfig);
24692
+ return { client, usedSsl: false };
24693
+ } catch (noSslErr) {
24694
+ if (isSslNegotiationError(noSslErr)) {
24695
+ const msg = noSslErr?.message || "";
24696
+ if (msg.toLowerCase().includes("ssl") && msg.toLowerCase().includes("required")) {
24697
+ throw sslErr;
24698
+ }
24699
+ }
24700
+ throw noSslErr;
24701
+ }
24702
+ }
24703
+ }
24704
+ function sqlDir() {
24705
+ const currentFile = fileURLToPath(import.meta.url);
24706
+ const currentDir = path3.dirname(currentFile);
24707
+ const candidates = [
24708
+ path3.resolve(currentDir, "..", "sql"),
24709
+ path3.resolve(currentDir, "..", "..", "sql")
24710
+ ];
24711
+ for (const candidate of candidates) {
24712
+ if (fs3.existsSync(candidate)) {
24713
+ return candidate;
24714
+ }
24715
+ }
24716
+ throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
24717
+ }
24718
+ function loadSqlTemplate(filename) {
24719
+ const p = path3.join(sqlDir(), filename);
24720
+ return fs3.readFileSync(p, "utf8");
24721
+ }
24722
+ function applyTemplate(sql, vars) {
24723
+ return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
24724
+ const v = vars[key];
24725
+ if (v === undefined)
24726
+ throw new Error(`Missing SQL template var: ${key}`);
24727
+ return v;
24728
+ });
24729
+ }
24730
+ function quoteIdent(ident) {
24731
+ if (ident.includes("\x00")) {
24732
+ throw new Error("Identifier cannot contain null bytes");
24057
24733
  }
24058
24734
  return `"${ident.replace(/"/g, '""')}"`;
24059
24735
  }
@@ -24261,6 +24937,7 @@ async function resolveMonitoringPassword(opts) {
24261
24937
  async function buildInitPlan(params) {
24262
24938
  const monitoringUser = params.monitoringUser || DEFAULT_MONITORING_USER;
24263
24939
  const database = params.database;
24940
+ const provider = params.provider ?? "self-managed";
24264
24941
  const qRole = quoteIdent(monitoringUser);
24265
24942
  const qDb = quoteIdent(database);
24266
24943
  const qPw = quoteLiteral(params.monitoringPassword);
@@ -24270,7 +24947,8 @@ async function buildInitPlan(params) {
24270
24947
  ROLE_IDENT: qRole,
24271
24948
  DB_IDENT: qDb
24272
24949
  };
24273
- const roleStmt = `do $$ begin
24950
+ if (!SKIP_ROLE_CREATION_PROVIDERS.includes(provider)) {
24951
+ const roleStmt = `do $$ begin
24274
24952
  if not exists (select 1 from pg_catalog.pg_roles where rolname = ${qRoleNameLit}) then
24275
24953
  begin
24276
24954
  create user ${qRole} with password ${qPw};
@@ -24280,24 +24958,40 @@ async function buildInitPlan(params) {
24280
24958
  end if;
24281
24959
  alter user ${qRole} with password ${qPw};
24282
24960
  end $$;`;
24283
- const roleSql = applyTemplate(loadSqlTemplate("01.role.sql"), { ...vars, ROLE_STMT: roleStmt });
24284
- steps.push({ name: "01.role", sql: roleSql });
24961
+ const roleSql = applyTemplate(loadSqlTemplate("01.role.sql"), { ...vars, ROLE_STMT: roleStmt });
24962
+ steps.push({ name: "01.role", sql: roleSql });
24963
+ }
24964
+ steps.push({
24965
+ name: "02.extensions",
24966
+ sql: loadSqlTemplate("02.extensions.sql")
24967
+ });
24968
+ let permissionsSql = applyTemplate(loadSqlTemplate("03.permissions.sql"), vars);
24969
+ if (SKIP_ALTER_USER_PROVIDERS.includes(provider)) {
24970
+ permissionsSql = permissionsSql.split(`
24971
+ `).filter((line) => {
24972
+ const trimmed = line.trim();
24973
+ if (trimmed.startsWith("--") || trimmed === "")
24974
+ return true;
24975
+ return !/^\s*alter\s+user\s+/i.test(line);
24976
+ }).join(`
24977
+ `);
24978
+ }
24285
24979
  steps.push({
24286
- name: "02.permissions",
24287
- sql: applyTemplate(loadSqlTemplate("02.permissions.sql"), vars)
24980
+ name: "03.permissions",
24981
+ sql: permissionsSql
24288
24982
  });
24289
24983
  steps.push({
24290
- name: "05.helpers",
24291
- sql: applyTemplate(loadSqlTemplate("05.helpers.sql"), vars)
24984
+ name: "06.helpers",
24985
+ sql: applyTemplate(loadSqlTemplate("06.helpers.sql"), vars)
24292
24986
  });
24293
24987
  if (params.includeOptionalPermissions) {
24294
24988
  steps.push({
24295
- name: "03.optional_rds",
24296
- sql: applyTemplate(loadSqlTemplate("03.optional_rds.sql"), vars),
24989
+ name: "04.optional_rds",
24990
+ sql: applyTemplate(loadSqlTemplate("04.optional_rds.sql"), vars),
24297
24991
  optional: true
24298
24992
  }, {
24299
- name: "04.optional_self_managed",
24300
- sql: applyTemplate(loadSqlTemplate("04.optional_self_managed.sql"), vars),
24993
+ name: "05.optional_self_managed",
24994
+ sql: applyTemplate(loadSqlTemplate("05.optional_self_managed.sql"), vars),
24301
24995
  optional: true
24302
24996
  });
24303
24997
  }
@@ -24365,6 +25059,62 @@ async function applyInitPlan(params) {
24365
25059
  }
24366
25060
  return { applied, skippedOptional };
24367
25061
  }
25062
+ async function buildUninitPlan(params) {
25063
+ const monitoringUser = params.monitoringUser || DEFAULT_MONITORING_USER;
25064
+ const database = params.database;
25065
+ const provider = params.provider ?? "self-managed";
25066
+ const dropRole = params.dropRole ?? true;
25067
+ const qRole = quoteIdent(monitoringUser);
25068
+ const qDb = quoteIdent(database);
25069
+ const qRoleLiteral = quoteLiteral(monitoringUser);
25070
+ const steps = [];
25071
+ const vars = {
25072
+ ROLE_IDENT: qRole,
25073
+ DB_IDENT: qDb,
25074
+ ROLE_LITERAL: qRoleLiteral
25075
+ };
25076
+ steps.push({
25077
+ name: "01.drop_helpers",
25078
+ sql: applyTemplate(loadSqlTemplate("uninit/01.helpers.sql"), vars)
25079
+ });
25080
+ steps.push({
25081
+ name: "02.revoke_permissions",
25082
+ sql: applyTemplate(loadSqlTemplate("uninit/02.permissions.sql"), vars)
25083
+ });
25084
+ if (dropRole && !SKIP_ROLE_CREATION_PROVIDERS.includes(provider)) {
25085
+ steps.push({
25086
+ name: "03.drop_role",
25087
+ sql: applyTemplate(loadSqlTemplate("uninit/03.role.sql"), vars)
25088
+ });
25089
+ }
25090
+ return { monitoringUser, database, steps, dropRole };
25091
+ }
25092
+ async function applyUninitPlan(params) {
25093
+ const applied = [];
25094
+ const errors3 = [];
25095
+ const executeStep = async (step) => {
25096
+ await params.client.query("begin;");
25097
+ try {
25098
+ await params.client.query(step.sql, step.params);
25099
+ await params.client.query("commit;");
25100
+ } catch (e) {
25101
+ try {
25102
+ await params.client.query("rollback;");
25103
+ } catch {}
25104
+ throw e;
25105
+ }
25106
+ };
25107
+ for (const step of params.plan.steps) {
25108
+ try {
25109
+ await executeStep(step);
25110
+ applied.push(step.name);
25111
+ } catch (e) {
25112
+ const msg = e instanceof Error ? e.message : String(e);
25113
+ errors3.push(`${step.name}: ${msg}`);
25114
+ }
25115
+ }
25116
+ return { applied, errors: errors3 };
25117
+ }
24368
25118
  async function verifyInitSetup(params) {
24369
25119
  await params.client.query("begin isolation level repeatable read;");
24370
25120
  try {
@@ -24372,6 +25122,7 @@ async function verifyInitSetup(params) {
24372
25122
  const missingOptional = [];
24373
25123
  const role = params.monitoringUser;
24374
25124
  const db = params.database;
25125
+ const provider = params.provider ?? "self-managed";
24375
25126
  const roleRes = await params.client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [role]);
24376
25127
  const roleExists = (roleRes.rowCount ?? 0) > 0;
24377
25128
  if (!roleExists) {
@@ -24407,15 +25158,17 @@ async function verifyInitSetup(params) {
24407
25158
  if (!schemaUsageRes.rows?.[0]?.ok) {
24408
25159
  missingRequired.push("USAGE on schema public");
24409
25160
  }
24410
- const rolcfgRes = await params.client.query("select rolconfig from pg_catalog.pg_roles where rolname = $1", [role]);
24411
- const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
24412
- const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
24413
- if (typeof spLine !== "string" || !spLine) {
24414
- missingRequired.push("role search_path is set");
24415
- } else {
24416
- const sp = spLine.toLowerCase();
24417
- if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
24418
- missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
25161
+ if (!SKIP_SEARCH_PATH_CHECK_PROVIDERS.includes(provider)) {
25162
+ const rolcfgRes = await params.client.query("select rolconfig from pg_catalog.pg_roles where rolname = $1", [role]);
25163
+ const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
25164
+ const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
25165
+ if (typeof spLine !== "string" || !spLine) {
25166
+ missingRequired.push("role search_path is set");
25167
+ } else {
25168
+ const sp = spLine.toLowerCase();
25169
+ if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
25170
+ missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
25171
+ }
24419
25172
  }
24420
25173
  }
24421
25174
  const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
@@ -24459,6 +25212,430 @@ async function verifyInitSetup(params) {
24459
25212
  }
24460
25213
  }
24461
25214
 
25215
+ // lib/supabase.ts
25216
+ var SUPABASE_API_BASE = "https://api.supabase.com";
25217
+ function isValidProjectRef(ref) {
25218
+ return /^[a-z0-9]{10,30}$/i.test(ref);
25219
+ }
25220
+
25221
+ class SupabaseClient {
25222
+ config;
25223
+ constructor(config2) {
25224
+ if (!config2.projectRef) {
25225
+ throw new Error("Supabase project reference is required");
25226
+ }
25227
+ if (!config2.accessToken) {
25228
+ throw new Error("Supabase access token is required");
25229
+ }
25230
+ if (!isValidProjectRef(config2.projectRef)) {
25231
+ throw new Error(`Invalid Supabase project reference format: "${config2.projectRef}". Expected 10-30 alphanumeric characters.`);
25232
+ }
25233
+ this.config = config2;
25234
+ }
25235
+ async query(sql, readOnly = false) {
25236
+ const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(this.config.projectRef)}/database/query`;
25237
+ const response = await fetch(url, {
25238
+ method: "POST",
25239
+ headers: {
25240
+ "Content-Type": "application/json",
25241
+ Authorization: `Bearer ${this.config.accessToken}`
25242
+ },
25243
+ body: JSON.stringify({
25244
+ query: sql,
25245
+ read_only: readOnly
25246
+ })
25247
+ });
25248
+ const body = await response.text();
25249
+ let data;
25250
+ try {
25251
+ data = JSON.parse(body);
25252
+ } catch {
25253
+ throw this.createPgError({
25254
+ message: `Supabase API returned non-JSON response: ${body.slice(0, 200)}`,
25255
+ httpStatus: response.status
25256
+ });
25257
+ }
25258
+ if (!response.ok) {
25259
+ throw this.parseApiError(data, response.status);
25260
+ }
25261
+ if (data && typeof data === "object" && "error" in data && data.error) {
25262
+ throw this.parseApiError(data, response.status);
25263
+ }
25264
+ const rows = Array.isArray(data) ? data : [];
25265
+ return {
25266
+ rows,
25267
+ rowCount: rows.length
25268
+ };
25269
+ }
25270
+ async testConnection() {
25271
+ const result = await this.query("SELECT current_database() as db, version() as version", true);
25272
+ const row = result.rows[0] ?? {};
25273
+ return {
25274
+ database: String(row.db ?? ""),
25275
+ version: String(row.version ?? "")
25276
+ };
25277
+ }
25278
+ async getCurrentDatabase() {
25279
+ const result = await this.query("SELECT current_database() as db", true);
25280
+ const row = result.rows[0] ?? {};
25281
+ return String(row.db ?? "");
25282
+ }
25283
+ parseApiError(data, httpStatus) {
25284
+ if (data && typeof data === "object" && !Array.isArray(data)) {
25285
+ const errObj = "error" in data && data.error ? data.error : data;
25286
+ const pgCode = this.extractPgErrorCode(errObj);
25287
+ const message = this.extractErrorMessage(errObj);
25288
+ const detail = this.extractField(errObj, ["details", "detail"]);
25289
+ const hint = this.extractField(errObj, ["hint"]);
25290
+ return this.createPgError({
25291
+ message,
25292
+ code: pgCode,
25293
+ detail,
25294
+ hint,
25295
+ httpStatus,
25296
+ supabaseErrorCode: typeof errObj === "object" && errObj && "code" in errObj ? String(errObj.code ?? "") : undefined
25297
+ });
25298
+ }
25299
+ return this.createPgError({
25300
+ message: `Supabase API error (HTTP ${httpStatus})`,
25301
+ httpStatus
25302
+ });
25303
+ }
25304
+ extractPgErrorCode(errObj) {
25305
+ if (!errObj || typeof errObj !== "object")
25306
+ return;
25307
+ const obj = errObj;
25308
+ if (typeof obj.code === "string") {
25309
+ const code = obj.code;
25310
+ if (/^\d{5}$/.test(code)) {
25311
+ return code;
25312
+ }
25313
+ return this.mapSupabaseCodeToPg(code);
25314
+ }
25315
+ return;
25316
+ }
25317
+ mapSupabaseCodeToPg(code) {
25318
+ const mapping = {
25319
+ PGRST301: "28000",
25320
+ PGRST302: "28P01",
25321
+ "42501": "42501",
25322
+ PGRST000: "42501",
25323
+ "42601": "42601",
25324
+ "42P01": "42P01",
25325
+ PGRST200: "42P01",
25326
+ "42883": "42883",
25327
+ "08000": "08000",
25328
+ "08003": "08003",
25329
+ "08006": "08006",
25330
+ "42710": "42710"
25331
+ };
25332
+ return mapping[code];
25333
+ }
25334
+ extractErrorMessage(errObj) {
25335
+ if (!errObj || typeof errObj !== "object") {
25336
+ return "Unknown Supabase API error";
25337
+ }
25338
+ const obj = errObj;
25339
+ for (const field of ["message", "error", "msg", "description"]) {
25340
+ if (typeof obj[field] === "string" && obj[field]) {
25341
+ return obj[field];
25342
+ }
25343
+ }
25344
+ if (obj.error && typeof obj.error === "object") {
25345
+ return this.extractErrorMessage(obj.error);
25346
+ }
25347
+ return "Unknown Supabase API error";
25348
+ }
25349
+ extractField(errObj, fieldNames) {
25350
+ if (!errObj || typeof errObj !== "object")
25351
+ return;
25352
+ const obj = errObj;
25353
+ for (const field of fieldNames) {
25354
+ if (typeof obj[field] === "string" && obj[field]) {
25355
+ return obj[field];
25356
+ }
25357
+ }
25358
+ return;
25359
+ }
25360
+ createPgError(opts) {
25361
+ const err = new Error(opts.message);
25362
+ if (opts.code)
25363
+ err.code = opts.code;
25364
+ if (opts.detail)
25365
+ err.detail = opts.detail;
25366
+ if (opts.hint)
25367
+ err.hint = opts.hint;
25368
+ if (opts.httpStatus)
25369
+ err.httpStatus = opts.httpStatus;
25370
+ if (opts.supabaseErrorCode)
25371
+ err.supabaseErrorCode = opts.supabaseErrorCode;
25372
+ return err;
25373
+ }
25374
+ }
25375
+ async function fetchPoolerDatabaseUrl(config2, username) {
25376
+ const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config2.projectRef)}/config/database/pooler`;
25377
+ try {
25378
+ const response = await fetch(url, {
25379
+ method: "GET",
25380
+ headers: {
25381
+ Authorization: `Bearer ${config2.accessToken}`
25382
+ }
25383
+ });
25384
+ if (!response.ok) {
25385
+ return null;
25386
+ }
25387
+ const data = await response.json();
25388
+ if (Array.isArray(data) && data.length > 0) {
25389
+ const pooler = data[0];
25390
+ if (pooler.db_host && pooler.db_port && pooler.db_name) {
25391
+ return `postgresql://${username}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
25392
+ }
25393
+ if (typeof pooler.connection_string === "string") {
25394
+ try {
25395
+ const connUrl = new URL(pooler.connection_string);
25396
+ const portPart = connUrl.port ? `:${connUrl.port}` : "";
25397
+ return `postgresql://${username}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
25398
+ } catch {
25399
+ return null;
25400
+ }
25401
+ }
25402
+ }
25403
+ return null;
25404
+ } catch {
25405
+ return null;
25406
+ }
25407
+ }
25408
+ function resolveSupabaseConfig(opts) {
25409
+ const accessToken = opts.accessToken?.trim() || process.env.SUPABASE_ACCESS_TOKEN?.trim() || "";
25410
+ const projectRef = opts.projectRef?.trim() || process.env.SUPABASE_PROJECT_REF?.trim() || "";
25411
+ if (!accessToken) {
25412
+ throw new Error(`Supabase access token is required.
25413
+ ` + `Provide it via --supabase-access-token or SUPABASE_ACCESS_TOKEN environment variable.
25414
+ ` + "Generate a token at: https://supabase.com/dashboard/account/tokens");
25415
+ }
25416
+ if (!projectRef) {
25417
+ throw new Error(`Supabase project reference is required.
25418
+ ` + `Provide it via --supabase-project-ref or SUPABASE_PROJECT_REF environment variable.
25419
+ ` + "Find your project ref in the Supabase dashboard URL: https://supabase.com/dashboard/project/<ref>");
25420
+ }
25421
+ return { accessToken, projectRef };
25422
+ }
25423
+ function extractProjectRefFromUrl(dbUrl) {
25424
+ try {
25425
+ const url = new URL(dbUrl);
25426
+ const host = url.hostname;
25427
+ const match = host.match(/^(?:db\.)?([^.]+)\.supabase\.co$/i);
25428
+ if (match && match[1]) {
25429
+ return match[1];
25430
+ }
25431
+ if (host.includes("pooler.supabase.com")) {
25432
+ const username = url.username;
25433
+ const userMatch = username.match(/^postgres\.([a-z0-9]+)$/i);
25434
+ if (userMatch && userMatch[1]) {
25435
+ return userMatch[1];
25436
+ }
25437
+ }
25438
+ const poolerMatch = host.match(/^([a-z0-9]+)\.pooler\.supabase\.com$/i);
25439
+ if (poolerMatch && poolerMatch[1] && !poolerMatch[1].startsWith("aws-")) {
25440
+ return poolerMatch[1];
25441
+ }
25442
+ return;
25443
+ } catch {
25444
+ return;
25445
+ }
25446
+ }
25447
+ async function applyInitPlanViaSupabase(params) {
25448
+ const applied = [];
25449
+ const skippedOptional = [];
25450
+ const executeStep = async (step) => {
25451
+ const wrappedSql = `BEGIN;
25452
+ ${step.sql}
25453
+ COMMIT;`;
25454
+ await params.client.query(wrappedSql, false);
25455
+ };
25456
+ for (const step of params.plan.steps.filter((s) => !s.optional)) {
25457
+ try {
25458
+ if (params.verbose) {
25459
+ console.log(`Executing step: ${step.name}`);
25460
+ }
25461
+ await executeStep(step);
25462
+ applied.push(step.name);
25463
+ } catch (e) {
25464
+ const msg = e instanceof Error ? e.message : String(e);
25465
+ const errAny = e;
25466
+ const wrapped = new Error(`Failed at step "${step.name}": ${msg}`);
25467
+ const pgErrorFields = [
25468
+ "code",
25469
+ "detail",
25470
+ "hint",
25471
+ "position",
25472
+ "internalPosition",
25473
+ "internalQuery",
25474
+ "where",
25475
+ "schema",
25476
+ "table",
25477
+ "column",
25478
+ "dataType",
25479
+ "constraint",
25480
+ "file",
25481
+ "line",
25482
+ "routine",
25483
+ "httpStatus",
25484
+ "supabaseErrorCode"
25485
+ ];
25486
+ for (const field of pgErrorFields) {
25487
+ if (errAny[field] !== undefined) {
25488
+ wrapped[field] = errAny[field];
25489
+ }
25490
+ }
25491
+ if (e instanceof Error && e.stack) {
25492
+ wrapped.stack = e.stack;
25493
+ }
25494
+ throw wrapped;
25495
+ }
25496
+ }
25497
+ for (const step of params.plan.steps.filter((s) => s.optional)) {
25498
+ try {
25499
+ if (params.verbose) {
25500
+ console.log(`Executing optional step: ${step.name}`);
25501
+ }
25502
+ await executeStep(step);
25503
+ applied.push(step.name);
25504
+ } catch {
25505
+ skippedOptional.push(step.name);
25506
+ }
25507
+ }
25508
+ return { applied, skippedOptional };
25509
+ }
25510
+ async function verifyInitSetupViaSupabase(params) {
25511
+ const missingRequired = [];
25512
+ const missingOptional = [];
25513
+ const role = params.monitoringUser;
25514
+ const db = params.database;
25515
+ if (!isValidIdentifier(role)) {
25516
+ throw new Error(`Invalid monitoring user name: "${role}". Must be a valid PostgreSQL identifier (letters, digits, underscores, max 63 chars, starting with letter or underscore).`);
25517
+ }
25518
+ const roleRes = await params.client.query(`SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
25519
+ const roleExists = roleRes.rowCount > 0;
25520
+ if (!roleExists) {
25521
+ missingRequired.push(`role "${role}" does not exist`);
25522
+ return { ok: false, missingRequired, missingOptional };
25523
+ }
25524
+ const connectRes = await params.client.query(`SELECT has_database_privilege('${escapeLiteral2(role)}', '${escapeLiteral2(db)}', 'CONNECT') as ok`, true);
25525
+ if (!connectRes.rows?.[0]?.ok) {
25526
+ missingRequired.push(`CONNECT on database "${db}"`);
25527
+ }
25528
+ const pgMonitorRes = await params.client.query(`SELECT pg_has_role('${escapeLiteral2(role)}', 'pg_monitor', 'member') as ok`, true);
25529
+ if (!pgMonitorRes.rows?.[0]?.ok) {
25530
+ missingRequired.push("membership in role pg_monitor");
25531
+ }
25532
+ const pgIndexRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'pg_catalog.pg_index', 'SELECT') as ok`, true);
25533
+ if (!pgIndexRes.rows?.[0]?.ok) {
25534
+ missingRequired.push("SELECT on pg_catalog.pg_index");
25535
+ }
25536
+ const schemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'postgres_ai'", true);
25537
+ if (schemaExistsRes.rowCount === 0) {
25538
+ missingRequired.push("schema postgres_ai exists");
25539
+ } else {
25540
+ const schemaPrivRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'postgres_ai', 'USAGE') as ok`, true);
25541
+ if (!schemaPrivRes.rows?.[0]?.ok) {
25542
+ missingRequired.push("USAGE on schema postgres_ai");
25543
+ }
25544
+ }
25545
+ const viewExistsRes = await params.client.query("SELECT to_regclass('postgres_ai.pg_statistic') IS NOT NULL as ok", true);
25546
+ if (!viewExistsRes.rows?.[0]?.ok) {
25547
+ missingRequired.push("view postgres_ai.pg_statistic exists");
25548
+ } else {
25549
+ const viewPrivRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'postgres_ai.pg_statistic', 'SELECT') as ok`, true);
25550
+ if (!viewPrivRes.rows?.[0]?.ok) {
25551
+ missingRequired.push("SELECT on view postgres_ai.pg_statistic");
25552
+ }
25553
+ }
25554
+ const publicSchemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'public'", true);
25555
+ if (publicSchemaExistsRes.rowCount === 0) {
25556
+ missingRequired.push("schema public exists");
25557
+ } else {
25558
+ const schemaUsageRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'public', 'USAGE') as ok`, true);
25559
+ if (!schemaUsageRes.rows?.[0]?.ok) {
25560
+ missingRequired.push("USAGE on schema public");
25561
+ }
25562
+ }
25563
+ const rolcfgRes = await params.client.query(`SELECT rolconfig FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
25564
+ const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
25565
+ const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
25566
+ if (typeof spLine !== "string" || !spLine) {
25567
+ missingRequired.push("role search_path is set");
25568
+ } else {
25569
+ const sp = spLine.toLowerCase();
25570
+ if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
25571
+ missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
25572
+ }
25573
+ }
25574
+ const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
25575
+ if (explainFnExistsRes.rowCount === 0) {
25576
+ missingRequired.push("function postgres_ai.explain_generic exists");
25577
+ } else {
25578
+ const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
25579
+ if (!explainFnRes.rows?.[0]?.ok) {
25580
+ missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
25581
+ }
25582
+ }
25583
+ const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
25584
+ if (tableDescribeFnExistsRes.rowCount === 0) {
25585
+ missingRequired.push("function postgres_ai.table_describe exists");
25586
+ } else {
25587
+ const tableDescribeFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.table_describe(text)', 'EXECUTE') as ok`, true);
25588
+ if (!tableDescribeFnRes.rows?.[0]?.ok) {
25589
+ missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
25590
+ }
25591
+ }
25592
+ if (params.includeOptionalPermissions) {
25593
+ const extRes = await params.client.query("SELECT 1 FROM pg_extension WHERE extname = 'rds_tools'", true);
25594
+ if (extRes.rowCount === 0) {
25595
+ missingOptional.push("extension rds_tools");
25596
+ } else {
25597
+ try {
25598
+ const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok`, true);
25599
+ if (!fnRes.rows?.[0]?.ok) {
25600
+ missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
25601
+ }
25602
+ } catch {
25603
+ missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
25604
+ }
25605
+ }
25606
+ const optionalFns = [
25607
+ "pg_catalog.pg_stat_file(text)",
25608
+ "pg_catalog.pg_stat_file(text, boolean)",
25609
+ "pg_catalog.pg_ls_dir(text)",
25610
+ "pg_catalog.pg_ls_dir(text, boolean, boolean)"
25611
+ ];
25612
+ for (const fn of optionalFns) {
25613
+ try {
25614
+ const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', '${fn}', 'EXECUTE') as ok`, true);
25615
+ if (!fnRes.rows?.[0]?.ok) {
25616
+ missingOptional.push(`EXECUTE on ${fn}`);
25617
+ }
25618
+ } catch {
25619
+ missingOptional.push(`EXECUTE on ${fn}`);
25620
+ }
25621
+ }
25622
+ }
25623
+ return {
25624
+ ok: missingRequired.length === 0,
25625
+ missingRequired,
25626
+ missingOptional
25627
+ };
25628
+ }
25629
+ function isValidIdentifier(name) {
25630
+ return /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(name);
25631
+ }
25632
+ function escapeLiteral2(value) {
25633
+ if (value.includes("\x00")) {
25634
+ throw new Error("SQL literal cannot contain null bytes");
25635
+ }
25636
+ return value.replace(/'/g, "''");
25637
+ }
25638
+
24462
25639
  // lib/pkce.ts
24463
25640
  import * as crypto from "crypto";
24464
25641
  function generateRandomString(length = 64) {
@@ -24958,17 +26135,17 @@ where
24958
26135
  statement_timeout_seconds: 300
24959
26136
  },
24960
26137
  pg_invalid_indexes: {
24961
- description: "This metric identifies invalid indexes in the database. It provides insights into the number of invalid indexes and their details. This metric helps administrators identify and fix invalid indexes to improve database performance.",
26138
+ description: "This metric identifies invalid indexes in the database with decision tree data for remediation. It provides insights into whether to DROP (if duplicate exists), RECREATE (if backs constraint), or flag as UNCERTAIN (if additional RCA is needed to check query plans). Decision tree: 1) Valid duplicate exists -> DROP, 2) Backs PK/UNIQUE constraint -> RECREATE, 3) Table < 10K rows -> RECREATE (small tables rebuild quickly, typically under 1 second), 4) Otherwise -> UNCERTAIN (need query plan analysis to assess impact).",
24962
26139
  sqls: {
24963
26140
  11: `with fk_indexes as ( /* pgwatch_generated */
24964
26141
  select
24965
- schemaname as tag_schema_name,
24966
- (indexrelid::regclass)::text as tag_index_name,
24967
- (relid::regclass)::text as tag_table_name,
24968
- (confrelid::regclass)::text as tag_fk_table_ref,
24969
- array_to_string(indclass, ', ') as tag_opclasses
24970
- from
24971
- pg_stat_all_indexes
26142
+ schemaname as schema_name,
26143
+ indexrelid,
26144
+ (indexrelid::regclass)::text as index_name,
26145
+ (relid::regclass)::text as table_name,
26146
+ (confrelid::regclass)::text as fk_table_ref,
26147
+ array_to_string(indclass, ', ') as opclasses
26148
+ from pg_stat_all_indexes
24972
26149
  join pg_index using (indexrelid)
24973
26150
  left join pg_constraint
24974
26151
  on array_to_string(indkey, ',') = array_to_string(conkey, ',')
@@ -24977,37 +26154,58 @@ where
24977
26154
  and contype = 'f'
24978
26155
  where idx_scan = 0
24979
26156
  and indisunique is false
24980
- and conkey is not null --conkey is not null then true else false end as is_fk_idx
24981
- ), data as (
26157
+ and conkey is not null
26158
+ ),
26159
+ -- Find valid indexes that could be duplicates (same table, same columns)
26160
+ valid_duplicates as (
26161
+ select
26162
+ inv.indexrelid as invalid_indexrelid,
26163
+ val.indexrelid as valid_indexrelid,
26164
+ (val.indexrelid::regclass)::text as valid_index_name,
26165
+ pg_get_indexdef(val.indexrelid) as valid_index_definition
26166
+ from pg_index inv
26167
+ join pg_index val on inv.indrelid = val.indrelid -- same table
26168
+ and inv.indkey = val.indkey -- same columns (in same order)
26169
+ and inv.indexrelid != val.indexrelid -- different index
26170
+ and val.indisvalid = true -- valid index
26171
+ where inv.indisvalid = false
26172
+ ),
26173
+ data as (
24982
26174
  select
24983
26175
  pci.relname as tag_index_name,
24984
26176
  pn.nspname as tag_schema_name,
24985
26177
  pct.relname as tag_table_name,
24986
- quote_ident(pn.nspname) as tag_schema_name,
24987
- quote_ident(pci.relname) as tag_index_name,
24988
- quote_ident(pct.relname) as tag_table_name,
24989
26178
  coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
24990
26179
  pg_get_indexdef(pidx.indexrelid) as index_definition,
24991
- pg_relation_size(pidx.indexrelid) index_size_bytes,
26180
+ pg_relation_size(pidx.indexrelid) as index_size_bytes,
26181
+ -- Constraint info
26182
+ pidx.indisprimary as is_pk,
26183
+ pidx.indisunique as is_unique,
26184
+ con.conname as constraint_name,
26185
+ -- Table row estimate
26186
+ pct.reltuples::bigint as table_row_estimate,
26187
+ -- Valid duplicate check
26188
+ (vd.valid_indexrelid is not null) as has_valid_duplicate,
26189
+ vd.valid_index_name,
26190
+ vd.valid_index_definition,
26191
+ -- FK support check
24992
26192
  ((
24993
26193
  select count(1)
24994
26194
  from fk_indexes fi
24995
- where
24996
- fi.tag_fk_table_ref = pct.relname
24997
- and fi.tag_opclasses like (array_to_string(pidx.indclass, ', ') || '%')
26195
+ where fi.fk_table_ref = pct.relname
26196
+ and fi.opclasses like (array_to_string(pidx.indclass, ', ') || '%')
24998
26197
  ) > 0)::int as supports_fk
24999
26198
  from pg_index pidx
25000
- join pg_class as pci on pci.oid = pidx.indexrelid
25001
- join pg_class as pct on pct.oid = pidx.indrelid
26199
+ join pg_class pci on pci.oid = pidx.indexrelid
26200
+ join pg_class pct on pct.oid = pidx.indrelid
25002
26201
  left join pg_namespace pn on pn.oid = pct.relnamespace
26202
+ left join pg_constraint con on con.conindid = pidx.indexrelid
26203
+ left join valid_duplicates vd on vd.invalid_indexrelid = pidx.indexrelid
25003
26204
  where pidx.indisvalid = false
25004
- ), data_total as (
25005
- select
25006
- sum(index_size_bytes) as index_size_bytes_sum
25007
- from data
25008
- ), num_data as (
26205
+ ),
26206
+ num_data as (
25009
26207
  select
25010
- row_number() over () num,
26208
+ row_number() over () as num,
25011
26209
  data.*
25012
26210
  from data
25013
26211
  )
@@ -25626,7 +26824,14 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
25626
26824
  index_size_bytes: indexSizeBytes,
25627
26825
  index_size_pretty: formatBytes(indexSizeBytes),
25628
26826
  index_definition: String(transformed.index_definition || ""),
25629
- supports_fk: toBool(transformed.supports_fk)
26827
+ supports_fk: toBool(transformed.supports_fk),
26828
+ is_pk: toBool(transformed.is_pk),
26829
+ is_unique: toBool(transformed.is_unique),
26830
+ constraint_name: transformed.constraint_name ? String(transformed.constraint_name) : null,
26831
+ table_row_estimate: parseInt(String(transformed.table_row_estimate || 0), 10),
26832
+ has_valid_duplicate: toBool(transformed.has_valid_duplicate),
26833
+ valid_duplicate_name: transformed.valid_index_name ? String(transformed.valid_index_name) : null,
26834
+ valid_duplicate_definition: transformed.valid_index_definition ? String(transformed.valid_index_definition) : null
25630
26835
  };
25631
26836
  });
25632
26837
  }
@@ -26715,7 +27920,7 @@ program2.command("set-default-project <project>").description("store default pro
26715
27920
  writeConfig({ defaultProject: value });
26716
27921
  console.log(`Default project saved: ${value}`);
26717
27922
  });
26718
- program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).addHelpText("after", [
27923
+ program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.").option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).option("--supabase", "Use Supabase Management API instead of direct PostgreSQL connection", false).option("--supabase-access-token <token>", "Supabase Management API access token (or SUPABASE_ACCESS_TOKEN env)").option("--supabase-project-ref <ref>", "Supabase project reference (or SUPABASE_PROJECT_REF env)").option("--json", "Output result as JSON (machine-readable)", false).addHelpText("after", [
26719
27924
  "",
26720
27925
  "Examples:",
26721
27926
  " postgresai prepare-db postgresql://admin@host:5432/dbname",
@@ -26751,21 +27956,60 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26751
27956
  " postgresai prepare-db <conn> --reset-password --password '...'",
26752
27957
  "",
26753
27958
  "Offline SQL plan (no DB connection):",
26754
- " postgresai prepare-db --print-sql"
27959
+ " postgresai prepare-db --print-sql",
27960
+ "",
27961
+ "Supabase mode (use Management API instead of direct connection):",
27962
+ " postgresai prepare-db --supabase --supabase-project-ref <ref>",
27963
+ " SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
27964
+ "",
27965
+ " Generate a token at: https://supabase.com/dashboard/account/tokens",
27966
+ " Find your project ref in: https://supabase.com/dashboard/project/<ref>",
27967
+ "",
27968
+ "Provider-specific behavior (for direct connections):",
27969
+ " --provider supabase Skip role creation (create user in Supabase dashboard)",
27970
+ " Skip ALTER USER (restricted by Supabase)"
26755
27971
  ].join(`
26756
27972
  `)).action(async (conn, opts, cmd) => {
26757
- if (opts.verify && opts.resetPassword) {
26758
- console.error("\u2717 Provide only one of --verify or --reset-password");
27973
+ const jsonOutput = opts.json;
27974
+ const outputJson = (data) => {
27975
+ console.log(JSON.stringify(data, null, 2));
27976
+ };
27977
+ const outputError = (error2) => {
27978
+ if (jsonOutput) {
27979
+ outputJson({
27980
+ success: false,
27981
+ mode: opts.supabase ? "supabase" : "direct",
27982
+ error: error2
27983
+ });
27984
+ } else {
27985
+ console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error2.message}`);
27986
+ if (error2.step)
27987
+ console.error(` Step: ${error2.step}`);
27988
+ if (error2.code)
27989
+ console.error(` Code: ${error2.code}`);
27990
+ if (error2.detail)
27991
+ console.error(` Detail: ${error2.detail}`);
27992
+ if (error2.hint)
27993
+ console.error(` Hint: ${error2.hint}`);
27994
+ if (error2.httpStatus)
27995
+ console.error(` HTTP Status: ${error2.httpStatus}`);
27996
+ }
26759
27997
  process.exitCode = 1;
27998
+ };
27999
+ if (opts.verify && opts.resetPassword) {
28000
+ outputError({ message: "Provide only one of --verify or --reset-password" });
26760
28001
  return;
26761
28002
  }
26762
28003
  if (opts.verify && opts.printSql) {
26763
- console.error("\u2717 --verify cannot be combined with --print-sql");
26764
- process.exitCode = 1;
28004
+ outputError({ message: "--verify cannot be combined with --print-sql" });
26765
28005
  return;
26766
28006
  }
26767
28007
  const shouldPrintSql = !!opts.printSql;
26768
28008
  const redactPasswords = (sql) => redactPasswordsInSql(sql);
28009
+ const providerWarning = validateProvider(opts.provider);
28010
+ if (providerWarning) {
28011
+ console.warn(`\u26A0 ${providerWarning}`);
28012
+ }
26769
28013
  if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
26770
28014
  if (shouldPrintSql) {
26771
28015
  const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
@@ -26775,12 +28019,14 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26775
28019
  database,
26776
28020
  monitoringUser: opts.monitoringUser,
26777
28021
  monitoringPassword: monPassword,
26778
- includeOptionalPermissions: includeOptionalPermissions2
28022
+ includeOptionalPermissions: includeOptionalPermissions2,
28023
+ provider: opts.provider
26779
28024
  });
26780
28025
  console.log(`
26781
28026
  --- SQL plan (offline; not connected) ---`);
26782
28027
  console.log(`-- database: ${database}`);
26783
28028
  console.log(`-- monitoring user: ${opts.monitoringUser}`);
28029
+ console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
26784
28030
  console.log(`-- optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
26785
28031
  for (const step of plan.steps) {
26786
28032
  console.log(`
@@ -26794,6 +28040,285 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26794
28040
  return;
26795
28041
  }
26796
28042
  }
28043
+ if (opts.supabase) {
28044
+ let supabaseConfig;
28045
+ try {
28046
+ let projectRef = opts.supabaseProjectRef;
28047
+ if (!projectRef && conn) {
28048
+ projectRef = extractProjectRefFromUrl(conn);
28049
+ }
28050
+ supabaseConfig = resolveSupabaseConfig({
28051
+ accessToken: opts.supabaseAccessToken,
28052
+ projectRef
28053
+ });
28054
+ } catch (e) {
28055
+ const msg = e instanceof Error ? e.message : String(e);
28056
+ outputError({ message: msg });
28057
+ return;
28058
+ }
28059
+ const includeOptionalPermissions2 = !opts.skipOptionalPermissions;
28060
+ if (!jsonOutput) {
28061
+ console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
28062
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
28063
+ console.log(`Optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
28064
+ }
28065
+ const supabaseClient = new SupabaseClient(supabaseConfig);
28066
+ let databaseUrl = null;
28067
+ if (jsonOutput) {
28068
+ databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
28069
+ }
28070
+ try {
28071
+ const database = await supabaseClient.getCurrentDatabase();
28072
+ if (!database) {
28073
+ throw new Error("Failed to resolve current database name");
28074
+ }
28075
+ if (!jsonOutput) {
28076
+ console.log(`Database: ${database}`);
28077
+ }
28078
+ if (opts.verify) {
28079
+ const v = await verifyInitSetupViaSupabase({
28080
+ client: supabaseClient,
28081
+ database,
28082
+ monitoringUser: opts.monitoringUser,
28083
+ includeOptionalPermissions: includeOptionalPermissions2
28084
+ });
28085
+ if (v.ok) {
28086
+ if (jsonOutput) {
28087
+ const result = {
28088
+ success: true,
28089
+ mode: "supabase",
28090
+ action: "verify",
28091
+ database,
28092
+ monitoringUser: opts.monitoringUser,
28093
+ verified: true,
28094
+ missingOptional: v.missingOptional
28095
+ };
28096
+ if (databaseUrl) {
28097
+ result.databaseUrl = databaseUrl;
28098
+ }
28099
+ outputJson(result);
28100
+ } else {
28101
+ console.log("\u2713 prepare-db verify: OK");
28102
+ if (v.missingOptional.length > 0) {
28103
+ console.log("\u26A0 Optional items missing:");
28104
+ for (const m of v.missingOptional)
28105
+ console.log(`- ${m}`);
28106
+ }
28107
+ }
28108
+ return;
28109
+ }
28110
+ if (jsonOutput) {
28111
+ const result = {
28112
+ success: false,
28113
+ mode: "supabase",
28114
+ action: "verify",
28115
+ database,
28116
+ monitoringUser: opts.monitoringUser,
28117
+ verified: false,
28118
+ missingRequired: v.missingRequired,
28119
+ missingOptional: v.missingOptional
28120
+ };
28121
+ if (databaseUrl) {
28122
+ result.databaseUrl = databaseUrl;
28123
+ }
28124
+ outputJson(result);
28125
+ } else {
28126
+ console.error("\u2717 prepare-db verify failed: missing required items");
28127
+ for (const m of v.missingRequired)
28128
+ console.error(`- ${m}`);
28129
+ if (v.missingOptional.length > 0) {
28130
+ console.error("Optional items missing:");
28131
+ for (const m of v.missingOptional)
28132
+ console.error(`- ${m}`);
28133
+ }
28134
+ }
28135
+ process.exitCode = 1;
28136
+ return;
28137
+ }
28138
+ let monPassword;
28139
+ let passwordGenerated = false;
28140
+ try {
28141
+ const resolved = await resolveMonitoringPassword({
28142
+ passwordFlag: opts.password,
28143
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
28144
+ monitoringUser: opts.monitoringUser
28145
+ });
28146
+ monPassword = resolved.password;
28147
+ passwordGenerated = resolved.generated;
28148
+ if (resolved.generated) {
28149
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
28150
+ if (canPrint) {
28151
+ if (!jsonOutput) {
28152
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
28153
+ console.error("");
28154
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
28155
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
28156
+ console.error("");
28157
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
28158
+ }
28159
+ } else {
28160
+ console.error([
28161
+ `\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
28162
+ "",
28163
+ "Provide it explicitly:",
28164
+ " --password <password> or PGAI_MON_PASSWORD=...",
28165
+ "",
28166
+ "Or (NOT recommended) print the generated password:",
28167
+ " --print-password"
28168
+ ].join(`
28169
+ `));
28170
+ process.exitCode = 1;
28171
+ return;
28172
+ }
28173
+ }
28174
+ } catch (e) {
28175
+ const msg = e instanceof Error ? e.message : String(e);
28176
+ outputError({ message: msg });
28177
+ return;
28178
+ }
28179
+ const plan = await buildInitPlan({
28180
+ database,
28181
+ monitoringUser: opts.monitoringUser,
28182
+ monitoringPassword: monPassword,
28183
+ includeOptionalPermissions: includeOptionalPermissions2
28184
+ });
28185
+ const supabaseApplicableSteps = plan.steps.filter((s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed");
28186
+ const effectivePlan = opts.resetPassword ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") } : { ...plan, steps: supabaseApplicableSteps };
28187
+ if (shouldPrintSql) {
28188
+ console.log(`
28189
+ --- SQL plan ---`);
28190
+ for (const step of effectivePlan.steps) {
28191
+ console.log(`
28192
+ -- ${step.name}${step.optional ? " (optional)" : ""}`);
28193
+ console.log(redactPasswords(step.sql));
28194
+ }
28195
+ console.log(`
28196
+ --- end SQL plan ---
28197
+ `);
28198
+ console.log("Note: passwords are redacted in the printed SQL output.");
28199
+ return;
28200
+ }
28201
+ const { applied, skippedOptional } = await applyInitPlanViaSupabase({
28202
+ client: supabaseClient,
28203
+ plan: effectivePlan
28204
+ });
28205
+ if (jsonOutput) {
28206
+ const result = {
28207
+ success: true,
28208
+ mode: "supabase",
28209
+ action: opts.resetPassword ? "reset-password" : "apply",
28210
+ database,
28211
+ monitoringUser: opts.monitoringUser,
28212
+ applied,
28213
+ skippedOptional,
28214
+ warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
28215
+ };
28216
+ if (passwordGenerated) {
28217
+ result.generatedPassword = monPassword;
28218
+ }
28219
+ if (databaseUrl) {
28220
+ result.databaseUrl = databaseUrl;
28221
+ }
28222
+ outputJson(result);
28223
+ } else {
28224
+ console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
28225
+ if (skippedOptional.length > 0) {
28226
+ console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
28227
+ for (const s of skippedOptional)
28228
+ console.log(`- ${s}`);
28229
+ }
28230
+ if (process.stdout.isTTY) {
28231
+ console.log(`Applied ${applied.length} steps`);
28232
+ }
28233
+ }
28234
+ } catch (error2) {
28235
+ const errAny = error2;
28236
+ let message = "";
28237
+ if (error2 instanceof Error && error2.message) {
28238
+ message = error2.message;
28239
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
28240
+ message = errAny.message;
28241
+ } else {
28242
+ message = String(error2);
28243
+ }
28244
+ if (!message || message === "[object Object]") {
28245
+ message = "Unknown error";
28246
+ }
28247
+ const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
28248
+ const failedStep = stepMatch?.[1];
28249
+ const errorObj = { message };
28250
+ if (failedStep)
28251
+ errorObj.step = failedStep;
28252
+ if (errAny && typeof errAny === "object") {
28253
+ if (typeof errAny.code === "string" && errAny.code)
28254
+ errorObj.code = errAny.code;
28255
+ if (typeof errAny.detail === "string" && errAny.detail)
28256
+ errorObj.detail = errAny.detail;
28257
+ if (typeof errAny.hint === "string" && errAny.hint)
28258
+ errorObj.hint = errAny.hint;
28259
+ if (typeof errAny.httpStatus === "number")
28260
+ errorObj.httpStatus = errAny.httpStatus;
28261
+ }
28262
+ if (jsonOutput) {
28263
+ outputJson({
28264
+ success: false,
28265
+ mode: "supabase",
28266
+ error: errorObj
28267
+ });
28268
+ process.exitCode = 1;
28269
+ } else {
28270
+ console.error(`Error: prepare-db (Supabase): ${message}`);
28271
+ if (failedStep) {
28272
+ console.error(` Step: ${failedStep}`);
28273
+ }
28274
+ if (errAny && typeof errAny === "object") {
28275
+ if (typeof errAny.code === "string" && errAny.code) {
28276
+ console.error(` Code: ${errAny.code}`);
28277
+ }
28278
+ if (typeof errAny.detail === "string" && errAny.detail) {
28279
+ console.error(` Detail: ${errAny.detail}`);
28280
+ }
28281
+ if (typeof errAny.hint === "string" && errAny.hint) {
28282
+ console.error(` Hint: ${errAny.hint}`);
28283
+ }
28284
+ if (typeof errAny.httpStatus === "number") {
28285
+ console.error(` HTTP Status: ${errAny.httpStatus}`);
28286
+ }
28287
+ }
28288
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
28289
+ if (errAny.code === "42501") {
28290
+ if (failedStep === "01.role") {
28291
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
28292
+ } else if (failedStep === "03.permissions") {
28293
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
28294
+ }
28295
+ console.error(" Fix: ensure your Supabase access token has sufficient permissions");
28296
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
28297
+ }
28298
+ if (errAny.code === "42P06" || message.includes("already exists") && failedStep === "03.permissions") {
28299
+ console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
28300
+ console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
28301
+ }
28302
+ }
28303
+ if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
28304
+ if (errAny.httpStatus === 401) {
28305
+ console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
28306
+ }
28307
+ if (errAny.httpStatus === 403) {
28308
+ console.error(" Hint: access denied; check your token permissions and project access");
28309
+ }
28310
+ if (errAny.httpStatus === 404) {
28311
+ console.error(" Hint: project not found; verify the project reference is correct");
28312
+ }
28313
+ if (errAny.httpStatus === 429) {
28314
+ console.error(" Hint: rate limited; wait a moment and try again");
28315
+ }
28316
+ }
28317
+ process.exitCode = 1;
28318
+ }
28319
+ }
28320
+ return;
28321
+ }
26797
28322
  let adminConn;
26798
28323
  try {
26799
28324
  adminConn = resolveAdminConnection({
@@ -26808,18 +28333,24 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26808
28333
  });
26809
28334
  } catch (e) {
26810
28335
  const msg = e instanceof Error ? e.message : String(e);
26811
- console.error(`Error: prepare-db: ${msg}`);
26812
- if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
26813
- console.error("");
26814
- cmd.outputHelp({ error: true });
28336
+ if (jsonOutput) {
28337
+ outputError({ message: msg });
28338
+ } else {
28339
+ console.error(`Error: prepare-db: ${msg}`);
28340
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
28341
+ console.error("");
28342
+ cmd.outputHelp({ error: true });
28343
+ }
28344
+ process.exitCode = 1;
26815
28345
  }
26816
- process.exitCode = 1;
26817
28346
  return;
26818
28347
  }
26819
28348
  const includeOptionalPermissions = !opts.skipOptionalPermissions;
26820
- console.log(`Connecting to: ${adminConn.display}`);
26821
- console.log(`Monitoring user: ${opts.monitoringUser}`);
26822
- console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
28349
+ if (!jsonOutput) {
28350
+ console.log(`Connecting to: ${adminConn.display}`);
28351
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
28352
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
28353
+ }
26823
28354
  let client;
26824
28355
  try {
26825
28356
  const connResult = await connectWithSslFallback(Client, adminConn);
@@ -26834,29 +28365,57 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26834
28365
  client,
26835
28366
  database,
26836
28367
  monitoringUser: opts.monitoringUser,
26837
- includeOptionalPermissions
28368
+ includeOptionalPermissions,
28369
+ provider: opts.provider
26838
28370
  });
26839
28371
  if (v.ok) {
26840
- console.log("\u2713 prepare-db verify: OK");
26841
- if (v.missingOptional.length > 0) {
26842
- console.log("\u26A0 Optional items missing:");
26843
- for (const m of v.missingOptional)
26844
- console.log(`- ${m}`);
28372
+ if (jsonOutput) {
28373
+ outputJson({
28374
+ success: true,
28375
+ mode: "direct",
28376
+ action: "verify",
28377
+ database,
28378
+ monitoringUser: opts.monitoringUser,
28379
+ provider: opts.provider,
28380
+ verified: true,
28381
+ missingOptional: v.missingOptional
28382
+ });
28383
+ } else {
28384
+ console.log(`\u2713 prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
28385
+ if (v.missingOptional.length > 0) {
28386
+ console.log("\u26A0 Optional items missing:");
28387
+ for (const m of v.missingOptional)
28388
+ console.log(`- ${m}`);
28389
+ }
26845
28390
  }
26846
28391
  return;
26847
28392
  }
26848
- console.error("\u2717 prepare-db verify failed: missing required items");
26849
- for (const m of v.missingRequired)
26850
- console.error(`- ${m}`);
26851
- if (v.missingOptional.length > 0) {
26852
- console.error("Optional items missing:");
26853
- for (const m of v.missingOptional)
28393
+ if (jsonOutput) {
28394
+ outputJson({
28395
+ success: false,
28396
+ mode: "direct",
28397
+ action: "verify",
28398
+ database,
28399
+ monitoringUser: opts.monitoringUser,
28400
+ verified: false,
28401
+ missingRequired: v.missingRequired,
28402
+ missingOptional: v.missingOptional
28403
+ });
28404
+ } else {
28405
+ console.error("\u2717 prepare-db verify failed: missing required items");
28406
+ for (const m of v.missingRequired)
26854
28407
  console.error(`- ${m}`);
28408
+ if (v.missingOptional.length > 0) {
28409
+ console.error("Optional items missing:");
28410
+ for (const m of v.missingOptional)
28411
+ console.error(`- ${m}`);
28412
+ }
26855
28413
  }
26856
28414
  process.exitCode = 1;
26857
28415
  return;
26858
28416
  }
26859
28417
  let monPassword;
28418
+ let passwordGenerated = false;
26860
28419
  try {
26861
28420
  const resolved = await resolveMonitoringPassword({
26862
28421
  passwordFlag: opts.password,
@@ -26864,15 +28423,18 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26864
28423
  monitoringUser: opts.monitoringUser
26865
28424
  });
26866
28425
  monPassword = resolved.password;
28426
+ passwordGenerated = resolved.generated;
26867
28427
  if (resolved.generated) {
26868
- const canPrint = process.stdout.isTTY || !!opts.printPassword;
28428
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
26869
28429
  if (canPrint) {
26870
- const shellSafe = monPassword.replace(/'/g, "'\\''");
26871
- console.error("");
26872
- console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
26873
- console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
26874
- console.error("");
26875
- console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
28430
+ if (!jsonOutput) {
28431
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
28432
+ console.error("");
28433
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
28434
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
28435
+ console.error("");
28436
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
28437
+ }
26876
28438
  } else {
26877
28439
  console.error([
26878
28440
  `\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
@@ -26890,40 +28452,321 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26890
28452
  }
26891
28453
  } catch (e) {
26892
28454
  const msg = e instanceof Error ? e.message : String(e);
26893
- console.error(`\u2717 ${msg}`);
28455
+ outputError({ message: msg });
28456
+ return;
28457
+ }
28458
+ const plan = await buildInitPlan({
28459
+ database,
28460
+ monitoringUser: opts.monitoringUser,
28461
+ monitoringPassword: monPassword,
28462
+ includeOptionalPermissions,
28463
+ provider: opts.provider
28464
+ });
28465
+ const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
28466
+ if (opts.resetPassword && effectivePlan.steps.length === 0) {
28467
+ console.error(`\u2717 --reset-password not supported for provider "${opts.provider}" (role creation is skipped)`);
28468
+ process.exitCode = 1;
28469
+ return;
28470
+ }
28471
+ if (shouldPrintSql) {
28472
+ console.log(`
28473
+ --- SQL plan ---`);
28474
+ for (const step of effectivePlan.steps) {
28475
+ console.log(`
28476
+ -- ${step.name}${step.optional ? " (optional)" : ""}`);
28477
+ console.log(redactPasswords(step.sql));
28478
+ }
28479
+ console.log(`
28480
+ --- end SQL plan ---
28481
+ `);
28482
+ console.log("Note: passwords are redacted in the printed SQL output.");
28483
+ return;
28484
+ }
28485
+ const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
28486
+ if (jsonOutput) {
28487
+ const result = {
28488
+ success: true,
28489
+ mode: "direct",
28490
+ action: opts.resetPassword ? "reset-password" : "apply",
28491
+ database,
28492
+ monitoringUser: opts.monitoringUser,
28493
+ applied,
28494
+ skippedOptional,
28495
+ warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
28496
+ };
28497
+ if (passwordGenerated) {
28498
+ result.generatedPassword = monPassword;
28499
+ }
28500
+ outputJson(result);
28501
+ } else {
28502
+ console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
28503
+ if (skippedOptional.length > 0) {
28504
+ console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
28505
+ for (const s of skippedOptional)
28506
+ console.log(`- ${s}`);
28507
+ }
28508
+ if (process.stdout.isTTY) {
28509
+ console.log(`Applied ${applied.length} steps`);
28510
+ }
28511
+ }
28512
+ } catch (error2) {
28513
+ const errAny = error2;
28514
+ let message = "";
28515
+ if (error2 instanceof Error && error2.message) {
28516
+ message = error2.message;
28517
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
28518
+ message = errAny.message;
28519
+ } else {
28520
+ message = String(error2);
28521
+ }
28522
+ if (!message || message === "[object Object]") {
28523
+ message = "Unknown error";
28524
+ }
28525
+ const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
28526
+ const failedStep = stepMatch?.[1];
28527
+ const errorObj = { message };
28528
+ if (failedStep)
28529
+ errorObj.step = failedStep;
28530
+ if (errAny && typeof errAny === "object") {
28531
+ if (typeof errAny.code === "string" && errAny.code)
28532
+ errorObj.code = errAny.code;
28533
+ if (typeof errAny.detail === "string" && errAny.detail)
28534
+ errorObj.detail = errAny.detail;
28535
+ if (typeof errAny.hint === "string" && errAny.hint)
28536
+ errorObj.hint = errAny.hint;
28537
+ }
28538
+ if (jsonOutput) {
28539
+ outputJson({
28540
+ success: false,
28541
+ mode: "direct",
28542
+ error: errorObj
28543
+ });
28544
+ process.exitCode = 1;
28545
+ } else {
28546
+ console.error(`Error: prepare-db: ${message}`);
28547
+ if (failedStep) {
28548
+ console.error(` Step: ${failedStep}`);
28549
+ }
28550
+ if (errAny && typeof errAny === "object") {
28551
+ if (typeof errAny.code === "string" && errAny.code) {
28552
+ console.error(` Code: ${errAny.code}`);
28553
+ }
28554
+ if (typeof errAny.detail === "string" && errAny.detail) {
28555
+ console.error(` Detail: ${errAny.detail}`);
28556
+ }
28557
+ if (typeof errAny.hint === "string" && errAny.hint) {
28558
+ console.error(` Hint: ${errAny.hint}`);
28559
+ }
28560
+ }
28561
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
28562
+ if (errAny.code === "42501") {
28563
+ if (failedStep === "01.role") {
28564
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
28565
+ } else if (failedStep === "03.permissions") {
28566
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
28567
+ }
28568
+ console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
28569
+ console.error(" Fix: on managed Postgres, use the provider's admin/master user");
28570
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
28571
+ }
28572
+ if (errAny.code === "42P06" || message.includes("already exists") && failedStep === "03.permissions") {
28573
+ console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
28574
+ console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
28575
+ }
28576
+ if (errAny.code === "ECONNREFUSED") {
28577
+ console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
28578
+ }
28579
+ if (errAny.code === "ENOTFOUND") {
28580
+ console.error(" Hint: DNS resolution failed; double-check the host name");
28581
+ }
28582
+ if (errAny.code === "ETIMEDOUT") {
28583
+ console.error(" Hint: connection timed out; check network/firewall rules");
28584
+ }
28585
+ }
28586
+ process.exitCode = 1;
28587
+ }
28588
+ } finally {
28589
+ if (client) {
28590
+ try {
28591
+ await client.end();
28592
+ } catch {}
28593
+ }
28594
+ }
28595
+ });
28596
+ program2.command("unprepare-db [conn]").description("remove monitoring setup: drop monitoring user, views, schema, and revoke permissions").option("--db-url <url>", "PostgreSQL connection URL (admin) (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to remove", DEFAULT_MONITORING_USER).option("--keep-role", "Keep the monitoring role (only revoke permissions and drop objects)", false).option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.").option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--force", "Skip confirmation prompt", false).option("--json", "Output result as JSON (machine-readable)", false).addHelpText("after", [
28597
+ "",
28598
+ "Examples:",
28599
+ " postgresai unprepare-db postgresql://admin@host:5432/dbname",
28600
+ ' postgresai unprepare-db "dbname=dbname host=host user=admin"',
28601
+ " postgresai unprepare-db -h host -p 5432 -U admin -d dbname",
28602
+ "",
28603
+ "Admin password:",
28604
+ " --admin-password <password> or PGPASSWORD=... (libpq standard)",
28605
+ "",
28606
+ "Keep role but remove objects/permissions:",
28607
+ " postgresai unprepare-db <conn> --keep-role",
28608
+ "",
28609
+ "Inspect SQL without applying changes:",
28610
+ " postgresai unprepare-db <conn> --print-sql",
28611
+ "",
28612
+ "Offline SQL plan (no DB connection):",
28613
+ " postgresai unprepare-db --print-sql",
28614
+ "",
28615
+ "Skip confirmation prompt:",
28616
+ " postgresai unprepare-db <conn> --force"
28617
+ ].join(`
28618
+ `)).action(async (conn, opts, cmd) => {
28619
+ const jsonOutput = opts.json;
28620
+ const outputJson = (data) => {
28621
+ console.log(JSON.stringify(data, null, 2));
28622
+ };
28623
+ const outputError = (error2) => {
28624
+ if (jsonOutput) {
28625
+ outputJson({
28626
+ success: false,
28627
+ error: error2
28628
+ });
28629
+ } else {
28630
+ console.error(`Error: unprepare-db: ${error2.message}`);
28631
+ if (error2.step)
28632
+ console.error(` Step: ${error2.step}`);
28633
+ if (error2.code)
28634
+ console.error(` Code: ${error2.code}`);
28635
+ if (error2.detail)
28636
+ console.error(` Detail: ${error2.detail}`);
28637
+ if (error2.hint)
28638
+ console.error(` Hint: ${error2.hint}`);
28639
+ }
28640
+ process.exitCode = 1;
28641
+ };
28642
+ const shouldPrintSql = !!opts.printSql;
28643
+ const dropRole = !opts.keepRole;
28644
+ const providerWarning = validateProvider(opts.provider);
28645
+ if (providerWarning) {
28646
+ console.warn(`\u26A0 ${providerWarning}`);
28647
+ }
28648
+ if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
28649
+ if (shouldPrintSql) {
28650
+ const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
28651
+ const plan = await buildUninitPlan({
28652
+ database,
28653
+ monitoringUser: opts.monitoringUser,
28654
+ dropRole,
28655
+ provider: opts.provider
28656
+ });
28657
+ console.log(`
28658
+ --- SQL plan (offline; not connected) ---`);
28659
+ console.log(`-- database: ${database}`);
28660
+ console.log(`-- monitoring user: ${opts.monitoringUser}`);
28661
+ console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
28662
+ console.log(`-- drop role: ${dropRole}`);
28663
+ for (const step of plan.steps) {
28664
+ console.log(`
28665
+ -- ${step.name}`);
28666
+ console.log(step.sql);
28667
+ }
28668
+ console.log(`
28669
+ --- end SQL plan ---
28670
+ `);
28671
+ return;
28672
+ }
28673
+ }
28674
+ let adminConn;
28675
+ try {
28676
+ adminConn = resolveAdminConnection({
28677
+ conn,
28678
+ dbUrlFlag: opts.dbUrl,
28679
+ host: opts.host ?? process.env.PGHOST,
28680
+ port: opts.port ?? process.env.PGPORT,
28681
+ username: opts.username ?? process.env.PGUSER,
28682
+ dbname: opts.dbname ?? process.env.PGDATABASE,
28683
+ adminPassword: opts.adminPassword,
28684
+ envPassword: process.env.PGPASSWORD
28685
+ });
28686
+ } catch (e) {
28687
+ const msg = e instanceof Error ? e.message : String(e);
28688
+ if (jsonOutput) {
28689
+ outputError({ message: msg });
28690
+ } else {
28691
+ console.error(`Error: unprepare-db: ${msg}`);
28692
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
28693
+ console.error("");
28694
+ cmd.outputHelp({ error: true });
28695
+ }
26894
28696
  process.exitCode = 1;
28697
+ }
28698
+ return;
28699
+ }
28700
+ if (!jsonOutput) {
28701
+ console.log(`Connecting to: ${adminConn.display}`);
28702
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
28703
+ console.log(`Drop role: ${dropRole}`);
28704
+ }
28705
+ if (!opts.force && !jsonOutput && !shouldPrintSql) {
28706
+ const answer = await new Promise((resolve6) => {
28707
+ const readline = getReadline();
28708
+ readline.question(`This will remove the monitoring setup for user "${opts.monitoringUser}"${dropRole ? " and drop the role" : ""}. Continue? [y/N] `, (ans) => resolve6(ans.trim().toLowerCase()));
28709
+ });
28710
+ if (answer !== "y" && answer !== "yes") {
28711
+ console.log("Aborted.");
26895
28712
  return;
26896
28713
  }
26897
- const plan = await buildInitPlan({
28714
+ }
28715
+ let client;
28716
+ try {
28717
+ const connResult = await connectWithSslFallback(Client, adminConn);
28718
+ client = connResult.client;
28719
+ const dbRes = await client.query("select current_database() as db");
28720
+ const database = dbRes.rows?.[0]?.db;
28721
+ if (typeof database !== "string" || !database) {
28722
+ throw new Error("Failed to resolve current database name");
28723
+ }
28724
+ const plan = await buildUninitPlan({
26898
28725
  database,
26899
28726
  monitoringUser: opts.monitoringUser,
26900
- monitoringPassword: monPassword,
26901
- includeOptionalPermissions
28727
+ dropRole,
28728
+ provider: opts.provider
26902
28729
  });
26903
- const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
26904
28730
  if (shouldPrintSql) {
26905
28731
  console.log(`
26906
28732
  --- SQL plan ---`);
26907
- for (const step of effectivePlan.steps) {
28733
+ for (const step of plan.steps) {
26908
28734
  console.log(`
26909
- -- ${step.name}${step.optional ? " (optional)" : ""}`);
26910
- console.log(redactPasswords(step.sql));
28735
+ -- ${step.name}`);
28736
+ console.log(step.sql);
26911
28737
  }
26912
28738
  console.log(`
26913
28739
  --- end SQL plan ---
26914
28740
  `);
26915
- console.log("Note: passwords are redacted in the printed SQL output.");
26916
28741
  return;
26917
28742
  }
26918
- const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
26919
- console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
26920
- if (skippedOptional.length > 0) {
26921
- console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
26922
- for (const s of skippedOptional)
26923
- console.log(`- ${s}`);
26924
- }
26925
- if (process.stdout.isTTY) {
26926
- console.log(`Applied ${applied.length} steps`);
28743
+ const { applied, errors: errors3 } = await applyUninitPlan({ client, plan });
28744
+ if (jsonOutput) {
28745
+ outputJson({
28746
+ success: errors3.length === 0,
28747
+ action: "unprepare",
28748
+ database,
28749
+ monitoringUser: opts.monitoringUser,
28750
+ dropRole,
28751
+ applied,
28752
+ errors: errors3
28753
+ });
28754
+ if (errors3.length > 0) {
28755
+ process.exitCode = 1;
28756
+ }
28757
+ } else {
28758
+ if (errors3.length === 0) {
28759
+ console.log("\u2713 unprepare-db completed");
28760
+ console.log(`Applied ${applied.length} steps`);
28761
+ } else {
28762
+ console.log("\u26A0 unprepare-db completed with errors");
28763
+ console.log(`Applied ${applied.length} steps`);
28764
+ console.log("Errors:");
28765
+ for (const err of errors3) {
28766
+ console.log(` - ${err}`);
28767
+ }
28768
+ process.exitCode = 1;
28769
+ }
26927
28770
  }
26928
28771
  } catch (error2) {
26929
28772
  const errAny = error2;
@@ -26938,51 +28781,58 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26938
28781
  if (!message || message === "[object Object]") {
26939
28782
  message = "Unknown error";
26940
28783
  }
26941
- console.error(`Error: prepare-db: ${message}`);
26942
- const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
26943
- const failedStep = stepMatch?.[1];
26944
- if (failedStep) {
26945
- console.error(` Step: ${failedStep}`);
26946
- }
28784
+ const errorObj = { message };
26947
28785
  if (errAny && typeof errAny === "object") {
26948
- if (typeof errAny.code === "string" && errAny.code) {
26949
- console.error(` Code: ${errAny.code}`);
26950
- }
26951
- if (typeof errAny.detail === "string" && errAny.detail) {
26952
- console.error(` Detail: ${errAny.detail}`);
26953
- }
26954
- if (typeof errAny.hint === "string" && errAny.hint) {
26955
- console.error(` Hint: ${errAny.hint}`);
26956
- }
26957
- }
26958
- if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
26959
- if (errAny.code === "42501") {
26960
- if (failedStep === "01.role") {
26961
- console.error(" Context: role creation/update requires CREATEROLE or superuser");
26962
- } else if (failedStep === "02.permissions") {
26963
- console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
28786
+ if (typeof errAny.code === "string" && errAny.code)
28787
+ errorObj.code = errAny.code;
28788
+ if (typeof errAny.detail === "string" && errAny.detail)
28789
+ errorObj.detail = errAny.detail;
28790
+ if (typeof errAny.hint === "string" && errAny.hint)
28791
+ errorObj.hint = errAny.hint;
28792
+ }
28793
+ if (jsonOutput) {
28794
+ outputJson({
28795
+ success: false,
28796
+ error: errorObj
28797
+ });
28798
+ process.exitCode = 1;
28799
+ } else {
28800
+ console.error(`Error: unprepare-db: ${message}`);
28801
+ if (errAny && typeof errAny === "object") {
28802
+ if (typeof errAny.code === "string" && errAny.code) {
28803
+ console.error(` Code: ${errAny.code}`);
28804
+ }
28805
+ if (typeof errAny.detail === "string" && errAny.detail) {
28806
+ console.error(` Detail: ${errAny.detail}`);
28807
+ }
28808
+ if (typeof errAny.hint === "string" && errAny.hint) {
28809
+ console.error(` Hint: ${errAny.hint}`);
26964
28810
  }
26965
- console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
26966
- console.error(" Fix: on managed Postgres, use the provider's admin/master user");
26967
- console.error(" Tip: run with --print-sql to review the exact SQL plan");
26968
- }
26969
- if (errAny.code === "ECONNREFUSED") {
26970
- console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
26971
- }
26972
- if (errAny.code === "ENOTFOUND") {
26973
- console.error(" Hint: DNS resolution failed; double-check the host name");
26974
28811
  }
26975
- if (errAny.code === "ETIMEDOUT") {
26976
- console.error(" Hint: connection timed out; check network/firewall rules");
28812
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
28813
+ if (errAny.code === "42501") {
28814
+ console.error(" Context: dropping roles/objects requires sufficient privileges");
28815
+ console.error(" Fix: connect as a superuser (or a role with appropriate DROP privileges)");
28816
+ }
28817
+ if (errAny.code === "ECONNREFUSED") {
28818
+ console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
28819
+ }
28820
+ if (errAny.code === "ENOTFOUND") {
28821
+ console.error(" Hint: DNS resolution failed; double-check the host name");
28822
+ }
28823
+ if (errAny.code === "ETIMEDOUT") {
28824
+ console.error(" Hint: connection timed out; check network/firewall rules");
28825
+ }
26977
28826
  }
28827
+ process.exitCode = 1;
26978
28828
  }
26979
- process.exitCode = 1;
26980
28829
  } finally {
26981
28830
  if (client) {
26982
28831
  try {
26983
28832
  await client.end();
26984
28833
  } catch {}
26985
28834
  }
28835
+ closeReadline();
26986
28836
  }
26987
28837
  });
26988
28838
  program2.command("checkup [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL").option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--[no-]upload", "upload JSON results to PostgresAI (default: enabled; requires API key)", undefined).option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout (implies --no-upload)").addHelpText("after", [
@@ -27111,14 +28961,14 @@ async function resolveOrInitPaths() {
27111
28961
  }
27112
28962
  function isDockerRunning() {
27113
28963
  try {
27114
- const result = spawnSync2("docker", ["info"], { stdio: "pipe" });
28964
+ const result = spawnSync2("docker", ["info"], { stdio: "pipe", timeout: 5000 });
27115
28965
  return result.status === 0;
27116
28966
  } catch {
27117
28967
  return false;
27118
28968
  }
27119
28969
  }
27120
28970
  function getComposeCmd() {
27121
- const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore" }).status === 0;
28971
+ const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
27122
28972
  if (tryCmd("docker-compose", ["version"]))
27123
28973
  return ["docker-compose"];
27124
28974
  if (tryCmd("docker", ["compose", "version"]))
@@ -27127,7 +28977,7 @@ function getComposeCmd() {
27127
28977
  }
27128
28978
  function checkRunningContainers() {
27129
28979
  try {
27130
- const result = spawnSync2("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8" });
28980
+ const result = spawnSync2("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8", timeout: 5000 });
27131
28981
  if (result.status === 0 && result.stdout) {
27132
28982
  const containers = result.stdout.trim().split(`
27133
28983
  `).filter(Boolean);
@@ -27213,14 +29063,10 @@ mon.command("local-install").description("install local monitoring stack (genera
27213
29063
  console.log(`Project directory: ${projectDir}
27214
29064
  `);
27215
29065
  const envFile = path5.resolve(projectDir, ".env");
27216
- let existingTag = null;
27217
29066
  let existingRegistry = null;
27218
29067
  let existingPassword = null;
27219
29068
  if (fs5.existsSync(envFile)) {
27220
29069
  const existingEnv = fs5.readFileSync(envFile, "utf8");
27221
- const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
27222
- if (tagMatch)
27223
- existingTag = tagMatch[1].trim();
27224
29070
  const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
27225
29071
  if (registryMatch)
27226
29072
  existingRegistry = registryMatch[1].trim();
@@ -27228,7 +29074,7 @@ mon.command("local-install").description("install local monitoring stack (genera
27228
29074
  if (pwdMatch)
27229
29075
  existingPassword = pwdMatch[1].trim();
27230
29076
  }
27231
- const imageTag = opts.tag || process.env.PGAI_TAG || existingTag || package_default.version;
29077
+ const imageTag = opts.tag || package_default.version;
27232
29078
  const envLines = [`PGAI_TAG=${imageTag}`];
27233
29079
  if (existingRegistry) {
27234
29080
  envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
@@ -27565,6 +29411,9 @@ var MONITORING_CONTAINERS = [
27565
29411
  "sources-generator",
27566
29412
  "postgres-reports"
27567
29413
  ];
29414
+ var COMPOSE_PROJECT_NAME = "postgres_ai";
29415
+ var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
29416
+ var NETWORK_CLEANUP_DELAY_MS = 2000;
27568
29417
  async function removeOrphanedContainers() {
27569
29418
  for (const container of MONITORING_CONTAINERS) {
27570
29419
  try {
@@ -27573,7 +29422,18 @@ async function removeOrphanedContainers() {
27573
29422
  }
27574
29423
  }
27575
29424
  mon.command("stop").description("stop monitoring services").action(async () => {
27576
- const code = await runCompose(["down"]);
29425
+ let code = await runCompose(["down", "--remove-orphans"]);
29426
+ if (code !== 0) {
29427
+ await removeOrphanedContainers();
29428
+ await new Promise((resolve6) => setTimeout(resolve6, NETWORK_CLEANUP_DELAY_MS));
29429
+ code = await runCompose(["down", "--remove-orphans"]);
29430
+ }
29431
+ if (code !== 0) {
29432
+ try {
29433
+ await execFilePromise("docker", ["network", "rm", DOCKER_NETWORK_NAME]);
29434
+ code = 0;
29435
+ } catch {}
29436
+ }
27577
29437
  if (code !== 0)
27578
29438
  process.exitCode = code;
27579
29439
  });
@@ -28340,18 +30200,36 @@ function interpretEscapes2(str2) {
28340
30200
  `).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\x00/g, "\\");
28341
30201
  }
28342
30202
  var issues = program2.command("issues").description("issues management");
28343
- issues.command("list").description("list issues").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
30203
+ issues.command("list").description("list issues").option("--status <status>", "filter by status: open, closed, or all (default: all)").option("--limit <n>", "max number of issues to return (default: 20)", parseInt).option("--offset <n>", "number of issues to skip (default: 0)", parseInt).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
30204
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
28344
30205
  try {
28345
30206
  const rootOpts = program2.opts();
28346
30207
  const cfg = readConfig();
28347
30208
  const { apiKey } = getConfig(rootOpts);
28348
30209
  if (!apiKey) {
30210
+ spinner.stop();
28349
30211
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28350
30212
  process.exitCode = 1;
28351
30213
  return;
28352
30214
  }
30215
+ const orgId = cfg.orgId ?? undefined;
28353
30216
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28354
- const result = await fetchIssues2({ apiKey, apiBaseUrl, debug: !!opts.debug });
30217
+ let statusFilter;
30218
+ if (opts.status === "open") {
30219
+ statusFilter = "open";
30220
+ } else if (opts.status === "closed") {
30221
+ statusFilter = "closed";
30222
+ }
30223
+ const result = await fetchIssues2({
30224
+ apiKey,
30225
+ apiBaseUrl,
30226
+ orgId,
30227
+ status: statusFilter,
30228
+ limit: opts.limit,
30229
+ offset: opts.offset,
30230
+ debug: !!opts.debug
30231
+ });
30232
+ spinner.stop();
28355
30233
  const trimmed = Array.isArray(result) ? result.map((r) => ({
28356
30234
  id: r.id,
28357
30235
  title: r.title,
@@ -28360,17 +30238,20 @@ issues.command("list").description("list issues").option("--debug", "enable debu
28360
30238
  })) : result;
28361
30239
  printResult(trimmed, opts.json);
28362
30240
  } catch (err) {
30241
+ spinner.stop();
28363
30242
  const message = err instanceof Error ? err.message : String(err);
28364
30243
  console.error(message);
28365
30244
  process.exitCode = 1;
28366
30245
  }
28367
30246
  });
28368
30247
  issues.command("view <issueId>").description("view issue details and comments").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
30248
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
28369
30249
  try {
28370
30250
  const rootOpts = program2.opts();
28371
30251
  const cfg = readConfig();
28372
30252
  const { apiKey } = getConfig(rootOpts);
28373
30253
  if (!apiKey) {
30254
+ spinner.stop();
28374
30255
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28375
30256
  process.exitCode = 1;
28376
30257
  return;
@@ -28378,32 +30259,38 @@ issues.command("view <issueId>").description("view issue details and comments").
28378
30259
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28379
30260
  const issue2 = await fetchIssue2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
28380
30261
  if (!issue2) {
30262
+ spinner.stop();
28381
30263
  console.error("Issue not found");
28382
30264
  process.exitCode = 1;
28383
30265
  return;
28384
30266
  }
30267
+ spinner.update("Fetching comments...");
28385
30268
  const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
30269
+ spinner.stop();
28386
30270
  const combined = { issue: issue2, comments };
28387
30271
  printResult(combined, opts.json);
28388
30272
  } catch (err) {
30273
+ spinner.stop();
28389
30274
  const message = err instanceof Error ? err.message : String(err);
28390
30275
  console.error(message);
28391
30276
  process.exitCode = 1;
28392
30277
  }
28393
30278
  });
28394
30279
  issues.command("post-comment <issueId> <content>").description("post a new comment to an issue").option("--parent <uuid>", "parent comment id").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, content, opts) => {
30280
+ if (opts.debug) {
30281
+ console.log(`Debug: Original content: ${JSON.stringify(content)}`);
30282
+ }
30283
+ content = interpretEscapes2(content);
30284
+ if (opts.debug) {
30285
+ console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
30286
+ }
30287
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
28395
30288
  try {
28396
- if (opts.debug) {
28397
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
28398
- }
28399
- content = interpretEscapes2(content);
28400
- if (opts.debug) {
28401
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
28402
- }
28403
30289
  const rootOpts = program2.opts();
28404
30290
  const cfg = readConfig();
28405
30291
  const { apiKey } = getConfig(rootOpts);
28406
30292
  if (!apiKey) {
30293
+ spinner.stop();
28407
30294
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28408
30295
  process.exitCode = 1;
28409
30296
  return;
@@ -28417,41 +30304,44 @@ issues.command("post-comment <issueId> <content>").description("post a new comme
28417
30304
  parentCommentId: opts.parent,
28418
30305
  debug: !!opts.debug
28419
30306
  });
30307
+ spinner.stop();
28420
30308
  printResult(result, opts.json);
28421
30309
  } catch (err) {
30310
+ spinner.stop();
28422
30311
  const message = err instanceof Error ? err.message : String(err);
28423
30312
  console.error(message);
28424
30313
  process.exitCode = 1;
28425
30314
  }
28426
30315
  });
28427
- issues.command("create <title>").description("create a new issue").option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10)).option("--project-id <id>", "project id", (v) => parseInt(v, 10)).option("--description <text>", "issue description (supports \\\\n)").option("--label <label>", "issue label (repeatable)", (value, previous) => {
30316
+ issues.command("create <title>").description("create a new issue").option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10)).option("--project-id <id>", "project id", (v) => parseInt(v, 10)).option("--description <text>", "issue description (use \\n for newlines)").option("--label <label>", "issue label (repeatable)", (value, previous) => {
28428
30317
  previous.push(value);
28429
30318
  return previous;
28430
30319
  }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
30320
+ const rootOpts = program2.opts();
30321
+ const cfg = readConfig();
30322
+ const { apiKey } = getConfig(rootOpts);
30323
+ if (!apiKey) {
30324
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30325
+ process.exitCode = 1;
30326
+ return;
30327
+ }
30328
+ const title = interpretEscapes2(String(rawTitle || "").trim());
30329
+ if (!title) {
30330
+ console.error("title is required");
30331
+ process.exitCode = 1;
30332
+ return;
30333
+ }
30334
+ const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
30335
+ if (typeof orgId !== "number") {
30336
+ console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
30337
+ process.exitCode = 1;
30338
+ return;
30339
+ }
30340
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30341
+ const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
30342
+ const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
30343
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
28431
30344
  try {
28432
- const rootOpts = program2.opts();
28433
- const cfg = readConfig();
28434
- const { apiKey } = getConfig(rootOpts);
28435
- if (!apiKey) {
28436
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28437
- process.exitCode = 1;
28438
- return;
28439
- }
28440
- const title = interpretEscapes2(String(rawTitle || "").trim());
28441
- if (!title) {
28442
- console.error("title is required");
28443
- process.exitCode = 1;
28444
- return;
28445
- }
28446
- const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
28447
- if (typeof orgId !== "number") {
28448
- console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
28449
- process.exitCode = 1;
28450
- return;
28451
- }
28452
- const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
28453
- const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
28454
- const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
28455
30345
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28456
30346
  const result = await createIssue2({
28457
30347
  apiKey,
@@ -28463,57 +30353,60 @@ issues.command("create <title>").description("create a new issue").option("--org
28463
30353
  labels,
28464
30354
  debug: !!opts.debug
28465
30355
  });
30356
+ spinner.stop();
28466
30357
  printResult(result, opts.json);
28467
30358
  } catch (err) {
30359
+ spinner.stop();
28468
30360
  const message = err instanceof Error ? err.message : String(err);
28469
30361
  console.error(message);
28470
30362
  process.exitCode = 1;
28471
30363
  }
28472
30364
  });
28473
- issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (supports \\\\n)").option("--description <text>", "new description (supports \\\\n)").option("--status <value>", "status: open|closed|0|1").option("--label <label>", "set labels (repeatable). If provided, replaces existing labels.", (value, previous) => {
30365
+ issues.command("update <issueId>").description("update an existing issue (title/description/status/labels)").option("--title <text>", "new title (use \\n for newlines)").option("--description <text>", "new description (use \\n for newlines)").option("--status <value>", "status: open|closed|0|1").option("--label <label>", "set labels (repeatable). If provided, replaces existing labels.", (value, previous) => {
28474
30366
  previous.push(value);
28475
30367
  return previous;
28476
30368
  }, []).option("--clear-labels", "set labels to an empty list").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
28477
- try {
28478
- const rootOpts = program2.opts();
28479
- const cfg = readConfig();
28480
- const { apiKey } = getConfig(rootOpts);
28481
- if (!apiKey) {
28482
- console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28483
- process.exitCode = 1;
28484
- return;
28485
- }
28486
- const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28487
- const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
28488
- const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
28489
- let status = undefined;
28490
- if (opts.status !== undefined) {
28491
- const raw = String(opts.status).trim().toLowerCase();
28492
- if (raw === "open")
28493
- status = 0;
28494
- else if (raw === "closed")
28495
- status = 1;
28496
- else {
28497
- const n = Number(raw);
28498
- if (!Number.isFinite(n)) {
28499
- console.error("status must be open|closed|0|1");
28500
- process.exitCode = 1;
28501
- return;
28502
- }
28503
- status = n;
28504
- }
28505
- if (status !== 0 && status !== 1) {
28506
- console.error("status must be 0 (open) or 1 (closed)");
30369
+ const rootOpts = program2.opts();
30370
+ const cfg = readConfig();
30371
+ const { apiKey } = getConfig(rootOpts);
30372
+ if (!apiKey) {
30373
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30374
+ process.exitCode = 1;
30375
+ return;
30376
+ }
30377
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30378
+ const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
30379
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30380
+ let status = undefined;
30381
+ if (opts.status !== undefined) {
30382
+ const raw = String(opts.status).trim().toLowerCase();
30383
+ if (raw === "open")
30384
+ status = 0;
30385
+ else if (raw === "closed")
30386
+ status = 1;
30387
+ else {
30388
+ const n = Number(raw);
30389
+ if (!Number.isFinite(n)) {
30390
+ console.error("status must be open|closed|0|1");
28507
30391
  process.exitCode = 1;
28508
30392
  return;
28509
30393
  }
30394
+ status = n;
28510
30395
  }
28511
- let labels = undefined;
28512
- if (opts.clearLabels) {
28513
- labels = [];
28514
- } else if (Array.isArray(opts.label) && opts.label.length > 0) {
28515
- labels = opts.label.map(String);
30396
+ if (status !== 0 && status !== 1) {
30397
+ console.error("status must be 0 (open) or 1 (closed)");
30398
+ process.exitCode = 1;
30399
+ return;
28516
30400
  }
30401
+ }
30402
+ let labels = undefined;
30403
+ if (opts.clearLabels) {
30404
+ labels = [];
30405
+ } else if (Array.isArray(opts.label) && opts.label.length > 0) {
30406
+ labels = opts.label.map(String);
30407
+ }
30408
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
30409
+ try {
28517
30410
  const result = await updateIssue2({
28518
30411
  apiKey,
28519
30412
  apiBaseUrl,
@@ -28524,40 +30417,217 @@ issues.command("update <issueId>").description("update an existing issue (title/
28524
30417
  labels,
28525
30418
  debug: !!opts.debug
28526
30419
  });
30420
+ spinner.stop();
28527
30421
  printResult(result, opts.json);
28528
30422
  } catch (err) {
30423
+ spinner.stop();
28529
30424
  const message = err instanceof Error ? err.message : String(err);
28530
30425
  console.error(message);
28531
30426
  process.exitCode = 1;
28532
30427
  }
28533
30428
  });
28534
30429
  issues.command("update-comment <commentId> <content>").description("update an existing issue comment").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commentId, content, opts) => {
30430
+ if (opts.debug) {
30431
+ console.log(`Debug: Original content: ${JSON.stringify(content)}`);
30432
+ }
30433
+ content = interpretEscapes2(content);
30434
+ if (opts.debug) {
30435
+ console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
30436
+ }
30437
+ const rootOpts = program2.opts();
30438
+ const cfg = readConfig();
30439
+ const { apiKey } = getConfig(rootOpts);
30440
+ if (!apiKey) {
30441
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30442
+ process.exitCode = 1;
30443
+ return;
30444
+ }
30445
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating comment...");
28535
30446
  try {
28536
- if (opts.debug) {
28537
- console.log(`Debug: Original content: ${JSON.stringify(content)}`);
28538
- }
28539
- content = interpretEscapes2(content);
28540
- if (opts.debug) {
28541
- console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
30447
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30448
+ const result = await updateIssueComment2({
30449
+ apiKey,
30450
+ apiBaseUrl,
30451
+ commentId,
30452
+ content,
30453
+ debug: !!opts.debug
30454
+ });
30455
+ spinner.stop();
30456
+ printResult(result, opts.json);
30457
+ } catch (err) {
30458
+ spinner.stop();
30459
+ const message = err instanceof Error ? err.message : String(err);
30460
+ console.error(message);
30461
+ process.exitCode = 1;
30462
+ }
30463
+ });
30464
+ issues.command("action-items <issueId>").description("list action items for an issue").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
30465
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action items...");
30466
+ try {
30467
+ const rootOpts = program2.opts();
30468
+ const cfg = readConfig();
30469
+ const { apiKey } = getConfig(rootOpts);
30470
+ if (!apiKey) {
30471
+ spinner.stop();
30472
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30473
+ process.exitCode = 1;
30474
+ return;
28542
30475
  }
30476
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30477
+ const result = await fetchActionItems2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
30478
+ spinner.stop();
30479
+ printResult(result, opts.json);
30480
+ } catch (err) {
30481
+ spinner.stop();
30482
+ const message = err instanceof Error ? err.message : String(err);
30483
+ console.error(message);
30484
+ process.exitCode = 1;
30485
+ }
30486
+ });
30487
+ issues.command("view-action-item <actionItemIds...>").description("view action item(s) with all details (supports multiple IDs)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemIds, opts) => {
30488
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action item(s)...");
30489
+ try {
28543
30490
  const rootOpts = program2.opts();
28544
30491
  const cfg = readConfig();
28545
30492
  const { apiKey } = getConfig(rootOpts);
28546
30493
  if (!apiKey) {
30494
+ spinner.stop();
28547
30495
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28548
30496
  process.exitCode = 1;
28549
30497
  return;
28550
30498
  }
28551
30499
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28552
- const result = await updateIssueComment2({
30500
+ const result = await fetchActionItem2({ apiKey, apiBaseUrl, actionItemIds, debug: !!opts.debug });
30501
+ if (result.length === 0) {
30502
+ spinner.stop();
30503
+ console.error("Action item(s) not found");
30504
+ process.exitCode = 1;
30505
+ return;
30506
+ }
30507
+ spinner.stop();
30508
+ printResult(result, opts.json);
30509
+ } catch (err) {
30510
+ spinner.stop();
30511
+ const message = err instanceof Error ? err.message : String(err);
30512
+ console.error(message);
30513
+ process.exitCode = 1;
30514
+ }
30515
+ });
30516
+ issues.command("create-action-item <issueId> <title>").description("create a new action item for an issue").option("--description <text>", "detailed description (use \\n for newlines)").option("--sql-action <sql>", "SQL command to execute").option("--config <json>", 'config change as JSON: {"parameter":"...","value":"..."} (repeatable)', (value, previous) => {
30517
+ try {
30518
+ previous.push(JSON.parse(value));
30519
+ } catch {
30520
+ console.error(`Invalid JSON for --config: ${value}`);
30521
+ process.exit(1);
30522
+ }
30523
+ return previous;
30524
+ }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, rawTitle, opts) => {
30525
+ const rootOpts = program2.opts();
30526
+ const cfg = readConfig();
30527
+ const { apiKey } = getConfig(rootOpts);
30528
+ if (!apiKey) {
30529
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30530
+ process.exitCode = 1;
30531
+ return;
30532
+ }
30533
+ const title = interpretEscapes2(String(rawTitle || "").trim());
30534
+ if (!title) {
30535
+ console.error("title is required");
30536
+ process.exitCode = 1;
30537
+ return;
30538
+ }
30539
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30540
+ const sqlAction = opts.sqlAction;
30541
+ const configs = Array.isArray(opts.config) && opts.config.length > 0 ? opts.config : undefined;
30542
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating action item...");
30543
+ try {
30544
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30545
+ const result = await createActionItem2({
28553
30546
  apiKey,
28554
30547
  apiBaseUrl,
28555
- commentId,
28556
- content,
30548
+ issueId,
30549
+ title,
30550
+ description,
30551
+ sqlAction,
30552
+ configs,
28557
30553
  debug: !!opts.debug
28558
30554
  });
28559
- printResult(result, opts.json);
30555
+ spinner.stop();
30556
+ printResult({ id: result }, opts.json);
30557
+ } catch (err) {
30558
+ spinner.stop();
30559
+ const message = err instanceof Error ? err.message : String(err);
30560
+ console.error(message);
30561
+ process.exitCode = 1;
30562
+ }
30563
+ });
30564
+ issues.command("update-action-item <actionItemId>").description("update an action item (title, description, status, sql_action, configs)").option("--title <text>", "new title (use \\n for newlines)").option("--description <text>", "new description (use \\n for newlines)").option("--done", "mark as done").option("--not-done", "mark as not done").option("--status <value>", "status: waiting_for_approval|approved|rejected").option("--status-reason <text>", "reason for status change").option("--sql-action <sql>", "SQL command (use empty string to clear)").option("--config <json>", "config change as JSON (repeatable, replaces all configs)", (value, previous) => {
30565
+ try {
30566
+ previous.push(JSON.parse(value));
30567
+ } catch {
30568
+ console.error(`Invalid JSON for --config: ${value}`);
30569
+ process.exit(1);
30570
+ }
30571
+ return previous;
30572
+ }, []).option("--clear-configs", "clear all config changes").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemId, opts) => {
30573
+ const rootOpts = program2.opts();
30574
+ const cfg = readConfig();
30575
+ const { apiKey } = getConfig(rootOpts);
30576
+ if (!apiKey) {
30577
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30578
+ process.exitCode = 1;
30579
+ return;
30580
+ }
30581
+ const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
30582
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30583
+ let isDone = undefined;
30584
+ if (opts.done)
30585
+ isDone = true;
30586
+ else if (opts.notDone)
30587
+ isDone = false;
30588
+ let status = undefined;
30589
+ if (opts.status !== undefined) {
30590
+ const validStatuses = ["waiting_for_approval", "approved", "rejected"];
30591
+ if (!validStatuses.includes(opts.status)) {
30592
+ console.error(`status must be one of: ${validStatuses.join(", ")}`);
30593
+ process.exitCode = 1;
30594
+ return;
30595
+ }
30596
+ status = opts.status;
30597
+ }
30598
+ const statusReason = opts.statusReason;
30599
+ const sqlAction = opts.sqlAction;
30600
+ let configs = undefined;
30601
+ if (opts.clearConfigs) {
30602
+ configs = [];
30603
+ } else if (Array.isArray(opts.config) && opts.config.length > 0) {
30604
+ configs = opts.config;
30605
+ }
30606
+ if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined && sqlAction === undefined && configs === undefined) {
30607
+ console.error("At least one update option is required");
30608
+ process.exitCode = 1;
30609
+ return;
30610
+ }
30611
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating action item...");
30612
+ try {
30613
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30614
+ await updateActionItem2({
30615
+ apiKey,
30616
+ apiBaseUrl,
30617
+ actionItemId,
30618
+ title,
30619
+ description,
30620
+ isDone,
30621
+ status,
30622
+ statusReason,
30623
+ sqlAction,
30624
+ configs,
30625
+ debug: !!opts.debug
30626
+ });
30627
+ spinner.stop();
30628
+ printResult({ success: true }, opts.json);
28560
30629
  } catch (err) {
30630
+ spinner.stop();
28561
30631
  const message = err instanceof Error ? err.message : String(err);
28562
30632
  console.error(message);
28563
30633
  process.exitCode = 1;