postgresai 0.17.0-rc.4 → 0.17.0

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.
@@ -13466,8 +13466,11 @@ function resolveBaseUrls2(opts, cfg, defaults = {}) {
13466
13466
  storageBaseUrl: normalizeBaseUrl2(storageCandidate)
13467
13467
  };
13468
13468
  }
13469
- var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000;
13470
- var init_util = () => {};
13469
+ var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, SENSITIVE_KEY_SRC2 = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)", SENSITIVE_PAIR_SEPARATOR_SRC2 = `["']?\\s*[:=]`, CREDENTIAL_CHAR_SRC2 = "[A-Za-z0-9_+/=~.-]", TEXT_SENSITIVE_PAIR2, CREDENTIAL_RUN_ONLY2;
13470
+ var init_util = __esm(() => {
13471
+ TEXT_SENSITIVE_PAIR2 = new RegExp(`(${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2})[^\\s,;&]+)*)` + `)`, "gi");
13472
+ CREDENTIAL_RUN_ONLY2 = new RegExp(`^${CREDENTIAL_CHAR_SRC2}+$`);
13473
+ });
13471
13474
 
13472
13475
  // node_modules/commander/index.js
13473
13476
  var exports_commander = {};
@@ -13530,7 +13533,7 @@ var {
13530
13533
  // package.json
13531
13534
  var package_default = {
13532
13535
  name: "postgresai",
13533
- version: "0.17.0-rc.4",
13536
+ version: "0.17.0",
13534
13537
  description: "postgres_ai CLI",
13535
13538
  license: "Apache-2.0",
13536
13539
  private: false,
@@ -16890,7 +16893,7 @@ var Result = import_lib.default.Result;
16890
16893
  var TypeOverrides = import_lib.default.TypeOverrides;
16891
16894
  var defaults = import_lib.default.defaults;
16892
16895
  // package.json
16893
- var version = "0.17.0-rc.4";
16896
+ var version = "0.17.0";
16894
16897
  var package_default2 = {
16895
16898
  name: "postgresai",
16896
16899
  version,
@@ -17166,6 +17169,22 @@ class HttpStatusError extends Error {
17166
17169
  function isRetryableHttpStatus(status) {
17167
17170
  return status >= 500 || status === 429;
17168
17171
  }
17172
+ function isUnknownRpcSignature(status, text, rpc) {
17173
+ if (status !== 404)
17174
+ return false;
17175
+ try {
17176
+ const parsed = JSON.parse(text);
17177
+ return typeof parsed.message === "string" && /Could not find the .* function/.test(parsed.message) && parsed.message.includes(`${rpc}(`);
17178
+ } catch {
17179
+ return false;
17180
+ }
17181
+ }
17182
+ function positiveSeconds(v) {
17183
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
17184
+ }
17185
+ function asyncPollDelayMs(attempt) {
17186
+ return Math.min(1000 * 2 ** attempt, 15000);
17187
+ }
17169
17188
  var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
17170
17189
 
17171
17190
  class HttpRequestTimeoutError extends Error {
@@ -17219,9 +17238,104 @@ function redactSecretsForLog(text) {
17219
17238
  return JSON.stringify(redactSecrets(parsed));
17220
17239
  }
17221
17240
  var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
17222
- var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
17241
+ var SENSITIVE_KEY_SRC = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)";
17242
+ var SENSITIVE_PAIR_SEPARATOR_SRC = `["']?\\s*[:=]`;
17243
+ var CREDENTIAL_CHAR_SRC = "[A-Za-z0-9_+/=~.-]";
17244
+ var MIN_CREDENTIAL_RUN = 12;
17245
+ var TEXT_SENSITIVE_PAIR = new RegExp(`(${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC})[^\\s,;&]+)*)` + `)`, "gi");
17246
+ var CREDENTIAL_RUN_ONLY = new RegExp(`^${CREDENTIAL_CHAR_SRC}+$`);
17247
+ var RUN_LEADING_PUNCTUATION = /^[("'\[{<]+/;
17248
+ var RUN_TRAILING_PUNCTUATION = /[)"'\]}>!?.,;:]+$/;
17249
+ var LONG_CONSONANT_RUN = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]{5,}/;
17250
+ var UUID_SHAPE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
17251
+ function isWordShaped(s) {
17252
+ if (!/^[A-Za-z]+$/.test(s))
17253
+ return false;
17254
+ if (s.length <= 2)
17255
+ return true;
17256
+ if (hasInternalCapital(s))
17257
+ return false;
17258
+ if (!/[aeiouyAEIOUY]/.test(s))
17259
+ return false;
17260
+ return !LONG_CONSONANT_RUN.test(s);
17261
+ }
17262
+ function hasInternalCapital(s) {
17263
+ return /[a-z]/.test(s) && /.[A-Z]/.test(s);
17264
+ }
17265
+ function hasHardStructure(core) {
17266
+ if (/=/.test(core.replace(/={1,2}$/, "")))
17267
+ return true;
17268
+ return UUID_SHAPE.test(core);
17269
+ }
17270
+ function isSeparatedName(core) {
17271
+ const segments = core.split(/[/_.-]/).filter((part) => part.length > 0);
17272
+ if (segments.length < 2)
17273
+ return false;
17274
+ if (!segments.some(isNameSegment))
17275
+ return false;
17276
+ return segments.every((part) => isNameSegment(part) || /^[0-9]{1,4}$/.test(part));
17277
+ }
17278
+ function isNameSegment(part) {
17279
+ return isWordShaped(part) || /^[A-Z]+$/.test(part) && part.length <= 6;
17280
+ }
17281
+ function hasTokenSignal(core) {
17282
+ if (/[0-9_+]/.test(core))
17283
+ return true;
17284
+ if (/=$/.test(core))
17285
+ return true;
17286
+ if (hasInternalCapital(core))
17287
+ return true;
17288
+ return LONG_CONSONANT_RUN.test(core);
17289
+ }
17290
+ function runCore(run) {
17291
+ return run.replace(RUN_LEADING_PUNCTUATION, "").replace(RUN_TRAILING_PUNCTUATION, "");
17292
+ }
17293
+ function looksLikeCredentialRun(core) {
17294
+ return core.length >= MIN_CREDENTIAL_RUN && !hasHardStructure(core) && hasTokenSignal(core);
17295
+ }
17296
+ function isChainableFragment(core) {
17297
+ return core.length > 0 && CREDENTIAL_RUN_ONLY.test(core) && !hasHardStructure(core) && !isWordShaped(core);
17298
+ }
17299
+ function replaceSensitivePair(_match, keyPrefix, quotedValue, _bareValue, tail) {
17300
+ if (quotedValue !== undefined)
17301
+ return `${keyPrefix}[REDACTED]`;
17302
+ const runs = [...(tail ?? "").matchAll(/(\s+)(\S+)/g)].map((m) => {
17303
+ const run = m[2] ?? "";
17304
+ return { gap: m[1] ?? "", run, core: runCore(run) };
17305
+ });
17306
+ const absorbed = runs.map(() => false);
17307
+ for (let i = 0;i < runs.length; ) {
17308
+ if (!isChainableFragment(runs[i].core)) {
17309
+ i++;
17310
+ continue;
17311
+ }
17312
+ let end = i;
17313
+ while (end < runs.length && isChainableFragment(runs[end].core))
17314
+ end++;
17315
+ const members = runs.slice(i, end);
17316
+ const chain = members.map((r) => r.core).join("");
17317
+ const allNames = members.every((r) => isSeparatedName(r.core));
17318
+ if (!allNames && looksLikeCredentialRun(chain))
17319
+ for (let k = i;k < end; k++)
17320
+ absorbed[k] = true;
17321
+ i = end;
17322
+ }
17323
+ let kept = "";
17324
+ for (let k = 0;k < runs.length; k++) {
17325
+ const r = runs[k];
17326
+ if (!absorbed[k]) {
17327
+ kept += r.gap + r.run;
17328
+ continue;
17329
+ }
17330
+ const lead = r.run.match(RUN_LEADING_PUNCTUATION)?.[0] ?? "";
17331
+ const trail = r.run.match(RUN_TRAILING_PUNCTUATION)?.[0] ?? "";
17332
+ if (lead || trail)
17333
+ kept += r.gap + lead + trail;
17334
+ }
17335
+ return `${keyPrefix}[REDACTED]${kept}`;
17336
+ }
17223
17337
  function redactTextSecrets(text) {
17224
- return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
17338
+ return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, replaceSensitivePair);
17225
17339
  }
17226
17340
  function isControlCodePoint(cp) {
17227
17341
  return cp < 32 || cp === 127 || cp >= 128 && cp <= 159 || cp === 8232 || cp === 8233;
@@ -26473,68 +26587,103 @@ function renderMarkdownForTerminal(md) {
26473
26587
  // lib/joe.ts
26474
26588
  var DEFAULT_BUDGET_MS = 25000;
26475
26589
  var DEFAULT_POLL_INTERVAL_MS = 800;
26590
+ var JOE_ASYNC_FALLBACK_WAIT_MS = 10 * 60 * 1000;
26591
+ var JOE_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
26592
+ var JOE_ASYNC_MAX_POLL_FAILURES = 5;
26476
26593
  async function callRpc(params) {
26594
+ return (await callRpcRaw(params)).data;
26595
+ }
26596
+ async function callRpcRaw(params) {
26477
26597
  const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26478
26598
  if (!apiKey) {
26479
26599
  throw new Error("API key is required");
26480
26600
  }
26481
26601
  const base = normalizeBaseUrl(apiBaseUrl);
26482
26602
  const url = new URL(`${base}/rpc/${fn}`);
26483
- const payload = JSON.stringify(body);
26484
26603
  const headers = buildAuthHeaders(apiKey);
26485
- if (debug) {
26486
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26487
- console.error(`Debug: POST URL: ${url.toString()}`);
26488
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26489
- console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26490
- }
26491
- let response;
26492
- const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26493
- try {
26494
- response = await fetch(url.toString(), {
26495
- method: "POST",
26496
- headers,
26497
- body: payload,
26498
- signal: requestTimeout.signal
26499
- });
26500
- } catch (err) {
26501
- if (isFetchTimeout(err)) {
26502
- throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26604
+ const send = async (bodyObj) => {
26605
+ const payload = JSON.stringify(bodyObj);
26606
+ if (debug) {
26607
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26608
+ console.error(`Debug: POST URL: ${url.toString()}`);
26609
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26610
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26611
+ }
26612
+ let response;
26613
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26614
+ try {
26615
+ response = await fetch(url.toString(), {
26616
+ method: "POST",
26617
+ headers,
26618
+ body: payload,
26619
+ signal: requestTimeout.signal
26620
+ });
26621
+ } catch (err) {
26622
+ if (isFetchTimeout(err)) {
26623
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26624
+ }
26625
+ throw new Error(describeFetchError(operation, base, err));
26503
26626
  }
26504
- throw new Error(describeFetchError(operation, base, err));
26505
- }
26506
- const text = await response.text();
26507
- if (debug) {
26508
- console.error(`Debug: Response status: ${response.status}`);
26509
- console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26627
+ const text = await response.text();
26628
+ if (debug) {
26629
+ console.error(`Debug: Response status: ${response.status}`);
26630
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26631
+ }
26632
+ return { response, text };
26633
+ };
26634
+ let { response, text } = await send(body);
26635
+ if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
26636
+ ({ response, text } = await send(params.fallbackBody));
26510
26637
  }
26511
26638
  if (!response.ok) {
26512
26639
  throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26513
26640
  }
26514
26641
  try {
26515
- return JSON.parse(text);
26642
+ return { data: JSON.parse(text), status: response.status };
26516
26643
  } catch {
26517
26644
  throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26518
26645
  }
26519
26646
  }
26520
- async function startCommand(params) {
26647
+ function joeAsyncWaitMs(handle) {
26648
+ const expires = positiveSeconds(handle.expires_in_s);
26649
+ return Math.min(expires ? expires * 1000 : JOE_ASYNC_FALLBACK_WAIT_MS, JOE_ASYNC_MAX_WAIT_MS);
26650
+ }
26651
+ function asyncHandleCommandId(reply, operation) {
26652
+ const commandId = reply?.command_id;
26653
+ if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
26654
+ throw new Error(`${operation}: the platform accepted the command over the job channel but ` + `returned no command id: ${redactSecretsForLog(JSON.stringify(reply))}`);
26655
+ }
26656
+ return commandId;
26657
+ }
26658
+ async function startCommandDetailed(params) {
26521
26659
  const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
26522
26660
  if (!String(command ?? "").trim()) {
26523
26661
  throw new Error("command text is required");
26524
26662
  }
26525
- const commandId = await callRpc({
26663
+ const operation = "Failed to run Joe command";
26664
+ const body = { instance_id: instanceId, command };
26665
+ const { data, status } = await callRpcRaw({
26526
26666
  apiKey,
26527
26667
  apiBaseUrl,
26528
26668
  fn: "joe_command_run",
26529
- body: { instance_id: instanceId, command },
26530
- operation: "Failed to run Joe command",
26669
+ body: { ...body, accept_async: true },
26670
+ fallbackBody: body,
26671
+ operation,
26531
26672
  debug,
26532
26673
  timeoutMs
26533
26674
  });
26534
- if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
26535
- throw new Error(`Failed to run Joe command: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(commandId))}`);
26675
+ if (status === 202) {
26676
+ const handle = data ?? {};
26677
+ return {
26678
+ commandId: asyncHandleCommandId(data, operation),
26679
+ asyncWaitMs: joeAsyncWaitMs(handle),
26680
+ firstAnswerEstimateS: positiveSeconds(handle.first_answer_estimate_s)
26681
+ };
26536
26682
  }
26537
- return commandId;
26683
+ if (typeof data !== "string" || !/^[0-9]+$/.test(data)) {
26684
+ throw new Error(`${operation}: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(data))}`);
26685
+ }
26686
+ return { commandId: data, asyncWaitMs: null, firstAnswerEstimateS: null };
26538
26687
  }
26539
26688
  async function getCommandOutput(params) {
26540
26689
  const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
@@ -26565,7 +26714,10 @@ function normalizeProjectRow(row) {
26565
26714
  joe_ready: Boolean(row.joe_ready ?? false),
26566
26715
  tunnel: Boolean(row.tunnel ?? false),
26567
26716
  instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
26568
- dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
26717
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id),
26718
+ ...row.monitoring_instance_ids !== undefined && {
26719
+ monitoring_instance_ids: row.monitoring_instance_ids ?? []
26720
+ }
26569
26721
  };
26570
26722
  }
26571
26723
  async function listProjects(params) {
@@ -26661,18 +26813,29 @@ function buildJoeCommandText(command, input = {}) {
26661
26813
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
26662
26814
  async function runCommand(params) {
26663
26815
  const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
26664
- const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : DEFAULT_BUDGET_MS;
26665
- const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
26816
+ const explicitBudgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : null;
26666
26817
  const now = params.now ?? Date.now;
26667
26818
  const sleep = params.sleep ?? defaultSleep;
26668
- const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
26819
+ const started = await startCommandDetailed({ apiKey, apiBaseUrl, instanceId, command, debug });
26820
+ const commandId = started.commandId;
26821
+ const budgetMs = explicitBudgetMs ?? started.asyncWaitMs ?? DEFAULT_BUDGET_MS;
26822
+ const pollDelayMs = (attempt) => params.pollIntervalMs ?? (started.asyncWaitMs === null ? DEFAULT_POLL_INTERVAL_MS : asyncPollDelayMs(attempt));
26669
26823
  const deadline = now() + budgetMs;
26670
26824
  let status = "pending";
26671
26825
  const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
26672
26826
  if (now() >= deadline) {
26673
- return { commandId, status, output: null, budgetExpired: true };
26827
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26674
26828
  }
26675
- for (;; ) {
26829
+ let announced = false;
26830
+ const announce = () => {
26831
+ if (announced || started.asyncWaitMs === null)
26832
+ return;
26833
+ announced = true;
26834
+ const estimate = started.firstAnswerEstimateS;
26835
+ console.error(`Queued on the Joe job channel (command ${commandId}` + (estimate ? `, reaches Joe in about ${estimate}s` : "") + `); waiting up to ${Math.round(budgetMs / 1000)}s...`);
26836
+ };
26837
+ let transientFailures = 0;
26838
+ for (let attempt = 0;; attempt++) {
26676
26839
  let output;
26677
26840
  try {
26678
26841
  output = await getCommandOutput({
@@ -26684,25 +26847,28 @@ async function runCommand(params) {
26684
26847
  });
26685
26848
  } catch (err) {
26686
26849
  if (err instanceof HttpRequestTimeoutError) {
26687
- return { commandId, status, output: null, budgetExpired: true };
26850
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26688
26851
  }
26689
26852
  if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
26690
- if (now() >= deadline) {
26691
- return { commandId, status, output: null, budgetExpired: true };
26853
+ transientFailures += 1;
26854
+ if (now() >= deadline || started.asyncWaitMs !== null && transientFailures >= JOE_ASYNC_MAX_POLL_FAILURES) {
26855
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26692
26856
  }
26693
- await sleep(pollIntervalMs);
26857
+ await sleep(pollDelayMs(attempt));
26694
26858
  continue;
26695
26859
  }
26696
26860
  throw err;
26697
26861
  }
26862
+ transientFailures = 0;
26698
26863
  status = output.status;
26699
26864
  if (status === "ok" || status === "error") {
26700
- return { commandId, status, output, budgetExpired: false };
26865
+ return { commandId, status, output, budgetExpired: false, budgetMs };
26701
26866
  }
26702
26867
  if (now() >= deadline) {
26703
- return { commandId, status, output: null, budgetExpired: true };
26868
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26704
26869
  }
26705
- await sleep(pollIntervalMs);
26870
+ announce();
26871
+ await sleep(pollDelayMs(attempt));
26706
26872
  }
26707
26873
  }
26708
26874
  async function executeJoeCommand(params) {
@@ -26813,46 +26979,58 @@ function formatProjectsTable(projects) {
26813
26979
  }
26814
26980
 
26815
26981
  // lib/joe.ts
26982
+ var JOE_ASYNC_FALLBACK_WAIT_MS2 = 10 * 60 * 1000;
26983
+ var JOE_ASYNC_MAX_WAIT_MS2 = 60 * 60 * 1000;
26816
26984
  async function callRpc2(params) {
26985
+ return (await callRpcRaw2(params)).data;
26986
+ }
26987
+ async function callRpcRaw2(params) {
26817
26988
  const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26818
26989
  if (!apiKey) {
26819
26990
  throw new Error("API key is required");
26820
26991
  }
26821
26992
  const base = normalizeBaseUrl(apiBaseUrl);
26822
26993
  const url = new URL(`${base}/rpc/${fn}`);
26823
- const payload = JSON.stringify(body);
26824
26994
  const headers = buildAuthHeaders(apiKey);
26825
- if (debug) {
26826
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26827
- console.error(`Debug: POST URL: ${url.toString()}`);
26828
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26829
- console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26830
- }
26831
- let response;
26832
- const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26833
- try {
26834
- response = await fetch(url.toString(), {
26835
- method: "POST",
26836
- headers,
26837
- body: payload,
26838
- signal: requestTimeout.signal
26839
- });
26840
- } catch (err) {
26841
- if (isFetchTimeout(err)) {
26842
- throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26995
+ const send = async (bodyObj) => {
26996
+ const payload = JSON.stringify(bodyObj);
26997
+ if (debug) {
26998
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26999
+ console.error(`Debug: POST URL: ${url.toString()}`);
27000
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27001
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
27002
+ }
27003
+ let response;
27004
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
27005
+ try {
27006
+ response = await fetch(url.toString(), {
27007
+ method: "POST",
27008
+ headers,
27009
+ body: payload,
27010
+ signal: requestTimeout.signal
27011
+ });
27012
+ } catch (err) {
27013
+ if (isFetchTimeout(err)) {
27014
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
27015
+ }
27016
+ throw new Error(describeFetchError(operation, base, err));
26843
27017
  }
26844
- throw new Error(describeFetchError(operation, base, err));
26845
- }
26846
- const text = await response.text();
26847
- if (debug) {
26848
- console.error(`Debug: Response status: ${response.status}`);
26849
- console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27018
+ const text = await response.text();
27019
+ if (debug) {
27020
+ console.error(`Debug: Response status: ${response.status}`);
27021
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27022
+ }
27023
+ return { response, text };
27024
+ };
27025
+ let { response, text } = await send(body);
27026
+ if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
27027
+ ({ response, text } = await send(params.fallbackBody));
26850
27028
  }
26851
27029
  if (!response.ok) {
26852
27030
  throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26853
27031
  }
26854
27032
  try {
26855
- return JSON.parse(text);
27033
+ return { data: JSON.parse(text), status: response.status };
26856
27034
  } catch {
26857
27035
  throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26858
27036
  }
@@ -26871,7 +27049,10 @@ function normalizeProjectRow2(row) {
26871
27049
  joe_ready: Boolean(row.joe_ready ?? false),
26872
27050
  tunnel: Boolean(row.tunnel ?? false),
26873
27051
  instance_id: row.instance_id == null ? null : preserveIntegerId2(row.instance_id),
26874
- dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id)
27052
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id),
27053
+ ...row.monitoring_instance_ids !== undefined && {
27054
+ monitoring_instance_ids: row.monitoring_instance_ids ?? []
27055
+ }
26875
27056
  };
26876
27057
  }
26877
27058
  async function listProjects2(params) {
@@ -26930,24 +27111,10 @@ async function resolveDblabInstanceId(params) {
26930
27111
  }
26931
27112
  return String(match.dblab_instance_id);
26932
27113
  }
26933
- async function callDblabApi(params) {
26934
- const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
26935
- if (!apiKey) {
26936
- throw new Error("API key is required");
26937
- }
26938
- if (!instanceId) {
26939
- throw new Error("instanceId is required");
26940
- }
27114
+ async function postDblabRpc(rpc, bodyObj, params) {
27115
+ const { apiKey, apiBaseUrl, operation, debug } = params;
26941
27116
  const base = normalizeBaseUrl(apiBaseUrl);
26942
- const url = new URL(`${base}/rpc/dblab_api_call`);
26943
- const bodyObj = {
26944
- instance_id: instanceId,
26945
- action,
26946
- method
26947
- };
26948
- if (data !== undefined) {
26949
- bodyObj.data = data;
26950
- }
27117
+ const url = new URL(`${base}/rpc/${rpc}`);
26951
27118
  const body = JSON.stringify(bodyObj);
26952
27119
  const headers = buildAuthHeaders(apiKey);
26953
27120
  if (debug) {
@@ -26971,13 +27138,163 @@ async function callDblabApi(params) {
26971
27138
  }
26972
27139
  throw new Error(describeFetchError(operation, base, err));
26973
27140
  }
26974
- const text = await response.text();
27141
+ let text;
27142
+ try {
27143
+ text = await response.text();
27144
+ } catch (err) {
27145
+ if (isFetchTimeout(err)) {
27146
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
27147
+ }
27148
+ throw new Error(describeFetchError(operation, base, err));
27149
+ }
26975
27150
  if (debug) {
26976
27151
  console.error(`Debug: Response status: ${response.status}`);
26977
27152
  console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26978
27153
  }
27154
+ return { response, text };
27155
+ }
27156
+ var DBLAB_ASYNC_FALLBACK_WAIT_MS = 15 * 60 * 1000;
27157
+ var DBLAB_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
27158
+ var DBLAB_POLL_MAX_CONSECUTIVE_FAILURES = 5;
27159
+ var DBLAB_JOB_PENDING = new Set(["queued", "running"]);
27160
+ function dblabMayHaveLandedAdvice() {
27161
+ return "The platform may have accepted this call even though the reply was lost — " + "check the instance's current state before running this command again.";
27162
+ }
27163
+ function dblabNotResentAdvice(jobId) {
27164
+ return `The call (job ${jobId}) was NOT re-sent and may still complete on the instance — ` + "check its current state (e.g. `pgai dblab clone list`) before running this command again.";
27165
+ }
27166
+ function stripControlsKeepingLines(text) {
27167
+ return text.split(`
27168
+ `).map(stripControlsToSpace).join(`
27169
+ `);
27170
+ }
27171
+ function scrubBoxText(text) {
27172
+ return stripControlsToSpace(redactTextSecrets(redactSecretsForLog(text))).replace(/\s+/g, " ").trim();
27173
+ }
27174
+ async function pollDblabCallOnce(jobIdRaw, jobId, params) {
27175
+ const { operation } = params;
27176
+ const advice = dblabNotResentAdvice(jobId);
27177
+ let response;
27178
+ let text;
27179
+ try {
27180
+ ({ response, text } = await postDblabRpc("dblab_call_result", { p_job_id: jobIdRaw }, params));
27181
+ } catch (err) {
27182
+ return { kind: "transient", why: err instanceof Error ? err.message : String(err) };
27183
+ }
26979
27184
  if (!response.ok) {
26980
- throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
27185
+ if (isRetryableHttpStatus(response.status)) {
27186
+ return { kind: "transient", why: `HTTP ${response.status}` };
27187
+ }
27188
+ throw new HttpStatusError(`${stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText))}
27189
+ ${advice}`, response.status);
27190
+ }
27191
+ let res;
27192
+ try {
27193
+ res = JSON.parse(text);
27194
+ } catch {
27195
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
27196
+ }
27197
+ if (!res || typeof res.status !== "string") {
27198
+ throw new Error(`${operation}: dblab_call_result returned an unexpected reply: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
27199
+ }
27200
+ return { kind: "result", res };
27201
+ }
27202
+ async function awaitDblabCall(handle, params) {
27203
+ const { operation } = params;
27204
+ const jobIdRaw = typeof handle.job_id === "string" ? handle.job_id : "";
27205
+ const jobId = jobIdRaw ? scrubBoxText(jobIdRaw) : "";
27206
+ if (!jobId) {
27207
+ throw new Error(`${operation}: the platform accepted the call but returned no job_id. ` + dblabMayHaveLandedAdvice());
27208
+ }
27209
+ const now = params.now ?? (() => Date.now());
27210
+ const sleep = params.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
27211
+ const waitMs = Math.min((positiveSeconds(handle.expires_in_s) ?? 0) * 1000 || DBLAB_ASYNC_FALLBACK_WAIT_MS, DBLAB_ASYNC_MAX_WAIT_MS);
27212
+ const deadline = now() + waitMs;
27213
+ const estimate = positiveSeconds(handle.first_answer_estimate_s);
27214
+ let announced = false;
27215
+ let failures = 0;
27216
+ let lastStatus = "unknown";
27217
+ for (let attempt = 0;; attempt++) {
27218
+ const outcome = await pollDblabCallOnce(jobIdRaw, jobId, params);
27219
+ if (outcome.kind === "transient") {
27220
+ failures += 1;
27221
+ if (failures >= DBLAB_POLL_MAX_CONSECUTIVE_FAILURES) {
27222
+ throw new Error(`${operation}: lost contact with the platform while waiting (${failures} polls in a row failed, ` + `last: ${outcome.why}). ${dblabNotResentAdvice(jobId)}`);
27223
+ }
27224
+ } else {
27225
+ failures = 0;
27226
+ const res = outcome.res;
27227
+ lastStatus = scrubBoxText(res.status);
27228
+ if (res.status === "done" && res.outcome === "ok") {
27229
+ return res.result ?? null;
27230
+ }
27231
+ if (!DBLAB_JOB_PENDING.has(res.status)) {
27232
+ const why = typeof res.error === "string" && res.error ? res.error : res.status === "expired" ? "no agent claimed it before it expired" : `outcome ${String(res.outcome ?? "unknown")}`;
27233
+ const klass = typeof res.failure_class === "string" && res.failure_class ? ` [${scrubBoxText(res.failure_class)}]` : "";
27234
+ const advice = res.status === "expired" ? "" : ` ${dblabNotResentAdvice(jobId)}`;
27235
+ throw new Error(`${operation}: the DBLab instance did not complete the call (status ${scrubBoxText(res.status)}) — ` + `${scrubBoxText(why)}${klass}.${advice}`);
27236
+ }
27237
+ }
27238
+ if (!announced && outcome.kind === "result") {
27239
+ announced = true;
27240
+ console.error(`Waiting for the DBLab instance to pick up the call (job ${jobId}` + (estimate ? `, typically ~${estimate}s` : "") + ")...");
27241
+ }
27242
+ const delay = asyncPollDelayMs(attempt);
27243
+ if (now() + delay >= deadline) {
27244
+ throw new Error(`${operation}: timed out waiting for the DBLab instance (job ${jobId}, last status ${lastStatus}). ` + dblabNotResentAdvice(jobId));
27245
+ }
27246
+ await sleep(delay);
27247
+ }
27248
+ }
27249
+ async function enqueueDblabCall(bodyObj, params, isWrite) {
27250
+ try {
27251
+ return await postDblabRpc("dblab_api_call", bodyObj, params);
27252
+ } catch (err) {
27253
+ if (!isWrite)
27254
+ throw err;
27255
+ const message = err instanceof Error ? err.message : String(err);
27256
+ throw new Error(`${message}
27257
+ ${dblabMayHaveLandedAdvice()}`, { cause: err });
27258
+ }
27259
+ }
27260
+ async function callDblabApi(params) {
27261
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
27262
+ if (!apiKey) {
27263
+ throw new Error("API key is required");
27264
+ }
27265
+ if (!instanceId) {
27266
+ throw new Error("instanceId is required");
27267
+ }
27268
+ const bodyObj = {
27269
+ instance_id: instanceId,
27270
+ action,
27271
+ method
27272
+ };
27273
+ if (data !== undefined) {
27274
+ bodyObj.data = data;
27275
+ }
27276
+ const rpcParams = { apiKey, apiBaseUrl, operation, debug };
27277
+ const isWrite = method.toLowerCase() !== "get";
27278
+ let { response, text } = await enqueueDblabCall({ ...bodyObj, accept_async: true }, rpcParams, isWrite);
27279
+ if (isUnknownRpcSignature(response.status, text, "dblab_api_call")) {
27280
+ ({ response, text } = await enqueueDblabCall(bodyObj, rpcParams, isWrite));
27281
+ }
27282
+ if (response.status === 202) {
27283
+ let handle;
27284
+ try {
27285
+ handle = JSON.parse(text);
27286
+ } catch {
27287
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ` + dblabMayHaveLandedAdvice());
27288
+ }
27289
+ if (!handle || typeof handle !== "object") {
27290
+ throw new Error(`${operation}: the platform accepted the call but returned no handle. ` + dblabMayHaveLandedAdvice());
27291
+ }
27292
+ return await awaitDblabCall(handle, rpcParams);
27293
+ }
27294
+ if (!response.ok) {
27295
+ const lost = isWrite && response.status >= 500;
27296
+ throw new HttpStatusError(stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText)) + (lost ? `
27297
+ ${dblabMayHaveLandedAdvice()}` : ""), response.status);
26981
27298
  }
26982
27299
  if (text.trim() === "") {
26983
27300
  return null;
@@ -26985,7 +27302,7 @@ async function callDblabApi(params) {
26985
27302
  try {
26986
27303
  return JSON.parse(text);
26987
27304
  } catch {
26988
- throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
27305
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}`);
26989
27306
  }
26990
27307
  }
26991
27308
  async function createClone(params) {
@@ -27222,6 +27539,37 @@ async function callRpc3(name, apiKey, body, opts) {
27222
27539
  throw new Error(`${name} returned a body that is not JSON`);
27223
27540
  }
27224
27541
  }
27542
+ async function resolveMonitoringInstanceId(params) {
27543
+ const ref = String(params.project ?? "").trim();
27544
+ if (!ref) {
27545
+ throw new Error("project is required (--project <id|alias>)");
27546
+ }
27547
+ const projects = await listProjects2({
27548
+ apiKey: params.apiKey,
27549
+ apiBaseUrl: params.apiBaseUrl,
27550
+ orgId: params.orgId
27551
+ });
27552
+ const needle = ref.toLowerCase();
27553
+ const matches = isNumericProjectRef2(ref) ? projects.filter((p) => String(p.project_id) === BigInt(ref).toString()) : projects.filter((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
27554
+ if (matches.length === 0) {
27555
+ throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
27556
+ }
27557
+ if (matches.length > 1) {
27558
+ throw new Error(`'${ref}' matches ${matches.length} projects (ids ${matches.map((p) => p.project_id).join(", ")}). ` + "Pass --project <id> instead.");
27559
+ }
27560
+ const match = matches[0];
27561
+ const ids = match.monitoring_instance_ids;
27562
+ if (ids === undefined) {
27563
+ throw new Error("This platform does not report monitoring instances per project yet. Pass --instance <uuid> instead.");
27564
+ }
27565
+ if (ids.length === 0) {
27566
+ throw new Error(`Project '${ref}' has no active monitoring instance.`);
27567
+ }
27568
+ if (ids.length > 1) {
27569
+ throw new Error(`Project '${ref}' has ${ids.length} active monitoring instances: ${ids.join(", ")}. Pass --instance <uuid>.`);
27570
+ }
27571
+ return ids[0];
27572
+ }
27225
27573
  async function enqueueQuery(apiKey, args, opts) {
27226
27574
  const body = {
27227
27575
  p_instance_id: args.instanceId,
@@ -27240,9 +27588,9 @@ async function enqueueQuery(apiKey, args, opts) {
27240
27588
  if (!jobId) {
27241
27589
  throw new Error("instance_query_enqueue returned no job_id");
27242
27590
  }
27243
- return { jobId, firstAnswerEstimateS: positiveSeconds(reply?.first_answer_estimate_s) };
27591
+ return { jobId, firstAnswerEstimateS: positiveSeconds2(reply?.first_answer_estimate_s) };
27244
27592
  }
27245
- function positiveSeconds(v) {
27593
+ function positiveSeconds2(v) {
27246
27594
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
27247
27595
  }
27248
27596
  async function readQueryResult(apiKey, jobId, opts) {
@@ -42810,7 +43158,7 @@ program2.command("orgs").description("list organizations this credential can rea
42810
43158
  process.exitCode = 1;
42811
43159
  }
42812
43160
  });
42813
- withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config)").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
43161
+ withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config); not with --project").option("--project <id|alias>", "the project's monitoring instance, by project id, alias or name; not with --instance").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
42814
43162
  try {
42815
43163
  const rootOpts = program2.opts();
42816
43164
  const cfg = readConfig();
@@ -42821,9 +43169,19 @@ withOrgOptions(program2.command("promql")).description("run a PromQL query on a
42821
43169
  return;
42822
43170
  }
42823
43171
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
42824
- const instanceId = opts.instance || readInstanceIdFromProject();
43172
+ if (opts.instance && opts.project) {
43173
+ console.error("Pass --instance or --project, not both.");
43174
+ process.exitCode = 1;
43175
+ return;
43176
+ }
43177
+ const instanceId = opts.instance || (opts.project ? await resolveMonitoringInstanceId({
43178
+ apiKey,
43179
+ apiBaseUrl,
43180
+ project: opts.project,
43181
+ orgId: configOrgIdForBody(apiKey, cfg.orgId)
43182
+ }) : readInstanceIdFromProject());
42825
43183
  if (!instanceId) {
42826
- console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
43184
+ console.error("No monitoring instance. Pass --project <id|alias> or --instance <uuid>, or run this on a box whose");
42827
43185
  console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
42828
43186
  process.exitCode = 1;
42829
43187
  return;
@@ -43675,7 +44033,7 @@ function tryParseJson(s) {
43675
44033
  }
43676
44034
  }
43677
44035
  function printJoeOutcome(outcome, json, budgetMs) {
43678
- const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
44036
+ const effectiveBudgetMs = Number.isFinite(outcome.budgetMs) ? outcome.budgetMs : typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
43679
44037
  const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
43680
44038
  if (outcome.budgetExpired) {
43681
44039
  if (json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.17.0-rc.4",
3
+ "version": "0.17.0",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,