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.
- package/dist/bin/postgres-ai.js +700 -740
- package/lib/dblab.ts +30 -59
- package/lib/joe.ts +9 -0
- package/package.json +1 -1
- package/test/dblab.cli.test.ts +10 -10
- package/test/dblab.test.ts +38 -31
- package/test/joe.test.ts +4 -4
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13425,7 +13425,7 @@ var {
|
|
|
13425
13425
|
// package.json
|
|
13426
13426
|
var package_default = {
|
|
13427
13427
|
name: "postgresai",
|
|
13428
|
-
version: "0.16.0-dev.
|
|
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.
|
|
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/
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
|
|
17302
|
-
|
|
17303
|
-
|
|
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
|
|
17338
|
-
const
|
|
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: ${
|
|
17320
|
+
console.error(`Debug: Request body: ${payload}`);
|
|
17357
17321
|
}
|
|
17358
|
-
const response = await fetch(url.toString(), {
|
|
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
|
|
17377
|
-
const { apiKey, apiBaseUrl,
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
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
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
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
|
|
17399
|
-
const { apiKey, apiBaseUrl,
|
|
17400
|
-
|
|
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
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
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
|
|
17411
|
-
const { apiKey, apiBaseUrl,
|
|
17412
|
-
if (!
|
|
17413
|
-
throw new Error("
|
|
17414
|
-
|
|
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
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
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
|
-
|
|
17425
|
-
const
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17435
|
-
|
|
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
|
|
17443
|
-
const { apiKey, apiBaseUrl,
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
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
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
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
|
-
|
|
17457
|
-
|
|
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
|
|
17469
|
-
const
|
|
17470
|
-
if (!
|
|
17471
|
-
throw new Error("
|
|
17472
|
-
|
|
17473
|
-
if (
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
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
|
-
|
|
17489
|
-
|
|
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
|
-
|
|
17503
|
-
const
|
|
17504
|
-
if (!
|
|
17505
|
-
|
|
17506
|
-
|
|
17507
|
-
|
|
17508
|
-
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
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
|
-
|
|
17517
|
-
const
|
|
17518
|
-
const
|
|
17519
|
-
|
|
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
|
-
|
|
17536
|
-
const
|
|
17537
|
-
if (!
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
|
|
17541
|
-
|
|
17542
|
-
|
|
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
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17507
|
+
command,
|
|
17508
|
+
projectId,
|
|
17509
|
+
sql,
|
|
17510
|
+
args,
|
|
17511
|
+
sessionId,
|
|
17550
17512
|
debug
|
|
17551
|
-
}
|
|
17552
|
-
|
|
17553
|
-
|
|
17554
|
-
const
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
const
|
|
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
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17522
|
+
command,
|
|
17523
|
+
projectId,
|
|
17524
|
+
sql,
|
|
17525
|
+
args,
|
|
17526
|
+
sessionId,
|
|
17527
|
+
idempotencyKey,
|
|
17565
17528
|
debug
|
|
17566
17529
|
});
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
|
|
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
|
-
|
|
17685
|
-
|
|
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
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
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
|
|
17712
|
-
|
|
17713
|
-
|
|
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 (
|
|
17716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
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 ``;
|
|
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
|
|
17754
|
-
const { apiKey,
|
|
17755
|
-
if (!
|
|
17756
|
-
|
|
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
|
|
17759
|
-
|
|
17760
|
-
|
|
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
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
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
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
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
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
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
|
-
|
|
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
|
|
17830
|
-
const
|
|
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: ${
|
|
17649
|
+
console.error(`Debug: Request body: ${body}`);
|
|
17841
17650
|
}
|
|
17842
|
-
const response = await fetch(url.toString(), {
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
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
|
|
17862
|
-
const { apiKey, apiBaseUrl,
|
|
17863
|
-
|
|
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
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17754
|
+
instanceId,
|
|
17755
|
+
action: "/branches",
|
|
17756
|
+
method: "get",
|
|
17757
|
+
operation: "Failed to list branches",
|
|
17882
17758
|
debug
|
|
17883
17759
|
});
|
|
17884
17760
|
}
|
|
17885
|
-
async function
|
|
17886
|
-
const { apiKey, apiBaseUrl,
|
|
17887
|
-
if (!
|
|
17888
|
-
throw new Error("
|
|
17889
|
-
}
|
|
17890
|
-
|
|
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
|
-
|
|
17894
|
-
|
|
17895
|
-
|
|
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
|
|
17900
|
-
const { apiKey, apiBaseUrl,
|
|
17901
|
-
if (!
|
|
17902
|
-
throw new Error("
|
|
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
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
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
|
|
17914
|
-
const
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
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
|
|
17925
|
-
const { apiKey, apiBaseUrl,
|
|
17926
|
-
const
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
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
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
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
|
|
17944
|
-
|
|
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
|
|
17947
|
-
const
|
|
17948
|
-
if (!
|
|
17949
|
-
throw new Error("
|
|
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 (
|
|
17952
|
-
|
|
17962
|
+
if (!storageBaseUrl) {
|
|
17963
|
+
throw new Error("storageBaseUrl is required");
|
|
17953
17964
|
}
|
|
17954
|
-
|
|
17955
|
-
|
|
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
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
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
|
-
|
|
17976
|
-
|
|
17977
|
-
if (
|
|
17978
|
-
|
|
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
|
-
|
|
17981
|
-
|
|
17982
|
-
}
|
|
17983
|
-
|
|
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
|
|
17994
|
-
|
|
17995
|
-
|
|
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
|
|
18004
|
-
if (
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
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
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
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 ``;
|
|
18017
18043
|
}
|
|
18018
|
-
return `
|
|
18044
|
+
return `[${safeName}](${fullUrl})`;
|
|
18019
18045
|
}
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
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
|
|
18064
|
-
|
|
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
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
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
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
throw new Error(`Failed to
|
|
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 =
|
|
27345
|
+
const match = projects.find((p) => {
|
|
27389
27346
|
if (numeric) {
|
|
27390
|
-
return String(
|
|
27347
|
+
return String(p.project_id) === ref;
|
|
27391
27348
|
}
|
|
27392
|
-
return
|
|
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
|
-
|
|
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;
|