postgresai 0.14.0-beta.12 → 0.14.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.13",
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.13";
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,6 +24340,243 @@ async function updateIssueComment2(params) {
23910
24340
  throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
23911
24341
  }
23912
24342
  }
24343
+ async function fetchActionItem2(params) {
24344
+ const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
24345
+ if (!apiKey) {
24346
+ throw new Error("API key is required");
24347
+ }
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");
24353
+ }
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(",")})`);
24360
+ }
24361
+ const headers = {
24362
+ "access-token": apiKey,
24363
+ Prefer: "return=representation",
24364
+ "Content-Type": "application/json",
24365
+ Connection: "close"
24366
+ };
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)}`);
24373
+ }
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) {
24384
+ try {
24385
+ const parsed = JSON.parse(data);
24386
+ if (Array.isArray(parsed)) {
24387
+ return parsed;
24388
+ }
24389
+ return parsed ? [parsed] : [];
24390
+ } catch {
24391
+ throw new Error(`Failed to parse action item response: ${data}`);
24392
+ }
24393
+ } else {
24394
+ throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
24395
+ }
24396
+ }
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
+ }
23913
24580
 
23914
24581
  // lib/util.ts
23915
24582
  function maskSecret2(secret) {
@@ -23947,6 +24614,15 @@ import { URL as URL2, fileURLToPath } from "url";
23947
24614
  import * as fs3 from "fs";
23948
24615
  import * as path3 from "path";
23949
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
+ }
23950
24626
  function sslModeToConfig(mode) {
23951
24627
  if (mode.toLowerCase() === "disable")
23952
24628
  return false;
@@ -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,11 +24958,23 @@ 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
+ let permissionsSql = applyTemplate(loadSqlTemplate("02.permissions.sql"), vars);
24965
+ if (SKIP_ALTER_USER_PROVIDERS.includes(provider)) {
24966
+ permissionsSql = permissionsSql.split(`
24967
+ `).filter((line) => {
24968
+ const trimmed = line.trim();
24969
+ if (trimmed.startsWith("--") || trimmed === "")
24970
+ return true;
24971
+ return !/^\s*alter\s+user\s+/i.test(line);
24972
+ }).join(`
24973
+ `);
24974
+ }
24285
24975
  steps.push({
24286
24976
  name: "02.permissions",
24287
- sql: applyTemplate(loadSqlTemplate("02.permissions.sql"), vars)
24977
+ sql: permissionsSql
24288
24978
  });
24289
24979
  steps.push({
24290
24980
  name: "05.helpers",
@@ -24301,25 +24991,413 @@ end $$;`;
24301
24991
  optional: true
24302
24992
  });
24303
24993
  }
24304
- return { monitoringUser, database, steps };
24994
+ return { monitoringUser, database, steps };
24995
+ }
24996
+ async function applyInitPlan(params) {
24997
+ const applied = [];
24998
+ const skippedOptional = [];
24999
+ const executeStep = async (step) => {
25000
+ await params.client.query("begin;");
25001
+ try {
25002
+ await params.client.query(step.sql, step.params);
25003
+ await params.client.query("commit;");
25004
+ } catch (e) {
25005
+ try {
25006
+ await params.client.query("rollback;");
25007
+ } catch {}
25008
+ throw e;
25009
+ }
25010
+ };
25011
+ for (const step of params.plan.steps.filter((s) => !s.optional)) {
25012
+ try {
25013
+ await executeStep(step);
25014
+ applied.push(step.name);
25015
+ } catch (e) {
25016
+ const msg = e instanceof Error ? e.message : String(e);
25017
+ const errAny = e;
25018
+ const wrapped = new Error(`Failed at step "${step.name}": ${msg}`);
25019
+ const pgErrorFields = [
25020
+ "code",
25021
+ "detail",
25022
+ "hint",
25023
+ "position",
25024
+ "internalPosition",
25025
+ "internalQuery",
25026
+ "where",
25027
+ "schema",
25028
+ "table",
25029
+ "column",
25030
+ "dataType",
25031
+ "constraint",
25032
+ "file",
25033
+ "line",
25034
+ "routine"
25035
+ ];
25036
+ if (errAny && typeof errAny === "object") {
25037
+ for (const field of pgErrorFields) {
25038
+ if (errAny[field] !== undefined)
25039
+ wrapped[field] = errAny[field];
25040
+ }
25041
+ }
25042
+ if (e instanceof Error && e.stack) {
25043
+ wrapped.stack = e.stack;
25044
+ }
25045
+ throw wrapped;
25046
+ }
25047
+ }
25048
+ for (const step of params.plan.steps.filter((s) => s.optional)) {
25049
+ try {
25050
+ await executeStep(step);
25051
+ applied.push(step.name);
25052
+ } catch {
25053
+ skippedOptional.push(step.name);
25054
+ }
25055
+ }
25056
+ return { applied, skippedOptional };
25057
+ }
25058
+ async function verifyInitSetup(params) {
25059
+ await params.client.query("begin isolation level repeatable read;");
25060
+ try {
25061
+ const missingRequired = [];
25062
+ const missingOptional = [];
25063
+ const role = params.monitoringUser;
25064
+ const db = params.database;
25065
+ const provider = params.provider ?? "self-managed";
25066
+ const roleRes = await params.client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [role]);
25067
+ const roleExists = (roleRes.rowCount ?? 0) > 0;
25068
+ if (!roleExists) {
25069
+ missingRequired.push(`role "${role}" does not exist`);
25070
+ return { ok: false, missingRequired, missingOptional };
25071
+ }
25072
+ const connectRes = await params.client.query("select has_database_privilege($1, $2, 'CONNECT') as ok", [role, db]);
25073
+ if (!connectRes.rows?.[0]?.ok) {
25074
+ missingRequired.push(`CONNECT on database "${db}"`);
25075
+ }
25076
+ const pgMonitorRes = await params.client.query("select pg_has_role($1, 'pg_monitor', 'member') as ok", [role]);
25077
+ if (!pgMonitorRes.rows?.[0]?.ok) {
25078
+ missingRequired.push("membership in role pg_monitor");
25079
+ }
25080
+ const pgIndexRes = await params.client.query("select has_table_privilege($1, 'pg_catalog.pg_index', 'SELECT') as ok", [role]);
25081
+ if (!pgIndexRes.rows?.[0]?.ok) {
25082
+ missingRequired.push("SELECT on pg_catalog.pg_index");
25083
+ }
25084
+ const schemaExistsRes = await params.client.query("select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok", [role]);
25085
+ if (!schemaExistsRes.rows?.[0]?.ok) {
25086
+ missingRequired.push("USAGE on schema postgres_ai");
25087
+ }
25088
+ const viewExistsRes = await params.client.query("select to_regclass('postgres_ai.pg_statistic') is not null as ok");
25089
+ if (!viewExistsRes.rows?.[0]?.ok) {
25090
+ missingRequired.push("view postgres_ai.pg_statistic exists");
25091
+ } else {
25092
+ const viewPrivRes = await params.client.query("select has_table_privilege($1, 'postgres_ai.pg_statistic', 'SELECT') as ok", [role]);
25093
+ if (!viewPrivRes.rows?.[0]?.ok) {
25094
+ missingRequired.push("SELECT on view postgres_ai.pg_statistic");
25095
+ }
25096
+ }
25097
+ const schemaUsageRes = await params.client.query("select has_schema_privilege($1, 'public', 'USAGE') as ok", [role]);
25098
+ if (!schemaUsageRes.rows?.[0]?.ok) {
25099
+ missingRequired.push("USAGE on schema public");
25100
+ }
25101
+ if (!SKIP_SEARCH_PATH_CHECK_PROVIDERS.includes(provider)) {
25102
+ const rolcfgRes = await params.client.query("select rolconfig from pg_catalog.pg_roles where rolname = $1", [role]);
25103
+ const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
25104
+ const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
25105
+ if (typeof spLine !== "string" || !spLine) {
25106
+ missingRequired.push("role search_path is set");
25107
+ } else {
25108
+ const sp = spLine.toLowerCase();
25109
+ if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
25110
+ missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
25111
+ }
25112
+ }
25113
+ }
25114
+ const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
25115
+ if (!explainFnRes.rows?.[0]?.ok) {
25116
+ missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
25117
+ }
25118
+ const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
25119
+ if (!tableDescribeFnRes.rows?.[0]?.ok) {
25120
+ missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
25121
+ }
25122
+ if (params.includeOptionalPermissions) {
25123
+ {
25124
+ const extRes = await params.client.query("select 1 from pg_extension where extname = 'rds_tools'");
25125
+ if ((extRes.rowCount ?? 0) === 0) {
25126
+ missingOptional.push("extension rds_tools");
25127
+ } else {
25128
+ const fnRes = await params.client.query("select has_function_privilege($1, 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok", [role]);
25129
+ if (!fnRes.rows?.[0]?.ok) {
25130
+ missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
25131
+ }
25132
+ }
25133
+ }
25134
+ const optionalFns = [
25135
+ "pg_catalog.pg_stat_file(text)",
25136
+ "pg_catalog.pg_stat_file(text, boolean)",
25137
+ "pg_catalog.pg_ls_dir(text)",
25138
+ "pg_catalog.pg_ls_dir(text, boolean, boolean)"
25139
+ ];
25140
+ for (const fn of optionalFns) {
25141
+ const fnRes = await params.client.query("select has_function_privilege($1, $2, 'EXECUTE') as ok", [role, fn]);
25142
+ if (!fnRes.rows?.[0]?.ok) {
25143
+ missingOptional.push(`EXECUTE on ${fn}`);
25144
+ }
25145
+ }
25146
+ }
25147
+ return { ok: missingRequired.length === 0, missingRequired, missingOptional };
25148
+ } finally {
25149
+ try {
25150
+ await params.client.query("rollback;");
25151
+ } catch {}
25152
+ }
25153
+ }
25154
+
25155
+ // lib/supabase.ts
25156
+ var SUPABASE_API_BASE = "https://api.supabase.com";
25157
+ function isValidProjectRef(ref) {
25158
+ return /^[a-z0-9]{10,30}$/i.test(ref);
25159
+ }
25160
+
25161
+ class SupabaseClient {
25162
+ config;
25163
+ constructor(config2) {
25164
+ if (!config2.projectRef) {
25165
+ throw new Error("Supabase project reference is required");
25166
+ }
25167
+ if (!config2.accessToken) {
25168
+ throw new Error("Supabase access token is required");
25169
+ }
25170
+ if (!isValidProjectRef(config2.projectRef)) {
25171
+ throw new Error(`Invalid Supabase project reference format: "${config2.projectRef}". Expected 10-30 alphanumeric characters.`);
25172
+ }
25173
+ this.config = config2;
25174
+ }
25175
+ async query(sql, readOnly = false) {
25176
+ const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(this.config.projectRef)}/database/query`;
25177
+ const response = await fetch(url, {
25178
+ method: "POST",
25179
+ headers: {
25180
+ "Content-Type": "application/json",
25181
+ Authorization: `Bearer ${this.config.accessToken}`
25182
+ },
25183
+ body: JSON.stringify({
25184
+ query: sql,
25185
+ read_only: readOnly
25186
+ })
25187
+ });
25188
+ const body = await response.text();
25189
+ let data;
25190
+ try {
25191
+ data = JSON.parse(body);
25192
+ } catch {
25193
+ throw this.createPgError({
25194
+ message: `Supabase API returned non-JSON response: ${body.slice(0, 200)}`,
25195
+ httpStatus: response.status
25196
+ });
25197
+ }
25198
+ if (!response.ok) {
25199
+ throw this.parseApiError(data, response.status);
25200
+ }
25201
+ if (data && typeof data === "object" && "error" in data && data.error) {
25202
+ throw this.parseApiError(data, response.status);
25203
+ }
25204
+ const rows = Array.isArray(data) ? data : [];
25205
+ return {
25206
+ rows,
25207
+ rowCount: rows.length
25208
+ };
25209
+ }
25210
+ async testConnection() {
25211
+ const result = await this.query("SELECT current_database() as db, version() as version", true);
25212
+ const row = result.rows[0] ?? {};
25213
+ return {
25214
+ database: String(row.db ?? ""),
25215
+ version: String(row.version ?? "")
25216
+ };
25217
+ }
25218
+ async getCurrentDatabase() {
25219
+ const result = await this.query("SELECT current_database() as db", true);
25220
+ const row = result.rows[0] ?? {};
25221
+ return String(row.db ?? "");
25222
+ }
25223
+ parseApiError(data, httpStatus) {
25224
+ if (data && typeof data === "object" && !Array.isArray(data)) {
25225
+ const errObj = "error" in data && data.error ? data.error : data;
25226
+ const pgCode = this.extractPgErrorCode(errObj);
25227
+ const message = this.extractErrorMessage(errObj);
25228
+ const detail = this.extractField(errObj, ["details", "detail"]);
25229
+ const hint = this.extractField(errObj, ["hint"]);
25230
+ return this.createPgError({
25231
+ message,
25232
+ code: pgCode,
25233
+ detail,
25234
+ hint,
25235
+ httpStatus,
25236
+ supabaseErrorCode: typeof errObj === "object" && errObj && "code" in errObj ? String(errObj.code ?? "") : undefined
25237
+ });
25238
+ }
25239
+ return this.createPgError({
25240
+ message: `Supabase API error (HTTP ${httpStatus})`,
25241
+ httpStatus
25242
+ });
25243
+ }
25244
+ extractPgErrorCode(errObj) {
25245
+ if (!errObj || typeof errObj !== "object")
25246
+ return;
25247
+ const obj = errObj;
25248
+ if (typeof obj.code === "string") {
25249
+ const code = obj.code;
25250
+ if (/^\d{5}$/.test(code)) {
25251
+ return code;
25252
+ }
25253
+ return this.mapSupabaseCodeToPg(code);
25254
+ }
25255
+ return;
25256
+ }
25257
+ mapSupabaseCodeToPg(code) {
25258
+ const mapping = {
25259
+ PGRST301: "28000",
25260
+ PGRST302: "28P01",
25261
+ "42501": "42501",
25262
+ PGRST000: "42501",
25263
+ "42601": "42601",
25264
+ "42P01": "42P01",
25265
+ PGRST200: "42P01",
25266
+ "42883": "42883",
25267
+ "08000": "08000",
25268
+ "08003": "08003",
25269
+ "08006": "08006",
25270
+ "42710": "42710"
25271
+ };
25272
+ return mapping[code];
25273
+ }
25274
+ extractErrorMessage(errObj) {
25275
+ if (!errObj || typeof errObj !== "object") {
25276
+ return "Unknown Supabase API error";
25277
+ }
25278
+ const obj = errObj;
25279
+ for (const field of ["message", "error", "msg", "description"]) {
25280
+ if (typeof obj[field] === "string" && obj[field]) {
25281
+ return obj[field];
25282
+ }
25283
+ }
25284
+ if (obj.error && typeof obj.error === "object") {
25285
+ return this.extractErrorMessage(obj.error);
25286
+ }
25287
+ return "Unknown Supabase API error";
25288
+ }
25289
+ extractField(errObj, fieldNames) {
25290
+ if (!errObj || typeof errObj !== "object")
25291
+ return;
25292
+ const obj = errObj;
25293
+ for (const field of fieldNames) {
25294
+ if (typeof obj[field] === "string" && obj[field]) {
25295
+ return obj[field];
25296
+ }
25297
+ }
25298
+ return;
25299
+ }
25300
+ createPgError(opts) {
25301
+ const err = new Error(opts.message);
25302
+ if (opts.code)
25303
+ err.code = opts.code;
25304
+ if (opts.detail)
25305
+ err.detail = opts.detail;
25306
+ if (opts.hint)
25307
+ err.hint = opts.hint;
25308
+ if (opts.httpStatus)
25309
+ err.httpStatus = opts.httpStatus;
25310
+ if (opts.supabaseErrorCode)
25311
+ err.supabaseErrorCode = opts.supabaseErrorCode;
25312
+ return err;
25313
+ }
25314
+ }
25315
+ async function fetchPoolerDatabaseUrl(config2, username) {
25316
+ const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config2.projectRef)}/config/database/pooler`;
25317
+ try {
25318
+ const response = await fetch(url, {
25319
+ method: "GET",
25320
+ headers: {
25321
+ Authorization: `Bearer ${config2.accessToken}`
25322
+ }
25323
+ });
25324
+ if (!response.ok) {
25325
+ return null;
25326
+ }
25327
+ const data = await response.json();
25328
+ if (Array.isArray(data) && data.length > 0) {
25329
+ const pooler = data[0];
25330
+ if (pooler.db_host && pooler.db_port && pooler.db_name) {
25331
+ return `postgresql://${username}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
25332
+ }
25333
+ if (typeof pooler.connection_string === "string") {
25334
+ try {
25335
+ const connUrl = new URL(pooler.connection_string);
25336
+ const portPart = connUrl.port ? `:${connUrl.port}` : "";
25337
+ return `postgresql://${username}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
25338
+ } catch {
25339
+ return null;
25340
+ }
25341
+ }
25342
+ }
25343
+ return null;
25344
+ } catch {
25345
+ return null;
25346
+ }
24305
25347
  }
24306
- async function applyInitPlan(params) {
25348
+ function resolveSupabaseConfig(opts) {
25349
+ const accessToken = opts.accessToken?.trim() || process.env.SUPABASE_ACCESS_TOKEN?.trim() || "";
25350
+ const projectRef = opts.projectRef?.trim() || process.env.SUPABASE_PROJECT_REF?.trim() || "";
25351
+ if (!accessToken) {
25352
+ throw new Error(`Supabase access token is required.
25353
+ ` + `Provide it via --supabase-access-token or SUPABASE_ACCESS_TOKEN environment variable.
25354
+ ` + "Generate a token at: https://supabase.com/dashboard/account/tokens");
25355
+ }
25356
+ if (!projectRef) {
25357
+ throw new Error(`Supabase project reference is required.
25358
+ ` + `Provide it via --supabase-project-ref or SUPABASE_PROJECT_REF environment variable.
25359
+ ` + "Find your project ref in the Supabase dashboard URL: https://supabase.com/dashboard/project/<ref>");
25360
+ }
25361
+ return { accessToken, projectRef };
25362
+ }
25363
+ function extractProjectRefFromUrl(dbUrl) {
25364
+ try {
25365
+ const url = new URL(dbUrl);
25366
+ const host = url.hostname;
25367
+ const match = host.match(/^(?:db\.)?([^.]+)\.supabase\.co$/i);
25368
+ if (match && match[1]) {
25369
+ return match[1];
25370
+ }
25371
+ if (host.includes("pooler.supabase.com")) {
25372
+ const username = url.username;
25373
+ const userMatch = username.match(/^postgres\.([a-z0-9]+)$/i);
25374
+ if (userMatch && userMatch[1]) {
25375
+ return userMatch[1];
25376
+ }
25377
+ }
25378
+ const poolerMatch = host.match(/^([a-z0-9]+)\.pooler\.supabase\.com$/i);
25379
+ if (poolerMatch && poolerMatch[1] && !poolerMatch[1].startsWith("aws-")) {
25380
+ return poolerMatch[1];
25381
+ }
25382
+ return;
25383
+ } catch {
25384
+ return;
25385
+ }
25386
+ }
25387
+ async function applyInitPlanViaSupabase(params) {
24307
25388
  const applied = [];
24308
25389
  const skippedOptional = [];
24309
25390
  const executeStep = async (step) => {
24310
- await params.client.query("begin;");
24311
- try {
24312
- await params.client.query(step.sql, step.params);
24313
- await params.client.query("commit;");
24314
- } catch (e) {
24315
- try {
24316
- await params.client.query("rollback;");
24317
- } catch {}
24318
- throw e;
24319
- }
25391
+ const wrappedSql = `BEGIN;
25392
+ ${step.sql}
25393
+ COMMIT;`;
25394
+ await params.client.query(wrappedSql, false);
24320
25395
  };
24321
25396
  for (const step of params.plan.steps.filter((s) => !s.optional)) {
24322
25397
  try {
25398
+ if (params.verbose) {
25399
+ console.log(`Executing step: ${step.name}`);
25400
+ }
24323
25401
  await executeStep(step);
24324
25402
  applied.push(step.name);
24325
25403
  } catch (e) {
@@ -24341,12 +25419,13 @@ async function applyInitPlan(params) {
24341
25419
  "constraint",
24342
25420
  "file",
24343
25421
  "line",
24344
- "routine"
25422
+ "routine",
25423
+ "httpStatus",
25424
+ "supabaseErrorCode"
24345
25425
  ];
24346
- if (errAny && typeof errAny === "object") {
24347
- for (const field of pgErrorFields) {
24348
- if (errAny[field] !== undefined)
24349
- wrapped[field] = errAny[field];
25426
+ for (const field of pgErrorFields) {
25427
+ if (errAny[field] !== undefined) {
25428
+ wrapped[field] = errAny[field];
24350
25429
  }
24351
25430
  }
24352
25431
  if (e instanceof Error && e.stack) {
@@ -24357,6 +25436,9 @@ async function applyInitPlan(params) {
24357
25436
  }
24358
25437
  for (const step of params.plan.steps.filter((s) => s.optional)) {
24359
25438
  try {
25439
+ if (params.verbose) {
25440
+ console.log(`Executing optional step: ${step.name}`);
25441
+ }
24360
25442
  await executeStep(step);
24361
25443
  applied.push(step.name);
24362
25444
  } catch {
@@ -24365,98 +25447,133 @@ async function applyInitPlan(params) {
24365
25447
  }
24366
25448
  return { applied, skippedOptional };
24367
25449
  }
24368
- async function verifyInitSetup(params) {
24369
- await params.client.query("begin isolation level repeatable read;");
24370
- try {
24371
- const missingRequired = [];
24372
- const missingOptional = [];
24373
- const role = params.monitoringUser;
24374
- const db = params.database;
24375
- const roleRes = await params.client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [role]);
24376
- const roleExists = (roleRes.rowCount ?? 0) > 0;
24377
- if (!roleExists) {
24378
- missingRequired.push(`role "${role}" does not exist`);
24379
- return { ok: false, missingRequired, missingOptional };
24380
- }
24381
- const connectRes = await params.client.query("select has_database_privilege($1, $2, 'CONNECT') as ok", [role, db]);
24382
- if (!connectRes.rows?.[0]?.ok) {
24383
- missingRequired.push(`CONNECT on database "${db}"`);
24384
- }
24385
- const pgMonitorRes = await params.client.query("select pg_has_role($1, 'pg_monitor', 'member') as ok", [role]);
24386
- if (!pgMonitorRes.rows?.[0]?.ok) {
24387
- missingRequired.push("membership in role pg_monitor");
24388
- }
24389
- const pgIndexRes = await params.client.query("select has_table_privilege($1, 'pg_catalog.pg_index', 'SELECT') as ok", [role]);
24390
- if (!pgIndexRes.rows?.[0]?.ok) {
24391
- missingRequired.push("SELECT on pg_catalog.pg_index");
24392
- }
24393
- const schemaExistsRes = await params.client.query("select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok", [role]);
24394
- if (!schemaExistsRes.rows?.[0]?.ok) {
25450
+ async function verifyInitSetupViaSupabase(params) {
25451
+ const missingRequired = [];
25452
+ const missingOptional = [];
25453
+ const role = params.monitoringUser;
25454
+ const db = params.database;
25455
+ if (!isValidIdentifier(role)) {
25456
+ 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).`);
25457
+ }
25458
+ const roleRes = await params.client.query(`SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
25459
+ const roleExists = roleRes.rowCount > 0;
25460
+ if (!roleExists) {
25461
+ missingRequired.push(`role "${role}" does not exist`);
25462
+ return { ok: false, missingRequired, missingOptional };
25463
+ }
25464
+ const connectRes = await params.client.query(`SELECT has_database_privilege('${escapeLiteral2(role)}', '${escapeLiteral2(db)}', 'CONNECT') as ok`, true);
25465
+ if (!connectRes.rows?.[0]?.ok) {
25466
+ missingRequired.push(`CONNECT on database "${db}"`);
25467
+ }
25468
+ const pgMonitorRes = await params.client.query(`SELECT pg_has_role('${escapeLiteral2(role)}', 'pg_monitor', 'member') as ok`, true);
25469
+ if (!pgMonitorRes.rows?.[0]?.ok) {
25470
+ missingRequired.push("membership in role pg_monitor");
25471
+ }
25472
+ const pgIndexRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'pg_catalog.pg_index', 'SELECT') as ok`, true);
25473
+ if (!pgIndexRes.rows?.[0]?.ok) {
25474
+ missingRequired.push("SELECT on pg_catalog.pg_index");
25475
+ }
25476
+ const schemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'postgres_ai'", true);
25477
+ if (schemaExistsRes.rowCount === 0) {
25478
+ missingRequired.push("schema postgres_ai exists");
25479
+ } else {
25480
+ const schemaPrivRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'postgres_ai', 'USAGE') as ok`, true);
25481
+ if (!schemaPrivRes.rows?.[0]?.ok) {
24395
25482
  missingRequired.push("USAGE on schema postgres_ai");
24396
25483
  }
24397
- const viewExistsRes = await params.client.query("select to_regclass('postgres_ai.pg_statistic') is not null as ok");
24398
- if (!viewExistsRes.rows?.[0]?.ok) {
24399
- missingRequired.push("view postgres_ai.pg_statistic exists");
24400
- } else {
24401
- const viewPrivRes = await params.client.query("select has_table_privilege($1, 'postgres_ai.pg_statistic', 'SELECT') as ok", [role]);
24402
- if (!viewPrivRes.rows?.[0]?.ok) {
24403
- missingRequired.push("SELECT on view postgres_ai.pg_statistic");
24404
- }
25484
+ }
25485
+ const viewExistsRes = await params.client.query("SELECT to_regclass('postgres_ai.pg_statistic') IS NOT NULL as ok", true);
25486
+ if (!viewExistsRes.rows?.[0]?.ok) {
25487
+ missingRequired.push("view postgres_ai.pg_statistic exists");
25488
+ } else {
25489
+ const viewPrivRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'postgres_ai.pg_statistic', 'SELECT') as ok`, true);
25490
+ if (!viewPrivRes.rows?.[0]?.ok) {
25491
+ missingRequired.push("SELECT on view postgres_ai.pg_statistic");
24405
25492
  }
