postgresai 0.14.0-dev.8 → 0.14.0-dev.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +161 -61
  2. package/bin/postgres-ai.ts +2596 -428
  3. package/bun.lock +258 -0
  4. package/bunfig.toml +20 -0
  5. package/dist/bin/postgres-ai.js +31277 -1575
  6. package/dist/sql/01.role.sql +16 -0
  7. package/dist/sql/02.extensions.sql +8 -0
  8. package/dist/sql/03.permissions.sql +38 -0
  9. package/dist/sql/04.optional_rds.sql +6 -0
  10. package/dist/sql/05.optional_self_managed.sql +8 -0
  11. package/dist/sql/06.helpers.sql +439 -0
  12. package/dist/sql/sql/01.role.sql +16 -0
  13. package/dist/sql/sql/02.extensions.sql +8 -0
  14. package/dist/sql/sql/03.permissions.sql +38 -0
  15. package/dist/sql/sql/04.optional_rds.sql +6 -0
  16. package/dist/sql/sql/05.optional_self_managed.sql +8 -0
  17. package/dist/sql/sql/06.helpers.sql +439 -0
  18. package/dist/sql/sql/uninit/01.helpers.sql +5 -0
  19. package/dist/sql/sql/uninit/02.permissions.sql +30 -0
  20. package/dist/sql/sql/uninit/03.role.sql +27 -0
  21. package/dist/sql/uninit/01.helpers.sql +5 -0
  22. package/dist/sql/uninit/02.permissions.sql +30 -0
  23. package/dist/sql/uninit/03.role.sql +27 -0
  24. package/lib/auth-server.ts +124 -106
  25. package/lib/checkup-api.ts +386 -0
  26. package/lib/checkup-dictionary.ts +113 -0
  27. package/lib/checkup.ts +1512 -0
  28. package/lib/config.ts +6 -3
  29. package/lib/init.ts +655 -189
  30. package/lib/issues.ts +848 -193
  31. package/lib/mcp-server.ts +391 -91
  32. package/lib/metrics-loader.ts +127 -0
  33. package/lib/supabase.ts +824 -0
  34. package/lib/util.ts +61 -0
  35. package/package.json +22 -10
  36. package/packages/postgres-ai/README.md +26 -0
  37. package/packages/postgres-ai/bin/postgres-ai.js +27 -0
  38. package/packages/postgres-ai/package.json +27 -0
  39. package/scripts/embed-checkup-dictionary.ts +106 -0
  40. package/scripts/embed-metrics.ts +154 -0
  41. package/sql/01.role.sql +16 -0
  42. package/sql/02.extensions.sql +8 -0
  43. package/sql/03.permissions.sql +38 -0
  44. package/sql/04.optional_rds.sql +6 -0
  45. package/sql/05.optional_self_managed.sql +8 -0
  46. package/sql/06.helpers.sql +439 -0
  47. package/sql/uninit/01.helpers.sql +5 -0
  48. package/sql/uninit/02.permissions.sql +30 -0
  49. package/sql/uninit/03.role.sql +27 -0
  50. package/test/auth.test.ts +258 -0
  51. package/test/checkup.integration.test.ts +321 -0
  52. package/test/checkup.test.ts +1116 -0
  53. package/test/config-consistency.test.ts +36 -0
  54. package/test/init.integration.test.ts +508 -0
  55. package/test/init.test.ts +916 -0
  56. package/test/issues.cli.test.ts +538 -0
  57. package/test/issues.test.ts +456 -0
  58. package/test/mcp-server.test.ts +1527 -0
  59. package/test/schema-validation.test.ts +81 -0
  60. package/test/supabase.test.ts +568 -0
  61. package/test/test-utils.ts +128 -0
  62. package/tsconfig.json +12 -20
  63. package/dist/bin/postgres-ai.d.ts +0 -3
  64. package/dist/bin/postgres-ai.d.ts.map +0 -1
  65. package/dist/bin/postgres-ai.js.map +0 -1
  66. package/dist/lib/auth-server.d.ts +0 -31
  67. package/dist/lib/auth-server.d.ts.map +0 -1
  68. package/dist/lib/auth-server.js +0 -263
  69. package/dist/lib/auth-server.js.map +0 -1
  70. package/dist/lib/config.d.ts +0 -45
  71. package/dist/lib/config.d.ts.map +0 -1
  72. package/dist/lib/config.js +0 -181
  73. package/dist/lib/config.js.map +0 -1
  74. package/dist/lib/init.d.ts +0 -64
  75. package/dist/lib/init.d.ts.map +0 -1
  76. package/dist/lib/init.js +0 -399
  77. package/dist/lib/init.js.map +0 -1
  78. package/dist/lib/issues.d.ts +0 -75
  79. package/dist/lib/issues.d.ts.map +0 -1
  80. package/dist/lib/issues.js +0 -336
  81. package/dist/lib/issues.js.map +0 -1
  82. package/dist/lib/mcp-server.d.ts +0 -9
  83. package/dist/lib/mcp-server.d.ts.map +0 -1
  84. package/dist/lib/mcp-server.js +0 -168
  85. package/dist/lib/mcp-server.js.map +0 -1
  86. package/dist/lib/pkce.d.ts +0 -32
  87. package/dist/lib/pkce.d.ts.map +0 -1
  88. package/dist/lib/pkce.js +0 -101
  89. package/dist/lib/pkce.js.map +0 -1
  90. package/dist/lib/util.d.ts +0 -27
  91. package/dist/lib/util.d.ts.map +0 -1
  92. package/dist/lib/util.js +0 -46
  93. package/dist/lib/util.js.map +0 -1
  94. package/dist/package.json +0 -46
  95. package/test/init.integration.test.cjs +0 -269
  96. package/test/init.test.cjs +0 -76
