postgresai 0.16.0-dev.2 → 0.16.0-dev.4

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 });
@@ -13425,7 +13425,7 @@ var {
13425
13425
  // package.json
13426
13426
  var package_default = {
13427
13427
  name: "postgresai",
13428
- version: "0.16.0-dev.2",
13428
+ version: "0.16.0-dev.4",
13429
13429
  description: "postgres_ai CLI",
13430
13430
  license: "Apache-2.0",
13431
13431
  private: false,
@@ -16236,11 +16236,11 @@ var safeLoadAll = renamed("safeLoadAll", "loadAll");
16236
16236
  var safeDump = renamed("safeDump", "dump");
16237
16237
 
16238
16238
  // bin/postgres-ai.ts
16239
- import * as fs9 from "fs";
16240
- import * as path7 from "path";
16239
+ import * as fs11 from "fs";
16240
+ import * as path9 from "path";
16241
16241
  import * as os3 from "os";
16242
16242
  import { fileURLToPath as fileURLToPath2 } from "url";
16243
- import * as crypto2 from "crypto";
16243
+ import * as crypto4 from "crypto";
16244
16244
 
16245
16245
  // node_modules/pg/esm/index.mjs
16246
16246
  var import_lib = __toESM(require_lib2(), 1);
@@ -16256,7 +16256,7 @@ var Result = import_lib.default.Result;
16256
16256
  var TypeOverrides = import_lib.default.TypeOverrides;
16257
16257
  var defaults = import_lib.default.defaults;
16258
16258
  // package.json
16259
- var version = "0.16.0-dev.2";
16259
+ var version = "0.16.0-dev.4";
16260
16260
  var package_default2 = {
16261
16261
  name: "postgresai",
16262
16262
  version,
@@ -17267,9 +17267,601 @@ async function fetchReportFileData(params) {
17267
17267
  }
17268
17268
  }
17269
17269
 
17270
- // lib/storage.ts
17270
+ // lib/joe.ts
17271
17271
  import * as fs3 from "fs";
17272
17272
  import * as path3 from "path";
17273
+ import * as crypto from "node:crypto";
17274
+ var JOE_COMMANDS = [
17275
+ "plan",
17276
+ "explain",
17277
+ "exec",
17278
+ "hypo",
17279
+ "activity",
17280
+ "terminate",
17281
+ "reset",
17282
+ "describe"
17283
+ ];
17284
+ var TERMINAL_STATES = new Set([
17285
+ "done",
17286
+ "error",
17287
+ "timed_out"
17288
+ ]);
17289
+ var AI_ENABLED_COMMANDS = new Set([
17290
+ "plan",
17291
+ "explain",
17292
+ "exec",
17293
+ "hypo",
17294
+ "activity",
17295
+ "describe"
17296
+ ]);
17297
+ var EXECUTING_COMMANDS = new Set([
17298
+ "exec",
17299
+ "explain"
17300
+ ]);
17301
+ var DEFAULT_BUDGET_MS = 25000;
17302
+ var DEFAULT_POLL_INTERVAL_MS = 800;
17303
+ async function callRpc(params) {
17304
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
17305
+ if (!apiKey) {
17306
+ throw new Error("API key is required");
17307
+ }
17308
+ const base = normalizeBaseUrl(apiBaseUrl);
17309
+ const url = new URL(`${base}/rpc/${fn}`);
17310
+ const payload = JSON.stringify(body);
17311
+ const headers = {
17312
+ "access-token": apiKey,
17313
+ "Content-Type": "application/json",
17314
+ Connection: "close"
17315
+ };
17316
+ if (debug) {
17317
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17318
+ console.error(`Debug: POST URL: ${url.toString()}`);
17319
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17320
+ console.error(`Debug: Request body: ${payload}`);
17321
+ }
17322
+ const response = await fetch(url.toString(), {
17323
+ method: "POST",
17324
+ headers,
17325
+ body: payload
17326
+ });
17327
+ const text = await response.text();
17328
+ if (debug) {
17329
+ console.error(`Debug: Response status: ${response.status}`);
17330
+ console.error(`Debug: Response body: ${text}`);
17331
+ }
17332
+ if (!response.ok) {
17333
+ throw new Error(formatHttpError(operation, response.status, text));
17334
+ }
17335
+ try {
17336
+ return JSON.parse(text);
17337
+ } catch {
17338
+ throw new Error(`${operation}: failed to parse response: ${text}`);
17339
+ }
17340
+ }
17341
+ async function submitCommand(params) {
17342
+ const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
17343
+ if (!JOE_COMMANDS.includes(command)) {
17344
+ throw new Error(`Unknown Joe command: ${command}`);
17345
+ }
17346
+ const body = {
17347
+ project_id: projectId,
17348
+ command,
17349
+ sql: sql ?? null,
17350
+ args: args ?? null,
17351
+ session_id: sessionId ?? null
17352
+ };
17353
+ if (idempotencyKey) {
17354
+ body.idempotency_key = idempotencyKey;
17355
+ }
17356
+ return callRpc({
17357
+ apiKey,
17358
+ apiBaseUrl,
17359
+ fn: "joe_command_submit",
17360
+ body,
17361
+ operation: `Failed to submit ${command} command`,
17362
+ debug
17363
+ });
17364
+ }
17365
+ async function getCommandStatus(params) {
17366
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
17367
+ if (!commandId) {
17368
+ throw new Error("commandId is required");
17369
+ }
17370
+ return callRpc({
17371
+ apiKey,
17372
+ apiBaseUrl,
17373
+ fn: "joe_command_status",
17374
+ body: { command_id: commandId },
17375
+ operation: "Failed to fetch command status",
17376
+ debug
17377
+ });
17378
+ }
17379
+ async function getCommandResult(params) {
17380
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
17381
+ if (!commandId) {
17382
+ throw new Error("commandId is required");
17383
+ }
17384
+ return callRpc({
17385
+ apiKey,
17386
+ apiBaseUrl,
17387
+ fn: "joe_command_result",
17388
+ body: { command_id: commandId },
17389
+ operation: "Failed to fetch command result",
17390
+ debug
17391
+ });
17392
+ }
17393
+ function normalizeProjectRow(row) {
17394
+ const projectId = row.project_id ?? row.id ?? 0;
17395
+ return {
17396
+ project_id: Number(projectId),
17397
+ alias: row.alias ?? null,
17398
+ name: row.name ?? null,
17399
+ label: row.label ?? null,
17400
+ joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
17401
+ tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
17402
+ instance_id: row.instance_id == null ? null : Number(row.instance_id),
17403
+ dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
17404
+ };
17405
+ }
17406
+ async function listProjects(params) {
17407
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
17408
+ const body = {};
17409
+ if (typeof orgId === "number") {
17410
+ body.org_id = orgId;
17411
+ }
17412
+ const rows = await callRpc({
17413
+ apiKey,
17414
+ apiBaseUrl,
17415
+ fn: "projects_list",
17416
+ body,
17417
+ operation: "Failed to list projects",
17418
+ debug
17419
+ });
17420
+ if (!Array.isArray(rows)) {
17421
+ return [];
17422
+ }
17423
+ return rows.map(normalizeProjectRow);
17424
+ }
17425
+ function isNumericProjectRef(ref) {
17426
+ return /^[0-9]+$/.test(ref.trim());
17427
+ }
17428
+ async function resolveProjectId(params) {
17429
+ const ref = String(params.project ?? "").trim();
17430
+ if (!ref) {
17431
+ throw new Error("project is required (--project <id|alias>)");
17432
+ }
17433
+ if (isNumericProjectRef(ref)) {
17434
+ return Number(ref);
17435
+ }
17436
+ const projects = await listProjects({
17437
+ apiKey: params.apiKey,
17438
+ apiBaseUrl: params.apiBaseUrl,
17439
+ orgId: params.orgId,
17440
+ debug: params.debug
17441
+ });
17442
+ const needle = ref.toLowerCase();
17443
+ const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
17444
+ if (!match) {
17445
+ throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
17446
+ }
17447
+ return match.project_id;
17448
+ }
17449
+ function sessionStorePath(dir) {
17450
+ return path3.join(dir ?? getConfigDir2(), "joe-sessions.json");
17451
+ }
17452
+ function readSessionStore(dir) {
17453
+ const file = sessionStorePath(dir);
17454
+ if (!fs3.existsSync(file)) {
17455
+ return {};
17456
+ }
17457
+ try {
17458
+ const parsed = JSON.parse(fs3.readFileSync(file, "utf8"));
17459
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
17460
+ return parsed;
17461
+ }
17462
+ } catch {}
17463
+ return {};
17464
+ }
17465
+ function readStoredSessionId(projectId, dir) {
17466
+ const store = readSessionStore(dir);
17467
+ const value = store[String(projectId)];
17468
+ return typeof value === "string" && value.length > 0 ? value : null;
17469
+ }
17470
+ function writeStoredSessionId(projectId, sessionId, dir) {
17471
+ const baseDir = dir ?? getConfigDir2();
17472
+ if (!fs3.existsSync(baseDir)) {
17473
+ fs3.mkdirSync(baseDir, { recursive: true, mode: 448 });
17474
+ }
17475
+ const store = readSessionStore(dir);
17476
+ store[String(projectId)] = sessionId;
17477
+ fs3.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
17478
+ `, { mode: 384 });
17479
+ }
17480
+ function clearStoredSessionId(projectId, dir) {
17481
+ const file = sessionStorePath(dir);
17482
+ if (!fs3.existsSync(file)) {
17483
+ return;
17484
+ }
17485
+ const store = readSessionStore(dir);
17486
+ if (String(projectId) in store) {
17487
+ delete store[String(projectId)];
17488
+ fs3.writeFileSync(file, JSON.stringify(store, null, 2) + `
17489
+ `, { mode: 384 });
17490
+ }
17491
+ }
17492
+ function computeIdempotencyKey(command, projectId, sql, args) {
17493
+ if (EXECUTING_COMMANDS.has(command)) {
17494
+ const canonical = `${projectId}
17495
+ ${command}
17496
+ ${sql ?? ""}
17497
+ ${JSON.stringify(args ?? null)}`;
17498
+ return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
17499
+ }
17500
+ return `joe-${crypto.randomUUID()}`;
17501
+ }
17502
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
17503
+ async function runCommand(params) {
17504
+ const {
17505
+ apiKey,
17506
+ apiBaseUrl,
17507
+ command,
17508
+ projectId,
17509
+ sql,
17510
+ args,
17511
+ sessionId,
17512
+ debug
17513
+ } = params;
17514
+ const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS;
17515
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17516
+ const now = params.now ?? Date.now;
17517
+ const sleep = params.sleep ?? defaultSleep;
17518
+ const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
17519
+ const submitted = await submitCommand({
17520
+ apiKey,
17521
+ apiBaseUrl,
17522
+ command,
17523
+ projectId,
17524
+ sql,
17525
+ args,
17526
+ sessionId,
17527
+ idempotencyKey,
17528
+ debug
17529
+ });
17530
+ const commandId = submitted.command_id;
17531
+ const newSessionId = submitted.session_id ?? sessionId ?? null;
17532
+ const deadline = now() + budgetMs;
17533
+ let status = submitted.status;
17534
+ while (true) {
17535
+ const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
17536
+ status = statusResp.status;
17537
+ if (TERMINAL_STATES.has(status)) {
17538
+ break;
17539
+ }
17540
+ if (now() >= deadline) {
17541
+ return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
17542
+ }
17543
+ await sleep(pollIntervalMs);
17544
+ }
17545
+ const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
17546
+ return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
17547
+ }
17548
+ async function executeJoeCommand(params) {
17549
+ const projectId = await resolveProjectId({
17550
+ apiKey: params.apiKey,
17551
+ apiBaseUrl: params.apiBaseUrl,
17552
+ project: params.project,
17553
+ orgId: params.orgId,
17554
+ debug: params.debug
17555
+ });
17556
+ let sessionId;
17557
+ if (params.newSession) {
17558
+ clearStoredSessionId(projectId, params.sessionDir);
17559
+ sessionId = null;
17560
+ } else if (params.session) {
17561
+ sessionId = String(params.session);
17562
+ } else {
17563
+ sessionId = readStoredSessionId(projectId, params.sessionDir);
17564
+ }
17565
+ const outcome = await runCommand({
17566
+ apiKey: params.apiKey,
17567
+ apiBaseUrl: params.apiBaseUrl,
17568
+ command: params.command,
17569
+ projectId,
17570
+ sql: params.sql,
17571
+ args: params.args,
17572
+ sessionId,
17573
+ budgetMs: params.budgetMs,
17574
+ pollIntervalMs: params.pollIntervalMs,
17575
+ debug: params.debug,
17576
+ now: params.now,
17577
+ sleep: params.sleep
17578
+ });
17579
+ if (outcome.sessionId) {
17580
+ writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
17581
+ }
17582
+ return { ...outcome, command: params.command, projectId };
17583
+ }
17584
+
17585
+ // lib/dblab.ts
17586
+ function isNumericProjectRef2(ref) {
17587
+ return /^[0-9]+$/.test(ref.trim());
17588
+ }
17589
+ async function resolveDblabInstanceId(params) {
17590
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
17591
+ if (!apiKey) {
17592
+ throw new Error("API key is required");
17593
+ }
17594
+ const ref = String(params.project ?? "").trim();
17595
+ if (!ref) {
17596
+ throw new Error("project is required (--project <id|alias>)");
17597
+ }
17598
+ let projects;
17599
+ try {
17600
+ projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
17601
+ } catch (err) {
17602
+ const message = err instanceof Error ? err.message : String(err);
17603
+ throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
17604
+ }
17605
+ const numeric = isNumericProjectRef2(ref);
17606
+ const needle = ref.toLowerCase();
17607
+ const match = projects.find((p) => {
17608
+ if (numeric) {
17609
+ return String(p.project_id) === ref;
17610
+ }
17611
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
17612
+ });
17613
+ if (!match) {
17614
+ throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
17615
+ }
17616
+ if (match.dblab_instance_id == null) {
17617
+ throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
17618
+ }
17619
+ return String(match.dblab_instance_id);
17620
+ }
17621
+ async function callDblabApi(params) {
17622
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
17623
+ if (!apiKey) {
17624
+ throw new Error("API key is required");
17625
+ }
17626
+ if (!instanceId) {
17627
+ throw new Error("instanceId is required");
17628
+ }
17629
+ const base = normalizeBaseUrl(apiBaseUrl);
17630
+ const url = new URL(`${base}/rpc/dblab_api_call`);
17631
+ const bodyObj = {
17632
+ instance_id: instanceId,
17633
+ action,
17634
+ method
17635
+ };
17636
+ if (data !== undefined) {
17637
+ bodyObj.data = data;
17638
+ }
17639
+ const body = JSON.stringify(bodyObj);
17640
+ const headers = {
17641
+ "access-token": apiKey,
17642
+ "Content-Type": "application/json",
17643
+ Connection: "close"
17644
+ };
17645
+ if (debug) {
17646
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17647
+ console.error(`Debug: POST URL: ${url.toString()}`);
17648
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17649
+ console.error(`Debug: Request body: ${body}`);
17650
+ }
17651
+ const response = await fetch(url.toString(), { method: "POST", headers, body });
17652
+ const text = await response.text();
17653
+ if (debug) {
17654
+ console.error(`Debug: Response status: ${response.status}`);
17655
+ console.error(`Debug: Response body: ${text}`);
17656
+ }
17657
+ if (!response.ok) {
17658
+ throw new Error(formatHttpError(operation, response.status, text));
17659
+ }
17660
+ if (text.trim() === "") {
17661
+ return null;
17662
+ }
17663
+ try {
17664
+ return JSON.parse(text);
17665
+ } catch {
17666
+ throw new Error(`${operation}: failed to parse response: ${text}`);
17667
+ }
17668
+ }
17669
+ async function createClone(params) {
17670
+ const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
17671
+ const data = { protected: Boolean(isProtected) };
17672
+ if (cloneId)
17673
+ data.id = cloneId;
17674
+ if (branch)
17675
+ data.branch = branch;
17676
+ if (snapshotId)
17677
+ data.snapshot = { id: snapshotId };
17678
+ if (dbUser && dbPassword)
17679
+ data.db = { username: dbUser, password: dbPassword };
17680
+ return callDblabApi({
17681
+ apiKey,
17682
+ apiBaseUrl,
17683
+ instanceId,
17684
+ action: "/clone",
17685
+ method: "post",
17686
+ data,
17687
+ operation: "Failed to create clone",
17688
+ debug
17689
+ });
17690
+ }
17691
+ async function listClones(params) {
17692
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
17693
+ return callDblabApi({
17694
+ apiKey,
17695
+ apiBaseUrl,
17696
+ instanceId,
17697
+ action: "/clones",
17698
+ method: "get",
17699
+ operation: "Failed to list clones",
17700
+ debug
17701
+ });
17702
+ }
17703
+ async function getClone(params) {
17704
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17705
+ if (!cloneId)
17706
+ throw new Error("cloneId is required");
17707
+ return callDblabApi({
17708
+ apiKey,
17709
+ apiBaseUrl,
17710
+ instanceId,
17711
+ action: `/clone/${encodeURIComponent(cloneId)}`,
17712
+ method: "get",
17713
+ operation: "Failed to get clone",
17714
+ debug
17715
+ });
17716
+ }
17717
+ async function resetClone(params) {
17718
+ const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
17719
+ if (!cloneId)
17720
+ throw new Error("cloneId is required");
17721
+ const data = { latest: latest ?? !snapshotId };
17722
+ if (snapshotId)
17723
+ data.snapshotID = snapshotId;
17724
+ return callDblabApi({
17725
+ apiKey,
17726
+ apiBaseUrl,
17727
+ instanceId,
17728
+ action: `/clone/${encodeURIComponent(cloneId)}/reset`,
17729
+ method: "post",
17730
+ data,
17731
+ operation: "Failed to reset clone",
17732
+ debug
17733
+ });
17734
+ }
17735
+ async function destroyClone(params) {
17736
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17737
+ if (!cloneId)
17738
+ throw new Error("cloneId is required");
17739
+ return callDblabApi({
17740
+ apiKey,
17741
+ apiBaseUrl,
17742
+ instanceId,
17743
+ action: `/clone/${encodeURIComponent(cloneId)}`,
17744
+ method: "delete",
17745
+ operation: "Failed to destroy clone",
17746
+ debug
17747
+ });
17748
+ }
17749
+ async function listBranches(params) {
17750
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
17751
+ return callDblabApi({
17752
+ apiKey,
17753
+ apiBaseUrl,
17754
+ instanceId,
17755
+ action: "/branches",
17756
+ method: "get",
17757
+ operation: "Failed to list branches",
17758
+ debug
17759
+ });
17760
+ }
17761
+ async function createBranch(params) {
17762
+ const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
17763
+ if (!branchName)
17764
+ throw new Error("branchName is required");
17765
+ const data = { branchName };
17766
+ if (baseBranch)
17767
+ data.baseBranch = baseBranch;
17768
+ if (snapshotId)
17769
+ data.snapshotID = snapshotId;
17770
+ return callDblabApi({
17771
+ apiKey,
17772
+ apiBaseUrl,
17773
+ instanceId,
17774
+ action: "/branch",
17775
+ method: "post",
17776
+ data,
17777
+ operation: "Failed to create branch",
17778
+ debug
17779
+ });
17780
+ }
17781
+ async function deleteBranch(params) {
17782
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17783
+ if (!branchName)
17784
+ throw new Error("branchName is required");
17785
+ return callDblabApi({
17786
+ apiKey,
17787
+ apiBaseUrl,
17788
+ instanceId,
17789
+ action: `/branch/${encodeURIComponent(branchName)}`,
17790
+ method: "delete",
17791
+ operation: "Failed to delete branch",
17792
+ debug
17793
+ });
17794
+ }
17795
+ async function branchLog(params) {
17796
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17797
+ if (!branchName)
17798
+ throw new Error("branchName is required");
17799
+ return callDblabApi({
17800
+ apiKey,
17801
+ apiBaseUrl,
17802
+ instanceId,
17803
+ action: `/branch/${encodeURIComponent(branchName)}/log`,
17804
+ method: "get",
17805
+ operation: "Failed to fetch branch log",
17806
+ debug
17807
+ });
17808
+ }
17809
+ async function listSnapshots(params) {
17810
+ const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
17811
+ const qs = new URLSearchParams;
17812
+ const branch = branchName?.trim();
17813
+ if (branch)
17814
+ qs.append("branch", branch);
17815
+ if (dataset)
17816
+ qs.append("dataset", dataset);
17817
+ const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
17818
+ return callDblabApi({
17819
+ apiKey,
17820
+ apiBaseUrl,
17821
+ instanceId,
17822
+ action,
17823
+ method: "get",
17824
+ operation: "Failed to list snapshots",
17825
+ debug
17826
+ });
17827
+ }
17828
+ async function createSnapshot(params) {
17829
+ const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
17830
+ if (!cloneId)
17831
+ throw new Error("cloneId is required");
17832
+ const data = { cloneID: cloneId };
17833
+ if (message)
17834
+ data.message = message;
17835
+ return callDblabApi({
17836
+ apiKey,
17837
+ apiBaseUrl,
17838
+ instanceId,
17839
+ action: "/branch/snapshot",
17840
+ method: "post",
17841
+ data,
17842
+ operation: "Failed to create snapshot",
17843
+ debug
17844
+ });
17845
+ }
17846
+ async function destroySnapshot(params) {
17847
+ const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
17848
+ if (!snapshotId)
17849
+ throw new Error("snapshotId is required");
17850
+ const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
17851
+ return callDblabApi({
17852
+ apiKey,
17853
+ apiBaseUrl,
17854
+ instanceId,
17855
+ action,
17856
+ method: "delete",
17857
+ operation: "Failed to destroy snapshot",
17858
+ debug
17859
+ });
17860
+ }
17861
+
17862
+ // lib/storage.ts
17863
+ import * as fs4 from "fs";
17864
+ import * as path4 from "path";
17273
17865
  var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
17274
17866
  var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17275
17867
  var MIME_TYPES = {
@@ -17310,11 +17902,11 @@ async function uploadFile(params) {
17310
17902
  if (!filePath) {
17311
17903
  throw new Error("filePath is required");
17312
17904
  }
17313
- const resolvedPath = path3.resolve(filePath);
17314
- if (!fs3.existsSync(resolvedPath)) {
17905
+ const resolvedPath = path4.resolve(filePath);
17906
+ if (!fs4.existsSync(resolvedPath)) {
17315
17907
  throw new Error(`File not found: ${resolvedPath}`);
17316
17908
  }
17317
- const stat = fs3.statSync(resolvedPath);
17909
+ const stat = fs4.statSync(resolvedPath);
17318
17910
  if (!stat.isFile()) {
17319
17911
  throw new Error(`Not a file: ${resolvedPath}`);
17320
17912
  }
@@ -17326,9 +17918,9 @@ async function uploadFile(params) {
17326
17918
  console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17327
17919
  }
17328
17920
  const url = `${base}/upload`;
17329
- const fileBuffer = fs3.readFileSync(resolvedPath);
17330
- const fileName = path3.basename(resolvedPath);
17331
- const ext = path3.extname(fileName).toLowerCase();
17921
+ const fileBuffer = fs4.readFileSync(resolvedPath);
17922
+ const fileName = path4.basename(resolvedPath);
17923
+ const ext = path4.extname(fileName).toLowerCase();
17332
17924
  const mimeType = MIME_TYPES[ext] || "application/octet-stream";
17333
17925
  const formData = new FormData;
17334
17926
  formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
@@ -17387,15 +17979,15 @@ async function downloadFile(params) {
17387
17979
  const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17388
17980
  fullUrl = `${base}${relativePath}`;
17389
17981
  }
17390
- const urlFilename = path3.basename(new URL(fullUrl).pathname);
17982
+ const urlFilename = path4.basename(new URL(fullUrl).pathname);
17391
17983
  if (!urlFilename) {
17392
17984
  throw new Error("Cannot derive filename from URL; please specify --output");
17393
17985
  }
17394
- const saveTo = outputPath ? path3.resolve(outputPath) : path3.resolve(urlFilename);
17986
+ const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
17395
17987
  if (!outputPath) {
17396
- const normalizedSave = path3.normalize(saveTo);
17397
- const cwd = path3.normalize(process.cwd());
17398
- if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path3.sep)) {
17988
+ const normalizedSave = path4.normalize(saveTo);
17989
+ const cwd = path4.normalize(process.cwd());
17990
+ if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
17399
17991
  throw new Error("Derived output path escapes current directory; please specify --output");
17400
17992
  }
17401
17993
  }
@@ -17427,11 +18019,11 @@ async function downloadFile(params) {
17427
18019
  }
17428
18020
  const arrayBuffer = await response.arrayBuffer();
17429
18021
  const buffer = Buffer.from(arrayBuffer);
17430
- const parentDir = path3.dirname(saveTo);
17431
- if (!fs3.existsSync(parentDir)) {
17432
- fs3.mkdirSync(parentDir, { recursive: true });
18022
+ const parentDir = path4.dirname(saveTo);
18023
+ if (!fs4.existsSync(parentDir)) {
18024
+ fs4.mkdirSync(parentDir, { recursive: true });
17433
18025
  }
17434
- fs3.writeFileSync(saveTo, buffer);
18026
+ fs4.writeFileSync(saveTo, buffer);
17435
18027
  return {
17436
18028
  savedTo: saveTo,
17437
18029
  size: buffer.length,
@@ -17443,9 +18035,9 @@ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
17443
18035
  const base = normalizeBaseUrl(storageBaseUrl);
17444
18036
  const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17445
18037
  const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
17446
- const name = filename || path3.basename(new URL(fullUrl).pathname);
18038
+ const name = filename || path4.basename(new URL(fullUrl).pathname);
17447
18039
  const safeName = name.replace(/[\[\]()]/g, "\\$&");
17448
- const ext = path3.extname(name).toLowerCase();
18040
+ const ext = path4.extname(name).toLowerCase();
17449
18041
  if (IMAGE_EXTENSIONS.has(ext)) {
17450
18042
  return `![${safeName}](${fullUrl})`;
17451
18043
  }
@@ -17733,10 +18325,10 @@ function mergeDefs(...defs) {
17733
18325
  function cloneDef(schema2) {
17734
18326
  return mergeDefs(schema2._zod.def);
17735
18327
  }
17736
- function getElementAtPath(obj, path4) {
17737
- if (!path4)
18328
+ function getElementAtPath(obj, path5) {
18329
+ if (!path5)
17738
18330
  return obj;
17739
- return path4.reduce((acc, key) => acc?.[key], obj);
18331
+ return path5.reduce((acc, key) => acc?.[key], obj);
17740
18332
  }
17741
18333
  function promiseAllObject(promisesObj) {
17742
18334
  const keys = Object.keys(promisesObj);
@@ -18100,11 +18692,11 @@ function aborted(x, startIndex = 0) {
18100
18692
  }
18101
18693
  return false;
18102
18694
  }
18103
- function prefixIssues(path4, issues) {
18695
+ function prefixIssues(path5, issues) {
18104
18696
  return issues.map((iss) => {
18105
18697
  var _a;
18106
18698
  (_a = iss).path ?? (_a.path = []);
18107
- iss.path.unshift(path4);
18699
+ iss.path.unshift(path5);
18108
18700
  return iss;
18109
18701
  });
18110
18702
  }
@@ -24376,49 +24968,243 @@ class StdioServerTransport {
24376
24968
  // lib/mcp-server.ts
24377
24969
  var interpretEscapes = (str2) => (str2 || "").replace(/\\n/g, `
24378
24970
  `).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) {
24387
- return {
24388
- content: [
24389
- {
24390
- type: "text",
24391
- text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
24392
- }
24393
- ],
24394
- isError: true
24395
- };
24396
- }
24397
- try {
24398
- if (toolName === "list_issues") {
24399
- const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
24400
- const statusArg = args.status ? String(args.status) : undefined;
24401
- let status;
24402
- if (statusArg === "open")
24403
- status = "open";
24404
- else if (statusArg === "closed")
24405
- status = "closed";
24406
- const limit = args.limit !== undefined ? Number(args.limit) : undefined;
24407
- const offset = args.offset !== undefined ? Number(args.offset) : undefined;
24408
- const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
24409
- return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
24410
- }
24411
- if (toolName === "view_issue") {
24412
- const issueId = String(args.issue_id || "").trim();
24413
- if (!issueId) {
24414
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
24971
+ var JOE_TOOL_TO_COMMAND = {
24972
+ plan_query: "plan",
24973
+ explain_query: "explain",
24974
+ exec_sql: "exec",
24975
+ hypo_index: "hypo",
24976
+ activity: "activity",
24977
+ terminate_backend: "terminate",
24978
+ reset_clone: "reset",
24979
+ describe: "describe"
24980
+ };
24981
+ function joeToolDefinitions() {
24982
+ const sessionProps = {
24983
+ project_id: {
24984
+ type: "string",
24985
+ description: "Target project by numeric id OR alias/name (id-or-alias). --project 12 ≡ main-db."
24986
+ },
24987
+ session_id: {
24988
+ type: "string",
24989
+ description: "Run on a specific session's clone (64-bit id as a string). Auto-reused per project when omitted."
24990
+ },
24991
+ new_session: { type: "boolean", description: "Force a fresh session/clone (null session)." },
24992
+ timeout_ms: { type: "number", description: "One-shot poll budget in ms (≤ 25000); on expiry returns a command_id to resume." },
24993
+ debug: { type: "boolean", description: "Enable verbose debug logs." }
24994
+ };
24995
+ return [
24996
+ {
24997
+ name: "plan_query",
24998
+ 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.",
24999
+ inputSchema: {
25000
+ type: "object",
25001
+ properties: { sql: { type: "string", description: "The query to plan (one explainable statement)." }, ...sessionProps },
25002
+ required: ["sql", "project_id"],
25003
+ additionalProperties: false
24415
25004
  }
24416
- const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
24417
- if (!issue2) {
24418
- return { content: [{ type: "text", text: "Issue not found" }], isError: true };
25005
+ },
25006
+ {
25007
+ name: "explain_query",
25008
+ 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).",
25009
+ inputSchema: {
25010
+ type: "object",
25011
+ properties: { sql: { type: "string", description: "The query to EXPLAIN ANALYZE (executes on the clone)." }, ...sessionProps },
25012
+ required: ["sql", "project_id"],
25013
+ additionalProperties: false
24419
25014
  }
24420
- const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
24421
- const combined = { issue: issue2, comments };
25015
+ },
25016
+ {
25017
+ name: "exec_sql",
25018
+ 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.",
25019
+ inputSchema: {
25020
+ type: "object",
25021
+ properties: { sql: { type: "string", description: "Arbitrary DDL/DML to run on the clone (e.g. create index, analyze)." }, ...sessionProps },
25022
+ required: ["sql", "project_id"],
25023
+ additionalProperties: false
25024
+ }
25025
+ },
25026
+ {
25027
+ name: "hypo_index",
25028
+ description: "HypoPG hypothetical index + re-plan (no real index built, no data change) — would the planner switch to it? LLM-facing. Requires joe:plan.",
25029
+ inputSchema: {
25030
+ type: "object",
25031
+ properties: {
25032
+ sql: { type: "string", description: "Candidate index DDL (e.g. create index on orders (customer_id))." },
25033
+ query: { type: "string", description: "Target query the hypothetical index is evaluated against." },
25034
+ ...sessionProps
25035
+ },
25036
+ required: ["sql", "query", "project_id"],
25037
+ additionalProperties: false
25038
+ }
25039
+ },
25040
+ {
25041
+ name: "activity",
25042
+ description: "pg_stat_activity snapshot on the clone (query text redacted). LLM-facing. Requires joe:plan.",
25043
+ inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
25044
+ },
25045
+ {
25046
+ name: "terminate_backend",
25047
+ description: "pg_terminate_backend(pid) on the clone. Status-only (no data to the LLM). Requires joe:admin.",
25048
+ inputSchema: {
25049
+ type: "object",
25050
+ properties: { pid: { type: "number", description: "Backend pid to terminate." }, ...sessionProps },
25051
+ required: ["pid", "project_id"],
25052
+ additionalProperties: false
25053
+ }
25054
+ },
25055
+ {
25056
+ name: "reset_clone",
25057
+ description: "Reset/recreate the session's thin clone. Status-only (no data to the LLM). Requires joe:admin.",
25058
+ inputSchema: { type: "object", properties: { ...sessionProps }, required: ["project_id"], additionalProperties: false }
25059
+ },
25060
+ {
25061
+ name: "describe",
25062
+ description: "\\d-family schema/relation/index/db/view metadata introspection. LLM-facing (metadata). Requires joe:plan.",
25063
+ inputSchema: {
25064
+ type: "object",
25065
+ properties: {
25066
+ object: { type: "string", description: "Object to describe (e.g. users)." },
25067
+ variant: { type: "string", description: "\\d-family variant (e.g. \\d+, \\di, \\dt)." },
25068
+ ...sessionProps
25069
+ },
25070
+ required: ["object", "project_id"],
25071
+ additionalProperties: false
25072
+ }
25073
+ },
25074
+ {
25075
+ name: "list_projects",
25076
+ 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.",
25077
+ inputSchema: {
25078
+ type: "object",
25079
+ properties: {
25080
+ org_id: { type: "number", description: "Organization ID (optional, falls back to config)." },
25081
+ debug: { type: "boolean", description: "Enable verbose debug logs." }
25082
+ },
25083
+ additionalProperties: false
25084
+ }
25085
+ }
25086
+ ];
25087
+ }
25088
+ async function runJoeTool(params) {
25089
+ const command = JOE_TOOL_TO_COMMAND[params.toolName];
25090
+ const a = params.args;
25091
+ const project = String(a.project_id ?? "").trim();
25092
+ if (!project) {
25093
+ return { content: [{ type: "text", text: "project_id is required" }], isError: true };
25094
+ }
25095
+ let sql = null;
25096
+ let cmdArgs = null;
25097
+ if (command === "plan" || command === "explain" || command === "exec" || command === "hypo") {
25098
+ sql = String(a.sql ?? "").trim();
25099
+ if (!sql) {
25100
+ return { content: [{ type: "text", text: "sql is required" }], isError: true };
25101
+ }
25102
+ }
25103
+ if (command === "hypo") {
25104
+ const query = String(a.query ?? "").trim();
25105
+ if (!query) {
25106
+ return { content: [{ type: "text", text: "query is required for hypo_index" }], isError: true };
25107
+ }
25108
+ cmdArgs = { query };
25109
+ }
25110
+ if (command === "terminate") {
25111
+ const pid = Number(a.pid);
25112
+ if (!Number.isFinite(pid)) {
25113
+ return { content: [{ type: "text", text: "pid (number) is required for terminate_backend" }], isError: true };
25114
+ }
25115
+ cmdArgs = { pid };
25116
+ }
25117
+ if (command === "describe") {
25118
+ const object4 = String(a.object ?? "").trim();
25119
+ if (!object4) {
25120
+ return { content: [{ type: "text", text: "object is required for describe" }], isError: true };
25121
+ }
25122
+ cmdArgs = { object: object4 };
25123
+ if (a.variant !== undefined)
25124
+ cmdArgs.variant = String(a.variant);
25125
+ }
25126
+ const session = a.session_id !== undefined && a.session_id !== null ? String(a.session_id) : null;
25127
+ const newSession = a.new_session === true;
25128
+ const budgetMs = a.timeout_ms !== undefined ? Number(a.timeout_ms) : undefined;
25129
+ const outcome = await executeJoeCommand({
25130
+ apiKey: params.apiKey,
25131
+ apiBaseUrl: params.apiBaseUrl,
25132
+ command,
25133
+ project,
25134
+ sql,
25135
+ args: cmdArgs,
25136
+ session,
25137
+ newSession,
25138
+ orgId: params.orgId,
25139
+ budgetMs,
25140
+ debug: params.debug
25141
+ });
25142
+ const payload = {
25143
+ command,
25144
+ command_id: outcome.commandId,
25145
+ session_id: outcome.sessionId,
25146
+ status: outcome.status,
25147
+ budget_expired: outcome.budgetExpired,
25148
+ resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
25149
+ result: outcome.result
25150
+ };
25151
+ const isError = outcome.status === "error" || outcome.status === "timed_out";
25152
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
25153
+ }
25154
+ async function resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfgOrgId, debug) {
25155
+ const project = String(args.project || "").trim();
25156
+ if (!project) {
25157
+ throw new Error("project is required (project id or alias)");
25158
+ }
25159
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfgOrgId ?? undefined;
25160
+ return resolveDblabInstanceId({ apiKey, apiBaseUrl, project, orgId, debug });
25161
+ }
25162
+ function optStr(v) {
25163
+ return v !== undefined && v !== null && String(v).length > 0 ? String(v) : undefined;
25164
+ }
25165
+ async function handleToolCall(req, rootOpts, extra) {
25166
+ const toolName = req.params.name;
25167
+ const args = req.params.arguments || {};
25168
+ const cfg = readConfig2();
25169
+ const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
25170
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
25171
+ const debug = Boolean(args.debug ?? extra?.debug);
25172
+ if (!apiKey) {
25173
+ return {
25174
+ content: [
25175
+ {
25176
+ type: "text",
25177
+ text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY."
25178
+ }
25179
+ ],
25180
+ isError: true
25181
+ };
25182
+ }
25183
+ try {
25184
+ if (toolName === "list_issues") {
25185
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
25186
+ const statusArg = args.status ? String(args.status) : undefined;
25187
+ let status;
25188
+ if (statusArg === "open")
25189
+ status = "open";
25190
+ else if (statusArg === "closed")
25191
+ status = "closed";
25192
+ const limit = args.limit !== undefined ? Number(args.limit) : undefined;
25193
+ const offset = args.offset !== undefined ? Number(args.offset) : undefined;
25194
+ const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
25195
+ return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
25196
+ }
25197
+ if (toolName === "view_issue") {
25198
+ const issueId = String(args.issue_id || "").trim();
25199
+ if (!issueId) {
25200
+ return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
25201
+ }
25202
+ const issue2 = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
25203
+ if (!issue2) {
25204
+ return { content: [{ type: "text", text: "Issue not found" }], isError: true };
25205
+ }
25206
+ const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
25207
+ const combined = { issue: issue2, comments };
24422
25208
  return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
24423
25209
  }
24424
25210
  if (toolName === "post_issue_comment") {
@@ -24653,6 +25439,147 @@ async function handleToolCall(req, rootOpts, extra) {
24653
25439
  const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type: type2, checkId, debug });
24654
25440
  return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
24655
25441
  }
25442
+ if (toolName === "list_projects") {
25443
+ const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
25444
+ const projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
25445
+ return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
25446
+ }
25447
+ if (toolName in JOE_TOOL_TO_COMMAND) {
25448
+ const orgId = cfg.orgId ?? undefined;
25449
+ return await runJoeTool({ toolName, apiKey, apiBaseUrl, orgId, args, debug });
25450
+ }
25451
+ if (toolName === "clone_create") {
25452
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25453
+ const result = await createClone({
25454
+ apiKey,
25455
+ apiBaseUrl,
25456
+ instanceId,
25457
+ cloneId: optStr(args.clone_id),
25458
+ branch: optStr(args.branch),
25459
+ snapshotId: optStr(args.snapshot_id),
25460
+ dbUser: optStr(args.db_user),
25461
+ dbPassword: optStr(args.db_password),
25462
+ isProtected: args.protected === true,
25463
+ debug
25464
+ });
25465
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25466
+ }
25467
+ if (toolName === "clone_list") {
25468
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25469
+ const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug });
25470
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25471
+ }
25472
+ if (toolName === "clone_status") {
25473
+ const cloneId = String(args.clone_id || "").trim();
25474
+ if (!cloneId)
25475
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25476
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25477
+ const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
25478
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25479
+ }
25480
+ if (toolName === "clone_reset") {
25481
+ const cloneId = String(args.clone_id || "").trim();
25482
+ if (!cloneId)
25483
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25484
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25485
+ const result = await resetClone({
25486
+ apiKey,
25487
+ apiBaseUrl,
25488
+ instanceId,
25489
+ cloneId,
25490
+ snapshotId: optStr(args.snapshot_id),
25491
+ latest: args.latest === true ? true : undefined,
25492
+ debug
25493
+ });
25494
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { reset: true, cloneId }, null, 2) }] };
25495
+ }
25496
+ if (toolName === "clone_destroy") {
25497
+ const cloneId = String(args.clone_id || "").trim();
25498
+ if (!cloneId)
25499
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25500
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25501
+ const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug });
25502
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, cloneId }, null, 2) }] };
25503
+ }
25504
+ if (toolName === "branch_list") {
25505
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25506
+ const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug });
25507
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25508
+ }
25509
+ if (toolName === "branch_create") {
25510
+ const branchName = String(args.name || "").trim();
25511
+ if (!branchName)
25512
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25513
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25514
+ const result = await createBranch({
25515
+ apiKey,
25516
+ apiBaseUrl,
25517
+ instanceId,
25518
+ branchName,
25519
+ baseBranch: optStr(args.base_branch),
25520
+ snapshotId: optStr(args.snapshot_id),
25521
+ debug
25522
+ });
25523
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25524
+ }
25525
+ if (toolName === "branch_delete") {
25526
+ const branchName = String(args.name || "").trim();
25527
+ if (!branchName)
25528
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25529
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25530
+ const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName, debug });
25531
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { deleted: true, branch: branchName }, null, 2) }] };
25532
+ }
25533
+ if (toolName === "branch_log") {
25534
+ const branchName = String(args.name || "").trim();
25535
+ if (!branchName)
25536
+ return { content: [{ type: "text", text: "name is required" }], isError: true };
25537
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25538
+ const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName, debug });
25539
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25540
+ }
25541
+ if (toolName === "snapshot_list") {
25542
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25543
+ const result = await listSnapshots({
25544
+ apiKey,
25545
+ apiBaseUrl,
25546
+ instanceId,
25547
+ branchName: optStr(args.branch),
25548
+ dataset: optStr(args.dataset),
25549
+ debug
25550
+ });
25551
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25552
+ }
25553
+ if (toolName === "snapshot_create") {
25554
+ const cloneId = String(args.clone_id || "").trim();
25555
+ if (!cloneId)
25556
+ return { content: [{ type: "text", text: "clone_id is required" }], isError: true };
25557
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25558
+ const result = await createSnapshot({
25559
+ apiKey,
25560
+ apiBaseUrl,
25561
+ instanceId,
25562
+ cloneId,
25563
+ message: optStr(args.message),
25564
+ debug
25565
+ });
25566
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
25567
+ }
25568
+ if (toolName === "snapshot_destroy") {
25569
+ const snapshotId = String(args.snapshot_id || "").trim();
25570
+ if (!snapshotId)
25571
+ return { content: [{ type: "text", text: "snapshot_id is required" }], isError: true };
25572
+ const instanceId = await resolveDblabInstanceForTool(apiKey, apiBaseUrl, args, cfg.orgId, debug);
25573
+ const result = await destroySnapshot({
25574
+ apiKey,
25575
+ apiBaseUrl,
25576
+ instanceId,
25577
+ snapshotId,
25578
+ force: args.force === true,
25579
+ debug
25580
+ });
25581
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { destroyed: true, snapshot: snapshotId }, null, 2) }] };
25582
+ }
24656
25583
  throw new Error(`Unknown tool: ${toolName}`);
24657
25584
  } catch (err) {
24658
25585
  const message = err instanceof Error ? err.message : String(err);
@@ -24929,6 +25856,197 @@ async function startMcpServer(rootOpts, extra) {
24929
25856
  },
24930
25857
  additionalProperties: false
24931
25858
  }
25859
+ },
25860
+ ...joeToolDefinitions(),
25861
+ {
25862
+ name: "clone_create",
25863
+ description: "Create a DBLab thin clone of the project's database (same as CLI 'clone create'). Requires the joe:plan scope.",
25864
+ inputSchema: {
25865
+ type: "object",
25866
+ properties: {
25867
+ project: { type: "string", description: "Project id or alias" },
25868
+ branch: { type: "string", description: "Branch to clone from" },
25869
+ snapshot_id: { type: "string", description: "Snapshot id to clone from" },
25870
+ clone_id: { type: "string", description: "Clone id (DBLab generates one when omitted)" },
25871
+ db_user: { type: "string", description: "Clone DB user" },
25872
+ db_password: { type: "string", description: "Clone DB password" },
25873
+ protected: { type: "boolean", description: "Protect the clone from auto-deletion" },
25874
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25875
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25876
+ },
25877
+ required: ["project"],
25878
+ additionalProperties: false
25879
+ }
25880
+ },
25881
+ {
25882
+ name: "clone_list",
25883
+ description: "List the project's DBLab thin clones (same as CLI 'clone list').",
25884
+ inputSchema: {
25885
+ type: "object",
25886
+ properties: {
25887
+ project: { type: "string", description: "Project id or alias" },
25888
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25889
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25890
+ },
25891
+ required: ["project"],
25892
+ additionalProperties: false
25893
+ }
25894
+ },
25895
+ {
25896
+ name: "clone_status",
25897
+ description: "Show a DBLab clone's status (same as CLI 'clone status').",
25898
+ inputSchema: {
25899
+ type: "object",
25900
+ properties: {
25901
+ project: { type: "string", description: "Project id or alias" },
25902
+ clone_id: { type: "string", description: "Clone id" },
25903
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25904
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25905
+ },
25906
+ required: ["project", "clone_id"],
25907
+ additionalProperties: false
25908
+ }
25909
+ },
25910
+ {
25911
+ name: "clone_reset",
25912
+ description: "Reset a DBLab clone to a pristine snapshot (same as CLI 'clone reset'). Requires the joe:admin scope.",
25913
+ inputSchema: {
25914
+ type: "object",
25915
+ properties: {
25916
+ project: { type: "string", description: "Project id or alias" },
25917
+ clone_id: { type: "string", description: "Clone id" },
25918
+ snapshot_id: { type: "string", description: "Snapshot id to reset to (default: latest)" },
25919
+ latest: { type: "boolean", description: "Reset to the latest snapshot" },
25920
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25921
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25922
+ },
25923
+ required: ["project", "clone_id"],
25924
+ additionalProperties: false
25925
+ }
25926
+ },
25927
+ {
25928
+ name: "clone_destroy",
25929
+ description: "Destroy a DBLab clone (same as CLI 'clone destroy'). Requires the joe:admin scope.",
25930
+ inputSchema: {
25931
+ type: "object",
25932
+ properties: {
25933
+ project: { type: "string", description: "Project id or alias" },
25934
+ clone_id: { type: "string", description: "Clone id" },
25935
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25936
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25937
+ },
25938
+ required: ["project", "clone_id"],
25939
+ additionalProperties: false
25940
+ }
25941
+ },
25942
+ {
25943
+ name: "branch_list",
25944
+ description: "List the project's DBLab branches (same as CLI 'branch list').",
25945
+ inputSchema: {
25946
+ type: "object",
25947
+ properties: {
25948
+ project: { type: "string", description: "Project id or alias" },
25949
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25950
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25951
+ },
25952
+ required: ["project"],
25953
+ additionalProperties: false
25954
+ }
25955
+ },
25956
+ {
25957
+ name: "branch_create",
25958
+ description: "Create a DBLab branch (same as CLI 'branch create'). Requires the joe:plan scope.",
25959
+ inputSchema: {
25960
+ type: "object",
25961
+ properties: {
25962
+ project: { type: "string", description: "Project id or alias" },
25963
+ name: { type: "string", description: "Branch name" },
25964
+ snapshot_id: { type: "string", description: "Snapshot id to base the branch on" },
25965
+ base_branch: { type: "string", description: "Parent branch to fork from" },
25966
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25967
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25968
+ },
25969
+ required: ["project", "name"],
25970
+ additionalProperties: false
25971
+ }
25972
+ },
25973
+ {
25974
+ name: "branch_delete",
25975
+ description: "Delete a DBLab branch (same as CLI 'branch delete'). Requires the joe:admin scope.",
25976
+ inputSchema: {
25977
+ type: "object",
25978
+ properties: {
25979
+ project: { type: "string", description: "Project id or alias" },
25980
+ name: { type: "string", description: "Branch name" },
25981
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25982
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25983
+ },
25984
+ required: ["project", "name"],
25985
+ additionalProperties: false
25986
+ }
25987
+ },
25988
+ {
25989
+ name: "branch_log",
25990
+ description: "Show a DBLab branch's snapshot log (same as CLI 'branch log').",
25991
+ inputSchema: {
25992
+ type: "object",
25993
+ properties: {
25994
+ project: { type: "string", description: "Project id or alias" },
25995
+ name: { type: "string", description: "Branch name" },
25996
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
25997
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
25998
+ },
25999
+ required: ["project", "name"],
26000
+ additionalProperties: false
26001
+ }
26002
+ },
26003
+ {
26004
+ name: "snapshot_list",
26005
+ description: "List the project's DBLab snapshots (same as CLI 'snapshot list').",
26006
+ inputSchema: {
26007
+ type: "object",
26008
+ properties: {
26009
+ project: { type: "string", description: "Project id or alias" },
26010
+ branch: { type: "string", description: "Filter by branch" },
26011
+ dataset: { type: "string", description: "Filter by dataset" },
26012
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26013
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26014
+ },
26015
+ required: ["project"],
26016
+ additionalProperties: false
26017
+ }
26018
+ },
26019
+ {
26020
+ name: "snapshot_create",
26021
+ description: "Create a DBLab snapshot from a clone (same as CLI 'snapshot create'). Requires the joe:plan scope.",
26022
+ inputSchema: {
26023
+ type: "object",
26024
+ properties: {
26025
+ project: { type: "string", description: "Project id or alias" },
26026
+ clone_id: { type: "string", description: "Clone id to snapshot" },
26027
+ message: { type: "string", description: "Snapshot message" },
26028
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26029
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26030
+ },
26031
+ required: ["project", "clone_id"],
26032
+ additionalProperties: false
26033
+ }
26034
+ },
26035
+ {
26036
+ name: "snapshot_destroy",
26037
+ description: "Destroy a DBLab snapshot (same as CLI 'snapshot destroy'). Requires the joe:admin scope.",
26038
+ inputSchema: {
26039
+ type: "object",
26040
+ properties: {
26041
+ project: { type: "string", description: "Project id or alias" },
26042
+ snapshot_id: { type: "string", description: "Snapshot id" },
26043
+ force: { type: "boolean", description: "Force-delete even when dependent clones exist" },
26044
+ org_id: { type: "number", description: "Organization id (optional, falls back to config)" },
26045
+ debug: { type: "boolean", description: "Enable verbose debug logs" }
26046
+ },
26047
+ required: ["project", "snapshot_id"],
26048
+ additionalProperties: false
26049
+ }
24932
26050
  }
24933
26051
  ]
24934
26052
  };
@@ -25801,11 +26919,689 @@ function renderMarkdownForTerminal(md) {
25801
26919
  `);
25802
26920
  }
