@mutagent/cli 0.2.53 → 0.2.55

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/cli.js CHANGED
@@ -893,6 +893,35 @@ var init_config = __esm(() => {
893
893
  CREDENTIALS_FILE = join2(CREDENTIALS_DIR, "credentials.json");
894
894
  });
895
895
 
896
+ // src/lib/tenancy-error.ts
897
+ function shellQuote(value) {
898
+ if (/^[A-Za-z0-9_-]+$/.test(value))
899
+ return value;
900
+ return `'${value.replace(/'/g, `'\\''`)}'`;
901
+ }
902
+ function workspaceDenied(ctx) {
903
+ const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
904
+ const own = ctx.keyScope === "workspace" ? ctx.keyWorkspace : undefined;
905
+ if (ctx.workspaceOrigin === "selection" && own && own.id !== ctx.workspaceId) {
906
+ return new TenancyError(TENANCY_DENIED, `This API key only works in workspace ${own.name}; your saved selection is ${shown}.`, `Run: mutagent workspaces use ${shellQuote(own.name)}`);
907
+ }
908
+ if (ctx.workspaceOrigin === "selection") {
909
+ return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
910
+ }
911
+ const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
912
+ return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
913
+ }
914
+ var TENANCY_DENIED = "TENANCY_DENIED", TenancyError;
915
+ var init_tenancy_error = __esm(() => {
916
+ init_errors();
917
+ TenancyError = class TenancyError extends MutagentError {
918
+ constructor(code, what, fix) {
919
+ super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
920
+ this.name = "TenancyError";
921
+ }
922
+ };
923
+ });
924
+
896
925
  // src/lib/organization-scope.ts
897
926
  function looksLikeWorkspaceId(value) {
898
927
  return UUID_PATTERN.test(value);
@@ -924,14 +953,6 @@ function parseBody(body) {
924
953
  function deadKeyError() {
925
954
  return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login");
926
955
  }
927
- function workspaceDenied(ctx) {
928
- const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
929
- if (ctx.workspaceOrigin === "selection") {
930
- return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
931
- }
932
- const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
933
- return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
934
- }
935
956
  function tenancyFailure(status, body, ctx) {
936
957
  const parsed = parseBody(body);
937
958
  if (status === 401)
@@ -997,11 +1018,6 @@ function matchesOrganization(value, org) {
997
1018
  return true;
998
1019
  return org.name !== undefined && value.toLowerCase() === org.name.toLowerCase();
999
1020
  }
1000
- function shellQuote(value) {
1001
- if (/^[A-Za-z0-9_-]+$/.test(value))
1002
- return value;
1003
- return `'${value.replace(/'/g, `'\\''`)}'`;
1004
- }
1005
1021
  function orgMismatch(value, org) {
1006
1022
  const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
1007
1023
  return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${shellQuote(value)}`);
@@ -1148,21 +1164,101 @@ async function findWorkspace(wanted, t) {
1148
1164
  const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
1149
1165
  throw otherOrganizationHint(wanted, t.organization, orgs);
1150
1166
  }
1151
- var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
1167
+ var UUID_PATTERN, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
1152
1168
  var init_organization_scope = __esm(() => {
1153
1169
  init_errors();
1154
1170
  init_config();
1171
+ init_tenancy_error();
1155
1172
  UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1156
- TenancyError = class TenancyError extends MutagentError {
1157
- constructor(code, what, fix) {
1158
- super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
1159
- this.name = "TenancyError";
1160
- }
1161
- };
1162
1173
  keySelfProbes = new Map;
1163
1174
  keyScopeConfirmations = new Map;
1164
1175
  });
1165
1176
 
1177
+ // src/lib/key-scope-heal.ts
1178
+ function isOrgKeyWithoutWorkspace(status, body) {
1179
+ return status === 400 && body.toLowerCase().includes(ORG_KEY_NEEDS_WORKSPACE);
1180
+ }
1181
+ function codeOf(body) {
1182
+ try {
1183
+ return JSON.parse(body)?.code;
1184
+ } catch {
1185
+ return;
1186
+ }
1187
+ }
1188
+ async function readKeySelf(key, fetchImpl) {
1189
+ try {
1190
+ const response = await fetchImpl(`${key.endpoint}/api/api-keys/self`, { headers: { "x-api-key": key.apiKey } });
1191
+ const self = response.ok ? await response.json() : undefined;
1192
+ return self?.scope === "workspace" || self?.scope === "organization" || self?.scope === "user" ? self : undefined;
1193
+ } catch {
1194
+ return;
1195
+ }
1196
+ }
1197
+ async function explainSelectionDenial(status, body, ctx, key, fetchImpl = fetch) {
1198
+ if (status !== 403 || ctx.workspaceOrigin !== "selection" || ctx.keyScope !== "workspace" || ctx.keyWorkspace)
1199
+ return;
1200
+ if (codeOf(body) !== TENANCY_DENIED)
1201
+ return;
1202
+ const self = await readKeySelf(key, fetchImpl);
1203
+ if (!self)
1204
+ return;
1205
+ ctx.keyScope = self.scope;
1206
+ if (self.scope === "workspace") {
1207
+ ctx.keyWorkspace = self.workspace ?? undefined;
1208
+ return;
1209
+ }
1210
+ if (!envKeyReplacesFile())
1211
+ recordKeyScope(self);
1212
+ }
1213
+ async function explainThrownDenial(error, ctx, key) {
1214
+ if (ctx === undefined || !(error instanceof Error) || !("statusCode" in error) || typeof error.statusCode !== "number")
1215
+ return;
1216
+ const body = "body" in error && typeof error.body === "string" ? error.body : "";
1217
+ await explainSelectionDenial(error.statusCode, body, ctx, key);
1218
+ }
1219
+ function sameHeaders(a, b) {
1220
+ const keys = Object.keys(a);
1221
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
1222
+ }
1223
+ async function fetchWithScopeHeal(input, init, target) {
1224
+ const spare = input instanceof Request ? input.clone() : undefined;
1225
+ const response = await (init == null ? fetch(input) : fetch(input, init));
1226
+ if (response.status === 403 && target.tenancy) {
1227
+ const denied = await response.clone().text().catch(() => "");
1228
+ await explainSelectionDenial(403, denied, target.tenancy, target);
1229
+ return response;
1230
+ }
1231
+ if (response.status !== 400)
1232
+ return response;
1233
+ const body = await response.clone().text().catch(() => "");
1234
+ if (!isOrgKeyWithoutWorkspace(response.status, body))
1235
+ return response;
1236
+ const before = target.headers();
1237
+ const self = await confirmKeyScope(target.apiKey, target.endpoint).catch(() => {
1238
+ return;
1239
+ });
1240
+ if (!self)
1241
+ return response;
1242
+ target.adopt(self);
1243
+ const after = target.headers();
1244
+ if (sameHeaders(before, after))
1245
+ return response;
1246
+ if (spare) {
1247
+ const headers = new Headers(spare.headers);
1248
+ for (const [name, value] of Object.entries(after))
1249
+ headers.set(name, value);
1250
+ return fetch(new Request(spare, { headers }));
1251
+ }
1252
+ const headers = { ...init?.headers, ...after };
1253
+ return fetch(input, { ...init, headers });
1254
+ }
1255
+ var ORG_KEY_NEEDS_WORKSPACE = "x-workspace-id header required";
1256
+ var init_key_scope_heal = __esm(() => {
1257
+ init_config();
1258
+ init_organization_scope();
1259
+ init_tenancy_error();
1260
+ });
1261
+
1166
1262
  // src/lib/sdk-debug.ts
1167
1263
  function isDebugEnabled(env = process.env) {
1168
1264
  return TRUTHY.has((env.MUTAGENT_DEBUG ?? "").trim().toLowerCase());
@@ -1477,46 +1573,6 @@ function buildCreateProviderBody(data) {
1477
1573
  return body;
1478
1574
  }
1479
1575
 
1480
- // src/lib/key-scope-heal.ts
1481
- function isOrgKeyWithoutWorkspace(status, body) {
1482
- return status === 400 && body.toLowerCase().includes(ORG_KEY_NEEDS_WORKSPACE);
1483
- }
1484
- function sameHeaders(a, b) {
1485
- const keys = Object.keys(a);
1486
- return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
1487
- }
1488
- async function fetchWithScopeHeal(input, init, target) {
1489
- const spare = input instanceof Request ? input.clone() : undefined;
1490
- const response = await (init == null ? fetch(input) : fetch(input, init));
1491
- if (response.status !== 400)
1492
- return response;
1493
- const body = await response.clone().text().catch(() => "");
1494
- if (!isOrgKeyWithoutWorkspace(response.status, body))
1495
- return response;
1496
- const before = target.headers();
1497
- const self = await confirmKeyScope(target.apiKey, target.endpoint).catch(() => {
1498
- return;
1499
- });
1500
- if (!self)
1501
- return response;
1502
- target.adopt(self);
1503
- const after = target.headers();
1504
- if (sameHeaders(before, after))
1505
- return response;
1506
- if (spare) {
1507
- const headers = new Headers(spare.headers);
1508
- for (const [name, value] of Object.entries(after))
1509
- headers.set(name, value);
1510
- return fetch(new Request(spare, { headers }));
1511
- }
1512
- const headers = { ...init?.headers, ...after };
1513
- return fetch(input, { ...init, headers });
1514
- }
1515
- var ORG_KEY_NEEDS_WORKSPACE = "x-workspace-id header required";
1516
- var init_key_scope_heal = __esm(() => {
1517
- init_organization_scope();
1518
- });
1519
-
1520
1576
  // src/lib/sdk-mappers.ts
1521
1577
  import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
1522
1578
  function isProviderType(value) {
@@ -1714,9 +1770,8 @@ class SDKClientWrapper {
1714
1770
  const tenancy = tenancyFailure(response.status, error, this.tenancy);
1715
1771
  if (tenancy)
1716
1772
  throw tenancy;
1717
- if (response.status === 401) {
1773
+ if (response.status === 401)
1718
1774
  throw new AuthenticationError;
1719
- }
1720
1775
  if (response.status === 403) {
1721
1776
  throw new ApiError(403, error || "Access denied");
1722
1777
  }
@@ -1887,6 +1942,7 @@ class SDKClientWrapper {
1887
1942
  apiKey: this.apiKey,
1888
1943
  endpoint: this.endpoint,
1889
1944
  headers: () => this.tenancyHeaders(),
1945
+ tenancy: this.tenancy,
1890
1946
  adopt: (self) => {
1891
1947
  this.tenancy.keyScope = self.scope;
1892
1948
  if (!this.workspaceId && self.scope === "workspace" && self.workspace)
@@ -18030,6 +18086,7 @@ import chalk2 from "chalk";
18030
18086
  // src/lib/agent/spec-api.ts
18031
18087
  init_config();
18032
18088
  init_errors();
18089
+ init_key_scope_heal();
18033
18090
  init_known_secrets();
18034
18091
  init_organization_scope();
18035
18092
  init_sdk_debug();
@@ -18107,6 +18164,7 @@ async function connectAgentSpec() {
18107
18164
  try {
18108
18165
  return await fn(sdk.managedAgents);
18109
18166
  } catch (error) {
18167
+ await explainThrownDenial(error, tenancy, { apiKey, endpoint: resolved.endpoint });
18110
18168
  throw agentSpecError(error, { endpoint: resolved.endpoint, tenancy, slug });
18111
18169
  }
18112
18170
  }
@@ -27316,6 +27374,7 @@ init_secure_file();
27316
27374
  init_errors();
27317
27375
  init_known_secrets();
27318
27376
  init_organization_scope();
27377
+ init_key_scope_heal();
27319
27378
  import { existsSync as existsSync15, readFileSync as readFileSync16, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
27320
27379
  import { homedir as homedir6 } from "os";
27321
27380
  import { join as join21, dirname as dirname8 } from "path";
@@ -27401,6 +27460,7 @@ async function exchangeToken(input, deps = {}) {
27401
27460
  if (response.status === 403) {
27402
27461
  const context = { ...input.tenancy, workspaceId: input.workspaceId };
27403
27462
  const body = await response.text().catch(() => "");
27463
+ await explainSelectionDenial(403, body, context, input, doFetch);
27404
27464
  throw tenancyFailure(403, body, context) ?? workspaceDenied(context);
27405
27465
  }
27406
27466
  if (!response.ok) {
@@ -31699,6 +31759,7 @@ import chalk42 from "chalk";
31699
31759
 
31700
31760
  // src/lib/gateway-api.ts
31701
31761
  init_config();
31762
+ init_key_scope_heal();
31702
31763
  init_known_secrets();
31703
31764
  init_organization_scope();
31704
31765
  class GatewayApiError extends Error {
@@ -31799,6 +31860,7 @@ async function gatewayRequest(path, init = {}) {
31799
31860
  if (!response.ok) {
31800
31861
  const bodyText = await response.text().catch(() => "");
31801
31862
  if (response.status === 403 && tenancyCodeOf(bodyText) === TENANCY_DENIED) {
31863
+ await explainSelectionDenial(403, bodyText, connection.tenancy, connection);
31802
31864
  const denied = tenancyFailure(response.status, bodyText, connection.tenancy);
31803
31865
  if (denied)
31804
31866
  throw denied;
@@ -33666,6 +33728,7 @@ init_known_secrets();
33666
33728
 
33667
33729
  // src/lib/gateway-sdk.ts
33668
33730
  import { HTTPClient as HTTPClient4, Mutagent as Mutagent4 } from "@mutagent/sdk";
33731
+ init_key_scope_heal();
33669
33732
  init_organization_scope();
33670
33733
  init_sdk_debug();
33671
33734
  var SDK_TRANSPORT_ERRORS3 = new Set(["ConnectionError", "RequestTimeoutError", "RequestAbortedError", "UnexpectedClientError"]);
@@ -33683,12 +33746,12 @@ function gatewaySdkError(error, connection, what) {
33683
33746
  const body = "body" in error && typeof error.body === "string" ? error.body : "";
33684
33747
  if (status === 401)
33685
33748
  return deadKeyError();
33686
- const parsed = parseGatewayErrorBody(status, body, "");
33687
- if (status === 403 && parsed.code === TENANCY_DENIED) {
33749
+ if (status === 403 && tenancyCodeOf(body) === TENANCY_DENIED) {
33688
33750
  const denied = tenancyFailure(status, body, connection.tenancy);
33689
33751
  if (denied)
33690
33752
  return denied;
33691
33753
  }
33754
+ const parsed = parseGatewayErrorBody(status, body, "");
33692
33755
  return new GatewayApiError(status, parsed.code, parsed.reason, parsed.fix);
33693
33756
  }
33694
33757
  return error;
@@ -33714,6 +33777,7 @@ async function callGatewaySdk(what, fn) {
33714
33777
  try {
33715
33778
  return await fn(sdk.gateway, connection);
33716
33779
  } catch (error) {
33780
+ await explainThrownDenial(error, connection.tenancy, connection);
33717
33781
  throw gatewaySdkError(error, connection, what);
33718
33782
  }
33719
33783
  }
@@ -34145,6 +34209,7 @@ import chalk50 from "chalk";
34145
34209
 
34146
34210
  // src/lib/gateway-api-events.ts
34147
34211
  import { GatewayStreamError, streamGatewayUpdates } from "@mutagent/sdk/custom/gateway-stream.js";
34212
+ init_key_scope_heal();
34148
34213
  init_known_secrets();
34149
34214
  init_organization_scope();
34150
34215
  function iso3(value) {
@@ -34164,11 +34229,13 @@ async function listNotices(query) {
34164
34229
  return scrubKnownSuccessDeep({ items, nextCursor: page.nextCursor });
34165
34230
  });
34166
34231
  }
34167
- function streamError(error, tenancy) {
34232
+ async function streamError(error, connection) {
34168
34233
  if (error.status === 401)
34169
34234
  return deadKeyError();
34170
34235
  if (error.status === 403 && error.code === TENANCY_DENIED) {
34171
- const denied = tenancyFailure(403, JSON.stringify({ code: error.code }), tenancy);
34236
+ const body = JSON.stringify({ code: error.code });
34237
+ await explainSelectionDenial(403, body, connection.tenancy, connection);
34238
+ const denied = tenancyFailure(403, body, connection.tenancy);
34172
34239
  if (denied)
34173
34240
  return denied;
34174
34241
  }
@@ -34187,7 +34254,7 @@ async function* followGateway(signal) {
34187
34254
  });
34188
34255
  } catch (error) {
34189
34256
  if (error instanceof GatewayStreamError)
34190
- throw streamError(error, connection.tenancy);
34257
+ throw await streamError(error, connection);
34191
34258
  throw error;
34192
34259
  }
34193
34260
  }
@@ -34728,6 +34795,7 @@ init_output();
34728
34795
  // src/lib/reports-api.ts
34729
34796
  init_config();
34730
34797
  init_errors();
34798
+ init_key_scope_heal();
34731
34799
  init_known_secrets();
34732
34800
  init_organization_scope();
34733
34801
  init_sdk_debug();
@@ -34797,6 +34865,10 @@ function reportsError(error, ctx) {
34797
34865
  return new MutagentError("REQUEST_FAILED", error instanceof Error ? error.message : "Request failed", "Run: mutagent reports --help");
34798
34866
  }
34799
34867
  function connection(sdk, ctx) {
34868
+ const explain = async (error) => {
34869
+ if (ctx.apiKey !== undefined)
34870
+ await explainThrownDenial(error, ctx.tenancy, { apiKey: ctx.apiKey, endpoint: ctx.endpoint });
34871
+ };
34800
34872
  return {
34801
34873
  endpoint: ctx.endpoint,
34802
34874
  credential: ctx.credential,
@@ -34805,6 +34877,7 @@ function connection(sdk, ctx) {
34805
34877
  try {
34806
34878
  return await fn(sdk.reports);
34807
34879
  } catch (error) {
34880
+ await explain(error);
34808
34881
  throw reportsError(error, ctx);
34809
34882
  }
34810
34883
  },
@@ -34812,6 +34885,7 @@ function connection(sdk, ctx) {
34812
34885
  try {
34813
34886
  return await fn(sdk);
34814
34887
  } catch (error) {
34888
+ await explain(error);
34815
34889
  throw reportsError(error, ctx);
34816
34890
  }
34817
34891
  }
@@ -34865,6 +34939,7 @@ async function connectReports(opts) {
34865
34939
  return connection(sdk, {
34866
34940
  endpoint: tenancy.endpoint,
34867
34941
  credential: "api-key",
34942
+ apiKey,
34868
34943
  tenancy: {
34869
34944
  keyScope: tenancy.keyScope,
34870
34945
  workspaceId: tenancy.workspaceId,
@@ -36548,6 +36623,7 @@ function fixFor(code, serverFix, ctx) {
36548
36623
  init_config();
36549
36624
  init_errors();
36550
36625
  import { HTTPClient as HTTPClient6, Mutagent as Mutagent6 } from "@mutagent/sdk";
36626
+ init_key_scope_heal();
36551
36627
  init_known_secrets();
36552
36628
  init_organization_scope();
36553
36629
  init_sdk_debug();
@@ -36616,6 +36692,7 @@ async function connectIntegrations() {
36616
36692
  const tenancy = await resolveKeyTenancy(apiKey);
36617
36693
  if (tenancy.keyScope !== "workspace" && !tenancy.workspaceId)
36618
36694
  throw noWorkspaceSelected();
36695
+ const key = { apiKey, endpoint: tenancy.endpoint };
36619
36696
  const headers = tenancyHeaders({
36620
36697
  keyScope: tenancy.keyScope,
36621
36698
  workspaceId: tenancy.workspaceId,
@@ -36667,6 +36744,7 @@ async function connectIntegrations() {
36667
36744
  try {
36668
36745
  return await fn(sdk.integrations);
36669
36746
  } catch (error) {
36747
+ await explainThrownDenial(error, tenancyCtx, key);
36670
36748
  throw integrationsError(error, { endpoint: tenancy.endpoint, tenancy: tenancyCtx, call: callCtx });
36671
36749
  }
36672
36750
  }
@@ -38163,4 +38241,4 @@ if (process.env.CLI_VERSION) {
38163
38241
  }
38164
38242
  runCli(process.argv.slice(2), cliVersion);
38165
38243
 
38166
- //# debugId=F12EBD8D0A6ACFED64756E2164756E21
38244
+ //# debugId=CAC2930F9F23245664756E2164756E21
package/dist/index.js CHANGED
@@ -1043,6 +1043,35 @@ var init_key_check = __esm(() => {
1043
1043
  };
1044
1044
  });
1045
1045
 
1046
+ // src/lib/tenancy-error.ts
1047
+ function shellQuote(value) {
1048
+ if (/^[A-Za-z0-9_-]+$/.test(value))
1049
+ return value;
1050
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1051
+ }
1052
+ function workspaceDenied(ctx) {
1053
+ const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
1054
+ const own = ctx.keyScope === "workspace" ? ctx.keyWorkspace : undefined;
1055
+ if (ctx.workspaceOrigin === "selection" && own && own.id !== ctx.workspaceId) {
1056
+ return new TenancyError(TENANCY_DENIED, `This API key only works in workspace ${own.name}; your saved selection is ${shown}.`, `Run: mutagent workspaces use ${shellQuote(own.name)}`);
1057
+ }
1058
+ if (ctx.workspaceOrigin === "selection") {
1059
+ return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
1060
+ }
1061
+ const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
1062
+ return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
1063
+ }
1064
+ var TENANCY_DENIED = "TENANCY_DENIED", TenancyError;
1065
+ var init_tenancy_error = __esm(() => {
1066
+ init_errors();
1067
+ TenancyError = class TenancyError extends MutagentError {
1068
+ constructor(code, what, fix) {
1069
+ super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
1070
+ this.name = "TenancyError";
1071
+ }
1072
+ };
1073
+ });
1074
+
1046
1075
  // src/lib/organization-scope.ts
1047
1076
  function looksLikeWorkspaceId(value) {
1048
1077
  return UUID_PATTERN.test(value);
@@ -1074,14 +1103,6 @@ function parseBody(body) {
1074
1103
  function deadKeyError() {
1075
1104
  return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login");
1076
1105
  }
1077
- function workspaceDenied(ctx) {
1078
- const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
1079
- if (ctx.workspaceOrigin === "selection") {
1080
- return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
1081
- }
1082
- const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
1083
- return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
1084
- }
1085
1106
  function tenancyFailure(status, body, ctx) {
1086
1107
  const parsed = parseBody(body);
1087
1108
  if (status === 401)
@@ -1147,11 +1168,6 @@ function matchesOrganization(value, org) {
1147
1168
  return true;
1148
1169
  return org.name !== undefined && value.toLowerCase() === org.name.toLowerCase();
1149
1170
  }
1150
- function shellQuote(value) {
1151
- if (/^[A-Za-z0-9_-]+$/.test(value))
1152
- return value;
1153
- return `'${value.replace(/'/g, `'\\''`)}'`;
1154
- }
1155
1171
  function orgMismatch(value, org) {
1156
1172
  const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
1157
1173
  return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${shellQuote(value)}`);
@@ -1298,17 +1314,12 @@ async function findWorkspace(wanted, t) {
1298
1314
  const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
1299
1315
  throw otherOrganizationHint(wanted, t.organization, orgs);
1300
1316
  }
1301
- var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
1317
+ var UUID_PATTERN, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
1302
1318
  var init_organization_scope = __esm(() => {
1303
1319
  init_errors();
1304
1320
  init_config();
1321
+ init_tenancy_error();
1305
1322
  UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1306
- TenancyError = class TenancyError extends MutagentError {
1307
- constructor(code, what, fix) {
1308
- super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
1309
- this.name = "TenancyError";
1310
- }
1311
- };
1312
1323
  keySelfProbes = new Map;
1313
1324
  keyScopeConfirmations = new Map;
1314
1325
  });
@@ -1434,6 +1445,44 @@ var init_sdk_debug = __esm(() => {
1434
1445
  function isOrgKeyWithoutWorkspace(status, body) {
1435
1446
  return status === 400 && body.toLowerCase().includes(ORG_KEY_NEEDS_WORKSPACE);
1436
1447
  }
1448
+ function codeOf(body) {
1449
+ try {
1450
+ return JSON.parse(body)?.code;
1451
+ } catch {
1452
+ return;
1453
+ }
1454
+ }
1455
+ async function readKeySelf(key, fetchImpl) {
1456
+ try {
1457
+ const response = await fetchImpl(`${key.endpoint}/api/api-keys/self`, { headers: { "x-api-key": key.apiKey } });
1458
+ const self = response.ok ? await response.json() : undefined;
1459
+ return self?.scope === "workspace" || self?.scope === "organization" || self?.scope === "user" ? self : undefined;
1460
+ } catch {
1461
+ return;
1462
+ }
1463
+ }
1464
+ async function explainSelectionDenial(status, body, ctx, key, fetchImpl = fetch) {
1465
+ if (status !== 403 || ctx.workspaceOrigin !== "selection" || ctx.keyScope !== "workspace" || ctx.keyWorkspace)
1466
+ return;
1467
+ if (codeOf(body) !== TENANCY_DENIED)
1468
+ return;
1469
+ const self = await readKeySelf(key, fetchImpl);
1470
+ if (!self)
1471
+ return;
1472
+ ctx.keyScope = self.scope;
1473
+ if (self.scope === "workspace") {
1474
+ ctx.keyWorkspace = self.workspace ?? undefined;
1475
+ return;
1476
+ }
1477
+ if (!envKeyReplacesFile())
1478
+ recordKeyScope(self);
1479
+ }
1480
+ async function explainThrownDenial(error, ctx, key) {
1481
+ if (ctx === undefined || !(error instanceof Error) || !("statusCode" in error) || typeof error.statusCode !== "number")
1482
+ return;
1483
+ const body = "body" in error && typeof error.body === "string" ? error.body : "";
1484
+ await explainSelectionDenial(error.statusCode, body, ctx, key);
1485
+ }
1437
1486
  function sameHeaders(a, b) {
1438
1487
  const keys = Object.keys(a);
1439
1488
  return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
@@ -1441,6 +1490,11 @@ function sameHeaders(a, b) {
1441
1490
  async function fetchWithScopeHeal(input, init, target) {
1442
1491
  const spare = input instanceof Request ? input.clone() : undefined;
1443
1492
  const response = await (init == null ? fetch(input) : fetch(input, init));
1493
+ if (response.status === 403 && target.tenancy) {
1494
+ const denied = await response.clone().text().catch(() => "");
1495
+ await explainSelectionDenial(403, denied, target.tenancy, target);
1496
+ return response;
1497
+ }
1444
1498
  if (response.status !== 400)
1445
1499
  return response;
1446
1500
  const body = await response.clone().text().catch(() => "");
@@ -1467,7 +1521,9 @@ async function fetchWithScopeHeal(input, init, target) {
1467
1521
  }
1468
1522
  var ORG_KEY_NEEDS_WORKSPACE = "x-workspace-id header required";
1469
1523
  var init_key_scope_heal = __esm(() => {
1524
+ init_config();
1470
1525
  init_organization_scope();
1526
+ init_tenancy_error();
1471
1527
  });
1472
1528
 
1473
1529
  // src/lib/sdk-mappers.ts
@@ -1667,9 +1723,8 @@ class SDKClientWrapper {
1667
1723
  const tenancy = tenancyFailure(response.status, error, this.tenancy);
1668
1724
  if (tenancy)
1669
1725
  throw tenancy;
1670
- if (response.status === 401) {
1726
+ if (response.status === 401)
1671
1727
  throw new AuthenticationError;
1672
- }
1673
1728
  if (response.status === 403) {
1674
1729
  throw new ApiError(403, error || "Access denied");
1675
1730
  }
@@ -1840,6 +1895,7 @@ class SDKClientWrapper {
1840
1895
  apiKey: this.apiKey,
1841
1896
  endpoint: this.endpoint,
1842
1897
  headers: () => this.tenancyHeaders(),
1898
+ tenancy: this.tenancy,
1843
1899
  adopt: (self) => {
1844
1900
  this.tenancy.keyScope = self.scope;
1845
1901
  if (!this.workspaceId && self.scope === "workspace" && self.workspace)
@@ -1989,4 +2045,4 @@ export {
1989
2045
  version
1990
2046
  };
1991
2047
 
1992
- //# debugId=CB6D96307760100964756E2164756E21
2048
+ //# debugId=C31E7F3B419630CE64756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutagent/cli",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
4
4
  "description": "Bun-native CLI for the MutagenT AI platform - auth, provider config, and lifecycle-tool installer for AI-native workflows",
5
5
  "type": "module",
6
6
  "bin": {