24406
- const schemaUsageRes = await params.client.query("select has_schema_privilege($1, 'public', 'USAGE') as ok", [role]);
25493
+ }
25494
+ const publicSchemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'public'", true);
25495
+ if (publicSchemaExistsRes.rowCount === 0) {
25496
+ missingRequired.push("schema public exists");
25497
+ } else {
25498
+ const schemaUsageRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'public', 'USAGE') as ok`, true);
24407
25499
  if (!schemaUsageRes.rows?.[0]?.ok) {
24408
25500
  missingRequired.push("USAGE on schema public");
24409
25501
  }
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");
24419
- }
25502
+ }
25503
+ const rolcfgRes = await params.client.query(`SELECT rolconfig FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
25504
+ const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
25505
+ const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
25506
+ if (typeof spLine !== "string" || !spLine) {
25507
+ missingRequired.push("role search_path is set");
25508
+ } else {
25509
+ const sp = spLine.toLowerCase();
25510
+ if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
25511
+ missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
24420
25512
  }
24421
- const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
25513
+ }
25514
+ 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);
25515
+ if (explainFnExistsRes.rowCount === 0) {
25516
+ missingRequired.push("function postgres_ai.explain_generic exists");
25517
+ } else {
25518
+ const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
24422
25519
  if (!explainFnRes.rows?.[0]?.ok) {
24423
25520
  missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
24424
25521
  }
24425
- const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
25522
+ }
25523
+ 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);
25524
+ if (tableDescribeFnExistsRes.rowCount === 0) {
25525
+ missingRequired.push("function postgres_ai.table_describe exists");
25526
+ } else {
25527
+ const tableDescribeFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.table_describe(text)', 'EXECUTE') as ok`, true);
24426
25528
  if (!tableDescribeFnRes.rows?.[0]?.ok) {
24427
25529
  missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
24428
25530
  }
24429
- if (params.includeOptionalPermissions) {
24430
- {
24431
- const extRes = await params.client.query("select 1 from pg_extension where extname = 'rds_tools'");
24432
- if ((extRes.rowCount ?? 0) === 0) {
24433
- missingOptional.push("extension rds_tools");
24434
- } else {
24435
- const fnRes = await params.client.query("select has_function_privilege($1, 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok", [role]);
24436
- if (!fnRes.rows?.[0]?.ok) {
24437
- missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
24438
- }
25531
+ }
25532
+ if (params.includeOptionalPermissions) {
25533
+ const extRes = await params.client.query("SELECT 1 FROM pg_extension WHERE extname = 'rds_tools'", true);
25534
+ if (extRes.rowCount === 0) {
25535
+ missingOptional.push("extension rds_tools");
25536
+ } else {
25537
+ try {
25538
+ const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok`, true);
25539
+ if (!fnRes.rows?.[0]?.ok) {
25540
+ missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
24439
25541
  }
25542
+ } catch {
25543
+ missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
24440
25544
  }
24441
- const optionalFns = [
24442
- "pg_catalog.pg_stat_file(text)",
24443
- "pg_catalog.pg_stat_file(text, boolean)",
24444
- "pg_catalog.pg_ls_dir(text)",
24445
- "pg_catalog.pg_ls_dir(text, boolean, boolean)"
24446
- ];
24447
- for (const fn of optionalFns) {
24448
- const fnRes = await params.client.query("select has_function_privilege($1, $2, 'EXECUTE') as ok", [role, fn]);
25545
+ }
25546
+ const optionalFns = [
25547
+ "pg_catalog.pg_stat_file(text)",
25548
+ "pg_catalog.pg_stat_file(text, boolean)",
25549
+ "pg_catalog.pg_ls_dir(text)",
25550
+ "pg_catalog.pg_ls_dir(text, boolean, boolean)"
25551
+ ];
25552
+ for (const fn of optionalFns) {
25553
+ try {
25554
+ const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', '${fn}', 'EXECUTE') as ok`, true);
24449
25555
  if (!fnRes.rows?.[0]?.ok) {
24450
25556
  missingOptional.push(`EXECUTE on ${fn}`);
24451
25557
  }
25558
+ } catch {
25559
+ missingOptional.push(`EXECUTE on ${fn}`);
24452
25560
  }
24453
25561
  }
24454
- return { ok: missingRequired.length === 0, missingRequired, missingOptional };
24455
- } finally {
24456
- try {
24457
- await params.client.query("rollback;");
24458
- } catch {}
24459
25562
  }
25563
+ return {
25564
+ ok: missingRequired.length === 0,
25565
+ missingRequired,
25566
+ missingOptional
25567
+ };
25568
+ }
25569
+ function isValidIdentifier(name) {
25570
+ return /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(name);
25571
+ }
25572
+ function escapeLiteral2(value) {
25573
+ if (value.includes("\x00")) {
25574
+ throw new Error("SQL literal cannot contain null bytes");
25575
+ }
25576
+ return value.replace(/'/g, "''");
24460
25577
  }
24461
25578
 
24462
25579
  // lib/pkce.ts
@@ -24958,17 +26075,17 @@ where
24958
26075
  statement_timeout_seconds: 300
24959
26076
  },
24960
26077
  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.",
26078
+ 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
26079
  sqls: {
24963
26080
  11: `with fk_indexes as ( /* pgwatch_generated */
24964
26081
  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
26082
+ schemaname as schema_name,
26083
+ indexrelid,
26084
+ (indexrelid::regclass)::text as index_name,
26085
+ (relid::regclass)::text as table_name,
26086
+ (confrelid::regclass)::text as fk_table_ref,
26087
+ array_to_string(indclass, ', ') as opclasses
26088
+ from pg_stat_all_indexes
24972
26089
  join pg_index using (indexrelid)
24973
26090
  left join pg_constraint
24974
26091
  on array_to_string(indkey, ',') = array_to_string(conkey, ',')
@@ -24977,37 +26094,58 @@ where
24977
26094
  and contype = 'f'
24978
26095
  where idx_scan = 0
24979
26096
  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 (
26097
+ and conkey is not null
26098
+ ),
26099
+ -- Find valid indexes that could be duplicates (same table, same columns)
26100
+ valid_duplicates as (
26101
+ select
26102
+ inv.indexrelid as invalid_indexrelid,
26103
+ val.indexrelid as valid_indexrelid,
26104
+ (val.indexrelid::regclass)::text as valid_index_name,
26105
+ pg_get_indexdef(val.indexrelid) as valid_index_definition
26106
+ from pg_index inv
26107
+ join pg_index val on inv.indrelid = val.indrelid -- same table
26108
+ and inv.indkey = val.indkey -- same columns (in same order)
26109
+ and inv.indexrelid != val.indexrelid -- different index
26110
+ and val.indisvalid = true -- valid index
26111
+ where inv.indisvalid = false
26112
+ ),
26113
+ data as (
24982
26114
  select
24983
26115
  pci.relname as tag_index_name,
24984
26116
  pn.nspname as tag_schema_name,
24985
26117
  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
26118
  coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
24990
26119
  pg_get_indexdef(pidx.indexrelid) as index_definition,
24991
- pg_relation_size(pidx.indexrelid) index_size_bytes,
26120
+ pg_relation_size(pidx.indexrelid) as index_size_bytes,
26121
+ -- Constraint info
26122
+ pidx.indisprimary as is_pk,
26123
+ pidx.indisunique as is_unique,
26124
+ con.conname as constraint_name,
26125
+ -- Table row estimate
26126
+ pct.reltuples::bigint as table_row_estimate,
26127
+ -- Valid duplicate check
26128
+ (vd.valid_indexrelid is not null) as has_valid_duplicate,
26129
+ vd.valid_index_name,
26130
+ vd.valid_index_definition,
26131
+ -- FK support check
24992
26132
  ((
24993
26133
  select count(1)
24994
26134
  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, ', ') || '%')
26135
+ where fi.fk_table_ref = pct.relname
26136
+ and fi.opclasses like (array_to_string(pidx.indclass, ', ') || '%')
24998
26137
  ) > 0)::int as supports_fk
24999
26138
  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
26139
+ join pg_class pci on pci.oid = pidx.indexrelid
26140
+ join pg_class pct on pct.oid = pidx.indrelid
25002
26141
  left join pg_namespace pn on pn.oid = pct.relnamespace
26142
+ left join pg_constraint con on con.conindid = pidx.indexrelid
26143
+ left join valid_duplicates vd on vd.invalid_indexrelid = pidx.indexrelid
25003
26144
  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 (
26145
+ ),
26146
+ num_data as (
25009
26147
  select
25010
- row_number() over () num,
26148
+ row_number() over () as num,
25011
26149
  data.*
25012
26150
  from data
25013
26151
  )
@@ -25626,7 +26764,14 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
25626
26764
  index_size_bytes: indexSizeBytes,
25627
26765
  index_size_pretty: formatBytes(indexSizeBytes),
25628
26766
  index_definition: String(transformed.index_definition || ""),
25629
- supports_fk: toBool(transformed.supports_fk)
26767
+ supports_fk: toBool(transformed.supports_fk),
26768
+ is_pk: toBool(transformed.is_pk),
26769
+ is_unique: toBool(transformed.is_unique),
26770
+ constraint_name: transformed.constraint_name ? String(transformed.constraint_name) : null,
26771
+ table_row_estimate: parseInt(String(transformed.table_row_estimate || 0), 10),
26772
+ has_valid_duplicate: toBool(transformed.has_valid_duplicate),
26773
+ valid_duplicate_name: transformed.valid_index_name ? String(transformed.valid_index_name) : null,
26774
+ valid_duplicate_definition: transformed.valid_index_definition ? String(transformed.valid_index_definition) : null
25630
26775
  };
25631
26776
  });
25632
26777
  }
@@ -26715,7 +27860,7 @@ program2.command("set-default-project <project>").description("store default pro
26715
27860
  writeConfig({ defaultProject: value });
26716
27861
  console.log(`Default project saved: ${value}`);
26717
27862
  });
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", [
27863
+ 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
27864
  "",
26720
27865
  "Examples:",
26721
27866
  " postgresai prepare-db postgresql://admin@host:5432/dbname",
@@ -26751,21 +27896,60 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26751
27896
  " postgresai prepare-db <conn> --reset-password --password '...'",
26752
27897
  "",
26753
27898
  "Offline SQL plan (no DB connection):",
26754
- " postgresai prepare-db --print-sql"
27899
+ " postgresai prepare-db --print-sql",
27900
+ "",
27901
+ "Supabase mode (use Management API instead of direct connection):",
27902
+ " postgresai prepare-db --supabase --supabase-project-ref <ref>",
27903
+ " SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
27904
+ "",
27905
+ " Generate a token at: https://supabase.com/dashboard/account/tokens",
27906
+ " Find your project ref in: https://supabase.com/dashboard/project/<ref>",
27907
+ "",
27908
+ "Provider-specific behavior (for direct connections):",
27909
+ " --provider supabase Skip role creation (create user in Supabase dashboard)",
27910
+ " Skip ALTER USER (restricted by Supabase)"
26755
27911
  ].join(`
26756
27912
  `)).action(async (conn, opts, cmd) => {
26757
- if (opts.verify && opts.resetPassword) {
26758
- console.error("\u2717 Provide only one of --verify or --reset-password");
27913
+ const jsonOutput = opts.json;
27914
+ const outputJson = (data) => {
27915
+ console.log(JSON.stringify(data, null, 2));
27916
+ };
27917
+ const outputError = (error2) => {
27918
+ if (jsonOutput) {
27919
+ outputJson({
27920
+ success: false,
27921
+ mode: opts.supabase ? "supabase" : "direct",
27922
+ error: error2
27923
+ });
27924
+ } else {
27925
+ console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error2.message}`);
27926
+ if (error2.step)
27927
+ console.error(` Step: ${error2.step}`);
27928
+ if (error2.code)
27929
+ console.error(` Code: ${error2.code}`);
27930
+ if (error2.detail)
27931
+ console.error(` Detail: ${error2.detail}`);
27932
+ if (error2.hint)
27933
+ console.error(` Hint: ${error2.hint}`);
27934
+ if (error2.httpStatus)
27935
+ console.error(` HTTP Status: ${error2.httpStatus}`);
27936
+ }
26759
27937
  process.exitCode = 1;
