postgresai 0.16.0-dev.5 → 0.16.0-dev.7

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.
@@ -5424,7 +5424,7 @@ interface JoeCliOpts {
5424
5424
  // ============================================================================
5425
5425
  // DBLab companion command surface (Joe API v2 · SPEC §8)
5426
5426
  //
5427
- // `pgai clone|branch|snapshot …` proxy the SAME Platform DBLab API the Console
5427
+ // `pgai dblab clone|branch|snapshot …` proxy the SAME Platform DBLab API the Console
5428
5428
  // already drives (v1.dblab_api_call). Every verb is `--project <id|alias>`
5429
5429
  // scoped: the project's single DBLab instance is resolved server-side listing,
5430
5430
  // then the verb is proxied. Destructive verbs (clone reset/destroy, branch
@@ -5465,7 +5465,7 @@ function printJoeOutcome(command: JoeCommand, outcome: ExecuteJoeOutcome, json:
5465
5465
  status: outcome.status,
5466
5466
  session_id: outcome.sessionId,
5467
5467
  budget_expired: true,
5468
- resume: `pgai result ${outcome.commandId}`,
5468
+ resume: `pgai joe result ${outcome.commandId}`,
5469
5469
  },
5470
5470
  null,
5471
5471
  2
@@ -5473,7 +5473,7 @@ function printJoeOutcome(command: JoeCommand, outcome: ExecuteJoeOutcome, json:
5473
5473
  );
5474
5474
  } else {
5475
5475
  console.log(
5476
- `submitted ${outcome.commandId} · ${outcome.status} · budget ${budgetSeconds}s reached — resume: pgai result ${outcome.commandId}`
5476
+ `submitted ${outcome.commandId} · ${outcome.status} · budget ${budgetSeconds}s reached — resume: pgai joe result ${outcome.commandId}`
5477
5477
  );
5478
5478
  }
5479
5479
  return;
@@ -5501,7 +5501,7 @@ function printJoeOutcome(command: JoeCommand, outcome: ExecuteJoeOutcome, json:
5501
5501
  // Server-side timed_out (the dispatcher gave up). The genuine reply may still
5502
5502
  // land; suggest a resume, but signal failure with a non-zero exit.
5503
5503
  console.error(
5504
- `command ${outcome.commandId} timed out on the server — resume: pgai result ${outcome.commandId}`
5504
+ `command ${outcome.commandId} timed out on the server — resume: pgai joe result ${outcome.commandId}`
5505
5505
  );
5506
5506
  process.exitCode = 1;
5507
5507
  }
@@ -5564,8 +5564,17 @@ function withJoeOptions(cmd: import("commander").Command): import("commander").C
5564
5564
  .option("--json", "output raw JSON");
5565
5565
  }
5566
5566
 
