postgresai 0.16.0-dev.3 → 0.16.0-dev.5

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.5",
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.5";
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,740 +17332,752 @@ 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({
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 {
17526
17505
  apiKey,
17527
17506
  apiBaseUrl,
17528
- instanceId,
17529
- action,
17530
- method: "get",
17531
- operation: "Failed to list snapshots",
17507
+ command,
17508
+ projectId,
17509
+ sql,
17510
+ args,
17511
+ sessionId,
17532
17512
  debug
17533
- });
17534
- }
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({
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({
17543
17520
  apiKey,
17544
17521
  apiBaseUrl,
17545
- instanceId,
17546
- action: "/branch/snapshot",
17547
- method: "post",
17548
- data,
17549
- operation: "Failed to create snapshot",
17522
+ command,
17523
+ projectId,
17524
+ sql,
17525
+ args,
17526
+ sessionId,
17527
+ idempotencyKey,
17550
17528
  debug
17551
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 };
17552
17547
  }
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({
17559
- apiKey,
17560
- apiBaseUrl,
17561
- instanceId,
17562
- action,
17563
- method: "delete",
17564
- operation: "Failed to destroy snapshot",
17565
- debug
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
17566
17578
  });
17579
+ if (outcome.sessionId) {
17580
+ writeStoredSessionId(projectId, outcome.sessionId, params.sessionDir);
17581
+ }
17582
+ return { ...outcome, command: params.command, projectId };
17567
17583
  }
17568
17584
 
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;
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;
17603
17591
  if (!apiKey) {
17604
17592
  throw new Error("API key is required");
17605
17593
  }
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)}`);
17594
+ const ref = String(params.project ?? "").trim();
17595
+ if (!ref) {
17596
+ throw new Error("project is required (--project <id|alias>)");
17643
17597
  }
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()))}`);
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}`);
17652
17604
  }
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}`);
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;
17659
17610
  }
17660
- } else {
17661
- throw new Error(formatHttpError("Failed to upload file", response.status, data));
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.`);
17662
17618
  }
17619
+ return String(match.dblab_instance_id);
17663
17620
  }
17664
- async function downloadFile(params) {
17665
- const { apiKey, storageBaseUrl, fileUrl, outputPath, debug } = params;
17621
+ async function callDblabApi(params) {
17622
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
17666
17623
  if (!apiKey) {
17667
17624
  throw new Error("API key is required");
17668
17625
  }
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}`);
17683
- }
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");
17626
+ if (!instanceId) {
17627
+ throw new Error("instanceId is required");
17692
17628
  }
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");
17699
- }
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;
17700
17638
  }
17639
+ const body = JSON.stringify(bodyObj);
17701
17640
  const headers = {
17702
- "access-token": apiKey
17641
+ "access-token": apiKey,
17642
+ "Content-Type": "application/json",
17643
+ Connection: "close"
17703
17644
  };
17704
17645
  if (debug) {
17705
17646
  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}`);
17647
+ console.error(`Debug: POST URL: ${url.toString()}`);
17709
17648
  console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17649
+ console.error(`Debug: Request body: ${body}`);
17710
17650
  }
17711
- const response = await fetch(fullUrl, {
17712
- method: "GET",
17713
- headers
17714
- });
17651
+ const response = await fetch(url.toString(), { method: "POST", headers, body });
17652
+ const text = await response.text();
17715
17653
  if (debug) {
17716
17654
  console.error(`Debug: Response status: ${response.status}`);
17717
- console.error(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
17655
+ console.error(`Debug: Response body: ${text}`);
17718
17656
  }
17719
17657
  if (!response.ok) {
17720
- const data = await response.text();
17721
- throw new Error(formatHttpError("Failed to download file", response.status, data));
17658
+ throw new Error(formatHttpError(operation, response.status, text));
17722
17659
  }
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})`);
17660
+ if (text.trim() === "") {
17661
+ return null;
17726
17662
  }
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 });
17663
+ try {
17664
+ return JSON.parse(text);
17665
+ } catch {
17666
+ throw new Error(`${operation}: failed to parse response: ${text}`);
17732
17667
  }
17733
- fs3.writeFileSync(saveTo, buffer);
17734
- return {
17735
- savedTo: saveTo,
17736
- size: buffer.length,
17737
- mimeType: response.headers.get("content-type")
17738
- };
17739
17668
  }
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})`;
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
+ });
17752
17690
  }
17753
- async function uploadAttachments(params) {
17754
- const { apiKey, storageBaseUrl, attachmentPaths, debug } = params;
17755
- if (!attachmentPaths || attachmentPaths.length === 0) {
17756
- return [];
17757
- }
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
- });
17773
- }
17774
- return out;
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
+ });
17775
17702
  }
17776
- function appendAttachmentsToContent(content, attachments) {
17777
- if (!attachments || attachments.length === 0) {
17778
- return content;
17779
- }
17780
- const links = attachments.map((a) => a.markdown).join(`
17781
- `);
17782
- if (!content || !content.trim()) {
17783
- return links;
17784
- }
17785
- return `${content}
17786
-
17787
- ${links}`;
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
+ });
17788
17716
  }
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;
17825
- if (!apiKey) {
17826
- throw new Error("API key is required");
17827
- }
17828
- const base = normalizeBaseUrl(apiBaseUrl);
17829
- const url = new URL(`${base}/rpc/${fn}`);
17830
- const payload = JSON.stringify(body);
17831
- const headers = {
17832
- "access-token": apiKey,
17833
- "Content-Type": "application/json",
17834
- Connection: "close"
17835
- };
17836
- if (debug) {
17837
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
17838
- console.error(`Debug: POST URL: ${url.toString()}`);
17839
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
17840
- console.error(`Debug: Request body: ${payload}`);
17841
- }
17842
- const response = await fetch(url.toString(), {
17843
- method: "POST",
17844
- headers,
17845
- body: payload
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/${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;
@@ -27625,7 +27585,7 @@ async function destroySnapshot2(params) {
27625
27585
  const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
27626
27586
  if (!snapshotId)
27627
27587
  throw new Error("snapshotId is required");
27628
- const action = `/snapshot/${encodeURIComponent(snapshotId)}?force=${Boolean(force)}`;
27588
+ const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
27629
27589
  return callDblabApi2({
27630
27590
  apiKey,
27631
27591
  apiBaseUrl,