27938
+ };
27939
+ if (opts.verify && opts.resetPassword) {
27940
+ outputError({ message: "Provide only one of --verify or --reset-password" });
26760
27941
  return;
26761
27942
  }
26762
27943
  if (opts.verify && opts.printSql) {
26763
- console.error("\u2717 --verify cannot be combined with --print-sql");
26764
- process.exitCode = 1;
27944
+ outputError({ message: "--verify cannot be combined with --print-sql" });
26765
27945
  return;
26766
27946
  }
26767
27947
  const shouldPrintSql = !!opts.printSql;
26768
27948
  const redactPasswords = (sql) => redactPasswordsInSql(sql);
27949
+ const providerWarning = validateProvider(opts.provider);
27950
+ if (providerWarning) {
27951
+ console.warn(`\u26A0 ${providerWarning}`);
27952
+ }
26769
27953
  if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
26770
27954
  if (shouldPrintSql) {
26771
27955
  const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
@@ -26775,12 +27959,14 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26775
27959
  database,
26776
27960
  monitoringUser: opts.monitoringUser,
26777
27961
  monitoringPassword: monPassword,
26778
- includeOptionalPermissions: includeOptionalPermissions2
27962
+ includeOptionalPermissions: includeOptionalPermissions2,
27963
+ provider: opts.provider
26779
27964
  });