package/lib/issues.ts CHANGED
@@ -1,6 +1,26 @@
1
- import * as https from "https";
2
- import { URL } from "url";
3
- import { maskSecret, normalizeBaseUrl } from "./util";
1
+ import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
2
+
3
+ /**
4
+ * Issue status constants.
5
+ * Used in updateIssue to change issue state.
6
+ */
7
+ export const IssueStatus = {
8
+ /** Issue is open and active */
9
+ OPEN: 0,
10
+ /** Issue is closed/resolved */
11
+ CLOSED: 1,
12
+ } as const;
13
+
14
+ /**
15
+ * Represents a PostgreSQL configuration parameter change recommendation.
16
+ * Used in action items to suggest config tuning.
17
+ */
18
+ export interface ConfigChange {
19
+ /** PostgreSQL configuration parameter name (e.g., 'work_mem', 'shared_buffers') */
20
+ parameter: string;
21
+ /** Recommended value for the parameter (e.g., '256MB', '4GB') */
22
+ value: string;
23
+ }
4
24
 
5
25
  export interface IssueActionItem {
6
26
  id: string;
@@ -11,10 +31,27 @@ export interface IssueActionItem {
11
31
  is_done: boolean;
12
32
  done_by: number | null;
13
33
  done_at: string | null;
34
+ status: string;
35
+ status_reason: string | null;
36
+ status_changed_by: number | null;
37
+ status_changed_at: string | null;
38
+ sql_action: string | null;
39
+ configs: ConfigChange[];
14
40
  created_at: string;
15
41
  updated_at: string;
16
42
  }
17
43
 
44
+ /**
45
+ * Summary of an action item (minimal fields for list views).
46
+ * Used in issue detail responses to provide quick overview of action items.
47
+ */
48
+ export interface IssueActionItemSummary {
49
+ /** Action item ID (UUID) */
50
+ id: string;
51
+ /** Action item title */
52
+ title: string;
53
+ }
54
+
18
55
  export interface Issue {
19
56
  id: string;
20
57
  title: string;
@@ -50,15 +87,21 @@ export interface IssueComment {
50
87
 
51
88
  export type IssueListItem = Pick<Issue, "id" | "title" | "status" | "created_at">;
52
89
 
53
- export type IssueDetail = Pick<Issue, "id" | "title" | "description" | "status" | "created_at" | "author_display_name">;
90
+ export type IssueDetail = Pick<Issue, "id" | "title" | "description" | "status" | "created_at" | "author_display_name"> & {
91
+ action_items: IssueActionItemSummary[];
92
+ };
54
93
  export interface FetchIssuesParams {
55
94
  apiKey: string;
56
95
  apiBaseUrl: string;
96
+ orgId?: number;
97
+ status?: "open" | "closed";
98
+ limit?: number;
99
+ offset?: number;
57
100
  debug?: boolean;
58
101
  }
59
102
 
60
103
  export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListItem[]> {
61
- const { apiKey, apiBaseUrl, debug } = params;
104
+ const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
62
105
  if (!apiKey) {
63
106
  throw new Error("API key is required");
64
107
  }
@@ -66,68 +109,54 @@ export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListI
66
109
  const base = normalizeBaseUrl(apiBaseUrl);
67
110
  const url = new URL(`${base}/issues`);
68
111
  url.searchParams.set("select", "id,title,status,created_at");
112
+ url.searchParams.set("order", "id.desc");
113
+ url.searchParams.set("limit", String(limit));
114
+ url.searchParams.set("offset", String(offset));
115
+ if (typeof orgId === "number") {
116
+ url.searchParams.set("org_id", `eq.${orgId}`);
117
+ }
118
+ if (status === "open") {
119
+ url.searchParams.set("status", "eq.0");
120
+ } else if (status === "closed") {
121
+ url.searchParams.set("status", "eq.1");
122
+ }
69
123
 
70
124
  const headers: Record<string, string> = {
71
125
  "access-token": apiKey,
72
126
  "Prefer": "return=representation",
73
127
  "Content-Type": "application/json",
128
+ "Connection": "close",
74
129
  };
75
130
 
76
131
  if (debug) {
77
132
  const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
78
- // eslint-disable-next-line no-console
79
133
  console.log(`Debug: Resolved API base URL: ${base}`);
80
- // eslint-disable-next-line no-console
81
134
  console.log(`Debug: GET URL: ${url.toString()}`);
82
- // eslint-disable-next-line no-console
83
135
  console.log(`Debug: Auth scheme: access-token`);
84
- // eslint-disable-next-line no-console
85
136
  console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
86
137
  }
87
138
 
88
- return new Promise((resolve, reject) => {
89
- const req = https.request(
90
- url,
91
- {
92
- method: "GET",
93
- headers,
94
- },
95
- (res) => {
96
- let data = "";
97
- res.on("data", (chunk) => (data += chunk));
98
- res.on("end", () => {
99
- if (debug) {
100
- // eslint-disable-next-line no-console
101
- console.log(`Debug: Response status: ${res.statusCode}`);
102
- // eslint-disable-next-line no-console
103
- console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
104
- }
105
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
106
- try {
107
- const parsed = JSON.parse(data) as IssueListItem[];
108
- resolve(parsed);
109
- } catch {
110
- reject(new Error(`Failed to parse issues response: ${data}`));
111
- }
112
- } else {
113
- let errMsg = `Failed to fetch issues: HTTP ${res.statusCode}`;
114
- if (data) {
115
- try {
116
- const errObj = JSON.parse(data);
117
- errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
118
- } catch {
119
- errMsg += `\n${data}`;
120
- }
121
- }
122
- reject(new Error(errMsg));
123
- }
124
- });
125
- }
126
- );
127
-
128
- req.on("error", (err: Error) => reject(err));
129
- req.end();
139
+ const response = await fetch(url.toString(), {
140
+ method: "GET",
141
+ headers,
130
142
  });
143
+
144
+ if (debug) {
145
+ console.log(`Debug: Response status: ${response.status}`);
146
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
147
+ }
148
+
149
+ const data = await response.text();
150
+
151
+ if (response.ok) {
152
+ try {
153
+ return JSON.parse(data) as IssueListItem[];
154
+ } catch {
155
+ throw new Error(`Failed to parse issues response: ${data}`);
156
+ }
157
+ } else {
158
+ throw new Error(formatHttpError("Failed to fetch issues", response.status, data));
159
+ }
131
160
  }
132
161
 
133
162
 
@@ -154,63 +183,38 @@ export async function fetchIssueComments(params: FetchIssueCommentsParams): Prom
154
183
  "access-token": apiKey,
155
184
  "Prefer": "return=representation",
156
185
  "Content-Type": "application/json",
186
+ "Connection": "close",
157
187
  };
158
188
 
159
189
  if (debug) {
160
190
  const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
161
- // eslint-disable-next-line no-console
162
191
  console.log(`Debug: Resolved API base URL: ${base}`);
163
- // eslint-disable-next-line no-console
164
192
  console.log(`Debug: GET URL: ${url.toString()}`);
165
- // eslint-disable-next-line no-console
166
193
  console.log(`Debug: Auth scheme: access-token`);
167
- // eslint-disable-next-line no-console
168
194
  console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
169
195
  }
170
196
 
171
- return new Promise((resolve, reject) => {
172
- const req = https.request(
173
- url,
174
- {
175
- method: "GET",
176
- headers,
177
- },
178
- (res) => {
179
- let data = "";
180
- res.on("data", (chunk) => (data += chunk));
181
- res.on("end", () => {
182
- if (debug) {
183
- // eslint-disable-next-line no-console
184
- console.log(`Debug: Response status: ${res.statusCode}`);
185
- // eslint-disable-next-line no-console
186
- console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
187
- }
188
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
189
- try {
190
- const parsed = JSON.parse(data) as IssueComment[];
191
- resolve(parsed);
192
- } catch {
193
- reject(new Error(`Failed to parse issue comments response: ${data}`));
194
- }
195
- } else {
196
- let errMsg = `Failed to fetch issue comments: HTTP ${res.statusCode}`;
197
- if (data) {
198
- try {
199
- const errObj = JSON.parse(data);
200
- errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
201
- } catch {
202
- errMsg += `\n${data}`;
203
- }
204
- }
205
- reject(new Error(errMsg));
206
- }
207
- });
208
- }
209
- );
210
-
211
- req.on("error", (err: Error) => reject(err));
212
- req.end();
197
+ const response = await fetch(url.toString(), {
198
+ method: "GET",
199
+ headers,
213
200
  });
201
+
202
+ if (debug) {
203
+ console.log(`Debug: Response status: ${response.status}`);
204
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
205
+ }
206
+
207
+ const data = await response.text();
208
+
209
+ if (response.ok) {
210
+ try {
211
+ return JSON.parse(data) as IssueComment[];
212
+ } catch {
213
+ throw new Error(`Failed to parse issue comments response: ${data}`);
214
+ }
215
+ } else {
216
+ throw new Error(formatHttpError("Failed to fetch issue comments", response.status, data));
217
+ }
214
218
  }
215
219
 
216
220
  export interface FetchIssueParams {
@@ -231,7 +235,7 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
231
235
 
232
236
  const base = normalizeBaseUrl(apiBaseUrl);
233
237
  const url = new URL(`${base}/issues`);
234
- url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
238
+ url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
235
239
  url.searchParams.set("id", `eq.${issueId}`);
236
240
  url.searchParams.set("limit", "1");
237
241
 
@@ -239,67 +243,161 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
239
243
  "access-token": apiKey,
240
244
  "Prefer": "return=representation",
241
245
  "Content-Type": "application/json",
246
+ "Connection": "close",
242
247
  };
243
248
 
244
249
  if (debug) {
245
250
  const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
246
- // eslint-disable-next-line no-console
247
251
  console.log(`Debug: Resolved API base URL: ${base}`);
248
- // eslint-disable-next-line no-console
249
252
  console.log(`Debug: GET URL: ${url.toString()}`);
250
- // eslint-disable-next-line no-console
251
253
  console.log(`Debug: Auth scheme: access-token`);
252
- // eslint-disable-next-line no-console
253
254
  console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
254
255
  }
255
256
 
256
- return new Promise((resolve, reject) => {
257
- const req = https.request(
258
- url,
259
- {
260
- method: "GET",
261
- headers,
262
- },
263
- (res) => {
264
- let data = "";
265
- res.on("data", (chunk) => (data += chunk));
266
- res.on("end", () => {
267
- if (debug) {
268
- // eslint-disable-next-line no-console
269
- console.log(`Debug: Response status: ${res.statusCode}`);
270
- // eslint-disable-next-line no-console
271
- console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
272
- }
273
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
274
- try {
275
- const parsed = JSON.parse(data);
276
- if (Array.isArray(parsed)) {
277
- resolve((parsed[0] as IssueDetail) ?? null);
278
- } else {
279
- resolve(parsed as IssueDetail);
280
- }
281
- } catch {
282
- reject(new Error(`Failed to parse issue response: ${data}`));
283
- }
284
- } else {
285
- let errMsg = `Failed to fetch issue: HTTP ${res.statusCode}`;
286
- if (data) {
287
- try {
288
- const errObj = JSON.parse(data);
289
- errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
290
- } catch {
291
- errMsg += `\n${data}`;
292
- }
293
- }
294
- reject(new Error(errMsg));
295
- }
296
- });
257
+ const response = await fetch(url.toString(), {
258
+ method: "GET",
259
+ headers,
260
+ });
261
+
262
+ if (debug) {
263
+ console.log(`Debug: Response status: ${response.status}`);
264
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
265
+ }
266
+
267
+ const data = await response.text();
268
+
269
+ if (response.ok) {
270
+ try {
271
+ const parsed = JSON.parse(data);
272
+ const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
273
+ if (!rawIssue) {
274
+ return null;
297
275
  }
298
- );
276
+ // Map action_items to summary (id, title only)
277
+ const actionItemsSummary: IssueActionItemSummary[] = Array.isArray(rawIssue.action_items)
278
+ ? rawIssue.action_items.map((item: IssueActionItem) => ({ id: item.id, title: item.title }))
279
+ : [];
280
+ return {
281
+ id: rawIssue.id,
282
+ title: rawIssue.title,
283
+ description: rawIssue.description,
284
+ status: rawIssue.status,
285
+ created_at: rawIssue.created_at,
286
+ author_display_name: rawIssue.author_display_name,
287
+ action_items: actionItemsSummary,
288
+ } as IssueDetail;
289
+ } catch {
290
+ throw new Error(`Failed to parse issue response: ${data}`);
291
+ }
292
+ } else {
293
+ throw new Error(formatHttpError("Failed to fetch issue", response.status, data));
294
+ }
295
+ }
299
296
 
300
- req.on("error", (err: Error) => reject(err));
301
- req.end();
297
+ export interface CreateIssueParams {
298
+ apiKey: string;
299
+ apiBaseUrl: string;
300
+ title: string;
301
+ orgId: number;
302
+ description?: string;
303
+ projectId?: number;
304
+ labels?: string[];
305
+ debug?: boolean;
306
+ }
307
+
308
+ export interface CreatedIssue {
309
+ id: string;
310
+ title: string;
311
+ description: string | null;
312
+ created_at: string;
313
+ status: number;
314
+ project_id: number | null;
315
+ labels: string[] | null;
316
+ }
317
+
318
+ /**
319
+ * Create a new issue in the PostgresAI platform.
320
+ *
321
+ * @param params - The parameters for creating an issue
322
+ * @param params.apiKey - API key for authentication
323
+ * @param params.apiBaseUrl - Base URL for the API
324
+ * @param params.title - Issue title (required)
325
+ * @param params.orgId - Organization ID (required)
326
+ * @param params.description - Optional issue description
327
+ * @param params.projectId - Optional project ID to associate with
328
+ * @param params.labels - Optional array of label strings
329
+ * @param params.debug - Enable debug logging
330
+ * @returns The created issue object
331
+ * @throws Error if API key, title, or orgId is missing, or if the API call fails
332
+ */
333
+ export async function createIssue(params: CreateIssueParams): Promise<CreatedIssue> {
334
+ const { apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug } = params;
335
+ if (!apiKey) {
336
+ throw new Error("API key is required");
337
+ }
338
+ if (!title) {
339
+ throw new Error("title is required");
340
+ }
341
+ if (typeof orgId !== "number") {
342
+ throw new Error("orgId is required");
343
+ }
344
+
345
+ const base = normalizeBaseUrl(apiBaseUrl);
346
+ const url = new URL(`${base}/rpc/issue_create`);
347
+
348
+ const bodyObj: Record<string, unknown> = {
349
+ title: title,
350
+ org_id: orgId,
351
+ };
352
+ if (description !== undefined) {
353
+ bodyObj.description = description;
354
+ }
355
+ if (projectId !== undefined) {
356
+ bodyObj.project_id = projectId;
357
+ }
358
+ if (labels && labels.length > 0) {
359
+ bodyObj.labels = labels;
360
+ }
361
+ const body = JSON.stringify(bodyObj);
362
+
363
+ const headers: Record<string, string> = {
364
+ "access-token": apiKey,
365
+ "Prefer": "return=representation",
366
+ "Content-Type": "application/json",
367
+ "Connection": "close",
368
+ };
369
+
370
+ if (debug) {
371
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
372
+ console.log(`Debug: Resolved API base URL: ${base}`);
373
+ console.log(`Debug: POST URL: ${url.toString()}`);
374
+ console.log(`Debug: Auth scheme: access-token`);
375
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
376
+ console.log(`Debug: Request body: ${body}`);
377
+ }
378
+
379
+ const response = await fetch(url.toString(), {
380
+ method: "POST",
381
+ headers,
382
+ body,
302
383
  });
384
+
385
+ if (debug) {
386
+ console.log(`Debug: Response status: ${response.status}`);
387
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
388
+ }
389
+
390
+ const data = await response.text();
391
+
392
+ if (response.ok) {
393
+ try {
394
+ return JSON.parse(data) as CreatedIssue;
395
+ } catch {
396
+ throw new Error(`Failed to parse create issue response: ${data}`);
397
+ }
398
+ } else {
399
+ throw new Error(formatHttpError("Failed to create issue", response.status, data));
400
+ }
303
401
  }
304
402
 
305
403
  export interface CreateIssueCommentParams {
@@ -339,67 +437,624 @@ export async function createIssueComment(params: CreateIssueCommentParams): Prom
339
437
  "access-token": apiKey,
340
438
  "Prefer": "return=representation",
341
439
  "Content-Type": "application/json",
342
- "Content-Length": Buffer.byteLength(body).toString(),
440
+ "Connection": "close",
343
441
  };
344
442
 
345
443
  if (debug) {
346
444
  const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
347
- // eslint-disable-next-line no-console
348
445
  console.log(`Debug: Resolved API base URL: ${base}`);
349
- // eslint-disable-next-line no-console
350
446
  console.log(`Debug: POST URL: ${url.toString()}`);
351
- // eslint-disable-next-line no-console
352
447
  console.log(`Debug: Auth scheme: access-token`);
353
- // eslint-disable-next-line no-console
354
448
  console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
355
- // eslint-disable-next-line no-console
356
449
  console.log(`Debug: Request body: ${body}`);
357
450
  }
358
451
 
359
- return new Promise((resolve, reject) => {
360
- const req = https.request(
361
- url,
362
- {
363
- method: "POST",
364
- headers,
365
- },
366
- (res) => {
367
- let data = "";
368
- res.on("data", (chunk) => (data += chunk));
369
- res.on("end", () => {
370
- if (debug) {
371
- // eslint-disable-next-line no-console
372
- console.log(`Debug: Response status: ${res.statusCode}`);
373
- // eslint-disable-next-line no-console
374
- console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
375
- }
376
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
377
- try {
378
- const parsed = JSON.parse(data) as IssueComment;
379
- resolve(parsed);
380
- } catch {
381
- reject(new Error(`Failed to parse create comment response: ${data}`));
382
- }
383
- } else {
384
- let errMsg = `Failed to create issue comment: HTTP ${res.statusCode}`;
385
- if (data) {
386
- try {
387
- const errObj = JSON.parse(data);
388
- errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
389
- } catch {
390
- errMsg += `\n${data}`;
391
- }
392
- }
393
- reject(new Error(errMsg));
394
- }
395
- });
452
+ const response = await fetch(url.toString(), {
453
+ method: "POST",
454
+ headers,
455
+ body,
456
+ });
457
+
458
+ if (debug) {
459
+ console.log(`Debug: Response status: ${response.status}`);
460
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
461
+ }
462
+
463
+ const data = await response.text();
464
+
465
+ if (response.ok) {
466
+ try {
467
+ return JSON.parse(data) as IssueComment;
468
+ } catch {
469
+ throw new Error(`Failed to parse create comment response: ${data}`);
470
+ }
471
+ } else {
472
+ throw new Error(formatHttpError("Failed to create issue comment", response.status, data));
473
+ }
474
+ }
475
+
476
+ export interface UpdateIssueParams {
477
+ apiKey: string;
478
+ apiBaseUrl: string;
479
+ issueId: string;
480
+ title?: string;
481
+ description?: string;
482
+ status?: number;
483
+ labels?: string[];
484
+ debug?: boolean;
485
+ }
486
+
487
+ export interface UpdatedIssue {
488
+ id: string;
489
+ title: string;
490
+ description: string | null;
491
+ status: number;
492
+ updated_at: string;
493
+ labels: string[] | null;
494
+ }
495
+
496
+ /**
497
+ * Update an existing issue in the PostgresAI platform.
498
+ *
499
+ * @param params - The parameters for updating an issue
500
+ * @param params.apiKey - API key for authentication
501
+ * @param params.apiBaseUrl - Base URL for the API
502
+ * @param params.issueId - ID of the issue to update (required)
503
+ * @param params.title - New title (optional)
504
+ * @param params.description - New description (optional)
505
+ * @param params.status - New status: 0 = open, 1 = closed (optional)
506
+ * @param params.labels - New labels array (optional, replaces existing)
507
+ * @param params.debug - Enable debug logging
508
+ * @returns The updated issue object
509
+ * @throws Error if API key or issueId is missing, if no fields to update are provided, or if the API call fails
510
+ */
511
+ export async function updateIssue(params: UpdateIssueParams): Promise<UpdatedIssue> {
512
+ const { apiKey, apiBaseUrl, issueId, title, description, status, labels, debug } = params;
513
+ if (!apiKey) {
514
+ throw new Error("API key is required");
515
+ }
516
+ if (!issueId) {
517
+ throw new Error("issueId is required");
518
+ }
519
+ if (title === undefined && description === undefined && status === undefined && labels === undefined) {
520
+ throw new Error("At least one field to update is required (title, description, status, or labels)");
521
+ }
522
+
523
+ const base = normalizeBaseUrl(apiBaseUrl);
524
+ const url = new URL(`${base}/rpc/issue_update`);
525
+
526
+ // Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
527
+ const bodyObj: Record<string, unknown> = {
528
+ p_id: issueId,
529
+ };
530
+ if (title !== undefined) {
531
+ bodyObj.p_title = title;
532
+ }
533
+ if (description !== undefined) {
534
+ bodyObj.p_description = description;
535
+ }
536
+ if (status !== undefined) {
537
+ bodyObj.p_status = status;
538
+ }
539
+ if (labels !== undefined) {
540
+ bodyObj.p_labels = labels;
541
+ }
542
+ const body = JSON.stringify(bodyObj);
543
+
544
+ const headers: Record<string, string> = {
545
+ "access-token": apiKey,
546
+ "Prefer": "return=representation",
547
+ "Content-Type": "application/json",
548
+ "Connection": "close",
549
+ };
550
+
551
+ if (debug) {
552
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
553
+ console.log(`Debug: Resolved API base URL: ${base}`);
554
+ console.log(`Debug: POST URL: ${url.toString()}`);
555
+ console.log(`Debug: Auth scheme: access-token`);
556
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
557
+ console.log(`Debug: Request body: ${body}`);
558
+ }
559
+
560
+ const response = await fetch(url.toString(), {
561
+ method: "POST",
562
+ headers,
563
+ body,
564
+ });
565
+
566
+ if (debug) {
567
+ console.log(`Debug: Response status: ${response.status}`);
568
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
569
+ }
570
+
571
+ const data = await response.text();
572
+
573
+ if (response.ok) {
574
+ try {
575
+ return JSON.parse(data) as UpdatedIssue;
576
+ } catch {
577
+ throw new Error(`Failed to parse update issue response: ${data}`);
578
+ }
579
+ } else {
580
+ throw new Error(formatHttpError("Failed to update issue", response.status, data));
581
+ }
582
+ }
583
+
584
+ export interface UpdateIssueCommentParams {
585
+ apiKey: string;
586
+ apiBaseUrl: string;
587
+ commentId: string;
588
+ content: string;
589
+ debug?: boolean;
590
+ }
591
+
592
+ export interface UpdatedIssueComment {
593
+ id: string;
594
+ issue_id: string;
595
+ content: string;
596
+ updated_at: string;
597
+ }
598
+
599
+ /**
600
+ * Update an existing issue comment in the PostgresAI platform.
601
+ *
602
+ * @param params - The parameters for updating a comment
603
+ * @param params.apiKey - API key for authentication
604
+ * @param params.apiBaseUrl - Base URL for the API
605
+ * @param params.commentId - ID of the comment to update (required)
606
+ * @param params.content - New comment content (required)
607
+ * @param params.debug - Enable debug logging
608
+ * @returns The updated comment object
609
+ * @throws Error if API key, commentId, or content is missing, or if the API call fails
610
+ */
611
+ export async function updateIssueComment(params: UpdateIssueCommentParams): Promise<UpdatedIssueComment> {
612
+ const { apiKey, apiBaseUrl, commentId, content, debug } = params;
613
+ if (!apiKey) {
614
+ throw new Error("API key is required");
615
+ }
616
+ if (!commentId) {
617
+ throw new Error("commentId is required");
618
+ }
619
+ if (!content) {
620
+ throw new Error("content is required");
621
+ }
622
+
623
+ const base = normalizeBaseUrl(apiBaseUrl);
624
+ const url = new URL(`${base}/rpc/issue_comment_update`);
625
+
626
+ const bodyObj: Record<string, unknown> = {
627
+ // Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
628
+ p_id: commentId,
629
+ p_content: content,
630
+ };
631
+ const body = JSON.stringify(bodyObj);
632
+
633
+ const headers: Record<string, string> = {
634
+ "access-token": apiKey,
635
+ "Prefer": "return=representation",
636
+ "Content-Type": "application/json",
637
+ "Connection": "close",
638
+ };
639
+
640
+ if (debug) {
641
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
642
+ console.log(`Debug: Resolved API base URL: ${base}`);
643
+ console.log(`Debug: POST URL: ${url.toString()}`);
644
+ console.log(`Debug: Auth scheme: access-token`);
645
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
646
+ console.log(`Debug: Request body: ${body}`);
647
+ }
648
+
649
+ const response = await fetch(url.toString(), {
650
+ method: "POST",
651
+ headers,
652
+ body,
653
+ });
654
+
655
+ if (debug) {
656
+ console.log(`Debug: Response status: ${response.status}`);
657
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
658
+ }
659
+
660
+ const data = await response.text();
661
+
662
+ if (response.ok) {
663
+ try {
664
+ return JSON.parse(data) as UpdatedIssueComment;
665
+ } catch {
666
+ throw new Error(`Failed to parse update comment response: ${data}`);
667
+ }
668
+ } else {
669
+ throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
670
+ }
671
+ }
672
+
673
+ // ============================================================================
674
+ // Action Items API Functions
675
+ // ============================================================================
676
+
677
+ export interface FetchActionItemParams {
678
+ apiKey: string;
679
+ apiBaseUrl: string;
680
+ actionItemIds: string | string[];
681
+ debug?: boolean;
682
+ }
683
+
684
+ /**
685
+ * Fetch action item(s) by ID(s).
686
+ * Supports single ID or array of IDs.
687
+ *
688
+ * @param params - Fetch parameters
689
+ * @param params.apiKey - API authentication key
690
+ * @param params.apiBaseUrl - Base URL for the API
691
+ * @param params.actionItemIds - Single action item ID or array of IDs (UUIDs)
692
+ * @param params.debug - Enable debug logging
693
+ * @returns Array of action items matching the provided IDs
694
+ * @throws Error if API key is missing or no valid IDs provided
695
+ *
696
+ * @example
697
+ * // Fetch single action item
698
+ * const items = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds: "uuid-123" });
699
+ *
700
+ * @example
701
+ * // Fetch multiple action items
702
+ * const items = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds: ["uuid-1", "uuid-2"] });
703
+ */
704
+ export async function fetchActionItem(params: FetchActionItemParams): Promise<IssueActionItem[]> {
705
+ const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
706
+ if (!apiKey) {
707
+ throw new Error("API key is required");
708
+ }
709
+ // UUID format pattern for validation
710
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
711
+ // Normalize to array, filter out null/undefined, trim, and validate UUID format
712
+ const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
713
+ const validIds = rawIds
714
+ .filter((id): id is string => id != null && typeof id === "string")
715
+ .map(id => id.trim())
716
+ .filter(id => id.length > 0 && uuidPattern.test(id));
717
+ if (validIds.length === 0) {
718
+ throw new Error("actionItemId is required and must be a valid UUID");
719
+ }
720
+
721
+ const base = normalizeBaseUrl(apiBaseUrl);
722
+ const url = new URL(`${base}/issue_action_items`);
723
+ if (validIds.length === 1) {
724
+ url.searchParams.set("id", `eq.${validIds[0]}`);
725
+ } else {
726
+ // PostgREST IN syntax: id=in.(val1,val2,val3)
727
+ url.searchParams.set("id", `in.(${validIds.join(",")})`)
728
+ }
729
+
730
+ const headers: Record<string, string> = {
731
+ "access-token": apiKey,
732
+ "Prefer": "return=representation",
733
+ "Content-Type": "application/json",
734
+ "Connection": "close",
735
+ };
736
+
737
+ if (debug) {
738
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
739
+ console.log(`Debug: Resolved API base URL: ${base}`);
740
+ console.log(`Debug: GET URL: ${url.toString()}`);
741
+ console.log(`Debug: Auth scheme: access-token`);
742
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
743
+ }
744
+
745
+ const response = await fetch(url.toString(), {
746
+ method: "GET",
747
+ headers,
748
+ });
749
+
750
+ if (debug) {
751
+ console.log(`Debug: Response status: ${response.status}`);
752
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
753
+ }
754
+
755
+ const data = await response.text();
756
+
757
+ if (response.ok) {
758
+ try {
759
+ const parsed = JSON.parse(data);
760
+ if (Array.isArray(parsed)) {
761
+ return parsed as IssueActionItem[];
396
762
  }
397
- );
763
+ return parsed ? [parsed as IssueActionItem] : [];
764
+ } catch {
765
+ throw new Error(`Failed to parse action item response: ${data}`);
766
+ }
767
+ } else {
768
+ throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
769
+ }
770
+ }
771
+
772
+ export interface FetchActionItemsParams {
773
+ apiKey: string;
774
+ apiBaseUrl: string;
775
+ issueId: string;
776
+ debug?: boolean;
777
+ }
778
+
779
+ /**
780
+ * Fetch all action items for an issue.
781
+ *
782
+ * @param params - Fetch parameters
783
+ * @param params.apiKey - API authentication key
784
+ * @param params.apiBaseUrl - Base URL for the API
785
+ * @param params.issueId - Issue ID (UUID) to fetch action items for
786
+ * @param params.debug - Enable debug logging
787
+ * @returns Array of action items for the specified issue
788
+ * @throws Error if API key or issue ID is missing
789
+ */
790
+ export async function fetchActionItems(params: FetchActionItemsParams): Promise<IssueActionItem[]> {
791
+ const { apiKey, apiBaseUrl, issueId, debug } = params;
792
+ if (!apiKey) {
793
+ throw new Error("API key is required");
794
+ }
795
+ if (!issueId) {
796
+ throw new Error("issueId is required");
797
+ }
798
+ // Validate UUID format to prevent PostgREST injection
799
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
800
+ if (!uuidPattern.test(issueId.trim())) {
801
+ throw new Error("issueId must be a valid UUID");
802
+ }
803
+
804
+ const base = normalizeBaseUrl(apiBaseUrl);
805
+ const url = new URL(`${base}/issue_action_items`);
806
+ url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
807
+
808
+ const headers: Record<string, string> = {
809
+ "access-token": apiKey,
810
+ "Prefer": "return=representation",
811
+ "Content-Type": "application/json",
812
+ "Connection": "close",
813
+ };
814
+
815
+ if (debug) {
816
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
817
+ console.log(`Debug: Resolved API base URL: ${base}`);
818
+ console.log(`Debug: GET URL: ${url.toString()}`);
819
+ console.log(`Debug: Auth scheme: access-token`);
820
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
821
+ }
822
+
823
+ const response = await fetch(url.toString(), {
824
+ method: "GET",
825
+ headers,
826
+ });
827
+
828
+ if (debug) {
829
+ console.log(`Debug: Response status: ${response.status}`);
830
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
831
+ }
832
+
833
+ const data = await response.text();
834
+
835
+ if (response.ok) {
836
+ try {
837
+ return JSON.parse(data) as IssueActionItem[];
838
+ } catch {
839
+ throw new Error(`Failed to parse action items response: ${data}`);
840
+ }
841
+ } else {
842
+ throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
843
+ }
844
+ }
845
+
846
+ export interface CreateActionItemParams {
847
+ apiKey: string;
848
+ apiBaseUrl: string;
849
+ issueId: string;
850
+ title: string;
851
+ description?: string;
852
+ sqlAction?: string;
853
+ configs?: ConfigChange[];
854
+ debug?: boolean;
855
+ }
856
+
857
+ /**
858
+ * Create a new action item for an issue.
859
+ *
860
+ * @param params - Creation parameters
861
+ * @param params.apiKey - API authentication key
862
+ * @param params.apiBaseUrl - Base URL for the API
863
+ * @param params.issueId - Issue ID (UUID) to create action item for
864
+ * @param params.title - Action item title
865
+ * @param params.description - Optional detailed description
866
+ * @param params.sqlAction - Optional SQL command to execute
867
+ * @param params.configs - Optional configuration parameter changes
868
+ * @param params.debug - Enable debug logging
869
+ * @returns Created action item ID
870
+ * @throws Error if required fields are missing or API call fails
871
+ */
872
+ export async function createActionItem(params: CreateActionItemParams): Promise<string> {
873
+ const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
874
+ if (!apiKey) {
875
+ throw new Error("API key is required");
876
+ }
877
+ if (!issueId) {
878
+ throw new Error("issueId is required");
879
+ }
880
+ // Validate UUID format
881
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
882
+ if (!uuidPattern.test(issueId.trim())) {
883
+ throw new Error("issueId must be a valid UUID");
884
+ }
885
+ if (!title) {
886
+ throw new Error("title is required");
887
+ }
888
+
889
+ const base = normalizeBaseUrl(apiBaseUrl);
890
+ const url = new URL(`${base}/rpc/issue_action_item_create`);
891
+
892
+ const bodyObj: Record<string, unknown> = {
893
+ issue_id: issueId,
894
+ title: title,
895
+ };
896
+ if (description !== undefined) {
897
+ bodyObj.description = description;
898
+ }
899
+ if (sqlAction !== undefined) {
900
+ bodyObj.sql_action = sqlAction;
901
+ }
902
+ if (configs !== undefined) {
903
+ bodyObj.configs = configs;
904
+ }
905
+ const body = JSON.stringify(bodyObj);
906
+
907
+ const headers: Record<string, string> = {
908
+ "access-token": apiKey,
909
+ "Prefer": "return=representation",
910
+ "Content-Type": "application/json",
911
+ "Connection": "close",
912
+ };
398
913
 
399
- req.on("error", (err: Error) => reject(err));
400
- req.write(body);
401
- req.end();
914
+ if (debug) {
915
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
916
+ console.log(`Debug: Resolved API base URL: ${base}`);
917
+ console.log(`Debug: POST URL: ${url.toString()}`);
918
+ console.log(`Debug: Auth scheme: access-token`);
919
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
920
+ console.log(`Debug: Request body: ${body}`);
921
+ }
922
+
923
+ const response = await fetch(url.toString(), {
924
+ method: "POST",
925
+ headers,
926
+ body,
402
927
  });
928
+
929
+ if (debug) {
930
+ console.log(`Debug: Response status: ${response.status}`);
931
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
932
+ }
933
+
934
+ const data = await response.text();
935
+
936
+ if (response.ok) {
937
+ try {
938
+ return JSON.parse(data) as string;
939
+ } catch {
940
+ throw new Error(`Failed to parse create action item response: ${data}`);
941
+ }
942
+ } else {
943
+ throw new Error(formatHttpError("Failed to create action item", response.status, data));
944
+ }
945
+ }
946
+
947
+ export interface UpdateActionItemParams {
948
+ apiKey: string;
949
+ apiBaseUrl: string;
950
+ actionItemId: string;
951
+ title?: string;
952
+ description?: string;
953
+ isDone?: boolean;
954
+ status?: string;
955
+ statusReason?: string;
956
+ sqlAction?: string;
957
+ configs?: ConfigChange[];
958
+ debug?: boolean;
403
959
  }
404
960
 
961
+ /**
962
+ * Update an existing action item.
963
+ *
964
+ * @param params - Update parameters
965
+ * @param params.apiKey - API authentication key
966
+ * @param params.apiBaseUrl - Base URL for the API
967
+ * @param params.actionItemId - Action item ID (UUID) to update
968
+ * @param params.title - New title
969
+ * @param params.description - New description
970
+ * @param params.isDone - Mark as done/not done
971
+ * @param params.status - Approval status: 'waiting_for_approval', 'approved', 'rejected'
972
+ * @param params.statusReason - Reason for status change
973
+ * @param params.sqlAction - SQL command to execute
974
+ * @param params.configs - Configuration parameter changes
975
+ * @param params.debug - Enable debug logging
976
+ * @throws Error if required fields missing or no update fields provided
977
+ */
978
+ export async function updateActionItem(params: UpdateActionItemParams): Promise<void> {
979
+ const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
980
+ if (!apiKey) {
981
+ throw new Error("API key is required");
982
+ }
983
+ if (!actionItemId) {
984
+ throw new Error("actionItemId is required");
985
+ }
986
+ // Validate UUID format
987
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
988
+ if (!uuidPattern.test(actionItemId.trim())) {
989
+ throw new Error("actionItemId must be a valid UUID");
990
+ }
991
+
992
+ // Check that at least one update field is provided
993
+ const hasUpdateField = title !== undefined || description !== undefined ||
994
+ isDone !== undefined || status !== undefined ||
995
+ statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
996
+ if (!hasUpdateField) {
997
+ throw new Error("At least one field to update is required");
998
+ }
999
+
1000
+ const base = normalizeBaseUrl(apiBaseUrl);
1001
+ const url = new URL(`${base}/rpc/issue_action_item_update`);
1002
+
1003
+ const bodyObj: Record<string, unknown> = {
1004
+ action_item_id: actionItemId,
1005
+ };
1006
+ if (title !== undefined) {
1007
+ bodyObj.title = title;
1008
+ }
1009
+ if (description !== undefined) {
1010
+ bodyObj.description = description;
1011
+ }
1012
+ if (isDone !== undefined) {
1013
+ bodyObj.is_done = isDone;
1014
+ }
1015
+ if (status !== undefined) {
1016
+ bodyObj.status = status;
1017
+ }
1018
+ if (statusReason !== undefined) {
1019
+ bodyObj.status_reason = statusReason;
1020
+ }
1021
+ if (sqlAction !== undefined) {
1022
+ bodyObj.sql_action = sqlAction;
1023
+ }
1024
+ if (configs !== undefined) {
1025
+ bodyObj.configs = configs;
1026
+ }
1027
+ const body = JSON.stringify(bodyObj);
1028
+
1029
+ const headers: Record<string, string> = {
1030
+ "access-token": apiKey,
1031
+ "Prefer": "return=representation",
1032
+ "Content-Type": "application/json",
1033
+ "Connection": "close",
1034
+ };
405
1035
 
1036
+ if (debug) {
1037
+ const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
1038
+ console.log(`Debug: Resolved API base URL: ${base}`);
1039
+ console.log(`Debug: POST URL: ${url.toString()}`);
1040
+ console.log(`Debug: Auth scheme: access-token`);
1041
+ console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
1042
+ console.log(`Debug: Request body: ${body}`);
1043
+ }
1044
+
1045
+ const response = await fetch(url.toString(), {
1046
+ method: "POST",
1047
+ headers,
1048
+ body,
1049
+ });
1050
+
1051
+ if (debug) {
1052
+ console.log(`Debug: Response status: ${response.status}`);
1053
+ console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
1054
+ }
1055
+
1056
+ if (!response.ok) {
1057
+ const data = await response.text();
1058
+ throw new Error(formatHttpError("Failed to update action item", response.status, data));
1059
+ }
1060
+ }