postgresai 0.16.0-dev.1 → 0.16.0-dev.10

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.
@@ -9980,8 +9980,8 @@ var require_utils3 = __commonJS((exports, module) => {
9980
9980
  }
9981
9981
  return ind;
9982
9982
  }
9983
- function removeDotSegments(path4) {
9984
- let input = path4;
9983
+ function removeDotSegments(path5) {
9984
+ let input = path5;
9985
9985
  const output = [];
9986
9986
  let nextSlash = -1;
9987
9987
  let len = 0;
@@ -10171,8 +10171,8 @@ var require_schemes = __commonJS((exports, module) => {
10171
10171
  wsComponent.secure = undefined;
10172
10172
  }
10173
10173
  if (wsComponent.resourceName) {
10174
- const [path4, query] = wsComponent.resourceName.split("?");
10175
- wsComponent.path = path4 && path4 !== "/" ? path4 : undefined;
10174
+ const [path5, query] = wsComponent.resourceName.split("?");
10175
+ wsComponent.path = path5 && path5 !== "/" ? path5 : undefined;
10176
10176
  wsComponent.query = query;
10177
10177
  wsComponent.resourceName = undefined;
10178
10178
  }
@@ -13307,12 +13307,12 @@ var require_dist2 = __commonJS((exports, module) => {
13307
13307
  throw new Error(`Unknown format "${name}"`);
13308
13308
  return f;
13309
13309
  };
13310
- function addFormats(ajv, list, fs4, exportName) {
13310
+ function addFormats(ajv, list, fs5, exportName) {
13311
13311
  var _a2;
13312
13312
  var _b;
13313
13313
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== undefined || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
13314
13314
  for (const f of list)
13315
- ajv.addFormat(f, fs4[f]);
13315
+ ajv.addFormat(f, fs5[f]);
13316
13316
  }
13317
13317
  module.exports = exports = formatsPlugin;
13318
13318
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -13323,43 +13323,55 @@ var require_dist2 = __commonJS((exports, module) => {
13323
13323
  var exports_util2 = {};
13324
13324
  __export(exports_util2, {
13325
13325
  resolveBaseUrls: () => resolveBaseUrls2,
13326
+ redactSecretsForLog: () => redactSecretsForLog2,
13326
13327
  normalizeBaseUrl: () => normalizeBaseUrl2,
13327
13328
  maskSecret: () => maskSecret2,
13328
- formatHttpError: () => formatHttpError2
13329
+ formatHttpError: () => formatHttpError2,
13330
+ describeFetchError: () => describeFetchError2
13329
13331
  });
13330
13332
  function isHtmlContent2(text) {
13331
13333
  const trimmed = text.trim();
13332
13334
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
13333
13335
  }
13334
- function formatHttpError2(operation, status, responseBody) {
13335
- const statusMessage = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13336
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
13336
+ function formatHttpError2(operation, status, responseBody, statusText) {
13337
+ const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13337
13338
  const remediation = status === 401 ? `
13338
13339
  ${AUTH_REMEDIATION_HINT2}` : "";
13339
- if (responseBody) {
13340
- if (isHtmlContent2(responseBody)) {
13341
- return errMsg + remediation;
13342
- }
13340
+ let bodyMessage;
13341
+ let bodyDetails;
13342
+ if (responseBody && !isHtmlContent2(responseBody)) {
13343
13343
  try {
13344
13344
  const errObj = JSON.parse(responseBody);
13345
- const message = errObj.message || errObj.error || errObj.detail;
13346
- if (message && typeof message === "string") {
13347
- errMsg += `
13348
- ${message}`;
13349
- } else {
13350
- errMsg += `
13351
- ${JSON.stringify(errObj, null, 2)}`;
13345
+ const message = errObj.message ?? errObj.error;
13346
+ if (typeof message === "string" && message.trim().length > 0) {
13347
+ bodyMessage = message.trim();
13348
+ }
13349
+ const details = errObj.details ?? errObj.detail;
13350
+ if (typeof details === "string" && details.trim().length > 0) {
13351
+ bodyDetails = details.trim();
13352
13352
  }
13353
13353
  } catch {
13354
13354
  const trimmed = responseBody.trim();
13355
13355
  if (trimmed.length > 0 && trimmed.length < 500) {
13356
- errMsg += `
13357
- ${trimmed}`;
13356
+ bodyDetails = trimmed;
13358
13357
  }
13359
13358
  }
13360
13359
  }
13360
+ const trimmedReason = statusText?.trim();
13361
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
13362
+ const headline = bodyMessage ?? reasonPhrase ?? generic;
13363
+ let errMsg = `${operation}: HTTP ${status} - ${headline}`;
13364
+ if (bodyDetails && bodyDetails !== headline) {
13365
+ errMsg += `
13366
+ ${bodyDetails}`;
13367
+ }
13361
13368
  return errMsg + remediation;
13362
13369
  }
13370
+ function describeFetchError2(operation, url, err) {
13371
+ const cause = err?.cause;
13372
+ const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
13373
+ return `${operation}: could not reach ${url} (${detail})`;
13374
+ }
13363
13375
  function maskSecret2(secret) {
13364
13376
  if (!secret)
13365
13377
  return "";
@@ -13369,6 +13381,28 @@ function maskSecret2(secret) {
13369
13381
  return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
13370
13382
  return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
13371
13383
  }
13384
+ function redactSecretsForLog2(text) {
13385
+ let parsed;
13386
+ try {
13387
+ parsed = JSON.parse(text);
13388
+ } catch {
13389
+ return text;
13390
+ }
13391
+ const walk = (node) => {
13392
+ if (Array.isArray(node)) {
13393
+ return node.map(walk);
13394
+ }
13395
+ if (node && typeof node === "object") {
13396
+ const out = {};
13397
+ for (const [key, value] of Object.entries(node)) {
13398
+ out[key] = SENSITIVE_LOG_KEY2.test(key) && value != null ? "[REDACTED]" : walk(value);
13399
+ }
13400
+ return out;
13401
+ }
13402
+ return node;
13403
+ };
13404
+ return JSON.stringify(walk(parsed));
13405
+ }
13372
13406
  function normalizeBaseUrl2(value) {
13373
13407
  const trimmed = (value || "").replace(/\/$/, "");
13374
13408
  try {
@@ -13391,7 +13425,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
13391
13425
  storageBaseUrl: normalizeBaseUrl2(storageCandidate)
13392
13426
  };
13393
13427
  }
13394
- var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
13428
+ var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2, SENSITIVE_LOG_KEY2;
13395
13429
  var init_util = __esm(() => {
13396
13430
  HTTP_STATUS_MESSAGES2 = {
13397
13431
  400: "Bad Request",
@@ -13405,6 +13439,21 @@ var init_util = __esm(() => {
13405
13439
  503: "Service Unavailable - server temporarily unavailable",
13406
13440
  504: "Gateway Timeout - server temporarily unavailable"
13407
13441
  };
13442
+ STANDARD_REASON_PHRASES2 = {
13443
+ 400: "Bad Request",
13444
+ 401: "Unauthorized",
13445
+ 403: "Forbidden",
13446
+ 404: "Not Found",
13447
+ 408: "Request Timeout",
13448
+ 409: "Conflict",
13449
+ 413: "Payload Too Large",
13450
+ 429: "Too Many Requests",
13451
+ 500: "Internal Server Error",
13452
+ 502: "Bad Gateway",
13453
+ 503: "Service Unavailable",
13454
+ 504: "Gateway Timeout"
13455
+ };
13456
+ SENSITIVE_LOG_KEY2 = /password|connstr/i;
13408
13457
  });
13409
13458
 
13410
13459
  // node_modules/commander/esm.mjs
@@ -13425,7 +13474,7 @@ var {
13425
13474
  // package.json
13426
13475
  var package_default = {
13427
13476
  name: "postgresai",
13428
- version: "0.16.0-dev.1",
13477
+ version: "0.16.0-dev.10",
13429
13478
  description: "postgres_ai CLI",
13430
13479
  license: "Apache-2.0",
13431
13480
  private: false,
@@ -16236,11 +16285,11 @@ var safeLoadAll = renamed("safeLoadAll", "loadAll");
16236
16285
  var safeDump = renamed("safeDump", "dump");
16237
16286
 
16238
16287
  // bin/postgres-ai.ts
16239
- import * as fs9 from "fs";
16240
- import * as path7 from "path";
16288
+ import * as fs11 from "fs";
16289
+ import * as path9 from "path";
16241
16290
  import * as os3 from "os";
16242
16291
  import { fileURLToPath as fileURLToPath2 } from "url";
16243
- import * as crypto2 from "crypto";
16292
+ import * as crypto4 from "crypto";
16244
16293
 
16245
16294
  // node_modules/pg/esm/index.mjs
16246
16295
  var import_lib = __toESM(require_lib2(), 1);
@@ -16256,7 +16305,7 @@ var Result = import_lib.default.Result;
16256
16305
  var TypeOverrides = import_lib.default.TypeOverrides;
16257
16306
  var defaults = import_lib.default.defaults;
16258
16307
  // package.json
16259
- var version = "0.16.0-dev.1";
16308
+ var version = "0.16.0-dev.10";
16260
16309
  var package_default2 = {
16261
16310
  name: "postgresai",
16262
16311
  version,
@@ -16398,35 +16447,59 @@ function isHtmlContent(text) {
16398
16447
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
16399
16448
  }
16400
16449
  var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
16401
- function formatHttpError(operation, status, responseBody) {
16402
- const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
16403
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
16450
+ var STANDARD_REASON_PHRASES = {
16451
+ 400: "Bad Request",
16452
+ 401: "Unauthorized",
16453
+ 403: "Forbidden",
16454
+ 404: "Not Found",
16455
+ 408: "Request Timeout",
16456
+ 409: "Conflict",
16457
+ 413: "Payload Too Large",
16458
+ 429: "Too Many Requests",
16459
+ 500: "Internal Server Error",
16460
+ 502: "Bad Gateway",
16461
+ 503: "Service Unavailable",
16462
+ 504: "Gateway Timeout"
16463
+ };
16464
+ function formatHttpError(operation, status, responseBody, statusText) {
16465
+ const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
16404
16466
  const remediation = status === 401 ? `
16405
16467
  ${AUTH_REMEDIATION_HINT}` : "";
16406
- if (responseBody) {
16407
- if (isHtmlContent(responseBody)) {
16408
- return errMsg + remediation;
16409
- }
16468
+ let bodyMessage;
16469
+ let bodyDetails;
16470
+ if (responseBody && !isHtmlContent(responseBody)) {
16410
16471
  try {
16411
16472
  const errObj = JSON.parse(responseBody);
16412
- const message = errObj.message || errObj.error || errObj.detail;
16413
- if (message && typeof message === "string") {
16414
- errMsg += `
16415
- ${message}`;
16416
- } else {
16417
- errMsg += `
16418
- ${JSON.stringify(errObj, null, 2)}`;
16473
+ const message = errObj.message ?? errObj.error;
16474
+ if (typeof message === "string" && message.trim().length > 0) {
16475
+ bodyMessage = message.trim();
16476
+ }
16477
+ const details = errObj.details ?? errObj.detail;
16478
+ if (typeof details === "string" && details.trim().length > 0) {
16479
+ bodyDetails = details.trim();
16419
16480
  }
16420
16481
  } catch {
16421
16482
  const trimmed = responseBody.trim();
16422
16483
  if (trimmed.length > 0 && trimmed.length < 500) {
16423
- errMsg += `
16424
- ${trimmed}`;
16484
+ bodyDetails = trimmed;
16425
16485
  }
16426
16486
  }
16427
16487
  }
16488
+ const trimmedReason = statusText?.trim();
16489
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
16490
+ const headline = bodyMessage ?? reasonPhrase ?? generic;
16491
+ let errMsg = `${operation}: HTTP ${status} - ${headline}`;
16492
+ if (bodyDetails && bodyDetails !== headline) {
16493
+ errMsg += `
16494
+ ${bodyDetails}`;
16495
+ }
16428
16496
  return errMsg + remediation;
16429
16497
  }
16498
+ function describeFetchError(operation, url, err) {
16499
+ const cause = err?.cause;
16500
+ const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
16501
+ return `${operation}: could not reach ${url} (${detail})`;
16502
+ }
16430
16503
  function maskSecret(secret) {
16431
16504
  if (!secret)
16432
16505
  return "";
@@ -16436,6 +16509,29 @@ function maskSecret(secret) {
16436
16509
  return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
16437
16510
  return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
16438
16511
  }
16512
+ var SENSITIVE_LOG_KEY = /password|connstr/i;
16513
+ function redactSecretsForLog(text) {
16514
+ let parsed;
16515
+ try {
16516
+ parsed = JSON.parse(text);
16517
+ } catch {
16518
+ return text;
16519
+ }
16520
+ const walk = (node) => {
16521
+ if (Array.isArray(node)) {
16522
+ return node.map(walk);
16523
+ }
16524
+ if (node && typeof node === "object") {
16525
+ const out = {};
16526
+ for (const [key, value] of Object.entries(node)) {
16527
+ out[key] = SENSITIVE_LOG_KEY.test(key) && value != null ? "[REDACTED]" : walk(value);
16528
+ }
16529
+ return out;
16530
+ }
16531
+ return node;
16532
+ };
16533
+ return JSON.stringify(walk(parsed));
16534
+ }
16439
16535
  function normalizeBaseUrl(value) {
16440
16536
  const trimmed = (value || "").replace(/\/$/, "");
16441
16537
  try {
@@ -17267,9 +17363,611 @@ async function fetchReportFileData(params) {
17267
17363
  }
17268
17364
  }
17269
17365
 
17270
- // lib/storage.ts
17366
+ // lib/joe.ts
17271
17367
  import * as fs3 from "fs";
17272
17368
  import * as path3 from "path";
17369
+ import * as crypto from "node:crypto";
17370
+ var JOE_COMMANDS = [
17371
+ "plan",
17372
+ "explain",
17373
+ "exec",
17374
+ "hypo",
17375
+ "activity",
17376
+ "terminate",
17377
+ "reset",
17378
+ "describe"
17379
+ ];
17380
+ var TERMINAL_STATES = new Set([
17381
+ "done",
17382
+ "error",
17383
+ "timed_out"
17384
+ ]);
17385
+ var AI_ENABLED_COMMANDS = new Set([
17386
+ "plan",
17387
+ "explain",
17388
+ "exec",
17389
+ "hypo",
17390
+ "activity",
17391
+ "describe"
17392
+ ]);
17393
+ var EXECUTING_COMMANDS = new Set([
17394
+ "exec",
17395
+ "explain"
17396
+ ]);
17397
+ var DEFAULT_BUDGET_MS = 25000;
17398
+ var DEFAULT_POLL_INTERVAL_MS = 800;
17399
+ async function callRpc(params) {
17400
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
17401
+ if (!apiKey) {
17402
+ throw new Error("API key is required");
17403
+ }
17404
+ const base = normalizeBaseUrl(apiBaseUrl);
17405
+ const url = new URL(`${base}/rpc/${fn}`);
17406
+ const payload = JSON.stringify(body);
17407
+ const headers = {
17408
+ "access-token": apiKey,
17409
+ "Content-Type": "application/json",
17410
+ Connection: "close"
17411
+ };
17412
+ if (debug) {
17413
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17414
+ console.error(`Debug: POST URL: ${url.toString()}`);
17415
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17416
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
17417
+ }
17418
+ let response;
17419
+ try {
17420
+ response = await fetch(url.toString(), {
17421
+ method: "POST",
17422
+ headers,
17423
+ body: payload
17424
+ });
17425
+ } catch (err) {
17426
+ throw new Error(describeFetchError(operation, base, err));
17427
+ }
17428
+ const text = await response.text();
17429
+ if (debug) {
17430
+ console.error(`Debug: Response status: ${response.status}`);
17431
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
17432
+ }
17433
+ if (!response.ok) {
17434
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
17435
+ }
17436
+ try {
17437
+ return JSON.parse(text);
17438
+ } catch {
17439
+ throw new Error(`${operation}: failed to parse response: ${text}`);
17440
+ }
17441
+ }
17442
+ async function submitCommand(params) {
17443
+ const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
17444
+ if (!JOE_COMMANDS.includes(command)) {
17445
+ throw new Error(`Unknown Joe command: ${command}`);
17446
+ }
17447
+ const body = {
17448
+ project_id: projectId,
17449
+ command,
17450
+ sql: sql ?? null,
17451
+ args: args ?? null,
17452
+ session_id: sessionId ?? null
17453
+ };
17454
+ if (idempotencyKey) {
17455
+ body.idempotency_key = idempotencyKey;
17456
+ }
17457
+ return callRpc({
17458
+ apiKey,
17459
+ apiBaseUrl,
17460
+ fn: "joe_command_submit",
17461
+ body,
17462
+ operation: `Failed to submit ${command} command`,
17463
+ debug
17464
+ });
17465
+ }
17466
+ async function getCommandStatus(params) {
17467
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
17468
+ if (!commandId) {
17469
+ throw new Error("commandId is required");
17470
+ }
17471
+ return callRpc({
17472
+ apiKey,
17473
+ apiBaseUrl,
17474
+ fn: "joe_command_status",
17475
+ body: { command_id: commandId },
17476
+ operation: "Failed to fetch command status",
17477
+ debug
17478
+ });
17479
+ }
17480
+ async function getCommandResult(params) {
17481
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
17482
+ if (!commandId) {
17483
+ throw new Error("commandId is required");
17484
+ }
17485
+ return callRpc({
17486
+ apiKey,
17487
+ apiBaseUrl,
17488
+ fn: "joe_command_result",
17489
+ body: { command_id: commandId },
17490
+ operation: "Failed to fetch command result",
17491
+ debug
17492
+ });
17493
+ }
17494
+ function normalizeProjectRow(row) {
17495
+ const projectId = row.project_id ?? row.id ?? 0;
17496
+ return {
17497
+ project_id: Number(projectId),
17498
+ alias: row.alias ?? null,
17499
+ name: row.name ?? null,
17500
+ label: row.label ?? null,
17501
+ joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
17502
+ tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
17503
+ instance_id: row.instance_id == null ? null : Number(row.instance_id),
17504
+ dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
17505
+ };
17506
+ }
17507
+ async function listProjects(params) {
17508
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
17509
+ const body = {};
17510
+ if (typeof orgId === "number") {
17511
+ body.org_id = orgId;
17512
+ }
17513
+ const rows = await callRpc({
17514
+ apiKey,
17515
+ apiBaseUrl,
17516
+ fn: "projects_list",
17517
+ body,
17518
+ operation: "Failed to list projects",
17519
+ debug
17520
+ });
17521
+ if (!Array.isArray(rows)) {
17522
+ return [];
17523
+ }
17524
+ return rows.map(normalizeProjectRow);
17525
+ }
17526
+ function isNumericProjectRef(ref) {
17527
+ return /^[0-9]+$/.test(ref.trim());
17528
+ }
17529
+ async function resolveProjectId(params) {
17530
+ const ref = String(params.project ?? "").trim();
17531
+ if (!ref) {
17532
+ throw new Error("project is required (--project <id|alias>)");
17533
+ }
17534
+ if (isNumericProjectRef(ref)) {
17535
+ return Number(ref);
17536
+ }
17537
+ const projects = await listProjects({
17538
+ apiKey: params.apiKey,
17539
+ apiBaseUrl: params.apiBaseUrl,
17540
+ orgId: params.orgId,
17541
+ debug: params.debug
17542
+ });
17543
+ const needle = ref.toLowerCase();
17544
+ const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle);
17545
+ if (!match) {
17546
+ throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
17547
+ }
17548
+ return match.project_id;
17549
+ }
17550
+ function sessionStorePath(dir) {
17551
+ return path3.join(dir ?? getConfigDir2(), "joe-sessions.json");
17552
+ }
17553
+ function readSessionStore(dir) {
17554
+ const file = sessionStorePath(dir);
17555
+ if (!fs3.existsSync(file)) {
17556
+ return {};
17557
+ }
17558
+ try {
17559
+ const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
17560
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
17561
+ return parsed;
17562
+ }
17563
+ } catch {}
17564
+ return {};
17565
+ }
17566
+ function readStoredSessionId(projectId, dir) {
17567
+ const store = readSessionStore(dir);
17568
+ const value = store[String(projectId)];
17569
+ return typeof value === "string" && value.length > 0 ? value : null;
17570
+ }
17571
+ function writeStoredSessionId(projectId, sessionId, dir) {
17572
+ const baseDir = dir ?? getConfigDir2();
17573
+ if (!fs3.existsSync(baseDir)) {
17574
+ fs3.mkdirSync(baseDir, { recursive: true, mode: 448 });
17575
+ }
17576
+ const store = readSessionStore(dir);
17577
+ store[String(projectId)] = sessionId;
17578
+ fs3.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
17579
+ `, { mode: 384 });
17580
+ }
17581
+ function clearStoredSessionId(projectId, dir) {
17582
+ const file = sessionStorePath(dir);
17583
+ if (!fs3.existsSync(file)) {
17584
+ return;
17585
+ }
17586
+ const store = readSessionStore(dir);
17587
+ if (String(projectId) in store) {
17588
+ delete store[String(projectId)];
17589
+ fs3.writeFileSync(file, JSON.stringify(store, null, 2) + `
17590
+ `, { mode: 384 });
17591
+ }
17592
+ }
17593
+ function computeIdempotencyKey(command, projectId, sql, args) {
17594
+ if (EXECUTING_COMMANDS.has(command)) {
17595
+ const canonical = `${projectId}
17596
+ ${command}
17597
+ ${sql ?? ""}
17598
+ ${JSON.stringify(args ?? null)}`;
17599
+ return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
17600
+ }
17601
+ return `joe-${crypto.randomUUID()}`;
17602
+ }
17603
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
17604
+ async function runCommand(params) {
17605
+ const {
17606
+ apiKey,
17607
+ apiBaseUrl,
17608
+ command,
17609
+ projectId,
17610
+ sql,
17611
+ args,
17612
+ sessionId,
17613
+ debug
17614
+ } = params;
17615
+ const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) ? params.budgetMs : DEFAULT_BUDGET_MS;
17616
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17617
+ const now = params.now ?? Date.now;
17618
+ const sleep = params.sleep ?? defaultSleep;
17619
+ const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
17620
+ const submitted = await submitCommand({
17621
+ apiKey,
17622
+ apiBaseUrl,
17623
+ command,
17624
+ projectId,
17625
+ sql,
17626
+ args,
17627
+ sessionId,
17628
+ idempotencyKey,
17629
+ debug
17630
+ });
17631
+ const commandId = submitted.command_id;
17632
+ const newSessionId = submitted.session_id ?? sessionId ?? null;
17633
+ const deadline = now() + budgetMs;
17634
+ let status = submitted.status;
17635
+ while (true) {
17636
+ const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
17637
+ status = statusResp.status;
17638
+ if (TERMINAL_STATES.has(status)) {
17639
+ break;
17640
+ }
17641
+ if (now() >= deadline) {
17642
+ return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
17643
+ }
17644
+ await sleep(pollIntervalMs);
17645
+ }
17646
+ const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
17647
+ return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
17648
+ }
17649
+ async function executeJoeCommand(params) {
17650
+ const projectId = await resolveProjectId({
17651
+ apiKey: params.apiKey,
17652
+ apiBaseUrl: params.apiBaseUrl,
17653
+ project: params.project,
17654
+ orgId: params.orgId,
17655
+ debug: params.debug
17656
+ });
17657
+ let sessionId;
17658
+ if (params.newSession) {
17659
+ clearStoredSessionId(projectId, params.sessionDir);
17660
+ sessionId = null;
17661
+ } else if (params.session) {
17662
+ sessionId = String(params.session);
17663
+ } else {
17664
+ sessionId = readStoredSessionId(projectId, params.sessionDir);
17665
+ }
17666
+ const outcome = await runCommand({
17667
+ apiKey: params.apiKey,
17668
+ apiBaseUrl: params.apiBaseUrl,
17669
+ command: params.command,
17670
+ projectId,
17671
+ sql: params.sql,
17672
+ args: params.args,
17673
+ sessionId,
17674
+ budgetMs: params.budgetMs,
17675
+ pollIntervalMs: params.pollIntervalMs,
17676
+ debug: params.debug,
17677
+ now: params.now,
17678
+ sleep: params.sleep
17679
+ });
17680
+ if (outcome.sessionId) {
17681
+ writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
17682
+ }
17683
+ return { ...outcome, command: params.command, projectId };
17684
+ }
17685
+
17686
+ // lib/dblab.ts
17687
+ function isNumericProjectRef2(ref) {
17688
+ return /^[0-9]+$/.test(ref.trim());
17689
+ }
17690
+ async function resolveDblabInstanceId(params) {
17691
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
17692
+ if (!apiKey) {
17693
+ throw new Error("API key is required");
17694
+ }
17695
+ const ref = String(params.project ?? "").trim();
17696
+ if (!ref) {
17697
+ throw new Error("project is required (--project <id|alias>)");
17698
+ }
17699
+ let projects;
17700
+ try {
17701
+ projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
17702
+ } catch (err) {
17703
+ const message = err instanceof Error ? err.message : String(err);
17704
+ throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
17705
+ }
17706
+ const numeric = isNumericProjectRef2(ref);
17707
+ const needle = ref.toLowerCase();
17708
+ const match = projects.find((p) => {
17709
+ if (numeric) {
17710
+ return String(p.project_id) === ref;
17711
+ }
17712
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
17713
+ });
17714
+ if (!match) {
17715
+ throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
17716
+ }
17717
+ if (match.dblab_instance_id == null) {
17718
+ throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
17719
+ }
17720
+ return String(match.dblab_instance_id);
17721
+ }
17722
+ async function callDblabApi(params) {
17723
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
17724
+ if (!apiKey) {
17725
+ throw new Error("API key is required");
17726
+ }
17727
+ if (!instanceId) {
17728
+ throw new Error("instanceId is required");
17729
+ }
17730
+ const base = normalizeBaseUrl(apiBaseUrl);
17731
+ const url = new URL(`${base}/rpc/dblab_api_call`);
17732
+ const bodyObj = {
17733
+ instance_id: instanceId,
17734
+ action,
17735
+ method
17736
+ };
17737
+ if (data !== undefined) {
17738
+ bodyObj.data = data;
17739
+ }
17740
+ const body = JSON.stringify(bodyObj);
17741
+ const headers = {
17742
+ "access-token": apiKey,
17743
+ "Content-Type": "application/json",
17744
+ Connection: "close"
17745
+ };
17746
+ if (debug) {
17747
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17748
+ console.error(`Debug: POST URL: ${url.toString()}`);
17749
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17750
+ console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
17751
+ }
17752
+ let response;
17753
+ try {
17754
+ response = await fetch(url.toString(), { method: "POST", headers, body });
17755
+ } catch (err) {
17756
+ throw new Error(describeFetchError(operation, base, err));
17757
+ }
17758
+ const text = await response.text();
17759
+ if (debug) {
17760
+ console.error(`Debug: Response status: ${response.status}`);
17761
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
17762
+ }
17763
+ if (!response.ok) {
17764
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
17765
+ }
17766
+ if (text.trim() === "") {
17767
+ return null;
17768
+ }
17769
+ try {
17770
+ return JSON.parse(text);
17771
+ } catch {
17772
+ throw new Error(`${operation}: failed to parse response: ${text}`);
17773
+ }
17774
+ }
17775
+ async function createClone(params) {
17776
+ const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
17777
+ const data = { protected: Boolean(isProtected) };
17778
+ if (cloneId)
17779
+ data.id = cloneId;
17780
+ if (branch)
17781
+ data.branch = branch;
17782
+ if (snapshotId)
17783
+ data.snapshot = { id: snapshotId };
17784
+ if (dbUser && dbPassword)
17785
+ data.db = { username: dbUser, password: dbPassword };
17786
+ return callDblabApi({
17787
+ apiKey,
17788
+ apiBaseUrl,
17789
+ instanceId,
17790
+ action: "/clone",
17791
+ method: "post",
17792
+ data,
17793
+ operation: "Failed to create clone",
17794
+ debug
17795
+ });
17796
+ }
17797
+ async function listClones(params) {
17798
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
17799
+ return callDblabApi({
17800
+ apiKey,
17801
+ apiBaseUrl,
17802
+ instanceId,
17803
+ action: "/clones",
17804
+ method: "get",
17805
+ operation: "Failed to list clones",
17806
+ debug
17807
+ });
17808
+ }
17809
+ async function getClone(params) {
17810
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17811
+ if (!cloneId)
17812
+ throw new Error("cloneId is required");
17813
+ return callDblabApi({
17814
+ apiKey,
17815
+ apiBaseUrl,
17816
+ instanceId,
17817
+ action: `/clone/${encodeURIComponent(cloneId)}`,
17818
+ method: "get",
17819
+ operation: "Failed to get clone",
17820
+ debug
17821
+ });
17822
+ }
17823
+ async function resetClone(params) {
17824
+ const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
17825
+ if (!cloneId)
17826
+ throw new Error("cloneId is required");
17827
+ const data = { latest: latest ?? !snapshotId };
17828
+ if (snapshotId)
17829
+ data.snapshotID = snapshotId;
17830
+ return callDblabApi({
17831
+ apiKey,
17832
+ apiBaseUrl,
17833
+ instanceId,
17834
+ action: `/clone/${encodeURIComponent(cloneId)}/reset`,
17835
+ method: "post",
17836
+ data,
17837
+ operation: "Failed to reset clone",
17838
+ debug
17839
+ });
17840
+ }
17841
+ async function destroyClone(params) {
17842
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17843
+ if (!cloneId)
17844
+ throw new Error("cloneId is required");
17845
+ return callDblabApi({
17846
+ apiKey,
17847
+ apiBaseUrl,
17848
+ instanceId,
17849
+ action: `/clone/${encodeURIComponent(cloneId)}`,
17850
+ method: "delete",
17851
+ operation: "Failed to destroy clone",
17852
+ debug
17853
+ });
17854
+ }
17855
+ async function listBranches(params) {
17856
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
17857
+ return callDblabApi({
17858
+ apiKey,
17859
+ apiBaseUrl,
17860
+ instanceId,
17861
+ action: "/branches",
17862
+ method: "get",
17863
+ operation: "Failed to list branches",
17864
+ debug
17865
+ });
17866
+ }
17867
+ async function createBranch(params) {
17868
+ const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
17869
+ if (!branchName)
17870
+ throw new Error("branchName is required");
17871
+ const data = { branchName };
17872
+ if (baseBranch)
17873
+ data.baseBranch = baseBranch;
17874
+ if (snapshotId)
17875
+ data.snapshotID = snapshotId;
17876
+ return callDblabApi({
17877
+ apiKey,
17878
+ apiBaseUrl,
17879
+ instanceId,
17880
+ action: "/branch",
17881
+ method: "post",
17882
+ data,
17883
+ operation: "Failed to create branch",
17884
+ debug
17885
+ });
17886
+ }
17887
+ async function deleteBranch(params) {
17888
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17889
+ if (!branchName)
17890
+ throw new Error("branchName is required");
17891
+ return callDblabApi({
17892
+ apiKey,
17893
+ apiBaseUrl,
17894
+ instanceId,
17895
+ action: `/branch/${encodeURIComponent(branchName)}`,
17896
+ method: "delete",
17897
+ operation: "Failed to delete branch",
17898
+ debug
17899
+ });
17900
+ }
17901
+ async function branchLog(params) {
17902
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17903
+ if (!branchName)
17904
+ throw new Error("branchName is required");
17905
+ return callDblabApi({
17906
+ apiKey,
17907
+ apiBaseUrl,
17908
+ instanceId,
17909
+ action: `/branch/${encodeURIComponent(branchName)}/log`,
17910
+ method: "get",
17911
+ operation: "Failed to fetch branch log",
17912
+ debug
17913
+ });
17914
+ }
17915
+ async function listSnapshots(params) {
17916
+ const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
17917
+ const qs = new URLSearchParams;
17918
+ const branch = branchName?.trim();
17919
+ if (branch)
17920
+ qs.append("branch", branch);
17921
+ if (dataset)
17922
+ qs.append("dataset", dataset);
17923
+ const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
17924
+ return callDblabApi({
17925
+ apiKey,
17926
+ apiBaseUrl,
17927
+ instanceId,
17928
+ action,
17929
+ method: "get",
17930
+ operation: "Failed to list snapshots",
17931
+ debug
17932
+ });
17933
+ }
17934
+ async function createSnapshot(params) {
17935
+ const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
17936
+ if (!cloneId)
17937
+ throw new Error("cloneId is required");
17938
+ const data = { cloneID: cloneId };
17939
+ if (message)
17940
+ data.message = message;
17941
+ return callDblabApi({
17942
+ apiKey,
17943
+ apiBaseUrl,
17944
+ instanceId,
17945
+ action: "/branch/snapshot",
17946
+ method: "post",
17947
+ data,
17948
+ operation: "Failed to create snapshot",
17949
+ debug
17950
+ });
17951
+ }
17952
+ async function destroySnapshot(params) {
17953
+ const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
17954
+ if (!snapshotId)
17955
+ throw new Error("snapshotId is required");
17956
+ const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
17957
+ return callDblabApi({
17958
+ apiKey,
17959
+ apiBaseUrl,
17960
+ instanceId,
17961
+ action,
17962
+ method: "delete",
17963
+ operation: "Failed to destroy snapshot",
17964
+ debug
17965
+ });
17966
+ }
17967
+
17968
+ // lib/storage.ts
17969
+ import * as fs4 from "fs";
17970
+ import * as path4 from "path";
17273
17971
  var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
17274
17972
  var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17275
17973
  var MIME_TYPES = {
@@ -17310,11 +18008,11 @@ async function uploadFile(params) {
17310
18008
  if (!filePath) {
17311
18009
  throw new Error("filePath is required");
17312
18010
  }
17313
- const resolvedPath = path3.resolve(filePath);
17314
- if (!fs3.existsSync(resolvedPath)) {
18011
+ const resolvedPath = path4.resolve(filePath);
18012
+ if (!fs4.existsSync(resolvedPath)) {
17315
18013
  throw new Error(`File not found: ${resolvedPath}`);
17316
18014
  }
17317
- const stat = fs3.statSync(resolvedPath);
18015
+ const stat = fs4.statSync(resolvedPath);
17318
18016
  if (!stat.isFile()) {
17319
18017
  throw new Error(`Not a file: ${resolvedPath}`);
17320
18018
  }
@@ -17326,9 +18024,9 @@ async function uploadFile(params) {
17326
18024
  console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17327
18025
  }
17328
18026
  const url = `${base}/upload`;
17329
- const fileBuffer = fs3.readFileSync(resolvedPath);
17330
- const fileName = path3.basename(resolvedPath);
17331
- const ext = path3.extname(fileName).toLowerCase();
18027
+ const fileBuffer = fs4.readFileSync(resolvedPath);
18028
+ const fileName = path4.basename(resolvedPath);
18029
+ const ext = path4.extname(fileName).toLowerCase();
17332
18030
  const mimeType = MIME_TYPES[ext] || "application/octet-stream";
17333
18031
  const formData = new FormData;
17334
18032
  formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
@@ -17387,15 +18085,15 @@ async function downloadFile(params) {
17387
18085
  const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17388
18086
  fullUrl = `${base}${relativePath}`;
17389
18087
  }
17390
- const urlFilename = path3.basename(new URL(fullUrl).pathname);
18088
+ const urlFilename = path4.basename(new URL(fullUrl).pathname);
17391
18089
  if (!urlFilename) {
17392
18090
  throw new Error("Cannot derive filename from URL; please specify --output");
17393
18091
  }
17394
- const saveTo = outputPath ? path3.resolve(outputPath) : path3.resolve(urlFilename);
18092
+ const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
17395
18093
  if (!outputPath) {
17396
- const normalizedSave = path3.normalize(saveTo);
17397
- const cwd = path3.normalize(process.cwd());
17398
- if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path3.sep)) {
18094
+ const normalizedSave = path4.normalize(saveTo);
18095
+ const cwd = path4.normalize(process.cwd());
18096
+ if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
17399
18097
  throw new Error("Derived output path escapes current directory; please specify --output");
17400
18098
  }
17401
18099
  }
@@ -17427,11 +18125,11 @@ async function downloadFile(params) {
17427
18125
  }
17428
18126
  const arrayBuffer = await response.arrayBuffer();
17429
18127
  const buffer = Buffer.from(arrayBuffer);
17430
- const parentDir = path3.dirname(saveTo);
17431
- if (!fs3.existsSync(parentDir)) {
17432
- fs3.mkdirSync(parentDir, { recursive: true });
18128
+ const parentDir = path4.dirname(saveTo);
18129
+ if (!fs4.existsSync(parentDir)) {
18130
+ fs4.mkdirSync(parentDir, { recursive: true });
17433
18131
  }
17434
- fs3.writeFileSync(saveTo, buffer);
18132
+ fs4.writeFileSync(saveTo, buffer);
17435
18133
  return {
17436
18134
  savedTo: saveTo,
17437
18135
  size: buffer.length,
@@ -17443,9 +18141,9 @@ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
17443
18141
  const base = normalizeBaseUrl(storageBaseUrl);
17444
18142
  const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17445
18143
  const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
17446
- const name = filename || path3.basename(new URL(fullUrl).pathname);
18144
+ const name = filename || path4.basename(new URL(fullUrl).pathname);
17447
18145
  const safeName = name.replace(/[\[\]()]/g, "\\$&");
17448
- const ext = path3.extname(name).toLowerCase();
18146
+ const ext = path4.extname(name).toLowerCase();
17449
18147
  if (IMAGE_EXTENSIONS.has(ext)) {
17450
18148
  return `![${safeName}](${fullUrl})`;
17451
18149
  }
@@ -17733,10 +18431,10 @@ function mergeDefs(...defs) {
17733
18431
  function cloneDef(schema2) {
17734
18432
  return mergeDefs(schema2._zod.def);
17735
18433
  }
17736
- function getElementAtPath(obj, path4) {
17737
- if (!path4)
18434
+ function getElementAtPath(obj, path5) {
18435
+ if (!path5)
17738
18436
  return obj;
17739
- return path4.reduce((acc, key) => acc?.[key], obj);
18437
+ return path5.reduce((acc, key) => acc?.[key], obj);
17740
18438
  }
17741
18439
  function promiseAllObject(promisesObj) {
17742
18440
  const keys = Object.keys(promisesObj);
@@ -18100,11 +18798,11 @@ function aborted(x, startIndex = 0) {
18100
18798
  }
18101
18799
  return false;
18102
18800
  }
18103
- function prefixIssues(path4, issues) {
18801
+ function prefixIssues(path5, issues) {
18104
18802
  return issues.map((iss) => {
18105
18803
  var _a;
18106
18804
  (_a = iss).path ?? (_a.path = []);
18107
- iss.path.unshift(path4);
18805
+ iss.path.unshift(path5);
18108
18806
  return iss;
18109
18807
  });
18110
18808
  }
@@ -24376,14 +25074,219 @@ class StdioServerTransport {
24376
25074
  // lib/mcp-server.ts
24377
25075
  var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
24378
25076
  `).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\'/g, "'");
24379
- async function handleToolCall(req, rootOpts, extra) {
24380
- const toolName = req.params.name;
24381
- const args = req.params.arguments || {};
24382
- const cfg = readConfig2();
24383
- const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
24384
- const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
24385
- const debug = Boolean(args.debug ?? extra?.debug);
24386
- if (!apiKey) {
25077
+ var JOE_TOOL_TO_COMMAND = {
25078
+ plan_query: "plan",
25079
+ explain_query: "explain",
25080
+ exec_sql: "exec",
25081
+ hypo_index: "hypo",
25082
+ activity: "activity",
25083
+ terminate_backend: "terminate",
25084
+ reset_clone: "reset",
25085
+ describe: "describe"
25086
+ };
25087
+ function joeToolDefinitions() {
25088
+ const sessionProps = {
25089
+ project_id: {
25090
+ type: "string",
25091
+ description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db."
25092
+ },
25093
+ session_id: {
25094
+ type: "string",
25095
+ description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted."
25096
+ },
25097
+ new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
25098
+ timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
25099
+ debug: { type: "boolean", description: "Enable verbose debug logs." }
25100
+ };
25101
+ return [
25102
+ {
25103
+ name: "plan_query",
25104
+ description: "Plan a query (EXPLAIN, plan-only — NEVER executes; the fast/safe default). Returns a sanitized plan tree (literals redacted). LLM-facing: feeds the plan/stats to your LLM when the org's AI features are enabled. Requires joe:plan.",
25105
+ inputSchema: {
25106
+ type: "object",
25107
+ properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
25108
+ required: ["sql", "project_id"],
25109
+ additionalProperties: false
25110
+ }
25111
+ },
25112
+ {
25113
+ name: "explain_query",
25114
+ description: "EXPLAIN ANALYZE a query — EXECUTES it on the hardened, ephemeral DBLab clone and returns the measured, annotated plan (node types, costs, actual-rows/timings, buffers — literals redacted), NOT raw SELECT result rows. LLM-facing: feeds the measured plan/stats to your LLM when AI features are enabled. Requires joe:exec; subject to the org execution policy (read_only allows SELECT-only).",
25115
+ inputSchema: {
25116
+ type: "object",
25117
+ properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
25118
+ required: ["sql", "project_id"],
25119
+ additionalProperties: false
25120
+ }
25121
+ },
25122
+ {
25123
+ name: "exec_sql",
25124
+ description: "WARNING: executes arbitrary SQL (DDL/DML) on the disposable clone and CAN RETURN REAL (own-data) TABLE ROWS that will enter this agent's LLM context (subject to the agent row-cap / metadata-only minimization). Prefer plan_query (non-executing) when you only need a plan. Requires an explicit joe:exec grant and is subject to the org's joe_execution_policy.",
25125
+ inputSchema: {
25126
+ type: "object",
25127
+ properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
25128
+ required: ["sql", "project_id"],
25129
+ additionalProperties: false
25130
+ }
25131
+ },
25132
+ {
25133
+ name: "hypo_index",
25134
+ description: "HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
25135
+ inputSchema: {
25136
+ type: "object",
25137
+ properties: {
25138
+ sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
25139
+ query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
25140
+ ...sessionProps
25141
+ },
25142
+ required: ["sql", "query", "project_id"],
25143
+ additionalProperties: false
25144
+ }
25145
+ },
25146
+ {
25147
+ name: "activity",
25148
+ description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
25149
+ inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
25150
+ },
25151
+ {
25152
+ name: "terminate_backend",
25153
+ description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
25154
+ inputSchema: {
25155
+ type: "object",
25156
+ properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
25157
+ required: ["pid", "project_id"],
25158
+ additionalProperties: false
25159
+ }
25160
+ },
25161
+ {
25162
+ name: "reset_clone",
25163
+ description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
25164
+ inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
25165
+ },
25166
+ {
25167
+ name: "describe",
25168
+ description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
25169
+ inputSchema: {
25170
+ type: "object",
25171
+ properties: {
25172
+ object: { type: "string", description: "Object to describe (e.g. users)." },
25173
+ variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
25174
+ ...sessionProps
25175
+ },
25176
+ required: ["object", "project_id"],
25177
+ additionalProperties: false
25178
+ }
25179
+ },
25180
+ {
25181
+ name: "list_projects",
25182
+ description: "List the org's projects and which have Joe ready (org-level discovery — NOT a Joe endpoint; mirrors `pgai projects`). Use it to find the project_ids/aliases you may address with the Joe command tools.",
25183
+ inputSchema: {
25184
+ type: "object",
25185
+ properties: {
25186
+ org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
25187
+ debug: { type: "boolean", description: "Enable verbose debug logs." }
25188
+ },
25189
+ additionalProperties: false
25190
+ }
25191
+ }
25192
+ ];
25193
+ }
25194
+ function sanitizeBudgetMs(value) {
25195
+ if (value === undefined || value === null) {
25196
+ return;
25197
+ }
25198
+ const n = Number(value);
25199
+ return Number.isFinite(n) ? n : undefined;
25200
+ }
25201
+ async function runJoeTool(params) {
25202
+ const command = JOE_TOOL_TO_COMMAND[params.toolName];
25203
+ const a = params.args;
25204
+ const project = String(a.project_id ?? "").trim();
25205
+ if (!project) {
25206
+ return { content: [{ type: "text", text: "project_id is required" }], isError: true };
25207
+ }
25208
+ let sql = null;
25209
+ let cmdArgs = null;
25210
+ if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
25211
+ sql = String(a.sql ?? "").trim();
25212
+ if (!sql) {
25213
+ return { content: [{ type: "text", text: "sql is required" }], isError: true };
25214
+ }
25215
+ }
25216
+ if (command === "hypo") {
25217
+ const query = String(a.query ?? "").trim();
25218
+ if (!query) {
25219
+ return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
25220
+ }
25221
+ cmdArgs = { query };
25222
+ }
25223
+ if (command === "terminate") {
25224
+ const raw = a.pid;
25225
+ const pid = typeof raw === "number" ? raw : typeof raw === "string" && /^[0-9]+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
25226
+ if (!Number.isInteger(pid) || pid <= 0) {
25227
+ return {
25228
+ content: [{ type: "text", text: "pid must be a strict positive integer (backend pid) for terminate_backend" }],
25229
+ isError: true
25230
+ };
25231
+ }
25232
+ cmdArgs = { pid };
25233
+ }
25234
+ if (command === "describe") {
25235
+ const object4 = String(a.object ?? "").trim();
25236
+ if (!object4) {
25237
+ return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
25238
+ }
25239
+ cmdArgs = { object: object4 };
25240
+ if (a.variant !== undefined)
25241
+ cmdArgs.variant = String(a.variant);
25242
+ }
25243
+ const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
25244
+ const newSession = a.new_session === true;
25245
+ const budgetMs = sanitizeBudgetMs(a.timeout_ms);
25246
+ const outcome = await executeJoeCommand({
25247
+ apiKey: params.apiKey,
25248
+ apiBaseUrl: params.apiBaseUrl,
25249
+ command,
25250
+ project,
25251
+ sql,
25252
+ args: cmdArgs,
25253
+ session,
25254
+ newSession,
25255
+ orgId: params.orgId,
25256
+ budgetMs,
25257
+ debug: params.debug
25258
+ });
25259
+ const payload = {
25260
+ command,
25261
+ command_id: outcome.commandId,
25262
+ session_id: outcome.sessionId,
25263
+ status: outcome.status,
25264
+ budget_expired: outcome.budgetExpired,
25265
+ resume: outcome.budgetExpired ? `pgai joe result ${outcome.commandId}` : undefined,
25266
+ result: outcome.result
25267
+ };
25268
+ const isError = outcome.status === "error" || outcome.status === "timed_out";
25269
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
25270
+ }
25271
+ async function resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfgOrgId, debug) {
25272
+ const project = String(args.project || "").trim();
25273
+ if (!project) {
25274
+ throw new Error("project is required (project id or alias)");
25275
+ }
25276
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
25277
+ return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
25278
+ }
25279
+ function optStr(v) {
25280
+ return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
25281
+ }
25282
+ async function handleToolCall(req, rootOpts, extra) {
25283
+ const toolName = req.params.name;
25284
+ const args = req.params.arguments || {};
25285
+ const cfg = readConfig2();
25286
+ const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
25287
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
25288
+ const debug = Boolean(args.debug ?? extra?.debug);
25289
+ if (!apiKey) {
24387
25290
  return {
24388
25291
  content: [
24389
25292
  {
@@ -24653,6 +25556,147 @@ async function handleToolCall(req, rootOpts, extra) {
24653
25556
  const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
24654
25557
  return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
24655
25558
  }
25559
+ if (toolName === "list_projects") {
25560
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
25561
+ const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
25562
+ return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
25563
+ }
25564
+ if (toolName in JOE_TOOL_TO_COMMAND) {
25565
+ const orgId = cfg.orgId ?? undefined;
25566
+ return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
25567
+ }
25568
+ if (toolName === "clone_create") {
25569
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25570
+ const result = await createClone({
25571
+ apiKey,
25572
+ apiBaseUrl,
25573
+ instanceId,
25574
+ cloneId: optStr(args.clone_id),
25575
+ branch: optStr(args.branch),
25576
+ snapshotId: optStr(args.snapshot_id),
25577
+ dbUser: optStr(args.db_user),
25578
+ dbPassword: optStr(args.db_password),
25579
+ isProtected: args.protected === true,
25580
+ debug
25581
+ });
25582
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25583
+ }
25584
+ if (toolName === "clone_list") {
25585
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25586
+ const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
25587
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25588
+ }
25589
+ if (toolName === "clone_status") {
25590
+ const cloneId = String(args.clone_id || "").trim();
25591
+ if (!cloneId)
25592
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25593
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25594
+ const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
25595
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25596
+ }
25597
+ if (toolName === "clone_reset") {
25598
+ const cloneId = String(args.clone_id || "").trim();
25599
+ if (!cloneId)
25600
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25601
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25602
+ const result = await resetClone({
25603
+ apiKey,
25604
+ apiBaseUrl,
25605
+ instanceId,
25606
+ cloneId,
25607
+ snapshotId: optStr(args.snapshot_id),
25608
+ latest: args.latest === true ? true : undefined,
25609
+ debug
25610
+ });
25611
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
25612
+ }
25613
+ if (toolName === "clone_destroy") {
25614
+ const cloneId = String(args.clone_id || "").trim();
25615
+ if (!cloneId)
25616
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25617
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25618
+ const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
25619
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
25620
+ }
25621
+ if (toolName === "branch_list") {
25622
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25623
+ const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
25624
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25625
+ }
25626
+ if (toolName === "branch_create") {
25627
+ const branchName = String(args.name || "").trim();
25628
+ if (!branchName)
25629
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25630
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25631
+ const result = await createBranch({
25632
+ apiKey,
25633
+ apiBaseUrl,
25634
+ instanceId,
25635
+ branchName,
25636
+ baseBranch: optStr(args.base_branch),
25637
+ snapshotId: optStr(args.snapshot_id),
25638
+ debug
25639
+ });
25640
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25641
+ }
25642
+ if (toolName === "branch_delete") {
25643
+ const branchName = String(args.name || "").trim();
25644
+ if (!branchName)
25645
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25646
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25647
+ const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
25648
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
25649
+ }
25650
+ if (toolName === "branch_log") {
25651
+ const branchName = String(args.name || "").trim();
25652
+ if (!branchName)
25653
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25654
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25655
+ const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
25656
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25657
+ }
25658
+ if (toolName === "snapshot_list") {
25659
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25660
+ const result = await listSnapshots({
25661
+ apiKey,
25662
+ apiBaseUrl,
25663
+ instanceId,
25664
+ branchName: optStr(args.branch),
25665
+ dataset: optStr(args.dataset),
25666
+ debug
25667
+ });
25668
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25669
+ }
25670
+ if (toolName === "snapshot_create") {
25671
+ const cloneId = String(args.clone_id || "").trim();
25672
+ if (!cloneId)
25673
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25674
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25675
+ const result = await createSnapshot({
25676
+ apiKey,
25677
+ apiBaseUrl,
25678
+ instanceId,
25679
+ cloneId,
25680
+ message: optStr(args.message),
25681
+ debug
25682
+ });
25683
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25684
+ }
25685
+ if (toolName === "snapshot_destroy") {
25686
+ const snapshotId = String(args.snapshot_id || "").trim();
25687
+ if (!snapshotId)
25688
+ return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
25689
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25690
+ const result = await destroySnapshot({
25691
+ apiKey,
25692
+ apiBaseUrl,
25693
+ instanceId,
25694
+ snapshotId,
25695
+ force: args.force === true,
25696
+ debug
25697
+ });
25698
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
25699
+ }
24656
25700
  throw new Error(`Unknown tool: ${toolName}`);
24657
25701
  } catch (err) {
24658
25702
  const message = err instanceof Error ? err.message : String(err);
@@ -24929,6 +25973,197 @@ async function startMcpServer(rootOpts, extra) {
24929
25973
  },
24930
25974
  additionalProperties: false
24931
25975
  }
25976
+ },
25977
+ ...joeToolDefinitions(),
25978
+ {
25979
+ name: "clone_create",
25980
+ description: "Create a DBLab thin clone of the project's database (same as CLI 'pgai dblab clone create'). Requires the joe:plan scope.",
25981
+ inputSchema: {
25982
+ type: "object",
25983
+ properties: {
25984
+ project: { type: "string", description: "Project id or alias" },
25985
+ branch: { type: "string", description: "Branch to clone from" },
25986
+ snapshot_id: { type: "string", description: "Snapshot id to clone from" },
25987
+ clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
25988
+ db_user: { type: "string", description: "Clone DB user" },
25989
+ db_password: { type: "string", description: "Clone DB password" },
25990
+ protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
25991
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25992
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25993
+ },
25994
+ required: ["project"],
25995
+ additionalProperties: false
25996
+ }
25997
+ },
25998
+ {
25999
+ name: "clone_list",
26000
+ description: "List the project's DBLab thin clones (same as CLI 'pgai dblab clone list').",
26001
+ inputSchema: {
26002
+ type: "object",
26003
+ properties: {
26004
+ project: { type: "string", description: "Project id or alias" },
26005
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26006
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26007
+ },
26008
+ required: ["project"],
26009
+ additionalProperties: false
26010
+ }
26011
+ },
26012
+ {
26013
+ name: "clone_status",
26014
+ description: "Show a DBLab clone's status (same as CLI 'pgai dblab clone status').",
26015
+ inputSchema: {
26016
+ type: "object",
26017
+ properties: {
26018
+ project: { type: "string", description: "Project id or alias" },
26019
+ clone_id: { type: "string", description: "Clone id" },
26020
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26021
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26022
+ },
26023
+ required: ["project", "clone_id"],
26024
+ additionalProperties: false
26025
+ }
26026
+ },
26027
+ {
26028
+ name: "clone_reset",
26029
+ description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'pgai dblab clone reset'). Requires the joe:admin scope.",
26030
+ inputSchema: {
26031
+ type: "object",
26032
+ properties: {
26033
+ project: { type: "string", description: "Project id or alias" },
26034
+ clone_id: { type: "string", description: "Clone id" },
26035
+ snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
26036
+ latest: { type: "boolean", description: "Reset to the latest snapshot" },
26037
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26038
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26039
+ },
26040
+ required: ["project", "clone_id"],
26041
+ additionalProperties: false
26042
+ }
26043
+ },
26044
+ {
26045
+ name: "clone_destroy",
26046
+ description: "Destroy a DBLab clone (same as CLI 'pgai dblab clone destroy'). Requires the joe:admin scope.",
26047
+ inputSchema: {
26048
+ type: "object",
26049
+ properties: {
26050
+ project: { type: "string", description: "Project id or alias" },
26051
+ clone_id: { type: "string", description: "Clone id" },
26052
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26053
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26054
+ },
26055
+ required: ["project", "clone_id"],
26056
+ additionalProperties: false
26057
+ }
26058
+ },
26059
+ {
26060
+ name: "branch_list",
26061
+ description: "List the project's DBLab branches (same as CLI 'pgai dblab branch list').",
26062
+ inputSchema: {
26063
+ type: "object",
26064
+ properties: {
26065
+ project: { type: "string", description: "Project id or alias" },
26066
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26067
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26068
+ },
26069
+ required: ["project"],
26070
+ additionalProperties: false
26071
+ }
26072
+ },
26073
+ {
26074
+ name: "branch_create",
26075
+ description: "Create a DBLab branch (same as CLI 'pgai dblab branch create'). Requires the joe:plan scope.",
26076
+ inputSchema: {
26077
+ type: "object",
26078
+ properties: {
26079
+ project: { type: "string", description: "Project id or alias" },
26080
+ name: { type: "string", description: "Branch name" },
26081
+ snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
26082
+ base_branch: { type: "string", description: "Parent branch to fork from" },
26083
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26084
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26085
+ },
26086
+ required: ["project", "name"],
26087
+ additionalProperties: false
26088
+ }
26089
+ },
26090
+ {
26091
+ name: "branch_delete",
26092
+ description: "Delete a DBLab branch (same as CLI 'pgai dblab branch delete'). Requires the joe:admin scope.",
26093
+ inputSchema: {
26094
+ type: "object",
26095
+ properties: {
26096
+ project: { type: "string", description: "Project id or alias" },
26097
+ name: { type: "string", description: "Branch name" },
26098
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26099
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26100
+ },
26101
+ required: ["project", "name"],
26102
+ additionalProperties: false
26103
+ }
26104
+ },
26105
+ {
26106
+ name: "branch_log",
26107
+ description: "Show a DBLab branch's snapshot log (same as CLI 'pgai dblab branch log').",
26108
+ inputSchema: {
26109
+ type: "object",
26110
+ properties: {
26111
+ project: { type: "string", description: "Project id or alias" },
26112
+ name: { type: "string", description: "Branch name" },
26113
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26114
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26115
+ },
26116
+ required: ["project", "name"],
26117
+ additionalProperties: false
26118
+ }
26119
+ },
26120
+ {
26121
+ name: "snapshot_list",
26122
+ description: "List the project's DBLab snapshots (same as CLI 'pgai dblab snapshot list').",
26123
+ inputSchema: {
26124
+ type: "object",
26125
+ properties: {
26126
+ project: { type: "string", description: "Project id or alias" },
26127
+ branch: { type: "string", description: "Filter by branch" },
26128
+ dataset: { type: "string", description: "Filter by dataset" },
26129
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26130
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26131
+ },
26132
+ required: ["project"],
26133
+ additionalProperties: false
26134
+ }
26135
+ },
26136
+ {
26137
+ name: "snapshot_create",
26138
+ description: "Create a DBLab snapshot from a clone (same as CLI 'pgai dblab snapshot create'). Requires the joe:plan scope.",
26139
+ inputSchema: {
26140
+ type: "object",
26141
+ properties: {
26142
+ project: { type: "string", description: "Project id or alias" },
26143
+ clone_id: { type: "string", description: "Clone id to snapshot" },
26144
+ message: { type: "string", description: "Snapshot message" },
26145
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26146
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26147
+ },
26148
+ required: ["project", "clone_id"],
26149
+ additionalProperties: false
26150
+ }
26151
+ },
26152
+ {
26153
+ name: "snapshot_destroy",
26154
+ description: "Destroy a DBLab snapshot (same as CLI 'pgai dblab snapshot destroy'). Requires the joe:admin scope.",
26155
+ inputSchema: {
26156
+ type: "object",
26157
+ properties: {
26158
+ project: { type: "string", description: "Project id or alias" },
26159
+ snapshot_id: { type: "string", description: "Snapshot id" },
26160
+ force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
26161
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26162
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26163
+ },
26164
+ required: ["project", "snapshot_id"],
26165
+ additionalProperties: false
26166
+ }
24932
26167
  }
24933
26168
  ]
24934
26169
  };
@@ -25801,11 +27036,699 @@ function renderMarkdownForTerminal(md) {
25801
27036
  `);
25802
27037
  }
25803
27038
 
27039
+ // lib/joe.ts
27040
+ import * as fs5 from "fs";
27041
+ import * as path5 from "path";
27042
+ import * as crypto2 from "node:crypto";
27043
+ var JOE_COMMANDS2 = [
27044
+ "plan",
27045
+ "explain",
27046
+ "exec",
27047
+ "hypo",
27048
+ "activity",
27049
+ "terminate",
27050
+ "reset",
27051
+ "describe"
27052
+ ];
27053
+ var TERMINAL_STATES2 = new Set([
27054
+ "done",
27055
+ "error",
27056
+ "timed_out"
27057
+ ]);
27058
+ var AI_ENABLED_COMMANDS2 = new Set([
27059
+ "plan",
27060
+ "explain",
27061
+ "exec",
27062
+ "hypo",
27063
+ "activity",
27064
+ "describe"
27065
+ ]);
27066
+ var EXECUTING_COMMANDS2 = new Set([
27067
+ "exec",
27068
+ "explain"
27069
+ ]);
27070
+ var DEFAULT_BUDGET_MS2 = 25000;
27071
+ var DEFAULT_POLL_INTERVAL_MS2 = 800;
27072
+ async function callRpc2(params) {
27073
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
27074
+ if (!apiKey) {
27075
+ throw new Error("API key is required");
27076
+ }
27077
+ const base = normalizeBaseUrl(apiBaseUrl);
27078
+ const url = new URL(`${base}/rpc/${fn}`);
27079
+ const payload = JSON.stringify(body);
27080
+ const headers = {
27081
+ "access-token": apiKey,
27082
+ "Content-Type": "application/json",
27083
+ Connection: "close"
27084
+ };
27085
+ if (debug) {
27086
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
27087
+ console.error(`Debug: POST URL: ${url.toString()}`);
27088
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27089
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
27090
+ }
27091
+ let response;
27092
+ try {
27093
+ response = await fetch(url.toString(), {
27094
+ method: "POST",
27095
+ headers,
27096
+ body: payload
27097
+ });
27098
+ } catch (err) {
27099
+ throw new Error(describeFetchError(operation, base, err));
27100
+ }
27101
+ const text = await response.text();
27102
+ if (debug) {
27103
+ console.error(`Debug: Response status: ${response.status}`);
27104
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27105
+ }
27106
+ if (!response.ok) {
27107
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
27108
+ }
27109
+ try {
27110
+ return JSON.parse(text);
27111
+ } catch {
27112
+ throw new Error(`${operation}: failed to parse response: ${text}`);
27113
+ }
27114
+ }
27115
+ async function submitCommand2(params) {
27116
+ const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
27117
+ if (!JOE_COMMANDS2.includes(command)) {
27118
+ throw new Error(`Unknown Joe command: ${command}`);
27119
+ }
27120
+ const body = {
27121
+ project_id: projectId,
27122
+ command,
27123
+ sql: sql ?? null,
27124
+ args: args ?? null,
27125
+ session_id: sessionId ?? null
27126
+ };
27127
+ if (idempotencyKey) {
27128
+ body.idempotency_key = idempotencyKey;
27129
+ }
27130
+ return callRpc2({
27131
+ apiKey,
27132
+ apiBaseUrl,
27133
+ fn: "joe_command_submit",
27134
+ body,
27135
+ operation: `Failed to submit ${command} command`,
27136
+ debug
27137
+ });
27138
+ }
27139
+ async function getCommandStatus2(params) {
27140
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
27141
+ if (!commandId) {
27142
+ throw new Error("commandId is required");
27143
+ }
27144
+ return callRpc2({
27145
+ apiKey,
27146
+ apiBaseUrl,
27147
+ fn: "joe_command_status",
27148
+ body: { command_id: commandId },
27149
+ operation: "Failed to fetch command status",
27150
+ debug
27151
+ });
27152
+ }
27153
+ async function getCommandResult2(params) {
27154
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
27155
+ if (!commandId) {
27156
+ throw new Error("commandId is required");
27157
+ }
27158
+ return callRpc2({
27159
+ apiKey,
27160
+ apiBaseUrl,
27161
+ fn: "joe_command_result",
27162
+ body: { command_id: commandId },
27163
+ operation: "Failed to fetch command result",
27164
+ debug
27165
+ });
27166
+ }
27167
+ function normalizeProjectRow2(row) {
27168
+ const projectId = row.project_id ?? row.id ?? 0;
27169
+ return {
27170
+ project_id: Number(projectId),
27171
+ alias: row.alias ?? null,
27172
+ name: row.name ?? null,
27173
+ label: row.label ?? null,
27174
+ joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
27175
+ tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
27176
+ instance_id: row.instance_id == null ? null : Number(row.instance_id),
27177
+ dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
27178
+ };
27179
+ }
27180
+ async function listProjects2(params) {
27181
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
27182
+ const body = {};
27183
+ if (typeof orgId === "number") {
27184
+ body.org_id = orgId;
27185
+ }
27186
+ const rows = await callRpc2({
27187
+ apiKey,
27188
+ apiBaseUrl,
27189
+ fn: "projects_list",
27190
+ body,
27191
+ operation: "Failed to list projects",
27192
+ debug
27193
+ });
27194
+ if (!Array.isArray(rows)) {
27195
+ return [];
27196
+ }
27197
+ return rows.map(normalizeProjectRow2);
27198
+ }
27199
+ function isNumericProjectRef3(ref) {
27200
+ return /^[0-9]+$/.test(ref.trim());
27201
+ }
27202
+ async function resolveProjectId2(params) {
27203
+ const ref = String(params.project ?? "").trim();
27204
+ if (!ref) {
27205
+ throw new Error("project is required (--project <id|alias>)");
27206
+ }
27207
+ if (isNumericProjectRef3(ref)) {
27208
+ return Number(ref);
27209
+ }
27210
+ const projects = await listProjects2({
27211
+ apiKey: params.apiKey,
27212
+ apiBaseUrl: params.apiBaseUrl,
27213
+ orgId: params.orgId,
27214
+ debug: params.debug
27215
+ });
27216
+ const needle = ref.toLowerCase();
27217
+ const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle);
27218
+ if (!match) {
27219
+ throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
27220
+ }
27221
+ return match.project_id;
27222
+ }
27223
+ function sessionStorePath2(dir) {
27224
+ return path5.join(dir ?? getConfigDir2(), "joe-sessions.json");
27225
+ }
27226
+ function readSessionStore2(dir) {
27227
+ const file = sessionStorePath2(dir);
27228
+ if (!fs5.existsSync(file)) {
27229
+ return {};
27230
+ }
27231
+ try {
27232
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
27233
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
27234
+ return parsed;
27235
+ }
27236
+ } catch {}
27237
+ return {};
27238
+ }
27239
+ function readStoredSessionId2(projectId, dir) {
27240
+ const store = readSessionStore2(dir);
27241
+ const value = store[String(projectId)];
27242
+ return typeof value === "string" && value.length > 0 ? value : null;
27243
+ }
27244
+ function writeStoredSessionId2(projectId, sessionId, dir) {
27245
+ const baseDir = dir ?? getConfigDir2();
27246
+ if (!fs5.existsSync(baseDir)) {
27247
+ fs5.mkdirSync(baseDir, { recursive: true, mode: 448 });
27248
+ }
27249
+ const store = readSessionStore2(dir);
27250
+ store[String(projectId)] = sessionId;
27251
+ fs5.writeFileSync(sessionStorePath2(dir), JSON.stringify(store, null, 2) + `
27252
+ `, { mode: 384 });
27253
+ }
27254
+ function clearStoredSessionId2(projectId, dir) {
27255
+ const file = sessionStorePath2(dir);
27256
+ if (!fs5.existsSync(file)) {
27257
+ return;
27258
+ }
27259
+ const store = readSessionStore2(dir);
27260
+ if (String(projectId) in store) {
27261
+ delete store[String(projectId)];
27262
+ fs5.writeFileSync(file, JSON.stringify(store, null, 2) + `
27263
+ `, { mode: 384 });
27264
+ }
27265
+ }
27266
+ function computeIdempotencyKey2(command, projectId, sql, args) {
27267
+ if (EXECUTING_COMMANDS2.has(command)) {
27268
+ const canonical = `${projectId}
27269
+ ${command}
27270
+ ${sql ?? ""}
27271
+ ${JSON.stringify(args ?? null)}`;
27272
+ return `joe-${crypto2.createHash("sha256").update(canonical).digest("hex")}`;
27273
+ }
27274
+ return `joe-${crypto2.randomUUID()}`;
27275
+ }
27276
+ var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
27277
+ async function runCommand2(params) {
27278
+ const {
27279
+ apiKey,
27280
+ apiBaseUrl,
27281
+ command,
27282
+ projectId,
27283
+ sql,
27284
+ args,
27285
+ sessionId,
27286
+ debug
27287
+ } = params;
27288
+ const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) ? params.budgetMs : DEFAULT_BUDGET_MS2;
27289
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
27290
+ const now = params.now ?? Date.now;
27291
+ const sleep = params.sleep ?? defaultSleep2;
27292
+ const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey2(command, projectId, sql, args);
27293
+ const submitted = await submitCommand2({
27294
+ apiKey,
27295
+ apiBaseUrl,
27296
+ command,
27297
+ projectId,
27298
+ sql,
27299
+ args,
27300
+ sessionId,
27301
+ idempotencyKey,
27302
+ debug
27303
+ });
27304
+ const commandId = submitted.command_id;
27305
+ const newSessionId = submitted.session_id ?? sessionId ?? null;
27306
+ const deadline = now() + budgetMs;
27307
+ let status = submitted.status;
27308
+ while (true) {
27309
+ const statusResp = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug });
27310
+ status = statusResp.status;
27311
+ if (TERMINAL_STATES2.has(status)) {
27312
+ break;
27313
+ }
27314
+ if (now() >= deadline) {
27315
+ return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
27316
+ }
27317
+ await sleep(pollIntervalMs);
27318
+ }
27319
+ const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
27320
+ return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
27321
+ }
27322
+ async function executeJoeCommand2(params) {
27323
+ const projectId = await resolveProjectId2({
27324
+ apiKey: params.apiKey,
27325
+ apiBaseUrl: params.apiBaseUrl,
27326
+ project: params.project,
27327
+ orgId: params.orgId,
27328
+ debug: params.debug
27329
+ });
27330
+ let sessionId;
27331
+ if (params.newSession) {
27332
+ clearStoredSessionId2(projectId, params.sessionDir);
27333
+ sessionId = null;
27334
+ } else if (params.session) {
27335
+ sessionId = String(params.session);
27336
+ } else {
27337
+ sessionId = readStoredSessionId2(projectId, params.sessionDir);
27338
+ }
27339
+ const outcome = await runCommand2({
27340
+ apiKey: params.apiKey,
27341
+ apiBaseUrl: params.apiBaseUrl,
27342
+ command: params.command,
27343
+ projectId,
27344
+ sql: params.sql,
27345
+ args: params.args,
27346
+ sessionId,
27347
+ budgetMs: params.budgetMs,
27348
+ pollIntervalMs: params.pollIntervalMs,
27349
+ debug: params.debug,
27350
+ now: params.now,
27351
+ sleep: params.sleep
27352
+ });
27353
+ if (outcome.sessionId) {
27354
+ writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
27355
+ }
27356
+ return { ...outcome, command: params.command, projectId };
27357
+ }
27358
+ function clientSidePlanFlags(planJson) {
27359
+ const flags = [];
27360
+ const walk = (node) => {
27361
+ if (!node || typeof node !== "object") {
27362
+ return;
27363
+ }
27364
+ const nodeType = node["Node Type"];
27365
+ if (nodeType === "Seq Scan") {
27366
+ const rel = node["Relation Name"];
27367
+ flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
27368
+ }
27369
+ if (Array.isArray(node.Plans)) {
27370
+ for (const child of node.Plans) {
27371
+ walk(child);
27372
+ }
27373
+ }
27374
+ };
27375
+ if (planJson && typeof planJson === "object") {
27376
+ const root = planJson;
27377
+ walk(root.Plan ?? planJson);
27378
+ }
27379
+ return flags;
27380
+ }
27381
+ function formatJoeResult(command, result) {
27382
+ const lines = [];
27383
+ switch (command) {
27384
+ case "plan":
27385
+ case "explain": {
27386
+ if (result.plan_text) {
27387
+ lines.push(result.plan_text);
27388
+ }
27389
+ for (const flag of clientSidePlanFlags(result.plan_json)) {
27390
+ lines.push(`⚑ ${flag}`);
27391
+ }
27392
+ if (result.queryid || result.plan_fingerprint) {
27393
+ lines.push(`(queryid ${result.queryid ?? "?"} · plan_fp ${result.plan_fingerprint ?? "?"})`);
27394
+ }
27395
+ break;
27396
+ }
27397
+ case "exec": {
27398
+ const notices = result.notices ?? [];
27399
+ const rowCount = result.row_count ?? 0;
27400
+ lines.push(`${notices.join(" ") || "OK"} · ${rowCount} row${rowCount === 1 ? "" : "s"}`);
27401
+ if (result.result_rows && result.result_rows.length > 0) {
27402
+ lines.push(JSON.stringify(result.result_rows, null, 2));
27403
+ }
27404
+ break;
27405
+ }
27406
+ case "hypo": {
27407
+ lines.push(result.hypo_used ? "hypothetical index would be used" : "hypothetical index would NOT be used");
27408
+ if (result.hypo_plan !== undefined) {
27409
+ lines.push(JSON.stringify(result.hypo_plan, null, 2));
27410
+ }
27411
+ break;
27412
+ }
27413
+ case "activity":
27414
+ case "describe": {
27415
+ lines.push(JSON.stringify(result.snapshot ?? null, null, 2));
27416
+ break;
27417
+ }
27418
+ case "terminate": {
27419
+ lines.push(`terminated ${result.terminated ? "yes" : "no"} · pid ${result.pid ?? "?"}`);
27420
+ break;
27421
+ }
27422
+ case "reset": {
27423
+ lines.push(`reset ${result.reset ? "ok" : "no"}`);
27424
+ break;
27425
+ }
27426
+ }
27427
+ return lines.join(`
27428
+ `);
27429
+ }
27430
+ function formatProjectsTable(projects) {
27431
+ const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
27432
+ const rows = projects.map((p) => [
27433
+ String(p.project_id),
27434
+ p.alias ?? "-",
27435
+ p.name ?? "-",
27436
+ p.joe_ready ? "ready" : "no",
27437
+ p.tunnel ? "yes" : "no"
27438
+ ]);
27439
+ const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
27440
+ const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
27441
+ return [pad(header), ...rows.map(pad)].join(`
27442
+ `);
27443
+ }
27444
+
27445
+ // lib/dblab.ts
27446
+ function isNumericProjectRef4(ref) {
27447
+ return /^[0-9]+$/.test(ref.trim());
27448
+ }
27449
+ async function resolveDblabInstanceId2(params) {
27450
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
27451
+ if (!apiKey) {
27452
+ throw new Error("API key is required");
27453
+ }
27454
+ const ref = String(params.project ?? "").trim();
27455
+ if (!ref) {
27456
+ throw new Error("project is required (--project <id|alias>)");
27457
+ }
27458
+ let projects;
27459
+ try {
27460
+ projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
27461
+ } catch (err) {
27462
+ const message = err instanceof Error ? err.message : String(err);
27463
+ throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
27464
+ }
27465
+ const numeric = isNumericProjectRef4(ref);
27466
+ const needle = ref.toLowerCase();
27467
+ const match = projects.find((p) => {
27468
+ if (numeric) {
27469
+ return String(p.project_id) === ref;
27470
+ }
27471
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
27472
+ });
27473
+ if (!match) {
27474
+ throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
27475
+ }
27476
+ if (match.dblab_instance_id == null) {
27477
+ throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
27478
+ }
27479
+ return String(match.dblab_instance_id);
27480
+ }
27481
+ async function callDblabApi2(params) {
27482
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
27483
+ if (!apiKey) {
27484
+ throw new Error("API key is required");
27485
+ }
27486
+ if (!instanceId) {
27487
+ throw new Error("instanceId is required");
27488
+ }
27489
+ const base = normalizeBaseUrl(apiBaseUrl);
27490
+ const url = new URL(`${base}/rpc/dblab_api_call`);
27491
+ const bodyObj = {
27492
+ instance_id: instanceId,
27493
+ action,
27494
+ method
27495
+ };
27496
+ if (data !== undefined) {
27497
+ bodyObj.data = data;
27498
+ }
27499
+ const body = JSON.stringify(bodyObj);
27500
+ const headers = {
27501
+ "access-token": apiKey,
27502
+ "Content-Type": "application/json",
27503
+ Connection: "close"
27504
+ };
27505
+ if (debug) {
27506
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
27507
+ console.error(`Debug: POST URL: ${url.toString()}`);
27508
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27509
+ console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
27510
+ }
27511
+ let response;
27512
+ try {
27513
+ response = await fetch(url.toString(), { method: "POST", headers, body });
27514
+ } catch (err) {
27515
+ throw new Error(describeFetchError(operation, base, err));
27516
+ }
27517
+ const text = await response.text();
27518
+ if (debug) {
27519
+ console.error(`Debug: Response status: ${response.status}`);
27520
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27521
+ }
27522
+ if (!response.ok) {
27523
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
27524
+ }
27525
+ if (text.trim() === "") {
27526
+ return null;
27527
+ }
27528
+ try {
27529
+ return JSON.parse(text);
27530
+ } catch {
27531
+ throw new Error(`${operation}: failed to parse response: ${text}`);
27532
+ }
27533
+ }
27534
+ async function createClone2(params) {
27535
+ const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
27536
+ const data = { protected: Boolean(isProtected) };
27537
+ if (cloneId)
27538
+ data.id = cloneId;
27539
+ if (branch)
27540
+ data.branch = branch;
27541
+ if (snapshotId)
27542
+ data.snapshot = { id: snapshotId };
27543
+ if (dbUser && dbPassword)
27544
+ data.db = { username: dbUser, password: dbPassword };
27545
+ return callDblabApi2({
27546
+ apiKey,
27547
+ apiBaseUrl,
27548
+ instanceId,
27549
+ action: "/clone",
27550
+ method: "post",
27551
+ data,
27552
+ operation: "Failed to create clone",
27553
+ debug
27554
+ });
27555
+ }
27556
+ async function listClones2(params) {
27557
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
27558
+ return callDblabApi2({
27559
+ apiKey,
27560
+ apiBaseUrl,
27561
+ instanceId,
27562
+ action: "/clones",
27563
+ method: "get",
27564
+ operation: "Failed to list clones",
27565
+ debug
27566
+ });
27567
+ }
27568
+ async function getClone2(params) {
27569
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
27570
+ if (!cloneId)
27571
+ throw new Error("cloneId is required");
27572
+ return callDblabApi2({
27573
+ apiKey,
27574
+ apiBaseUrl,
27575
+ instanceId,
27576
+ action: `/clone/${encodeURIComponent(cloneId)}`,
27577
+ method: "get",
27578
+ operation: "Failed to get clone",
27579
+ debug
27580
+ });
27581
+ }
27582
+ async function resetClone2(params) {
27583
+ const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
27584
+ if (!cloneId)
27585
+ throw new Error("cloneId is required");
27586
+ const data = { latest: latest ?? !snapshotId };
27587
+ if (snapshotId)
27588
+ data.snapshotID = snapshotId;
27589
+ return callDblabApi2({
27590
+ apiKey,
27591
+ apiBaseUrl,
27592
+ instanceId,
27593
+ action: `/clone/${encodeURIComponent(cloneId)}/reset`,
27594
+ method: "post",
27595
+ data,
27596
+ operation: "Failed to reset clone",
27597
+ debug
27598
+ });
27599
+ }
27600
+ async function destroyClone2(params) {
27601
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
27602
+ if (!cloneId)
27603
+ throw new Error("cloneId is required");
27604
+ return callDblabApi2({
27605
+ apiKey,
27606
+ apiBaseUrl,
27607
+ instanceId,
27608
+ action: `/clone/${encodeURIComponent(cloneId)}`,
27609
+ method: "delete",
27610
+ operation: "Failed to destroy clone",
27611
+ debug
27612
+ });
27613
+ }
27614
+ async function listBranches2(params) {
27615
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
27616
+ return callDblabApi2({
27617
+ apiKey,
27618
+ apiBaseUrl,
27619
+ instanceId,
27620
+ action: "/branches",
27621
+ method: "get",
27622
+ operation: "Failed to list branches",
27623
+ debug
27624
+ });
27625
+ }
27626
+ async function createBranch2(params) {
27627
+ const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
27628
+ if (!branchName)
27629
+ throw new Error("branchName is required");
27630
+ const data = { branchName };
27631
+ if (baseBranch)
27632
+ data.baseBranch = baseBranch;
27633
+ if (snapshotId)
27634
+ data.snapshotID = snapshotId;
27635
+ return callDblabApi2({
27636
+ apiKey,
27637
+ apiBaseUrl,
27638
+ instanceId,
27639
+ action: "/branch",
27640
+ method: "post",
27641
+ data,
27642
+ operation: "Failed to create branch",
27643
+ debug
27644
+ });
27645
+ }
27646
+ async function deleteBranch2(params) {
27647
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
27648
+ if (!branchName)
27649
+ throw new Error("branchName is required");
27650
+ return callDblabApi2({
27651
+ apiKey,
27652
+ apiBaseUrl,
27653
+ instanceId,
27654
+ action: `/branch/${encodeURIComponent(branchName)}`,
27655
+ method: "delete",
27656
+ operation: "Failed to delete branch",
27657
+ debug
27658
+ });
27659
+ }
27660
+ async function branchLog2(params) {
27661
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
27662
+ if (!branchName)
27663
+ throw new Error("branchName is required");
27664
+ return callDblabApi2({
27665
+ apiKey,
27666
+ apiBaseUrl,
27667
+ instanceId,
27668
+ action: `/branch/${encodeURIComponent(branchName)}/log`,
27669
+ method: "get",
27670
+ operation: "Failed to fetch branch log",
27671
+ debug
27672
+ });
27673
+ }
27674
+ async function listSnapshots2(params) {
27675
+ const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
27676
+ const qs = new URLSearchParams;
27677
+ const branch = branchName?.trim();
27678
+ if (branch)
27679
+ qs.append("branch", branch);
27680
+ if (dataset)
27681
+ qs.append("dataset", dataset);
27682
+ const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
27683
+ return callDblabApi2({
27684
+ apiKey,
27685
+ apiBaseUrl,
27686
+ instanceId,
27687
+ action,
27688
+ method: "get",
27689
+ operation: "Failed to list snapshots",
27690
+ debug
27691
+ });
27692
+ }
27693
+ async function createSnapshot2(params) {
27694
+ const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
27695
+ if (!cloneId)
27696
+ throw new Error("cloneId is required");
27697
+ const data = { cloneID: cloneId };
27698
+ if (message)
27699
+ data.message = message;
27700
+ return callDblabApi2({
27701
+ apiKey,
27702
+ apiBaseUrl,
27703
+ instanceId,
27704
+ action: "/branch/snapshot",
27705
+ method: "post",
27706
+ data,
27707
+ operation: "Failed to create snapshot",
27708
+ debug
27709
+ });
27710
+ }
27711
+ async function destroySnapshot2(params) {
27712
+ const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
27713
+ if (!snapshotId)
27714
+ throw new Error("snapshotId is required");
27715
+ const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
27716
+ return callDblabApi2({
27717
+ apiKey,
27718
+ apiBaseUrl,
27719
+ instanceId,
27720
+ action,
27721
+ method: "delete",
27722
+ operation: "Failed to destroy snapshot",
27723
+ debug
27724
+ });
27725
+ }
27726
+
25804
27727
  // bin/postgres-ai.ts
25805
27728
  init_util();
25806
27729
 
25807
27730
  // lib/instances.ts
25808
- import * as fs4 from "fs";
27731
+ import * as fs6 from "fs";
25809
27732
 
25810
27733
  // node_modules/js-yaml/dist/js-yaml.mjs
25811
27734
  /*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
@@ -28470,11 +30393,11 @@ class InstancesParseError extends Error {
28470
30393
  }
28471
30394
  }
28472
30395
  function loadInstances(file) {
28473
- if (!fs4.existsSync(file))
30396
+ if (!fs6.existsSync(file))
28474
30397
  return [];
28475
- if (fs4.lstatSync(file).isDirectory())
30398
+ if (fs6.lstatSync(file).isDirectory())
28476
30399
  return [];
28477
- const text = fs4.readFileSync(file, "utf8");
30400
+ const text = fs6.readFileSync(file, "utf8");
28478
30401
  if (text.trim() === "")
28479
30402
  return [];
28480
30403
  let parsed;
@@ -28652,8 +30575,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
28652
30575
  }
28653
30576
 
28654
30577
  // lib/storage.ts
28655
- import * as fs5 from "fs";
28656
- import * as path4 from "path";
30578
+ import * as fs7 from "fs";
30579
+ import * as path6 from "path";
28657
30580
  var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
28658
30581
  var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
28659
30582
  var MIME_TYPES2 = {
@@ -28694,11 +30617,11 @@ async function uploadFile2(params) {
28694
30617
  if (!filePath) {
28695
30618
  throw new Error("filePath is required");
28696
30619
  }
28697
- const resolvedPath = path4.resolve(filePath);
28698
- if (!fs5.existsSync(resolvedPath)) {
30620
+ const resolvedPath = path6.resolve(filePath);
30621
+ if (!fs7.existsSync(resolvedPath)) {
28699
30622
  throw new Error(`File not found: ${resolvedPath}`);
28700
30623
  }
28701
- const stat = fs5.statSync(resolvedPath);
30624
+ const stat = fs7.statSync(resolvedPath);
28702
30625
  if (!stat.isFile()) {
28703
30626
  throw new Error(`Not a file: ${resolvedPath}`);
28704
30627
  }
@@ -28710,9 +30633,9 @@ async function uploadFile2(params) {
28710
30633
  console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
28711
30634
  }
28712
30635
  const url = `${base}/upload`;
28713
- const fileBuffer = fs5.readFileSync(resolvedPath);
28714
- const fileName = path4.basename(resolvedPath);
28715
- const ext = path4.extname(fileName).toLowerCase();
30636
+ const fileBuffer = fs7.readFileSync(resolvedPath);
30637
+ const fileName = path6.basename(resolvedPath);
30638
+ const ext = path6.extname(fileName).toLowerCase();
28716
30639
  const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
28717
30640
  const formData = new FormData;
28718
30641
  formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
@@ -28771,15 +30694,15 @@ async function downloadFile2(params) {
28771
30694
  const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
28772
30695
  fullUrl = `${base}${relativePath}`;
28773
30696
  }
28774
- const urlFilename = path4.basename(new URL(fullUrl).pathname);
30697
+ const urlFilename = path6.basename(new URL(fullUrl).pathname);
28775
30698
  if (!urlFilename) {
28776
30699
  throw new Error("Cannot derive filename from URL; please specify --output");
28777
30700
  }
28778
- const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
30701
+ const saveTo = outputPath ? path6.resolve(outputPath) : path6.resolve(urlFilename);
28779
30702
  if (!outputPath) {
28780
- const normalizedSave = path4.normalize(saveTo);
28781
- const cwd = path4.normalize(process.cwd());
28782
- if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
30703
+ const normalizedSave = path6.normalize(saveTo);
30704
+ const cwd = path6.normalize(process.cwd());
30705
+ if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path6.sep)) {
28783
30706
  throw new Error("Derived output path escapes current directory; please specify --output");
28784
30707
  }
28785
30708
  }
@@ -28811,11 +30734,11 @@ async function downloadFile2(params) {
28811
30734
  }
28812
30735
  const arrayBuffer = await response.arrayBuffer();
28813
30736
  const buffer = Buffer.from(arrayBuffer);
28814
- const parentDir = path4.dirname(saveTo);
28815
- if (!fs5.existsSync(parentDir)) {
28816
- fs5.mkdirSync(parentDir, { recursive: true });
30737
+ const parentDir = path6.dirname(saveTo);
30738
+ if (!fs7.existsSync(parentDir)) {
30739
+ fs7.mkdirSync(parentDir, { recursive: true });
28817
30740
  }
28818
- fs5.writeFileSync(saveTo, buffer);
30741
+ fs7.writeFileSync(saveTo, buffer);
28819
30742
  return {
28820
30743
  savedTo: saveTo,
28821
30744
  size: buffer.length,
@@ -28827,9 +30750,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
28827
30750
  const base = normalizeBaseUrl(storageBaseUrl);
28828
30751
  const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
28829
30752
  const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
28830
- const name = filename || path4.basename(new URL(fullUrl).pathname);
30753
+ const name = filename || path6.basename(new URL(fullUrl).pathname);
28831
30754
  const safeName = name.replace(/[\[\]()]/g, "\\$&");
28832
- const ext = path4.extname(name).toLowerCase();
30755
+ const ext = path6.extname(name).toLowerCase();
28833
30756
  if (IMAGE_EXTENSIONS2.has(ext)) {
28834
30757
  return `![${safeName}](${fullUrl})`;
28835
30758
  }
@@ -28875,8 +30798,8 @@ ${links}`;
28875
30798
  // lib/init.ts
28876
30799
  import { randomBytes } from "crypto";
28877
30800
  import { URL as URL2, fileURLToPath } from "url";
28878
- import * as fs6 from "fs";
28879
- import * as path5 from "path";
30801
+ import * as fs8 from "fs";
30802
+ import * as path7 from "path";
28880
30803
  var DEFAULT_MONITORING_USER = "postgres_ai_mon";
28881
30804
  var KNOWN_PROVIDERS = ["self-managed", "supabase"];
28882
30805
  var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
@@ -28968,21 +30891,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
28968
30891
  }
28969
30892
  function sqlDir() {
28970
30893
  const currentFile = fileURLToPath(import.meta.url);
28971
- const currentDir = path5.dirname(currentFile);
30894
+ const currentDir = path7.dirname(currentFile);
28972
30895
  const candidates = [
28973
- path5.resolve(currentDir, "..", "sql"),
28974
- path5.resolve(currentDir, "..", "..", "sql")
30896
+ path7.resolve(currentDir, "..", "sql"),
30897
+ path7.resolve(currentDir, "..", "..", "sql")
28975
30898
  ];
28976
30899
  for (const candidate of candidates) {
28977
- if (fs6.existsSync(candidate)) {
30900
+ if (fs8.existsSync(candidate)) {
28978
30901
  return candidate;
28979
30902
  }
28980
30903
  }
28981
30904
  throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
28982
30905
  }
28983
30906
  function loadSqlTemplate(filename) {
28984
- const p = path5.join(sqlDir(), filename);
28985
- return fs6.readFileSync(p, "utf8");
30907
+ const p = path7.join(sqlDir(), filename);
30908
+ return fs8.readFileSync(p, "utf8");
28986
30909
  }
28987
30910
  function applyTemplate(sql, vars) {
28988
30911
  return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
@@ -29452,10 +31375,6 @@ async function verifyInitSetup(params) {
29452
31375
  }
29453
31376
  }
29454
31377
  }
29455
- const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
29456
- if (!explainFnRes.rows?.[0]?.ok) {
29457
- missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
29458
- }
29459
31378
  const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
29460
31379
  if (!tableDescribeFnRes.rows?.[0]?.ok) {
29461
31380
  missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
@@ -29974,15 +31893,6 @@ async function verifyInitSetupViaSupabase(params) {
29974
31893
  missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
29975
31894
  }
29976
31895
  }
29977
- 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);
29978
- if (explainFnExistsRes.rowCount === 0) {
29979
- missingRequired.push("function postgres_ai.explain_generic exists");
29980
- } else {
29981
- const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
29982
- if (!explainFnRes.rows?.[0]?.ok) {
29983
- missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
29984
- }
29985
- }
29986
31896
  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);
29987
31897
  if (tableDescribeFnExistsRes.rowCount === 0) {
29988
31898
  missingRequired.push("function postgres_ai.table_describe exists");
@@ -30040,9 +31950,9 @@ function escapeLiteral2(value) {
30040
31950
  }
30041
31951
 
30042
31952
  // lib/pkce.ts
30043
- import * as crypto from "crypto";
31953
+ import * as crypto3 from "crypto";
30044
31954
  function generateRandomString(length = 64) {
30045
- const bytes = crypto.randomBytes(length);
31955
+ const bytes = crypto3.randomBytes(length);
30046
31956
  return base64URLEncode(bytes);
30047
31957
  }
30048
31958
  function base64URLEncode(buffer) {
@@ -30052,7 +31962,7 @@ function generateCodeVerifier() {
30052
31962
  return generateRandomString(32);
30053
31963
  }
30054
31964
  function generateCodeChallenge(verifier) {
30055
- const hash = crypto.createHash("sha256").update(verifier).digest();
31965
+ const hash = crypto3.createHash("sha256").update(verifier).digest();
30056
31966
  return base64URLEncode(hash);
30057
31967
  }
30058
31968
  function generateState() {
@@ -30281,8 +32191,8 @@ import { createInterface } from "readline";
30281
32191
  import * as childProcess from "child_process";
30282
32192
 
30283
32193
  // lib/checkup.ts
30284
- import * as fs7 from "fs";
30285
- import * as path6 from "path";
32194
+ import * as fs9 from "fs";
32195
+ import * as path8 from "path";
30286
32196
 
30287
32197
  // lib/metrics-embedded.ts
30288
32198
  var METRICS = {
@@ -32013,7 +33923,7 @@ function buildCheckInfoMap() {
32013
33923
  }
32014
33924
 
32015
33925
  // lib/checkup.ts
32016
- var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
33926
+ var __dirname = "/home/agent/workspace/postgresai-joe-v2/cli/lib";
32017
33927
  var SECONDS_PER_DAY = 86400;
32018
33928
  var SECONDS_PER_HOUR = 3600;
32019
33929
  var SECONDS_PER_MINUTE = 60;
@@ -32499,7 +34409,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
32499
34409
  }
32500
34410
  function readTextFileSafe(p) {
32501
34411
  try {
32502
- const value = fs7.readFileSync(p, "utf8").trim();
34412
+ const value = fs9.readFileSync(p, "utf8").trim();
32503
34413
  return value || null;
32504
34414
  } catch {
32505
34415
  return null;
@@ -32512,8 +34422,8 @@ function resolveBuildTs() {
32512
34422
  if (fromFile)
32513
34423
  return fromFile;
32514
34424
  try {
32515
- const pkgRoot = path6.resolve(__dirname, "..");
32516
- const fromPkgFile = readTextFileSafe(path6.join(pkgRoot, "BUILD_TS"));
34425
+ const pkgRoot = path8.resolve(__dirname, "..");
34426
+ const fromPkgFile = readTextFileSafe(path8.join(pkgRoot, "BUILD_TS"));
32517
34427
  if (fromPkgFile)
32518
34428
  return fromPkgFile;
32519
34429
  } catch (err) {
@@ -32521,8 +34431,8 @@ function resolveBuildTs() {
32521
34431
  console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
32522
34432
  }
32523
34433
  try {
32524
- const pkgJsonPath = path6.resolve(__dirname, "..", "package.json");
32525
- const st = fs7.statSync(pkgJsonPath);
34434
+ const pkgJsonPath = path8.resolve(__dirname, "..", "package.json");
34435
+ const st = fs9.statSync(pkgJsonPath);
32526
34436
  return st.mtime.toISOString();
32527
34437
  } catch (err) {
32528
34438
  if (process.env.DEBUG) {
@@ -33803,7 +35713,7 @@ function summarizeG003(nodeData) {
33803
35713
  }
33804
35714
 
33805
35715
  // lib/instances.ts
33806
- import * as fs8 from "fs";
35716
+ import * as fs10 from "fs";
33807
35717
  class InstancesParseError2 extends Error {
33808
35718
  constructor(file, cause) {
33809
35719
  const causeMsg = cause instanceof Error ? cause.message : String(cause);
@@ -33812,11 +35722,11 @@ class InstancesParseError2 extends Error {
33812
35722
  }
33813
35723
  }
33814
35724
  function loadInstances2(file) {
33815
- if (!fs8.existsSync(file))
35725
+ if (!fs10.existsSync(file))
33816
35726
  return [];
33817
- if (fs8.lstatSync(file).isDirectory())
35727
+ if (fs10.lstatSync(file).isDirectory())
33818
35728
  return [];
33819
- const text = fs8.readFileSync(file, "utf8");
35729
+ const text = fs10.readFileSync(file, "utf8");
33820
35730
  if (text.trim() === "")
33821
35731
  return [];
33822
35732
  let parsed;
@@ -33849,22 +35759,22 @@ function buildInstance(name, connStr) {
33849
35759
  };
33850
35760
  }
33851
35761
  function addInstanceToFile(file, instance) {
33852
- if (fs8.existsSync(file) && fs8.lstatSync(file).isDirectory()) {
33853
- fs8.rmSync(file, { recursive: true, force: true });
35762
+ if (fs10.existsSync(file) && fs10.lstatSync(file).isDirectory()) {
35763
+ fs10.rmSync(file, { recursive: true, force: true });
33854
35764
  }
33855
35765
  const existing = loadInstances2(file);
33856
35766
  if (existing.some((i3) => i3.name === instance.name)) {
33857
35767
  throw new Error(`Monitoring target '${instance.name}' already exists`);
33858
35768
  }
33859
35769
  existing.push(instance);
33860
- fs8.writeFileSync(file, dump2(existing), "utf8");
35770
+ fs10.writeFileSync(file, dump2(existing), "utf8");
33861
35771
  }
33862
35772
  function removeInstanceFromFile(file, name) {
33863
35773
  const instances = loadInstances2(file);
33864
35774
  const filtered = instances.filter((i3) => i3.name !== name);
33865
35775
  if (filtered.length === instances.length)
33866
35776
  return false;
33867
- fs8.writeFileSync(file, dump2(filtered), "utf8");
35777
+ fs10.writeFileSync(file, dump2(filtered), "utf8");
33868
35778
  return true;
33869
35779
  }
33870
35780
  function extractSslmode(connStr) {
@@ -33959,13 +35869,13 @@ function stripMatchingQuotes(value) {
33959
35869
  return trimmed;
33960
35870
  }
33961
35871
  var REQUIRED_ENV_KEYS = [
33962
- { key: "REPLICATOR_PASSWORD", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.13" },
35872
+ { key: "REPLICATOR_PASSWORD", defaultValue: () => crypto4.randomBytes(32).toString("hex"), introducedIn: "0.13" },
33963
35873
  { key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
33964
- { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" }
35874
+ { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto4.randomBytes(18).toString("base64"), introducedIn: "0.15" }
33965
35875
  ];
33966
35876
  function ensureRequiredEnvVars(projectDir) {
33967
- const envFile = path7.resolve(projectDir, ".env");
33968
- const existing = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
35877
+ const envFile = path9.resolve(projectDir, ".env");
35878
+ const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
33969
35879
  const added = [];
33970
35880
  const appendLines = [];
33971
35881
  for (const spec of REQUIRED_ENV_KEYS) {
@@ -33984,7 +35894,7 @@ function ensureRequiredEnvVars(projectDir) {
33984
35894
  ` : "") + appendLines.join(`
33985
35895
  `) + `
33986
35896
  `;
33987
- fs9.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
35897
+ fs11.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
33988
35898
  return added;
33989
35899
  }
33990
35900
  async function execFilePromise(file, args) {
@@ -34049,7 +35959,7 @@ function expandHomePath(p) {
34049
35959
  if (s === "~")
34050
35960
  return os3.homedir();
34051
35961
  if (s.startsWith("~/") || s.startsWith("~\\")) {
34052
- return path7.join(os3.homedir(), s.slice(2));
35962
+ return path9.join(os3.homedir(), s.slice(2));
34053
35963
  }
34054
35964
  return s;
34055
35965
  }
@@ -34098,10 +36008,10 @@ function prepareOutputDirectory(outputOpt) {
34098
36008
  if (!outputOpt)
34099
36009
  return;
34100
36010
  const outputDir = expandHomePath(outputOpt);
34101
- const outputPath = path7.isAbsolute(outputDir) ? outputDir : path7.resolve(process.cwd(), outputDir);
34102
- if (!fs9.existsSync(outputPath)) {
36011
+ const outputPath = path9.isAbsolute(outputDir) ? outputDir : path9.resolve(process.cwd(), outputDir);
36012
+ if (!fs11.existsSync(outputPath)) {
34103
36013
  try {
34104
- fs9.mkdirSync(outputPath, { recursive: true });
36014
+ fs11.mkdirSync(outputPath, { recursive: true });
34105
36015
  } catch (e) {
34106
36016
  const errAny = e;
34107
36017
  const code = typeof errAny?.code === "string" ? errAny.code : "";
@@ -34137,7 +36047,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
34137
36047
  let project = (opts.project || cfg.defaultProject || "").trim();
34138
36048
  let projectWasGenerated = false;
34139
36049
  if (!project) {
34140
- project = `project_${crypto2.randomBytes(6).toString("hex")}`;
36050
+ project = `project_${crypto4.randomBytes(6).toString("hex")}`;
34141
36051
  projectWasGenerated = true;
34142
36052
  try {
34143
36053
  writeConfig({ defaultProject: project });
@@ -34183,8 +36093,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
34183
36093
  }
34184
36094
  function writeReportFiles(reports, outputPath) {
34185
36095
  for (const [checkId, report] of Object.entries(reports)) {
34186
- const filePath = path7.join(outputPath, `${checkId}.json`);
34187
- fs9.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
36096
+ const filePath = path9.join(outputPath, `${checkId}.json`);
36097
+ fs11.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
34188
36098
  const title = report.checkTitle || checkId;
34189
36099
  console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
34190
36100
  }
@@ -34229,7 +36139,7 @@ function getDefaultMonitoringProjectDir() {
34229
36139
  const override = process.env.PGAI_PROJECT_DIR;
34230
36140
  if (override && override.trim())
34231
36141
  return override.trim();
34232
- return path7.join(getConfigDir(), "monitoring");
36142
+ return path9.join(getConfigDir(), "monitoring");
34233
36143
  }
34234
36144
  async function downloadText(url) {
34235
36145
  const controller = new AbortController;
@@ -34246,12 +36156,12 @@ async function downloadText(url) {
34246
36156
  }
34247
36157
  async function ensureDefaultMonitoringProject() {
34248
36158
  const projectDir = getDefaultMonitoringProjectDir();
34249
- const composeFile = path7.resolve(projectDir, "docker-compose.yml");
34250
- const instancesFile = path7.resolve(projectDir, "instances.yml");
34251
- if (!fs9.existsSync(projectDir)) {
34252
- fs9.mkdirSync(projectDir, { recursive: true, mode: 448 });
36159
+ const composeFile = path9.resolve(projectDir, "docker-compose.yml");
36160
+ const instancesFile = path9.resolve(projectDir, "instances.yml");
36161
+ if (!fs11.existsSync(projectDir)) {
36162
+ fs11.mkdirSync(projectDir, { recursive: true, mode: 448 });
34253
36163
  }
34254
- if (!fs9.existsSync(composeFile)) {
36164
+ if (!fs11.existsSync(composeFile)) {
34255
36165
  const refs = [
34256
36166
  process.env.PGAI_PROJECT_REF,
34257
36167
  package_default.version,
@@ -34263,39 +36173,39 @@ async function ensureDefaultMonitoringProject() {
34263
36173
  const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
34264
36174
  try {
34265
36175
  const text = await downloadText(url);
34266
- fs9.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
36176
+ fs11.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
34267
36177
  break;
34268
36178
  } catch (err) {
34269
36179
  lastErr = err;
34270
36180
  }
34271
36181
  }
34272
- if (!fs9.existsSync(composeFile)) {
36182
+ if (!fs11.existsSync(composeFile)) {
34273
36183
  const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
34274
36184
  throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
34275
36185
  }
34276
36186
  }
34277
- if (fs9.existsSync(instancesFile) && fs9.lstatSync(instancesFile).isDirectory()) {
34278
- fs9.rmSync(instancesFile, { recursive: true, force: true });
36187
+ if (fs11.existsSync(instancesFile) && fs11.lstatSync(instancesFile).isDirectory()) {
36188
+ fs11.rmSync(instancesFile, { recursive: true, force: true });
34279
36189
  }
34280
- if (!fs9.existsSync(instancesFile)) {
36190
+ if (!fs11.existsSync(instancesFile)) {
34281
36191
  const header = `# PostgreSQL instances to monitor
34282
36192
  ` + `# Add your instances using: pgai mon targets add <connection-string> <name>
34283
36193
 
34284
36194
  `;
34285
- fs9.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
36195
+ fs11.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
34286
36196
  }
34287
- const pgwatchConfig = path7.resolve(projectDir, ".pgwatch-config");
34288
- if (!fs9.existsSync(pgwatchConfig)) {
34289
- fs9.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
36197
+ const pgwatchConfig = path9.resolve(projectDir, ".pgwatch-config");
36198
+ if (!fs11.existsSync(pgwatchConfig)) {
36199
+ fs11.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
34290
36200
  }
34291
- const envFile = path7.resolve(projectDir, ".env");
34292
- if (!fs9.existsSync(envFile)) {
36201
+ const envFile = path9.resolve(projectDir, ".env");
36202
+ if (!fs11.existsSync(envFile)) {
34293
36203
  const envText = `PGAI_TAG=${package_default.version}
34294
36204
  # PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
34295
36205
  `;
34296
- fs9.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
36206
+ fs11.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
34297
36207
  }
34298
- return { fs: fs9, path: path7, projectDir, composeFile, instancesFile };
36208
+ return { fs: fs11, path: path9, projectDir, composeFile, instancesFile };
34299
36209
  }
34300
36210
  function sanitizeTagForBackup(tag) {
34301
36211
  if (tag == null)
@@ -34304,10 +36214,10 @@ function sanitizeTagForBackup(tag) {
34304
36214
  return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
34305
36215
  }
34306
36216
  function readDeployedTag(projectDir) {
34307
- const envFile = path7.resolve(projectDir, ".env");
34308
- if (!fs9.existsSync(envFile))
36217
+ const envFile = path9.resolve(projectDir, ".env");
36218
+ if (!fs11.existsSync(envFile))
34309
36219
  return null;
34310
- const m = fs9.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
36220
+ const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
34311
36221
  return m ? sanitizeTagForBackup(m[1]) : null;
34312
36222
  }
34313
36223
  function isValidComposeYaml(text) {
@@ -34344,10 +36254,10 @@ function composeContentEqual(a, b) {
34344
36254
  return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
34345
36255
  }
34346
36256
  async function refreshBundledComposeIfStale(projectDir, oldTag) {
34347
- if (fs9.existsSync(path7.resolve(projectDir, ".git")))
36257
+ if (fs11.existsSync(path9.resolve(projectDir, ".git")))
34348
36258
  return false;
34349
- const composeFile = path7.resolve(projectDir, "docker-compose.yml");
34350
- if (!fs9.existsSync(composeFile))
36259
+ const composeFile = path9.resolve(projectDir, "docker-compose.yml");
36260
+ if (!fs11.existsSync(composeFile))
34351
36261
  return false;
34352
36262
  const refs = [
34353
36263
  process.env.PGAI_PROJECT_REF,
@@ -34360,24 +36270,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
34360
36270
  console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
34361
36271
  return false;
34362
36272
  }
34363
- const existing = fs9.readFileSync(composeFile, "utf8");
36273
+ const existing = fs11.readFileSync(composeFile, "utf8");
34364
36274
  if (composeContentEqual(existing, fetched))
34365
36275
  return false;
34366
36276
  const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
34367
36277
  const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
34368
- const oldHash = crypto2.createHash("sha256").update(existing).digest("hex").slice(0, 8);
34369
- const backup = path7.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
36278
+ const oldHash = crypto4.createHash("sha256").update(existing).digest("hex").slice(0, 8);
36279
+ const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
34370
36280
  let backupName = null;
34371
36281
  try {
34372
- fs9.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
34373
- backupName = path7.basename(backup);
36282
+ fs11.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
36283
+ backupName = path9.basename(backup);
34374
36284
  } catch (err) {
34375
36285
  const e = err;
34376
36286
  if (e && e.code === "EEXIST") {
34377
- backupName = path7.basename(backup);
36287
+ backupName = path9.basename(backup);
34378
36288
  }
34379
36289
  }
34380
- fs9.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
36290
+ fs11.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
34381
36291
  const backupNote = backupName ? ` (backup: ${backupName})` : "";
34382
36292
  console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
34383
36293
  return true;
@@ -35611,12 +37521,12 @@ function resolvePaths() {
35611
37521
  const startDir = process.cwd();
35612
37522
  let currentDir = startDir;
35613
37523
  while (true) {
35614
- const composeFile = path7.resolve(currentDir, "docker-compose.yml");
35615
- if (fs9.existsSync(composeFile)) {
35616
- const instancesFile = path7.resolve(currentDir, "instances.yml");
35617
- return { fs: fs9, path: path7, projectDir: currentDir, composeFile, instancesFile };
37524
+ const composeFile = path9.resolve(currentDir, "docker-compose.yml");
37525
+ if (fs11.existsSync(composeFile)) {
37526
+ const instancesFile = path9.resolve(currentDir, "instances.yml");
37527
+ return { fs: fs11, path: path9, projectDir: currentDir, composeFile, instancesFile };
35618
37528
  }
35619
- const parentDir = path7.dirname(currentDir);
37529
+ const parentDir = path9.dirname(currentDir);
35620
37530
  if (parentDir === currentDir)
35621
37531
  break;
35622
37532
  currentDir = parentDir;
@@ -35659,6 +37569,16 @@ function checkRunningContainers() {
35659
37569
  return { running: false, containers: [] };
35660
37570
  }
35661
37571
  }
37572
+ function planMonitoringRegistration(args) {
37573
+ const projectName = args.project?.trim() || undefined;
37574
+ if (args.instanceId) {
37575
+ return { kind: "adopt", projectName };
37576
+ }
37577
+ if (!projectName) {
37578
+ return { kind: "error-missing-project", projectName: undefined };
37579
+ }
37580
+ return { kind: "self-register", projectName };
37581
+ }
35662
37582
  async function registerMonitoringInstance(apiKey, projectName, opts) {
35663
37583
  const { apiBaseUrl } = resolveBaseUrls2(opts);
35664
37584
  const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
@@ -35666,16 +37586,19 @@ async function registerMonitoringInstance(apiKey, projectName, opts) {
35666
37586
  const instanceId = opts?.instanceId;
35667
37587
  const retries = opts?.retries ?? (instanceId ? 1 : 0);
35668
37588
  const retryDelayMs = opts?.retryDelayMs ?? 400;
37589
+ const hasProjectName = !!(projectName && projectName.trim());
35669
37590
  if (debug) {
35670
37591
  console.error(`
35671
37592
  Debug: Registering monitoring instance...`);
35672
37593
  console.error(`Debug: POST ${url}`);
35673
- console.error(`Debug: project_name=${projectName}${instanceId ? ` instance_id=${instanceId}` : ""}`);
37594
+ console.error(`Debug: ${hasProjectName ? `project_name=${projectName}` : "project_name=(omitted)"}${instanceId ? ` instance_id=${instanceId}` : ""}`);
35674
37595
  }
35675
37596
  const requestBody = {
35676
- api_token: apiKey,
35677
- project_name: projectName
37597
+ api_token: apiKey
35678
37598
  };
37599
+ if (hasProjectName) {
37600
+ requestBody.project_name = projectName;
37601
+ }
35679
37602
  if (instanceId) {
35680
37603
  requestBody.instance_id = instanceId;
35681
37604
  }
@@ -35735,10 +37658,10 @@ function resolveAdoptedProject(reg) {
35735
37658
  }
35736
37659
  function updatePgwatchConfig(configPath, updates) {
35737
37660
  let lines = [];
35738
- if (fs9.existsSync(configPath)) {
35739
- const stats = fs9.statSync(configPath);
37661
+ if (fs11.existsSync(configPath)) {
37662
+ const stats = fs11.statSync(configPath);
35740
37663
  if (!stats.isDirectory()) {
35741
- const content = fs9.readFileSync(configPath, "utf8");
37664
+ const content = fs11.readFileSync(configPath, "utf8");
35742
37665
  lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
35743
37666
  }
35744
37667
  }
@@ -35750,7 +37673,7 @@ function updatePgwatchConfig(configPath, updates) {
35750
37673
  lines.push(`${key}=${value}`);
35751
37674
  }
35752
37675
  }
35753
- fs9.writeFileSync(configPath, lines.join(`
37676
+ fs11.writeFileSync(configPath, lines.join(`
35754
37677
  `) + `
35755
37678
  `, { encoding: "utf8", mode: 384 });
35756
37679
  }
@@ -35788,12 +37711,12 @@ async function runCompose(args, grafanaPassword) {
35788
37711
  if (grafanaPassword) {
35789
37712
  env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
35790
37713
  } else {
35791
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
35792
- if (fs9.existsSync(cfgPath)) {
37714
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
37715
+ if (fs11.existsSync(cfgPath)) {
35793
37716
  try {
35794
- const stats = fs9.statSync(cfgPath);
37717
+ const stats = fs11.statSync(cfgPath);
35795
37718
  if (!stats.isDirectory()) {
35796
- const content = fs9.readFileSync(cfgPath, "utf8");
37719
+ const content = fs11.readFileSync(cfgPath, "utf8");
35797
37720
  const match = content.match(/^grafana_password=([^\r\n]+)/m);
35798
37721
  if (match) {
35799
37722
  env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
@@ -35806,10 +37729,10 @@ async function runCompose(args, grafanaPassword) {
35806
37729
  }
35807
37730
  }
35808
37731
  }
35809
- const envFilePath = path7.resolve(projectDir, ".env");
35810
- if (fs9.existsSync(envFilePath)) {
37732
+ const envFilePath = path9.resolve(projectDir, ".env");
37733
+ if (fs11.existsSync(envFilePath)) {
35811
37734
  try {
35812
- const envContent = fs9.readFileSync(envFilePath, "utf8");
37735
+ const envContent = fs11.readFileSync(envFilePath, "utf8");
35813
37736
  if (!env.VM_AUTH_USERNAME) {
35814
37737
  const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
35815
37738
  if (m)
@@ -35866,20 +37789,20 @@ mon.command("local-install").description("install local monitoring stack (genera
35866
37789
  console.log(`Project directory: ${projectDir}
35867
37790
  `);
35868
37791
  if (opts.project) {
35869
- const cfgPath2 = path7.resolve(projectDir, ".pgwatch-config");
37792
+ const cfgPath2 = path9.resolve(projectDir, ".pgwatch-config");
35870
37793
  updatePgwatchConfig(cfgPath2, { project_name: opts.project });
35871
37794
  console.log(`Using project name: ${opts.project}
35872
37795
  `);
35873
37796
  }
35874
- const envFile = path7.resolve(projectDir, ".env");
37797
+ const envFile = path9.resolve(projectDir, ".env");
35875
37798
  let existingRegistry = null;
35876
37799
  let existingPassword = null;
35877
37800
  let existingReplicatorPassword = null;
35878
37801
  let existingVmAuthUsername = null;
35879
37802
  let existingVmAuthPassword = null;
35880
37803
  let previousTag = null;
35881
- if (fs9.existsSync(envFile)) {
35882
- const existingEnv = fs9.readFileSync(envFile, "utf8");
37804
+ if (fs11.existsSync(envFile)) {
37805
+ const existingEnv = fs11.readFileSync(envFile, "utf8");
35883
37806
  const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
35884
37807
  if (previousTagMatch)
35885
37808
  previousTag = previousTagMatch[1].trim();
@@ -35907,10 +37830,10 @@ mon.command("local-install").description("install local monitoring stack (genera
35907
37830
  if (existingPassword) {
35908
37831
  envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
35909
37832
  }
35910
- envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto2.randomBytes(32).toString("hex")}`);
37833
+ envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto4.randomBytes(32).toString("hex")}`);
35911
37834
  envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
35912
- envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto2.randomBytes(18).toString("base64")}`);
35913
- fs9.writeFileSync(envFile, envLines.join(`
37835
+ envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto4.randomBytes(18).toString("base64")}`);
37836
+ fs11.writeFileSync(envFile, envLines.join(`
35914
37837
  `) + `
35915
37838
  `, { encoding: "utf8", mode: 384 });
35916
37839
  await refreshBundledComposeIfStale(projectDir, previousTag);
@@ -35947,7 +37870,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
35947
37870
  if (apiKey) {
35948
37871
  console.log("Using API key provided via --api-key parameter");
35949
37872
  writeConfig({ apiKey });
35950
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
37873
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
35951
37874
  console.log(`\u2713 API key saved
35952
37875
  `);
35953
37876
  } else if (opts.yes) {
@@ -35964,7 +37887,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
35964
37887
  const trimmedKey = inputApiKey.trim();
35965
37888
  if (trimmedKey) {
35966
37889
  writeConfig({ apiKey: trimmedKey });
35967
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
37890
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
35968
37891
  apiKey = trimmedKey;
35969
37892
  console.log(`\u2713 API key saved
35970
37893
  `);
@@ -35998,7 +37921,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
35998
37921
  # Add your instances using: postgres-ai mon targets add
35999
37922
 
36000
37923
  `;
36001
- fs9.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
37924
+ fs11.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
36002
37925
  console.log(`Instances file: ${instancesPath2}`);
36003
37926
  console.log(`Project directory: ${projectDir2}
36004
37927
  `);
@@ -36102,17 +38025,17 @@ You can provide either:`);
36102
38025
  }
36103
38026
  } else {
36104
38027
  console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
36105
- const currentDir = path7.dirname(fileURLToPath2(import.meta.url));
38028
+ const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
36106
38029
  const demoCandidates = [
36107
- path7.resolve(currentDir, "..", "..", "instances.demo.yml"),
36108
- path7.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
38030
+ path9.resolve(currentDir, "..", "..", "instances.demo.yml"),
38031
+ path9.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
36109
38032
  ];
36110
- const demoSrc = demoCandidates.find((p) => fs9.existsSync(p));
38033
+ const demoSrc = demoCandidates.find((p) => fs11.existsSync(p));
36111
38034
  if (demoSrc) {
36112
- if (fs9.existsSync(instancesPath) && fs9.lstatSync(instancesPath).isDirectory()) {
36113
- fs9.rmSync(instancesPath, { recursive: true, force: true });
38035
+ if (fs11.existsSync(instancesPath) && fs11.lstatSync(instancesPath).isDirectory()) {
38036
+ fs11.rmSync(instancesPath, { recursive: true, force: true });
36114
38037
  }
36115
- fs9.copyFileSync(demoSrc, instancesPath);
38038
+ fs11.copyFileSync(demoSrc, instancesPath);
36116
38039
  console.log(`\u2713 Demo monitoring target configured
36117
38040
  `);
36118
38041
  } else {
@@ -36131,15 +38054,15 @@ Searched: ${demoCandidates.join(", ")}
36131
38054
  console.log(`\u2713 Configuration updated
36132
38055
  `);
36133
38056
  console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
36134
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
38057
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
36135
38058
  let grafanaPassword = "";
36136
38059
  let vmAuthUsername = "";
36137
38060
  let vmAuthPassword = "";
36138
38061
  try {
36139
- if (fs9.existsSync(cfgPath)) {
36140
- const stats = fs9.statSync(cfgPath);
38062
+ if (fs11.existsSync(cfgPath)) {
38063
+ const stats = fs11.statSync(cfgPath);
36141
38064
  if (!stats.isDirectory()) {
36142
- const content = fs9.readFileSync(cfgPath, "utf8");
38065
+ const content = fs11.readFileSync(cfgPath, "utf8");
36143
38066
  const match = content.match(/^grafana_password=([^\r\n]+)/m);
36144
38067
  if (match) {
36145
38068
  grafanaPassword = match[1].trim();
@@ -36151,15 +38074,15 @@ Searched: ${demoCandidates.join(", ")}
36151
38074
  const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
36152
38075
  grafanaPassword = password.trim().replace(/\n/g, "");
36153
38076
  let configContent = "";
36154
- if (fs9.existsSync(cfgPath)) {
36155
- const stats = fs9.statSync(cfgPath);
38077
+ if (fs11.existsSync(cfgPath)) {
38078
+ const stats = fs11.statSync(cfgPath);
36156
38079
  if (!stats.isDirectory()) {
36157
- configContent = fs9.readFileSync(cfgPath, "utf8");
38080
+ configContent = fs11.readFileSync(cfgPath, "utf8");
36158
38081
  }
36159
38082
  }
36160
38083
  const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
36161
38084
  lines.push(`grafana_password=${grafanaPassword}`);
36162
- fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
38085
+ fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
36163
38086
  `) + `
36164
38087
  `, "utf8");
36165
38088
  }
@@ -36172,9 +38095,9 @@ Searched: ${demoCandidates.join(", ")}
36172
38095
  grafanaPassword = "demo";
36173
38096
  }
36174
38097
  try {
36175
- const envFile2 = path7.resolve(projectDir, ".env");
36176
- if (fs9.existsSync(envFile2)) {
36177
- const envContent = fs9.readFileSync(envFile2, "utf8");
38098
+ const envFile2 = path9.resolve(projectDir, ".env");
38099
+ if (fs11.existsSync(envFile2)) {
38100
+ const envContent = fs11.readFileSync(envFile2, "utf8");
36178
38101
  const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
36179
38102
  const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
36180
38103
  if (userMatch)
@@ -36190,13 +38113,13 @@ Searched: ${demoCandidates.join(", ")}
36190
38113
  vmAuthPassword = vmPass.trim().replace(/\n/g, "");
36191
38114
  }
36192
38115
  let envContent = "";
36193
- if (fs9.existsSync(envFile2)) {
36194
- envContent = fs9.readFileSync(envFile2, "utf8");
38116
+ if (fs11.existsSync(envFile2)) {
38117
+ envContent = fs11.readFileSync(envFile2, "utf8");
36195
38118
  }
36196
38119
  const envLines2 = envContent.split(/\r?\n/).filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l)).filter((l, i3, arr) => !(i3 === arr.length - 1 && l === ""));
36197
38120
  envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
36198
38121
  envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
36199
- fs9.writeFileSync(envFile2, envLines2.join(`
38122
+ fs11.writeFileSync(envFile2, envLines2.join(`
36200
38123
  `) + `
36201
38124
  `, { encoding: "utf8", mode: 384 });
36202
38125
  }
@@ -36218,8 +38141,9 @@ Searched: ${demoCandidates.join(", ")}
36218
38141
  console.log(`\u2713 Services started
36219
38142
  `);
36220
38143
  if (apiKey && !opts.demo) {
36221
- const projectName = opts.project || "postgres-ai-monitoring";
36222
38144
  const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
38145
+ const plan = planMonitoringRegistration({ project: opts.project, instanceId });
38146
+ const projectName = plan.projectName;
36223
38147
  if (instanceId) {
36224
38148
  const reg = await registerMonitoringInstance(apiKey, projectName, {
36225
38149
  apiBaseUrl: globalOpts.apiBaseUrl,
@@ -36228,16 +38152,16 @@ Searched: ${demoCandidates.join(", ")}
36228
38152
  });
36229
38153
  const adoptedProject = resolveAdoptedProject(reg);
36230
38154
  if (adoptedProject != null) {
36231
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
38155
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), {
36232
38156
  project_name: adoptedProject
36233
38157
  });
36234
38158
  const verb = reg?.created ? "Registered" : "Adopted";
36235
38159
  console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
36236
38160
  `);
36237
38161
  } else if (reg) {
36238
- console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project \u2014 reports will use project '${projectName}'`);
38162
+ console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
36239
38163
  } else {
36240
- console.error(`\u26A0 Could not adopt provisioned instance ${instanceId} \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`);
38164
+ console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
36241
38165
  }
36242
38166
  const aas = await registerAasCollection(apiKey, instanceId, {
36243
38167
  grafanaPassword,
@@ -36253,6 +38177,9 @@ Searched: ${demoCandidates.join(", ")}
36253
38177
  console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
36254
38178
  `);
36255
38179
  }
38180
+ } else if (plan.kind === "error-missing-project") {
38181
+ console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
38182
+ process.exitCode = 1;
36256
38183
  } else {
36257
38184
  registerMonitoringInstance(apiKey, projectName, {
36258
38185
  apiBaseUrl: globalOpts.apiBaseUrl,
@@ -36441,11 +38368,11 @@ mon.command("config").description("show monitoring services configuration").acti
36441
38368
  console.log(`Project Directory: ${projectDir}`);
36442
38369
  console.log(`Docker Compose File: ${composeFile}`);
36443
38370
  console.log(`Instances File: ${instancesFile}`);
36444
- if (fs9.existsSync(instancesFile) && !fs9.lstatSync(instancesFile).isDirectory()) {
38371
+ if (fs11.existsSync(instancesFile) && !fs11.lstatSync(instancesFile).isDirectory()) {
36445
38372
  console.log(`
36446
38373
  Instances configuration:
36447
38374
  `);
36448
- const text = fs9.readFileSync(instancesFile, "utf8");
38375
+ const text = fs11.readFileSync(instancesFile, "utf8");
36449
38376
  process.stdout.write(text);
36450
38377
  if (!/\n$/.test(text))
36451
38378
  console.log();
@@ -36494,8 +38421,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
36494
38421
  console.log("\u2713 .env is up to date");
36495
38422
  }
36496
38423
  console.log();
36497
- const gitDir = path7.resolve(projectDir, ".git");
36498
- if (fs9.existsSync(gitDir)) {
38424
+ const gitDir = path9.resolve(projectDir, ".git");
38425
+ if (fs11.existsSync(gitDir)) {
36499
38426
  console.log("Fetching latest changes...");
36500
38427
  await execFilePromise("git", ["fetch", "origin"]);
36501
38428
  const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -36650,7 +38577,7 @@ mon.command("check").description("monitoring services system readiness check").a
36650
38577
  var targets = mon.command("targets").description("manage databases to monitor");
36651
38578
  targets.command("list").description("list monitoring target databases").action(async () => {
36652
38579
  const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
36653
- if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
38580
+ if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
36654
38581
  console.error(`instances.yml not found in ${projectDir}`);
36655
38582
  process.exitCode = 1;
36656
38583
  return;
@@ -36713,7 +38640,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
36713
38640
  });
36714
38641
  targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
36715
38642
  const { instancesFile: file } = await resolveOrInitPaths();
36716
- if (!fs9.existsSync(file) || fs9.lstatSync(file).isDirectory()) {
38643
+ if (!fs11.existsSync(file) || fs11.lstatSync(file).isDirectory()) {
36717
38644
  console.error("instances.yml not found");
36718
38645
  process.exitCode = 1;
36719
38646
  return;
@@ -36741,7 +38668,7 @@ targets.command("remove <name>").description("remove monitoring target database"
36741
38668
  });
36742
38669
  targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
36743
38670
  const { instancesFile: instancesPath } = await resolveOrInitPaths();
36744
- if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
38671
+ if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
36745
38672
  console.error("instances.yml not found");
36746
38673
  process.exitCode = 1;
36747
38674
  return;
@@ -37003,15 +38930,15 @@ To authenticate, run: pgai auth`);
37003
38930
  });
37004
38931
  auth.command("remove-key").description("remove API key").action(async () => {
37005
38932
  const newConfigPath = getConfigPath();
37006
- const hasNewConfig = fs9.existsSync(newConfigPath);
38933
+ const hasNewConfig = fs11.existsSync(newConfigPath);
37007
38934
  let legacyPath;
37008
38935
  try {
37009
38936
  const { projectDir } = await resolveOrInitPaths();
37010
- legacyPath = path7.resolve(projectDir, ".pgwatch-config");
38937
+ legacyPath = path9.resolve(projectDir, ".pgwatch-config");
37011
38938
  } catch {
37012
- legacyPath = path7.resolve(process.cwd(), ".pgwatch-config");
38939
+ legacyPath = path9.resolve(process.cwd(), ".pgwatch-config");
37013
38940
  }
37014
- const hasLegacyConfig = fs9.existsSync(legacyPath) && fs9.statSync(legacyPath).isFile();
38941
+ const hasLegacyConfig = fs11.existsSync(legacyPath) && fs11.statSync(legacyPath).isFile();
37015
38942
  if (!hasNewConfig && !hasLegacyConfig) {
37016
38943
  console.log("No API key configured");
37017
38944
  return;
@@ -37021,11 +38948,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
37021
38948
  }
37022
38949
  if (hasLegacyConfig) {
37023
38950
  try {
37024
- const content = fs9.readFileSync(legacyPath, "utf8");
38951
+ const content = fs11.readFileSync(legacyPath, "utf8");
37025
38952
  const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
37026
38953
  `).replace(/\n+$/g, `
37027
38954
  `);
37028
- fs9.writeFileSync(legacyPath, filtered, "utf8");
38955
+ fs11.writeFileSync(legacyPath, filtered, "utf8");
37029
38956
  } catch (err) {
37030
38957
  console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
37031
38958
  }
@@ -37036,7 +38963,7 @@ To authenticate again, run: pgai auth`);
37036
38963
  });
37037
38964
  mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
37038
38965
  const { projectDir } = await resolveOrInitPaths();
37039
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
38966
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
37040
38967
  try {
37041
38968
  const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
37042
38969
  const newPassword = password.trim().replace(/\n/g, "");
@@ -37046,17 +38973,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
37046
38973
  return;
37047
38974
  }
37048
38975
  let configContent = "";
37049
- if (fs9.existsSync(cfgPath)) {
37050
- const stats = fs9.statSync(cfgPath);
38976
+ if (fs11.existsSync(cfgPath)) {
38977
+ const stats = fs11.statSync(cfgPath);
37051
38978
  if (stats.isDirectory()) {
37052
38979
  console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
37053
38980
  } else {
37054
- configContent = fs9.readFileSync(cfgPath, "utf8");
38981
+ configContent = fs11.readFileSync(cfgPath, "utf8");
37055
38982
  }
37056
38983
  }
37057
38984
  const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
37058
38985
  lines.push(`grafana_password=${newPassword}`);
37059
- fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
38986
+ fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
37060
38987
  `) + `
37061
38988
  `, "utf8");
37062
38989
  console.log("\u2713 New Grafana password generated and saved");
@@ -37078,19 +39005,19 @@ Note: This command requires 'openssl' to be installed`);
37078
39005
  });
37079
39006
  mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
37080
39007
  const { projectDir } = await resolveOrInitPaths();
37081
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
37082
- if (!fs9.existsSync(cfgPath)) {
39008
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
39009
+ if (!fs11.existsSync(cfgPath)) {
37083
39010
  console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
37084
39011
  process.exitCode = 1;
37085
39012
  return;
37086
39013
  }
37087
- const stats = fs9.statSync(cfgPath);
39014
+ const stats = fs11.statSync(cfgPath);
37088
39015
  if (stats.isDirectory()) {
37089
39016
  console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
37090
39017
  process.exitCode = 1;
37091
39018
  return;
37092
39019
  }
37093
- const content = fs9.readFileSync(cfgPath, "utf8");
39020
+ const content = fs11.readFileSync(cfgPath, "utf8");
37094
39021
  const lines = content.split(/\r?\n/);
37095
39022
  let password = "";
37096
39023
  for (const line of lines) {
@@ -37110,9 +39037,9 @@ Grafana credentials:`);
37110
39037
  console.log(" URL: http://localhost:3000");
37111
39038
  console.log(" Username: monitor");
37112
39039
  console.log(` Password: ${password}`);
37113
- const envFile = path7.resolve(projectDir, ".env");
37114
- if (fs9.existsSync(envFile)) {
37115
- const envContent = fs9.readFileSync(envFile, "utf8");
39040
+ const envFile = path9.resolve(projectDir, ".env");
39041
+ if (fs11.existsSync(envFile)) {
39042
+ const envContent = fs11.readFileSync(envFile, "utf8");
37116
39043
  const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
37117
39044
  const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
37118
39045
  if (vmUser && vmPass) {
@@ -37838,13 +39765,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
37838
39765
  });
37839
39766
  spinner.stop();
37840
39767
  if (opts.output) {
37841
- const dir = path7.resolve(opts.output);
37842
- fs9.mkdirSync(dir, { recursive: true });
39768
+ const dir = path9.resolve(opts.output);
39769
+ fs11.mkdirSync(dir, { recursive: true });
37843
39770
  for (const f of result) {
37844
- const safeName = path7.basename(f.filename);
37845
- const filePath = path7.join(dir, safeName);
39771
+ const safeName = path9.basename(f.filename);
39772
+ const filePath = path9.join(dir, safeName);
37846
39773
  const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
37847
- fs9.writeFileSync(filePath, content, "utf-8");
39774
+ fs11.writeFileSync(filePath, content, "utf-8");
37848
39775
  console.log(filePath);
37849
39776
  }
37850
39777
  } else if (opts.json) {
@@ -37889,6 +39816,419 @@ function tryParseJson(s) {
37889
39816
  return s;
37890
39817
  }
37891
39818
  }
39819
+ function inferCommandFromResult(r) {
39820
+ if (r.command)
39821
+ return r.command;
39822
+ if (r.plan_text !== undefined || r.plan_json !== undefined)
39823
+ return "plan";
39824
+ if (r.row_count !== undefined || r.result_rows !== undefined)
39825
+ return "exec";
39826
+ if (r.hypo_used !== undefined)
39827
+ return "hypo";
39828
+ if (r.terminated !== undefined)
39829
+ return "terminate";
39830
+ if (r.reset !== undefined)
39831
+ return "reset";
39832
+ if (r.snapshot !== undefined)
39833
+ return "activity";
39834
+ return null;
39835
+ }
39836
+ function printJoeOutcome(command, outcome, json3, budgetMs) {
39837
+ const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS2;
39838
+ const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
39839
+ if (outcome.budgetExpired) {
39840
+ if (json3) {
39841
+ console.log(JSON.stringify({
39842
+ command_id: outcome.commandId,
39843
+ status: outcome.status,
39844
+ session_id: outcome.sessionId,
39845
+ budget_expired: true,
39846
+ resume: `pgai joe result ${outcome.commandId}`
39847
+ }, null, 2));
39848
+ } else {
39849
+ console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
39850
+ }
39851
+ return;
39852
+ }
39853
+ const result = outcome.result;
39854
+ if (outcome.status === "done" && result) {
39855
+ if (json3) {
39856
+ console.log(JSON.stringify(result, null, 2));
39857
+ } else {
39858
+ console.log(`submitted ${outcome.commandId} \xB7 done`);
39859
+ const body = formatJoeResult(command, result);
39860
+ if (body)
39861
+ console.log(body);
39862
+ }
39863
+ return;
39864
+ }
39865
+ if (outcome.status === "error") {
39866
+ const msg = result?.error ?? "command failed";
39867
+ console.error(`command ${outcome.commandId} error: ${msg}`);
39868
+ process.exitCode = 1;
39869
+ return;
39870
+ }
39871
+ console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai joe result ${outcome.commandId}`);
39872
+ process.exitCode = 1;
39873
+ }
39874
+ async function runJoeCli(command, sql, args, opts) {
39875
+ try {
39876
+ const rootOpts = program2.opts();
39877
+ const cfg = readConfig();
39878
+ const { apiKey } = getConfig(rootOpts);
39879
+ if (!apiKey) {
39880
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39881
+ process.exitCode = 1;
39882
+ return;
39883
+ }
39884
+ const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
39885
+ if (!projectRef) {
39886
+ console.error("Project is required. Pass --project <id|alias>.");
39887
+ process.exitCode = 1;
39888
+ return;
39889
+ }
39890
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39891
+ const budgetMs = typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
39892
+ const outcome = await executeJoeCommand2({
39893
+ apiKey,
39894
+ apiBaseUrl,
39895
+ command,
39896
+ project: projectRef,
39897
+ sql,
39898
+ args,
39899
+ session: opts.session ?? null,
39900
+ newSession: !!opts.newSession,
39901
+ orgId: cfg.orgId ?? undefined,
39902
+ budgetMs,
39903
+ debug: !!opts.debug
39904
+ });
39905
+ printJoeOutcome(command, outcome, !!opts.json, budgetMs);
39906
+ } catch (err) {
39907
+ const message = err instanceof Error ? err.message : String(err);
39908
+ console.error(message);
39909
+ process.exitCode = 1;
39910
+ }
39911
+ }
39912
+ function withJoeOptions(cmd) {
39913
+ return cmd.option("--project <id|alias>", "target project by numeric id OR alias/name").option("--session <id>", "run on a specific session's clone").option("--new-session", "start a fresh session/clone (null session)").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseInt(v, 10)).option("--debug", "enable debug output").option("--json", "output raw JSON");
39914
+ }
39915
+ var joe = program2.command("joe").description("Joe API v2 \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones");
39916
+ withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
39917
+ await runJoeCli("plan", sql, null, opts);
39918
+ });
39919
+ withJoeOptions(joe.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
39920
+ await runJoeCli("explain", sql, null, opts);
39921
+ });
39922
+ withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
39923
+ await runJoeCli("exec", sql, null, opts);
39924
+ });
39925
+ withJoeOptions(joe.command("hypo <indexSql>").description("HypoPG hypothetical index + re-plan (no real index, no data change)").requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")).action(async (indexSql, opts) => {
39926
+ await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
39927
+ });
39928
+ withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
39929
+ await runJoeCli("activity", null, null, opts);
39930
+ });
39931
+ withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
39932
+ const pidStr = (pid ?? "").trim();
39933
+ if (!/^[0-9]+$/.test(pidStr)) {
39934
+ console.error("pid must be a number");
39935
+ process.exitCode = 1;
39936
+ return;
39937
+ }
39938
+ await runJoeCli("terminate", null, { pid: parseInt(pidStr, 10) }, opts);
39939
+ });
39940
+ withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
39941
+ await runJoeCli("reset", null, null, opts);
39942
+ });
39943
+ withJoeOptions(joe.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
39944
+ const args = { object: object4 };
39945
+ if (opts.variant)
39946
+ args.variant = opts.variant;
39947
+ await runJoeCli("describe", null, args, opts);
39948
+ });
39949
+ withJoeOptions(joe.command("history <terms>").description("search prior Joe analyses, metadata-only (M1b \u2014 not yet available)")).action(async (_terms, _opts) => {
39950
+ console.error("Joe history search is not available yet \u2014 it is planned for M1b. " + "No history-search backend is deployed; this command is a placeholder until then.");
39951
+ process.exitCode = 1;
39952
+ });
39953
+ program2.command("projects").description("list the org's projects (shows which have Joe ready) \u2014 org-level, not a Joe endpoint").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
39954
+ try {
39955
+ const rootOpts = program2.opts();
39956
+ const cfg = readConfig();
39957
+ const { apiKey } = getConfig(rootOpts);
39958
+ if (!apiKey) {
39959
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39960
+ process.exitCode = 1;
39961
+ return;
39962
+ }
39963
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39964
+ const projects = await listProjects2({
39965
+ apiKey,
39966
+ apiBaseUrl,
39967
+ orgId: cfg.orgId ?? undefined,
39968
+ debug: !!opts.debug
39969
+ });
39970
+ if (opts.json) {
39971
+ console.log(JSON.stringify(projects, null, 2));
39972
+ } else {
39973
+ console.log(formatProjectsTable(projects));
39974
+ }
39975
+ } catch (err) {
39976
+ const message = err instanceof Error ? err.message : String(err);
39977
+ console.error(message);
39978
+ process.exitCode = 1;
39979
+ }
39980
+ });
39981
+ async function resolveDblabTarget(project, debug) {
39982
+ const rootOpts = program2.opts();
39983
+ const cfg = readConfig();
39984
+ const { apiKey } = getConfig(rootOpts);
39985
+ if (!apiKey) {
39986
+ throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
39987
+ }
39988
+ const ref = (project ?? "").trim();
39989
+ if (!ref) {
39990
+ throw new Error("--project <id|alias> is required");
39991
+ }
39992
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39993
+ const orgId = cfg.orgId ?? undefined;
39994
+ const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
39995
+ return { apiKey, apiBaseUrl, orgId, instanceId };
39996
+ }
39997
+ var dblab = program2.command("dblab").description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
39998
+ var clone2 = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
39999
+ clone2.command("create").description("create a thin clone of the project's database").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "branch to clone from").option("--snapshot <id>", "snapshot id to clone from").option("--id <id>", "clone id (DBLab generates one when omitted)").option("--db-user <user>", "clone DB user").option("--db-password <password>", "clone DB password").option("--protected", "protect the clone from auto-deletion").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40000
+ try {
40001
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40002
+ const result = await createClone2({
40003
+ apiKey,
40004
+ apiBaseUrl,
40005
+ instanceId,
40006
+ cloneId: opts.id,
40007
+ branch: opts.branch,
40008
+ snapshotId: opts.snapshot,
40009
+ dbUser: opts.dbUser,
40010
+ dbPassword: opts.dbPassword,
40011
+ isProtected: !!opts.protected,
40012
+ debug: !!opts.debug
40013
+ });
40014
+ printResult(result, opts.json);
40015
+ } catch (err) {
40016
+ console.error(err instanceof Error ? err.message : String(err));
40017
+ process.exitCode = 1;
40018
+ }
40019
+ });
40020
+ joe.command("status <commandId>").description("show a Joe command's status (metadata only)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
40021
+ try {
40022
+ const rootOpts = program2.opts();
40023
+ const cfg = readConfig();
40024
+ const { apiKey } = getConfig(rootOpts);
40025
+ if (!apiKey) {
40026
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
40027
+ process.exitCode = 1;
40028
+ return;
40029
+ }
40030
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40031
+ const status = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
40032
+ if (opts.json) {
40033
+ console.log(JSON.stringify(status, null, 2));
40034
+ } else {
40035
+ console.log(`command ${status.command_id} \xB7 ${status.status}${status.error ? ` \xB7 ${status.error}` : ""}`);
40036
+ }
40037
+ } catch (err) {
40038
+ const message = err instanceof Error ? err.message : String(err);
40039
+ console.error(message);
40040
+ process.exitCode = 1;
40041
+ }
40042
+ });
40043
+ clone2.command("list").description("list the project's thin clones").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40044
+ try {
40045
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40046
+ const result = await listClones2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
40047
+ printResult(result, opts.json);
40048
+ } catch (err) {
40049
+ console.error(err instanceof Error ? err.message : String(err));
40050
+ process.exitCode = 1;
40051
+ }
40052
+ });
40053
+ joe.command("result <commandId>").description("fetch a Joe command's result by id (resume a timed-out one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
40054
+ try {
40055
+ const rootOpts = program2.opts();
40056
+ const cfg = readConfig();
40057
+ const { apiKey } = getConfig(rootOpts);
40058
+ if (!apiKey) {
40059
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
40060
+ process.exitCode = 1;
40061
+ return;
40062
+ }
40063
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40064
+ const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
40065
+ if (opts.json) {
40066
+ console.log(JSON.stringify(result, null, 2));
40067
+ if (result.status === "error" || result.status === "timed_out") {
40068
+ process.exitCode = 1;
40069
+ }
40070
+ return;
40071
+ }
40072
+ const inferred = inferCommandFromResult(result);
40073
+ if (result.status === "done" && inferred) {
40074
+ console.log(`command ${result.command_id} \xB7 done`);
40075
+ const body = formatJoeResult(inferred, result);
40076
+ if (body)
40077
+ console.log(body);
40078
+ } else if (result.status === "error") {
40079
+ console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
40080
+ process.exitCode = 1;
40081
+ } else if (result.status === "timed_out") {
40082
+ console.error(`command ${result.command_id} timed out on the server`);
40083
+ process.exitCode = 1;
40084
+ } else {
40085
+ console.log(`command ${result.command_id} \xB7 ${result.status}`);
40086
+ }
40087
+ } catch (err) {
40088
+ const message = err instanceof Error ? err.message : String(err);
40089
+ console.error(message);
40090
+ process.exitCode = 1;
40091
+ }
40092
+ });
40093
+ clone2.command("status <cloneId>").description("show a clone's status").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40094
+ try {
40095
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40096
+ const result = await getClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
40097
+ printResult(result, opts.json);
40098
+ } catch (err) {
40099
+ console.error(err instanceof Error ? err.message : String(err));
40100
+ process.exitCode = 1;
40101
+ }
40102
+ });
40103
+ clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to reset to (default: latest)").option("--latest", "reset to the latest snapshot").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40104
+ try {
40105
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40106
+ const result = await resetClone2({
40107
+ apiKey,
40108
+ apiBaseUrl,
40109
+ instanceId,
40110
+ cloneId,
40111
+ snapshotId: opts.snapshot,
40112
+ latest: opts.latest,
40113
+ debug: !!opts.debug
40114
+ });
40115
+ printResult(result ?? { reset: true, cloneId }, opts.json);
40116
+ } catch (err) {
40117
+ console.error(err instanceof Error ? err.message : String(err));
40118
+ process.exitCode = 1;
40119
+ }
40120
+ });
40121
+ clone2.command("destroy <cloneId>").description("destroy a clone (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40122
+ try {
40123
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40124
+ const result = await destroyClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
40125
+ printResult(result ?? { destroyed: true, cloneId }, opts.json);
40126
+ } catch (err) {
40127
+ console.error(err instanceof Error ? err.message : String(err));
40128
+ process.exitCode = 1;
40129
+ }
40130
+ });
40131
+ var branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
40132
+ branch.command("list").description("list the project's branches").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40133
+ try {
40134
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40135
+ const result = await listBranches2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
40136
+ printResult(result, opts.json);
40137
+ } catch (err) {
40138
+ console.error(err instanceof Error ? err.message : String(err));
40139
+ process.exitCode = 1;
40140
+ }
40141
+ });
40142
+ branch.command("create <name>").description("create a branch").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to base the branch on").option("--base-branch <branch>", "parent branch to fork from").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40143
+ try {
40144
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40145
+ const result = await createBranch2({
40146
+ apiKey,
40147
+ apiBaseUrl,
40148
+ instanceId,
40149
+ branchName: name,
40150
+ baseBranch: opts.baseBranch,
40151
+ snapshotId: opts.snapshot,
40152
+ debug: !!opts.debug
40153
+ });
40154
+ printResult(result, opts.json);
40155
+ } catch (err) {
40156
+ console.error(err instanceof Error ? err.message : String(err));
40157
+ process.exitCode = 1;
40158
+ }
40159
+ });
40160
+ branch.command("delete <name>").description("delete a branch (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40161
+ try {
40162
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40163
+ const result = await deleteBranch2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40164
+ printResult(result ?? { deleted: true, branch: name }, opts.json);
40165
+ } catch (err) {
40166
+ console.error(err instanceof Error ? err.message : String(err));
40167
+ process.exitCode = 1;
40168
+ }
40169
+ });
40170
+ branch.command("log <name>").description("show a branch's snapshot log").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40171
+ try {
40172
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40173
+ const result = await branchLog2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40174
+ printResult(result, opts.json);
40175
+ } catch (err) {
40176
+ console.error(err instanceof Error ? err.message : String(err));
40177
+ process.exitCode = 1;
40178
+ }
40179
+ });
40180
+ var snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
40181
+ snapshot.command("list").description("list the project's snapshots").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "filter by branch").option("--dataset <dataset>", "filter by dataset").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40182
+ try {
40183
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40184
+ const result = await listSnapshots2({
40185
+ apiKey,
40186
+ apiBaseUrl,
40187
+ instanceId,
40188
+ branchName: opts.branch,
40189
+ dataset: opts.dataset,
40190
+ debug: !!opts.debug
40191
+ });
40192
+ printResult(result, opts.json);
40193
+ } catch (err) {
40194
+ console.error(err instanceof Error ? err.message : String(err));
40195
+ process.exitCode = 1;
40196
+ }
40197
+ });
40198
+ snapshot.command("create").description("create a snapshot from a clone").requiredOption("--project <id|alias>", "project id or alias").requiredOption("--clone <id>", "clone id to snapshot").option("--message <message>", "snapshot message").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40199
+ try {
40200
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40201
+ const result = await createSnapshot2({
40202
+ apiKey,
40203
+ apiBaseUrl,
40204
+ instanceId,
40205
+ cloneId: opts.clone,
40206
+ message: opts.message,
40207
+ debug: !!opts.debug
40208
+ });
40209
+ printResult(result, opts.json);
40210
+ } catch (err) {
40211
+ console.error(err instanceof Error ? err.message : String(err));
40212
+ process.exitCode = 1;
40213
+ }
40214
+ });
40215
+ snapshot.command("destroy <snapshotId>").description("destroy a snapshot (requires joe:admin)").requiredOption("--project <id|alias>", "project id or alias").option("--force", "force-delete even when dependent clones exist").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (snapshotId, opts) => {
40216
+ try {
40217
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40218
+ const result = await destroySnapshot2({
40219
+ apiKey,
40220
+ apiBaseUrl,
40221
+ instanceId,
40222
+ snapshotId,
40223
+ force: !!opts.force,
40224
+ debug: !!opts.debug
40225
+ });
40226
+ printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
40227
+ } catch (err) {
40228
+ console.error(err instanceof Error ? err.message : String(err));
40229
+ process.exitCode = 1;
40230
+ }
40231
+ });
37892
40232
  var mcp = program2.command("mcp").description("MCP server integration");
37893
40233
  mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
37894
40234
  const rootOpts = program2.opts();
@@ -37962,29 +40302,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
37962
40302
  let configDir;
37963
40303
  switch (client) {
37964
40304
  case "cursor":
37965
- configPath = path7.join(homeDir, ".cursor", "mcp.json");
37966
- configDir = path7.dirname(configPath);
40305
+ configPath = path9.join(homeDir, ".cursor", "mcp.json");
40306
+ configDir = path9.dirname(configPath);
37967
40307
  break;
37968
40308
  case "windsurf":
37969
- configPath = path7.join(homeDir, ".windsurf", "mcp.json");
37970
- configDir = path7.dirname(configPath);
40309
+ configPath = path9.join(homeDir, ".windsurf", "mcp.json");
40310
+ configDir = path9.dirname(configPath);
37971
40311
  break;
37972
40312
  case "codex":
37973
- configPath = path7.join(homeDir, ".codex", "mcp.json");
37974
- configDir = path7.dirname(configPath);
40313
+ configPath = path9.join(homeDir, ".codex", "mcp.json");
40314
+ configDir = path9.dirname(configPath);
37975
40315
  break;
37976
40316
  default:
37977
40317
  console.error(`Configuration not implemented for: ${client}`);
37978
40318
  process.exitCode = 1;
37979
40319
  return;
37980
40320
  }
37981
- if (!fs9.existsSync(configDir)) {
37982
- fs9.mkdirSync(configDir, { recursive: true });
40321
+ if (!fs11.existsSync(configDir)) {
40322
+ fs11.mkdirSync(configDir, { recursive: true });
37983
40323
  }
37984
40324
  let config2 = { mcpServers: {} };
37985
- if (fs9.existsSync(configPath)) {
40325
+ if (fs11.existsSync(configPath)) {
37986
40326
  try {
37987
- const content = fs9.readFileSync(configPath, "utf8");
40327
+ const content = fs11.readFileSync(configPath, "utf8");
37988
40328
  config2 = JSON.parse(content);
37989
40329
  if (!config2.mcpServers) {
37990
40330
  config2.mcpServers = {};
@@ -37997,7 +40337,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
37997
40337
  command: pgaiPath,
37998
40338
  args: ["mcp", "start"]
37999
40339
  };
38000
- fs9.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
40340
+ fs11.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
38001
40341
  console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
38002
40342
  console.log(` Config file: ${configPath}`);
38003
40343
  console.log("");
@@ -38018,5 +40358,7 @@ export {
38018
40358
  registerMonitoringInstance,
38019
40359
  refreshBundledComposeIfStale,
38020
40360
  readDeployedTag,
38021
- isValidComposeYaml
40361
+ planMonitoringRegistration,
40362
+ isValidComposeYaml,
40363
+ inferCommandFromResult
38022
40364
  };