26780
27965
  console.log(`
26781
27966
  --- SQL plan (offline; not connected) ---`);
26782
27967
  console.log(`-- database: ${database}`);
26783
27968
  console.log(`-- monitoring user: ${opts.monitoringUser}`);
27969
+ console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
26784
27970
  console.log(`-- optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
26785
27971
  for (const step of plan.steps) {
26786
27972
  console.log(`
@@ -26794,6 +27980,281 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26794
27980
  return;
26795
27981
  }
26796
27982
  }
27983
+ if (opts.supabase) {
27984
+ let supabaseConfig;
27985
+ try {
27986
+ let projectRef = opts.supabaseProjectRef;
27987
+ if (!projectRef && conn) {
27988
+ projectRef = extractProjectRefFromUrl(conn);
27989
+ }
27990
+ supabaseConfig = resolveSupabaseConfig({
27991
+ accessToken: opts.supabaseAccessToken,
27992
+ projectRef
27993
+ });
27994
+ } catch (e) {
27995
+ const msg = e instanceof Error ? e.message : String(e);
27996
+ outputError({ message: msg });
27997
+ return;
27998
+ }
27999
+ const includeOptionalPermissions2 = !opts.skipOptionalPermissions;
28000
+ if (!jsonOutput) {
28001
+ console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
28002
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
28003
+ console.log(`Optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
28004
+ }
28005
+ const supabaseClient = new SupabaseClient(supabaseConfig);
28006
+ let databaseUrl = null;
28007
+ if (jsonOutput) {
28008
+ databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
28009
+ }
28010
+ try {
28011
+ const database = await supabaseClient.getCurrentDatabase();
28012
+ if (!database) {
28013
+ throw new Error("Failed to resolve current database name");
28014
+ }
28015
+ if (!jsonOutput) {
28016
+ console.log(`Database: ${database}`);
28017
+ }
28018
+ if (opts.verify) {
28019
+ const v = await verifyInitSetupViaSupabase({
28020
+ client: supabaseClient,
28021
+ database,
28022
+ monitoringUser: opts.monitoringUser,
28023
+ includeOptionalPermissions: includeOptionalPermissions2
28024
+ });
28025
+ if (v.ok) {
28026
+ if (jsonOutput) {
28027
+ const result = {
28028
+ success: true,
28029
+ mode: "supabase",
28030
+ action: "verify",
28031
+ database,
28032
+ monitoringUser: opts.monitoringUser,
28033
+ verified: true,
28034
+ missingOptional: v.missingOptional
28035
+ };
28036
+ if (databaseUrl) {
28037
+ result.databaseUrl = databaseUrl;
28038
+ }
28039
+ outputJson(result);
28040
+ } else {
28041
+ console.log("\u2713 prepare-db verify: OK");
28042
+ if (v.missingOptional.length > 0) {
28043
+ console.log("\u26A0 Optional items missing:");
28044
+ for (const m of v.missingOptional)
28045
+ console.log(`- ${m}`);
28046
+ }
28047
+ }
28048
+ return;
28049
+ }
28050
+ if (jsonOutput) {
28051
+ const result = {
28052
+ success: false,
28053
+ mode: "supabase",
28054
+ action: "verify",
28055
+ database,
28056
+ monitoringUser: opts.monitoringUser,
28057
+ verified: false,
28058
+ missingRequired: v.missingRequired,
28059
+ missingOptional: v.missingOptional
28060
+ };
28061
+ if (databaseUrl) {
28062
+ result.databaseUrl = databaseUrl;
28063
+ }
28064
+ outputJson(result);
28065
+ } else {
28066
+ console.error("\u2717 prepare-db verify failed: missing required items");
28067
+ for (const m of v.missingRequired)
28068
+ console.error(`- ${m}`);
28069
+ if (v.missingOptional.length > 0) {
28070
+ console.error("Optional items missing:");
28071
+ for (const m of v.missingOptional)
28072
+ console.error(`- ${m}`);
28073
+ }
28074
+ }
28075
+ process.exitCode = 1;
28076
+ return;
28077
+ }
28078
+ let monPassword;
28079
+ let passwordGenerated = false;
28080
+ try {
28081
+ const resolved = await resolveMonitoringPassword({
28082
+ passwordFlag: opts.password,
28083
+ passwordEnv: process.env.PGAI_MON_PASSWORD,
28084
+ monitoringUser: opts.monitoringUser
28085
+ });
28086
+ monPassword = resolved.password;
28087
+ passwordGenerated = resolved.generated;
28088
+ if (resolved.generated) {
28089
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
28090
+ if (canPrint) {
28091
+ if (!jsonOutput) {
28092
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
28093
+ console.error("");
28094
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
28095
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
28096
+ console.error("");
28097
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
28098
+ }
28099
+ } else {
28100
+ console.error([
28101
+ `\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
28102
+ "",
28103
+ "Provide it explicitly:",
28104
+ " --password <password> or PGAI_MON_PASSWORD=...",
28105
+ "",
28106
+ "Or (NOT recommended) print the generated password:",
28107
+ " --print-password"
28108
+ ].join(`
28109
+ `));
28110
+ process.exitCode = 1;
28111
+ return;
28112
+ }
28113
+ }
28114
+ } catch (e) {
28115
+ const msg = e instanceof Error ? e.message : String(e);
28116
+ outputError({ message: msg });
28117
+ return;
28118
+ }
28119
+ const plan = await buildInitPlan({
28120
+ database,
28121
+ monitoringUser: opts.monitoringUser,
28122
+ monitoringPassword: monPassword,
28123
+ includeOptionalPermissions: includeOptionalPermissions2
28124
+ });
28125
+ const supabaseApplicableSteps = plan.steps.filter((s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed");
28126
+ const effectivePlan = opts.resetPassword ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") } : { ...plan, steps: supabaseApplicableSteps };
28127
+ if (shouldPrintSql) {
28128
+ console.log(`
28129
+ --- SQL plan ---`);
28130
+ for (const step of effectivePlan.steps) {
28131
+ console.log(`
28132
+ -- ${step.name}${step.optional ? " (optional)" : ""}`);
28133
+ console.log(redactPasswords(step.sql));
28134
+ }
28135
+ console.log(`
28136
+ --- end SQL plan ---
28137
+ `);
28138
+ console.log("Note: passwords are redacted in the printed SQL output.");
28139
+ return;
28140
+ }
28141
+ const { applied, skippedOptional } = await applyInitPlanViaSupabase({
28142
+ client: supabaseClient,
28143
+ plan: effectivePlan
28144
+ });
28145
+ if (jsonOutput) {
28146
+ const result = {
28147
+ success: true,
28148
+ mode: "supabase",
28149
+ action: opts.resetPassword ? "reset-password" : "apply",
28150
+ database,
28151
+ monitoringUser: opts.monitoringUser,
28152
+ applied,
28153
+ skippedOptional,
28154
+ warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
28155
+ };
28156
+ if (passwordGenerated) {
28157
+ result.generatedPassword = monPassword;
28158
+ }
28159
+ if (databaseUrl) {
28160
+ result.databaseUrl = databaseUrl;
28161
+ }
28162
+ outputJson(result);
28163
+ } else {
28164
+ console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
28165
+ if (skippedOptional.length > 0) {
28166
+ console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
28167
+ for (const s of skippedOptional)
28168
+ console.log(`- ${s}`);
28169
+ }
28170
+ if (process.stdout.isTTY) {
28171
+ console.log(`Applied ${applied.length} steps`);
28172
+ }
28173
+ }
28174
+ } catch (error2) {
28175
+ const errAny = error2;
28176
+ let message = "";
28177
+ if (error2 instanceof Error && error2.message) {
28178
+ message = error2.message;
28179
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
28180
+ message = errAny.message;
28181
+ } else {
28182
+ message = String(error2);
28183
+ }
28184
+ if (!message || message === "[object Object]") {
28185
+ message = "Unknown error";
28186
+ }
28187
+ const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
28188
+ const failedStep = stepMatch?.[1];
28189
+ const errorObj = { message };
28190
+ if (failedStep)
28191
+ errorObj.step = failedStep;
28192
+ if (errAny && typeof errAny === "object") {
28193
+ if (typeof errAny.code === "string" && errAny.code)
28194
+ errorObj.code = errAny.code;
28195
+ if (typeof errAny.detail === "string" && errAny.detail)
28196
+ errorObj.detail = errAny.detail;
28197
+ if (typeof errAny.hint === "string" && errAny.hint)
28198
+ errorObj.hint = errAny.hint;
28199
+ if (typeof errAny.httpStatus === "number")
28200
+ errorObj.httpStatus = errAny.httpStatus;
28201
+ }
28202
+ if (jsonOutput) {
28203
+ outputJson({
28204
+ success: false,
28205
+ mode: "supabase",
28206
+ error: errorObj
28207
+ });
28208
+ process.exitCode = 1;
28209
+ } else {
28210
+ console.error(`Error: prepare-db (Supabase): ${message}`);
28211
+ if (failedStep) {
28212
+ console.error(` Step: ${failedStep}`);
28213
+ }
28214
+ if (errAny && typeof errAny === "object") {
28215
+ if (typeof errAny.code === "string" && errAny.code) {
28216
+ console.error(` Code: ${errAny.code}`);
28217
+ }
28218
+ if (typeof errAny.detail === "string" && errAny.detail) {
28219
+ console.error(` Detail: ${errAny.detail}`);
28220
+ }
28221
+ if (typeof errAny.hint === "string" && errAny.hint) {
28222
+ console.error(` Hint: ${errAny.hint}`);
28223
+ }
28224
+ if (typeof errAny.httpStatus === "number") {
28225
+ console.error(` HTTP Status: ${errAny.httpStatus}`);
28226
+ }
28227
+ }
28228
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
28229
+ if (errAny.code === "42501") {
28230
+ if (failedStep === "01.role") {
28231
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
28232
+ } else if (failedStep === "02.permissions") {
28233
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
28234
+ }
28235
+ console.error(" Fix: ensure your Supabase access token has sufficient permissions");
28236
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
28237
+ }
28238
+ }
28239
+ if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
28240
+ if (errAny.httpStatus === 401) {
28241
+ console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
28242
+ }
28243
+ if (errAny.httpStatus === 403) {
28244
+ console.error(" Hint: access denied; check your token permissions and project access");
28245
+ }
28246
+ if (errAny.httpStatus === 404) {
28247
+ console.error(" Hint: project not found; verify the project reference is correct");
28248
+ }
28249
+ if (errAny.httpStatus === 429) {
28250
+ console.error(" Hint: rate limited; wait a moment and try again");
28251
+ }
28252
+ }
28253
+ process.exitCode = 1;
28254
+ }
28255
+ }
28256
+ return;
28257
+ }
26797
28258
  let adminConn;