25803
26921
 
26922
+ // lib/joe.ts
26923
+ import * as fs5 from "fs";
26924
+ import * as path5 from "path";
26925
+ import * as crypto2 from "node:crypto";
26926
+ var JOE_COMMANDS2 = [
26927
+ "plan",
26928
+ "explain",
26929
+ "exec",
26930
+ "hypo",
26931
+ "activity",
26932
+ "terminate",
26933
+ "reset",
26934
+ "describe"
26935
+ ];
26936
+ var TERMINAL_STATES2 = new Set([
26937
+ "done",
26938
+ "error",
26939
+ "timed_out"
26940
+ ]);
26941
+ var AI_ENABLED_COMMANDS2 = new Set([
26942
+ "plan",
26943
+ "explain",
26944
+ "exec",
26945
+ "hypo",
26946
+ "activity",
26947
+ "describe"
26948
+ ]);
26949
+ var EXECUTING_COMMANDS2 = new Set([
26950
+ "exec",
26951
+ "explain"
26952
+ ]);
26953
+ var DEFAULT_BUDGET_MS2 = 25000;
26954
+ var DEFAULT_POLL_INTERVAL_MS2 = 800;
26955
+ async function callRpc2(params) {
26956
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26957
+ if (!apiKey) {
26958
+ throw new Error("API key is required");
26959
+ }
26960
+ const base = normalizeBaseUrl(apiBaseUrl);
26961
+ const url = new URL(`${base}/rpc/${fn}`);
26962
+ const payload = JSON.stringify(body);
26963
+ const headers = {
26964
+ "access-token": apiKey,
26965
+ "Content-Type": "application/json",
26966
+ Connection: "close"
26967
+ };
26968
+ if (debug) {
26969
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26970
+ console.error(`Debug: POST URL: ${url.toString()}`);
26971
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26972
+ console.error(`Debug: Request body: ${payload}`);
26973
+ }
26974
+ const response = await fetch(url.toString(), {
26975
+ method: "POST",
26976
+ headers,
26977
+ body: payload
26978
+ });
26979
+ const text = await response.text();
26980
+ if (debug) {
26981
+ console.error(`Debug: Response status: ${response.status}`);
26982
+ console.error(`Debug: Response body: ${text}`);
26983
+ }
26984
+ if (!response.ok) {
26985
+ throw new Error(formatHttpError(operation, response.status, text));
26986
+ }
26987
+ try {
26988
+ return JSON.parse(text);
26989
+ } catch {
26990
+ throw new Error(`${operation}: failed to parse response: ${text}`);
26991
+ }
26992
+ }
26993
+ async function submitCommand2(params) {
26994
+ const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
26995
+ if (!JOE_COMMANDS2.includes(command)) {
26996
+ throw new Error(`Unknown Joe command: ${command}`);
26997
+ }
26998
+ const body = {
26999
+ project_id: projectId,
27000
+ command,
27001
+ sql: sql ?? null,
27002
+ args: args ?? null,
27003
+ session_id: sessionId ?? null
27004
+ };
27005
+ if (idempotencyKey) {
27006
+ body.idempotency_key = idempotencyKey;
27007
+ }
27008
+ return callRpc2({
27009
+ apiKey,
27010
+ apiBaseUrl,
27011
+ fn: "joe_command_submit",
27012
+ body,
27013
+ operation: `Failed to submit ${command} command`,
27014
+ debug
27015
+ });
27016
+ }
27017
+ async function getCommandStatus2(params) {
27018
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
27019
+ if (!commandId) {
27020
+ throw new Error("commandId is required");
27021
+ }
27022
+ return callRpc2({
27023
+ apiKey,
27024
+ apiBaseUrl,
27025
+ fn: "joe_command_status",
27026
+ body: { command_id: commandId },
27027
+ operation: "Failed to fetch command status",
27028
+ debug
27029
+ });
27030
+ }
27031
+ async function getCommandResult2(params) {
27032
+ const { apiKey, apiBaseUrl, commandId, debug } = params;
27033
+ if (!commandId) {
27034
+ throw new Error("commandId is required");
27035
+ }
27036
+ return callRpc2({
27037
+ apiKey,
27038
+ apiBaseUrl,
27039
+ fn: "joe_command_result",
27040
+ body: { command_id: commandId },
27041
+ operation: "Failed to fetch command result",
27042
+ debug
27043
+ });
27044
+ }
27045
+ function normalizeProjectRow2(row) {
27046
+ const projectId = row.project_id ?? row.id ?? 0;
27047
+ return {
27048
+ project_id: Number(projectId),
27049
+ alias: row.alias ?? null,
27050
+ name: row.name ?? null,
27051
+ label: row.label ?? null,
27052
+ joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
27053
+ tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
27054
+ instance_id: row.instance_id == null ? null : Number(row.instance_id),
27055
+ dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id)
27056
+ };
27057
+ }
27058
+ async function listProjects2(params) {
27059
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
27060
+ const body = {};
27061
+ if (typeof orgId === "number") {
27062
+ body.org_id = orgId;
27063
+ }
27064
+ const rows = await callRpc2({
27065
+ apiKey,
27066
+ apiBaseUrl,
27067
+ fn: "projects_list",
27068
+ body,
27069
+ operation: "Failed to list projects",
27070
+ debug
27071
+ });
27072
+ if (!Array.isArray(rows)) {
27073
+ return [];
27074
+ }
27075
+ return rows.map(normalizeProjectRow2);
27076
+ }
27077
+ function isNumericProjectRef3(ref) {
27078
+ return /^[0-9]+$/.test(ref.trim());
27079
+ }
27080
+ async function resolveProjectId2(params) {
27081
+ const ref = String(params.project ?? "").trim();
27082
+ if (!ref) {
27083
+ throw new Error("project is required (--project <id|alias>)");
27084
+ }
27085
+ if (isNumericProjectRef3(ref)) {
27086
+ return Number(ref);
27087
+ }
27088
+ const projects = await listProjects2({
27089
+ apiKey: params.apiKey,
27090
+ apiBaseUrl: params.apiBaseUrl,
27091
+ orgId: params.orgId,
27092
+ debug: params.debug
27093
+ });
27094
+ const needle = ref.toLowerCase();
27095
+ const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
27096
+ if (!match) {
27097
+ throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
27098
+ }
27099
+ return match.project_id;
27100
+ }
27101
+ function sessionStorePath2(dir) {
27102
+ return path5.join(dir ?? getConfigDir2(), "joe-sessions.json");
27103
+ }
27104
+ function readSessionStore2(dir) {
27105
+ const file = sessionStorePath2(dir);
27106
+ if (!fs5.existsSync(file)) {
27107
+ return {};
27108
+ }
27109
+ try {
27110
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
27111
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
27112
+ return parsed;
27113
+ }
27114
+ } catch {}
27115
+ return {};
27116
+ }
27117
+ function readStoredSessionId2(projectId, dir) {
27118
+ const store = readSessionStore2(dir);
27119
+ const value = store[String(projectId)];
27120
+ return typeof value === "string" && value.length > 0 ? value : null;
27121
+ }
27122
+ function writeStoredSessionId2(projectId, sessionId, dir) {
27123
+ const baseDir = dir ?? getConfigDir2();
27124
+ if (!fs5.existsSync(baseDir)) {
27125
+ fs5.mkdirSync(baseDir, { recursive: true, mode: 448 });
27126
+ }
27127
+ const store = readSessionStore2(dir);
27128
+ store[String(projectId)] = sessionId;
27129
+ fs5.writeFileSync(sessionStorePath2(dir), JSON.stringify(store, null, 2) + `
27130
+ `, { mode: 384 });
27131
+ }
27132
+ function clearStoredSessionId2(projectId, dir) {
27133
+ const file = sessionStorePath2(dir);
27134
+ if (!fs5.existsSync(file)) {
27135
+ return;
27136
+ }
27137
+ const store = readSessionStore2(dir);
27138
+ if (String(projectId) in store) {
27139
+ delete store[String(projectId)];
27140
+ fs5.writeFileSync(file, JSON.stringify(store, null, 2) + `
27141
+ `, { mode: 384 });
27142
+ }
27143
+ }
27144
+ function computeIdempotencyKey2(command, projectId, sql, args) {
27145
+ if (EXECUTING_COMMANDS2.has(command)) {
27146
+ const canonical = `${projectId}
27147
+ ${command}
27148
+ ${sql ?? ""}
27149
+ ${JSON.stringify(args ?? null)}`;
27150
+ return `joe-${crypto2.createHash("sha256").update(canonical).digest("hex")}`;
27151
+ }
27152
+ return `joe-${crypto2.randomUUID()}`;
27153
+ }
27154
+ var defaultSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
27155
+ async function runCommand2(params) {
27156
+ const {
27157
+ apiKey,
27158
+ apiBaseUrl,
27159
+ command,
27160
+ projectId,
27161
+ sql,
27162
+ args,
27163
+ sessionId,
27164
+ debug
27165
+ } = params;
27166
+ const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS2;
27167
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS2;
27168
+ const now = params.now ?? Date.now;
27169
+ const sleep = params.sleep ?? defaultSleep2;
27170
+ const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey2(command, projectId, sql, args);
27171
+ const submitted = await submitCommand2({
27172
+ apiKey,
27173
+ apiBaseUrl,
27174
+ command,
27175
+ projectId,
27176
+ sql,
27177
+ args,
27178
+ sessionId,
27179
+ idempotencyKey,
27180
+ debug
27181
+ });
27182
+ const commandId = submitted.command_id;
27183
+ const newSessionId = submitted.session_id ?? sessionId ?? null;
27184
+ const deadline = now() + budgetMs;
27185
+ let status = submitted.status;
27186
+ while (true) {
27187
+ const statusResp = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug });
27188
+ status = statusResp.status;
27189
+ if (TERMINAL_STATES2.has(status)) {
27190
+ break;
27191
+ }
27192
+ if (now() >= deadline) {
27193
+ return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
27194
+ }
27195
+ await sleep(pollIntervalMs);
27196
+ }
27197
+ const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug });
27198
+ return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
27199
+ }
27200
+ async function executeJoeCommand2(params) {
27201
+ const projectId = await resolveProjectId2({
27202
+ apiKey: params.apiKey,
27203
+ apiBaseUrl: params.apiBaseUrl,
27204
+ project: params.project,
27205
+ orgId: params.orgId,
27206
+ debug: params.debug
27207
+ });
27208
+ let sessionId;
27209
+ if (params.newSession) {
27210
+ clearStoredSessionId2(projectId, params.sessionDir);
27211
+ sessionId = null;
27212
+ } else if (params.session) {
27213
+ sessionId = String(params.session);
27214
+ } else {
27215
+ sessionId = readStoredSessionId2(projectId, params.sessionDir);
27216
+ }
27217
+ const outcome = await runCommand2({
27218
+ apiKey: params.apiKey,
27219
+ apiBaseUrl: params.apiBaseUrl,
27220
+ command: params.command,
27221
+ projectId,
27222
+ sql: params.sql,
27223
+ args: params.args,
27224
+ sessionId,
27225
+ budgetMs: params.budgetMs,
27226
+ pollIntervalMs: params.pollIntervalMs,
27227
+ debug: params.debug,
27228
+ now: params.now,
27229
+ sleep: params.sleep
27230
+ });
27231
+ if (outcome.sessionId) {
27232
+ writeStoredSessionId2(projectId, outcome.sessionId, params.sessionDir);
27233
+ }
27234
+ return { ...outcome, command: params.command, projectId };
27235
+ }
27236
+ function clientSidePlanFlags(planJson) {
27237
+ const flags = [];
27238
+ const walk = (node) => {
27239
+ if (!node || typeof node !== "object") {
27240
+ return;
27241
+ }
27242
+ const nodeType = node["Node Type"];
27243
+ if (nodeType === "Seq Scan") {
27244
+ const rel = node["Relation Name"];
27245
+ flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
27246
+ }
27247
+ if (Array.isArray(node.Plans)) {
27248
+ for (const child of node.Plans) {
27249
+ walk(child);
27250
+ }
27251
+ }
27252
+ };
27253
+ if (planJson && typeof planJson === "object") {
27254
+ const root = planJson;
27255
+ walk(root.Plan ?? planJson);
27256
+ }
27257
+ return flags;
27258
+ }
27259
+ function formatJoeResult(command, result) {
27260
+ const lines = [];
27261
+ switch (command) {
27262
+ case "plan":
27263
+ case "explain": {
27264
+ if (result.plan_text) {
27265
+ lines.push(result.plan_text);
27266
+ }
27267
+ for (const flag of clientSidePlanFlags(result.plan_json)) {
27268
+ lines.push(`⚑ ${flag}`);
27269
+ }
27270
+ if (result.queryid || result.plan_fingerprint) {
27271
+ lines.push(`(queryid ${result.queryid ?? "?"} · plan_fp ${result.plan_fingerprint ?? "?"})`);
27272
+ }
27273
+ break;
27274
+ }
27275
+ case "exec": {
27276
+ const notices = result.notices ?? [];
27277
+ const rowCount = result.row_count ?? 0;
27278
+ lines.push(`${notices.join(" ") || "OK"} · ${rowCount} row${rowCount === 1 ? "" : "s"}`);
27279
+ if (result.result_rows && result.result_rows.length > 0) {
27280
+ lines.push(JSON.stringify(result.result_rows, null, 2));
27281
+ }
27282
+ break;
27283
+ }
27284
+ case "hypo": {
27285
+ lines.push(result.hypo_used ? "hypothetical index would be used" : "hypothetical index would NOT be used");
27286
+ if (result.hypo_plan !== undefined) {
27287
+ lines.push(JSON.stringify(result.hypo_plan, null, 2));
27288
+ }
27289
+ break;
27290
+ }
27291
+ case "activity":
27292
+ case "describe": {
27293
+ lines.push(JSON.stringify(result.snapshot ?? null, null, 2));
27294
+ break;
27295
+ }
27296
+ case "terminate": {
27297
+ lines.push(`terminated ${result.terminated ? "yes" : "no"} · pid ${result.pid ?? "?"}`);
27298
+ break;
27299
+ }
27300
+ case "reset": {
27301
+ lines.push(`reset ${result.reset ? "ok" : "no"}`);
27302
+ break;
27303
+ }
27304
+ }
27305
+ return lines.join(`
27306
+ `);
27307
+ }
27308
+ function formatProjectsTable(projects) {
27309
+ const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
27310
+ const rows = projects.map((p) => [
27311
+ String(p.project_id),
27312
+ p.alias ?? "-",
27313
+ p.name ?? "-",
27314
+ p.joe_ready ? "ready" : "no",
27315
+ p.tunnel ? "yes" : "no"
27316
+ ]);
27317
+ const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
27318
+ const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
27319
+ return [pad(header), ...rows.map(pad)].join(`
27320
+ `);
27321
+ }
27322
+
27323
+ // lib/dblab.ts
27324
+ function isNumericProjectRef4(ref) {
27325
+ return /^[0-9]+$/.test(ref.trim());
27326
+ }
27327
+ async function resolveDblabInstanceId2(params) {
27328
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
27329
+ if (!apiKey) {
27330
+ throw new Error("API key is required");
27331
+ }
27332
+ const ref = String(params.project ?? "").trim();
27333
+ if (!ref) {
27334
+ throw new Error("project is required (--project <id|alias>)");
27335
+ }
27336
+ let projects;
27337
+ try {
27338
+ projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
27339
+ } catch (err) {
27340
+ const message = err instanceof Error ? err.message : String(err);
27341
+ throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
27342
+ }
27343
+ const numeric = isNumericProjectRef4(ref);
27344
+ const needle = ref.toLowerCase();
27345
+ const match = projects.find((p) => {
27346
+ if (numeric) {
27347
+ return String(p.project_id) === ref;
27348
+ }
27349
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
27350
+ });
27351
+ if (!match) {
27352
+ throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
27353
+ }
27354
+ if (match.dblab_instance_id == null) {
27355
+ throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
27356
+ }
27357
+ return String(match.dblab_instance_id);
27358
+ }
27359
+ async function callDblabApi2(params) {
27360
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
27361
+ if (!apiKey) {
27362
+ throw new Error("API key is required");
27363
+ }
27364
+ if (!instanceId) {
27365
+ throw new Error("instanceId is required");
27366
+ }
27367
+ const base = normalizeBaseUrl(apiBaseUrl);
27368
+ const url = new URL(`${base}/rpc/dblab_api_call`);
27369
+ const bodyObj = {
27370
+ instance_id: instanceId,
27371
+ action,
27372
+ method
27373
+ };
27374
+ if (data !== undefined) {
27375
+ bodyObj.data = data;
27376
+ }
27377
+ const body = JSON.stringify(bodyObj);
27378
+ const headers = {
27379
+ "access-token": apiKey,
27380
+ "Content-Type": "application/json",
27381
+ Connection: "close"
27382
+ };
27383
+ if (debug) {
27384
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
27385
+ console.error(`Debug: POST URL: ${url.toString()}`);
27386
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27387
+ console.error(`Debug: Request body: ${body}`);
27388
+ }
27389
+ const response = await fetch(url.toString(), { method: "POST", headers, body });
27390
+ const text = await response.text();
27391
+ if (debug) {
27392
+ console.error(`Debug: Response status: ${response.status}`);
27393
+ console.error(`Debug: Response body: ${text}`);
27394
+ }
27395
+ if (!response.ok) {
27396
+ throw new Error(formatHttpError(operation, response.status, text));
27397
+ }
27398
+ if (text.trim() === "") {
27399
+ return null;
27400
+ }
27401
+ try {
27402
+ return JSON.parse(text);
27403
+ } catch {
27404
+ throw new Error(`${operation}: failed to parse response: ${text}`);
27405
+ }
27406
+ }
27407
+ async function createClone2(params) {
27408
+ const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
27409
+ const data = { protected: Boolean(isProtected) };
27410
+ if (cloneId)
27411
+ data.id = cloneId;
27412
+ if (branch)
27413
+ data.branch = branch;
27414
+ if (snapshotId)
27415
+ data.snapshot = { id: snapshotId };
27416
+ if (dbUser && dbPassword)
27417
+ data.db = { username: dbUser, password: dbPassword };
27418
+ return callDblabApi2({
27419
+ apiKey,
27420
+ apiBaseUrl,
27421
+ instanceId,
27422
+ action: "/clone",
27423
+ method: "post",
27424
+ data,
27425
+ operation: "Failed to create clone",
27426
+ debug
27427
+ });
27428
+ }
27429
+ async function listClones2(params) {
27430
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
27431
+ return callDblabApi2({
27432
+ apiKey,
27433
+ apiBaseUrl,
27434
+ instanceId,
27435
+ action: "/clones",
27436
+ method: "get",
27437
+ operation: "Failed to list clones",
27438
+ debug
27439
+ });
27440
+ }
27441
+ async function getClone2(params) {
27442
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
27443
+ if (!cloneId)
27444
+ throw new Error("cloneId is required");
27445
+ return callDblabApi2({
27446
+ apiKey,
27447
+ apiBaseUrl,
27448
+ instanceId,
27449
+ action: `/clone/${encodeURIComponent(cloneId)}`,
27450
+ method: "get",
27451
+ operation: "Failed to get clone",
27452
+ debug
27453
+ });
27454
+ }
27455
+ async function resetClone2(params) {
27456
+ const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
27457
+ if (!cloneId)
27458
+ throw new Error("cloneId is required");
27459
+ const data = { latest: latest ?? !snapshotId };
27460
+ if (snapshotId)
27461
+ data.snapshotID = snapshotId;
27462
+ return callDblabApi2({
27463
+ apiKey,
27464
+ apiBaseUrl,
27465
+ instanceId,
27466
+ action: `/clone/${encodeURIComponent(cloneId)}/reset`,
27467
+ method: "post",
27468
+ data,
27469
+ operation: "Failed to reset clone",
27470
+ debug
27471
+ });
27472
+ }
27473
+ async function destroyClone2(params) {
27474
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
27475
+ if (!cloneId)
27476
+ throw new Error("cloneId is required");
27477
+ return callDblabApi2({
27478
+ apiKey,
27479
+ apiBaseUrl,
27480
+ instanceId,
27481
+ action: `/clone/${encodeURIComponent(cloneId)}`,
27482
+ method: "delete",
27483
+ operation: "Failed to destroy clone",
27484
+ debug
27485
+ });
27486
+ }
27487
+ async function listBranches2(params) {
27488
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
27489
+ return callDblabApi2({
27490
+ apiKey,
27491
+ apiBaseUrl,
27492
+ instanceId,
27493
+ action: "/branches",
27494
+ method: "get",
27495
+ operation: "Failed to list branches",
27496
+ debug
27497
+ });
27498
+ }
27499
+ async function createBranch2(params) {
27500
+ const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
27501
+ if (!branchName)
27502
+ throw new Error("branchName is required");
27503
+ const data = { branchName };
27504
+ if (baseBranch)
27505
+ data.baseBranch = baseBranch;
27506
+ if (snapshotId)
27507
+ data.snapshotID = snapshotId;
27508
+ return callDblabApi2({
27509
+ apiKey,
27510
+ apiBaseUrl,
27511
+ instanceId,
27512
+ action: "/branch",
27513
+ method: "post",
27514
+ data,
27515
+ operation: "Failed to create branch",
27516
+ debug
27517
+ });
27518
+ }
27519
+ async function deleteBranch2(params) {
27520
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
27521
+ if (!branchName)
27522
+ throw new Error("branchName is required");
27523
+ return callDblabApi2({
27524
+ apiKey,
27525
+ apiBaseUrl,
27526
+ instanceId,
27527
+ action: `/branch/${encodeURIComponent(branchName)}`,
27528
+ method: "delete",
27529
+ operation: "Failed to delete branch",
27530
+ debug
27531
+ });
27532
+ }
27533
+ async function branchLog2(params) {
27534
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
27535
+ if (!branchName)
27536
+ throw new Error("branchName is required");
27537
+ return callDblabApi2({
27538
+ apiKey,
27539
+ apiBaseUrl,
27540
+ instanceId,
27541
+ action: `/branch/${encodeURIComponent(branchName)}/log`,
27542
+ method: "get",
27543
+ operation: "Failed to fetch branch log",
27544
+ debug
27545
+ });
27546
+ }
27547
+ async function listSnapshots2(params) {
27548
+ const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
27549
+ const qs = new URLSearchParams;
27550
+ const branch = branchName?.trim();
27551
+ if (branch)
27552
+ qs.append("branch", branch);
27553
+ if (dataset)
27554
+ qs.append("dataset", dataset);
27555
+ const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
27556
+ return callDblabApi2({
27557
+ apiKey,
27558
+ apiBaseUrl,
27559
+ instanceId,
27560
+ action,
27561
+ method: "get",
27562
+ operation: "Failed to list snapshots",
27563
+ debug
27564
+ });
27565
+ }
27566
+ async function createSnapshot2(params) {
27567
+ const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
27568
+ if (!cloneId)
27569
+ throw new Error("cloneId is required");
27570
+ const data = { cloneID: cloneId };
27571
+ if (message)
27572
+ data.message = message;
27573
+ return callDblabApi2({
27574
+ apiKey,
27575
+ apiBaseUrl,
27576
+ instanceId,
27577
+ action: "/branch/snapshot",
27578
+ method: "post",
27579
+ data,
27580
+ operation: "Failed to create snapshot",
27581
+ debug
27582
+ });
27583
+ }
27584
+ async function destroySnapshot2(params) {
27585
+ const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
27586
+ if (!snapshotId)
27587
+ throw new Error("snapshotId is required");
27588
+ const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
27589
+ return callDblabApi2({
27590
+ apiKey,
27591
+ apiBaseUrl,
27592
+ instanceId,
27593
+ action,
27594
+ method: "delete",
27595
+ operation: "Failed to destroy snapshot",
27596
+ debug
27597
+ });
27598
+ }
27599
+
25804
27600
  // bin/postgres-ai.ts