5567
+ // ---------------------------------------------------------------------------
5568
+ // Joe command group — every Joe verb lives under `pgai joe <verb>` (grouped CLI,
5569
+ // USER-AUTHORIZED restructure). The MCP tool names stay flat/unchanged; only the
5570
+ // CLI invocation path is grouped. `projects` stays top-level (org-level, SPEC §6).
5571
+ // ---------------------------------------------------------------------------
5572
+ const joe = program
5573
+ .command("joe")
5574
+ .description("Joe API v2 — plan/EXPLAIN/exec queries on ephemeral DBLab clones");
5575
+
5567
5576
  withJoeOptions(
5568
- program
5577
+ joe
5569
5578
  .command("plan <sql>")
5570
5579
  .description("plan a query (EXPLAIN, plan-only — no execution; the fast/safe default)")
5571
5580
  ).action(async (sql: string, opts: JoeCliOpts) => {
@@ -5573,7 +5582,7 @@ withJoeOptions(
5573
5582
  });
5574
5583
 
5575
5584
  withJoeOptions(
5576
- program
5585
+ joe
5577
5586
  .command("explain <sql>")
5578
5587
  .description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")
5579
5588
  ).action(async (sql: string, opts: JoeCliOpts) => {
@@ -5581,7 +5590,7 @@ withJoeOptions(
5581
5590
  });
5582
5591
 
5583
5592
  withJoeOptions(
5584
- program
5593
+ joe
5585
5594
  .command("exec <sql>")
5586
5595
  .description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")
5587
5596
  ).action(async (sql: string, opts: JoeCliOpts) => {
@@ -5589,7 +5598,7 @@ withJoeOptions(
5589
5598
  });
5590
5599
 
5591
5600
  withJoeOptions(
5592
- program
5601
+ joe
5593
5602
  .command("hypo <indexSql>")
5594
5603
  .description("HypoPG hypothetical index + re-plan (no real index, no data change)")
5595
5604
  .requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")
@@ -5598,7 +5607,7 @@ withJoeOptions(
5598
5607
  });
5599
5608
 
5600
5609
  withJoeOptions(
5601
- program
5610
+ joe
5602
5611
  .command("activity")
5603
5612
  .description("running-activity snapshot (pg_stat_activity; query text redacted)")
5604
5613
  ).action(async (opts: JoeCliOpts) => {
@@ -5606,7 +5615,7 @@ withJoeOptions(
5606
5615
  });
5607
5616
 
5608
5617
  withJoeOptions(
5609
- program
5618
+ joe
5610
5619
  .command("terminate <pid>")
5611
5620
  .description("pg_terminate_backend(pid) on the clone")
5612
5621
  ).action(async (pid: string, opts: JoeCliOpts) => {
@@ -5620,7 +5629,7 @@ withJoeOptions(
5620
5629
  });
5621
5630
 
5622
5631
  withJoeOptions(
5623
- program
5632
+ joe
5624
5633
  .command("reset")
5625
5634
  .description("reset/recreate the session's thin clone")
5626
5635
  ).action(async (opts: JoeCliOpts) => {
@@ -5628,7 +5637,7 @@ withJoeOptions(
5628
5637
  });
5629
5638
 
5630
5639
  withJoeOptions(
5631
- program
5640
+ joe
5632
5641
  .command("describe <object>")
5633
5642
  .description("\\d-family schema/relation/index metadata")
5634
5643
  .option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")
@@ -5699,9 +5708,18 @@ async function resolveDblabTarget(
5699
5708
  return { apiKey, apiBaseUrl, orgId, instanceId };
5700
5709
  }
5701
5710
 
5711
+ // ---------------------------------------------------------------------------
5712
+ // DBLab command group — every DBLab verb lives under
5713
+ // `pgai dblab <clone|branch|snapshot> …` (grouped CLI, USER-AUTHORIZED restructure).
5714
+ // MCP tool names (clone_create/…) stay flat/unchanged; only the CLI path is grouped.
5715
+ // ---------------------------------------------------------------------------
5716
+ const dblab = program
5717
+ .command("dblab")
5718
+ .description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
5719
+
5702
5720
  // ---- clone ----------------------------------------------------------------
5703
5721
 
5704
- const clone = program.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
5722
+ const clone = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
5705
5723
 
5706
5724
  clone
5707
5725
  .command("create")
@@ -5735,8 +5753,9 @@ clone
5735
5753
  }
5736
5754
  });
5737
5755
 
5738
- // Resume / inspect a previously submitted command by id.
5739
- program
5756
+ // Resume / inspect a previously submitted command by id. Part of the `joe` group
5757
+ // (`pgai joe status <commandId>`) — distinct from `pgai dblab clone status <cloneId>`.
5758
+ joe
5740
5759
  .command("status <commandId>")
5741
5760
  .description("show a Joe command's status (metadata only)")
5742
5761
  .option("--debug", "enable debug output")
@@ -5782,7 +5801,7 @@ clone
5782
5801
  }
5783
5802
  });
5784
5803
 
5785
- program
5804
+ joe
5786
5805
  .command("result <commandId>")
5787
5806
  .description("fetch a Joe command's result by id (resume a timed-out one-shot)")
5788
5807
  .option("--debug", "enable debug output")
@@ -5881,7 +5900,7 @@ clone
5881
5900
 
5882
5901
  // ---- branch ---------------------------------------------------------------
5883
5902
 
5884
- const branch = program.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
5903
+ const branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
5885
5904
 
5886
5905
  branch
5887
5906
  .command("list")
@@ -5961,7 +5980,7 @@ branch
5961
5980
 
5962
5981
  // ---- snapshot -------------------------------------------------------------
5963
5982
 
5964
- const snapshot = program.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
5983
+ const snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
5965
5984
 
5966
5985
  snapshot
5967
5986
  .command("list")
@@ -13331,33 +13331,38 @@ function isHtmlContent2(text) {
13331
13331
  const trimmed = text.trim();
13332
13332
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
13333
13333
  }
13334
- function formatHttpError2(operation, status, responseBody) {
13335
- const statusMessage = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13336
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
13334
+ function formatHttpError2(operation, status, responseBody, statusText) {
13335
+ const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13337
13336
  const remediation = status === 401 ? `
13338
13337
  ${AUTH_REMEDIATION_HINT2}` : "";
13339
- if (responseBody) {
13340
- if (isHtmlContent2(responseBody)) {
13341
- return errMsg + remediation;
13342
- }
13338
+ let bodyMessage;
13339
+ let bodyDetails;
13340
+ if (responseBody && !isHtmlContent2(responseBody)) {
13343
13341
  try {
13344
13342
  const errObj = JSON.parse(responseBody);
13345
- const message = errObj.message || errObj.error || errObj.detail;
13346
- if (message && typeof message === "string") {
13347
- errMsg += `
13348
- ${message}`;
13349
- } else {
13350
- errMsg += `
13351
- ${JSON.stringify(errObj, null, 2)}`;
13343
+ const message = errObj.message ?? errObj.error;
13344
+ if (typeof message === "string" && message.trim().length > 0) {
13345
+ bodyMessage = message.trim();
13346
+ }
13347
+ const details = errObj.details ?? errObj.detail;
13348
+ if (typeof details === "string" && details.trim().length > 0) {
13349
+ bodyDetails = details.trim();
13352
13350
  }
13353
13351
  } catch {
13354
13352
  const trimmed = responseBody.trim();
13355
13353
  if (trimmed.length > 0 && trimmed.length < 500) {
13356
- errMsg += `
13357
- ${trimmed}`;
13354
+ bodyDetails = trimmed;
13358
13355
  }
13359
13356
  }
13360
13357
  }
13358
+ const trimmedReason = statusText?.trim();
13359
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
13360
+ const headline = bodyMessage ?? reasonPhrase ?? generic;
13361
+ let errMsg = `${operation}: HTTP ${status} - ${headline}`;
13362
+ if (bodyDetails && bodyDetails !== headline) {
13363
+ errMsg += `
13364
+ ${bodyDetails}`;
13365
+ }
13361
13366
  return errMsg + remediation;
13362
13367
  }
13363
13368
  function maskSecret2(secret) {
@@ -13391,7 +13396,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
13391
13396
  storageBaseUrl: normalizeBaseUrl2(storageCandidate)
13392
13397
  };
13393
13398
  }
13394
- var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
13399
+ var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2;
13395
13400
  var init_util = __esm(() => {
13396
13401
  HTTP_STATUS_MESSAGES2 = {
13397
13402
  400: "Bad Request",
@@ -13405,6 +13410,20 @@ var init_util = __esm(() => {
13405
13410
  503: "Service Unavailable - server temporarily unavailable",
13406
13411
  504: "Gateway Timeout - server temporarily unavailable"
13407
13412
  };
13413
+ STANDARD_REASON_PHRASES2 = {
13414
+ 400: "Bad Request",
13415
+ 401: "Unauthorized",
13416
+ 403: "Forbidden",
13417
+ 404: "Not Found",
13418
+ 408: "Request Timeout",
13419
+ 409: "Conflict",
13420
+ 413: "Payload Too Large",
13421
+ 429: "Too Many Requests",
13422
+ 500: "Internal Server Error",
13423
+ 502: "Bad Gateway",
13424
+ 503: "Service Unavailable",
13425
+ 504: "Gateway Timeout"
13426
+ };
13408
13427
  });
13409
13428
 
13410
13429
  // node_modules/commander/esm.mjs
@@ -13425,7 +13444,7 @@ var {
13425
13444
  // package.json
13426
13445
  var package_default = {
13427
13446
  name: "postgresai",
13428
- version: "0.16.0-dev.5",
13447
+ version: "0.16.0-dev.7",
13429
13448
  description: "postgres_ai CLI",
13430
13449
  license: "Apache-2.0",
13431
13450
  private: false,
@@ -16256,7 +16275,7 @@ var Result = import_lib.default.Result;
16256
16275
  var TypeOverrides = import_lib.default.TypeOverrides;
16257
16276
  var defaults = import_lib.default.defaults;
16258
16277
  // package.json
16259
- var version = "0.16.0-dev.5";
16278
+ var version = "0.16.0-dev.7";
16260
16279
  var package_default2 = {
16261
16280
  name: "postgresai",
16262
16281
  version,
@@ -16398,33 +16417,52 @@ function isHtmlContent(text) {
16398
16417
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
16399
16418
  }
16400
16419
  var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
16401
- function formatHttpError(operation, status, responseBody) {
16402
- const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
16403
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
16420
+ var STANDARD_REASON_PHRASES = {
16421
+ 400: "Bad Request",
16422
+ 401: "Unauthorized",
16423
+ 403: "Forbidden",
16424
+ 404: "Not Found",
16425
+ 408: "Request Timeout",
16426
+ 409: "Conflict",
16427
+ 413: "Payload Too Large",
16428
+ 429: "Too Many Requests",
16429
+ 500: "Internal Server Error",
16430
+ 502: "Bad Gateway",
16431
+ 503: "Service Unavailable",
16432
+ 504: "Gateway Timeout"
16433
+ };
16434
+ function formatHttpError(operation, status, responseBody, statusText) {
16435
+ const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
16404
16436
  const remediation = status === 401 ? `
16405
16437
  ${AUTH_REMEDIATION_HINT}` : "";
16406
- if (responseBody) {
16407
- if (isHtmlContent(responseBody)) {
16408
- return errMsg + remediation;
16409
- }
16438
+ let bodyMessage;
16439
+ let bodyDetails;
16440
+ if (responseBody && !isHtmlContent(responseBody)) {
16410
16441
  try {
16411
16442
  const errObj = JSON.parse(responseBody);
16412
- const message = errObj.message || errObj.error || errObj.detail;
16413
- if (message && typeof message === "string") {
16414
- errMsg += `
16415
- ${message}`;
16416
- } else {
16417
- errMsg += `
16418
- ${JSON.stringify(errObj, null, 2)}`;
16443
+ const message = errObj.message ?? errObj.error;
16444
+ if (typeof message === "string" && message.trim().length > 0) {
16445
+ bodyMessage = message.trim();
16446
+ }
16447
+ const details = errObj.details ?? errObj.detail;
16448
+ if (typeof details === "string" && details.trim().length > 0) {
16449
+ bodyDetails = details.trim();
16419
16450
  }
16420
16451
  } catch {
16421
16452
  const trimmed = responseBody.trim();
16422
16453
  if (trimmed.length > 0 && trimmed.length < 500) {
16423
- errMsg += `
16424
- ${trimmed}`;
16454
+ bodyDetails = trimmed;
16425
16455
  }
16426
16456
  }
16427
16457
  }
16458
+ const trimmedReason = statusText?.trim();
16459
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
16460
+ const headline = bodyMessage ?? reasonPhrase ?? generic;
16461
+ let errMsg = `${operation}: HTTP ${status} - ${headline}`;
16462
+ if (bodyDetails && bodyDetails !== headline) {
16463
+ errMsg += `
16464
+ ${bodyDetails}`;
16465
+ }
16428
16466
  return errMsg + remediation;
16429
16467
  }
16430
16468
  function maskSecret(secret) {
@@ -17330,7 +17368,7 @@ async function callRpc(params) {
17330
17368
  console.error(`Debug: Response body: ${text}`);
17331
17369
  }
17332
17370
  if (!response.ok) {
17333
- throw new Error(formatHttpError(operation, response.status, text));
17371
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
17334
17372
  }
17335
17373
  try {
17336
17374
  return JSON.parse(text);
@@ -17655,7 +17693,7 @@ async function callDblabApi(params) {
17655
17693
  console.error(`Debug: Response body: ${text}`);
17656
17694
  }
17657
17695
  if (!response.ok) {
17658
- throw new Error(formatHttpError(operation, response.status, text));
17696
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
17659
17697
  }
17660
17698
  if (text.trim() === "") {
17661
17699
  return null;
@@ -25145,7 +25183,7 @@ async function runJoeTool(params) {
25145
25183
  session_id: outcome.sessionId,
25146
25184
  status: outcome.status,
25147
25185
  budget_expired: outcome.budgetExpired,
25148
- resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
25186
+ resume: outcome.budgetExpired ? `pgai joe result ${outcome.commandId}` : undefined,
25149
25187
  result: outcome.result
25150
25188
  };
25151
25189
  const isError = outcome.status === "error" || outcome.status === "timed_out";
@@ -26982,7 +27020,7 @@ async function callRpc2(params) {
26982
27020
  console.error(`Debug: Response body: ${text}`);
26983
27021
  }
26984
27022
  if (!response.ok) {
26985
- throw new Error(formatHttpError(operation, response.status, text));
27023
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
26986
27024
  }
26987
27025
  try {
26988
27026
  return JSON.parse(text);
@@ -27393,7 +27431,7 @@ async function callDblabApi2(params) {
27393
27431
  console.error(`Debug: Response body: ${text}`);
27394
27432
  }
27395
27433
  if (!response.ok) {
27396
- throw new Error(formatHttpError(operation, response.status, text));
27434
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
27397
27435
  }
27398
27436
  if (text.trim() === "") {
27399
27437
  return null;
@@ -39715,10 +39753,10 @@ function printJoeOutcome(command, outcome, json3) {
39715
39753
  status: outcome.status,
39716
39754
  session_id: outcome.sessionId,
39717
39755
  budget_expired: true,
39718
- resume: `pgai result ${outcome.commandId}`
39756
+ resume: `pgai joe result ${outcome.commandId}`
39719
39757
  }, null, 2));
39720
39758
  } else {
39721
- console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai result ${outcome.commandId}`);
39759
+ console.log(`submitted ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
39722
39760
  }
39723
39761
  return;
39724
39762
  }
@@ -39740,7 +39778,7 @@ function printJoeOutcome(command, outcome, json3) {
39740
39778
  process.exitCode = 1;
39741
39779
  return;
39742
39780
  }
39743
- console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai result ${outcome.commandId}`);
39781
+ console.error(`command ${outcome.commandId} timed out on the server \u2014 resume: pgai joe result ${outcome.commandId}`);
39744
39782
  process.exitCode = 1;
39745
39783
  }
39746
39784
  async function runJoeCli(command, sql, args, opts) {
@@ -39784,22 +39822,23 @@ async function runJoeCli(command, sql, args, opts) {
39784
39822
  function withJoeOptions(cmd) {
39785
39823
  return cmd.option("--project <id|alias>", "target project by numeric id OR alias/name").option("--session <id>", "run on a specific session's clone").option("--new-session", "start a fresh session/clone (null session)").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseInt(v, 10)).option("--debug", "enable debug output").option("--json", "output raw JSON");
39786
39824
  }
39787
- withJoeOptions(program2.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
39825
+ var joe = program2.command("joe").description("Joe API v2 \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones");
39826
+ withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
39788
39827
  await runJoeCli("plan", sql, null, opts);
39789
39828
  });
39790
- withJoeOptions(program2.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
39829
+ withJoeOptions(joe.command("explain <sql>").description("EXPLAIN ANALYZE a query (EXECUTES on the hardened, ephemeral clone)")).action(async (sql, opts) => {
39791
39830
  await runJoeCli("explain", sql, null, opts);
39792
39831
  });
39793
- withJoeOptions(program2.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
39832
+ withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
39794
39833
  await runJoeCli("exec", sql, null, opts);
39795
39834
  });
39796
- withJoeOptions(program2.command("hypo <indexSql>").description("HypoPG hypothetical index + re-plan (no real index, no data change)").requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")).action(async (indexSql, opts) => {
39835
+ withJoeOptions(joe.command("hypo <indexSql>").description("HypoPG hypothetical index + re-plan (no real index, no data change)").requiredOption("--query <sql>", "target query the hypothetical index is evaluated against")).action(async (indexSql, opts) => {
39797
39836
  await runJoeCli("hypo", indexSql, { query: opts.query }, opts);
39798
39837
  });
39799
- withJoeOptions(program2.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
39838
+ withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity; query text redacted)")).action(async (opts) => {
39800
39839
  await runJoeCli("activity", null, null, opts);
39801
39840
  });
39802
- withJoeOptions(program2.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
39841
+ withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
39803
39842
  const pidNum = parseInt(pid, 10);
39804
39843
  if (Number.isNaN(pidNum)) {
39805
39844
  console.error("pid must be a number");
@@ -39808,10 +39847,10 @@ withJoeOptions(program2.command("terminate <pid>").description("pg_terminate_bac
39808
39847
  }
39809
39848
  await runJoeCli("terminate", null, { pid: pidNum }, opts);
39810
39849
  });
39811
- withJoeOptions(program2.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
39850
+ withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
39812
39851
  await runJoeCli("reset", null, null, opts);
39813
39852
  });
39814
- withJoeOptions(program2.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
39853
+ withJoeOptions(joe.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
39815
39854
  const args = { object: object4 };
39816
39855
  if (opts.variant)
39817
39856
  args.variant = opts.variant;
@@ -39861,7 +39900,8 @@ async function resolveDblabTarget(project, debug) {
39861
39900
  const instanceId = await resolveDblabInstanceId2({ apiKey, apiBaseUrl, project: ref, orgId, debug });
39862
39901
  return { apiKey, apiBaseUrl, orgId, instanceId };
39863
39902
  }
39864
- var clone2 = program2.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
39903
+ var dblab = program2.command("dblab").description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
39904
+ var clone2 = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
39865
39905
  clone2.command("create").description("create a thin clone of the project's database").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "branch to clone from").option("--snapshot <id>", "snapshot id to clone from").option("--id <id>", "clone id (DBLab generates one when omitted)").option("--db-user <user>", "clone DB user").option("--db-password <password>", "clone DB password").option("--protected", "protect the clone from auto-deletion").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
39866
39906
  try {
39867
39907
  const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
@@ -39883,7 +39923,7 @@ clone2.command("create").description("create a thin clone of the project's datab
39883
39923
  process.exitCode = 1;
39884
39924
  }
39885
39925
  });
39886
- program2.command("status <commandId>").description("show a Joe command's status (metadata only)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
39926
+ joe.command("status <commandId>").description("show a Joe command's status (metadata only)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
39887
39927
  try {
39888
39928
  const rootOpts = program2.opts();
39889
39929
  const cfg = readConfig();
@@ -39916,7 +39956,7 @@ clone2.command("list").description("list the project's thin clones").requiredOpt
39916
39956
  process.exitCode = 1;
39917
39957
  }
39918
39958
  });
39919
- program2.command("result <commandId>").description("fetch a Joe command's result by id (resume a timed-out one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
39959
+ joe.command("result <commandId>").description("fetch a Joe command's result by id (resume a timed-out one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
39920
39960
  try {
39921
39961
  const rootOpts = program2.opts();
39922
39962
  const cfg = readConfig();
@@ -39988,7 +40028,7 @@ clone2.command("destroy <cloneId>").description("destroy a clone (requires joe:a
39988
40028
  process.exitCode = 1;
39989
40029
  }
39990
40030
  });
39991
- var branch = program2.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
40031
+ var branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
39992
40032
  branch.command("list").description("list the project's branches").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
39993
40033
  try {
39994
40034
  const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
@@ -40037,7 +40077,7 @@ branch.command("log <name>").description("show a branch's snapshot log").require
40037
40077
  process.exitCode = 1;
40038
40078
  }
40039
40079
  });
40040
- var snapshot = program2.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
40080
+ var snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
40041
40081
  snapshot.command("list").description("list the project's snapshots").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "filter by branch").option("--dataset <dataset>", "filter by dataset").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40042
40082
  try {
40043
40083
  const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
package/lib/dblab.ts CHANGED
@@ -196,8 +196,10 @@ async function callDblabApi<T>(params: DblabApiCallParams): Promise<T> {
196
196
 
197
197
  if (!response.ok) {
198
198
  // PostgREST maps custom `PTxyz` sqlstates to HTTP status `xyz`, so a missing
199
- // `joe:admin` scope on a destructive verb surfaces here as HTTP 403.
200
- throw new Error(formatHttpError(operation, response.status, text));
199
+ // `joe:admin` scope on a destructive verb surfaces here as HTTP 403. The
200
+ // RPC's user-facing message rides in the HTTP reason phrase (statusText),
201
+ // not the JSON body — pass it through.
202
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
201
203
  }
202
204
 
203
205
  // Some DBLab actions (reset/destroy) reply with an empty body on success.
package/lib/joe.ts CHANGED
@@ -177,9 +177,10 @@ async function callRpc<T>(params: RpcCallParams): Promise<T> {
177
177
 
178
178
  if (!response.ok) {
179
179
  // PostgREST maps a custom `PTxyz` sqlstate to HTTP status `xyz`, so PT403 →
180
- // HTTP 403, PT404 → 404, PT429 → 429, etc. formatHttpError surfaces the JSON
181
- // `message` (e.g. the "AI features disabled for this org" text for AC36).
182
- throw new Error(formatHttpError(operation, response.status, text));
180
+ // HTTP 403, PT404 → 404, PT429 → 429, etc. The RPC's user-facing message
181
+ // (e.g. the "AI features disabled for this org" text for AC36) rides in the
182
+ // HTTP reason phrase (statusText), not the JSON body — pass it through.
183
+ throw new Error(formatHttpError(operation, response.status, text, response.statusText));
183
184
  }
184
185
 
185
186
  try {
@@ -496,7 +497,7 @@ const defaultSleep = (ms: number): Promise<void> => new Promise((r) => setTimeou
496
497
  /**
497
498
  * Submit a command then client-side poll status/result within the one-shot budget.
498
499
  * On a terminal state returns the result; on budget expiry returns a resume handle
499
- * (`budgetExpired: true`) so the caller can `pgai result <command_id>` later.
500
+ * (`budgetExpired: true`) so the caller can `pgai joe result <command_id>` later.
500
501
  */
501
502
  export async function runCommand(params: RunCommandParams): Promise<RunCommandOutcome> {
502
503
  const {
package/lib/mcp-server.ts CHANGED
@@ -284,7 +284,7 @@ async function runJoeTool(params: RunJoeToolParams): Promise<McpToolResponse> {
284
284
  session_id: outcome.sessionId,
285
285
  status: outcome.status,
286
286
  budget_expired: outcome.budgetExpired,
287
- resume: outcome.budgetExpired ? `pgai result ${outcome.commandId}` : undefined,
287
+ resume: outcome.budgetExpired ? `pgai joe result ${outcome.commandId}` : undefined,
288
288
  result: outcome.result,
289
289
  };
290
290
  const isError = outcome.status === "error" || outcome.status === "timed_out";
@@ -648,7 +648,7 @@ export async function handleToolCall(
648
648
  }
649
649
 
650
650
  // ---- DBLab companion tools (SPEC §8) --------------------------------
651
- // Each mirrors the CLI `pgai clone|branch|snapshot …` verb 1:1: resolve the
651
+ // Each mirrors the CLI `pgai dblab clone|branch|snapshot …` verb 1:1: resolve the
652
652
  // project's single DBLab instance, then proxy the existing Platform DBLab
653
653
  // API (v1.dblab_api_call). Destructive verbs are `joe:admin`-gated backend
654
654
  // side (a PT403 surfaces as an isError response via the outer catch).
@@ -1058,7 +1058,7 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
1058
1058
  },
1059
1059
  ...joeToolDefinitions(),
1060
1060
  // DBLab companion tools (SPEC §8) — proxy the existing Platform DBLab API
1061
- // (v1.dblab_api_call) the Console drives; mirror `pgai clone|branch|snapshot`.
1061
+ // (v1.dblab_api_call) the Console drives; mirror `pgai dblab clone|branch|snapshot`.
1062
1062
  // `project` (id or alias) resolves the project's single DBLab instance.
1063
1063
  {
1064
1064
  name: "clone_create",
package/lib/util.ts CHANGED
@@ -28,42 +28,91 @@ function isHtmlContent(text: string): boolean {
28
28
  */
29
29
  const AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
30
30
 
31
+ /**
32
+ * Standard HTTP reason phrases we should NOT treat as a server-authored
33
+ * message: when the reason phrase equals the stock text for the status, it
34
+ * carries no extra information, so we prefer the friendlier generic label.
35
+ */
36
+ const STANDARD_REASON_PHRASES: Record<number, string> = {
37
+ 400: "Bad Request",
38
+ 401: "Unauthorized",
39
+ 403: "Forbidden",
40
+ 404: "Not Found",
41
+ 408: "Request Timeout",
42
+ 409: "Conflict",
43
+ 413: "Payload Too Large",
44
+ 429: "Too Many Requests",
45
+ 500: "Internal Server Error",
46
+ 502: "Bad Gateway",
47
+ 503: "Service Unavailable",
48
+ 504: "Gateway Timeout",
49
+ };
50
+
31
51
  /**
32
52
  * Format an HTTP error response into a clean, developer-friendly message.
33
53
  * Handles HTML error pages (e.g., from Cloudflare) by showing just the status code and message.
34
54
  * For 401 responses, appends a remediation hint pointing at `postgresai auth`.
55
+ *
56
+ * The platform's PostgREST layer uses the `PTxyz` custom-status convention:
57
+ * a raised `PT403`/`PT404`/… maps to the HTTP status and delivers the RPC's
58
+ * user-facing message in the HTTP **reason phrase** (`response.statusText`),
59
+ * NOT the JSON body — the body carries only `hint`/`details` (no `message`).
60
+ * So callers pass `statusText` and it is preferred over the built-in generic
61
+ * label. Headline precedence: JSON body `message` → custom reason phrase →
62
+ * generic label; the JSON `details` (plural, PostgREST's spelling) is shown as
63
+ * a supplementary line.
35
64
  */
36
- export function formatHttpError(operation: string, status: number, responseBody?: string): string {
37
- const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
38
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
65
+ export function formatHttpError(
66
+ operation: string,
67
+ status: number,
68
+ responseBody?: string,
69
+ statusText?: string
70
+ ): string {
71
+ const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
39
72
  const remediation = status === 401 ? `\n${AUTH_REMEDIATION_HINT}` : "";
40
73
 
41
- if (responseBody) {
42
- // If it's HTML (like Cloudflare error pages), don't dump the raw HTML
43
- if (isHtmlContent(responseBody)) {
44
- // Just use the status message, don't append HTML
45
- return errMsg + remediation;
46
- }
74
+ let bodyMessage: string | undefined;
75
+ let bodyDetails: string | undefined;
47
76
 
48
- // Try to parse as JSON for structured error info
77
+ if (responseBody && !isHtmlContent(responseBody)) {
78
+ // If it's HTML (like Cloudflare error pages), we fall through with no
79
+ // parsed fields and never dump the raw HTML.
49
80
  try {
50
81
  const errObj = JSON.parse(responseBody);
51
- // Extract common error message fields
52
- const message = errObj.message || errObj.error || errObj.detail;
53
- if (message && typeof message === "string") {
54
- errMsg += `\n${message}`;
55
- } else {
56
- errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
82
+ const message = errObj.message ?? errObj.error;
83
+ if (typeof message === "string" && message.trim().length > 0) {
84
+ bodyMessage = message.trim();
85
+ }
86
+ // PostgREST spells it `details` (plural); accept `detail` too.
87
+ const details = errObj.details ?? errObj.detail;
88
+ if (typeof details === "string" && details.trim().length > 0) {
89
+ bodyDetails = details.trim();
57
90
  }
58
91
  } catch {
59
- // Plain text error - append it if it's short and useful
92
+ // Plain text error - treat it as the details line if short and useful.
60
93
  const trimmed = responseBody.trim();
61
94
  if (trimmed.length > 0 && trimmed.length < 500) {
62
- errMsg += `\n${trimmed}`;
95
+ bodyDetails = trimmed;
63
96
  }
64
97
  }
65
98
  }
66
99
 
100
+ // A custom reason phrase (PTxyz message) is meaningful only when it differs
101
+ // from the stock HTTP reason phrase for this status.
102
+ const trimmedReason = statusText?.trim();
103
+ const reasonPhrase =
104
+ trimmedReason &&
105
+ trimmedReason !== STANDARD_REASON_PHRASES[status] &&
106
+ trimmedReason !== generic
107
+ ? trimmedReason
108
+ : undefined;
109
+
110
+ const headline = bodyMessage ?? reasonPhrase ?? generic;
111
+ let errMsg = `${operation}: HTTP ${status} - ${headline}`;
112
+ if (bodyDetails && bodyDetails !== headline) {
113
+ errMsg += `\n${bodyDetails}`;
114
+ }
115
+
67
116
  return errMsg + remediation;
68
117
  }
69
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.16.0-dev.5",
3
+ "version": "0.16.0-dev.7",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -93,17 +93,24 @@ async function startFakeApi(projects?: unknown[]) {
93
93
  };
94
94
  }
95
95
 
96
- describe("CLI DBLab companion command groups", () => {
97
- test("top-level help lists clone, branch, and snapshot groups", () => {
96
+ describe("CLI DBLab companion command groups (grouped under `pgai dblab …`)", () => {
97
+ test("top-level help lists the dblab group", () => {
98
98
  const r = runCli(["--help"], isolatedEnv());
99
99
  const out = `${r.stdout}\n${r.stderr}`;
100
+ expect(out).toContain("dblab");
101
+ });
102
+
103
+ test("dblab help exposes the clone, branch, and snapshot groups", () => {
104
+ const r = runCli(["dblab", "--help"], isolatedEnv());
105
+ expect(r.status).toBe(0);
106
+ const out = `${r.stdout}\n${r.stderr}`;
100
107
  expect(out).toContain("clone");
101
108
  expect(out).toContain("branch");
102
109
  expect(out).toContain("snapshot");
103
110
  });
104
111
 
105
112
  test("clone help exposes create/list/status/reset/destroy", () => {
106
- const r = runCli(["clone", "--help"], isolatedEnv());
113
+ const r = runCli(["dblab", "clone", "--help"], isolatedEnv());
107
114
  expect(r.status).toBe(0);
108
115
  const out = `${r.stdout}\n${r.stderr}`;
109
116
  expect(out).toContain("create");
@@ -114,7 +121,7 @@ describe("CLI DBLab companion command groups", () => {
114
121
  });
115
122
 
116
123
  test("branch help exposes list/create/delete/log", () => {
117
- const r = runCli(["branch", "--help"], isolatedEnv());
124
+ const r = runCli(["dblab", "branch", "--help"], isolatedEnv());
118
125
  expect(r.status).toBe(0);
119
126
  const out = `${r.stdout}\n${r.stderr}`;
120
127
  expect(out).toContain("list");
@@ -124,7 +131,7 @@ describe("CLI DBLab companion command groups", () => {
124
131
  });
125
132
 
126
133
  test("snapshot help exposes list/create/destroy", () => {
127
- const r = runCli(["snapshot", "--help"], isolatedEnv());
134
+ const r = runCli(["dblab", "snapshot", "--help"], isolatedEnv());
128
135
  expect(r.status).toBe(0);
129
136
  const out = `${r.stdout}\n${r.stderr}`;
130
137
  expect(out).toContain("list");
@@ -133,13 +140,13 @@ describe("CLI DBLab companion command groups", () => {
133
140
  });
134
141
 
135
142
  test("clone create fails fast without an API key", () => {
136
- const r = runCli(["clone", "create", "--project", "main-db"], isolatedEnv());
143
+ const r = runCli(["dblab", "clone", "create", "--project", "main-db"], isolatedEnv());
137
144
  expect(r.status).toBe(1);
138
145
  expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
139
146
  });
140
147
 
141
148
  test("clone list without --project errors", () => {
142
- const r = runCli(["clone", "list"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
149
+ const r = runCli(["dblab", "clone", "list"], isolatedEnv({ PGAI_API_KEY: "test-key" }));
143
150
  expect(r.status).not.toBe(0);
144
151
  expect(`${r.stdout}\n${r.stderr}`.toLowerCase()).toContain("project");
145
152
  });
@@ -148,7 +155,7 @@ describe("CLI DBLab companion command groups", () => {
148
155
  const api = await startFakeApi();
149
156
  try {
150
157
  const r = await runCliAsync(
151
- ["clone", "create", "--project", "main-db", "--json"],
158
+ ["dblab", "clone", "create", "--project", "main-db", "--json"],
152
159
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
153
160
  );
154
161
  expect(r.status).toBe(0);
@@ -170,7 +177,7 @@ describe("CLI DBLab companion command groups", () => {
170
177
  const api = await startFakeApi();
171
178
  try {
172
179
  const r = await runCliAsync(
173
- ["clone", "create", "--project", "main-db", "--branch", "feature-idx", "--snapshot", "s-9", "--protected", "--json"],
180
+ ["dblab", "clone", "create", "--project", "main-db", "--branch", "feature-idx", "--snapshot", "s-9", "--protected", "--json"],
174
181
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
175
182
  );
176
183
  expect(r.status).toBe(0);
@@ -185,7 +192,7 @@ describe("CLI DBLab companion command groups", () => {
185
192
  const api = await startFakeApi();
186
193
  try {
187
194
  const r = await runCliAsync(
188
- ["clone", "reset", "c-8f21", "--project", "main-db", "--json"],
195
+ ["dblab", "clone", "reset", "c-8f21", "--project", "main-db", "--json"],
189
196
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
190
197
  );
191
198
  expect(r.status).toBe(0);
@@ -202,7 +209,7 @@ describe("CLI DBLab companion command groups", () => {
202
209
  const api = await startFakeApi();
203
210
  try {
204
211
  const r = await runCliAsync(
205
- ["clone", "destroy", "c-8f21", "--project", "12", "--json"],
212
+ ["dblab", "clone", "destroy", "c-8f21", "--project", "12", "--json"],
206
213
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
207
214
  );
208
215
  expect(r.status).toBe(0);
@@ -219,7 +226,7 @@ describe("CLI DBLab companion command groups", () => {
219
226
  const api = await startFakeApi();
220
227
  try {
221
228
  const r = await runCliAsync(
222
- ["branch", "create", "feature-idx", "--project", "main-db", "--snapshot", "latest", "--json"],
229
+ ["dblab", "branch", "create", "feature-idx", "--project", "main-db", "--snapshot", "latest", "--json"],
223
230
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
224
231
  );
225
232
  expect(r.status).toBe(0);
@@ -236,7 +243,7 @@ describe("CLI DBLab companion command groups", () => {
236
243
  const api = await startFakeApi();
237
244
  try {
238
245
  const r = await runCliAsync(
239
- ["branch", "log", "feature-idx", "--project", "main-db", "--json"],
246
+ ["dblab", "branch", "log", "feature-idx", "--project", "main-db", "--json"],
240
247
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
241
248
  );
242
249
  expect(r.status).toBe(0);
@@ -252,7 +259,7 @@ describe("CLI DBLab companion command groups", () => {
252
259
  const api = await startFakeApi();
253
260
  try {
254
261
  const r = await runCliAsync(
255
- ["snapshot", "list", "--project", "main-db", "--json"],
262
+ ["dblab", "snapshot", "list", "--project", "main-db", "--json"],
256
263
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
257
264
  );
258
265
  expect(r.status).toBe(0);
@@ -268,7 +275,7 @@ describe("CLI DBLab companion command groups", () => {
268
275
  const api = await startFakeApi();
269
276
  try {
270
277
  const r = await runCliAsync(
271
- ["snapshot", "create", "--project", "main-db", "--clone", "c-8f21", "--message", "before idx", "--json"],
278
+ ["dblab", "snapshot", "create", "--project", "main-db", "--clone", "c-8f21", "--message", "before idx", "--json"],
272
279
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
273
280
  );
274
281
  expect(r.status).toBe(0);
@@ -284,7 +291,7 @@ describe("CLI DBLab companion command groups", () => {
284
291
  const api = await startFakeApi();
285
292
  try {
286
293
  const r = await runCliAsync(
287
- ["snapshot", "destroy", "s-1", "--project", "main-db", "--force", "--json"],
294
+ ["dblab", "snapshot", "destroy", "s-1", "--project", "main-db", "--force", "--json"],
288
295
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
289
296
  );
290
297
  expect(r.status).toBe(0);
@@ -300,7 +307,7 @@ describe("CLI DBLab companion command groups", () => {
300
307
  const api = await startFakeApi();
301
308
  try {
302
309
  const r = await runCliAsync(
303
- ["clone", "list", "--project", "does-not-exist"],
310
+ ["dblab", "clone", "list", "--project", "does-not-exist"],
304
311
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: api.baseUrl })
305
312
  );
306
313
  expect(r.status).toBe(1);
@@ -327,7 +334,7 @@ describe("CLI DBLab companion command groups", () => {
327
334
  try {
328
335
  const baseUrl = `http://${server.hostname}:${server.port}/api/general`;
329
336
  const r = await runCliAsync(
330
- ["clone", "destroy", "c-1", "--project", "main-db"],
337
+ ["dblab", "clone", "destroy", "c-1", "--project", "main-db"],
331
338
  isolatedEnv({ PGAI_API_KEY: "test-key", PGAI_API_BASE_URL: baseUrl })
332
339
  );
333
340
  expect(r.status).toBe(1);
@@ -126,11 +126,22 @@ function startFakeApi() {
126
126
  return { baseUrl, requests, stop: () => server.stop(true) };
127
127
  }
128
128
 
129
- describe("CLI Joe command surface", () => {
130
- test("top-level help lists the Joe verbs and projects/result", () => {
129
+ describe("CLI Joe command surface (grouped under `pgai joe …`)", () => {
130
+ test("top-level help lists the joe + dblab groups and top-level projects", () => {
131
131
  const r = runCli(["--help"], isolatedEnv());
132
132
  const out = `${r.stdout}\n${r.stderr}`;
133
- for (const verb of ["plan", "explain", "exec", "hypo", "activity", "terminate", "reset", "describe", "projects", "result", "status"]) {
133
+ // Joe verbs are grouped under `joe`; DBLab verbs under `dblab`. `projects`
134
+ // stays top-level (org-level, not a Joe endpoint).
135
+ for (const group of ["joe", "dblab", "projects"]) {
136
+ expect(out).toContain(group);
137
+ }
138
+ });
139
+
140
+ test("pgai joe --help lists all the Joe verbs", () => {
141
+ const r = runCli(["joe", "--help"], isolatedEnv());
142
+ expect(r.status).toBe(0);
143
+ const out = `${r.stdout}\n${r.stderr}`;
144
+ for (const verb of ["plan", "explain", "exec", "hypo", "activity", "terminate", "reset", "describe", "result", "status"]) {
134
145
  expect(out).toContain(verb);
135
146
  }
136
147
  });
@@ -153,11 +164,11 @@ describe("CLI Joe command surface", () => {
153
164
  }
154
165
  });
155
166
 
156
- test("pgai plan --project 12 submits command='plan' and prints the plan", async () => {
167
+ test("pgai joe plan --project 12 submits command='plan' and prints the plan", async () => {
157
168
  const api = startFakeApi();
158
169
  try {
159
170
  const r = await runCliAsync(
160
- ["plan", "select * from users where email = 'x@acme.io'", "--project", "12"],
171
+ ["joe", "plan", "select * from users where email = 'x@acme.io'", "--project", "12"],
161
172
  isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
162
173
  );
163
174
  expect(r.status).toBe(0);
@@ -174,11 +185,11 @@ describe("CLI Joe command surface", () => {
174
185
  }
175
186
  });
176
187
 
177
- test("pgai plan --project main-db resolves the alias via projects_list", async () => {
188
+ test("pgai joe plan --project main-db resolves the alias via projects_list", async () => {
178
189
  const api = startFakeApi();
179
190
  try {
180
191
  const r = await runCliAsync(
181
- ["plan", "select 1", "--project", "main-db"],
192
+ ["joe", "plan", "select 1", "--project", "main-db"],
182
193
  isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
183
194
  );
184
195
  expect(r.status).toBe(0);
@@ -190,15 +201,15 @@ describe("CLI Joe command surface", () => {
190
201
  }
191
202
  });
192
203
 
193
- test("optimize loop: plan then exec reuse the same session on one project", async () => {
204
+ test("optimize loop: joe plan then joe exec reuse the same session on one project", async () => {
194
205
  const api = startFakeApi();
195
206
  const env = isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl });
196
207
  try {
197
208
  // First command starts a fresh session; the server hands back session 88.
198
- const first = await runCliAsync(["plan", "select 1", "--project", "12", "--new-session"], env);
209
+ const first = await runCliAsync(["joe", "plan", "select 1", "--project", "12", "--new-session"], env);
199
210
  expect(first.status).toBe(0);
200
211
  // Second command auto-reuses the stored session (88) on the same project.
201
- const second = await runCliAsync(["exec", "create index on users (email)", "--project", "12"], env);
212
+ const second = await runCliAsync(["joe", "exec", "create index on users (email)", "--project", "12"], env);
202
213
  expect(second.status).toBe(0);
203
214
  expect(second.stdout).toContain("CREATE INDEX");
204
215
 
@@ -215,21 +226,21 @@ describe("CLI Joe command surface", () => {
215
226
  }
216
227
  });
217
228
 
218
- test("cold clone exceeds the budget -> resume handle (exit 0), then pgai result", async () => {
229
+ test("cold clone exceeds the budget -> resume handle (exit 0), then pgai joe result", async () => {
219
230
  const api = startFakeApi();
220
231
  const env = isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl });
221
232
  try {
222
233
  // project 77 (COLD) stays `running`; --budget 0 forces immediate resume.
223
- const r = await runCliAsync(["plan", "select 1", "--project", "77", "--budget", "0"], env);
234
+ const r = await runCliAsync(["joe", "plan", "select 1", "--project", "77", "--budget", "0"], env);
224
235
  expect(r.status).toBe(0);
225
236
  expect(r.stdout).toContain("resume:");
226
- expect(r.stdout).toMatch(/pgai result \d+/);
237
+ expect(r.stdout).toMatch(/pgai joe result \d+/);
227
238
 
228
- const match = r.stdout.match(/pgai result (\d+)/);
239
+ const match = r.stdout.match(/pgai joe result (\d+)/);
229
240
  const commandId = match ? match[1] : "";
230
241
  expect(commandId).not.toBe("");
231
242
 
232
- const resumed = await runCliAsync(["result", commandId], env);
243
+ const resumed = await runCliAsync(["joe", "result", commandId], env);
233
244
  expect(resumed.status).toBe(0);
234
245
  expect(resumed.stdout).toContain("Seq Scan on users");
235
246
  } finally {
@@ -241,7 +252,7 @@ describe("CLI Joe command surface", () => {
241
252
  const api = startFakeApi();
242
253
  try {
243
254
  const r = await runCliAsync(
244
- ["plan", "select 1", "--project", "999"],
255
+ ["joe", "plan", "select 1", "--project", "999"],
245
256
  isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
246
257
  );
247
258
  expect(r.status).toBe(1);
@@ -251,11 +262,11 @@ describe("CLI Joe command surface", () => {
251
262
  }
252
263
  });
253
264
 
254
- test("terminate requires a numeric pid and maps args.pid", async () => {
265
+ test("joe terminate requires a numeric pid and maps args.pid", async () => {
255
266
  const api = startFakeApi();
256
267
  try {
257
268
  const r = await runCliAsync(
258
- ["terminate", "4711", "--project", "12"],
269
+ ["joe", "terminate", "4711", "--project", "12"],
259
270
  isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
260
271
  );
261
272
  expect(r.status).toBe(0);
@@ -268,11 +279,11 @@ describe("CLI Joe command surface", () => {
268
279
  }
269
280
  });
270
281
 
271
- test("describe maps args.object (and --variant)", async () => {
282
+ test("joe describe maps args.object (and --variant)", async () => {
272
283
  const api = startFakeApi();
273
284
  try {
274
285
  const r = await runCliAsync(
275
- ["describe", "users", "--project", "12", "--variant", "\\d+"],
286
+ ["joe", "describe", "users", "--project", "12", "--variant", "\\d+"],
276
287
  isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
277
288
  );
278
289
  expect(r.status).toBe(0);
@@ -285,13 +296,13 @@ describe("CLI Joe command surface", () => {
285
296
  });
286
297
 
287
298
  test("missing --project fails fast", () => {
288
- const r = runCli(["plan", "select 1"], isolatedEnv({ PGAI_API_KEY: "k" }));
299
+ const r = runCli(["joe", "plan", "select 1"], isolatedEnv({ PGAI_API_KEY: "k" }));
289
300
  expect(r.status).toBe(1);
290
301
  expect(`${r.stdout}\n${r.stderr}`).toContain("Project is required");
291
302
  });
292
303
 
293
304
  test("missing API key fails fast", () => {
294
- const r = runCli(["plan", "select 1", "--project", "12"], isolatedEnv());
305
+ const r = runCli(["joe", "plan", "select 1", "--project", "12"], isolatedEnv());
295
306
  expect(r.status).toBe(1);
296
307
  expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
297
308
  });
package/test/util.test.ts CHANGED
@@ -41,4 +41,39 @@ describe("formatHttpError", () => {
41
41
  expect(msg.indexOf("Invalid token")).toBeGreaterThan(-1);
42
42
  expect(msg.indexOf("Invalid token")).toBeLessThan(msg.indexOf("Run 'postgresai auth'"));
43
43
  });
44
+
45
+ // PostgREST v9's PTxyz custom-status convention carries the RPC's user-facing
46
+ // `message` in the HTTP reason phrase (statusText); the JSON body holds only
47
+ // `hint`/`details` (no `message`/`code`). The formatter must surface the
48
+ // reason phrase as the headline instead of a hardcoded generic label, and
49
+ // still show the `details` (plural) line.
50
+ test("surfaces the PostgREST PTxyz reason-phrase message, not a generic label", () => {
51
+ const msg = formatHttpError(
52
+ "Failed to submit plan command",
53
+ 403,
54
+ '{"hint":null,"details":"The LLM-facing Joe surface is gated on the organization\'s ai_enabled setting."}',
55
+ "AI features disabled for this org — enable in AI Assistant Settings"
56
+ );
57
+
58
+ expect(msg).toContain("HTTP 403");
59
+ expect(msg).toContain("AI features disabled for this org — enable in AI Assistant Settings");
60
+ // the real reason must not be masked by the hardcoded generic label
61
+ expect(msg).not.toContain("Forbidden - access denied");
62
+ // the supplementary `details` (plural) line is still shown
63
+ expect(msg).toContain("gated on the organization's ai_enabled setting");
64
+ // and we never dump the raw JSON object
65
+ expect(msg).not.toContain('"hint"');
66
+ });
67
+
68
+ test("prefers a JSON body `message` over the reason phrase when both exist", () => {
69
+ const msg = formatHttpError("op", 400, '{"message":"real message","details":"more"}', "Bad Request");
70
+ expect(msg).toContain("real message");
71
+ expect(msg).toContain("more");
72
+ });
73
+
74
+ test("falls back to the generic label for a standard (non-custom) reason phrase", () => {
75
+ // a bare standard reason phrase must not replace the friendlier generic label
76
+ const msg = formatHttpError("op", 403, undefined, "Forbidden");
77
+ expect(msg).toContain("Forbidden - access denied");
78
+ });
44
79
  });