26798
28259
  try {
26799
28260
  adminConn = resolveAdminConnection({
@@ -26808,18 +28269,24 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26808
28269
  });
26809
28270
  } catch (e) {
26810
28271
  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 });
28272
+ if (jsonOutput) {
28273
+ outputError({ message: msg });
28274
+ } else {
28275
+ console.error(`Error: prepare-db: ${msg}`);
28276
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
28277
+ console.error("");
28278
+ cmd.outputHelp({ error: true });
28279
+ }
28280
+ process.exitCode = 1;
26815
28281
  }
26816
- process.exitCode = 1;
26817
28282
  return;
26818
28283
  }
26819
28284
  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"}`);
28285
+ if (!jsonOutput) {
28286
+ console.log(`Connecting to: ${adminConn.display}`);
28287
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
28288
+ console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
28289
+ }
26823
28290
  let client;
26824
28291
  try {
26825
28292
  const connResult = await connectWithSslFallback(Client, adminConn);
@@ -26834,29 +28301,57 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26834
28301
  client,
26835
28302
  database,
26836
28303
  monitoringUser: opts.monitoringUser,
26837
- includeOptionalPermissions
28304
+ includeOptionalPermissions,
28305
+ provider: opts.provider
26838
28306
  });
26839
28307
  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}`);
28308
+ if (jsonOutput) {
28309
+ outputJson({
28310
+ success: true,
28311
+ mode: "direct",
28312
+ action: "verify",
28313
+ database,
28314
+ monitoringUser: opts.monitoringUser,
28315
+ provider: opts.provider,
28316
+ verified: true,
28317
+ missingOptional: v.missingOptional
28318
+ });
28319
+ } else {
28320
+ console.log(`\u2713 prepare-db verify: OK${opts.provider ? ` (provider: ${opts.provider})` : ""}`);
28321
+ if (v.missingOptional.length > 0) {
28322
+ console.log("\u26A0 Optional items missing:");
28323
+ for (const m of v.missingOptional)
28324
+ console.log(`- ${m}`);
28325
+ }
26845
28326
  }
26846
28327
  return;
26847
28328
  }
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)
28329
+ if (jsonOutput) {
28330
+ outputJson({
28331
+ success: false,
28332
+ mode: "direct",
28333
+ action: "verify",
28334
+ database,
28335
+ monitoringUser: opts.monitoringUser,
28336
+ verified: false,
28337
+ missingRequired: v.missingRequired,
28338
+ missingOptional: v.missingOptional
28339
+ });
28340
+ } else {
28341
+ console.error("\u2717 prepare-db verify failed: missing required items");
28342
+ for (const m of v.missingRequired)
26854
28343
  console.error(`- ${m}`);
28344
+ if (v.missingOptional.length > 0) {
28345
+ console.error("Optional items missing:");
28346
+ for (const m of v.missingOptional)
28347
+ console.error(`- ${m}`);
28348
+ }
26855
28349
  }
26856
28350
  process.exitCode = 1;
26857
28351
  return;
26858
28352
  }
26859
28353
  let monPassword;
28354
+ let passwordGenerated = false;
26860
28355
  try {
26861
28356
  const resolved = await resolveMonitoringPassword({
26862
28357
  passwordFlag: opts.password,
@@ -26864,15 +28359,18 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26864
28359
  monitoringUser: opts.monitoringUser
26865
28360
  });
26866
28361
  monPassword = resolved.password;
28362
+ passwordGenerated = resolved.generated;
26867
28363
  if (resolved.generated) {
26868
- const canPrint = process.stdout.isTTY || !!opts.printPassword;
28364
+ const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
26869
28365
  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).");
28366
+ if (!jsonOutput) {
28367
+ const shellSafe = monPassword.replace(/'/g, "'\\''");
28368
+ console.error("");
28369
+ console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
28370
+ console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
28371
+ console.error("");
28372
+ console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
28373
+ }
26876
28374
  } else {
26877
28375
  console.error([
26878
28376
  `\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
