@mutagent/cli 0.1.379 → 0.1.381

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.
Files changed (3) hide show
  1. package/dist/bin/cli.js +945 -760
  2. package/dist/index.js +515 -411
  3. package/package.json +2 -2
package/dist/bin/cli.js CHANGED
@@ -268,7 +268,7 @@ function handleError(error, isJson) {
268
268
  }
269
269
  if (error instanceof WorkspaceContextError) {
270
270
  jsonOutput.remediation = {
271
- setWorkspace: "mutagent config set workspace <workspace-id>",
271
+ setWorkspace: "mutagent workspaces use <name>",
272
272
  listWorkspaces: "mutagent workspaces list"
273
273
  };
274
274
  }
@@ -290,7 +290,7 @@ function handleError(error, isJson) {
290
290
  console.log(JSON.stringify(scrubKnownDeep(jsonOutput), null, 2));
291
291
  } else {
292
292
  console.error(`Error: ${scrubKnown(error.message)}`);
293
- if (error instanceof AuthenticationError) {
293
+ if (error.name === "TenancyError") {} else if (error instanceof AuthenticationError) {
294
294
  console.error("");
295
295
  console.error(AUTH_REMEDIATION_MESSAGE);
296
296
  } else if (error instanceof WorkspaceContextError) {
@@ -393,14 +393,14 @@ var init_errors = __esm(() => {
393
393
  }
394
394
  };
395
395
  WORKSPACE_REMEDIATION_MESSAGE = [
396
- "Workspace context missing. To fix:",
397
- " mutagent config set workspace <workspace-id> # Set default workspace",
398
- " mutagent workspaces list # List available workspaces"
396
+ "No workspace selected. To fix:",
397
+ " mutagent workspaces use <name> # Select the workspace every later command works in",
398
+ " mutagent workspaces list # Your workspaces"
399
399
  ].join(`
400
400
  `);
401
401
  WorkspaceContextError = class WorkspaceContextError extends MutagentError {
402
402
  constructor(message) {
403
- super("WORKSPACE_REQUIRED", message ?? "Workspace context is required but not configured", "Run: mutagent config set workspace <workspace-id>", 3);
403
+ super("WORKSPACE_REQUIRED", message ?? "Workspace context is required but not configured", "Run: mutagent workspaces use <name>", 3);
404
404
  }
405
405
  };
406
406
  ValidationError = class ValidationError extends MutagentError {
@@ -16519,6 +16519,448 @@ function writeSecureFile(path, contents) {
16519
16519
  var SECURE_FILE_MODE = 384;
16520
16520
  var init_secure_file = () => {};
16521
16521
 
16522
+ // src/lib/config.ts
16523
+ import { cosmiconfigSync } from "cosmiconfig";
16524
+ import { z } from "zod";
16525
+ import { homedir as homedir2 } from "os";
16526
+ import { join as join7 } from "path";
16527
+ import { existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync } from "fs";
16528
+ function parseJsonSafe(content, schema4) {
16529
+ try {
16530
+ const parsed = JSON.parse(content);
16531
+ return schema4.parse(parsed);
16532
+ } catch {
16533
+ return null;
16534
+ }
16535
+ }
16536
+ function loadConfig() {
16537
+ const explorer = cosmiconfigSync("mutagent");
16538
+ const result = explorer.search();
16539
+ const rcConfig = result?.config;
16540
+ let credentials = null;
16541
+ if (existsSync2(CREDENTIALS_FILE)) {
16542
+ credentials = parseJsonSafe(readFileSync2(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
16543
+ }
16544
+ const envKey = process.env.MUTAGENT_API_KEY;
16545
+ const fileDescribesKey = envKey === undefined || envKey === credentials?.apiKey;
16546
+ const envWorkspace = process.env.MUTAGENT_WORKSPACE_ID?.trim() ?? "";
16547
+ const fileSelection = fileDescribesKey ? credentials?.defaultWorkspace : undefined;
16548
+ const selected = fileSelection ?? rcConfig?.defaultWorkspace;
16549
+ const defaultWorkspace = envWorkspace !== "" ? envWorkspace : selected;
16550
+ const merged = {
16551
+ providers: {},
16552
+ environment: "production",
16553
+ ...rcConfig,
16554
+ ...credentials,
16555
+ apiKey: envKey ?? credentials?.apiKey ?? rcConfig?.apiKey,
16556
+ endpoint: process.env.MUTAGENT_ENDPOINT ?? credentials?.endpoint ?? rcConfig?.endpoint,
16557
+ defaultWorkspace,
16558
+ workspaceOrigin: envWorkspace !== "" ? "override" : selected ? "selection" : undefined,
16559
+ workspaceName: envWorkspace === "" && fileSelection !== undefined && fileSelection === selected ? credentials?.workspaceName : undefined,
16560
+ defaultOrganization: fileDescribesKey ? credentials?.defaultOrganization ?? rcConfig?.defaultOrganization : undefined,
16561
+ organizationName: fileDescribesKey ? credentials?.organizationName : undefined,
16562
+ organizationSlug: fileDescribesKey ? credentials?.organizationSlug : undefined,
16563
+ keyScope: fileDescribesKey ? credentials?.keyScope : undefined
16564
+ };
16565
+ return configSchema.parse(merged);
16566
+ }
16567
+ function readCredentialsFile() {
16568
+ if (!existsSync2(CREDENTIALS_FILE))
16569
+ return {};
16570
+ try {
16571
+ return JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf-8"));
16572
+ } catch {
16573
+ return {};
16574
+ }
16575
+ }
16576
+ function updateCredentials(patch) {
16577
+ if (!existsSync2(CREDENTIALS_DIR)) {
16578
+ mkdirSync(CREDENTIALS_DIR, { recursive: true });
16579
+ }
16580
+ const updated = Object.fromEntries(Object.entries({ ...readCredentialsFile(), ...patch }).filter(([key, value2]) => value2 !== undefined && key !== "defaultWorkspaceSource"));
16581
+ writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
16582
+ }
16583
+ function saveCredentials(apiKey, endpoint) {
16584
+ if (!existsSync2(CREDENTIALS_DIR)) {
16585
+ mkdirSync(CREDENTIALS_DIR, { recursive: true });
16586
+ }
16587
+ const config = loadConfig();
16588
+ const credentials = {
16589
+ apiKey,
16590
+ endpoint: endpoint ?? config.endpoint
16591
+ };
16592
+ writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
16593
+ }
16594
+ function saveFullCredentials(creds) {
16595
+ const existing = readCredentialsFile();
16596
+ updateCredentials({
16597
+ apiKey: creds.apiKey,
16598
+ endpoint: creds.endpoint ?? existing.endpoint ?? "https://api.mutagent.io",
16599
+ defaultWorkspace: creds.workspaceId,
16600
+ workspaceName: creds.workspaceName,
16601
+ defaultOrganization: creds.organizationId,
16602
+ organizationName: creds.organizationName,
16603
+ organizationSlug: creds.organizationSlug,
16604
+ keyScope: creds.keyScope,
16605
+ expiresAt: creds.expiresAt
16606
+ });
16607
+ }
16608
+ function clearCredentials() {
16609
+ if (existsSync2(CREDENTIALS_FILE)) {
16610
+ writeSecureFile(CREDENTIALS_FILE, JSON.stringify({}));
16611
+ }
16612
+ }
16613
+ function hasCredentials() {
16614
+ if (process.env.MUTAGENT_API_KEY)
16615
+ return true;
16616
+ if (!existsSync2(CREDENTIALS_FILE))
16617
+ return false;
16618
+ const credentials = parseJsonSafe(readFileSync2(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
16619
+ return credentials?.apiKey !== undefined && credentials.apiKey !== "";
16620
+ }
16621
+ function envKeyReplacesFile() {
16622
+ const envKey = process.env.MUTAGENT_API_KEY;
16623
+ if (envKey === undefined)
16624
+ return false;
16625
+ if (!existsSync2(CREDENTIALS_FILE))
16626
+ return true;
16627
+ const credentials = parseJsonSafe(readFileSync2(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
16628
+ return envKey !== credentials?.apiKey;
16629
+ }
16630
+ function getApiKey() {
16631
+ const apiKey = process.env.MUTAGENT_API_KEY ?? loadConfig().apiKey;
16632
+ registerSecret(apiKey);
16633
+ return apiKey;
16634
+ }
16635
+ function setWorkspaceSelection(workspaceId, workspaceName) {
16636
+ updateCredentials({ defaultWorkspace: workspaceId, workspaceName });
16637
+ }
16638
+ function recordKeyScope(self) {
16639
+ updateCredentials({
16640
+ keyScope: self.scope,
16641
+ defaultOrganization: self.organization?.id,
16642
+ organizationName: self.organization?.name,
16643
+ organizationSlug: self.organization?.slug,
16644
+ ...self.scope === "workspace" && self.workspace ? { defaultWorkspace: self.workspace.id, workspaceName: self.workspace.name } : {}
16645
+ });
16646
+ }
16647
+ var keyScopeSchema, configSchema, credentialsSchema, CREDENTIALS_DIR, CREDENTIALS_FILE;
16648
+ var init_config = __esm(() => {
16649
+ init_secure_file();
16650
+ init_known_secrets();
16651
+ keyScopeSchema = z.enum(["organization", "workspace", "user"]);
16652
+ configSchema = z.object({
16653
+ apiKey: z.string().optional(),
16654
+ endpoint: z.string().default("https://api.mutagent.io"),
16655
+ format: z.enum(["table", "json"]).default("table"),
16656
+ timeout: z.number().default(30000),
16657
+ defaultWorkspace: z.string().optional(),
16658
+ workspaceOrigin: z.enum(["override", "selection"]).optional(),
16659
+ workspaceName: z.string().optional(),
16660
+ defaultOrganization: z.string().optional(),
16661
+ organizationName: z.string().optional(),
16662
+ organizationSlug: z.string().optional(),
16663
+ keyScope: keyScopeSchema.optional()
16664
+ });
16665
+ credentialsSchema = z.object({
16666
+ apiKey: z.string().optional(),
16667
+ endpoint: z.string().optional(),
16668
+ defaultWorkspace: z.string().optional(),
16669
+ workspaceName: z.string().optional(),
16670
+ defaultOrganization: z.string().optional(),
16671
+ organizationName: z.string().optional(),
16672
+ organizationSlug: z.string().optional(),
16673
+ keyScope: keyScopeSchema.optional(),
16674
+ expiresAt: z.string().optional()
16675
+ }).loose();
16676
+ CREDENTIALS_DIR = join7(homedir2(), ".config", "mutagent");
16677
+ CREDENTIALS_FILE = join7(CREDENTIALS_DIR, "credentials.json");
16678
+ });
16679
+
16680
+ // src/lib/organization-scope.ts
16681
+ var exports_organization_scope = {};
16682
+ __export(exports_organization_scope, {
16683
+ unreachableServerError: () => unreachableServerError,
16684
+ tenancyHeaders: () => tenancyHeaders,
16685
+ tenancyFailure: () => tenancyFailure,
16686
+ setWorkspaceFlag: () => setWorkspaceFlag,
16687
+ setOrgAssertion: () => setOrgAssertion,
16688
+ resolveKeyTenancy: () => resolveKeyTenancy,
16689
+ otherOrganizationHint: () => otherOrganizationHint,
16690
+ orgMismatch: () => orgMismatch,
16691
+ noWorkspaceSelected: () => noWorkspaceSelected,
16692
+ matchesOrganization: () => matchesOrganization,
16693
+ looksLikeWorkspaceId: () => looksLikeWorkspaceId,
16694
+ listKeyWorkspaces: () => listKeyWorkspaces,
16695
+ findWorkspace: () => findWorkspace,
16696
+ fetchOrganizations: () => fetchOrganizations,
16697
+ fetchKeySelf: () => fetchKeySelf,
16698
+ envKeyHasNoSelection: () => envKeyHasNoSelection,
16699
+ deadKeyError: () => deadKeyError,
16700
+ configTenancyHeaders: () => configTenancyHeaders,
16701
+ TenancyError: () => TenancyError,
16702
+ TENANCY_DENIED: () => TENANCY_DENIED
16703
+ });
16704
+ function looksLikeWorkspaceId(value2) {
16705
+ return UUID_PATTERN.test(value2);
16706
+ }
16707
+ function tenancyHeaders(t) {
16708
+ const headers = {};
16709
+ if (!t.workspaceId)
16710
+ return headers;
16711
+ if (t.keyScope === "workspace") {
16712
+ if (t.workspaceOrigin === "override")
16713
+ headers["x-workspace-id"] = t.workspaceId;
16714
+ return headers;
16715
+ }
16716
+ headers["x-workspace-id"] = t.workspaceId;
16717
+ if (t.keyScope === "user" && t.organizationId)
16718
+ headers["x-organization-id"] = t.organizationId;
16719
+ return headers;
16720
+ }
16721
+ function configTenancyHeaders(config) {
16722
+ return tenancyHeaders({
16723
+ keyScope: config.keyScope,
16724
+ workspaceId: config.defaultWorkspace,
16725
+ workspaceOrigin: config.workspaceOrigin,
16726
+ organizationId: config.defaultOrganization
16727
+ });
16728
+ }
16729
+ function parseBody(body) {
16730
+ try {
16731
+ const parsed = JSON.parse(body);
16732
+ if (parsed && typeof parsed === "object")
16733
+ return parsed;
16734
+ } catch {}
16735
+ return {};
16736
+ }
16737
+ function deadKeyError() {
16738
+ return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login", 2);
16739
+ }
16740
+ function tenancyFailure(status, body, ctx) {
16741
+ const parsed = parseBody(body);
16742
+ if (status === 401)
16743
+ return deadKeyError();
16744
+ if (status === 403 && parsed.code === TENANCY_DENIED) {
16745
+ const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
16746
+ if (ctx.workspaceOrigin === "selection") {
16747
+ return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
16748
+ }
16749
+ const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
16750
+ return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
16751
+ }
16752
+ if (status === 403 && ctx.keyScope === "workspace" && ctx.workspaceOrigin === "override") {
16753
+ return new TenancyError("WORKSPACE_KEY_MISMATCH", "This API key only works in its own workspace.", "Run: mutagent login (for a key that can switch workspaces)");
16754
+ }
16755
+ if (status === 400 && (parsed.message ?? body).toLowerCase().includes("x-workspace-id")) {
16756
+ return noWorkspaceSelected();
16757
+ }
16758
+ return;
16759
+ }
16760
+ function noWorkspaceSelected() {
16761
+ const fix = envKeyReplacesFile() ? ENV_KEY_WORKSPACE_FIX : "Run: mutagent workspaces use <name>";
16762
+ return new TenancyError("WORKSPACE_REQUIRED", "No workspace selected.", fix, 3);
16763
+ }
16764
+ function envKeyHasNoSelection() {
16765
+ return new TenancyError("WORKSPACE_FROM_ENV", "With MUTAGENT_API_KEY set, the workspace comes from --workspace or MUTAGENT_WORKSPACE_ID.", ENV_KEY_WORKSPACE_FIX);
16766
+ }
16767
+ async function tenancyFetch(url, init) {
16768
+ try {
16769
+ return await fetch(url, init);
16770
+ } catch {
16771
+ throw unreachableServerError(url);
16772
+ }
16773
+ }
16774
+ function unreachableServerError(url) {
16775
+ let origin = url;
16776
+ try {
16777
+ origin = new URL(url).origin;
16778
+ } catch {}
16779
+ return new MutagentError("REQUEST_FAILED", `Unable to connect to the Mutagent server at ${origin}.`, "Check your connection and the endpoint: mutagent config get endpoint");
16780
+ }
16781
+ async function fetchKeySelf(apiKey, endpoint, opts = {}) {
16782
+ const response = await tenancyFetch(`${endpoint}/api/api-keys/self`, {
16783
+ headers: { "x-api-key": apiKey, ...opts.headers }
16784
+ });
16785
+ if (!response.ok) {
16786
+ const body = await response.text().catch(() => "");
16787
+ throw tenancyFailure(response.status, body, opts.ctx ?? {}) ?? new MutagentError("KEY_SCOPE_UNAVAILABLE", `Could not read this API key's scope (${String(response.status)}).`, "Check the endpoint with: mutagent config get endpoint");
16788
+ }
16789
+ return await response.json();
16790
+ }
16791
+ async function fetchOrganizations(apiKey, endpoint) {
16792
+ try {
16793
+ const response = await fetch(`${endpoint}/api/organizations`, {
16794
+ headers: { "x-api-key": apiKey }
16795
+ });
16796
+ if (!response.ok)
16797
+ return [];
16798
+ const data = await response.json();
16799
+ return data.data ?? [];
16800
+ } catch {
16801
+ return [];
16802
+ }
16803
+ }
16804
+ function matchesOrganization(value2, org) {
16805
+ if (!org)
16806
+ return false;
16807
+ if (value2 === org.id || org.slug !== undefined && value2 === org.slug)
16808
+ return true;
16809
+ return org.name !== undefined && value2.toLowerCase() === org.name.toLowerCase();
16810
+ }
16811
+ function orgMismatch(value2, org) {
16812
+ const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
16813
+ return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${value2}`);
16814
+ }
16815
+ function otherOrganizationHint(wanted, keyOrg, orgs) {
16816
+ const others = orgs.filter((o) => o.id !== keyOrg?.id);
16817
+ const where = keyOrg ? ` in ${keyOrg.name}` : "";
16818
+ const what = `No workspace "${wanted}"${where}.`;
16819
+ const named = others.map((o) => `${o.name} (${o.slug ?? o.id})`).join(", ");
16820
+ if (others.length === 1 && others[0]) {
16821
+ const only = others[0];
16822
+ return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organization: ${named}.`, `To switch: mutagent login --org ${only.slug ?? only.id}`);
16823
+ }
16824
+ if (others.length > 1) {
16825
+ return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organizations: ${named}.`, "To switch: mutagent login --org <slug>");
16826
+ }
16827
+ return new TenancyError("WORKSPACE_NOT_FOUND", what, "See: mutagent workspaces list");
16828
+ }
16829
+ function setOrgAssertion(value2) {
16830
+ const trimmed = value2?.trim();
16831
+ orgAssertion = trimmed === "" ? undefined : trimmed;
16832
+ }
16833
+ function setWorkspaceFlag(value2) {
16834
+ const trimmed = value2?.trim();
16835
+ workspaceFlag = trimmed === "" ? undefined : trimmed;
16836
+ }
16837
+ function endpointOf(configured) {
16838
+ const trimmed = configured?.trim();
16839
+ return (trimmed === undefined || trimmed === "" ? "https://api.mutagent.io" : trimmed).replace(/\/+$/, "");
16840
+ }
16841
+ async function probeKeyScope(apiKey, endpoint, workspaceNamed) {
16842
+ const fromFile = !envKeyReplacesFile();
16843
+ if (!fromFile && workspaceNamed && orgAssertion === undefined)
16844
+ return;
16845
+ try {
16846
+ const self = await fetchKeySelf(apiKey, endpoint);
16847
+ if (fromFile)
16848
+ recordKeyScope(self);
16849
+ return self;
16850
+ } catch (error3) {
16851
+ if (error3 instanceof MutagentError && (error3.code === "AUTH_EXPIRED" || error3.code === "REQUEST_FAILED"))
16852
+ throw error3;
16853
+ return;
16854
+ }
16855
+ }
16856
+ function enforceOrgAssertion(org) {
16857
+ if (orgAssertion === undefined)
16858
+ return;
16859
+ if (org === undefined) {
16860
+ throw new TenancyError("ORG_UNKNOWN", "This key's organization is unknown, so --org cannot be checked.", "Run: mutagent workspaces current");
16861
+ }
16862
+ if (!matchesOrganization(orgAssertion, org)) {
16863
+ throw orgMismatch(orgAssertion, org);
16864
+ }
16865
+ }
16866
+ async function resolveKeyTenancy(apiKey, opts = {}) {
16867
+ const config = loadConfig();
16868
+ const endpoint = endpointOf(config.endpoint);
16869
+ let keyScope = config.keyScope;
16870
+ let organization = config.defaultOrganization ? { id: config.defaultOrganization, name: config.organizationName ?? config.defaultOrganization, slug: config.organizationSlug } : undefined;
16871
+ let workspaceId = config.defaultWorkspace;
16872
+ let workspaceName = config.workspaceName;
16873
+ const workspaceOrigin = config.workspaceOrigin;
16874
+ if (keyScope === undefined && (opts.probe !== false || orgAssertion !== undefined)) {
16875
+ const self = await probeKeyScope(apiKey, endpoint, workspaceId !== undefined);
16876
+ if (self) {
16877
+ keyScope = self.scope;
16878
+ organization = self.organization ?? undefined;
16879
+ if (self.scope === "workspace" && self.workspace && workspaceOrigin !== "override") {
16880
+ workspaceId = self.workspace.id;
16881
+ workspaceName = self.workspace.name;
16882
+ }
16883
+ }
16884
+ }
16885
+ enforceOrgAssertion(organization);
16886
+ if (opts.resolveOverride !== false && workspaceFlag !== undefined && workspaceId === workspaceFlag && !looksLikeWorkspaceId(workspaceId)) {
16887
+ const found = await findWorkspace(workspaceId, { apiKey, endpoint, keyScope, organization, workspaceId: config.defaultWorkspace, workspaceName });
16888
+ workspaceId = found.id;
16889
+ workspaceName = found.name;
16890
+ process.env.MUTAGENT_WORKSPACE_ID = found.id;
16891
+ }
16892
+ return { endpoint, keyScope, organization, workspaceId, workspaceName, workspaceOrigin };
16893
+ }
16894
+ async function listKeyWorkspaces(apiKey, endpoint, t) {
16895
+ const headers = { "x-api-key": apiKey };
16896
+ if (t.keyScope === "user" && t.organization)
16897
+ headers["x-organization-id"] = t.organization.id;
16898
+ const response = await tenancyFetch(`${endpoint}/api/workspaces`, { headers });
16899
+ if (!response.ok) {
16900
+ const body = await response.text().catch(() => "");
16901
+ throw tenancyFailure(response.status, body, { keyScope: t.keyScope, organizationName: t.organization?.name }) ?? new MutagentError("REQUEST_FAILED", `Could not list workspaces (${String(response.status)}).`, "Run: mutagent workspaces current");
16902
+ }
16903
+ const data = await response.json();
16904
+ return data.workspaces ?? [];
16905
+ }
16906
+ async function findWorkspace(wanted, t) {
16907
+ if (t.keyScope === "workspace") {
16908
+ if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
16909
+ return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
16910
+ }
16911
+ throw new TenancyError("WORKSPACE_KEY_MISMATCH", `This key only works in ${t.workspaceName ?? t.workspaceId ?? "its own workspace"}.`, "Run: mutagent login (for a key that can switch workspaces)");
16912
+ }
16913
+ const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
16914
+ const byId = rows.find((w) => w.id === wanted);
16915
+ if (byId)
16916
+ return { id: byId.id, name: byId.name };
16917
+ let matches = rows.filter((w) => w.name === wanted || w.slug === wanted);
16918
+ if (matches.length === 0)
16919
+ matches = rows.filter((w) => w.name.toLowerCase() === wanted.toLowerCase());
16920
+ if (matches.length === 1 && matches[0])
16921
+ return { id: matches[0].id, name: matches[0].name };
16922
+ if (matches.length > 1) {
16923
+ throw new TenancyError("WORKSPACE_AMBIGUOUS", `Several workspaces are named "${wanted}": ${matches.map((w) => `${w.name} (${w.id})`).join(", ")}.`, "Run: mutagent workspaces use <id>");
16924
+ }
16925
+ const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
16926
+ throw otherOrganizationHint(wanted, t.organization, orgs);
16927
+ }
16928
+ var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag;
16929
+ var init_organization_scope = __esm(() => {
16930
+ init_errors();
16931
+ init_config();
16932
+ UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16933
+ TenancyError = class TenancyError extends MutagentError {
16934
+ constructor(code, what, fix, exitCode = 1) {
16935
+ super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""), exitCode);
16936
+ this.name = "TenancyError";
16937
+ }
16938
+ };
16939
+ });
16940
+
16941
+ // src/lib/provider-request.ts
16942
+ function buildCreateProviderBody(data) {
16943
+ const body = {
16944
+ name: data.name,
16945
+ provider: data.provider,
16946
+ isDefault: data.isDefault ?? false
16947
+ };
16948
+ if (data.apiKey)
16949
+ body.apiKey = data.apiKey;
16950
+ if (data.baseUrl)
16951
+ body.baseUrl = data.baseUrl;
16952
+ if (data.hostedFamily)
16953
+ body.hostedFamily = data.hostedFamily;
16954
+ if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
16955
+ body.credentialFields = data.credentialFields;
16956
+ }
16957
+ if (data.metadata && Object.keys(data.metadata).length > 0)
16958
+ body.metadata = data.metadata;
16959
+ if (data.supportedModels !== undefined)
16960
+ body.supportedModels = data.supportedModels;
16961
+ return body;
16962
+ }
16963
+
16522
16964
  // src/lib/sdk-debug.ts
16523
16965
  function isDebugEnabled(env = process.env) {
16524
16966
  return TRUTHY.has((env.MUTAGENT_DEBUG ?? "").trim().toLowerCase());
@@ -16613,315 +17055,39 @@ var init_sdk_debug = __esm(() => {
16613
17055
  };
16614
17056
  });
16615
17057
 
16616
- // src/lib/organization-scope.ts
16617
- function isWellFormedOrganizationId(id) {
16618
- return ORGANIZATION_ID_PATTERN.test(id);
17058
+ // src/lib/sdk-mappers.ts
17059
+ import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
17060
+ function isProviderType(value2) {
17061
+ return PROVIDER_TYPES.includes(value2);
16619
17062
  }
16620
- function isOrganizationScopeMismatch(status, body) {
16621
- if (status !== 403)
16622
- return false;
16623
- return body.toLowerCase().includes("organization scope mismatch");
17063
+ function narrowProviderType(value2) {
17064
+ return isProviderType(value2) ? value2 : value2;
16624
17065
  }
16625
- async function lookupOrganizations(apiKey, endpoint) {
16626
- try {
16627
- const response = await fetch(`${endpoint}/api/organizations`, {
16628
- headers: { "x-api-key": apiKey }
16629
- });
16630
- if (!response.ok)
16631
- return [];
16632
- const data = await response.json();
16633
- return data.data ?? [];
16634
- } catch {
16635
- return [];
16636
- }
17066
+ function toIsoString(value2) {
17067
+ if (value2 === null || value2 === undefined)
17068
+ return null;
17069
+ return value2 instanceof Date ? value2.toISOString() : value2;
16637
17070
  }
16638
- async function fetchOrganizations(apiKey, endpoint) {
16639
- return lookupOrganizations(apiKey, endpoint);
16640
- }
16641
- async function assertUsableOrganizationId(id, lookup) {
16642
- const orgs = lookup.apiKey ? await lookup.fetchOrganizations(lookup.apiKey, lookup.endpoint).catch(() => []) : [];
16643
- if (orgs.length === 0) {
16644
- if (isWellFormedOrganizationId(id))
16645
- return;
16646
- throw new MutagentError("INVALID_ORGANIZATION_ID", `"${id}" is not an organization id.`, `Organization ids look like org_xxxxxxxxxxxxxxxxxxxxx.
16647
- ` + "Log in so the id can be checked against your account: mutagent auth login --browser");
16648
- }
16649
- if (orgs.some((org) => org.id === id))
16650
- return;
16651
- throw new MutagentError("UNKNOWN_ORGANIZATION", `"${id}" is not an organization this API key can access.`, `Available: ${orgs.map((org) => org.id).join(", ")}
16652
- ` + "A stored organization that does not match the key makes every request fail with 403.");
16653
- }
16654
- function createOrgScopeRecoveringFetcher(hooks) {
16655
- let recoveryUsed = false;
16656
- async function recover() {
16657
- if (recoveryUsed)
16658
- return;
16659
- recoveryUsed = true;
16660
- const stale = hooks.currentOrganization();
16661
- if (!stale)
16662
- return;
16663
- hooks.adoptOrganization(undefined);
16664
- hooks.forgetStoredOrganization?.();
16665
- const orgs = await lookupOrganizations(hooks.apiKey, hooks.endpoint);
16666
- const resolved = orgs[0]?.id;
16667
- if (!resolved || resolved === stale)
16668
- return;
16669
- hooks.adoptOrganization(resolved);
16670
- if (isDebugEnabled()) {
16671
- process.stderr.write(`[mutagent] Stored organization was rejected by the API; re-resolved and retried once.
16672
- `);
16673
- }
16674
- return resolved;
16675
- }
16676
- return async function fetchWithOrgRecovery(input, init) {
16677
- const retryTemplate = input instanceof Request ? input.clone() : undefined;
16678
- const response = init == null ? await fetch(input) : await fetch(input, init);
16679
- if (response.status !== 403)
16680
- return response;
16681
- const body = await response.clone().text().catch(() => "");
16682
- if (!isOrganizationScopeMismatch(response.status, body))
16683
- return response;
16684
- const resolved = await recover();
16685
- if (!resolved)
16686
- return response;
16687
- if (retryTemplate) {
16688
- retryTemplate.headers.set("x-organization-id", resolved);
16689
- return fetch(retryTemplate);
16690
- }
16691
- const headers = new Headers(init?.headers);
16692
- headers.set("x-organization-id", resolved);
16693
- return fetch(input, { ...init, headers });
16694
- };
16695
- }
16696
- var ORGANIZATION_ID_PATTERN;
16697
- var init_organization_scope = __esm(() => {
16698
- init_errors();
16699
- init_sdk_debug();
16700
- ORGANIZATION_ID_PATTERN = /^org_[A-Za-z0-9_-]{8,}$/;
16701
- });
16702
-
16703
- // src/lib/config.ts
16704
- import { cosmiconfigSync } from "cosmiconfig";
16705
- import { z } from "zod";
16706
- import { homedir as homedir2 } from "os";
16707
- import { join as join7 } from "path";
16708
- import { existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync } from "fs";
16709
- function parseJsonSafe(content, schema4) {
16710
- try {
16711
- const parsed = JSON.parse(content);
16712
- return schema4.parse(parsed);
16713
- } catch {
16714
- return null;
16715
- }
16716
- }
16717
- function loadConfig() {
16718
- const explorer = cosmiconfigSync("mutagent");
16719
- const result = explorer.search();
16720
- const rcConfig = result?.config;
16721
- let credentials = null;
16722
- if (existsSync2(CREDENTIALS_FILE)) {
16723
- credentials = parseJsonSafe(readFileSync2(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
16724
- }
16725
- const merged = {
16726
- providers: {},
16727
- environment: "production",
16728
- ...rcConfig,
16729
- ...credentials,
16730
- apiKey: process.env.MUTAGENT_API_KEY ?? credentials?.apiKey ?? rcConfig?.apiKey,
16731
- endpoint: process.env.MUTAGENT_ENDPOINT ?? credentials?.endpoint ?? rcConfig?.endpoint,
16732
- defaultWorkspace: process.env.MUTAGENT_WORKSPACE_ID ?? credentials?.defaultWorkspace ?? rcConfig?.defaultWorkspace,
16733
- defaultWorkspaceSource: process.env.MUTAGENT_WORKSPACE_ID ? "user" : credentials?.defaultWorkspace ? credentials.defaultWorkspaceSource ?? "login-inferred" : rcConfig?.defaultWorkspace ? "login-inferred" : undefined,
16734
- defaultOrganization: credentials?.defaultOrganization ?? rcConfig?.defaultOrganization
16735
- };
16736
- return configSchema.parse(merged);
16737
- }
16738
- function saveCredentials(apiKey, endpoint) {
16739
- if (!existsSync2(CREDENTIALS_DIR)) {
16740
- mkdirSync(CREDENTIALS_DIR, { recursive: true });
16741
- }
16742
- const config = loadConfig();
16743
- const credentials = {
16744
- apiKey,
16745
- endpoint: endpoint ?? config.endpoint
16746
- };
16747
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
16748
- }
16749
- function saveFullCredentials(creds) {
16750
- if (!existsSync2(CREDENTIALS_DIR)) {
16751
- mkdirSync(CREDENTIALS_DIR, { recursive: true });
16752
- }
16753
- let existingCredentials = {};
16754
- if (existsSync2(CREDENTIALS_FILE)) {
16755
- try {
16756
- existingCredentials = JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf-8"));
16757
- } catch {}
16758
- }
16759
- const existingSource = existingCredentials.defaultWorkspaceSource;
16760
- const workspaceChanged = creds.workspaceId !== undefined && creds.workspaceId !== existingCredentials.defaultWorkspace;
16761
- const source2 = creds.workspaceId === undefined ? existingSource : workspaceChanged ? "login-inferred" : existingSource ?? "login-inferred";
16762
- const credentials = {
16763
- ...existingCredentials,
16764
- apiKey: creds.apiKey,
16765
- endpoint: creds.endpoint ?? existingCredentials.endpoint ?? "https://api.mutagent.io",
16766
- defaultWorkspace: creds.workspaceId ?? existingCredentials.defaultWorkspace,
16767
- defaultWorkspaceSource: source2,
16768
- defaultOrganization: creds.organizationId ?? existingCredentials.defaultOrganization,
16769
- expiresAt: creds.expiresAt
16770
- };
16771
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
16772
- }
16773
- function clearCredentials() {
16774
- if (existsSync2(CREDENTIALS_FILE)) {
16775
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify({}));
16776
- }
16777
- }
16778
- function hasCredentials() {
16779
- if (process.env.MUTAGENT_API_KEY)
16780
- return true;
16781
- if (!existsSync2(CREDENTIALS_FILE))
16782
- return false;
16783
- const credentials = parseJsonSafe(readFileSync2(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
16784
- return credentials?.apiKey !== undefined && credentials.apiKey !== "";
16785
- }
16786
- function getApiKey() {
16787
- const apiKey = process.env.MUTAGENT_API_KEY ?? loadConfig().apiKey;
16788
- registerSecret(apiKey);
16789
- return apiKey;
16790
- }
16791
- function setDefaultWorkspace(workspaceId, source2 = "user") {
16792
- if (!existsSync2(CREDENTIALS_DIR)) {
16793
- mkdirSync(CREDENTIALS_DIR, { recursive: true });
16794
- }
16795
- let existingCredentials = {};
16796
- if (existsSync2(CREDENTIALS_FILE)) {
16797
- try {
16798
- existingCredentials = JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf-8"));
16799
- } catch {}
16800
- }
16801
- const updated = {
16802
- ...existingCredentials,
16803
- defaultWorkspace: workspaceId,
16804
- defaultWorkspaceSource: source2
16805
- };
16806
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
16807
- }
16808
- function setDefaultOrganization(organizationId) {
16809
- if (!isWellFormedOrganizationId(organizationId)) {
16810
- throw new MutagentError("INVALID_ORGANIZATION_ID", `"${organizationId}" is not an organization id.`, `Organization ids look like org_xxxxxxxxxxxxxxxxxxxxx.
16811
- ` + "List the ones you can use with: mutagent auth status");
16812
- }
16813
- if (!existsSync2(CREDENTIALS_DIR)) {
16814
- mkdirSync(CREDENTIALS_DIR, { recursive: true });
16815
- }
16816
- let existingCredentials = {};
16817
- if (existsSync2(CREDENTIALS_FILE)) {
16818
- try {
16819
- existingCredentials = JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf-8"));
16820
- } catch {}
16821
- }
16822
- const updated = {
16823
- ...existingCredentials,
16824
- defaultOrganization: organizationId
16825
- };
16826
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
16827
- }
16828
- function clearDefaultOrganization() {
16829
- if (!existsSync2(CREDENTIALS_FILE))
16830
- return;
16831
- let existingCredentials;
16832
- try {
16833
- existingCredentials = JSON.parse(readFileSync2(CREDENTIALS_FILE, "utf-8"));
16834
- } catch {
16835
- return;
16836
- }
16837
- if (!("defaultOrganization" in existingCredentials))
16838
- return;
16839
- delete existingCredentials.defaultOrganization;
16840
- writeSecureFile(CREDENTIALS_FILE, JSON.stringify(existingCredentials, null, 2));
16841
- }
16842
- var configSchema, credentialsSchema, CREDENTIALS_DIR, CREDENTIALS_FILE;
16843
- var init_config = __esm(() => {
16844
- init_secure_file();
16845
- init_known_secrets();
16846
- init_organization_scope();
16847
- init_errors();
16848
- configSchema = z.object({
16849
- apiKey: z.string().optional(),
16850
- endpoint: z.string().default("https://api.mutagent.io"),
16851
- format: z.enum(["table", "json"]).default("table"),
16852
- timeout: z.number().default(30000),
16853
- defaultWorkspace: z.string().optional(),
16854
- defaultWorkspaceSource: z.enum(["user", "login-inferred"]).optional(),
16855
- defaultOrganization: z.string().optional()
16856
- });
16857
- credentialsSchema = z.object({
16858
- apiKey: z.string().optional(),
16859
- endpoint: z.string().optional(),
16860
- defaultWorkspace: z.string().optional(),
16861
- defaultWorkspaceSource: z.enum(["user", "login-inferred"]).optional(),
16862
- defaultOrganization: z.string().optional(),
16863
- expiresAt: z.string().optional()
16864
- }).loose();
16865
- CREDENTIALS_DIR = join7(homedir2(), ".config", "mutagent");
16866
- CREDENTIALS_FILE = join7(CREDENTIALS_DIR, "credentials.json");
16867
- });
16868
-
16869
- // src/lib/provider-request.ts
16870
- function buildCreateProviderBody(data) {
16871
- const body = {
16872
- name: data.name,
16873
- provider: data.provider,
16874
- isDefault: data.isDefault ?? false
16875
- };
16876
- if (data.apiKey)
16877
- body.apiKey = data.apiKey;
16878
- if (data.baseUrl)
16879
- body.baseUrl = data.baseUrl;
16880
- if (data.hostedFamily)
16881
- body.hostedFamily = data.hostedFamily;
16882
- if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
16883
- body.credentialFields = data.credentialFields;
16884
- }
16885
- if (data.metadata && Object.keys(data.metadata).length > 0)
16886
- body.metadata = data.metadata;
16887
- if (data.supportedModels !== undefined)
16888
- body.supportedModels = data.supportedModels;
16889
- return body;
16890
- }
16891
-
16892
- // src/lib/sdk-mappers.ts
16893
- import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
16894
- function isProviderType(value2) {
16895
- return PROVIDER_TYPES.includes(value2);
16896
- }
16897
- function narrowProviderType(value2) {
16898
- return isProviderType(value2) ? value2 : value2;
16899
- }
16900
- function toIsoString(value2) {
16901
- if (value2 === null || value2 === undefined)
16902
- return null;
16903
- return value2 instanceof Date ? value2.toISOString() : value2;
16904
- }
16905
- function toAgent(sdk) {
16906
- return {
16907
- id: sdk.id,
16908
- name: sdk.name,
16909
- slug: sdk.slug,
16910
- description: sdk.description,
16911
- systemPrompt: sdk.systemPrompt,
16912
- model: sdk.model,
16913
- maxTurns: sdk.maxTurns,
16914
- permissionMode: sdk.permissionMode,
16915
- allowedTools: sdk.allowedTools,
16916
- disallowedTools: sdk.disallowedTools,
16917
- status: sdk.status,
16918
- isPublic: sdk.isPublic,
16919
- metadata: sdk.metadata,
16920
- tags: sdk.tags,
16921
- createdBy: sdk.createdBy,
16922
- createdAt: toIsoString(sdk.createdAt),
16923
- updatedAt: toIsoString(sdk.updatedAt)
16924
- };
17071
+ function toAgent(sdk) {
17072
+ return {
17073
+ id: sdk.id,
17074
+ name: sdk.name,
17075
+ slug: sdk.slug,
17076
+ description: sdk.description,
17077
+ systemPrompt: sdk.systemPrompt,
17078
+ model: sdk.model,
17079
+ maxTurns: sdk.maxTurns,
17080
+ permissionMode: sdk.permissionMode,
17081
+ allowedTools: sdk.allowedTools,
17082
+ disallowedTools: sdk.disallowedTools,
17083
+ status: sdk.status,
17084
+ isPublic: sdk.isPublic,
17085
+ metadata: sdk.metadata,
17086
+ tags: sdk.tags,
17087
+ createdBy: sdk.createdBy,
17088
+ createdAt: toIsoString(sdk.createdAt),
17089
+ updatedAt: toIsoString(sdk.updatedAt)
17090
+ };
16925
17091
  }
16926
17092
  function toAgentList(sdk) {
16927
17093
  return sdk.map(toAgent);
@@ -16977,15 +17143,6 @@ var init_sdk_mappers = __esm(() => {
16977
17143
  });
16978
17144
 
16979
17145
  // src/lib/sdk-client.ts
16980
- var exports_sdk_client = {};
16981
- __export(exports_sdk_client, {
16982
- validateApiKey: () => validateApiKey,
16983
- resetSDKClient: () => resetSDKClient,
16984
- getSDKClient: () => getSDKClient,
16985
- fetchWorkspaces: () => fetchWorkspaces,
16986
- fetchOrganizations: () => fetchOrganizations,
16987
- MutagentSDK: () => SDKClientWrapper
16988
- });
16989
17146
  import { Mutagent, HTTPClient } from "@mutagent/sdk";
16990
17147
 
16991
17148
  class SDKClientWrapper {
@@ -16994,32 +17151,24 @@ class SDKClientWrapper {
16994
17151
  endpoint;
16995
17152
  workspaceId;
16996
17153
  organizationId;
16997
- workspaceResolution = "none";
17154
+ tenancy;
16998
17155
  debugSecrets = new Set;
16999
- recoveringFetch;
17000
17156
  constructor(opts) {
17001
17157
  this.apiKey = opts.apiKey;
17002
17158
  this.endpoint = opts.serverURL ?? "http://localhost:3003";
17003
17159
  this.workspaceId = opts.workspaceId;
17004
17160
  this.organizationId = opts.organizationId;
17005
- if (opts.workspaceId && opts.workspaceSource === "user") {
17006
- this.workspaceResolution = "configured";
17007
- }
17008
- this.recoveringFetch = createOrgScopeRecoveringFetcher({
17009
- apiKey: this.apiKey,
17010
- endpoint: this.endpoint,
17011
- currentOrganization: () => this.organizationId,
17012
- adoptOrganization: (id) => {
17013
- this.organizationId = id;
17014
- },
17015
- forgetStoredOrganization: opts.clearStoredOrganization
17016
- });
17017
- const httpClient = new HTTPClient({ fetcher: this.recoveringFetch });
17161
+ this.tenancy = {
17162
+ keyScope: opts.keyScope,
17163
+ workspaceId: opts.workspaceId,
17164
+ workspaceName: opts.workspaceName,
17165
+ workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
17166
+ organizationName: opts.organizationName
17167
+ };
17168
+ const httpClient = new HTTPClient;
17018
17169
  httpClient.addHook("beforeRequest", (req) => {
17019
- if (this.workspaceId)
17020
- req.headers.set("x-workspace-id", this.workspaceId);
17021
- if (this.organizationId)
17022
- req.headers.set("x-organization-id", this.organizationId);
17170
+ for (const [name, value2] of Object.entries(this.tenancyHeaders()))
17171
+ req.headers.set(name, value2);
17023
17172
  return req;
17024
17173
  });
17025
17174
  this.debugSecrets.add(this.apiKey);
@@ -17034,8 +17183,16 @@ class SDKClientWrapper {
17034
17183
  });
17035
17184
  }
17036
17185
  handleError(error3) {
17186
+ if (error3 instanceof Error && SDK_TRANSPORT_ERRORS.has(error3.name)) {
17187
+ throw unreachableServerError(this.endpoint);
17188
+ }
17037
17189
  if (error3 && typeof error3 === "object") {
17038
17190
  const err = error3;
17191
+ if (err.statusCode) {
17192
+ const tenancy = tenancyFailure(err.statusCode, err.body ?? "", this.tenancy);
17193
+ if (tenancy)
17194
+ throw tenancy;
17195
+ }
17039
17196
  if (err.statusCode === 401) {
17040
17197
  throw new AuthenticationError;
17041
17198
  }
@@ -17056,7 +17213,7 @@ class SDKClientWrapper {
17056
17213
  }
17057
17214
  if (error3 instanceof TypeError && typeof error3.message === "string") {
17058
17215
  if (error3.message.includes("fetch") || error3.message.includes("network") || error3.message.includes("ECONNREFUSED")) {
17059
- throw new MutagentError("NETWORK_ERROR", "Unable to connect to the Mutagent server", "Check your internet connection and verify the server URL with: mutagent config list");
17216
+ throw unreachableServerError(this.endpoint);
17060
17217
  }
17061
17218
  }
17062
17219
  if (error3 instanceof Error) {
@@ -17074,28 +17231,31 @@ class SDKClientWrapper {
17074
17231
  } else if (optHeaders) {
17075
17232
  extraHeaders = optHeaders;
17076
17233
  }
17077
- const tenancyHeaders = {};
17078
- if (this.workspaceId)
17079
- tenancyHeaders["x-workspace-id"] = this.workspaceId;
17080
- if (this.organizationId)
17081
- tenancyHeaders["x-organization-id"] = this.organizationId;
17082
- const response = await this.recoveringFetch(`${this.endpoint}${path}`, {
17083
- ...options,
17084
- headers: {
17085
- "x-api-key": this.apiKey,
17086
- "Content-Type": "application/json",
17087
- ...tenancyHeaders,
17088
- ...extraHeaders
17089
- }
17090
- });
17234
+ let response;
17235
+ try {
17236
+ response = await fetch(`${this.endpoint}${path}`, {
17237
+ ...options,
17238
+ headers: {
17239
+ "x-api-key": this.apiKey,
17240
+ "Content-Type": "application/json",
17241
+ ...this.tenancyHeaders(),
17242
+ ...extraHeaders
17243
+ }
17244
+ });
17245
+ } catch {
17246
+ throw unreachableServerError(this.endpoint);
17247
+ }
17091
17248
  if (!response.ok) {
17249
+ const error3 = await response.text();
17250
+ const tenancy = tenancyFailure(response.status, error3, this.tenancy);
17251
+ if (tenancy)
17252
+ throw tenancy;
17092
17253
  if (response.status === 401) {
17093
17254
  throw new AuthenticationError;
17094
17255
  }
17095
17256
  if (response.status === 403) {
17096
- throw new ApiError(403, await response.text() || "Access denied");
17257
+ throw new ApiError(403, error3 || "Access denied");
17097
17258
  }
17098
- const error3 = await response.text();
17099
17259
  const errorLower = error3.toLowerCase();
17100
17260
  if (errorLower.includes("workspace") && (errorLower.includes("missing") || errorLower.includes("required")) || errorLower.includes("x-workspace-id")) {
17101
17261
  throw new WorkspaceContextError(error3);
@@ -17241,11 +17401,9 @@ class SDKClientWrapper {
17241
17401
  }
17242
17402
  async getModelsCatalog() {
17243
17403
  try {
17244
- const wsId = this.workspaceId;
17245
- if (!wsId)
17404
+ if (!this.workspaceId)
17246
17405
  return { models: [] };
17247
- const qs = `?workspaceId=${encodeURIComponent(wsId)}`;
17248
- return await this.request(`/api/providers/catalog${qs}`);
17406
+ return await this.request("/api/providers/catalog");
17249
17407
  } catch {
17250
17408
  return { models: [] };
17251
17409
  }
@@ -17253,56 +17411,17 @@ class SDKClientWrapper {
17253
17411
  getCurrentWorkspaceId() {
17254
17412
  return this.workspaceId;
17255
17413
  }
17256
- getCurrentOrgId() {
17257
- return this.organizationId;
17258
- }
17259
- async ensureContext() {
17260
- const needsWorkspaceClassification = !this.workspaceId || this.workspaceResolution === "none";
17261
- if (this.organizationId && !needsWorkspaceClassification)
17262
- return;
17263
- if (!this.organizationId) {
17264
- let orgs = await fetchOrganizations(this.apiKey, this.endpoint).catch(() => []);
17265
- if (orgs.length === 0) {
17266
- await new Promise((r) => setTimeout(r, 1000));
17267
- orgs = await fetchOrganizations(this.apiKey, this.endpoint).catch(() => []);
17268
- }
17269
- if (orgs.length > 0 && orgs[0]) {
17270
- this.organizationId = orgs[0].id;
17271
- }
17272
- }
17273
- if (this.workspaceResolution === "none" && this.organizationId) {
17274
- try {
17275
- const response = await this.sdk.workspaces.listWorkspaces({ includeInactive: false });
17276
- const workspaces = toWorkspaceList(response.workspaces);
17277
- if (workspaces.length === 1 && workspaces[0]) {
17278
- this.workspaceId = workspaces[0].id;
17279
- this.workspaceResolution = "sole";
17280
- } else if (workspaces.length > 1) {
17281
- const defaultWs = workspaces.find((ws) => ws.isDefault);
17282
- if (defaultWs) {
17283
- this.workspaceId = defaultWs.id;
17284
- this.workspaceResolution = "configured";
17285
- } else {
17286
- const first = workspaces[0] ?? undefined;
17287
- if (first) {
17288
- this.workspaceId = this.workspaceId ?? first.id;
17289
- this.workspaceResolution = "ambiguous";
17290
- }
17291
- }
17292
- }
17293
- } catch {}
17294
- }
17295
- }
17296
- getWorkspaceResolution() {
17297
- return this.workspaceResolution;
17414
+ tenancyHeaders() {
17415
+ return tenancyHeaders({
17416
+ keyScope: this.tenancy.keyScope,
17417
+ workspaceId: this.workspaceId,
17418
+ workspaceOrigin: this.tenancy.workspaceOrigin,
17419
+ organizationId: this.organizationId
17420
+ });
17298
17421
  }
17299
- requireUnambiguousWorkspace() {
17300
- if (!this.workspaceId || this.workspaceResolution === "none") {
17301
- throw new MutagentError("WORKSPACE_REQUIRED", "No workspace configured.", "Set one with: mutagent config set workspace <workspace-id> (list them with: mutagent workspaces list)");
17302
- }
17303
- if (this.workspaceResolution === "ambiguous") {
17304
- throw new MutagentError("WORKSPACE_AMBIGUOUS", "Several workspaces are available and none is marked default, so the target for this write is ambiguous.", "Choose one explicitly: mutagent config set workspace <workspace-id> (list them with: mutagent workspaces list)");
17305
- }
17422
+ requireSelectedWorkspace() {
17423
+ if (!this.workspaceId)
17424
+ throw noWorkspaceSelected();
17306
17425
  return this.workspaceId;
17307
17426
  }
17308
17427
  async testProvider(id) {
@@ -17316,7 +17435,7 @@ class SDKClientWrapper {
17316
17435
  }
17317
17436
  }
17318
17437
  async createProvider(data) {
17319
- this.requireUnambiguousWorkspace();
17438
+ this.requireSelectedWorkspace();
17320
17439
  registerSecret(data.apiKey);
17321
17440
  registerSecretFields(data.credentialFields);
17322
17441
  const body = buildCreateProviderBody(data);
@@ -17331,7 +17450,7 @@ class SDKClientWrapper {
17331
17450
  }
17332
17451
  }
17333
17452
  async updateProvider(id, data) {
17334
- const workspaceId = this.requireUnambiguousWorkspace();
17453
+ const workspaceId = this.requireSelectedWorkspace();
17335
17454
  if (data.apiKey)
17336
17455
  this.debugSecrets.add(data.apiKey);
17337
17456
  registerSecret(data.apiKey);
@@ -17370,7 +17489,7 @@ class SDKClientWrapper {
17370
17489
  }
17371
17490
  }
17372
17491
  async deleteProvider(id) {
17373
- const workspaceId = this.requireUnambiguousWorkspace();
17492
+ const workspaceId = this.requireSelectedWorkspace();
17374
17493
  try {
17375
17494
  await this.sdk.llmProviders.deleteProviderConfig({ id });
17376
17495
  return { workspaceId };
@@ -17385,21 +17504,18 @@ async function getSDKClient() {
17385
17504
  if (!apiKey) {
17386
17505
  throw new AuthenticationError;
17387
17506
  }
17388
- const config = loadConfig();
17507
+ const tenancy = await resolveKeyTenancy(apiKey);
17508
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
17509
+ throw noWorkspaceSelected();
17389
17510
  sdkClient = new SDKClientWrapper({
17390
17511
  apiKey,
17391
- serverURL: config.endpoint,
17392
- workspaceId: config.defaultWorkspace,
17393
- organizationId: config.defaultOrganization,
17394
- workspaceSource: config.defaultWorkspaceSource,
17395
- clearStoredOrganization: clearDefaultOrganization
17396
- });
17397
- await sdkClient.ensureContext().catch((err) => {
17398
- if (isDebugEnabled()) {
17399
- const message = err instanceof Error ? err.message : String(err);
17400
- process.stderr.write(`[mutagent] Context resolution failed: ${message}
17401
- `);
17402
- }
17512
+ serverURL: tenancy.endpoint,
17513
+ workspaceId: tenancy.workspaceId,
17514
+ workspaceName: tenancy.workspaceName,
17515
+ workspaceOrigin: tenancy.workspaceOrigin,
17516
+ organizationId: tenancy.organization?.id,
17517
+ organizationName: tenancy.organization?.name,
17518
+ keyScope: tenancy.keyScope
17403
17519
  });
17404
17520
  }
17405
17521
  return sdkClient;
@@ -17417,20 +17533,7 @@ async function validateApiKey(apiKey, endpoint) {
17417
17533
  return false;
17418
17534
  }
17419
17535
  }
17420
- async function fetchWorkspaces(apiKey, endpoint, orgId) {
17421
- try {
17422
- const response = await fetch(`${endpoint}/api/workspaces`, {
17423
- headers: { "x-api-key": apiKey, "x-organization-id": orgId }
17424
- });
17425
- if (!response.ok)
17426
- return [];
17427
- const data = await response.json();
17428
- return data.workspaces ?? [];
17429
- } catch {
17430
- return [];
17431
- }
17432
- }
17433
- var sdkClient = null;
17536
+ var SDK_TRANSPORT_ERRORS, sdkClient = null;
17434
17537
  var init_sdk_client = __esm(() => {
17435
17538
  init_errors();
17436
17539
  init_config();
@@ -17439,6 +17542,7 @@ var init_sdk_client = __esm(() => {
17439
17542
  init_known_secrets();
17440
17543
  init_sdk_mappers();
17441
17544
  init_organization_scope();
17545
+ SDK_TRANSPORT_ERRORS = new Set(["ConnectionError", "RequestTimeoutError", "RequestAbortedError", "UnexpectedClientError"]);
17442
17546
  });
17443
17547
 
17444
17548
  // src/lib/agent/sdk.ts
@@ -17558,7 +17662,7 @@ async function createAgentDeploymentAPI() {
17558
17662
  const context = await getSDKClient();
17559
17663
  let workspaceId;
17560
17664
  try {
17561
- workspaceId = context.requireUnambiguousWorkspace();
17665
+ workspaceId = context.requireSelectedWorkspace();
17562
17666
  } catch (error3) {
17563
17667
  if (error3 instanceof MutagentError)
17564
17668
  error3.exitCode = 3;
@@ -18496,17 +18600,13 @@ async function installHelix(options, deps = {}) {
18496
18600
 
18497
18601
  // src/lib/install-telemetry.ts
18498
18602
  init_config();
18603
+ init_organization_scope();
18499
18604
  var TELEMETRY_TIMEOUT_MS = 1500;
18500
18605
  function resolveApiAuth() {
18501
18606
  const apiKey = getApiKey();
18502
18607
  const config = loadConfig();
18503
18608
  const apiBase = config.endpoint ?? "https://api.mutagent.io";
18504
- const headers = {};
18505
- if (config.defaultWorkspace)
18506
- headers["x-workspace-id"] = config.defaultWorkspace;
18507
- if (config.defaultOrganization)
18508
- headers["x-organization-id"] = config.defaultOrganization;
18509
- return { apiBase, apiKey, headers };
18609
+ return { apiBase, apiKey, headers: configTenancyHeaders(config) };
18510
18610
  }
18511
18611
  async function reportInstall(event, deps = {}) {
18512
18612
  try {
@@ -18619,6 +18719,7 @@ async function runPostOnboarding(deps = {}) {
18619
18719
  // src/lib/auth-flow.ts
18620
18720
  init_config();
18621
18721
  init_sdk_client();
18722
+ init_organization_scope();
18622
18723
  init_errors();
18623
18724
  import { input, password, select } from "@inquirer/prompts";
18624
18725
  import chalk3 from "chalk";
@@ -18667,6 +18768,17 @@ async function openBrowser(url) {
18667
18768
  throw new BrowserAuthError("BROWSER_OPEN_FAILED", `Could not open browser automatically. Please visit: ${url}`);
18668
18769
  }
18669
18770
  }
18771
+ function withOrgPreselect(authUrl, org) {
18772
+ if (!org)
18773
+ return authUrl;
18774
+ try {
18775
+ const url = new URL(authUrl);
18776
+ url.searchParams.set("org", org);
18777
+ return url.toString();
18778
+ } catch {
18779
+ return `${authUrl}${authUrl.includes("?") ? "&" : "?"}org=${encodeURIComponent(org)}`;
18780
+ }
18781
+ }
18670
18782
  function sleep(ms) {
18671
18783
  return new Promise((resolve6) => setTimeout(resolve6, ms));
18672
18784
  }
@@ -18680,7 +18792,7 @@ async function performBrowserAuth(options, onStatusUpdate) {
18680
18792
  const cliToken = generateCliToken();
18681
18793
  onStatusUpdate?.("Initializing browser authentication...");
18682
18794
  const initResponse = await initBrowserAuth(endpoint, cliToken);
18683
- const { authUrl } = initResponse;
18795
+ const authUrl = withOrgPreselect(initResponse.authUrl, options.org);
18684
18796
  if (!skipBrowserOpen) {
18685
18797
  onStatusUpdate?.("Opening browser for authentication...");
18686
18798
  try {
@@ -18797,7 +18909,7 @@ async function performLoginAction(opts) {
18797
18909
  useBrowserAuth = method === "browser";
18798
18910
  }
18799
18911
  if (useBrowserAuth) {
18800
- return loginWithBrowser(endpoint, output, wasFirstLogin);
18912
+ return loginWithBrowser(endpoint, output, wasFirstLogin, opts.org);
18801
18913
  }
18802
18914
  return loginWithPastedKey(endpoint, output, wasFirstLogin);
18803
18915
  }
@@ -18814,55 +18926,76 @@ function buildLoginJsonResponse(result) {
18814
18926
  }
18815
18927
  };
18816
18928
  }
18817
- async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
18818
- output.info("Validating API key...");
18819
- const isValid = await validateApiKey(apiKey, endpoint);
18820
- if (!isValid) {
18821
- throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
18929
+ async function describeKey(apiKey, endpoint, pick3) {
18930
+ let self;
18931
+ try {
18932
+ self = await fetchKeySelf(apiKey, endpoint);
18933
+ } catch {
18934
+ return {};
18822
18935
  }
18823
- const orgs = await fetchOrganizations(apiKey, endpoint);
18824
- let orgId;
18825
- let orgName;
18826
- let wsId;
18827
- let wsName;
18828
- if (orgs.length >= 1 && orgs[0]) {
18829
- orgId = orgs[0].id;
18830
- orgName = orgs[0].name;
18831
- const workspaces = await fetchWorkspaces(apiKey, endpoint, orgId);
18832
- const defaultWs = workspaces.find((w) => w.isDefault) ?? workspaces[0];
18833
- if (defaultWs) {
18834
- wsId = defaultWs.id;
18835
- wsName = defaultWs.name;
18836
- }
18837
- }
18838
- saveFullCredentials({
18839
- apiKey,
18840
- endpoint,
18841
- workspaceId: wsId,
18842
- organizationId: orgId
18843
- });
18936
+ const organization = self.organization ?? undefined;
18937
+ const scope = {
18938
+ keyScope: self.scope,
18939
+ organizationId: organization?.id,
18940
+ organizationName: organization?.name,
18941
+ organizationSlug: organization?.slug
18942
+ };
18943
+ if (self.workspace) {
18944
+ return { ...scope, workspaceId: self.workspace.id, workspaceName: self.workspace.name };
18945
+ }
18946
+ const envWorkspace = process.env.MUTAGENT_WORKSPACE_ID?.trim();
18947
+ const rows = await listKeyWorkspaces(apiKey, endpoint, { keyScope: self.scope, organization }).catch(() => []);
18948
+ let chosen;
18949
+ if (envWorkspace)
18950
+ chosen = rows.find((w) => w.id === envWorkspace);
18951
+ else if (rows.length === 1)
18952
+ chosen = rows[0];
18953
+ else if (pick3 && rows.length > 1) {
18954
+ const id = await pick3(rows);
18955
+ chosen = rows.find((w) => w.id === id);
18956
+ }
18957
+ return chosen ? { ...scope, workspaceId: chosen.id, workspaceName: chosen.name } : scope;
18958
+ }
18959
+ function loginResult(apiKey, endpoint, described, wasFirstLogin) {
18844
18960
  return {
18845
18961
  authenticated: true,
18846
18962
  apiKey,
18847
18963
  endpoint,
18848
- workspace: wsId ? { id: wsId, name: wsName } : null,
18849
- organization: orgId ? { id: orgId, name: orgName } : null,
18964
+ workspace: described.workspaceId ? { id: described.workspaceId, name: described.workspaceName } : null,
18965
+ organization: described.organizationId ? { id: described.organizationId, name: described.organizationName } : null,
18850
18966
  wasFirstLogin
18851
18967
  };
18852
18968
  }
18853
- async function loginWithBrowser(endpoint, output, wasFirstLogin) {
18969
+ async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
18970
+ output.info("Validating API key...");
18971
+ const isValid = await validateApiKey(apiKey, endpoint);
18972
+ if (!isValid) {
18973
+ throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
18974
+ }
18975
+ const described = await describeKey(apiKey, endpoint);
18976
+ saveFullCredentials({ apiKey, endpoint, ...described });
18977
+ return loginResult(apiKey, endpoint, described, wasFirstLogin);
18978
+ }
18979
+ async function loginWithBrowser(endpoint, output, wasFirstLogin, org) {
18854
18980
  const spinner = ora2({ text: "Opening browser for authentication...", spinner: "dots" });
18855
18981
  try {
18856
18982
  spinner.start();
18857
- const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000 }, (message) => {
18983
+ const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000, org }, (message) => {
18858
18984
  spinner.text = message;
18859
18985
  });
18860
18986
  spinner.succeed("Authenticated successfully!");
18987
+ const self = await fetchKeySelf(result.apiKey, endpoint).catch(() => {
18988
+ return;
18989
+ });
18861
18990
  saveFullCredentials({
18862
18991
  apiKey: result.apiKey,
18863
18992
  endpoint,
18864
18993
  workspaceId: result.workspaceId,
18994
+ workspaceName: result.workspaceName,
18865
18995
  organizationId: result.organizationId,
18996
+ organizationName: result.organizationName,
18997
+ organizationSlug: self?.organization?.slug,
18998
+ keyScope: self?.scope,
18866
18999
  expiresAt: result.expiresAt
18867
19000
  });
18868
19001
  if (result.workspaceName)
@@ -18900,55 +19033,12 @@ async function loginWithPastedKey(initialEndpoint, output, wasFirstLogin) {
18900
19033
  if (!isValid) {
18901
19034
  throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
18902
19035
  }
18903
- let selectedOrgId;
18904
- let selectedOrgName;
18905
- let selectedWsId;
18906
- let selectedWsName;
18907
- const orgs = await fetchOrganizations(apiKey, endpoint);
18908
- if (orgs.length === 1 && orgs[0]) {
18909
- selectedOrgId = orgs[0].id;
18910
- selectedOrgName = orgs[0].name;
18911
- } else if (orgs.length > 1) {
18912
- selectedOrgId = await select({
18913
- message: "Select organization:",
18914
- choices: orgs.map((o) => ({ name: o.name, value: o.id }))
18915
- });
18916
- selectedOrgName = orgs.find((o) => o.id === selectedOrgId)?.name;
18917
- }
18918
- if (selectedOrgId) {
18919
- const workspaces = await fetchWorkspaces(apiKey, endpoint, selectedOrgId);
18920
- const defaultWs = workspaces.find((w) => w.isDefault);
18921
- if (workspaces.length === 1 && workspaces[0]) {
18922
- selectedWsId = workspaces[0].id;
18923
- selectedWsName = workspaces[0].name;
18924
- } else if (defaultWs) {
18925
- selectedWsId = defaultWs.id;
18926
- selectedWsName = defaultWs.name;
18927
- } else if (workspaces.length > 1) {
18928
- selectedWsId = await select({
18929
- message: "Select workspace:",
18930
- choices: workspaces.map((w) => ({
18931
- name: w.name + (w.isDefault ? " (default)" : ""),
18932
- value: w.id
18933
- }))
18934
- });
18935
- selectedWsName = workspaces.find((w) => w.id === selectedWsId)?.name;
18936
- }
18937
- }
18938
- saveFullCredentials({
18939
- apiKey,
18940
- endpoint,
18941
- workspaceId: selectedWsId,
18942
- organizationId: selectedOrgId
18943
- });
18944
- return {
18945
- authenticated: true,
18946
- apiKey,
18947
- endpoint,
18948
- workspace: selectedWsId ? { id: selectedWsId, name: selectedWsName } : null,
18949
- organization: selectedOrgId ? { id: selectedOrgId, name: selectedOrgName } : null,
18950
- wasFirstLogin
18951
- };
19036
+ const described = await describeKey(apiKey, endpoint, async (rows) => rows.find((w) => w.isDefault)?.id ?? select({
19037
+ message: "Select workspace:",
19038
+ choices: rows.map((w) => ({ name: w.name, value: w.id }))
19039
+ }));
19040
+ saveFullCredentials({ apiKey, endpoint, ...described });
19041
+ return loginResult(apiKey, endpoint, described, wasFirstLogin);
18952
19042
  }
18953
19043
 
18954
19044
  // src/commands/auth.ts
@@ -18978,6 +19068,7 @@ See mutagent login --help for the full description.
18978
19068
  endpoint: options.endpoint,
18979
19069
  browser: options.browser,
18980
19070
  nonInteractive: options.nonInteractive,
19071
+ org: auth.optsWithGlobals().org,
18981
19072
  isJson,
18982
19073
  output
18983
19074
  });
@@ -18990,7 +19081,7 @@ See mutagent login --help for the full description.
18990
19081
  if (result.workspace?.name)
18991
19082
  output.info(`Workspace: ${result.workspace.name}`);
18992
19083
  output.info(`Endpoint: ${result.endpoint}`);
18993
- output.info("Next: mutagent workspaces list --json");
19084
+ output.info(result.workspace ? "Next: mutagent workspaces current" : "Next: mutagent workspaces use <name>");
18994
19085
  }
18995
19086
  if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
18996
19087
  await runPostOnboarding();
@@ -19094,10 +19185,13 @@ Examples:
19094
19185
  ${chalk5.dim("$")} mutagent login ${chalk5.dim("# In a terminal, choose browser or API key")}
19095
19186
  ${chalk5.dim("$")} mutagent login --browser ${chalk5.dim("# Sign in with the browser")}
19096
19187
  ${chalk5.dim("$")} mutagent login --non-interactive ${chalk5.dim("# Browser sign-in without the method prompt (a person approves in the browser)")}
19188
+ ${chalk5.dim("$")} mutagent login --org <slug> ${chalk5.dim("# Sign in for another organization (preselected on the approval page)")}
19097
19189
  ${chalk5.dim("$")} MUTAGENT_API_KEY=mt_... mutagent login --json ${chalk5.dim("# Sign in with a key (CI)")}
19098
19190
 
19099
19191
  Browser sign-in opens app.mutagent.io, where you sign in or sign up, finish onboarding
19100
19192
  and approve this CLI. The CLI waits up to 5 minutes and saves the credentials.
19193
+ The key works in every workspace you are a member of in the approved organization, for
19194
+ 30 days. The approved workspace is selected; switch with mutagent workspaces use <name>.
19101
19195
 
19102
19196
  When MUTAGENT_API_KEY is set, login skips the browser and validates the key.
19103
19197
 
@@ -19108,8 +19202,8 @@ Environment:
19108
19202
  CI=true Same as --non-interactive
19109
19203
 
19110
19204
  Then:
19111
- ${chalk5.dim("$")} mutagent auth status
19112
- ${chalk5.dim("$")} mutagent workspaces list --json
19205
+ ${chalk5.dim("$")} mutagent workspaces current
19206
+ ${chalk5.dim("$")} mutagent workspaces use <name>
19113
19207
 
19114
19208
  mutagent auth login runs the same sign-in.
19115
19209
  `).action(async (options) => {
@@ -19120,6 +19214,7 @@ mutagent auth login runs the same sign-in.
19120
19214
  endpoint: options.endpoint,
19121
19215
  browser: options.browser,
19122
19216
  nonInteractive: options.nonInteractive,
19217
+ org: login.optsWithGlobals().org,
19123
19218
  isJson,
19124
19219
  output
19125
19220
  });
@@ -19132,7 +19227,7 @@ mutagent auth login runs the same sign-in.
19132
19227
  if (result.workspace?.name)
19133
19228
  output.info(`Workspace: ${result.workspace.name}`);
19134
19229
  output.info(`Endpoint: ${result.endpoint}`);
19135
- output.info("Next: mutagent workspaces list --json");
19230
+ output.info(result.workspace ? "Next: mutagent workspaces current" : "Next: mutagent workspaces use <name>");
19136
19231
  }
19137
19232
  if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
19138
19233
  await runPostOnboarding();
@@ -19150,163 +19245,163 @@ mutagent auth login runs the same sign-in.
19150
19245
 
19151
19246
  // src/commands/config.ts
19152
19247
  init_config();
19248
+ import { Command as Command5 } from "commander";
19249
+ import chalk7 from "chalk";
19250
+ init_errors();
19251
+ init_organization_scope();
19252
+
19253
+ // src/commands/workspaces.ts
19254
+ init_sdk_client();
19153
19255
  import { Command as Command4 } from "commander";
19154
19256
  import chalk6 from "chalk";
19155
19257
  init_errors();
19156
- init_sdk_client();
19258
+ init_config();
19157
19259
  init_organization_scope();
19158
- var VALID_CONFIG_KEYS = ["apiKey", "endpoint", "format", "timeout", "defaultWorkspace", "defaultOrganization"];
19159
- function createConfigCommand() {
19160
- const config = new Command4("config").description("Manage CLI configuration").addHelpText("after", `
19260
+ async function useWorkspace(value2) {
19261
+ const apiKey = getApiKey();
19262
+ if (!apiKey)
19263
+ throw new AuthenticationError;
19264
+ if (envKeyReplacesFile())
19265
+ throw envKeyHasNoSelection();
19266
+ const tenancy = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19267
+ const config = loadConfig();
19268
+ const workspace = await findWorkspace(value2, {
19269
+ apiKey,
19270
+ endpoint: tenancy.endpoint,
19271
+ keyScope: tenancy.keyScope,
19272
+ organization: tenancy.organization,
19273
+ workspaceId: config.workspaceOrigin === "selection" ? tenancy.workspaceId : undefined,
19274
+ workspaceName: config.workspaceOrigin === "selection" ? tenancy.workspaceName : undefined
19275
+ });
19276
+ setWorkspaceSelection(workspace.id, workspace.name);
19277
+ resetSDKClient();
19278
+ const org = tenancy.organization;
19279
+ return { workspace, organization: org ? { id: org.id, name: org.name } : null };
19280
+ }
19281
+ function createWorkspacesCommand() {
19282
+ const workspaces = new Command4("workspaces").alias("workspace").description("List, select and inspect workspaces").addHelpText("after", `
19161
19283
  Examples:
19162
- ${chalk6.dim("$")} mutagent config list
19163
- ${chalk6.dim("$")} mutagent config get endpoint
19164
- ${chalk6.dim("$")} mutagent config set workspace <workspace-id>
19165
- ${chalk6.dim("$")} mutagent config set org <org-id>
19284
+ ${chalk6.dim("$")} mutagent workspaces list
19285
+ ${chalk6.dim("$")} mutagent workspaces use <name|id> ${chalk6.dim("# Every later command works in this workspace")}
19286
+ ${chalk6.dim("$")} mutagent workspaces current --json
19287
+ ${chalk6.dim("$")} mutagent providers list --workspace <name|id> ${chalk6.dim("# One command in another workspace")}
19288
+
19289
+ The CLI key from \`mutagent login\` works in every workspace you are a member of in its
19290
+ organization. Workspace precedence: --workspace, then MUTAGENT_WORKSPACE_ID, then the
19291
+ workspace chosen with \`workspaces use\`.
19292
+ To work in another organization: mutagent login --org <slug>
19293
+ Create or change workspaces in the dashboard: ${getAppBaseUrl()}
19166
19294
  `);
19167
- config.command("list").description("List all configuration").addHelpText("after", `
19295
+ workspaces.command("list").description("List your workspaces in the key's organization (* = selected)").option("-l, --limit <n>", "Limit results", "50").option("-o, --offset <n>", "Offset for pagination").addHelpText("after", `
19168
19296
  Examples:
19169
- ${chalk6.dim("$")} mutagent config list
19170
- ${chalk6.dim("$")} mutagent config list --json
19171
- `).action(() => {
19172
- const isJson = getJsonFlag(config);
19173
- const output = new OutputFormatter(isJson ? "json" : "table");
19174
- const cfg = loadConfig();
19175
- const display = {
19176
- ...cfg,
19177
- apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined
19178
- };
19179
- output.output(display);
19180
- });
19181
- config.command("get").description("Get configuration value").argument("<key>", "Configuration key (apiKey, endpoint, format, timeout, defaultWorkspace, defaultOrganization)").addHelpText("after", `
19182
- Examples:
19183
- ${chalk6.dim("$")} mutagent config get endpoint
19184
- ${chalk6.dim("$")} mutagent config get defaultWorkspace
19185
- ${chalk6.dim("$")} mutagent config get apiKey --json
19297
+ ${chalk6.dim("$")} mutagent workspaces list
19298
+ ${chalk6.dim("$")} mutagent workspaces list --limit 10 --json
19186
19299
 
19187
- ${chalk6.dim("Keys: apiKey, endpoint, format, timeout, defaultWorkspace, defaultOrganization")}
19188
- ${chalk6.dim("A key with no value set is reported as unknown.")}
19189
- `).action((key) => {
19190
- const isJson = getJsonFlag(config);
19300
+ Lists only workspaces you are a member of. Works even when the selected workspace is no
19301
+ longer available, so you can pick another with: mutagent workspaces use <name>
19302
+ `).action(async (options) => {
19303
+ const isJson = getJsonFlag(workspaces);
19191
19304
  const output = new OutputFormatter(isJson ? "json" : "table");
19192
19305
  try {
19193
- const cfg = loadConfig();
19194
- if (!(key in cfg)) {
19195
- throw new MutagentError("INVALID_ARGUMENTS", `Unknown config key: "${key}"`, `Valid keys: ${VALID_CONFIG_KEYS.join(", ")}
19196
- ` + "Example: mutagent config get endpoint");
19197
- }
19198
- const value2 = cfg[key];
19199
- const stringValue = typeof value2 === "object" ? JSON.stringify(value2) : String(value2 ?? "");
19200
- const displayValue = key === "apiKey" && value2 ? `${stringValue.slice(0, 8)}...` : stringValue;
19306
+ const apiKey = getApiKey();
19307
+ if (!apiKey)
19308
+ throw new AuthenticationError;
19309
+ const tenancy = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19310
+ const rows = await listKeyWorkspaces(apiKey, tenancy.endpoint, tenancy);
19311
+ const offset = options.offset ? parseInt(options.offset, 10) : 0;
19312
+ const limit = parseInt(options.limit, 10) || 50;
19313
+ const page = rows.slice(offset, offset + limit);
19314
+ const selected = tenancy.workspaceId;
19201
19315
  if (isJson) {
19202
- output.output({ [key]: displayValue });
19316
+ output.output({
19317
+ data: page.map((w) => ({
19318
+ id: w.id,
19319
+ name: w.name,
19320
+ slug: w.slug,
19321
+ isDefault: w.isDefault,
19322
+ selected: w.id === selected,
19323
+ _links: workspaceLinks(w.id)
19324
+ })),
19325
+ total: rows.length
19326
+ });
19203
19327
  } else {
19204
- output.info(displayValue);
19328
+ output.output(page.map((w) => ({
19329
+ selected: w.id === selected ? "*" : "",
19330
+ id: w.id,
19331
+ name: w.name,
19332
+ slug: w.slug,
19333
+ url: workspaceLink(w.id)
19334
+ })));
19205
19335
  }
19206
19336
  } catch (error3) {
19207
19337
  handleError(error3, isJson);
19208
19338
  }
19209
19339
  });
19210
- const set2 = new Command4("set").description("Set configuration value").action(() => {
19211
- set2.help();
19212
- });
19213
- set2.command("workspace").description("Set default workspace ID").argument("<id>", "Workspace ID to set as default").addHelpText("after", `
19340
+ workspaces.command("use").description("Select the workspace every later command works in").argument("<name|id>", "Workspace name or id").addHelpText("after", `
19214
19341
  Examples:
19215
- ${chalk6.dim("$")} mutagent config set workspace <workspace-id>
19342
+ ${chalk6.dim("$")} mutagent workspaces use staging
19343
+ ${chalk6.dim("$")} mutagent workspaces use <workspace-id> --json
19216
19344
 
19217
- ${chalk6.dim("Commands use this workspace until you change it.")}
19218
- `).action((id) => {
19219
- const isJson = getJsonFlag(config);
19220
- const output = new OutputFormatter(isJson ? "json" : "table");
19221
- setDefaultWorkspace(id);
19222
- resetSDKClient();
19223
- if (isJson) {
19224
- output.output({ success: true, defaultWorkspace: id });
19225
- } else {
19226
- output.success(`Default workspace set to ${id}`);
19227
- }
19228
- });
19229
- set2.command("org").description("Set default organization ID").argument("<id>", "Organization ID to set as default").addHelpText("after", `
19230
- Examples:
19231
- ${chalk6.dim("$")} mutagent config set org <org-id>
19232
-
19233
- ${chalk6.dim("Persists organization ID for org-scoped API keys.")}
19234
- ${chalk6.dim("The ID is checked against your account first — a stored organization the")}
19235
- ${chalk6.dim("API key does not own makes every later command fail with 403.")}
19236
- `).action(async (id) => {
19237
- const isJson = getJsonFlag(config);
19345
+ Resolves against your workspaces in the key's organization and saves the choice. No
19346
+ browser and no prompt. A workspace in another organization needs a key for that
19347
+ organization: mutagent login --org <slug>. Two workspaces with the same name: pass the id.
19348
+ `).action(async (value2) => {
19349
+ const isJson = getJsonFlag(workspaces);
19238
19350
  const output = new OutputFormatter(isJson ? "json" : "table");
19239
19351
  try {
19240
- await assertUsableOrganizationId(id, {
19241
- apiKey: getApiKey(),
19242
- endpoint: loadConfig().endpoint ?? "https://api.mutagent.io",
19243
- fetchOrganizations
19244
- });
19245
- setDefaultOrganization(id);
19246
- resetSDKClient();
19352
+ const result = await useWorkspace(value2);
19247
19353
  if (isJson) {
19248
- output.output({ success: true, defaultOrganization: id });
19354
+ output.output({ success: true, ...result, _links: workspaceLinks(result.workspace.id) });
19249
19355
  } else {
19250
- output.success(`Default organization set to ${id}`);
19356
+ output.success(`Workspace: ${result.workspace.name} (${result.workspace.id})`);
19251
19357
  }
19252
19358
  } catch (error3) {
19253
19359
  handleError(error3, isJson);
19254
19360
  }
19255
19361
  });
19256
- config.addCommand(set2);
19257
- return config;
19258
- }
19259
-
19260
- // src/commands/workspaces.ts
19261
- init_sdk_client();
19262
- import { Command as Command5 } from "commander";
19263
- import chalk7 from "chalk";
19264
- init_errors();
19265
- function createWorkspacesCommand() {
19266
- const workspaces = new Command5("workspaces").description("View workspaces (read-only)").addHelpText("after", `
19362
+ workspaces.command("current").description("Show the key's scope, organization and the verified workspace").addHelpText("after", `
19267
19363
  Examples:
19268
- ${chalk7.dim("$")} mutagent workspaces list
19269
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id>
19364
+ ${chalk6.dim("$")} mutagent workspaces current
19365
+ ${chalk6.dim("$")} mutagent workspaces current --json
19270
19366
 
19271
- Choose the active workspace with: mutagent config set workspace <workspace-id>
19272
- Create or change workspaces in the dashboard: https://app.mutagent.io
19273
- `);
19274
- workspaces.command("list").description("List all workspaces").option("-l, --limit <n>", "Limit results", "50").option("-o, --offset <n>", "Offset for pagination").addHelpText("after", `
19275
- Examples:
19276
- ${chalk7.dim("$")} mutagent workspaces list
19277
- ${chalk7.dim("$")} mutagent workspaces list --limit 10 --json
19278
- `).action(async (options) => {
19367
+ Asks the server (GET /api/api-keys/self) with the workspace this command would use, so
19368
+ the answer is verified, not read from local config.
19369
+ `).action(async () => {
19279
19370
  const isJson = getJsonFlag(workspaces);
19280
19371
  const output = new OutputFormatter(isJson ? "json" : "table");
19281
19372
  try {
19282
- const client = await getSDKClient();
19283
- const filters = {
19284
- limit: parseInt(options.limit, 10) || 50
19285
- };
19286
- if (options.offset) {
19287
- filters.offset = parseInt(options.offset, 10);
19288
- }
19289
- const result = await client.listWorkspaces(filters);
19373
+ const apiKey = getApiKey();
19374
+ if (!apiKey)
19375
+ throw new AuthenticationError;
19376
+ const tenancy = await resolveKeyTenancy(apiKey);
19377
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
19378
+ throw noWorkspaceSelected();
19379
+ const headers = tenancyHeaders({
19380
+ keyScope: tenancy.keyScope,
19381
+ workspaceId: tenancy.workspaceId,
19382
+ workspaceOrigin: tenancy.workspaceOrigin,
19383
+ organizationId: tenancy.organization?.id
19384
+ });
19385
+ const self = await fetchKeySelf(apiKey, tenancy.endpoint, { headers, ctx: tenancy });
19386
+ if (!envKeyReplacesFile())
19387
+ recordKeyScope(self);
19388
+ if (!self.workspace && self.scope === "organization")
19389
+ throw noWorkspaceSelected();
19290
19390
  if (isJson) {
19291
- const withLinks = result.data.map((w) => ({
19292
- id: w.id,
19293
- name: w.name,
19294
- slug: w.slug,
19295
- isDefault: w.isDefault,
19296
- updatedAt: w.updatedAt,
19297
- _links: workspaceLinks(w.id)
19298
- }));
19299
- output.output({ ...result, data: withLinks });
19391
+ output.output({
19392
+ scope: self.scope,
19393
+ organization: self.organization,
19394
+ workspace: self.workspace,
19395
+ expiresAt: self.expiresAt ?? null
19396
+ });
19300
19397
  } else {
19301
- const formatted = result.data.map((w) => ({
19302
- id: w.id,
19303
- name: w.name,
19304
- slug: w.slug,
19305
- isDefault: w.isDefault ? "Yes" : "No",
19306
- updated: w.updatedAt ? new Date(w.updatedAt).toLocaleDateString() : "N/A",
19307
- url: workspaceLink(w.id)
19308
- }));
19309
- output.output(formatted);
19398
+ output.info(`Key scope: ${self.scope}`);
19399
+ if (self.organization)
19400
+ output.info(`Organization: ${self.organization.name} (${self.organization.id})`);
19401
+ if (self.workspace)
19402
+ output.info(`Workspace: ${self.workspace.name} (${self.workspace.id})`);
19403
+ if (self.expiresAt)
19404
+ output.info(`Key expires: ${self.expiresAt}`);
19310
19405
  }
19311
19406
  } catch (error3) {
19312
19407
  handleError(error3, isJson);
@@ -19314,8 +19409,8 @@ Examples:
19314
19409
  });
19315
19410
  workspaces.command("get").description("Get workspace details").argument("<id>", "Workspace ID").addHelpText("after", `
19316
19411
  Examples:
19317
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id>
19318
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id> --json
19412
+ ${chalk6.dim("$")} mutagent workspaces get <workspace-id>
19413
+ ${chalk6.dim("$")} mutagent workspaces get <workspace-id> --json
19319
19414
  `).action(async (id) => {
19320
19415
  const isJson = getJsonFlag(workspaces);
19321
19416
  const output = new OutputFormatter(isJson ? "json" : "table");
@@ -19345,6 +19440,115 @@ Examples:
19345
19440
  return workspaces;
19346
19441
  }
19347
19442
 
19443
+ // src/commands/config.ts
19444
+ var VALID_CONFIG_KEYS = ["apiKey", "endpoint", "format", "timeout", "defaultWorkspace", "workspaceName", "defaultOrganization", "organizationName", "keyScope"];
19445
+ function createConfigCommand() {
19446
+ const config = new Command5("config").description("Manage CLI configuration").addHelpText("after", `
19447
+ Examples:
19448
+ ${chalk7.dim("$")} mutagent config list
19449
+ ${chalk7.dim("$")} mutagent config get endpoint
19450
+ ${chalk7.dim("$")} mutagent config set workspace <name|id> ${chalk7.dim("# Same as: mutagent workspaces use")}
19451
+ `);
19452
+ config.command("list").description("List all configuration").addHelpText("after", `
19453
+ Examples:
19454
+ ${chalk7.dim("$")} mutagent config list
19455
+ ${chalk7.dim("$")} mutagent config list --json
19456
+ `).action(() => {
19457
+ const isJson = getJsonFlag(config);
19458
+ const output = new OutputFormatter(isJson ? "json" : "table");
19459
+ const cfg = loadConfig();
19460
+ const display = {
19461
+ ...cfg,
19462
+ apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined
19463
+ };
19464
+ output.output(display);
19465
+ });
19466
+ config.command("get").description("Get configuration value").argument("<key>", `Configuration key (${VALID_CONFIG_KEYS.join(", ")})`).addHelpText("after", `
19467
+ Examples:
19468
+ ${chalk7.dim("$")} mutagent config get endpoint
19469
+ ${chalk7.dim("$")} mutagent config get defaultWorkspace
19470
+ ${chalk7.dim("$")} mutagent config get apiKey --json
19471
+
19472
+ ${chalk7.dim(`Keys: ${VALID_CONFIG_KEYS.join(", ")}`)}
19473
+ ${chalk7.dim("A key with no value set is reported as unknown.")}
19474
+ `).action((key) => {
19475
+ const isJson = getJsonFlag(config);
19476
+ const output = new OutputFormatter(isJson ? "json" : "table");
19477
+ try {
19478
+ const cfg = loadConfig();
19479
+ if (!(key in cfg)) {
19480
+ throw new MutagentError("INVALID_ARGUMENTS", `Unknown config key: "${key}"`, `Valid keys: ${VALID_CONFIG_KEYS.join(", ")}
19481
+ ` + "Example: mutagent config get endpoint");
19482
+ }
19483
+ const value2 = cfg[key];
19484
+ const stringValue = typeof value2 === "object" ? JSON.stringify(value2) : String(value2 ?? "");
19485
+ const displayValue = key === "apiKey" && value2 ? `${stringValue.slice(0, 8)}...` : stringValue;
19486
+ if (isJson) {
19487
+ output.output({ [key]: displayValue });
19488
+ } else {
19489
+ output.info(displayValue);
19490
+ }
19491
+ } catch (error3) {
19492
+ handleError(error3, isJson);
19493
+ }
19494
+ });
19495
+ const set2 = new Command5("set").description("Set configuration value").action(() => {
19496
+ set2.help();
19497
+ });
19498
+ set2.command("workspace").description("Select the workspace every later command works in (same as: workspaces use)").argument("<name|id>", "Workspace name or id").addHelpText("after", `
19499
+ Examples:
19500
+ ${chalk7.dim("$")} mutagent config set workspace staging
19501
+ ${chalk7.dim("$")} mutagent config set workspace <workspace-id>
19502
+
19503
+ ${chalk7.dim("Runs mutagent workspaces use: resolves the name among your workspaces and saves it.")}
19504
+ `).action(async (value2) => {
19505
+ const isJson = getJsonFlag(config);
19506
+ const output = new OutputFormatter(isJson ? "json" : "table");
19507
+ try {
19508
+ const result = await useWorkspace(value2);
19509
+ if (isJson) {
19510
+ output.output({ success: true, defaultWorkspace: result.workspace.id, ...result });
19511
+ } else {
19512
+ output.success(`Workspace: ${result.workspace.name} (${result.workspace.id})`);
19513
+ }
19514
+ } catch (error3) {
19515
+ handleError(error3, isJson);
19516
+ }
19517
+ });
19518
+ set2.command("org").description("Confirm the key's organization (it comes from the key; nothing is stored)").argument("<id|slug>", "Organization id or slug").addHelpText("after", `
19519
+ Examples:
19520
+ ${chalk7.dim("$")} mutagent config set org <org-id|slug>
19521
+
19522
+ ${chalk7.dim("The CLI key belongs to one organization. The same organization: nothing to do.")}
19523
+ ${chalk7.dim("Another organization: sign in for it with mutagent login --org <id|slug>.")}
19524
+ `).action(async (value2) => {
19525
+ const isJson = getJsonFlag(config);
19526
+ const output = new OutputFormatter(isJson ? "json" : "table");
19527
+ try {
19528
+ const apiKey = getApiKey();
19529
+ if (!apiKey)
19530
+ throw new AuthenticationError;
19531
+ setOrgAssertion(value2);
19532
+ const { organization } = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19533
+ if (!organization) {
19534
+ throw new MutagentError("ORG_UNKNOWN", "This key's organization is unknown.", "Run: mutagent workspaces current");
19535
+ }
19536
+ if (!matchesOrganization(value2, organization)) {
19537
+ throw orgMismatch(value2, organization);
19538
+ }
19539
+ if (isJson) {
19540
+ output.output({ success: true, organization });
19541
+ } else {
19542
+ output.success(`Organization: ${organization.name} (${organization.id}), from the key`);
19543
+ }
19544
+ } catch (error3) {
19545
+ handleError(error3, isJson);
19546
+ }
19547
+ });
19548
+ config.addCommand(set2);
19549
+ return config;
19550
+ }
19551
+
19348
19552
  // src/commands/providers/index.ts
19349
19553
  init_sdk_client();
19350
19554
  import { Command as Command6 } from "commander";
@@ -19837,7 +20041,7 @@ anthropic for an endpoint that speaks the Anthropic API; unset, the server defau
19837
20041
  The base URL must be a public address: hosts ending in .internal, .local, .localhost or
19838
20042
  .home.arpa are refused.
19839
20043
 
19840
- Adds the LLM provider to the configured workspace (mutagent config set workspace <id>).
20044
+ Adds the LLM provider to the configured workspace (mutagent workspaces use <name>, or --workspace for one command).
19841
20045
  The key is encrypted and never returned.
19842
20046
  Workspace default model: when the workspace has none, adding a keyed LLM provider sets
19843
20047
  one on the server. An existing workspace default model is never changed.
@@ -19875,7 +20079,7 @@ AI Agent Directive:
19875
20079
  validateApiFormat(providerType, options.apiFormat);
19876
20080
  }
19877
20081
  const client = await getSDKClient();
19878
- const workspaceId = client.requireUnambiguousWorkspace();
20082
+ const workspaceId = client.requireSelectedWorkspace();
19879
20083
  const credentialFields = collectCredentialFields(options);
19880
20084
  if (secretWireName !== "apiKey")
19881
20085
  credentialFields[secretWireName] = secret;
@@ -20787,7 +20991,7 @@ function statusGlyph(result) {
20787
20991
  }
20788
20992
  }
20789
20993
  function registerMirrorCommand(parent) {
20790
- parent.command("mirror").description("Copy the API keys from your local Helix login store into the workspace as LLM providers").option("-y, --yes", "Consent without prompting (required when non-interactive)").option("--no-overwrite", "Leave LLM providers the workspace already has untouched").option("--workspace <id>", "Target workspace for this run (default: the configured one)").option("--channel <channel>", `Helix channel whose store is read: ${HELIX_CHANNELS2.join(" | ")}`, "stable").option("--provider <id>", "Only this local LLM provider id (repeatable)", (value2, prev = []) => [...prev, value2]).addHelpText("after", `
20994
+ parent.command("mirror").description("Copy the API keys from your local Helix login store into the workspace as LLM providers").option("-y, --yes", "Consent without prompting (required when non-interactive)").option("--no-overwrite", "Leave LLM providers the workspace already has untouched").option("--channel <channel>", `Helix channel whose store is read: ${HELIX_CHANNELS2.join(" | ")}`, "stable").option("--provider <id>", "Only this local LLM provider id (repeatable)", (value2, prev = []) => [...prev, value2]).addHelpText("after", `
20791
20995
  Examples:
20792
20996
  ${chalk11.dim("$")} mutagent providers mirror
20793
20997
  ${chalk11.dim("$")} mutagent providers mirror --json ${chalk11.dim("Show the plan without sending anything")}
@@ -20816,6 +21020,9 @@ default model is never changed. Change it with
20816
21020
 
20817
21021
  Mirroring does not test the keys: check \`mutagent helix models\`, then run a model.
20818
21022
 
21023
+ --workspace <name|id> still works here: it is the global flag and targets this run only
21024
+ (mutagent workspaces use <name> changes the default).
21025
+
20819
21026
  Requires \`mutagent login\` and a workspace. Keys are sent only to the configured
20820
21027
  endpoint and are never printed. Consent is asked once per run; without a terminal,
20821
21028
  --yes is required. Without it the run exits 2 (with --json it returns the plan).
@@ -20835,12 +21042,9 @@ AI Agent Directive:
20835
21042
  try {
20836
21043
  const apiKey = getApiKey();
20837
21044
  const config = loadConfig();
20838
- const workspaceId = options.workspace ?? config.defaultWorkspace;
21045
+ const workspaceId = config.defaultWorkspace;
20839
21046
  if (!apiKey || !workspaceId) {
20840
- throw new MutagentError("AUTH_REQUIRED", "No platform identity for this machine — run `mutagent auth login` first.", "mutagent auth login (then, if needed: mutagent config set workspace <workspace-id>)", MIRROR_EXIT_NO_IDENTITY);
20841
- }
20842
- if (options.workspace) {
20843
- process.env.MUTAGENT_WORKSPACE_ID = options.workspace;
21047
+ throw new MutagentError("AUTH_REQUIRED", "No platform identity for this machine — run `mutagent auth login` first.", "mutagent login (then, if needed: mutagent workspaces use <name>)", MIRROR_EXIT_NO_IDENTITY);
20844
21048
  }
20845
21049
  if (!HELIX_CHANNELS2.includes(options.channel)) {
20846
21050
  throw new MutagentError("INVALID_OPTION", `Unknown channel '${options.channel}'.`, `Use --channel ${HELIX_CHANNELS2.join(" or ")}`);
@@ -20851,7 +21055,7 @@ AI Agent Directive:
20851
21055
  for (const secret of localCredentialSecrets(store.entries))
20852
21056
  registerSecret(secret);
20853
21057
  const client = await getSDKClient();
20854
- const target = client.requireUnambiguousWorkspace();
21058
+ const target = client.requireSelectedWorkspace();
20855
21059
  const rows = (await client.listProviders({ limit: 1000 })).data.map((row) => ({
20856
21060
  id: row.id,
20857
21061
  name: row.name,
@@ -21782,22 +21986,28 @@ exits — run \`mutagent login\` first, then re-run \`mutagent init\`.
21782
21986
 
21783
21987
  \`\`\`bash
21784
21988
  mutagent config list --json # show all config
21785
- mutagent config get <key> --json # keys: apiKey, endpoint, format, timeout,
21786
- # defaultWorkspace, defaultOrganization
21787
- mutagent config set workspace <id> --json # set default workspace
21788
- mutagent config set org <id> --json # set default organization
21989
+ mutagent config get <key> --json # keys: apiKey, endpoint, format, timeout, defaultWorkspace,
21990
+ # workspaceName, defaultOrganization, organizationName, keyScope
21991
+ mutagent config set workspace <name|id> --json # same as: mutagent workspaces use
21992
+ mutagent config set org <id|slug> --json # confirms the key's organization; another org exits 1
21789
21993
  \`\`\`
21790
21994
 
21791
21995
  ---
21792
21996
 
21793
- ## 4. workspaces (read-only)
21997
+ ## 4. workspaces
21794
21998
 
21795
21999
  \`\`\`bash
21796
- mutagent workspaces list --json # list workspaces
22000
+ mutagent workspaces list --json # your workspaces in the key's organization (selected: true)
22001
+ mutagent workspaces use <name|id> --json # select the workspace later commands work in (no browser, no prompt)
22002
+ mutagent workspaces current --json # key scope, organization, verified workspace, key expiry
21797
22003
  mutagent workspaces get <id> --json # workspace details
21798
22004
  \`\`\`
21799
22005
 
21800
- To make a workspace the default, use \`mutagent config set workspace <id>\`.
22006
+ The login key works in every workspace the user is a member of in its organization.
22007
+ For one command in another workspace, pass \`--workspace <name|id>\` (it beats
22008
+ \`MUTAGENT_WORKSPACE_ID\`, which beats the selection). For another organization:
22009
+ \`mutagent login --org <slug>\`. A tenancy error is one line ending in the fix command:
22010
+ run that command, never retry the same request.
21801
22011
 
21802
22012
  ---
21803
22013
 
@@ -22008,51 +22218,33 @@ It never prompts. Without a sign-in it prints what to run and exits.
22008
22218
  } else {
22009
22219
  const config = (deps.loadConfig ?? loadConfig)();
22010
22220
  const endpoint = config.endpoint ?? "https://api.mutagent.io";
22011
- let workspace = config.defaultWorkspace;
22221
+ const workspace = config.defaultWorkspace;
22012
22222
  let workspaceValidation;
22013
- let resolvedOrgId;
22014
22223
  try {
22015
22224
  const apiKey = (deps.getApiKey ?? getApiKey)();
22016
- if (apiKey) {
22017
- const sdk = await Promise.resolve().then(() => (init_sdk_client(), exports_sdk_client));
22018
- const fetchOrgs = deps.fetchOrganizations ?? sdk.fetchOrganizations;
22019
- const fetchWs = deps.fetchWorkspaces ?? sdk.fetchWorkspaces;
22020
- let orgId = config.defaultOrganization;
22021
- if (!orgId) {
22022
- const orgs = await fetchOrgs(apiKey, endpoint);
22023
- orgId = orgs[0]?.id;
22024
- }
22025
- if (orgId) {
22026
- resolvedOrgId = orgId;
22027
- const workspaces = await fetchWs(apiKey, endpoint, orgId);
22028
- if (workspaces.length > 0) {
22029
- const found = workspace ? workspaces.find((w) => w.id === workspace) : undefined;
22030
- if (found) {
22031
- workspaceValidation = { id: found.id, name: found.name, validated: true, corrected: false };
22032
- } else if (workspaces[0]) {
22033
- const first = workspaces[0];
22034
- (deps.setDefaultWorkspace ?? setDefaultWorkspace)(first.id, "login-inferred");
22035
- workspace = first.id;
22036
- workspaceValidation = { id: first.id, name: first.name, validated: true, corrected: true };
22037
- }
22038
- }
22039
- }
22225
+ if (apiKey && workspace) {
22226
+ const list = deps.listWorkspaces ?? (async (key) => {
22227
+ const { listKeyWorkspaces: listKeyWorkspaces2, resolveKeyTenancy: resolveKeyTenancy2 } = await Promise.resolve().then(() => (init_organization_scope(), exports_organization_scope));
22228
+ const tenancy = await resolveKeyTenancy2(key, { resolveOverride: false });
22229
+ return listKeyWorkspaces2(key, tenancy.endpoint, tenancy);
22230
+ });
22231
+ const found = (await list(apiKey)).find((w) => w.id === workspace);
22232
+ workspaceValidation = { id: workspace, name: found?.name, found: found !== undefined };
22040
22233
  }
22041
22234
  } catch {}
22042
22235
  rcConfig = {
22043
22236
  endpoint,
22044
- ...workspace ? { defaultWorkspace: workspace } : {},
22045
- ...resolvedOrgId ? { defaultOrganization: resolvedOrgId } : {}
22237
+ ...workspace ? { defaultWorkspace: workspace } : {}
22046
22238
  };
22047
22239
  writeRcConfig(rcConfig, cwd);
22048
22240
  if (!isJson) {
22049
22241
  output.success("Created .mutagentrc.json");
22050
- if (workspaceValidation) {
22051
- if (workspaceValidation.corrected) {
22052
- output.warn(`Workspace auto-corrected to: ${workspaceValidation.name} (${workspaceValidation.id})`);
22053
- } else {
22054
- output.info(`Workspace: ${workspaceValidation.name} (${workspaceValidation.id})`);
22055
- }
22242
+ if (workspaceValidation?.found) {
22243
+ output.info(`Workspace: ${workspaceValidation.name ?? workspaceValidation.id} (${workspaceValidation.id})`);
22244
+ } else if (workspaceValidation) {
22245
+ output.warn(`Your saved workspace ${workspaceValidation.id} is not among your workspaces. Run: mutagent workspaces use <name>`);
22246
+ } else if (!workspace) {
22247
+ output.warn("No workspace selected. Run: mutagent workspaces use <name>");
22056
22248
  }
22057
22249
  }
22058
22250
  }
@@ -22236,22 +22428,8 @@ function parseTranscript(raw) {
22236
22428
  // src/commands/hooks/api.ts
22237
22429
  init_config();
22238
22430
  init_known_secrets();
22431
+ init_organization_scope();
22239
22432
  var API_TIMEOUT_MS = 5000;
22240
- function resolveTenancyHeaders(config) {
22241
- if (config.defaultWorkspace && !config.defaultOrganization) {
22242
- return {
22243
- ok: false,
22244
- warning: "[mutagent hooks] Warning: a default workspace is set but no default organization, " + "so the server will reject every trace batch. " + `Fix with: mutagent config set organization <org-id>
22245
- `
22246
- };
22247
- }
22248
- const headers = {};
22249
- if (config.defaultWorkspace)
22250
- headers["x-workspace-id"] = config.defaultWorkspace;
22251
- if (config.defaultOrganization)
22252
- headers["x-organization-id"] = config.defaultOrganization;
22253
- return { ok: true, headers };
22254
- }
22255
22433
  async function sendBatchTrace(traces) {
22256
22434
  const apiKey = getApiKey();
22257
22435
  if (!apiKey) {
@@ -22265,12 +22443,7 @@ async function sendBatchTrace(traces) {
22265
22443
  "x-api-key": apiKey,
22266
22444
  "Content-Type": "application/json"
22267
22445
  };
22268
- const tenancy = resolveTenancyHeaders(config);
22269
- if (!tenancy.ok) {
22270
- process.stderr.write(tenancy.warning);
22271
- return;
22272
- }
22273
- Object.assign(headers, tenancy.headers);
22446
+ Object.assign(headers, configTenancyHeaders(config));
22274
22447
  const controller = new AbortController;
22275
22448
  const timeout = setTimeout(() => {
22276
22449
  controller.abort();
@@ -23282,6 +23455,7 @@ import { join as join18, dirname as dirname5 } from "path";
23282
23455
  import { fileURLToPath as fileURLToPath3 } from "url";
23283
23456
  init_errors();
23284
23457
  init_config();
23458
+ init_organization_scope();
23285
23459
 
23286
23460
  // src/lib/transcript.ts
23287
23461
  init_errors();
@@ -23691,15 +23865,12 @@ function buildAutoContext(harness) {
23691
23865
  nodeVersion: process.version
23692
23866
  };
23693
23867
  }
23694
- async function postToServer(payload, endpoint, apiKey, workspaceId, organizationId) {
23868
+ async function postToServer(payload, endpoint, apiKey, tenancy) {
23695
23869
  const headers = {
23696
23870
  "Content-Type": "application/json",
23697
- "x-api-key": apiKey
23871
+ "x-api-key": apiKey,
23872
+ ...tenancy
23698
23873
  };
23699
- if (workspaceId)
23700
- headers["x-workspace-id"] = workspaceId;
23701
- if (organizationId)
23702
- headers["x-organization-id"] = organizationId;
23703
23874
  let response;
23704
23875
  try {
23705
23876
  response = await fetch(`${endpoint}/api/feedback`, {
@@ -23823,7 +23994,7 @@ Or set an API key: mutagent config set apiKey <key>`);
23823
23994
  const payload = buildPayload(feedbackBody, category, options.title, options.session, transcript);
23824
23995
  const config = loadConfig();
23825
23996
  const endpoint = config.endpoint ?? "https://api.mutagent.io";
23826
- const result = await postToServer(payload, endpoint, apiKey, config.defaultWorkspace, config.defaultOrganization);
23997
+ const result = await postToServer(payload, endpoint, apiKey, configTenancyHeaders(config));
23827
23998
  if (isJson) {
23828
23999
  output.output({
23829
24000
  success: true,
@@ -24657,12 +24828,14 @@ init_errors();
24657
24828
 
24658
24829
  // src/lib/sandbox-api.ts
24659
24830
  init_config();
24831
+ init_organization_scope();
24660
24832
  init_errors();
24661
24833
 
24662
24834
  // src/lib/sandbox-token.ts
24663
24835
  init_secure_file();
24664
24836
  init_errors();
24665
24837
  init_known_secrets();
24838
+ init_organization_scope();
24666
24839
  import { existsSync as existsSync12, readFileSync as readFileSync12, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
24667
24840
  import { homedir as homedir6 } from "os";
24668
24841
  import { join as join20, dirname as dirname6 } from "path";
@@ -24743,12 +24916,11 @@ async function exchangeToken(input2, deps = {}) {
24743
24916
  const detail = error3 instanceof Error ? error3.message : String(error3);
24744
24917
  throw new MutagentError("SANDBOX_UNREACHABLE", `Could not reach the MutagenT API at ${input2.endpoint}: ${detail}`, "Check your connection, then verify the endpoint with: mutagent config list", 5);
24745
24918
  }
24746
- if (response.status === 401) {
24747
- throw new AuthenticationError("Your MutagenT API key was rejected. It may have been revoked or expired.", { suggestions: ["mutagent auth login --browser", "mutagent auth status"] });
24748
- }
24919
+ if (response.status === 401)
24920
+ throw deadKeyError();
24749
24921
  if (response.status === 403) {
24750
24922
  const detail = scrubKnown(messageFrom(await response.text().catch(() => "")));
24751
- throw new MutagentError("SANDBOX_WORKSPACE_FORBIDDEN", detail !== "" ? detail : `Your API key is valid, but this account cannot use workspace "${input2.workspaceId}".`, `Pick a workspace you belong to: mutagent workspaces list, then mutagent config set workspace <id>`, 3);
24923
+ throw new MutagentError("SANDBOX_WORKSPACE_FORBIDDEN", detail !== "" ? detail : `Your API key is valid, but this account cannot use workspace "${input2.workspaceId}".`, `Pick a workspace you belong to: mutagent workspaces list, then mutagent workspaces use <name>`, 3);
24752
24924
  }
24753
24925
  if (!response.ok) {
24754
24926
  const detail = scrubKnown(messageFrom(await response.text().catch(() => "")));
@@ -24821,22 +24993,25 @@ async function withStoppingRetry(send, sleep2 = (ms) => new Promise((resolve9) =
24821
24993
 
24822
24994
  // src/lib/sandbox-api.ts
24823
24995
  init_known_secrets();
24824
- function resolveConnection() {
24996
+ async function resolveConnection() {
24825
24997
  const apiKey = getApiKey();
24826
24998
  if (!apiKey)
24827
24999
  throw new AuthenticationError;
24828
- const config = loadConfig();
24829
- const configured = config.endpoint?.trim();
24830
- const endpoint = (configured === undefined || configured === "" ? "https://api.mutagent.io" : configured).replace(/\/+$/, "");
24831
- const workspaceId = requireWorkspace(config.defaultWorkspace);
24832
- const headers = { "x-workspace-id": workspaceId };
24833
- if (config.defaultOrganization)
24834
- headers["x-organization-id"] = config.defaultOrganization;
24835
- return { endpoint, apiKey, workspaceId, headers };
25000
+ const tenancy = await resolveKeyTenancy(apiKey, { probe: envKeyReplacesFile() });
25001
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
25002
+ throw noWorkspaceSelected();
25003
+ const workspaceId = requireWorkspace(tenancy.workspaceId);
25004
+ const headers = tenancyHeaders({
25005
+ keyScope: tenancy.keyScope,
25006
+ workspaceId,
25007
+ workspaceOrigin: tenancy.workspaceOrigin,
25008
+ organizationId: tenancy.organization?.id
25009
+ });
25010
+ return { endpoint: tenancy.endpoint, apiKey, workspaceId, headers };
24836
25011
  }
24837
25012
  function requireWorkspace(workspaceId) {
24838
25013
  if (workspaceId === undefined || workspaceId.trim() === "") {
24839
- throw new WorkspaceContextError("A workspace is required: sandbox access is minted per workspace, not per account.");
25014
+ throw new WorkspaceContextError("No workspace selected: sandbox access is minted per workspace, not per account.");
24840
25015
  }
24841
25016
  return workspaceId.trim();
24842
25017
  }
@@ -24852,7 +25027,7 @@ async function fetchWithTokenRetry(url, init, baseHeaders, transport) {
24852
25027
  return attempt(true);
24853
25028
  }
24854
25029
  async function sandboxFetch(path, init, deps = {}) {
24855
- const connection = resolveConnection();
25030
+ const connection = await resolveConnection();
24856
25031
  const { endpoint } = connection;
24857
25032
  try {
24858
25033
  const response = await fetchWithTokenRetry(`${endpoint}${path}`, init, { ...connection.headers, ...toHeaderRecord(init.headers) }, {
@@ -28091,7 +28266,7 @@ Examples:
28091
28266
  ${chalk34.dim("$")} mutagent sandbox ls --json
28092
28267
 
28093
28268
  Lists the sandboxes in the configured workspace
28094
- (mutagent config set workspace <workspace-id>).
28269
+ (mutagent workspaces use <name>).
28095
28270
  An empty list means none are running.
28096
28271
 
28097
28272
  AI Agent Directive:
@@ -28323,7 +28498,7 @@ AI Agent Directive:
28323
28498
  spawned sandbox runs until it is removed or stopped by the idle policy: confirm
28324
28499
  with the user before 'spawn' and before 'rm', and tell them the sandbox ID.
28325
28500
 
28326
- Requires a workspace: mutagent config set workspace <workspace-id>
28501
+ Requires a workspace: mutagent workspaces use <name> (or --workspace for one command)
28327
28502
  `);
28328
28503
  registerSpawnCommand(sandbox);
28329
28504
  registerLsCommand(sandbox);
@@ -28459,7 +28634,7 @@ Values are write-only. \`show\` prints each entry's name and a sha256[:8] finger
28459
28634
  Names: Environments use letters, digits, '.', '_' and '-' (up to 64); variables use
28460
28635
  A-Z, 0-9 and _, not starting with a digit. An Environment holds up to 64 KiB.
28461
28636
 
28462
- Requires a workspace: mutagent config set workspace <workspace-id>
28637
+ Requires a workspace: mutagent workspaces use <name> (or --workspace for one command)
28463
28638
 
28464
28639
  AI Agent Directive:
28465
28640
  Use 'set' to create or update — it creates the Environment when absent and
@@ -28694,6 +28869,7 @@ function printEntries(environment) {
28694
28869
 
28695
28870
  // src/bin/cli.ts
28696
28871
  init_config();
28872
+ init_organization_scope();
28697
28873
 
28698
28874
  // src/lib/brand.ts
28699
28875
  var BRAND = {
@@ -28762,7 +28938,7 @@ ${lines.join(`
28762
28938
  }
28763
28939
 
28764
28940
  // src/lib/global-version-flag.ts
28765
- var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint"]);
28941
+ var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint", "--workspace", "--org"]);
28766
28942
  function findSubcommandIndex(rawArgs) {
28767
28943
  for (let i = 0;i < rawArgs.length; i++) {
28768
28944
  const arg = rawArgs[i];
@@ -28862,7 +29038,7 @@ To pass the Mutagent platform key instead, put it before the command (mutagent -
28862
29038
  function rootValueOptionRefusal(option, label) {
28863
29039
  const command = label.replace(/^mutagent /, "");
28864
29040
  const what = option === "--endpoint" ? "its own server endpoint, and would send the Mutagent platform key there" : "its own option";
28865
- const env = option === "--endpoint" ? " or export MUTAGENT_ENDPOINT" : "";
29041
+ const env = option === "--endpoint" ? " or export MUTAGENT_ENDPOINT" : option === "--workspace" ? " or export MUTAGENT_WORKSPACE_ID" : "";
28866
29042
  return new MutagentError("NOT_SUPPORTED", `${option} after '${label}' and before a -- is refused: the mutagent CLI would read it as ${what}, and the command it forwards would never see it.`, `For the mutagent CLI, put it before the command (mutagent ${option} <value> ${command} …)${env}.
28867
29043
  For the forwarded command, put it after --: ${label} … -- … ${option} <value>.`, 2);
28868
29044
  }
@@ -29254,7 +29430,7 @@ var program = new Command18;
29254
29430
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
29255
29431
 
29256
29432
  Documentation: https://docs.mutagent.io/cli
29257
- Dashboard: https://app.mutagent.io`).option("--json", "Output results as JSON (for AI agents)").option("--api-key <key>", "Mutagent API key").option("--endpoint <url>", "Mutagent server endpoint").option("--non-interactive", "Disable interactive prompts (for CI/AI agents)").configureHelp({
29433
+ Dashboard: https://app.mutagent.io`).option("--json", "Output results as JSON (for AI agents)").option("--api-key <key>", "Mutagent API key").option("--endpoint <url>", "Mutagent server endpoint").option("--workspace <name|id>", "Workspace for this command only (beats MUTAGENT_WORKSPACE_ID and the selection)").option("--org <id|slug>", "Check that the key belongs to this organization (its id, slug or name); a mismatch exits 1 and sends nothing (with login: the organization to sign in to)").option("--non-interactive", "Disable interactive prompts (for CI/AI agents)").configureHelp({
29258
29434
  sortSubcommands: true,
29259
29435
  showGlobalOptions: true
29260
29436
  });
@@ -29272,9 +29448,11 @@ ${chalk37.yellow("Managed agents:")}
29272
29448
  mutagent agent --help ${chalk37.dim("check, pack, run, deploy, activate, list, inspect, retire")}
29273
29449
 
29274
29450
  ${chalk37.yellow("Workspace:")}
29275
- mutagent auth status ${chalk37.dim("Sign-in state and the active workspace")}
29276
- mutagent workspaces list ${chalk37.dim("Workspaces you can see")}
29277
- mutagent config set workspace <id> ${chalk37.dim("Choose the active workspace")}
29451
+ mutagent workspaces use <name> ${chalk37.dim("Choose the workspace every later command works in")}
29452
+ mutagent workspaces current ${chalk37.dim("The key's scope, organization and the verified workspace")}
29453
+ mutagent workspaces list ${chalk37.dim("Your workspaces in the key's organization")}
29454
+ --workspace <name|id> ${chalk37.dim("One command in another workspace (then MUTAGENT_WORKSPACE_ID, then the selection)")}
29455
+ mutagent login --org <slug> ${chalk37.dim("Sign in for another organization")}
29278
29456
  mutagent providers list ${chalk37.dim("LLM providers in the workspace")}
29279
29457
  mutagent helix models ${chalk37.dim("Models Helix can use in the cloud, and the workspace default model")}
29280
29458
  mutagent env ls ${chalk37.dim("Environments: variables and secrets for tools")}
@@ -29337,7 +29515,7 @@ var rootOptionError = forwardedRootOptionRefusal(rawArgs);
29337
29515
  if (rootOptionError !== undefined) {
29338
29516
  handleError(rootOptionError, rawArgs.includes("--json"));
29339
29517
  }
29340
- program.hook("preAction", (thisCommand) => {
29518
+ program.hook("preAction", (thisCommand, actionCommand) => {
29341
29519
  const globalOpts = thisCommand.optsWithGlobals();
29342
29520
  if (globalOpts.apiKey && !process.env.MUTAGENT_API_KEY) {
29343
29521
  process.env.MUTAGENT_API_KEY = globalOpts.apiKey;
@@ -29345,6 +29523,13 @@ program.hook("preAction", (thisCommand) => {
29345
29523
  if (globalOpts.endpoint && !process.env.MUTAGENT_ENDPOINT) {
29346
29524
  process.env.MUTAGENT_ENDPOINT = globalOpts.endpoint;
29347
29525
  }
29526
+ if (globalOpts.workspace) {
29527
+ process.env.MUTAGENT_WORKSPACE_ID = globalOpts.workspace;
29528
+ setWorkspaceFlag(globalOpts.workspace);
29529
+ }
29530
+ if (actionCommand.name() !== "login") {
29531
+ setOrgAssertion(globalOpts.org);
29532
+ }
29348
29533
  if (globalOpts.nonInteractive || process.env.CI === "true" || !process.stdin.isTTY) {
29349
29534
  process.env.MUTAGENT_NON_INTERACTIVE = "true";
29350
29535
  }
@@ -29388,4 +29573,4 @@ if (isAgentInvocation(rawArgs)) {
29388
29573
  program.parse(argvForCommander, { from: "user" });
29389
29574
  }
29390
29575
 
29391
- //# debugId=DD369C05E2A6AE2D64756E2164756E21
29576
+ //# debugId=22EA0CB4BCE602F064756E2164756E21