postgresai 0.16.0-dev.3 → 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.
@@ -13425,7 +13425,7 @@ var {
13425
13425
  // package.json
13426
13426
  var package_default = {
13427
13427
  name: "postgresai",
13428
- version: "0.16.0-dev.3",
13428
+ version: "0.16.0-dev.4",
13429
13429
  description: "postgres_ai CLI",
13430
13430
  license: "Apache-2.0",
13431
13431
  private: false,
@@ -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.3";
16259
+ var version = "0.16.0-dev.4";
16260
16260
  var package_default2 = {
16261
16261
  name: "postgresai",
16262
16262
  version,
@@ -17267,83 +17267,47 @@ async function fetchReportFileData(params) {
17267
17267
  }
17268
17268
  }
17269
17269
 
17270
- // lib/dblab.ts
17271
- function isNumericProjectRef(ref) {
17272
- return /^[0-9]+$/.test(ref.trim());
17273
- }
17274
- async function resolveDblabInstanceId(params) {
17275
- const { apiKey, apiBaseUrl, orgId, debug } = params;
17276
- if (!apiKey) {
17277
- throw new Error("API key is required");
17278
- }
17279
- const ref = String(params.project ?? "").trim();
17280
- if (!ref) {
17281
- throw new Error("project is required (--project <id|alias>)");
17282
- }
17283
- const base = normalizeBaseUrl(apiBaseUrl);
17284
- const url = new URL(`${base}/dblab_instances`);
17285
- url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
17286
- if (typeof orgId === "number") {
17287
- url.searchParams.set("org_id", `eq.${orgId}`);
17288
- }
17289
- const headers = {
17290
- "access-token": apiKey,
17291
- "Content-Type": "application/json",
17292
- Connection: "close"
17293
- };
17294
- if (debug) {
17295
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17296
- console.error(`Debug: GET URL: ${url.toString()}`);
17297
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17298
- }
17299
- const response = await fetch(url.toString(), { method: "GET", headers });
17300
- const text = await response.text();
17301
- if (debug) {
17302
- console.error(`Debug: Response status: ${response.status}`);
17303
- console.error(`Debug: Response body: ${text}`);
17304
- }
17305
- if (!response.ok) {
17306
- throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
17307
- }
17308
- let rows;
17309
- try {
17310
- const parsed = JSON.parse(text);
17311
- rows = Array.isArray(parsed) ? parsed : [];
17312
- } catch {
17313
- throw new Error(`Failed to parse DBLab instances response: ${text}`);
17314
- }
17315
- const numeric = isNumericProjectRef(ref);
17316
- const needle = ref.toLowerCase();
17317
- const match = rows.find((r) => {
17318
- if (numeric) {
17319
- return String(r.project_id ?? "") === ref;
17320
- }
17321
- return r.project_alias != null && String(r.project_alias).toLowerCase() === needle || r.project_name != null && String(r.project_name).toLowerCase() === needle || r.project_label_or_name != null && String(r.project_label_or_name).toLowerCase() === needle;
17322
- });
17323
- if (!match) {
17324
- throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
17325
- }
17326
- return String(match.id);
17327
- }
17328
- async function callDblabApi(params) {
17329
- const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
17270
+ // lib/joe.ts
17271
+ import * as fs3 from "fs";
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;
17330
17305
  if (!apiKey) {
17331
17306
  throw new Error("API key is required");
17332
17307
  }
17333
- if (!instanceId) {
17334
- throw new Error("instanceId is required");
17335
- }
17336
17308
  const base = normalizeBaseUrl(apiBaseUrl);
17337
- const url = new URL(`${base}/rpc/dblab_api_call`);
17338
- const bodyObj = {
17339
- instance_id: instanceId,
17340
- action,
17341
- method
17342
- };
17343
- if (data !== undefined) {
17344
- bodyObj.data = data;
17345
- }
17346
- const body = JSON.stringify(bodyObj);
17309
+ const url = new URL(`${base}/rpc/${fn}`);
17310
+ const payload = JSON.stringify(body);
17347
17311
  const headers = {
17348
17312
  "access-token": apiKey,
17349
17313
  "Content-Type": "application/json",
@@ -17353,9 +17317,13 @@ async function callDblabApi(params) {
17353
17317
  const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17354
17318
  console.error(`Debug: POST URL: ${url.toString()}`);
17355
17319
  console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17356
- console.error(`Debug: Request body: ${body}`);
17320
+ console.error(`Debug: Request body: ${payload}`);
17357
17321
  }
17358
- const response = await fetch(url.toString(), { method: "POST", headers, body });
17322
+ const response = await fetch(url.toString(), {
17323
+ method: "POST",
17324
+ headers,
17325
+ body: payload
17326
+ });
17359
17327
  const text = await response.text();
17360
17328
  if (debug) {
17361
17329
  console.error(`Debug: Response status: ${response.status}`);
@@ -17364,470 +17332,311 @@ async function callDblabApi(params) {
17364
17332
  if (!response.ok) {
17365
17333
  throw new Error(formatHttpError(operation, response.status, text));
17366
17334
  }
17367
- if (text.trim() === "") {
17368
- return null;
17369
- }
17370
17335
  try {
17371
17336
  return JSON.parse(text);
17372
17337
  } catch {
17373
17338
  throw new Error(`${operation}: failed to parse response: ${text}`);
17374
17339
  }
17375
17340
  }
17376
- async function createClone(params) {
17377
- const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
17378
- const data = { protected: Boolean(isProtected) };
17379
- if (cloneId)
17380
- data.id = cloneId;
17381
- if (branch)
17382
- data.branch = branch;
17383
- if (snapshotId)
17384
- data.snapshot = { id: snapshotId };
17385
- if (dbUser && dbPassword)
17386
- data.db = { username: dbUser, password: dbPassword };
17387
- return callDblabApi({
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({
17388
17357
  apiKey,
17389
17358
  apiBaseUrl,
17390
- instanceId,
17391
- action: "/clone",
17392
- method: "post",
17393
- data,
17394
- operation: "Failed to create clone",
17359
+ fn: "joe_command_submit",
17360
+ body,
17361
+ operation: `Failed to submit ${command} command`,
17395
17362
  debug
17396
17363
  });
17397
17364
  }
17398
- async function listClones(params) {
17399
- const { apiKey, apiBaseUrl, instanceId, debug } = params;
17400
- return callDblabApi({
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({
17401
17371
  apiKey,
17402
17372
  apiBaseUrl,
17403
- instanceId,
17404
- action: "/clones",
17405
- method: "get",
17406
- operation: "Failed to list clones",
17373
+ fn: "joe_command_status",
17374
+ body: { command_id: commandId },
17375
+ operation: "Failed to fetch command status",
17407
17376
  debug
17408
17377
  });
17409
17378
  }
17410
- async function getClone(params) {
17411
- const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17412
- if (!cloneId)
17413
- throw new Error("cloneId is required");
17414
- return callDblabApi({
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({
17415
17385
  apiKey,
17416
17386
  apiBaseUrl,
17417
- instanceId,
17418
- action: `/clone/${encodeURIComponent(cloneId)}`,
17419
- method: "get",
17420
- operation: "Failed to get clone",
17387
+ fn: "joe_command_result",
17388
+ body: { command_id: commandId },
17389
+ operation: "Failed to fetch command result",
17421
17390
  debug
17422
17391
  });
17423
17392
  }
17424
- async function resetClone(params) {
17425
- const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
17426
- if (!cloneId)
17427
- throw new Error("cloneId is required");
17428
- const data = { latest: latest ?? !snapshotId };
17429
- if (snapshotId)
17430
- data.snapshotID = snapshotId;
17431
- return callDblabApi({
17432
- apiKey,
17433
- apiBaseUrl,
17434
- instanceId,
17435
- action: `/clone/${encodeURIComponent(cloneId)}/reset`,
17436
- method: "post",
17437
- data,
17438
- operation: "Failed to reset clone",
17439
- debug
17440
- });
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
+ };
17441
17405
  }
17442
- async function destroyClone(params) {
17443
- const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
17444
- if (!cloneId)
17445
- throw new Error("cloneId is required");
17446
- return callDblabApi({
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({
17447
17413
  apiKey,
17448
17414
  apiBaseUrl,
17449
- instanceId,
17450
- action: `/clone/${encodeURIComponent(cloneId)}`,
17451
- method: "delete",
17452
- operation: "Failed to destroy clone",
17415
+ fn: "projects_list",
17416
+ body,
17417
+ operation: "Failed to list projects",
17453
17418
  debug
17454
17419
  });
17420
+ if (!Array.isArray(rows)) {
17421
+ return [];
17422
+ }
17423
+ return rows.map(normalizeProjectRow);
17455
17424
  }
17456
- async function listBranches(params) {
17457
- const { apiKey, apiBaseUrl, instanceId, debug } = params;
17458
- return callDblabApi({
17459
- apiKey,
17460
- apiBaseUrl,
17461
- instanceId,
17462
- action: "/branches",
17463
- method: "get",
17464
- operation: "Failed to list branches",
17465
- debug
17466
- });
17425
+ function isNumericProjectRef(ref) {
17426
+ return /^[0-9]+$/.test(ref.trim());
17467
17427
  }
17468
- async function createBranch(params) {
17469
- const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
17470
- if (!branchName)
17471
- throw new Error("branchName is required");
17472
- const data = { branchName };
17473
- if (baseBranch)
17474
- data.baseBranch = baseBranch;
17475
- if (snapshotId)
17476
- data.snapshotID = snapshotId;
17477
- return callDblabApi({
17478
- apiKey,
17479
- apiBaseUrl,
17480
- instanceId,
17481
- action: "/branch",
17482
- method: "post",
17483
- data,
17484
- operation: "Failed to create branch",
17485
- debug
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
17486
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;
17487
17448
  }
17488
- async function deleteBranch(params) {
17489
- const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17490
- if (!branchName)
17491
- throw new Error("branchName is required");
17492
- return callDblabApi({
17493
- apiKey,
17494
- apiBaseUrl,
17495
- instanceId,
17496
- action: `/branch/${encodeURIComponent(branchName)}`,
17497
- method: "delete",
17498
- operation: "Failed to delete branch",
17499
- debug
17500
- });
17449
+ function sessionStorePath(dir) {
17450
+ return path3.join(dir ?? getConfigDir2(), "joe-sessions.json");
17501
17451
  }
17502
- async function branchLog(params) {
17503
- const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
17504
- if (!branchName)
17505
- throw new Error("branchName is required");
17506
- return callDblabApi({
17507
- apiKey,
17508
- apiBaseUrl,
17509
- instanceId,
17510
- action: `/branch/${encodeURIComponent(branchName)}/log`,
17511
- method: "get",
17512
- operation: "Failed to fetch branch log",
17513
- debug
17514
- });
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 {};
17515
17464
  }
17516
- async function listSnapshots(params) {
17517
- const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
17518
- const qs = new URLSearchParams;
17519
- const branch = branchName?.trim();
17520
- if (branch)
17521
- qs.append("branch", branch);
17522
- if (dataset)
17523
- qs.append("dataset", dataset);
17524
- const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
17525
- return callDblabApi({
17526
- apiKey,
17527
- apiBaseUrl,
17528
- instanceId,
17529
- action,
17530
- method: "get",
17531
- operation: "Failed to list snapshots",
17532
- debug
17533
- });
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;
17534
17469
  }
17535
- async function createSnapshot(params) {
17536
- const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
17537
- if (!cloneId)
17538
- throw new Error("cloneId is required");
17539
- const data = { cloneID: cloneId };
17540
- if (message)
17541
- data.message = message;
17542
- return callDblabApi({
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 {
17543
17505
  apiKey,
17544
17506
  apiBaseUrl,
17545
- instanceId,
17546
- action: "/branch/snapshot",
17547
- method: "post",
17548
- data,
17549
- operation: "Failed to create snapshot",
17507
+ command,
17508
+ projectId,
17509
+ sql,
17510
+ args,
17511
+ sessionId,
17550
17512
  debug
17551
- });
17552
- }
17553
- async function destroySnapshot(params) {
17554
- const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
17555
- if (!snapshotId)
17556
- throw new Error("snapshotId is required");
17557
- const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
17558
- return callDblabApi({
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({
17559
17520
  apiKey,
17560
17521
  apiBaseUrl,
17561
- instanceId,
17562
- action,
17563
- method: "delete",
17564
- operation: "Failed to destroy snapshot",
17522
+ command,
17523
+ projectId,
17524
+ sql,
17525
+ args,
17526
+ sessionId,
17527
+ idempotencyKey,
17565
17528
  debug
17566
17529
  });
17567
- }
17568
-
17569
- // lib/storage.ts
17570
- import * as fs3 from "fs";
17571
- import * as path3 from "path";
17572
- var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
17573
- var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17574
- var MIME_TYPES = {
17575
- ".png": "image/png",
17576
- ".jpg": "image/jpeg",
17577
- ".jpeg": "image/jpeg",
17578
- ".gif": "image/gif",
17579
- ".webp": "image/webp",
17580
- ".svg": "image/svg+xml",
17581
- ".bmp": "image/bmp",
17582
- ".ico": "image/x-icon",
17583
- ".pdf": "application/pdf",
17584
- ".json": "application/json",
17585
- ".xml": "application/xml",
17586
- ".zip": "application/zip",
17587
- ".gz": "application/gzip",
17588
- ".tar": "application/x-tar",
17589
- ".csv": "text/csv",
17590
- ".txt": "text/plain",
17591
- ".log": "text/plain",
17592
- ".sql": "application/sql",
17593
- ".html": "text/html",
17594
- ".css": "text/css",
17595
- ".js": "application/javascript",
17596
- ".ts": "application/typescript",
17597
- ".md": "text/markdown",
17598
- ".yaml": "application/x-yaml",
17599
- ".yml": "application/x-yaml"
17600
- };
17601
- async function uploadFile(params) {
17602
- const { apiKey, storageBaseUrl, filePath, debug } = params;
17603
- if (!apiKey) {
17604
- throw new Error("API key is required");
17605
- }
17606
- if (!storageBaseUrl) {
17607
- throw new Error("storageBaseUrl is required");
17608
- }
17609
- if (!filePath) {
17610
- throw new Error("filePath is required");
17611
- }
17612
- const resolvedPath = path3.resolve(filePath);
17613
- if (!fs3.existsSync(resolvedPath)) {
17614
- throw new Error(`File not found: ${resolvedPath}`);
17615
- }
17616
- const stat = fs3.statSync(resolvedPath);
17617
- if (!stat.isFile()) {
17618
- throw new Error(`Not a file: ${resolvedPath}`);
17619
- }
17620
- if (stat.size > MAX_UPLOAD_SIZE) {
17621
- throw new Error(`File too large: ${stat.size} bytes (max ${MAX_UPLOAD_SIZE})`);
17622
- }
17623
- const base = normalizeBaseUrl(storageBaseUrl);
17624
- if (new URL(base).protocol === "http:") {
17625
- console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17626
- }
17627
- const url = `${base}/upload`;
17628
- const fileBuffer = fs3.readFileSync(resolvedPath);
17629
- const fileName = path3.basename(resolvedPath);
17630
- const ext = path3.extname(fileName).toLowerCase();
17631
- const mimeType = MIME_TYPES[ext] || "application/octet-stream";
17632
- const formData = new FormData;
17633
- formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
17634
- const headers = {
17635
- "access-token": apiKey
17636
- };
17637
- if (debug) {
17638
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17639
- console.error(`Debug: Storage base URL: ${base}`);
17640
- console.error(`Debug: POST URL: ${url}`);
17641
- console.error(`Debug: File: ${resolvedPath} (${stat.size} bytes, ${mimeType})`);
17642
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17643
- }
17644
- const response = await fetch(url, {
17645
- method: "POST",
17646
- headers,
17647
- body: formData
17648
- });
17649
- if (debug) {
17650
- console.error(`Debug: Response status: ${response.status}`);
17651
- console.error(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
17652
- }
17653
- const data = await response.text();
17654
- if (response.ok) {
17655
- try {
17656
- return JSON.parse(data);
17657
- } catch {
17658
- throw new Error(`Failed to parse upload response: ${data}`);
17659
- }
17660
- } else {
17661
- throw new Error(formatHttpError("Failed to upload file", response.status, data));
17662
- }
17663
- }
17664
- async function downloadFile(params) {
17665
- const { apiKey, storageBaseUrl, fileUrl, outputPath, debug } = params;
17666
- if (!apiKey) {
17667
- throw new Error("API key is required");
17668
- }
17669
- if (!storageBaseUrl) {
17670
- throw new Error("storageBaseUrl is required");
17671
- }
17672
- if (!fileUrl) {
17673
- throw new Error("fileUrl is required");
17674
- }
17675
- const base = normalizeBaseUrl(storageBaseUrl);
17676
- if (new URL(base).protocol === "http:") {
17677
- console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17678
- }
17679
- let fullUrl;
17680
- if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
17681
- if (!fileUrl.startsWith(base + "/")) {
17682
- throw new Error(`URL must be under storage base URL: ${base}`);
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;
17683
17539
  }
17684
- fullUrl = fileUrl;
17685
- } else {
17686
- const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17687
- fullUrl = `${base}${relativePath}`;
17688
- }
17689
- const urlFilename = path3.basename(new URL(fullUrl).pathname);
17690
- if (!urlFilename) {
17691
- throw new Error("Cannot derive filename from URL; please specify --output");
17692
- }
17693
- const saveTo = outputPath ? path3.resolve(outputPath) : path3.resolve(urlFilename);
17694
- if (!outputPath) {
17695
- const normalizedSave = path3.normalize(saveTo);
17696
- const cwd = path3.normalize(process.cwd());
17697
- if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path3.sep)) {
17698
- throw new Error("Derived output path escapes current directory; please specify --output");
17540
+ if (now() >= deadline) {
17541
+ return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
17699
17542
  }
17543
+ await sleep(pollIntervalMs);
17700
17544
  }
17701
- const headers = {
17702
- "access-token": apiKey
17703
- };
17704
- if (debug) {
17705
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17706
- console.error(`Debug: Storage base URL: ${base}`);
17707
- console.error(`Debug: GET URL: ${fullUrl}`);
17708
- console.error(`Debug: Output: ${saveTo}`);
17709
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
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);
17710
17564
  }
17711
- const response = await fetch(fullUrl, {
17712
- method: "GET",
17713
- headers
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
17714
17578
  });
17715
- if (debug) {
17716
- console.error(`Debug: Response status: ${response.status}`);
17717
- console.error(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
17718
- }
17719
- if (!response.ok) {
17720
- const data = await response.text();
17721
- throw new Error(formatHttpError("Failed to download file", response.status, data));
17722
- }
17723
- const contentLength = response.headers.get("content-length");
17724
- if (contentLength && parseInt(contentLength, 10) > MAX_DOWNLOAD_SIZE) {
17725
- throw new Error(`File too large: ${contentLength} bytes (max ${MAX_DOWNLOAD_SIZE})`);
17726
- }
17727
- const arrayBuffer = await response.arrayBuffer();
17728
- const buffer = Buffer.from(arrayBuffer);
17729
- const parentDir = path3.dirname(saveTo);
17730
- if (!fs3.existsSync(parentDir)) {
17731
- fs3.mkdirSync(parentDir, { recursive: true });
17579
+ if (outcome.sessionId) {
17580
+ writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
17732
17581
  }
17733
- fs3.writeFileSync(saveTo, buffer);
17734
- return {
17735
- savedTo: saveTo,
17736
- size: buffer.length,
17737
- mimeType: response.headers.get("content-type")
17738
- };
17582
+ return { ...outcome, command: params.command, projectId };
17739
17583
  }
17740
- var IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
17741
- function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
17742
- const base = normalizeBaseUrl(storageBaseUrl);
17743
- const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17744
- const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
17745
- const name = filename || path3.basename(new URL(fullUrl).pathname);
17746
- const safeName = name.replace(/[\[\]()]/g, "\\$&");
17747
- const ext = path3.extname(name).toLowerCase();
17748
- if (IMAGE_EXTENSIONS.has(ext)) {
17749
- return `![${safeName}](${fullUrl})`;
17750
- }
17751
- return `[${safeName}](${fullUrl})`;
17584
+
17585
+ // lib/dblab.ts
17586
+ function isNumericProjectRef2(ref) {
17587
+ return /^[0-9]+$/.test(ref.trim());
17752
17588
  }
17753
- async function uploadAttachments(params) {
17754
- const { apiKey, storageBaseUrl, attachmentPaths, debug } = params;
17755
- if (!attachmentPaths || attachmentPaths.length === 0) {
17756
- return [];
17589
+ async function resolveDblabInstanceId(params) {
17590
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
17591
+ if (!apiKey) {
17592
+ throw new Error("API key is required");
17757
17593
  }
17758
- const out = [];
17759
- for (const attachmentPath of attachmentPaths) {
17760
- const result = await uploadFile({
17761
- apiKey,
17762
- storageBaseUrl,
17763
- filePath: attachmentPath,
17764
- debug
17765
- });
17766
- const markdown = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
17767
- out.push({
17768
- path: attachmentPath,
17769
- url: result.url,
17770
- markdown,
17771
- metadata: result.metadata
17772
- });
17594
+ const ref = String(params.project ?? "").trim();
17595
+ if (!ref) {
17596
+ throw new Error("project is required (--project <id|alias>)");
17773
17597
  }
17774
- return out;
17775
- }
17776
- function appendAttachmentsToContent(content, attachments) {
17777
- if (!attachments || attachments.length === 0) {
17778
- return content;
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}`);
17779
17604
  }
17780
- const links = attachments.map((a) => a.markdown).join(`
17781
- `);
17782
- if (!content || !content.trim()) {
17783
- return links;
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.`);
17784
17615
  }
17785
- return `${content}
17786
-
17787
- ${links}`;
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);
17788
17620
  }
17789
-
17790
- // lib/joe.ts
17791
- import * as fs4 from "fs";
17792
- import * as path4 from "path";
17793
- import * as crypto from "node:crypto";
17794
- var JOE_COMMANDS = [
17795
- "plan",
17796
- "explain",
17797
- "exec",
17798
- "hypo",
17799
- "activity",
17800
- "terminate",
17801
- "reset",
17802
- "describe"
17803
- ];
17804
- var TERMINAL_STATES = new Set([
17805
- "done",
17806
- "error",
17807
- "timed_out"
17808
- ]);
17809
- var AI_ENABLED_COMMANDS = new Set([
17810
- "plan",
17811
- "explain",
17812
- "exec",
17813
- "hypo",
17814
- "activity",
17815
- "describe"
17816
- ]);
17817
- var EXECUTING_COMMANDS = new Set([
17818
- "exec",
17819
- "explain"
17820
- ]);
17821
- var DEFAULT_BUDGET_MS = 25000;
17822
- var DEFAULT_POLL_INTERVAL_MS = 800;
17823
- async function callRpc(params) {
17824
- const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
17621
+ async function callDblabApi(params) {
17622
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
17825
17623
  if (!apiKey) {
17826
17624
  throw new Error("API key is required");
17827
17625
  }
17626
+ if (!instanceId) {
17627
+ throw new Error("instanceId is required");
17628
+ }
17828
17629
  const base = normalizeBaseUrl(apiBaseUrl);
17829
- const url = new URL(`${base}/rpc/${fn}`);
17830
- const payload = JSON.stringify(body);
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);
17831
17640
  const headers = {
17832
17641
  "access-token": apiKey,
17833
17642
  "Content-Type": "application/json",
@@ -17837,267 +17646,438 @@ async function callRpc(params) {
17837
17646
  const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17838
17647
  console.error(`Debug: POST URL: ${url.toString()}`);
17839
17648
  console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17840
- console.error(`Debug: Request body: ${payload}`);
17649
+ console.error(`Debug: Request body: ${body}`);
17841
17650
  }
17842
- const response = await fetch(url.toString(), {
17843
- method: "POST",
17844
- headers,
17845
- body: payload
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
17846
17747
  });
17847
- const text = await response.text();
17848
- if (debug) {
17849
- console.error(`Debug: Response status: ${response.status}`);
17850
- console.error(`Debug: Response body: ${text}`);
17851
- }
17852
- if (!response.ok) {
17853
- throw new Error(formatHttpError(operation, response.status, text));
17854
- }
17855
- try {
17856
- return JSON.parse(text);
17857
- } catch {
17858
- throw new Error(`${operation}: failed to parse response: ${text}`);
17859
- }
17860
17748
  }
17861
- async function submitCommand(params) {
17862
- const { apiKey, apiBaseUrl, command, projectId, sql, args, sessionId, idempotencyKey, debug } = params;
17863
- if (!JOE_COMMANDS.includes(command)) {
17864
- throw new Error(`Unknown Joe command: ${command}`);
17865
- }
17866
- const body = {
17867
- project_id: projectId,
17868
- command,
17869
- sql: sql ?? null,
17870
- args: args ?? null,
17871
- session_id: sessionId ?? null
17872
- };
17873
- if (idempotencyKey) {
17874
- body.idempotency_key = idempotencyKey;
17875
- }
17876
- return callRpc({
17749
+ async function listBranches(params) {
17750
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
17751
+ return callDblabApi({
17877
17752
  apiKey,
17878
17753
  apiBaseUrl,
17879
- fn: "joe_command_submit",
17880
- body,
17881
- operation: `Failed to submit ${command} command`,
17754
+ instanceId,
17755
+ action: "/branches",
17756
+ method: "get",
17757
+ operation: "Failed to list branches",
17882
17758
  debug
17883
17759
  });
17884
17760
  }
17885
- async function getCommandStatus(params) {
17886
- const { apiKey, apiBaseUrl, commandId, debug } = params;
17887
- if (!commandId) {
17888
- throw new Error("commandId is required");
17889
- }
17890
- return callRpc({
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({
17891
17771
  apiKey,
17892
17772
  apiBaseUrl,
17893
- fn: "joe_command_status",
17894
- body: { command_id: commandId },
17895
- operation: "Failed to fetch command status",
17773
+ instanceId,
17774
+ action: "/branch",
17775
+ method: "post",
17776
+ data,
17777
+ operation: "Failed to create branch",
17896
17778
  debug
17897
17779
  });
17898
17780
  }
17899
- async function getCommandResult(params) {
17900
- const { apiKey, apiBaseUrl, commandId, debug } = params;
17901
- if (!commandId) {
17902
- throw new Error("commandId is required");
17903
- }
17904
- return callRpc({
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({
17905
17786
  apiKey,
17906
17787
  apiBaseUrl,
17907
- fn: "joe_command_result",
17908
- body: { command_id: commandId },
17909
- operation: "Failed to fetch command result",
17788
+ instanceId,
17789
+ action: `/branch/${encodeURIComponent(branchName)}`,
17790
+ method: "delete",
17791
+ operation: "Failed to delete branch",
17910
17792
  debug
17911
17793
  });
17912
17794
  }
17913
- function normalizeProjectRow(row) {
17914
- const projectId = row.project_id ?? row.id ?? 0;
17915
- return {
17916
- project_id: Number(projectId),
17917
- alias: row.alias ?? null,
17918
- name: row.name ?? null,
17919
- label: row.label ?? null,
17920
- joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
17921
- tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false)
17922
- };
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
+ });
17923
17808
  }
17924
- async function listProjects(params) {
17925
- const { apiKey, apiBaseUrl, orgId, debug } = params;
17926
- const body = {};
17927
- if (typeof orgId === "number") {
17928
- body.org_id = orgId;
17929
- }
17930
- const rows = await callRpc({
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({
17931
17819
  apiKey,
17932
17820
  apiBaseUrl,
17933
- fn: "projects_list",
17934
- body,
17935
- operation: "Failed to list projects",
17821
+ instanceId,
17822
+ action,
17823
+ method: "get",
17824
+ operation: "Failed to list snapshots",
17936
17825
  debug
17937
17826
  });
17938
- if (!Array.isArray(rows)) {
17939
- return [];
17940
- }
17941
- return rows.map(normalizeProjectRow);
17942
17827
  }
17943
- function isNumericProjectRef2(ref) {
17944
- return /^[0-9]+$/.test(ref.trim());
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";
17865
+ var MAX_UPLOAD_SIZE = 500 * 1024 * 1024;
17866
+ var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17867
+ var MIME_TYPES = {
17868
+ ".png": "image/png",
17869
+ ".jpg": "image/jpeg",
17870
+ ".jpeg": "image/jpeg",
17871
+ ".gif": "image/gif",
17872
+ ".webp": "image/webp",
17873
+ ".svg": "image/svg+xml",
17874
+ ".bmp": "image/bmp",
17875
+ ".ico": "image/x-icon",
17876
+ ".pdf": "application/pdf",
17877
+ ".json": "application/json",
17878
+ ".xml": "application/xml",
17879
+ ".zip": "application/zip",
17880
+ ".gz": "application/gzip",
17881
+ ".tar": "application/x-tar",
17882
+ ".csv": "text/csv",
17883
+ ".txt": "text/plain",
17884
+ ".log": "text/plain",
17885
+ ".sql": "application/sql",
17886
+ ".html": "text/html",
17887
+ ".css": "text/css",
17888
+ ".js": "application/javascript",
17889
+ ".ts": "application/typescript",
17890
+ ".md": "text/markdown",
17891
+ ".yaml": "application/x-yaml",
17892
+ ".yml": "application/x-yaml"
17893
+ };
17894
+ async function uploadFile(params) {
17895
+ const { apiKey, storageBaseUrl, filePath, debug } = params;
17896
+ if (!apiKey) {
17897
+ throw new Error("API key is required");
17898
+ }
17899
+ if (!storageBaseUrl) {
17900
+ throw new Error("storageBaseUrl is required");
17901
+ }
17902
+ if (!filePath) {
17903
+ throw new Error("filePath is required");
17904
+ }
17905
+ const resolvedPath = path4.resolve(filePath);
17906
+ if (!fs4.existsSync(resolvedPath)) {
17907
+ throw new Error(`File not found: ${resolvedPath}`);
17908
+ }
17909
+ const stat = fs4.statSync(resolvedPath);
17910
+ if (!stat.isFile()) {
17911
+ throw new Error(`Not a file: ${resolvedPath}`);
17912
+ }
17913
+ if (stat.size > MAX_UPLOAD_SIZE) {
17914
+ throw new Error(`File too large: ${stat.size} bytes (max ${MAX_UPLOAD_SIZE})`);
17915
+ }
17916
+ const base = normalizeBaseUrl(storageBaseUrl);
17917
+ if (new URL(base).protocol === "http:") {
17918
+ console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17919
+ }
17920
+ const url = `${base}/upload`;
17921
+ const fileBuffer = fs4.readFileSync(resolvedPath);
17922
+ const fileName = path4.basename(resolvedPath);
17923
+ const ext = path4.extname(fileName).toLowerCase();
17924
+ const mimeType = MIME_TYPES[ext] || "application/octet-stream";
17925
+ const formData = new FormData;
17926
+ formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
17927
+ const headers = {
17928
+ "access-token": apiKey
17929
+ };
17930
+ if (debug) {
17931
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17932
+ console.error(`Debug: Storage base URL: ${base}`);
17933
+ console.error(`Debug: POST URL: ${url}`);
17934
+ console.error(`Debug: File: ${resolvedPath} (${stat.size} bytes, ${mimeType})`);
17935
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17936
+ }
17937
+ const response = await fetch(url, {
17938
+ method: "POST",
17939
+ headers,
17940
+ body: formData
17941
+ });
17942
+ if (debug) {
17943
+ console.error(`Debug: Response status: ${response.status}`);
17944
+ console.error(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
17945
+ }
17946
+ const data = await response.text();
17947
+ if (response.ok) {
17948
+ try {
17949
+ return JSON.parse(data);
17950
+ } catch {
17951
+ throw new Error(`Failed to parse upload response: ${data}`);
17952
+ }
17953
+ } else {
17954
+ throw new Error(formatHttpError("Failed to upload file", response.status, data));
17955
+ }
17945
17956
  }
17946
- async function resolveProjectId(params) {
17947
- const ref = String(params.project ?? "").trim();
17948
- if (!ref) {
17949
- throw new Error("project is required (--project <id|alias>)");
17957
+ async function downloadFile(params) {
17958
+ const { apiKey, storageBaseUrl, fileUrl, outputPath, debug } = params;
17959
+ if (!apiKey) {
17960
+ throw new Error("API key is required");
17950
17961
  }
17951
- if (isNumericProjectRef2(ref)) {
17952
- return Number(ref);
17962
+ if (!storageBaseUrl) {
17963
+ throw new Error("storageBaseUrl is required");
17953
17964
  }
17954
- const projects = await listProjects({
17955
- apiKey: params.apiKey,
17956
- apiBaseUrl: params.apiBaseUrl,
17957
- orgId: params.orgId,
17958
- debug: params.debug
17959
- });
17960
- const needle = ref.toLowerCase();
17961
- const match = projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
17962
- if (!match) {
17963
- throw new Error(`Project not found for alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
17965
+ if (!fileUrl) {
17966
+ throw new Error("fileUrl is required");
17964
17967
  }
17965
- return match.project_id;
17966
- }
17967
- function sessionStorePath(dir) {
17968
- return path4.join(dir ?? getConfigDir2(), "joe-sessions.json");
17969
- }
17970
- function readSessionStore(dir) {
17971
- const file = sessionStorePath(dir);
17972
- if (!fs4.existsSync(file)) {
17973
- return {};
17968
+ const base = normalizeBaseUrl(storageBaseUrl);
17969
+ if (new URL(base).protocol === "http:") {
17970
+ console.error("Warning: storage URL uses HTTP — API key will be sent unencrypted");
17974
17971
  }
17975
- try {
17976
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
17977
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
17978
- return parsed;
17972
+ let fullUrl;
17973
+ if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
17974
+ if (!fileUrl.startsWith(base + "/")) {
17975
+ throw new Error(`URL must be under storage base URL: ${base}`);
17979
17976
  }
17980
- } catch {}
17981
- return {};
17982
- }
17983
- function readStoredSessionId(projectId, dir) {
17984
- const store = readSessionStore(dir);
17985
- const value = store[String(projectId)];
17986
- return typeof value === "string" && value.length > 0 ? value : null;
17987
- }
17988
- function writeStoredSessionId(projectId, sessionId, dir) {
17989
- const baseDir = dir ?? getConfigDir2();
17990
- if (!fs4.existsSync(baseDir)) {
17991
- fs4.mkdirSync(baseDir, { recursive: true, mode: 448 });
17977
+ fullUrl = fileUrl;
17978
+ } else {
17979
+ const relativePath = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
17980
+ fullUrl = `${base}${relativePath}`;
17992
17981
  }
17993
- const store = readSessionStore(dir);
17994
- store[String(projectId)] = sessionId;
17995
- fs4.writeFileSync(sessionStorePath(dir), JSON.stringify(store, null, 2) + `
17996
- `, { mode: 384 });
17997
- }
17998
- function clearStoredSessionId(projectId, dir) {
17999
- const file = sessionStorePath(dir);
18000
- if (!fs4.existsSync(file)) {
18001
- return;
17982
+ const urlFilename = path4.basename(new URL(fullUrl).pathname);
17983
+ if (!urlFilename) {
17984
+ throw new Error("Cannot derive filename from URL; please specify --output");
18002
17985
  }
18003
- const store = readSessionStore(dir);
18004
- if (String(projectId) in store) {
18005
- delete store[String(projectId)];
18006
- fs4.writeFileSync(file, JSON.stringify(store, null, 2) + `
18007
- `, { mode: 384 });
17986
+ const saveTo = outputPath ? path4.resolve(outputPath) : path4.resolve(urlFilename);
17987
+ if (!outputPath) {
17988
+ const normalizedSave = path4.normalize(saveTo);
17989
+ const cwd = path4.normalize(process.cwd());
17990
+ if (normalizedSave !== cwd && !normalizedSave.startsWith(cwd + path4.sep)) {
17991
+ throw new Error("Derived output path escapes current directory; please specify --output");
17992
+ }
17993
+ }
17994
+ const headers = {
17995
+ "access-token": apiKey
17996
+ };
17997
+ if (debug) {
17998
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17999
+ console.error(`Debug: Storage base URL: ${base}`);
18000
+ console.error(`Debug: GET URL: ${fullUrl}`);
18001
+ console.error(`Debug: Output: ${saveTo}`);
18002
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
18003
+ }
18004
+ const response = await fetch(fullUrl, {
18005
+ method: "GET",
18006
+ headers
18007
+ });
18008
+ if (debug) {
18009
+ console.error(`Debug: Response status: ${response.status}`);
18010
+ console.error(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
18011
+ }
18012
+ if (!response.ok) {
18013
+ const data = await response.text();
18014
+ throw new Error(formatHttpError("Failed to download file", response.status, data));
18015
+ }
18016
+ const contentLength = response.headers.get("content-length");
18017
+ if (contentLength && parseInt(contentLength, 10) > MAX_DOWNLOAD_SIZE) {
18018
+ throw new Error(`File too large: ${contentLength} bytes (max ${MAX_DOWNLOAD_SIZE})`);
18019
+ }
18020
+ const arrayBuffer = await response.arrayBuffer();
18021
+ const buffer = Buffer.from(arrayBuffer);
18022
+ const parentDir = path4.dirname(saveTo);
18023
+ if (!fs4.existsSync(parentDir)) {
18024
+ fs4.mkdirSync(parentDir, { recursive: true });
18008
18025
  }
18026
+ fs4.writeFileSync(saveTo, buffer);
18027
+ return {
18028
+ savedTo: saveTo,
18029
+ size: buffer.length,
18030
+ mimeType: response.headers.get("content-type")
18031
+ };
18009
18032
  }
18010
- function computeIdempotencyKey(command, projectId, sql, args) {
18011
- if (EXECUTING_COMMANDS.has(command)) {
18012
- const canonical = `${projectId}
18013
- ${command}
18014
- ${sql ?? ""}
18015
- ${JSON.stringify(args ?? null)}`;
18016
- return `joe-${crypto.createHash("sha256").update(canonical).digest("hex")}`;
18033
+ var IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
18034
+ function buildMarkdownLink(fileUrl, storageBaseUrl, filename) {
18035
+ const base = normalizeBaseUrl(storageBaseUrl);
18036
+ const normalizedFileUrl = fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`;
18037
+ const fullUrl = fileUrl.startsWith("http://") || fileUrl.startsWith("https://") ? fileUrl : `${base}${normalizedFileUrl}`;
18038
+ const name = filename || path4.basename(new URL(fullUrl).pathname);
18039
+ const safeName = name.replace(/[\[\]()]/g, "\\$&");
18040
+ const ext = path4.extname(name).toLowerCase();
18041
+ if (IMAGE_EXTENSIONS.has(ext)) {
18042
+ return `![${safeName}](${fullUrl})`;
18017
18043
  }
18018
- return `joe-${crypto.randomUUID()}`;
18044
+ return `[${safeName}](${fullUrl})`;
18019
18045
  }
18020
- var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
18021
- async function runCommand(params) {
18022
- const {
18023
- apiKey,
18024
- apiBaseUrl,
18025
- command,
18026
- projectId,
18027
- sql,
18028
- args,
18029
- sessionId,
18030
- debug
18031
- } = params;
18032
- const budgetMs = params.budgetMs ?? DEFAULT_BUDGET_MS;
18033
- const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
18034
- const now = params.now ?? Date.now;
18035
- const sleep = params.sleep ?? defaultSleep;
18036
- const idempotencyKey = params.idempotencyKey ?? computeIdempotencyKey(command, projectId, sql, args);
18037
- const submitted = await submitCommand({
18038
- apiKey,
18039
- apiBaseUrl,
18040
- command,
18041
- projectId,
18042
- sql,
18043
- args,
18044
- sessionId,
18045
- idempotencyKey,
18046
- debug
18047
- });
18048
- const commandId = submitted.command_id;
18049
- const newSessionId = submitted.session_id ?? sessionId ?? null;
18050
- const deadline = now() + budgetMs;
18051
- let status = submitted.status;
18052
- while (true) {
18053
- const statusResp = await getCommandStatus({ apiKey, apiBaseUrl, commandId, debug });
18054
- status = statusResp.status;
18055
- if (TERMINAL_STATES.has(status)) {
18056
- break;
18057
- }
18058
- if (now() >= deadline) {
18059
- return { commandId, sessionId: newSessionId, status, result: null, budgetExpired: true };
18060
- }
18061
- await sleep(pollIntervalMs);
18046
+ async function uploadAttachments(params) {
18047
+ const { apiKey, storageBaseUrl, attachmentPaths, debug } = params;
18048
+ if (!attachmentPaths || attachmentPaths.length === 0) {
18049
+ return [];
18062
18050
  }
18063
- const result = await getCommandResult({ apiKey, apiBaseUrl, commandId, debug });
18064
- return { commandId, sessionId: newSessionId, status, result, budgetExpired: false };
18051
+ const out = [];
18052
+ for (const attachmentPath of attachmentPaths) {
18053
+ const result = await uploadFile({
18054
+ apiKey,
18055
+ storageBaseUrl,
18056
+ filePath: attachmentPath,
18057
+ debug
18058
+ });
18059
+ const markdown = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
18060
+ out.push({
18061
+ path: attachmentPath,
18062
+ url: result.url,
18063
+ markdown,
18064
+ metadata: result.metadata
18065
+ });
18066
+ }
18067
+ return out;
18065
18068
  }
18066
- async function executeJoeCommand(params) {
18067
- const projectId = await resolveProjectId({
18068
- apiKey: params.apiKey,
18069
- apiBaseUrl: params.apiBaseUrl,
18070
- project: params.project,
18071
- orgId: params.orgId,
18072
- debug: params.debug
18073
- });
18074
- let sessionId;
18075
- if (params.newSession) {
18076
- clearStoredSessionId(projectId, params.sessionDir);
18077
- sessionId = null;
18078
- } else if (params.session) {
18079
- sessionId = String(params.session);
18080
- } else {
18081
- sessionId = readStoredSessionId(projectId, params.sessionDir);
18069
+ function appendAttachmentsToContent(content, attachments) {
18070
+ if (!attachments || attachments.length === 0) {
18071
+ return content;
18082
18072
  }
18083
- const outcome = await runCommand({
18084
- apiKey: params.apiKey,
18085
- apiBaseUrl: params.apiBaseUrl,
18086
- command: params.command,
18087
- projectId,
18088
- sql: params.sql,
18089
- args: params.args,
18090
- sessionId,
18091
- budgetMs: params.budgetMs,
18092
- pollIntervalMs: params.pollIntervalMs,
18093
- debug: params.debug,
18094
- now: params.now,
18095
- sleep: params.sleep
18096
- });
18097
- if (outcome.sessionId) {
18098
- writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
18073
+ const links = attachments.map((a) => a.markdown).join(`
18074
+ `);
18075
+ if (!content || !content.trim()) {
18076
+ return links;
18099
18077
  }
18100
- return { ...outcome, command: params.command, projectId };
18078
+ return `${content}
18079
+
18080
+ ${links}`;
18101
18081
  }
18102
18082
 
18103
18083
  // node_modules/zod/v4/core/core.js
@@ -27070,7 +27050,9 @@ function normalizeProjectRow2(row) {
27070
27050
  name: row.name ?? null,
27071
27051
  label: row.label ?? null,
27072
27052
  joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
27073
- tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? 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)
27074
27056
  };
27075
27057
  }
27076
27058
  async function listProjects2(params) {
@@ -27351,50 +27333,28 @@ async function resolveDblabInstanceId2(params) {
27351
27333
  if (!ref) {
27352
27334
  throw new Error("project is required (--project <id|alias>)");
27353
27335
  }
27354
- const base = normalizeBaseUrl(apiBaseUrl);
27355
- const url = new URL(`${base}/dblab_instances`);
27356
- url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
27357
- if (typeof orgId === "number") {
27358
- url.searchParams.set("org_id", `eq.${orgId}`);
27359
- }
27360
- const headers = {
27361
- "access-token": apiKey,
27362
- "Content-Type": "application/json",
27363
- Connection: "close"
27364
- };
27365
- if (debug) {
27366
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
27367
- console.error(`Debug: GET URL: ${url.toString()}`);
27368
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27369
- }
27370
- const response = await fetch(url.toString(), { method: "GET", headers });
27371
- const text = await response.text();
27372
- if (debug) {
27373
- console.error(`Debug: Response status: ${response.status}`);
27374
- console.error(`Debug: Response body: ${text}`);
27375
- }
27376
- if (!response.ok) {
27377
- throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
27378
- }
27379
- let rows;
27336
+ let projects;
27380
27337
  try {
27381
- const parsed = JSON.parse(text);
27382
- rows = Array.isArray(parsed) ? parsed : [];
27383
- } catch {
27384
- throw new Error(`Failed to parse DBLab instances response: ${text}`);
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}`);
27385
27342
  }
27386
27343
  const numeric = isNumericProjectRef4(ref);
27387
27344
  const needle = ref.toLowerCase();
27388
- const match = rows.find((r) => {
27345
+ const match = projects.find((p) => {
27389
27346
  if (numeric) {
27390
- return String(r.project_id ?? "") === ref;
27347
+ return String(p.project_id) === ref;
27391
27348
  }
27392
- return r.project_alias != null && String(r.project_alias).toLowerCase() === needle || r.project_name != null && String(r.project_name).toLowerCase() === needle || r.project_label_or_name != null && String(r.project_label_or_name).toLowerCase() === needle;
27349
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle || p.label !== null && p.label.toLowerCase() === needle;
27393
27350
  });
27394
27351
  if (!match) {
27395
27352
  throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
27396
27353
  }
27397
- return String(match.id);
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);
27398
27358
  }
27399
27359
  async function callDblabApi2(params) {
27400
27360
  const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;