@@ -26890,17 +28388,22 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26890
28388
  }
26891
28389
  } catch (e) {
26892
28390
  const msg = e instanceof Error ? e.message : String(e);
26893
- console.error(`\u2717 ${msg}`);
26894
- process.exitCode = 1;
28391
+ outputError({ message: msg });
26895
28392
  return;
26896
28393
  }
26897
28394
  const plan = await buildInitPlan({
26898
28395
  database,
26899
28396
  monitoringUser: opts.monitoringUser,
26900
28397
  monitoringPassword: monPassword,
26901
- includeOptionalPermissions
28398
+ includeOptionalPermissions,
28399
+ provider: opts.provider
26902
28400
  });
26903
28401
  const effectivePlan = opts.resetPassword ? { ...plan, steps: plan.steps.filter((s) => s.name === "01.role") } : plan;
28402
+ if (opts.resetPassword && effectivePlan.steps.length === 0) {
28403
+ console.error(`\u2717 --reset-password not supported for provider "${opts.provider}" (role creation is skipped)`);
28404
+ process.exitCode = 1;
28405
+ return;
28406
+ }
26904
28407
  if (shouldPrintSql) {
26905
28408
  console.log(`
26906
28409
  --- SQL plan ---`);
@@ -26916,14 +28419,31 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26916
28419
  return;
26917
28420
  }
26918
28421
  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`);
28422
+ if (jsonOutput) {
28423
+ const result = {
28424
+ success: true,
28425
+ mode: "direct",
28426
+ action: opts.resetPassword ? "reset-password" : "apply",
28427
+ database,
28428
+ monitoringUser: opts.monitoringUser,
28429
+ applied,
28430
+ skippedOptional,
28431
+ warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
28432
+ };
28433
+ if (passwordGenerated) {
28434
+ result.generatedPassword = monPassword;
28435
+ }
28436
+ outputJson(result);
28437
+ } else {
28438
+ console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
28439
+ if (skippedOptional.length > 0) {
28440
+ console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
28441
+ for (const s of skippedOptional)
28442
+ console.log(`- ${s}`);
28443
+ }
28444
+ if (process.stdout.isTTY) {
28445
+ console.log(`Applied ${applied.length} steps`);
28446
+ }
26927
28447
  }
26928
28448
  } catch (error2) {
26929
28449
  const errAny = error2;
@@ -26938,45 +28458,65 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
26938
28458
  if (!message || message === "[object Object]") {
26939
28459
  message = "Unknown error";
26940
28460
  }
26941
- console.error(`Error: prepare-db: ${message}`);
26942
28461
  const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
26943
28462
  const failedStep = stepMatch?.[1];
26944
- if (failedStep) {
26945
- console.error(` Step: ${failedStep}`);
26946
- }
28463
+ const errorObj = { message };
28464
+ if (failedStep)
28465
+ errorObj.step = failedStep;
26947
28466
  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}`);
28467
+ if (typeof errAny.code === "string" && errAny.code)
28468
+ errorObj.code = errAny.code;
28469
+ if (typeof errAny.detail === "string" && errAny.detail)
28470
+ errorObj.detail = errAny.detail;
28471
+ if (typeof errAny.hint === "string" && errAny.hint)
28472
+ errorObj.hint = errAny.hint;
28473
+ }
28474
+ if (jsonOutput) {
28475
+ outputJson({
28476
+ success: false,
28477
+ mode: "direct",
28478
+ error: errorObj
28479
+ });
28480
+ process.exitCode = 1;
28481
+ } else {
28482
+ console.error(`Error: prepare-db: ${message}`);
28483
+ if (failedStep) {
28484
+ console.error(` Step: ${failedStep}`);
26956
28485
  }
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");
28486
+ if (errAny && typeof errAny === "object") {
28487
+ if (typeof errAny.code === "string" && errAny.code) {
28488
+ console.error(` Code: ${errAny.code}`);
28489
+ }
28490
+ if (typeof errAny.detail === "string" && errAny.detail) {
28491
+ console.error(` Detail: ${errAny.detail}`);
28492
+ }
28493
+ if (typeof errAny.hint === "string" && errAny.hint) {
28494
+ console.error(` Hint: ${errAny.hint}`);
26964
28495
  }
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
28496
  }
26975
- if (errAny.code === "ETIMEDOUT") {
26976
- console.error(" Hint: connection timed out; check network/firewall rules");
28497
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
28498
+ if (errAny.code === "42501") {
28499
+ if (failedStep === "01.role") {
28500
+ console.error(" Context: role creation/update requires CREATEROLE or superuser");
28501
+ } else if (failedStep === "02.permissions") {
28502
+ console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
28503
+ }
28504
+ console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
28505
+ console.error(" Fix: on managed Postgres, use the provider's admin/master user");
28506
+ console.error(" Tip: run with --print-sql to review the exact SQL plan");
28507
+ }
28508
+ if (errAny.code === "ECONNREFUSED") {
28509
+ console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
28510
+ }
28511
+ if (errAny.code === "ENOTFOUND") {
28512
+ console.error(" Hint: DNS resolution failed; double-check the host name");
28513
+ }
28514
+ if (errAny.code === "ETIMEDOUT") {
28515
+ console.error(" Hint: connection timed out; check network/firewall rules");
28516
+ }
26977
28517
  }
28518
+ process.exitCode = 1;
26978
28519
  }