25805
27601
  init_util();
25806
27602
 
25807
27603
  // lib/instances.ts
25808
- import * as fs4 from "fs";
27604
+ import * as fs6 from "fs";
25809
27605
 
25810
27606
  // node_modules/js-yaml/dist/js-yaml.mjs
25811
27607
  /*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
@@ -28470,11 +30266,11 @@ class InstancesParseError extends Error {
28470
30266
  }
28471
30267
  }
28472
30268
  function loadInstances(file) {
28473
- if (!fs4.existsSync(file))
30269
+ if (!fs6.existsSync(file))
28474
30270
  return [];
28475
- if (fs4.lstatSync(file).isDirectory())
30271
+ if (fs6.lstatSync(file).isDirectory())
28476
30272
  return [];
28477
- const text = fs4.readFileSync(file, "utf8");
30273
+ const text = fs6.readFileSync(file, "utf8");
28478
30274
  if (text.trim() === "")
28479
30275
  return [];
28480
30276
  let parsed;
@@ -28652,8 +30448,8 @@ async function registerAasCollection(apiKey, instanceId, opts) {
28652
30448
  }
28653
30449
 
28654
30450
  // lib/storage.ts
28655
- import * as fs5 from "fs";
28656
- import * as path4 from "path";
30451
+ import * as fs7 from "fs";
30452
+ import * as path6 from "path";
28657
30453
  var MAX_UPLOAD_SIZE2 = 500 * 1024 * 1024;
28658
30454
  var MAX_DOWNLOAD_SIZE2 = 500 * 1024 * 1024;
28659
30455
  var MIME_TYPES2 = {
@@ -28694,11 +30490,11 @@ async function uploadFile2(params) {
28694
30490
  if (!filePath) {
28695
30491
  throw new Error("filePath is required");
28696
30492
  }
28697
- const resolvedPath = path4.resolve(filePath);
28698
- if (!fs5.existsSync(resolvedPath)) {
30493
+ const resolvedPath = path6.resolve(filePath);
30494
+ if (!fs7.existsSync(resolvedPath)) {
28699
30495
  throw new Error(`File not found: ${resolvedPath}`);
28700
30496
  }
28701
- const stat = fs5.statSync(resolvedPath);
30497
+ const stat = fs7.statSync(resolvedPath);
28702
30498
  if (!stat.isFile()) {
28703
30499
  throw new Error(`Not a file: ${resolvedPath}`);
28704
30500
  }
@@ -28710,9 +30506,9 @@ async function uploadFile2(params) {
28710
30506
  console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
28711
30507
  }
28712
30508
  const url = `${base}/upload`;
28713
- const fileBuffer = fs5.readFileSync(resolvedPath);
28714
- const fileName = path4.basename(resolvedPath);
28715
- const ext = path4.extname(fileName).toLowerCase();
30509
+ const fileBuffer = fs7.readFileSync(resolvedPath);
30510
+ const fileName = path6.basename(resolvedPath);
30511
+ const ext = path6.extname(fileName).toLowerCase();
28716
30512
  const mimeType = MIME_TYPES2[ext] || "application/octet-stream";
28717
30513
  const formData = new FormData;
28718
30514
  formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
@@ -28771,15 +30567,15 @@ async function downloadFile2(params) {
28771
30567
  const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
28772
30568
  fullUrl = `${base}${relativePath}`;
28773
30569
  }
28774
- const urlFilename = path4.basename(new URL(fullUrl).pathname);
30570
+ const urlFilename = path6.basename(new URL(fullUrl).pathname);
28775
30571
  if (!urlFilename) {
28776
30572
  throw new Error("Cannot derive filename from URL; please specify --output");
28777
30573
  }
28778
- const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
30574
+ const saveTo = outputPath ? path6.resolve(outputPath) : path6.resolve(urlFilename);
28779
30575
  if (!outputPath) {
28780
- const normalizedSave = path4.normalize(saveTo);
28781
- const cwd = path4.normalize(process.cwd());
28782
- if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
30576
+ const normalizedSave = path6.normalize(saveTo);
30577
+ const cwd = path6.normalize(process.cwd());
30578
+ if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path6.sep)) {
28783
30579
  throw new Error("Derived output path escapes current directory; please specify --output");
28784
30580
  }
28785
30581
  }
@@ -28811,11 +30607,11 @@ async function downloadFile2(params) {
28811
30607
  }
28812
30608
  const arrayBuffer = await response.arrayBuffer();
28813
30609
  const buffer = Buffer.from(arrayBuffer);
28814
- const parentDir = path4.dirname(saveTo);
28815
- if (!fs5.existsSync(parentDir)) {
28816
- fs5.mkdirSync(parentDir, { recursive: true });
30610
+ const parentDir = path6.dirname(saveTo);
30611
+ if (!fs7.existsSync(parentDir)) {
30612
+ fs7.mkdirSync(parentDir, { recursive: true });
28817
30613
  }
28818
- fs5.writeFileSync(saveTo, buffer);
30614
+ fs7.writeFileSync(saveTo, buffer);
28819
30615
  return {
28820
30616
  savedTo: saveTo,
28821
30617
  size: buffer.length,
@@ -28827,9 +30623,9 @@ function buildMarkdownLink2(fileUrl, storageBaseUrl, filename) {
28827
30623
  const base = normalizeBaseUrl(storageBaseUrl);
28828
30624
  const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
28829
30625
  const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
28830
- const name = filename || path4.basename(new URL(fullUrl).pathname);
30626
+ const name = filename || path6.basename(new URL(fullUrl).pathname);
28831
30627
  const safeName = name.replace(/[\[\]()]/g, "\\$&");
28832
- const ext = path4.extname(name).toLowerCase();
30628
+ const ext = path6.extname(name).toLowerCase();
28833
30629
  if (IMAGE_EXTENSIONS2.has(ext)) {
28834
30630
  return `![${safeName}](${fullUrl})`;
28835
30631
  }
@@ -28875,8 +30671,8 @@ ${links}`;
28875
30671
  // lib/init.ts
28876
30672
  import { randomBytes } from "crypto";
28877
30673
  import { URL as URL2, fileURLToPath } from "url";
28878
- import * as fs6 from "fs";
28879
- import * as path5 from "path";
30674
+ import * as fs8 from "fs";
30675
+ import * as path7 from "path";
28880
30676
  var DEFAULT_MONITORING_USER = "postgres_ai_mon";
28881
30677
  var KNOWN_PROVIDERS = ["self-managed", "supabase"];
28882
30678
  var SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
@@ -28968,21 +30764,21 @@ async function connectWithSslFallback(ClientClass, adminConn, verbose) {
28968
30764
  }
28969
30765
  function sqlDir() {
28970
30766
  const currentFile = fileURLToPath(import.meta.url);
28971
- const currentDir = path5.dirname(currentFile);
30767
+ const currentDir = path7.dirname(currentFile);
28972
30768
  const candidates = [
28973
- path5.resolve(currentDir, "..", "sql"),
28974
- path5.resolve(currentDir, "..", "..", "sql")
30769
+ path7.resolve(currentDir, "..", "sql"),
30770
+ path7.resolve(currentDir, "..", "..", "sql")
28975
30771
  ];
28976
30772
  for (const candidate of candidates) {
28977
- if (fs6.existsSync(candidate)) {
30773
+ if (fs8.existsSync(candidate)) {
28978
30774
  return candidate;
28979
30775
  }
28980
30776
  }
28981
30777
  throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
28982
30778
  }
28983
30779
  function loadSqlTemplate(filename) {
28984
- const p = path5.join(sqlDir(), filename);
28985
- return fs6.readFileSync(p, "utf8");
30780
+ const p = path7.join(sqlDir(), filename);
30781
+ return fs8.readFileSync(p, "utf8");
28986
30782
  }
28987
30783
  function applyTemplate(sql, vars) {
28988
30784
  return sql.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
@@ -29452,10 +31248,6 @@ async function verifyInitSetup(params) {
29452
31248
  }
29453
31249
  }
29454
31250
  }
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
31251
  const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
29460
31252
  if (!tableDescribeFnRes.rows?.[0]?.ok) {
29461
31253
  missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
@@ -29974,15 +31766,6 @@ async function verifyInitSetupViaSupabase(params) {
29974
31766
  missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
29975
31767
  }
29976
31768
  }
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
31769
  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
31770
  if (tableDescribeFnExistsRes.rowCount === 0) {
29988
31771
  missingRequired.push("function postgres_ai.table_describe exists");
@@ -30040,9 +31823,9 @@ function escapeLiteral2(value) {
30040
31823
  }
30041
31824
 
30042
31825
  // lib/pkce.ts
30043
- import * as crypto from "crypto";
31826
+ import * as crypto3 from "crypto";
30044
31827
  function generateRandomString(length = 64) {
30045
- const bytes = crypto.randomBytes(length);
31828
+ const bytes = crypto3.randomBytes(length);
30046
31829
  return base64URLEncode(bytes);
30047
31830
  }
30048
31831
  function base64URLEncode(buffer) {
@@ -30052,7 +31835,7 @@ function generateCodeVerifier() {
30052
31835
  return generateRandomString(32);
30053
31836
  }
30054
31837
  function generateCodeChallenge(verifier) {
30055
- const hash = crypto.createHash("sha256").update(verifier).digest();
31838
+ const hash = crypto3.createHash("sha256").update(verifier).digest();
30056
31839
  return base64URLEncode(hash);
30057
31840
  }
30058
31841
  function generateState() {
@@ -30281,8 +32064,8 @@ import { createInterface } from "readline";
30281
32064
  import * as childProcess from "child_process";
30282
32065
 
30283
32066
  // lib/checkup.ts
30284
- import * as fs7 from "fs";
30285
- import * as path6 from "path";
32067
+ import * as fs9 from "fs";
32068
+ import * as path8 from "path";
30286
32069
 
30287
32070
  // lib/metrics-embedded.ts
30288
32071
  var METRICS = {
@@ -32013,7 +33796,7 @@ function buildCheckInfoMap() {
32013
33796
  }
32014
33797
 
32015
33798
  // lib/checkup.ts
32016
- var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
33799
+ var __dirname = "/home/agent/workspace/postgresai-joe-v2/cli/lib";
32017
33800
  var SECONDS_PER_DAY = 86400;
32018
33801
  var SECONDS_PER_HOUR = 3600;
32019
33802
  var SECONDS_PER_MINUTE = 60;
@@ -32499,7 +34282,7 @@ function createBaseReport(checkId, checkTitle, nodeName) {
32499
34282
  }
32500
34283
  function readTextFileSafe(p) {
32501
34284
  try {
32502
- const value = fs7.readFileSync(p, "utf8").trim();
34285
+ const value = fs9.readFileSync(p, "utf8").trim();
32503
34286
  return value || null;
32504
34287
  } catch {
32505
34288
  return null;
@@ -32512,8 +34295,8 @@ function resolveBuildTs() {
32512
34295
  if (fromFile)
32513
34296
  return fromFile;
32514
34297
  try {
32515
- const pkgRoot = path6.resolve(__dirname, "..");
32516
- const fromPkgFile = readTextFileSafe(path6.join(pkgRoot, "BUILD_TS"));
34298
+ const pkgRoot = path8.resolve(__dirname, "..");
34299
+ const fromPkgFile = readTextFileSafe(path8.join(pkgRoot, "BUILD_TS"));
32517
34300
  if (fromPkgFile)
32518
34301
  return fromPkgFile;
32519
34302
  } catch (err) {
@@ -32521,8 +34304,8 @@ function resolveBuildTs() {
32521
34304
  console.warn(`[resolveBuildTs] Warning: path resolution failed: ${errorMsg}`);
32522
34305
  }
32523
34306
  try {
32524
- const pkgJsonPath = path6.resolve(__dirname, "..", "package.json");
32525
- const st = fs7.statSync(pkgJsonPath);
34307
+ const pkgJsonPath = path8.resolve(__dirname, "..", "package.json");
34308
+ const st = fs9.statSync(pkgJsonPath);
32526
34309
  return st.mtime.toISOString();
32527
34310
  } catch (err) {
32528
34311
  if (process.env.DEBUG) {
@@ -33803,7 +35586,7 @@ function summarizeG003(nodeData) {
33803
35586
  }
33804
35587
 
33805
35588
  // lib/instances.ts
33806
- import * as fs8 from "fs";
35589
+ import * as fs10 from "fs";
33807
35590
  class InstancesParseError2 extends Error {
33808
35591
  constructor(file, cause) {
33809
35592
  const causeMsg = cause instanceof Error ? cause.message : String(cause);
@@ -33812,11 +35595,11 @@ class InstancesParseError2 extends Error {
33812
35595
  }
33813
35596
  }
33814
35597
  function loadInstances2(file) {
33815
- if (!fs8.existsSync(file))
35598
+ if (!fs10.existsSync(file))
33816
35599
  return [];
33817
- if (fs8.lstatSync(file).isDirectory())
35600
+ if (fs10.lstatSync(file).isDirectory())
33818
35601
  return [];
33819
- const text = fs8.readFileSync(file, "utf8");
35602
+ const text = fs10.readFileSync(file, "utf8");
33820
35603
  if (text.trim() === "")
33821
35604
  return [];
33822
35605
  let parsed;
@@ -33849,22 +35632,22 @@ function buildInstance(name, connStr) {
33849
35632
  };
33850
35633
  }
33851
35634
  function addInstanceToFile(file, instance) {
33852
- if (fs8.existsSync(file) && fs8.lstatSync(file).isDirectory()) {
33853
- fs8.rmSync(file, { recursive: true, force: true });
35635
+ if (fs10.existsSync(file) && fs10.lstatSync(file).isDirectory()) {
35636
+ fs10.rmSync(file, { recursive: true, force: true });
33854
35637
  }
33855
35638
  const existing = loadInstances2(file);
33856
35639
  if (existing.some((i3) => i3.name === instance.name)) {
33857
35640
  throw new Error(`Monitoring target '${instance.name}' already exists`);
33858
35641
  }
33859
35642
  existing.push(instance);
33860
- fs8.writeFileSync(file, dump2(existing), "utf8");
35643
+ fs10.writeFileSync(file, dump2(existing), "utf8");
33861
35644
  }
33862
35645
  function removeInstanceFromFile(file, name) {
33863
35646
  const instances = loadInstances2(file);
33864
35647
  const filtered = instances.filter((i3) => i3.name !== name);
33865
35648
  if (filtered.length === instances.length)
33866
35649
  return false;
33867
- fs8.writeFileSync(file, dump2(filtered), "utf8");
35650
+ fs10.writeFileSync(file, dump2(filtered), "utf8");
33868
35651
  return true;
33869
35652
  }
33870
35653
  function extractSslmode(connStr) {
@@ -33959,13 +35742,13 @@ function stripMatchingQuotes(value) {
33959
35742
  return trimmed;
33960
35743
  }
33961
35744
  var REQUIRED_ENV_KEYS = [
33962
- { key: "REPLICATOR_PASSWORD", defaultValue: () => crypto2.randomBytes(32).toString("hex"), introducedIn: "0.13" },
35745
+ { key: "REPLICATOR_PASSWORD", defaultValue: () => crypto4.randomBytes(32).toString("hex"), introducedIn: "0.13" },
33963
35746
  { key: "VM_AUTH_USERNAME", defaultValue: () => "vmauth", introducedIn: "0.15" },
33964
- { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto2.randomBytes(18).toString("base64"), introducedIn: "0.15" }
35747
+ { key: "VM_AUTH_PASSWORD", defaultValue: () => crypto4.randomBytes(18).toString("base64"), introducedIn: "0.15" }
33965
35748
  ];
33966
35749
  function ensureRequiredEnvVars(projectDir) {
33967
- const envFile = path7.resolve(projectDir, ".env");
33968
- const existing = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
35750
+ const envFile = path9.resolve(projectDir, ".env");
35751
+ const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
33969
35752
  const added = [];
33970
35753
  const appendLines = [];
33971
35754
  for (const spec of REQUIRED_ENV_KEYS) {
@@ -33984,7 +35767,7 @@ function ensureRequiredEnvVars(projectDir) {
33984
35767
  ` : "") + appendLines.join(`
33985
35768
  `) + `
33986
35769
  `;
33987
- fs9.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
35770
+ fs11.writeFileSync(envFile, newContent, { encoding: "utf8", mode: 384 });
33988
35771
  return added;
33989
35772
  }
33990
35773
  async function execFilePromise(file, args) {
@@ -34049,7 +35832,7 @@ function expandHomePath(p) {
34049
35832
  if (s === "~")
34050
35833
  return os3.homedir();
34051
35834
  if (s.startsWith("~/") || s.startsWith("~\\")) {
34052
- return path7.join(os3.homedir(), s.slice(2));
35835
+ return path9.join(os3.homedir(), s.slice(2));
34053
35836
  }
34054
35837
  return s;
34055
35838
  }
@@ -34098,10 +35881,10 @@ function prepareOutputDirectory(outputOpt) {
34098
35881
  if (!outputOpt)
34099
35882
  return;
34100
35883
  const outputDir = expandHomePath(outputOpt);
34101
- const outputPath = path7.isAbsolute(outputDir) ? outputDir : path7.resolve(process.cwd(), outputDir);
34102
- if (!fs9.existsSync(outputPath)) {
35884
+ const outputPath = path9.isAbsolute(outputDir) ? outputDir : path9.resolve(process.cwd(), outputDir);
35885
+ if (!fs11.existsSync(outputPath)) {
34103
35886
  try {
34104
- fs9.mkdirSync(outputPath, { recursive: true });
35887
+ fs11.mkdirSync(outputPath, { recursive: true });
34105
35888
  } catch (e) {
34106
35889
  const errAny = e;
34107
35890
  const code = typeof errAny?.code === "string" ? errAny.code : "";
@@ -34137,7 +35920,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
34137
35920
  let project = (opts.project || cfg.defaultProject || "").trim();
34138
35921
  let projectWasGenerated = false;
34139
35922
  if (!project) {
34140
- project = `project_${crypto2.randomBytes(6).toString("hex")}`;
35923
+ project = `project_${crypto4.randomBytes(6).toString("hex")}`;
34141
35924
  projectWasGenerated = true;
34142
35925
  try {
34143
35926
  writeConfig({ defaultProject: project });
@@ -34183,8 +35966,8 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
34183
35966
  }
34184
35967
  function writeReportFiles(reports, outputPath) {
34185
35968
  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");
35969
+ const filePath = path9.join(outputPath, `${checkId}.json`);
35970
+ fs11.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
34188
35971
  const title = report.checkTitle || checkId;
34189
35972
  console.log(`\u2713 ${checkId} ${title}: ${filePath}`);
34190
35973
  }
@@ -34229,7 +36012,7 @@ function getDefaultMonitoringProjectDir() {
34229
36012
  const override = process.env.PGAI_PROJECT_DIR;
34230
36013
  if (override && override.trim())
34231
36014
  return override.trim();
34232
- return path7.join(getConfigDir(), "monitoring");
36015
+ return path9.join(getConfigDir(), "monitoring");
34233
36016
  }
34234
36017
  async function downloadText(url) {
34235
36018
  const controller = new AbortController;
@@ -34246,12 +36029,12 @@ async function downloadText(url) {
34246
36029
  }
34247
36030
  async function ensureDefaultMonitoringProject() {
34248
36031
  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 });
36032
+ const composeFile = path9.resolve(projectDir, "docker-compose.yml");
36033
+ const instancesFile = path9.resolve(projectDir, "instances.yml");
36034
+ if (!fs11.existsSync(projectDir)) {
36035
+ fs11.mkdirSync(projectDir, { recursive: true, mode: 448 });
34253
36036
  }
34254
- if (!fs9.existsSync(composeFile)) {
36037
+ if (!fs11.existsSync(composeFile)) {
34255
36038
  const refs = [
34256
36039
  process.env.PGAI_PROJECT_REF,
34257
36040
  package_default.version,
@@ -34263,39 +36046,39 @@ async function ensureDefaultMonitoringProject() {
34263
36046
  const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
34264
36047
  try {
34265
36048
  const text = await downloadText(url);
34266
- fs9.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
36049
+ fs11.writeFileSync(composeFile, text, { encoding: "utf8", mode: 384 });
34267
36050
  break;
34268
36051
  } catch (err) {
34269
36052
  lastErr = err;
34270
36053
  }
34271
36054
  }
34272
- if (!fs9.existsSync(composeFile)) {
36055
+ if (!fs11.existsSync(composeFile)) {
34273
36056
  const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
34274
36057
  throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
34275
36058
  }
34276
36059
  }
34277
- if (fs9.existsSync(instancesFile) && fs9.lstatSync(instancesFile).isDirectory()) {
34278
- fs9.rmSync(instancesFile, { recursive: true, force: true });
36060
+ if (fs11.existsSync(instancesFile) && fs11.lstatSync(instancesFile).isDirectory()) {
36061
+ fs11.rmSync(instancesFile, { recursive: true, force: true });
34279
36062
  }
34280
- if (!fs9.existsSync(instancesFile)) {
36063
+ if (!fs11.existsSync(instancesFile)) {
34281
36064
  const header = `# PostgreSQL instances to monitor
34282
36065
  ` + `# Add your instances using: pgai mon targets add <connection-string> <name>
34283
36066
 
34284
36067
  `;
34285
- fs9.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
36068
+ fs11.writeFileSync(instancesFile, header, { encoding: "utf8", mode: 384 });
34286
36069
  }
34287
- const pgwatchConfig = path7.resolve(projectDir, ".pgwatch-config");
34288
- if (!fs9.existsSync(pgwatchConfig)) {
34289
- fs9.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
36070
+ const pgwatchConfig = path9.resolve(projectDir, ".pgwatch-config");
36071
+ if (!fs11.existsSync(pgwatchConfig)) {
36072
+ fs11.writeFileSync(pgwatchConfig, "", { encoding: "utf8", mode: 384 });
34290
36073
  }
34291
- const envFile = path7.resolve(projectDir, ".env");
34292
- if (!fs9.existsSync(envFile)) {
36074
+ const envFile = path9.resolve(projectDir, ".env");
36075
+ if (!fs11.existsSync(envFile)) {
34293
36076
  const envText = `PGAI_TAG=${package_default.version}
34294
36077
  # PGAI_REGISTRY=registry.gitlab.com/postgres-ai/postgres_ai
34295
36078
  `;
34296
- fs9.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
36079
+ fs11.writeFileSync(envFile, envText, { encoding: "utf8", mode: 384 });
34297
36080
  }
34298
- return { fs: fs9, path: path7, projectDir, composeFile, instancesFile };
36081
+ return { fs: fs11, path: path9, projectDir, composeFile, instancesFile };
34299
36082
  }
34300
36083
  function sanitizeTagForBackup(tag) {
34301
36084
  if (tag == null)
@@ -34304,10 +36087,10 @@ function sanitizeTagForBackup(tag) {
34304
36087
  return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
34305
36088
  }
34306
36089
  function readDeployedTag(projectDir) {
34307
- const envFile = path7.resolve(projectDir, ".env");
34308
- if (!fs9.existsSync(envFile))
36090
+ const envFile = path9.resolve(projectDir, ".env");
36091
+ if (!fs11.existsSync(envFile))
34309
36092
  return null;
34310
- const m = fs9.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
36093
+ const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
34311
36094
  return m ? sanitizeTagForBackup(m[1]) : null;
34312
36095
  }
34313
36096
  function isValidComposeYaml(text) {
@@ -34344,10 +36127,10 @@ function composeContentEqual(a, b) {
34344
36127
  return a.replace(/\s+$/, "") === b.replace(/\s+$/, "");
34345
36128
  }
34346
36129
  async function refreshBundledComposeIfStale(projectDir, oldTag) {
34347
- if (fs9.existsSync(path7.resolve(projectDir, ".git")))
36130
+ if (fs11.existsSync(path9.resolve(projectDir, ".git")))
34348
36131
  return false;
34349
- const composeFile = path7.resolve(projectDir, "docker-compose.yml");
34350
- if (!fs9.existsSync(composeFile))
36132
+ const composeFile = path9.resolve(projectDir, "docker-compose.yml");
36133
+ if (!fs11.existsSync(composeFile))
34351
36134
  return false;
34352
36135
  const refs = [
34353
36136
  process.env.PGAI_PROJECT_REF,
@@ -34360,24 +36143,24 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
34360
36143
  console.error(" Keeping the existing compose. If dashboards are blank after upgrade, re-run this command once network is available.");
34361
36144
  return false;
34362
36145
  }
34363
- const existing = fs9.readFileSync(composeFile, "utf8");
36146
+ const existing = fs11.readFileSync(composeFile, "utf8");
34364
36147
  if (composeContentEqual(existing, fetched))
34365
36148
  return false;
34366
36149
  const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
34367
36150
  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}`);
36151
+ const oldHash = crypto4.createHash("sha256").update(existing).digest("hex").slice(0, 8);
36152
+ const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
34370
36153
  let backupName = null;
34371
36154
  try {
34372
- fs9.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
34373
- backupName = path7.basename(backup);
36155
+ fs11.writeFileSync(backup, existing, { encoding: "utf8", mode: 384, flag: "wx" });
36156
+ backupName = path9.basename(backup);
34374
36157
  } catch (err) {
34375
36158
  const e = err;
34376
36159
  if (e && e.code === "EEXIST") {
34377
- backupName = path7.basename(backup);
36160
+ backupName = path9.basename(backup);
34378
36161
  }
34379
36162
  }
34380
- fs9.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
36163
+ fs11.writeFileSync(composeFile, fetched, { encoding: "utf8", mode: 384 });
34381
36164
  const backupNote = backupName ? ` (backup: ${backupName})` : "";
34382
36165
  console.log(`\u2713 Refreshed docker-compose.yml to ${package_default.version}${backupNote}`);
34383
36166
  return true;
@@ -35611,12 +37394,12 @@ function resolvePaths() {
35611
37394
  const startDir = process.cwd();
35612
37395
  let currentDir = startDir;
35613
37396
  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 };
37397
+ const composeFile = path9.resolve(currentDir, "docker-compose.yml");
37398
+ if (fs11.existsSync(composeFile)) {
37399
+ const instancesFile = path9.resolve(currentDir, "instances.yml");
37400
+ return { fs: fs11, path: path9, projectDir: currentDir, composeFile, instancesFile };
35618
37401
  }
35619
- const parentDir = path7.dirname(currentDir);
37402
+ const parentDir = path9.dirname(currentDir);
35620
37403
  if (parentDir === currentDir)
35621
37404
  break;
35622
37405
  currentDir = parentDir;
@@ -35748,10 +37531,10 @@ function resolveAdoptedProject(reg) {
35748
37531
  }
35749
37532
  function updatePgwatchConfig(configPath, updates) {
35750
37533
  let lines = [];
35751
- if (fs9.existsSync(configPath)) {
35752
- const stats = fs9.statSync(configPath);
37534
+ if (fs11.existsSync(configPath)) {
37535
+ const stats = fs11.statSync(configPath);
35753
37536
  if (!stats.isDirectory()) {
35754
- const content = fs9.readFileSync(configPath, "utf8");
37537
+ const content = fs11.readFileSync(configPath, "utf8");
35755
37538
  lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
35756
37539
  }
35757
37540
  }
@@ -35763,7 +37546,7 @@ function updatePgwatchConfig(configPath, updates) {
35763
37546
  lines.push(`${key}=${value}`);
35764
37547
  }
35765
37548
  }
35766
- fs9.writeFileSync(configPath, lines.join(`
37549
+ fs11.writeFileSync(configPath, lines.join(`
35767
37550
  `) + `
35768
37551
  `, { encoding: "utf8", mode: 384 });
35769
37552
  }
@@ -35801,12 +37584,12 @@ async function runCompose(args, grafanaPassword) {
35801
37584
  if (grafanaPassword) {
35802
37585
  env.GF_SECURITY_ADMIN_PASSWORD = grafanaPassword;
35803
37586
  } else {
35804
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
35805
- if (fs9.existsSync(cfgPath)) {
37587
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
37588
+ if (fs11.existsSync(cfgPath)) {
35806
37589
  try {
35807
- const stats = fs9.statSync(cfgPath);
37590
+ const stats = fs11.statSync(cfgPath);
35808
37591
  if (!stats.isDirectory()) {
35809
- const content = fs9.readFileSync(cfgPath, "utf8");
37592
+ const content = fs11.readFileSync(cfgPath, "utf8");
35810
37593
  const match = content.match(/^grafana_password=([^\r\n]+)/m);
35811
37594
  if (match) {
35812
37595
  env.GF_SECURITY_ADMIN_PASSWORD = match[1].trim();
@@ -35819,10 +37602,10 @@ async function runCompose(args, grafanaPassword) {
35819
37602
  }
35820
37603
  }
35821
37604
  }
35822
- const envFilePath = path7.resolve(projectDir, ".env");
35823
- if (fs9.existsSync(envFilePath)) {
37605
+ const envFilePath = path9.resolve(projectDir, ".env");
37606
+ if (fs11.existsSync(envFilePath)) {
35824
37607
  try {
35825
- const envContent = fs9.readFileSync(envFilePath, "utf8");
37608
+ const envContent = fs11.readFileSync(envFilePath, "utf8");
35826
37609
  if (!env.VM_AUTH_USERNAME) {
35827
37610
  const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
35828
37611
  if (m)
@@ -35879,20 +37662,20 @@ mon.command("local-install").description("install local monitoring stack (genera
35879
37662
  console.log(`Project directory: ${projectDir}
35880
37663
  `);
35881
37664
  if (opts.project) {
35882
- const cfgPath2 = path7.resolve(projectDir, ".pgwatch-config");
37665
+ const cfgPath2 = path9.resolve(projectDir, ".pgwatch-config");
35883
37666
  updatePgwatchConfig(cfgPath2, { project_name: opts.project });
35884
37667
  console.log(`Using project name: ${opts.project}
35885
37668
  `);
35886
37669
  }
35887
- const envFile = path7.resolve(projectDir, ".env");
37670
+ const envFile = path9.resolve(projectDir, ".env");
35888
37671
  let existingRegistry = null;
35889
37672
  let existingPassword = null;
35890
37673
  let existingReplicatorPassword = null;
35891
37674
  let existingVmAuthUsername = null;
35892
37675
  let existingVmAuthPassword = null;
35893
37676
  let previousTag = null;
35894
- if (fs9.existsSync(envFile)) {
35895
- const existingEnv = fs9.readFileSync(envFile, "utf8");
37677
+ if (fs11.existsSync(envFile)) {
37678
+ const existingEnv = fs11.readFileSync(envFile, "utf8");
35896
37679
  const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
35897
37680
  if (previousTagMatch)
35898
37681
  previousTag = previousTagMatch[1].trim();
@@ -35920,10 +37703,10 @@ mon.command("local-install").description("install local monitoring stack (genera
35920
37703
  if (existingPassword) {
35921
37704
  envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
35922
37705
  }
35923
- envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto2.randomBytes(32).toString("hex")}`);
37706
+ envLines.push(`REPLICATOR_PASSWORD=${existingReplicatorPassword || crypto4.randomBytes(32).toString("hex")}`);
35924
37707
  envLines.push(`VM_AUTH_USERNAME=${existingVmAuthUsername || "vmauth"}`);
35925
- envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto2.randomBytes(18).toString("base64")}`);
35926
- fs9.writeFileSync(envFile, envLines.join(`
37708
+ envLines.push(`VM_AUTH_PASSWORD=${existingVmAuthPassword || crypto4.randomBytes(18).toString("base64")}`);
37709
+ fs11.writeFileSync(envFile, envLines.join(`
35927
37710
  `) + `
35928
37711
  `, { encoding: "utf8", mode: 384 });
35929
37712
  await refreshBundledComposeIfStale(projectDir, previousTag);
@@ -35960,7 +37743,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
35960
37743
  if (apiKey) {
35961
37744
  console.log("Using API key provided via --api-key parameter");
35962
37745
  writeConfig({ apiKey });
35963
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
37746
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
35964
37747
  console.log(`\u2713 API key saved
35965
37748
  `);
35966
37749
  } else if (opts.yes) {
@@ -35977,7 +37760,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
35977
37760
  const trimmedKey = inputApiKey.trim();
35978
37761
  if (trimmedKey) {
35979
37762
  writeConfig({ apiKey: trimmedKey });
35980
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
37763
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
35981
37764
  apiKey = trimmedKey;
35982
37765
  console.log(`\u2713 API key saved
35983
37766
  `);
@@ -36011,7 +37794,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
36011
37794
  # Add your instances using: postgres-ai mon targets add
36012
37795
 
36013
37796
  `;
36014
- fs9.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
37797
+ fs11.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
36015
37798
  console.log(`Instances file: ${instancesPath2}`);
36016
37799
  console.log(`Project directory: ${projectDir2}
36017
37800
  `);
@@ -36115,17 +37898,17 @@ You can provide either:`);
36115
37898
  }
36116
37899
  } else {
36117
37900
  console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
36118
- const currentDir = path7.dirname(fileURLToPath2(import.meta.url));
37901
+ const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
36119
37902
  const demoCandidates = [
36120
- path7.resolve(currentDir, "..", "..", "instances.demo.yml"),
36121
- path7.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
37903
+ path9.resolve(currentDir, "..", "..", "instances.demo.yml"),
37904
+ path9.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
36122
37905
  ];
36123
- const demoSrc = demoCandidates.find((p) => fs9.existsSync(p));
37906
+ const demoSrc = demoCandidates.find((p) => fs11.existsSync(p));
36124
37907
  if (demoSrc) {
36125
- if (fs9.existsSync(instancesPath) && fs9.lstatSync(instancesPath).isDirectory()) {
36126
- fs9.rmSync(instancesPath, { recursive: true, force: true });
37908
+ if (fs11.existsSync(instancesPath) && fs11.lstatSync(instancesPath).isDirectory()) {
37909
+ fs11.rmSync(instancesPath, { recursive: true, force: true });
36127
37910
  }
36128
- fs9.copyFileSync(demoSrc, instancesPath);
37911
+ fs11.copyFileSync(demoSrc, instancesPath);
36129
37912
  console.log(`\u2713 Demo monitoring target configured
36130
37913
  `);
36131
37914
  } else {
@@ -36144,15 +37927,15 @@ Searched: ${demoCandidates.join(", ")}
36144
37927
  console.log(`\u2713 Configuration updated
36145
37928
  `);
36146
37929
  console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
36147
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
37930
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
36148
37931
  let grafanaPassword = "";
36149
37932
  let vmAuthUsername = "";
36150
37933
  let vmAuthPassword = "";
36151
37934
  try {
36152
- if (fs9.existsSync(cfgPath)) {
36153
- const stats = fs9.statSync(cfgPath);
37935
+ if (fs11.existsSync(cfgPath)) {
37936
+ const stats = fs11.statSync(cfgPath);
36154
37937
  if (!stats.isDirectory()) {
36155
- const content = fs9.readFileSync(cfgPath, "utf8");
37938
+ const content = fs11.readFileSync(cfgPath, "utf8");
36156
37939
  const match = content.match(/^grafana_password=([^\r\n]+)/m);
36157
37940
  if (match) {
36158
37941
  grafanaPassword = match[1].trim();
@@ -36164,15 +37947,15 @@ Searched: ${demoCandidates.join(", ")}
36164
37947
  const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
36165
37948
  grafanaPassword = password.trim().replace(/\n/g, "");
36166
37949
  let configContent = "";
36167
- if (fs9.existsSync(cfgPath)) {
36168
- const stats = fs9.statSync(cfgPath);
37950
+ if (fs11.existsSync(cfgPath)) {
37951
+ const stats = fs11.statSync(cfgPath);
36169
37952
  if (!stats.isDirectory()) {
36170
- configContent = fs9.readFileSync(cfgPath, "utf8");
37953
+ configContent = fs11.readFileSync(cfgPath, "utf8");
36171
37954
  }
36172
37955
  }
36173
37956
  const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
36174
37957
  lines.push(`grafana_password=${grafanaPassword}`);
36175
- fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
37958
+ fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
36176
37959
  `) + `
36177
37960
  `, "utf8");
36178
37961
  }
@@ -36185,9 +37968,9 @@ Searched: ${demoCandidates.join(", ")}
36185
37968
  grafanaPassword = "demo";
36186
37969
  }
36187
37970
  try {
36188
- const envFile2 = path7.resolve(projectDir, ".env");
36189
- if (fs9.existsSync(envFile2)) {
36190
- const envContent = fs9.readFileSync(envFile2, "utf8");
37971
+ const envFile2 = path9.resolve(projectDir, ".env");
37972
+ if (fs11.existsSync(envFile2)) {
37973
+ const envContent = fs11.readFileSync(envFile2, "utf8");
36191
37974
  const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
36192
37975
  const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
36193
37976
  if (userMatch)
@@ -36203,13 +37986,13 @@ Searched: ${demoCandidates.join(", ")}
36203
37986
  vmAuthPassword = vmPass.trim().replace(/\n/g, "");
36204
37987
  }
36205
37988
  let envContent = "";
36206
- if (fs9.existsSync(envFile2)) {
36207
- envContent = fs9.readFileSync(envFile2, "utf8");
37989
+ if (fs11.existsSync(envFile2)) {
37990
+ envContent = fs11.readFileSync(envFile2, "utf8");
36208
37991
  }
36209
37992
  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 === ""));
36210
37993
  envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
36211
37994
  envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
36212
- fs9.writeFileSync(envFile2, envLines2.join(`
37995
+ fs11.writeFileSync(envFile2, envLines2.join(`
36213
37996
  `) + `
36214
37997
  `, { encoding: "utf8", mode: 384 });
36215
37998
  }
@@ -36242,7 +38025,7 @@ Searched: ${demoCandidates.join(", ")}
36242
38025
  });
36243
38026
  const adoptedProject = resolveAdoptedProject(reg);
36244
38027
  if (adoptedProject != null) {
36245
- updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
38028
+ updatePgwatchConfig(path9.resolve(projectDir, ".pgwatch-config"), {
36246
38029
  project_name: adoptedProject
36247
38030
  });
36248
38031
  const verb = reg?.created ? "Registered" : "Adopted";
@@ -36458,11 +38241,11 @@ mon.command("config").description("show monitoring services configuration").acti
36458
38241
  console.log(`Project Directory: ${projectDir}`);
36459
38242
  console.log(`Docker Compose File: ${composeFile}`);
36460
38243
  console.log(`Instances File: ${instancesFile}`);
36461
- if (fs9.existsSync(instancesFile) && !fs9.lstatSync(instancesFile).isDirectory()) {
38244
+ if (fs11.existsSync(instancesFile) && !fs11.lstatSync(instancesFile).isDirectory()) {
36462
38245
  console.log(`
36463
38246
  Instances configuration:
36464
38247
  `);
36465
- const text = fs9.readFileSync(instancesFile, "utf8");
38248
+ const text = fs11.readFileSync(instancesFile, "utf8");
36466
38249
  process.stdout.write(text);
36467
38250
  if (!/\n$/.test(text))
36468
38251
  console.log();
@@ -36511,8 +38294,8 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
36511
38294
  console.log("\u2713 .env is up to date");
36512
38295
  }
36513
38296
  console.log();
36514
- const gitDir = path7.resolve(projectDir, ".git");
36515
- if (fs9.existsSync(gitDir)) {
38297
+ const gitDir = path9.resolve(projectDir, ".git");
38298
+ if (fs11.existsSync(gitDir)) {
36516
38299
  console.log("Fetching latest changes...");
36517
38300
  await execFilePromise("git", ["fetch", "origin"]);
36518
38301
  const { stdout: branch } = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -36667,7 +38450,7 @@ mon.command("check").description("monitoring services system readiness check").a
36667
38450
  var targets = mon.command("targets").description("manage databases to monitor");
36668
38451
  targets.command("list").description("list monitoring target databases").action(async () => {
36669
38452
  const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
36670
- if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
38453
+ if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
36671
38454
  console.error(`instances.yml not found in ${projectDir}`);
36672
38455
  process.exitCode = 1;
36673
38456
  return;
@@ -36730,7 +38513,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
36730
38513
  });
36731
38514
  targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
36732
38515
  const { instancesFile: file } = await resolveOrInitPaths();
36733
- if (!fs9.existsSync(file) || fs9.lstatSync(file).isDirectory()) {
38516
+ if (!fs11.existsSync(file) || fs11.lstatSync(file).isDirectory()) {
36734
38517
  console.error("instances.yml not found");
36735
38518
  process.exitCode = 1;
36736
38519
  return;
@@ -36758,7 +38541,7 @@ targets.command("remove <name>").description("remove monitoring target database"
36758
38541
  });
36759
38542
  targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
36760
38543
  const { instancesFile: instancesPath } = await resolveOrInitPaths();
36761
- if (!fs9.existsSync(instancesPath) || fs9.lstatSync(instancesPath).isDirectory()) {
38544
+ if (!fs11.existsSync(instancesPath) || fs11.lstatSync(instancesPath).isDirectory()) {
36762
38545
  console.error("instances.yml not found");
36763
38546
  process.exitCode = 1;
36764
38547
  return;
@@ -37020,15 +38803,15 @@ To authenticate, run: pgai auth`);
37020
38803
  });
37021
38804
  auth.command("remove-key").description("remove API key").action(async () => {
37022
38805
  const newConfigPath = getConfigPath();
37023
- const hasNewConfig = fs9.existsSync(newConfigPath);
38806
+ const hasNewConfig = fs11.existsSync(newConfigPath);
37024
38807
  let legacyPath;
37025
38808
  try {
37026
38809
  const { projectDir } = await resolveOrInitPaths();
37027
- legacyPath = path7.resolve(projectDir, ".pgwatch-config");
38810
+ legacyPath = path9.resolve(projectDir, ".pgwatch-config");
37028
38811
  } catch {
37029
- legacyPath = path7.resolve(process.cwd(), ".pgwatch-config");
38812
+ legacyPath = path9.resolve(process.cwd(), ".pgwatch-config");
37030
38813
  }
37031
- const hasLegacyConfig = fs9.existsSync(legacyPath) && fs9.statSync(legacyPath).isFile();
38814
+ const hasLegacyConfig = fs11.existsSync(legacyPath) && fs11.statSync(legacyPath).isFile();
37032
38815
  if (!hasNewConfig && !hasLegacyConfig) {
37033
38816
  console.log("No API key configured");
37034
38817
  return;
@@ -37038,11 +38821,11 @@ auth.command("remove-key").description("remove API key").action(async () => {
37038
38821
  }
37039
38822
  if (hasLegacyConfig) {
37040
38823
  try {
37041
- const content = fs9.readFileSync(legacyPath, "utf8");
38824
+ const content = fs11.readFileSync(legacyPath, "utf8");
37042
38825
  const filtered = content.split(/\r?\n/).filter((l) => !/^api_key=/.test(l)).join(`
37043
38826
  `).replace(/\n+$/g, `
37044
38827
  `);
37045
- fs9.writeFileSync(legacyPath, filtered, "utf8");
38828
+ fs11.writeFileSync(legacyPath, filtered, "utf8");
37046
38829
  } catch (err) {
37047
38830
  console.warn(`Warning: Could not update legacy config: ${err instanceof Error ? err.message : String(err)}`);
37048
38831
  }
@@ -37053,7 +38836,7 @@ To authenticate again, run: pgai auth`);
37053
38836
  });
37054
38837
  mon.command("generate-grafana-password").description("generate Grafana password for monitoring services").action(async () => {
37055
38838
  const { projectDir } = await resolveOrInitPaths();
37056
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
38839
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
37057
38840
  try {
37058
38841
  const { stdout: password } = await execFilePromise("openssl", ["rand", "-base64", "12"]);
37059
38842
  const newPassword = password.trim().replace(/\n/g, "");
@@ -37063,17 +38846,17 @@ mon.command("generate-grafana-password").description("generate Grafana password
37063
38846
  return;
37064
38847
  }
37065
38848
  let configContent = "";
37066
- if (fs9.existsSync(cfgPath)) {
37067
- const stats = fs9.statSync(cfgPath);
38849
+ if (fs11.existsSync(cfgPath)) {
38850
+ const stats = fs11.statSync(cfgPath);
37068
38851
  if (stats.isDirectory()) {
37069
38852
  console.error(".pgwatch-config is a directory, expected a file. Skipping read.");
37070
38853
  } else {
37071
- configContent = fs9.readFileSync(cfgPath, "utf8");
38854
+ configContent = fs11.readFileSync(cfgPath, "utf8");
37072
38855
  }
37073
38856
  }
37074
38857
  const lines = configContent.split(/\r?\n/).filter((l) => !/^grafana_password=/.test(l));
37075
38858
  lines.push(`grafana_password=${newPassword}`);
37076
- fs9.writeFileSync(cfgPath, lines.filter(Boolean).join(`
38859
+ fs11.writeFileSync(cfgPath, lines.filter(Boolean).join(`
37077
38860
  `) + `
37078
38861
  `, "utf8");
37079
38862
  console.log("\u2713 New Grafana password generated and saved");
@@ -37095,19 +38878,19 @@ Note: This command requires 'openssl' to be installed`);
37095
38878
  });
37096
38879
  mon.command("show-grafana-credentials").description("show Grafana credentials for monitoring services").action(async () => {
37097
38880
  const { projectDir } = await resolveOrInitPaths();
37098
- const cfgPath = path7.resolve(projectDir, ".pgwatch-config");
37099
- if (!fs9.existsSync(cfgPath)) {
38881
+ const cfgPath = path9.resolve(projectDir, ".pgwatch-config");
38882
+ if (!fs11.existsSync(cfgPath)) {
37100
38883
  console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
37101
38884
  process.exitCode = 1;
37102
38885
  return;
37103
38886
  }
37104
- const stats = fs9.statSync(cfgPath);
38887
+ const stats = fs11.statSync(cfgPath);
37105
38888
  if (stats.isDirectory()) {
37106
38889
  console.error(".pgwatch-config is a directory, expected a file. Cannot read credentials.");
37107
38890
  process.exitCode = 1;
37108
38891
  return;
37109
38892
  }
37110
- const content = fs9.readFileSync(cfgPath, "utf8");
38893
+ const content = fs11.readFileSync(cfgPath, "utf8");
37111
38894
  const lines = content.split(/\r?\n/);
37112
38895
  let password = "";
37113
38896
  for (const line of lines) {
@@ -37127,9 +38910,9 @@ Grafana credentials:`);
37127
38910
  console.log(" URL: http://localhost:3000");
37128
38911
  console.log(" Username: monitor");
37129
38912
  console.log(` Password: ${password}`);
37130
- const envFile = path7.resolve(projectDir, ".env");
37131
- if (fs9.existsSync(envFile)) {
37132
- const envContent = fs9.readFileSync(envFile, "utf8");
38913
+ const envFile = path9.resolve(projectDir, ".env");
38914
+ if (fs11.existsSync(envFile)) {
38915
+ const envContent = fs11.readFileSync(envFile, "utf8");
37133
38916
  const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
37134
38917
  const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
37135
38918
  if (vmUser && vmPass) {
@@ -37855,13 +39638,13 @@ reports.command("data [reportId]").description("get checkup report file data (in
37855
39638
  });
37856
39639
  spinner.stop();
37857
39640
  if (opts.output) {
37858
- const dir = path7.resolve(opts.output);
37859
- fs9.mkdirSync(dir, { recursive: true });
39641
+ const dir = path9.resolve(opts.output);
39642
+ fs11.mkdirSync(dir, { recursive: true });
37860
39643
  for (const f of result) {
37861
- const safeName = path7.basename(f.filename);
37862
- const filePath = path7.join(dir, safeName);
39644
+ const safeName = path9.basename(f.filename);
39645
+ const filePath = path9.join(dir, safeName);
37863
39646
  const content = f.type === "json" ? JSON.stringify(tryParseJson(f.data), null, 2) : f.data;
37864
- fs9.writeFileSync(filePath, content, "utf-8");
39647
+ fs11.writeFileSync(filePath, content, "utf-8");
37865
39648
  console.log(filePath);
37866
39649
  }
37867
39650
  } else if (opts.json) {
@@ -37906,6 +39689,406 @@ function tryParseJson(s) {
37906
39689
  return s;
37907
39690
  }
37908
39691
  }
39692
+ function inferCommandFromResult(r) {
39693
+ if (r.command)
39694
+ return r.command;
39695
+ if (r.plan_text !== undefined || r.plan_json !== undefined)
39696
+ return "plan";
39697
+ if (r.row_count !== undefined || r.result_rows !== undefined)
39698
+ return "exec";
39699
+ if (r.hypo_used !== undefined)
39700
+ return "hypo";
39701
+ if (r.terminated !== undefined)
39702
+ return "terminate";
39703
+ if (r.reset !== undefined)
39704
+ return "reset";
39705
+ if (r.snapshot !== undefined)
39706
+ return "activity";
39707
+ return null;
39708
+ }
39709
+ function printJoeOutcome(command, outcome, json3) {
39710
+ const budgetSeconds = Math.round(DEFAULT_BUDGET_MS2 / 1000);
39711
+ if (outcome.budgetExpired) {
39712
+ if (json3) {
39713
+ console.log(JSON.stringify({
39714
+ command_id: outcome.commandId,
39715
+ status: outcome.status,
39716
+ session_id: outcome.sessionId,
39717
+ budget_expired: true,
39718
+ resume: `pgai result ${outcome.commandId}`
39719
+ }, null, 2));
39720
+ } else {
39721
+ console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai result ${outcome.commandId}`);
39722
+ }
39723
+ return;
39724
+ }
39725
+ const result = outcome.result;
39726
+ if (outcome.status === "done" && result) {
39727
+ if (json3) {
39728
+ console.log(JSON.stringify(result, null, 2));
39729
+ } else {
39730
+ console.log(`submitted ${outcome.commandId} \xB7 done`);
39731
+ const body = formatJoeResult(command, result);
39732
+ if (body)
39733
+ console.log(body);
39734
+ }
39735
+ return;
39736
+ }
39737
+ if (outcome.status === "error") {
39738
+ const msg = result?.error ?? "command failed";
39739
+ console.error(`command ${outcome.commandId} error: ${msg}`);
39740
+ process.exitCode = 1;
39741
+ return;
39742
+ }
39743
+ console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai result ${outcome.commandId}`);
39744
+ process.exitCode = 1;
39745
+ }
39746
+ async function runJoeCli(command, sql, args, opts) {
39747
+ try {
39748
+ const rootOpts = program2.opts();
39749
+ const cfg = readConfig();
39750
+ const { apiKey } = getConfig(rootOpts);
39751
+ if (!apiKey) {
39752
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39753
+ process.exitCode = 1;
39754
+ return;
39755
+ }
39756
+ const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
39757
+ if (!projectRef) {
39758
+ console.error("Project is required. Pass --project <id|alias>.");
39759
+ process.exitCode = 1;
39760
+ return;
39761
+ }
39762
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39763
+ const budgetMs = typeof opts.budget === "number" && !Number.isNaN(opts.budget) ? opts.budget * 1000 : undefined;
39764
+ const outcome = await executeJoeCommand2({
39765
+ apiKey,
39766
+ apiBaseUrl,
39767
+ command,
39768
+ project: projectRef,
39769
+ sql,
39770
+ args,
39771
+ session: opts.session ?? null,
39772
+ newSession: !!opts.newSession,
39773
+ orgId: cfg.orgId ?? undefined,
39774
+ budgetMs,
39775
+ debug: !!opts.debug
39776
+ });
39777
+ printJoeOutcome(command, outcome, !!opts.json);
39778
+ } catch (err) {
39779
+ const message = err instanceof Error ? err.message : String(err);
39780
+ console.error(message);
39781
+ process.exitCode = 1;
39782
+ }
39783
+ }
39784
+ function withJoeOptions(cmd) {
39785
+ 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");
39786
+ }
39787
+ withJoeOptions(program2.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
39788
+ await runJoeCli("plan", sql, null, opts);
39789
+ });
39790
+ withJoeOptions(program2.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
39791
+ await runJoeCli("explain", sql, null, opts);
39792
+ });
39793
+ withJoeOptions(program2.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
39794
+ await runJoeCli("exec", sql, null, opts);
39795
+ });
39796
+ withJoeOptions(program2.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) => {
39797
+ await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
39798
+ });
39799
+ withJoeOptions(program2.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
39800
+ await runJoeCli("activity", null, null, opts);
39801
+ });
39802
+ withJoeOptions(program2.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
39803
+ const pidNum = parseInt(pid, 10);
39804
+ if (Number.isNaN(pidNum)) {
39805
+ console.error("pid must be a number");
39806
+ process.exitCode = 1;
39807
+ return;
39808
+ }
39809
+ await runJoeCli("terminate", null, { pid: pidNum }, opts);
39810
+ });
39811
+ withJoeOptions(program2.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
39812
+ await runJoeCli("reset", null, null, opts);
39813
+ });
39814
+ withJoeOptions(program2.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) => {
39815
+ const args = { object: object4 };
39816
+ if (opts.variant)
39817
+ args.variant = opts.variant;
39818
+ await runJoeCli("describe", null, args, opts);
39819
+ });
39820
+ 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) => {
39821
+ try {
39822
+ const rootOpts = program2.opts();
39823
+ const cfg = readConfig();
39824
+ const { apiKey } = getConfig(rootOpts);
39825
+ if (!apiKey) {
39826
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39827
+ process.exitCode = 1;
39828
+ return;
39829
+ }
39830
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39831
+ const projects = await listProjects2({
39832
+ apiKey,
39833
+ apiBaseUrl,
39834
+ orgId: cfg.orgId ?? undefined,
39835
+ debug: !!opts.debug
39836
+ });
39837
+ if (opts.json) {
39838
+ console.log(JSON.stringify(projects, null, 2));
39839
+ } else {
39840
+ console.log(formatProjectsTable(projects));
39841
+ }
39842
+ } catch (err) {
39843
+ const message = err instanceof Error ? err.message : String(err);
39844
+ console.error(message);
39845
+ process.exitCode = 1;
39846
+ }
39847
+ });
39848
+ async function resolveDblabTarget(project, debug) {
39849
+ const rootOpts = program2.opts();
39850
+ const cfg = readConfig();
39851
+ const { apiKey } = getConfig(rootOpts);
39852
+ if (!apiKey) {
39853
+ throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
39854
+ }
39855
+ const ref = (project ?? "").trim();
39856
+ if (!ref) {
39857
+ throw new Error("--project <id|alias> is required");
39858
+ }
39859
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39860
+ const orgId = cfg.orgId ?? undefined;
39861
+ const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
39862
+ return { apiKey, apiBaseUrl, orgId, instanceId };
39863
+ }
39864
+ var clone2 = program2.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
39865
+ 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) => {
39866
+ try {
39867
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39868
+ const result = await createClone2({
39869
+ apiKey,
39870
+ apiBaseUrl,
39871
+ instanceId,
39872
+ cloneId: opts.id,
39873
+ branch: opts.branch,
39874
+ snapshotId: opts.snapshot,
39875
+ dbUser: opts.dbUser,
39876
+ dbPassword: opts.dbPassword,
39877
+ isProtected: !!opts.protected,
39878
+ debug: !!opts.debug
39879
+ });
39880
+ printResult(result, opts.json);
39881
+ } catch (err) {
39882
+ console.error(err instanceof Error ? err.message : String(err));
39883
+ process.exitCode = 1;
39884
+ }
39885
+ });
39886
+ program2.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) => {
39887
+ try {
39888
+ const rootOpts = program2.opts();
39889
+ const cfg = readConfig();
39890
+ const { apiKey } = getConfig(rootOpts);
39891
+ if (!apiKey) {
39892
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39893
+ process.exitCode = 1;
39894
+ return;
39895
+ }
39896
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39897
+ const status = await getCommandStatus2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
39898
+ if (opts.json) {
39899
+ console.log(JSON.stringify(status, null, 2));
39900
+ } else {
39901
+ console.log(`command ${status.command_id} \xB7 ${status.status}${status.error ? ` \xB7 ${status.error}` : ""}`);
39902
+ }
39903
+ } catch (err) {
39904
+ const message = err instanceof Error ? err.message : String(err);
39905
+ console.error(message);
39906
+ process.exitCode = 1;
39907
+ }
39908
+ });
39909
+ 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) => {
39910
+ try {
39911
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39912
+ const result = await listClones2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
39913
+ printResult(result, opts.json);
39914
+ } catch (err) {
39915
+ console.error(err instanceof Error ? err.message : String(err));
39916
+ process.exitCode = 1;
39917
+ }
39918
+ });
39919
+ program2.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) => {
39920
+ try {
39921
+ const rootOpts = program2.opts();
39922
+ const cfg = readConfig();
39923
+ const { apiKey } = getConfig(rootOpts);
39924
+ if (!apiKey) {
39925
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
39926
+ process.exitCode = 1;
39927
+ return;
39928
+ }
39929
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
39930
+ const result = await getCommandResult2({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
39931
+ if (opts.json) {
39932
+ console.log(JSON.stringify(result, null, 2));
39933
+ return;
39934
+ }
39935
+ const inferred = inferCommandFromResult(result);
39936
+ if (result.status === "done" && inferred) {
39937
+ console.log(`command ${result.command_id} \xB7 done`);
39938
+ const body = formatJoeResult(inferred, result);
39939
+ if (body)
39940
+ console.log(body);
39941
+ } else if (result.status === "error") {
39942
+ console.error(`command ${result.command_id} error: ${result.error ?? "command failed"}`);
39943
+ process.exitCode = 1;
39944
+ } else {
39945
+ console.log(`command ${result.command_id} \xB7 ${result.status}`);
39946
+ }
39947
+ } catch (err) {
39948
+ const message = err instanceof Error ? err.message : String(err);
39949
+ console.error(message);
39950
+ process.exitCode = 1;
39951
+ }
39952
+ });
39953
+ 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) => {
39954
+ try {
39955
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39956
+ const result = await getClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
39957
+ printResult(result, opts.json);
39958
+ } catch (err) {
39959
+ console.error(err instanceof Error ? err.message : String(err));
39960
+ process.exitCode = 1;
39961
+ }
39962
+ });
39963
+ clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot (requires joe:admin)").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) => {
39964
+ try {
39965
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39966
+ const result = await resetClone2({
39967
+ apiKey,
39968
+ apiBaseUrl,
39969
+ instanceId,
39970
+ cloneId,
39971
+ snapshotId: opts.snapshot,
39972
+ latest: opts.latest,
39973
+ debug: !!opts.debug
39974
+ });
39975
+ printResult(result ?? { reset: true, cloneId }, opts.json);
39976
+ } catch (err) {
39977
+ console.error(err instanceof Error ? err.message : String(err));
39978
+ process.exitCode = 1;
39979
+ }
39980
+ });
39981
+ 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) => {
39982
+ try {
39983
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39984
+ const result = await destroyClone2({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
39985
+ printResult(result ?? { destroyed: true, cloneId }, opts.json);
39986
+ } catch (err) {
39987
+ console.error(err instanceof Error ? err.message : String(err));
39988
+ process.exitCode = 1;
39989
+ }
39990
+ });
39991
+ var branch = program2.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
39992
+ 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) => {
39993
+ try {
39994
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
39995
+ const result = await listBranches2({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
39996
+ printResult(result, opts.json);
39997
+ } catch (err) {
39998
+ console.error(err instanceof Error ? err.message : String(err));
39999
+ process.exitCode = 1;
40000
+ }
40001
+ });
40002
+ 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) => {
40003
+ try {
40004
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40005
+ const result = await createBranch2({
40006
+ apiKey,
40007
+ apiBaseUrl,
40008
+ instanceId,
40009
+ branchName: name,
40010
+ baseBranch: opts.baseBranch,
40011
+ snapshotId: opts.snapshot,
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
+ 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) => {
40021
+ try {
40022
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40023
+ const result = await deleteBranch2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40024
+ printResult(result ?? { deleted: true, branch: name }, opts.json);
40025
+ } catch (err) {
40026
+ console.error(err instanceof Error ? err.message : String(err));
40027
+ process.exitCode = 1;
40028
+ }
40029
+ });
40030
+ 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) => {
40031
+ try {
40032
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40033
+ const result = await branchLog2({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40034
+ printResult(result, opts.json);
40035
+ } catch (err) {
40036
+ console.error(err instanceof Error ? err.message : String(err));
40037
+ process.exitCode = 1;
40038
+ }
40039
+ });
40040
+ var snapshot = program2.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
40041
+ 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) => {
40042
+ try {
40043
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40044
+ const result = await listSnapshots2({
40045
+ apiKey,
40046
+ apiBaseUrl,
40047
+ instanceId,
40048
+ branchName: opts.branch,
40049
+ dataset: opts.dataset,
40050
+ debug: !!opts.debug
40051
+ });
40052
+ printResult(result, opts.json);
40053
+ } catch (err) {
40054
+ console.error(err instanceof Error ? err.message : String(err));
40055
+ process.exitCode = 1;
40056
+ }
40057
+ });
40058
+ 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) => {
40059
+ try {
40060
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40061
+ const result = await createSnapshot2({
40062
+ apiKey,
40063
+ apiBaseUrl,
40064
+ instanceId,
40065
+ cloneId: opts.clone,
40066
+ message: opts.message,
40067
+ debug: !!opts.debug
40068
+ });
40069
+ printResult(result, opts.json);
40070
+ } catch (err) {
40071
+ console.error(err instanceof Error ? err.message : String(err));
40072
+ process.exitCode = 1;
40073
+ }
40074
+ });
40075
+ 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) => {
40076
+ try {
40077
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40078
+ const result = await destroySnapshot2({
40079
+ apiKey,
40080
+ apiBaseUrl,
40081
+ instanceId,
40082
+ snapshotId,
40083
+ force: !!opts.force,
40084
+ debug: !!opts.debug
40085
+ });
40086
+ printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
40087
+ } catch (err) {
40088
+ console.error(err instanceof Error ? err.message : String(err));
40089
+ process.exitCode = 1;
40090
+ }
40091
+ });
37909
40092
  var mcp = program2.command("mcp").description("MCP server integration");
37910
40093
  mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
37911
40094
  const rootOpts = program2.opts();
@@ -37979,29 +40162,29 @@ mcp.command("install [client]").description("install MCP server configuration fo
37979
40162
  let configDir;
37980
40163
  switch (client) {
37981
40164
  case "cursor":
37982
- configPath = path7.join(homeDir, ".cursor", "mcp.json");
37983
- configDir = path7.dirname(configPath);
40165
+ configPath = path9.join(homeDir, ".cursor", "mcp.json");
40166
+ configDir = path9.dirname(configPath);
37984
40167
  break;
37985
40168
  case "windsurf":
37986
- configPath = path7.join(homeDir, ".windsurf", "mcp.json");
37987
- configDir = path7.dirname(configPath);
40169
+ configPath = path9.join(homeDir, ".windsurf", "mcp.json");
40170
+ configDir = path9.dirname(configPath);
37988
40171
  break;
37989
40172
  case "codex":
37990
- configPath = path7.join(homeDir, ".codex", "mcp.json");
37991
- configDir = path7.dirname(configPath);
40173
+ configPath = path9.join(homeDir, ".codex", "mcp.json");
40174
+ configDir = path9.dirname(configPath);
37992
40175
  break;
37993
40176
  default:
37994
40177
  console.error(`Configuration not implemented for: ${client}`);
37995
40178
  process.exitCode = 1;
37996
40179
  return;
37997
40180
  }
37998
- if (!fs9.existsSync(configDir)) {
37999
- fs9.mkdirSync(configDir, { recursive: true });
40181
+ if (!fs11.existsSync(configDir)) {
40182
+ fs11.mkdirSync(configDir, { recursive: true });
38000
40183
  }
38001
40184
  let config2 = { mcpServers: {} };
38002
- if (fs9.existsSync(configPath)) {
40185
+ if (fs11.existsSync(configPath)) {
38003
40186
  try {
38004
- const content = fs9.readFileSync(configPath, "utf8");
40187
+ const content = fs11.readFileSync(configPath, "utf8");
38005
40188
  config2 = JSON.parse(content);
38006
40189
  if (!config2.mcpServers) {
38007
40190
  config2.mcpServers = {};
@@ -38014,7 +40197,7 @@ mcp.command("install [client]").description("install MCP server configuration fo
38014
40197
  command: pgaiPath,
38015
40198
  args: ["mcp", "start"]
38016
40199
  };
38017
- fs9.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
40200
+ fs11.writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf8");
38018
40201
  console.log(`\u2713 PostgresAI MCP server configured for ${client}`);
38019
40202
  console.log(` Config file: ${configPath}`);
38020
40203
  console.log("");