26979
- process.exitCode = 1;
26980
28520
  } finally {
26981
28521
  if (client) {
26982
28522
  try {
@@ -27213,14 +28753,10 @@ mon.command("local-install").description("install local monitoring stack (genera
27213
28753
  console.log(`Project directory: ${projectDir}
27214
28754
  `);
27215
28755
  const envFile = path5.resolve(projectDir, ".env");
27216
- let existingTag = null;
27217
28756
  let existingRegistry = null;
27218
28757
  let existingPassword = null;
27219
28758
  if (fs5.existsSync(envFile)) {
27220
28759
  const existingEnv = fs5.readFileSync(envFile, "utf8");
27221
- const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
27222
- if (tagMatch)
27223
- existingTag = tagMatch[1].trim();
27224
28760
  const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
27225
28761
  if (registryMatch)
27226
28762
  existingRegistry = registryMatch[1].trim();
@@ -27228,7 +28764,7 @@ mon.command("local-install").description("install local monitoring stack (genera
27228
28764
  if (pwdMatch)
27229
28765
  existingPassword = pwdMatch[1].trim();
27230
28766
  }
27231
- const imageTag = opts.tag || process.env.PGAI_TAG || existingTag || package_default.version;
28767
+ const imageTag = opts.tag || package_default.version;
27232
28768
  const envLines = [`PGAI_TAG=${imageTag}`];
27233
28769
  if (existingRegistry) {
27234
28770
  envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
@@ -27565,6 +29101,9 @@ var MONITORING_CONTAINERS = [
27565
29101
  "sources-generator",
27566
29102
  "postgres-reports"
27567
29103
  ];
29104
+ var COMPOSE_PROJECT_NAME = "postgres_ai";
29105
+ var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
29106
+ var NETWORK_CLEANUP_DELAY_MS = 2000;
27568
29107
  async function removeOrphanedContainers() {
27569
29108
  for (const container of MONITORING_CONTAINERS) {
27570
29109
  try {
@@ -27573,7 +29112,18 @@ async function removeOrphanedContainers() {
27573
29112
  }
27574
29113
  }
27575
29114
  mon.command("stop").description("stop monitoring services").action(async () => {
27576
- const code = await runCompose(["down"]);
29115
+ let code = await runCompose(["down", "--remove-orphans"]);
29116
+ if (code !== 0) {
29117
+ await removeOrphanedContainers();
29118
+ await new Promise((resolve6) => setTimeout(resolve6, NETWORK_CLEANUP_DELAY_MS));
29119
+ code = await runCompose(["down", "--remove-orphans"]);
29120
+ }
29121
+ if (code !== 0) {
29122
+ try {
29123
+ await execFilePromise("docker", ["network", "rm", DOCKER_NETWORK_NAME]);
29124
+ code = 0;
29125
+ } catch {}
29126
+ }
27577
29127
  if (code !== 0)
27578
29128
  process.exitCode = code;
27579
29129
  });
@@ -28340,18 +29890,36 @@ function interpretEscapes2(str2) {
28340
29890
  `).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\x00/g, "\\");
28341
29891
  }
28342
29892
  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) => {
29893
+ 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) => {
29894
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
28344
29895
  try {
28345
29896
  const rootOpts = program2.opts();
28346
29897
  const cfg = readConfig();
28347
29898
  const { apiKey } = getConfig(rootOpts);
28348
29899
  if (!apiKey) {
29900
+ spinner.stop();
28349
29901
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28350
29902
  process.exitCode = 1;
28351
29903
  return;
28352
29904
  }
29905
+ const orgId = cfg.orgId ?? undefined;
28353
29906
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28354
- const result = await fetchIssues2({ apiKey, apiBaseUrl, debug: !!opts.debug });
29907
+ let statusFilter;
29908
+ if (opts.status === "open") {
29909
+ statusFilter = "open";
29910
+ } else if (opts.status === "closed") {
29911
+ statusFilter = "closed";
29912
+ }
29913
+ const result = await fetchIssues2({
29914
+ apiKey,
29915
+ apiBaseUrl,
29916
+ orgId,
29917
+ status: statusFilter,
29918
+ limit: opts.limit,
29919
+ offset: opts.offset,
29920
+ debug: !!opts.debug
29921
+ });
29922
+ spinner.stop();
28355
29923
  const trimmed = Array.isArray(result) ? result.map((r) => ({
28356
29924
  id: r.id,
28357
29925
  title: r.title,
@@ -28360,17 +29928,20 @@ issues.command("list").description("list issues").option("--debug", "enable debu
28360
29928
  })) : result;
28361
29929
  printResult(trimmed, opts.json);
28362
29930
  } catch (err) {
29931
+ spinner.stop();
28363
29932
  const message = err instanceof Error ? err.message : String(err);
28364
29933
  console.error(message);
28365
29934
  process.exitCode = 1;
28366
29935
  }
28367
29936
  });
28368
29937
  issues.command("view <issueId>").description("view issue details and comments").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, opts) => {
29938
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issue...");
28369
29939
  try {
28370
29940
  const rootOpts = program2.opts();
28371
29941
  const cfg = readConfig();
28372
29942
  const { apiKey } = getConfig(rootOpts);
28373
29943
  if (!apiKey) {
29944
+ spinner.stop();
28374
29945
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28375
29946
  process.exitCode = 1;
28376
29947
  return;
@@ -28378,32 +29949,38 @@ issues.command("view <issueId>").description("view issue details and comments").
28378
29949
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28379
29950
  const issue2 = await fetchIssue2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
28380
29951
  if (!issue2) {
29952
+ spinner.stop();
28381
29953
  console.error("Issue not found");
28382
29954
  process.exitCode = 1;
28383
29955
  return;
28384
29956
  }
29957
+ spinner.update("Fetching comments...");
28385
29958
  const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
29959
+ spinner.stop();
28386
29960
  const combined = { issue: issue2, comments };
28387
29961
  printResult(combined, opts.json);
28388
29962
  } catch (err) {
29963
+ spinner.stop();
28389
29964
  const message = err instanceof Error ? err.message : String(err);
28390
29965
  console.error(message);
28391
29966
  process.exitCode = 1;
28392
29967
  }
28393
29968
  });
28394
29969
  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) => {
29970
+ if (opts.debug) {
29971
+ console.log(`Debug: Original content: ${JSON.stringify(content)}`);
29972
+ }
29973
+ content = interpretEscapes2(content);
29974
+ if (opts.debug) {
29975
+ console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
29976
+ }
29977
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Posting comment...");
28395
29978
  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
29979
  const rootOpts = program2.opts();
28404
29980
  const cfg = readConfig();
28405
29981
  const { apiKey } = getConfig(rootOpts);
28406
29982
  if (!apiKey) {
29983
+ spinner.stop();
28407
29984
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28408
29985
  process.exitCode = 1;
28409
29986
  return;
@@ -28417,41 +29994,44 @@ issues.command("post-comment <issueId> <content>").description("post a new comme
28417
29994
  parentCommentId: opts.parent,
28418
29995
  debug: !!opts.debug
28419
29996
  });
29997
+ spinner.stop();
28420
29998
  printResult(result, opts.json);
28421
29999
  } catch (err) {
30000
+ spinner.stop();
28422
30001
  const message = err instanceof Error ? err.message : String(err);
28423
30002
  console.error(message);
28424
30003
  process.exitCode = 1;
28425
30004
  }
28426
30005
  });
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) => {
30006
+ 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
30007
  previous.push(value);
28429
30008
  return previous;
28430
30009
  }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (rawTitle, opts) => {
30010
+ const rootOpts = program2.opts();
30011
+ const cfg = readConfig();
30012
+ const { apiKey } = getConfig(rootOpts);
30013
+ if (!apiKey) {
30014
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30015
+ process.exitCode = 1;
30016
+ return;
30017
+ }
30018
+ const title = interpretEscapes2(String(rawTitle || "").trim());
30019
+ if (!title) {
30020
+ console.error("title is required");
30021
+ process.exitCode = 1;
30022
+ return;
30023
+ }
30024
+ const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
30025
+ if (typeof orgId !== "number") {
30026
+ console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
30027
+ process.exitCode = 1;
30028
+ return;
30029
+ }
30030
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30031
+ const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
30032
+ const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
30033
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating issue...");
28431
30034
  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
30035
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28456
30036
  const result = await createIssue2({
28457
30037
  apiKey,
@@ -28463,57 +30043,60 @@ issues.command("create <title>").description("create a new issue").option("--org
28463
30043
  labels,
28464
30044
  debug: !!opts.debug
28465
30045
  });
30046
+ spinner.stop();
28466
30047
  printResult(result, opts.json);
28467
30048
  } catch (err) {
30049
+ spinner.stop();
28468
30050
  const message = err instanceof Error ? err.message : String(err);
28469
30051
  console.error(message);
28470
30052
  process.exitCode = 1;
28471
30053
  }
28472
30054
  });
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) => {
30055
+ 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
30056
  previous.push(value);
28475
30057
  return previous;
28476
30058
  }, []).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)");
30059
+ const rootOpts = program2.opts();
30060
+ const cfg = readConfig();
30061
+ const { apiKey } = getConfig(rootOpts);
30062
+ if (!apiKey) {
30063
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30064
+ process.exitCode = 1;
30065
+ return;
30066
+ }
30067
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30068
+ const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
30069
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30070
+ let status = undefined;
30071
+ if (opts.status !== undefined) {
30072
+ const raw = String(opts.status).trim().toLowerCase();
30073
+ if (raw === "open")
30074
+ status = 0;
30075
+ else if (raw === "closed")
30076
+ status = 1;
30077
+ else {
30078
+ const n = Number(raw);
30079
+ if (!Number.isFinite(n)) {
30080
+ console.error("status must be open|closed|0|1");
28507
30081
  process.exitCode = 1;
28508
30082
  return;
28509
30083
  }
30084
+ status = n;
28510
30085
  }
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);
30086
+ if (status !== 0 && status !== 1) {
30087
+ console.error("status must be 0 (open) or 1 (closed)");
30088
+ process.exitCode = 1;
30089
+ return;
28516
30090
  }
30091
+ }
30092
+ let labels = undefined;
30093
+ if (opts.clearLabels) {
30094
+ labels = [];
30095
+ } else if (Array.isArray(opts.label) && opts.label.length > 0) {
30096
+ labels = opts.label.map(String);
30097
+ }
30098
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating issue...");
30099
+ try {
28517
30100
  const result = await updateIssue2({
28518
30101
  apiKey,
28519
30102
  apiBaseUrl,
@@ -28524,40 +30107,217 @@ issues.command("update <issueId>").description("update an existing issue (title/
28524
30107
  labels,
28525
30108
  debug: !!opts.debug
28526
30109
  });
30110
+ spinner.stop();
28527
30111
  printResult(result, opts.json);
28528
30112
  } catch (err) {
30113
+ spinner.stop();
28529
30114
  const message = err instanceof Error ? err.message : String(err);
28530
30115
  console.error(message);
28531
30116
  process.exitCode = 1;
28532
30117
  }
28533
30118
  });
28534
30119
  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) => {
30120
+ if (opts.debug) {
30121
+ console.log(`Debug: Original content: ${JSON.stringify(content)}`);
30122
+ }
30123
+ content = interpretEscapes2(content);
30124
+ if (opts.debug) {
30125
+ console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
30126
+ }
30127
+ const rootOpts = program2.opts();
30128
+ const cfg = readConfig();
30129
+ const { apiKey } = getConfig(rootOpts);
30130
+ if (!apiKey) {
30131
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30132
+ process.exitCode = 1;
30133
+ return;
30134
+ }
30135
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating comment...");
28535
30136
  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)}`);
30137
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30138
+ const result = await updateIssueComment2({
30139
+ apiKey,
30140
+ apiBaseUrl,
30141
+ commentId,
30142
+ content,
30143
+ debug: !!opts.debug
30144
+ });
30145
+ spinner.stop();
30146
+ printResult(result, opts.json);
30147
+ } catch (err) {
30148
+ spinner.stop();
30149
+ const message = err instanceof Error ? err.message : String(err);
30150
+ console.error(message);
30151
+ process.exitCode = 1;
30152
+ }
30153
+ });
30154
+ 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) => {
30155
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action items...");
30156
+ try {
30157
+ const rootOpts = program2.opts();
30158
+ const cfg = readConfig();
30159
+ const { apiKey } = getConfig(rootOpts);
30160
+ if (!apiKey) {
30161
+ spinner.stop();
30162
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30163
+ process.exitCode = 1;
30164
+ return;
28542
30165
  }
30166
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30167
+ const result = await fetchActionItems2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
30168
+ spinner.stop();
30169
+ printResult(result, opts.json);
30170
+ } catch (err) {
30171
+ spinner.stop();
30172
+ const message = err instanceof Error ? err.message : String(err);
30173
+ console.error(message);
30174
+ process.exitCode = 1;
30175
+ }
30176
+ });
30177
+ 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) => {
30178
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching action item(s)...");
30179
+ try {
28543
30180
  const rootOpts = program2.opts();
28544
30181
  const cfg = readConfig();
28545
30182
  const { apiKey } = getConfig(rootOpts);
28546
30183
  if (!apiKey) {
30184
+ spinner.stop();
28547
30185
  console.error("API key is required. Run 'pgai auth' first or set --api-key.");
28548
30186
  process.exitCode = 1;
28549
30187
  return;
28550
30188
  }
28551
30189
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
28552
- const result = await updateIssueComment2({
30190
+ const result = await fetchActionItem2({ apiKey, apiBaseUrl, actionItemIds, debug: !!opts.debug });
30191
+ if (result.length === 0) {
30192
+ spinner.stop();
30193
+ console.error("Action item(s) not found");
30194
+ process.exitCode = 1;
30195
+ return;
30196
+ }
30197
+ spinner.stop();
30198
+ printResult(result, opts.json);
30199
+ } catch (err) {
30200
+ spinner.stop();
30201
+ const message = err instanceof Error ? err.message : String(err);
30202
+ console.error(message);
30203
+ process.exitCode = 1;
30204
+ }
30205
+ });
30206
+ 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) => {
30207
+ try {
30208
+ previous.push(JSON.parse(value));
30209
+ } catch {
30210
+ console.error(`Invalid JSON for --config: ${value}`);
30211
+ process.exit(1);
30212
+ }
30213
+ return previous;
30214
+ }, []).option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (issueId, rawTitle, opts) => {
30215
+ const rootOpts = program2.opts();
30216
+ const cfg = readConfig();
30217
+ const { apiKey } = getConfig(rootOpts);
30218
+ if (!apiKey) {
30219
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30220
+ process.exitCode = 1;
30221
+ return;
30222
+ }
30223
+ const title = interpretEscapes2(String(rawTitle || "").trim());
30224
+ if (!title) {
30225
+ console.error("title is required");
30226
+ process.exitCode = 1;
30227
+ return;
30228
+ }
30229
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30230
+ const sqlAction = opts.sqlAction;
30231
+ const configs = Array.isArray(opts.config) && opts.config.length > 0 ? opts.config : undefined;
30232
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Creating action item...");
30233
+ try {
30234
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30235
+ const result = await createActionItem2({
28553
30236
  apiKey,
28554
30237
  apiBaseUrl,
28555
- commentId,
28556
- content,
30238
+ issueId,
30239
+ title,
30240
+ description,
30241
+ sqlAction,
30242
+ configs,
28557
30243
  debug: !!opts.debug
28558
30244
  });
28559
- printResult(result, opts.json);
30245
+ spinner.stop();
30246
+ printResult({ id: result }, opts.json);
30247
+ } catch (err) {
30248
+ spinner.stop();
30249
+ const message = err instanceof Error ? err.message : String(err);
30250
+ console.error(message);
30251
+ process.exitCode = 1;
30252
+ }
30253
+ });
30254
+ 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) => {
30255
+ try {
30256
+ previous.push(JSON.parse(value));
30257
+ } catch {
30258
+ console.error(`Invalid JSON for --config: ${value}`);
30259
+ process.exit(1);
30260
+ }
30261
+ return previous;
30262
+ }, []).option("--clear-configs", "clear all config changes").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (actionItemId, opts) => {
30263
+ const rootOpts = program2.opts();
30264
+ const cfg = readConfig();
30265
+ const { apiKey } = getConfig(rootOpts);
30266
+ if (!apiKey) {
30267
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
30268
+ process.exitCode = 1;
30269
+ return;
30270
+ }
30271
+ const title = opts.title !== undefined ? interpretEscapes2(String(opts.title)) : undefined;
30272
+ const description = opts.description !== undefined ? interpretEscapes2(String(opts.description)) : undefined;
30273
+ let isDone = undefined;
30274
+ if (opts.done)
30275
+ isDone = true;
30276
+ else if (opts.notDone)
30277
+ isDone = false;
30278
+ let status = undefined;
30279
+ if (opts.status !== undefined) {
30280
+ const validStatuses = ["waiting_for_approval", "approved", "rejected"];
30281
+ if (!validStatuses.includes(opts.status)) {
30282
+ console.error(`status must be one of: ${validStatuses.join(", ")}`);
30283
+ process.exitCode = 1;
30284
+ return;
30285
+ }
30286
+ status = opts.status;
30287
+ }
30288
+ const statusReason = opts.statusReason;
30289
+ const sqlAction = opts.sqlAction;
30290
+ let configs = undefined;
30291
+ if (opts.clearConfigs) {
30292
+ configs = [];
30293
+ } else if (Array.isArray(opts.config) && opts.config.length > 0) {
30294
+ configs = opts.config;
30295
+ }
30296
+ if (title === undefined && description === undefined && isDone === undefined && status === undefined && statusReason === undefined && sqlAction === undefined && configs === undefined) {
30297
+ console.error("At least one update option is required");
30298
+ process.exitCode = 1;
30299
+ return;
30300
+ }
30301
+ const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Updating action item...");
30302
+ try {
30303
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
30304
+ await updateActionItem2({
30305
+ apiKey,
30306
+ apiBaseUrl,
30307
+ actionItemId,
30308
+ title,
30309
+ description,
30310
+ isDone,
30311
+ status,
30312
+ statusReason,
30313
+ sqlAction,
30314
+ configs,
30315
+ debug: !!opts.debug
30316
+ });
30317
+ spinner.stop();
30318
+ printResult({ success: true }, opts.json);
28560
30319
  } catch (err) {
30320
+ spinner.stop();
28561
30321
  const message = err instanceof Error ? err.message : String(err);
28562
30322
  console.error(message);
28563
30323
  process.exitCode = 1;