@mutagent/cli 0.1.379 → 0.1.380

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 +908 -750
  2. package/dist/index.js +479 -402
  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,430 @@ 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
+ tenancyHeaders: () => tenancyHeaders,
16684
+ tenancyFailure: () => tenancyFailure,
16685
+ setWorkspaceFlag: () => setWorkspaceFlag,
16686
+ setOrgAssertion: () => setOrgAssertion,
16687
+ resolveKeyTenancy: () => resolveKeyTenancy,
16688
+ otherOrganizationHint: () => otherOrganizationHint,
16689
+ noWorkspaceSelected: () => noWorkspaceSelected,
16690
+ matchesOrganization: () => matchesOrganization,
16691
+ looksLikeWorkspaceId: () => looksLikeWorkspaceId,
16692
+ listKeyWorkspaces: () => listKeyWorkspaces,
16693
+ findWorkspace: () => findWorkspace,
16694
+ fetchOrganizations: () => fetchOrganizations,
16695
+ fetchKeySelf: () => fetchKeySelf,
16696
+ envKeyHasNoSelection: () => envKeyHasNoSelection,
16697
+ configTenancyHeaders: () => configTenancyHeaders,
16698
+ TenancyError: () => TenancyError,
16699
+ TENANCY_DENIED: () => TENANCY_DENIED
16700
+ });
16701
+ function looksLikeWorkspaceId(value2) {
16702
+ return UUID_PATTERN.test(value2);
16703
+ }
16704
+ function tenancyHeaders(t) {
16705
+ const headers = {};
16706
+ if (!t.workspaceId)
16707
+ return headers;
16708
+ if (t.keyScope === "workspace") {
16709
+ if (t.workspaceOrigin === "override")
16710
+ headers["x-workspace-id"] = t.workspaceId;
16711
+ return headers;
16712
+ }
16713
+ headers["x-workspace-id"] = t.workspaceId;
16714
+ if (t.keyScope === "user" && t.organizationId)
16715
+ headers["x-organization-id"] = t.organizationId;
16716
+ return headers;
16717
+ }
16718
+ function configTenancyHeaders(config) {
16719
+ return tenancyHeaders({
16720
+ keyScope: config.keyScope,
16721
+ workspaceId: config.defaultWorkspace,
16722
+ workspaceOrigin: config.workspaceOrigin,
16723
+ organizationId: config.defaultOrganization
16724
+ });
16725
+ }
16726
+ function parseBody(body) {
16727
+ try {
16728
+ const parsed = JSON.parse(body);
16729
+ if (parsed && typeof parsed === "object")
16730
+ return parsed;
16731
+ } catch {}
16732
+ return {};
16733
+ }
16734
+ function tenancyFailure(status, body, ctx) {
16735
+ const parsed = parseBody(body);
16736
+ if (status === 401) {
16737
+ return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login", 2);
16738
+ }
16739
+ if (status === 403 && parsed.code === TENANCY_DENIED) {
16740
+ const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
16741
+ if (ctx.workspaceOrigin === "selection") {
16742
+ return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
16743
+ }
16744
+ const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
16745
+ return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
16746
+ }
16747
+ if (status === 403 && ctx.keyScope === "workspace" && ctx.workspaceOrigin === "override") {
16748
+ 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)");
16749
+ }
16750
+ if (status === 400 && (parsed.message ?? body).toLowerCase().includes("x-workspace-id")) {
16751
+ return noWorkspaceSelected();
16752
+ }
16753
+ return;
16754
+ }
16755
+ function noWorkspaceSelected() {
16756
+ const fix = envKeyReplacesFile() ? ENV_KEY_WORKSPACE_FIX : "Run: mutagent workspaces use <name>";
16757
+ return new TenancyError("WORKSPACE_REQUIRED", "No workspace selected.", fix, 3);
16758
+ }
16759
+ function envKeyHasNoSelection() {
16760
+ return new TenancyError("WORKSPACE_FROM_ENV", "With MUTAGENT_API_KEY set, the workspace comes from --workspace or MUTAGENT_WORKSPACE_ID.", ENV_KEY_WORKSPACE_FIX);
16761
+ }
16762
+ async function tenancyFetch(url, init) {
16763
+ try {
16764
+ return await fetch(url, init);
16765
+ } catch {
16766
+ throw new MutagentError("REQUEST_FAILED", `Unable to connect to the Mutagent server at ${new URL(url).origin}.`, "Check your connection and the endpoint: mutagent config get endpoint");
16767
+ }
16768
+ }
16769
+ async function fetchKeySelf(apiKey, endpoint, opts = {}) {
16770
+ const response = await tenancyFetch(`${endpoint}/api/api-keys/self`, {
16771
+ headers: { "x-api-key": apiKey, ...opts.headers }
16772
+ });
16773
+ if (!response.ok) {
16774
+ const body = await response.text().catch(() => "");
16775
+ 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");
16776
+ }
16777
+ return await response.json();
16778
+ }
16779
+ async function fetchOrganizations(apiKey, endpoint) {
16780
+ try {
16781
+ const response = await fetch(`${endpoint}/api/organizations`, {
16782
+ headers: { "x-api-key": apiKey }
16783
+ });
16784
+ if (!response.ok)
16785
+ return [];
16786
+ const data = await response.json();
16787
+ return data.data ?? [];
16788
+ } catch {
16789
+ return [];
16790
+ }
16791
+ }
16792
+ function matchesOrganization(value2, org) {
16793
+ if (!org)
16794
+ return false;
16795
+ return value2 === org.id || org.slug !== undefined && value2 === org.slug;
16796
+ }
16797
+ function otherOrganizationHint(wanted, keyOrg, orgs) {
16798
+ const others = orgs.filter((o) => o.id !== keyOrg?.id);
16799
+ const where = keyOrg ? ` in ${keyOrg.name}` : "";
16800
+ const what = `No workspace "${wanted}"${where}.`;
16801
+ const named = others.map((o) => `${o.name} (${o.slug ?? o.id})`).join(", ");
16802
+ if (others.length === 1 && others[0]) {
16803
+ const only = others[0];
16804
+ return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organization: ${named}.`, `To switch: mutagent login --org ${only.slug ?? only.id}`);
16805
+ }
16806
+ if (others.length > 1) {
16807
+ return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organizations: ${named}.`, "To switch: mutagent login --org <slug>");
16808
+ }
16809
+ return new TenancyError("WORKSPACE_NOT_FOUND", what, "See: mutagent workspaces list");
16810
+ }
16811
+ function setOrgAssertion(value2) {
16812
+ const trimmed = value2?.trim();
16813
+ orgAssertion = trimmed === "" ? undefined : trimmed;
16814
+ }
16815
+ function setWorkspaceFlag(value2) {
16816
+ const trimmed = value2?.trim();
16817
+ workspaceFlag = trimmed === "" ? undefined : trimmed;
16818
+ }
16819
+ function endpointOf(configured) {
16820
+ const trimmed = configured?.trim();
16821
+ return (trimmed === undefined || trimmed === "" ? "https://api.mutagent.io" : trimmed).replace(/\/+$/, "");
16822
+ }
16823
+ async function probeKeyScope(apiKey, endpoint, workspaceNamed) {
16824
+ const fromFile = !envKeyReplacesFile();
16825
+ if (!fromFile && workspaceNamed && orgAssertion === undefined)
16826
+ return;
16827
+ try {
16828
+ const self = await fetchKeySelf(apiKey, endpoint);
16829
+ if (fromFile)
16830
+ recordKeyScope(self);
16831
+ return self;
16832
+ } catch (error3) {
16833
+ if (error3 instanceof MutagentError && (error3.code === "AUTH_EXPIRED" || error3.code === "REQUEST_FAILED"))
16834
+ throw error3;
16835
+ return;
16836
+ }
16837
+ }
16838
+ function enforceOrgAssertion(org) {
16839
+ if (orgAssertion === undefined)
16840
+ return;
16841
+ if (org === undefined) {
16842
+ throw new TenancyError("ORG_UNKNOWN", "This key's organization is unknown, so --org cannot be checked.", "Run: mutagent workspaces current");
16843
+ }
16844
+ if (!matchesOrganization(orgAssertion, org)) {
16845
+ throw new TenancyError("ORG_MISMATCH", `Your key is for ${org.name}.`, `Run: mutagent login --org ${orgAssertion}`);
16846
+ }
16847
+ }
16848
+ async function resolveKeyTenancy(apiKey, opts = {}) {
16849
+ const config = loadConfig();
16850
+ const endpoint = endpointOf(config.endpoint);
16851
+ let keyScope = config.keyScope;
16852
+ let organization = config.defaultOrganization ? { id: config.defaultOrganization, name: config.organizationName ?? config.defaultOrganization, slug: config.organizationSlug } : undefined;
16853
+ let workspaceId = config.defaultWorkspace;
16854
+ let workspaceName = config.workspaceName;
16855
+ const workspaceOrigin = config.workspaceOrigin;
16856
+ if (keyScope === undefined && (opts.probe !== false || orgAssertion !== undefined)) {
16857
+ const self = await probeKeyScope(apiKey, endpoint, workspaceId !== undefined);
16858
+ if (self) {
16859
+ keyScope = self.scope;
16860
+ organization = self.organization ?? undefined;
16861
+ if (self.scope === "workspace" && self.workspace && workspaceOrigin !== "override") {
16862
+ workspaceId = self.workspace.id;
16863
+ workspaceName = self.workspace.name;
16864
+ }
16865
+ }
16866
+ }
16867
+ enforceOrgAssertion(organization);
16868
+ if (opts.resolveOverride !== false && workspaceFlag !== undefined && workspaceId === workspaceFlag && !looksLikeWorkspaceId(workspaceId)) {
16869
+ const found = await findWorkspace(workspaceId, { apiKey, endpoint, keyScope, organization, workspaceId: config.defaultWorkspace, workspaceName });
16870
+ workspaceId = found.id;
16871
+ workspaceName = found.name;
16872
+ process.env.MUTAGENT_WORKSPACE_ID = found.id;
16873
+ }
16874
+ return { endpoint, keyScope, organization, workspaceId, workspaceName, workspaceOrigin };
16875
+ }
16876
+ async function listKeyWorkspaces(apiKey, endpoint, t) {
16877
+ const headers = { "x-api-key": apiKey };
16878
+ if (t.keyScope === "user" && t.organization)
16879
+ headers["x-organization-id"] = t.organization.id;
16880
+ const response = await tenancyFetch(`${endpoint}/api/workspaces`, { headers });
16881
+ if (!response.ok) {
16882
+ const body = await response.text().catch(() => "");
16883
+ 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");
16884
+ }
16885
+ const data = await response.json();
16886
+ return data.workspaces ?? [];
16887
+ }
16888
+ async function findWorkspace(wanted, t) {
16889
+ if (t.keyScope === "workspace") {
16890
+ if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
16891
+ return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
16892
+ }
16893
+ 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)");
16894
+ }
16895
+ const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
16896
+ const byId = rows.find((w) => w.id === wanted);
16897
+ if (byId)
16898
+ return { id: byId.id, name: byId.name };
16899
+ let matches = rows.filter((w) => w.name === wanted || w.slug === wanted);
16900
+ if (matches.length === 0)
16901
+ matches = rows.filter((w) => w.name.toLowerCase() === wanted.toLowerCase());
16902
+ if (matches.length === 1 && matches[0])
16903
+ return { id: matches[0].id, name: matches[0].name };
16904
+ if (matches.length > 1) {
16905
+ throw new TenancyError("WORKSPACE_AMBIGUOUS", `Several workspaces are named "${wanted}": ${matches.map((w) => `${w.name} (${w.id})`).join(", ")}.`, "Run: mutagent workspaces use <id>");
16906
+ }
16907
+ const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
16908
+ throw otherOrganizationHint(wanted, t.organization, orgs);
16909
+ }
16910
+ var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag;
16911
+ var init_organization_scope = __esm(() => {
16912
+ init_errors();
16913
+ init_config();
16914
+ UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16915
+ TenancyError = class TenancyError extends MutagentError {
16916
+ constructor(code, what, fix, exitCode = 1) {
16917
+ super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""), exitCode);
16918
+ this.name = "TenancyError";
16919
+ }
16920
+ };
16921
+ });
16922
+
16923
+ // src/lib/provider-request.ts
16924
+ function buildCreateProviderBody(data) {
16925
+ const body = {
16926
+ name: data.name,
16927
+ provider: data.provider,
16928
+ isDefault: data.isDefault ?? false
16929
+ };
16930
+ if (data.apiKey)
16931
+ body.apiKey = data.apiKey;
16932
+ if (data.baseUrl)
16933
+ body.baseUrl = data.baseUrl;
16934
+ if (data.hostedFamily)
16935
+ body.hostedFamily = data.hostedFamily;
16936
+ if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
16937
+ body.credentialFields = data.credentialFields;
16938
+ }
16939
+ if (data.metadata && Object.keys(data.metadata).length > 0)
16940
+ body.metadata = data.metadata;
16941
+ if (data.supportedModels !== undefined)
16942
+ body.supportedModels = data.supportedModels;
16943
+ return body;
16944
+ }
16945
+
16522
16946
  // src/lib/sdk-debug.ts
16523
16947
  function isDebugEnabled(env = process.env) {
16524
16948
  return TRUTHY.has((env.MUTAGENT_DEBUG ?? "").trim().toLowerCase());
@@ -16613,318 +17037,42 @@ var init_sdk_debug = __esm(() => {
16613
17037
  };
16614
17038
  });
16615
17039
 
16616
- // src/lib/organization-scope.ts
16617
- function isWellFormedOrganizationId(id) {
16618
- return ORGANIZATION_ID_PATTERN.test(id);
17040
+ // src/lib/sdk-mappers.ts
17041
+ import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
17042
+ function isProviderType(value2) {
17043
+ return PROVIDER_TYPES.includes(value2);
16619
17044
  }
16620
- function isOrganizationScopeMismatch(status, body) {
16621
- if (status !== 403)
16622
- return false;
16623
- return body.toLowerCase().includes("organization scope mismatch");
17045
+ function narrowProviderType(value2) {
17046
+ return isProviderType(value2) ? value2 : value2;
16624
17047
  }
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
- }
17048
+ function toIsoString(value2) {
17049
+ if (value2 === null || value2 === undefined)
17050
+ return null;
17051
+ return value2 instanceof Date ? value2.toISOString() : value2;
16637
17052
  }
16638
- async function fetchOrganizations(apiKey, endpoint) {
16639
- return lookupOrganizations(apiKey, endpoint);
17053
+ function toAgent(sdk) {
17054
+ return {
17055
+ id: sdk.id,
17056
+ name: sdk.name,
17057
+ slug: sdk.slug,
17058
+ description: sdk.description,
17059
+ systemPrompt: sdk.systemPrompt,
17060
+ model: sdk.model,
17061
+ maxTurns: sdk.maxTurns,
17062
+ permissionMode: sdk.permissionMode,
17063
+ allowedTools: sdk.allowedTools,
17064
+ disallowedTools: sdk.disallowedTools,
17065
+ status: sdk.status,
17066
+ isPublic: sdk.isPublic,
17067
+ metadata: sdk.metadata,
17068
+ tags: sdk.tags,
17069
+ createdBy: sdk.createdBy,
17070
+ createdAt: toIsoString(sdk.createdAt),
17071
+ updatedAt: toIsoString(sdk.updatedAt)
17072
+ };
16640
17073
  }
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
- };
16925
- }
16926
- function toAgentList(sdk) {
16927
- return sdk.map(toAgent);
17074
+ function toAgentList(sdk) {
17075
+ return sdk.map(toAgent);
16928
17076
  }
16929
17077
  function toWorkspace(sdk) {
16930
17078
  return {
@@ -16977,15 +17125,6 @@ var init_sdk_mappers = __esm(() => {
16977
17125
  });
16978
17126
 
16979
17127
  // 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
17128
  import { Mutagent, HTTPClient } from "@mutagent/sdk";
16990
17129
 
16991
17130
  class SDKClientWrapper {
@@ -16994,32 +17133,24 @@ class SDKClientWrapper {
16994
17133
  endpoint;
16995
17134
  workspaceId;
16996
17135
  organizationId;
16997
- workspaceResolution = "none";
17136
+ tenancy;
16998
17137
  debugSecrets = new Set;
16999
- recoveringFetch;
17000
17138
  constructor(opts) {
17001
17139
  this.apiKey = opts.apiKey;
17002
17140
  this.endpoint = opts.serverURL ?? "http://localhost:3003";
17003
17141
  this.workspaceId = opts.workspaceId;
17004
17142
  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 });
17143
+ this.tenancy = {
17144
+ keyScope: opts.keyScope,
17145
+ workspaceId: opts.workspaceId,
17146
+ workspaceName: opts.workspaceName,
17147
+ workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
17148
+ organizationName: opts.organizationName
17149
+ };
17150
+ const httpClient = new HTTPClient;
17018
17151
  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);
17152
+ for (const [name, value2] of Object.entries(this.tenancyHeaders()))
17153
+ req.headers.set(name, value2);
17023
17154
  return req;
17024
17155
  });
17025
17156
  this.debugSecrets.add(this.apiKey);
@@ -17036,6 +17167,11 @@ class SDKClientWrapper {
17036
17167
  handleError(error3) {
17037
17168
  if (error3 && typeof error3 === "object") {
17038
17169
  const err = error3;
17170
+ if (err.statusCode) {
17171
+ const tenancy = tenancyFailure(err.statusCode, err.body ?? "", this.tenancy);
17172
+ if (tenancy)
17173
+ throw tenancy;
17174
+ }
17039
17175
  if (err.statusCode === 401) {
17040
17176
  throw new AuthenticationError;
17041
17177
  }
@@ -17074,28 +17210,26 @@ class SDKClientWrapper {
17074
17210
  } else if (optHeaders) {
17075
17211
  extraHeaders = optHeaders;
17076
17212
  }
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}`, {
17213
+ const response = await fetch(`${this.endpoint}${path}`, {
17083
17214
  ...options,
17084
17215
  headers: {
17085
17216
  "x-api-key": this.apiKey,
17086
17217
  "Content-Type": "application/json",
17087
- ...tenancyHeaders,
17218
+ ...this.tenancyHeaders(),
17088
17219
  ...extraHeaders
17089
17220
  }
17090
17221
  });
17091
17222
  if (!response.ok) {
17223
+ const error3 = await response.text();
17224
+ const tenancy = tenancyFailure(response.status, error3, this.tenancy);
17225
+ if (tenancy)
17226
+ throw tenancy;
17092
17227
  if (response.status === 401) {
17093
17228
  throw new AuthenticationError;
17094
17229
  }
17095
17230
  if (response.status === 403) {
17096
- throw new ApiError(403, await response.text() || "Access denied");
17231
+ throw new ApiError(403, error3 || "Access denied");
17097
17232
  }
17098
- const error3 = await response.text();
17099
17233
  const errorLower = error3.toLowerCase();
17100
17234
  if (errorLower.includes("workspace") && (errorLower.includes("missing") || errorLower.includes("required")) || errorLower.includes("x-workspace-id")) {
17101
17235
  throw new WorkspaceContextError(error3);
@@ -17241,11 +17375,9 @@ class SDKClientWrapper {
17241
17375
  }
17242
17376
  async getModelsCatalog() {
17243
17377
  try {
17244
- const wsId = this.workspaceId;
17245
- if (!wsId)
17378
+ if (!this.workspaceId)
17246
17379
  return { models: [] };
17247
- const qs = `?workspaceId=${encodeURIComponent(wsId)}`;
17248
- return await this.request(`/api/providers/catalog${qs}`);
17380
+ return await this.request("/api/providers/catalog");
17249
17381
  } catch {
17250
17382
  return { models: [] };
17251
17383
  }
@@ -17253,56 +17385,17 @@ class SDKClientWrapper {
17253
17385
  getCurrentWorkspaceId() {
17254
17386
  return this.workspaceId;
17255
17387
  }
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;
17388
+ tenancyHeaders() {
17389
+ return tenancyHeaders({
17390
+ keyScope: this.tenancy.keyScope,
17391
+ workspaceId: this.workspaceId,
17392
+ workspaceOrigin: this.tenancy.workspaceOrigin,
17393
+ organizationId: this.organizationId
17394
+ });
17298
17395
  }
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
- }
17396
+ requireSelectedWorkspace() {
17397
+ if (!this.workspaceId)
17398
+ throw noWorkspaceSelected();
17306
17399
  return this.workspaceId;
17307
17400
  }
17308
17401
  async testProvider(id) {
@@ -17316,7 +17409,7 @@ class SDKClientWrapper {
17316
17409
  }
17317
17410
  }
17318
17411
  async createProvider(data) {
17319
- this.requireUnambiguousWorkspace();
17412
+ this.requireSelectedWorkspace();
17320
17413
  registerSecret(data.apiKey);
17321
17414
  registerSecretFields(data.credentialFields);
17322
17415
  const body = buildCreateProviderBody(data);
@@ -17331,7 +17424,7 @@ class SDKClientWrapper {
17331
17424
  }
17332
17425
  }
17333
17426
  async updateProvider(id, data) {
17334
- const workspaceId = this.requireUnambiguousWorkspace();
17427
+ const workspaceId = this.requireSelectedWorkspace();
17335
17428
  if (data.apiKey)
17336
17429
  this.debugSecrets.add(data.apiKey);
17337
17430
  registerSecret(data.apiKey);
@@ -17370,7 +17463,7 @@ class SDKClientWrapper {
17370
17463
  }
17371
17464
  }
17372
17465
  async deleteProvider(id) {
17373
- const workspaceId = this.requireUnambiguousWorkspace();
17466
+ const workspaceId = this.requireSelectedWorkspace();
17374
17467
  try {
17375
17468
  await this.sdk.llmProviders.deleteProviderConfig({ id });
17376
17469
  return { workspaceId };
@@ -17385,21 +17478,18 @@ async function getSDKClient() {
17385
17478
  if (!apiKey) {
17386
17479
  throw new AuthenticationError;
17387
17480
  }
17388
- const config = loadConfig();
17481
+ const tenancy = await resolveKeyTenancy(apiKey);
17482
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
17483
+ throw noWorkspaceSelected();
17389
17484
  sdkClient = new SDKClientWrapper({
17390
17485
  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
- }
17486
+ serverURL: tenancy.endpoint,
17487
+ workspaceId: tenancy.workspaceId,
17488
+ workspaceName: tenancy.workspaceName,
17489
+ workspaceOrigin: tenancy.workspaceOrigin,
17490
+ organizationId: tenancy.organization?.id,
17491
+ organizationName: tenancy.organization?.name,
17492
+ keyScope: tenancy.keyScope
17403
17493
  });
17404
17494
  }
17405
17495
  return sdkClient;
@@ -17417,19 +17507,6 @@ async function validateApiKey(apiKey, endpoint) {
17417
17507
  return false;
17418
17508
  }
17419
17509
  }
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
17510
  var sdkClient = null;
17434
17511
  var init_sdk_client = __esm(() => {
17435
17512
  init_errors();
@@ -17558,7 +17635,7 @@ async function createAgentDeploymentAPI() {
17558
17635
  const context = await getSDKClient();
17559
17636
  let workspaceId;
17560
17637
  try {
17561
- workspaceId = context.requireUnambiguousWorkspace();
17638
+ workspaceId = context.requireSelectedWorkspace();
17562
17639
  } catch (error3) {
17563
17640
  if (error3 instanceof MutagentError)
17564
17641
  error3.exitCode = 3;
@@ -18496,17 +18573,13 @@ async function installHelix(options, deps = {}) {
18496
18573
 
18497
18574
  // src/lib/install-telemetry.ts
18498
18575
  init_config();
18576
+ init_organization_scope();
18499
18577
  var TELEMETRY_TIMEOUT_MS = 1500;
18500
18578
  function resolveApiAuth() {
18501
18579
  const apiKey = getApiKey();
18502
18580
  const config = loadConfig();
18503
18581
  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 };
18582
+ return { apiBase, apiKey, headers: configTenancyHeaders(config) };
18510
18583
  }
18511
18584
  async function reportInstall(event, deps = {}) {
18512
18585
  try {
@@ -18619,6 +18692,7 @@ async function runPostOnboarding(deps = {}) {
18619
18692
  // src/lib/auth-flow.ts
18620
18693
  init_config();
18621
18694
  init_sdk_client();
18695
+ init_organization_scope();
18622
18696
  init_errors();
18623
18697
  import { input, password, select } from "@inquirer/prompts";
18624
18698
  import chalk3 from "chalk";
@@ -18667,6 +18741,17 @@ async function openBrowser(url) {
18667
18741
  throw new BrowserAuthError("BROWSER_OPEN_FAILED", `Could not open browser automatically. Please visit: ${url}`);
18668
18742
  }
18669
18743
  }
18744
+ function withOrgPreselect(authUrl, org) {
18745
+ if (!org)
18746
+ return authUrl;
18747
+ try {
18748
+ const url = new URL(authUrl);
18749
+ url.searchParams.set("org", org);
18750
+ return url.toString();
18751
+ } catch {
18752
+ return `${authUrl}${authUrl.includes("?") ? "&" : "?"}org=${encodeURIComponent(org)}`;
18753
+ }
18754
+ }
18670
18755
  function sleep(ms) {
18671
18756
  return new Promise((resolve6) => setTimeout(resolve6, ms));
18672
18757
  }
@@ -18680,7 +18765,7 @@ async function performBrowserAuth(options, onStatusUpdate) {
18680
18765
  const cliToken = generateCliToken();
18681
18766
  onStatusUpdate?.("Initializing browser authentication...");
18682
18767
  const initResponse = await initBrowserAuth(endpoint, cliToken);
18683
- const { authUrl } = initResponse;
18768
+ const authUrl = withOrgPreselect(initResponse.authUrl, options.org);
18684
18769
  if (!skipBrowserOpen) {
18685
18770
  onStatusUpdate?.("Opening browser for authentication...");
18686
18771
  try {
@@ -18797,7 +18882,7 @@ async function performLoginAction(opts) {
18797
18882
  useBrowserAuth = method === "browser";
18798
18883
  }
18799
18884
  if (useBrowserAuth) {
18800
- return loginWithBrowser(endpoint, output, wasFirstLogin);
18885
+ return loginWithBrowser(endpoint, output, wasFirstLogin, opts.org);
18801
18886
  }
18802
18887
  return loginWithPastedKey(endpoint, output, wasFirstLogin);
18803
18888
  }
@@ -18814,55 +18899,76 @@ function buildLoginJsonResponse(result) {
18814
18899
  }
18815
18900
  };
18816
18901
  }
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");
18902
+ async function describeKey(apiKey, endpoint, pick3) {
18903
+ let self;
18904
+ try {
18905
+ self = await fetchKeySelf(apiKey, endpoint);
18906
+ } catch {
18907
+ return {};
18822
18908
  }
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
- });
18909
+ const organization = self.organization ?? undefined;
18910
+ const scope = {
18911
+ keyScope: self.scope,
18912
+ organizationId: organization?.id,
18913
+ organizationName: organization?.name,
18914
+ organizationSlug: organization?.slug
18915
+ };
18916
+ if (self.workspace) {
18917
+ return { ...scope, workspaceId: self.workspace.id, workspaceName: self.workspace.name };
18918
+ }
18919
+ const envWorkspace = process.env.MUTAGENT_WORKSPACE_ID?.trim();
18920
+ const rows = await listKeyWorkspaces(apiKey, endpoint, { keyScope: self.scope, organization }).catch(() => []);
18921
+ let chosen;
18922
+ if (envWorkspace)
18923
+ chosen = rows.find((w) => w.id === envWorkspace);
18924
+ else if (rows.length === 1)
18925
+ chosen = rows[0];
18926
+ else if (pick3 && rows.length > 1) {
18927
+ const id = await pick3(rows);
18928
+ chosen = rows.find((w) => w.id === id);
18929
+ }
18930
+ return chosen ? { ...scope, workspaceId: chosen.id, workspaceName: chosen.name } : scope;
18931
+ }
18932
+ function loginResult(apiKey, endpoint, described, wasFirstLogin) {
18844
18933
  return {
18845
18934
  authenticated: true,
18846
18935
  apiKey,
18847
18936
  endpoint,
18848
- workspace: wsId ? { id: wsId, name: wsName } : null,
18849
- organization: orgId ? { id: orgId, name: orgName } : null,
18937
+ workspace: described.workspaceId ? { id: described.workspaceId, name: described.workspaceName } : null,
18938
+ organization: described.organizationId ? { id: described.organizationId, name: described.organizationName } : null,
18850
18939
  wasFirstLogin
18851
18940
  };
18852
18941
  }
18853
- async function loginWithBrowser(endpoint, output, wasFirstLogin) {
18942
+ async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
18943
+ output.info("Validating API key...");
18944
+ const isValid = await validateApiKey(apiKey, endpoint);
18945
+ if (!isValid) {
18946
+ throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
18947
+ }
18948
+ const described = await describeKey(apiKey, endpoint);
18949
+ saveFullCredentials({ apiKey, endpoint, ...described });
18950
+ return loginResult(apiKey, endpoint, described, wasFirstLogin);
18951
+ }
18952
+ async function loginWithBrowser(endpoint, output, wasFirstLogin, org) {
18854
18953
  const spinner = ora2({ text: "Opening browser for authentication...", spinner: "dots" });
18855
18954
  try {
18856
18955
  spinner.start();
18857
- const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000 }, (message) => {
18956
+ const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000, org }, (message) => {
18858
18957
  spinner.text = message;
18859
18958
  });
18860
18959
  spinner.succeed("Authenticated successfully!");
18960
+ const self = await fetchKeySelf(result.apiKey, endpoint).catch(() => {
18961
+ return;
18962
+ });
18861
18963
  saveFullCredentials({
18862
18964
  apiKey: result.apiKey,
18863
18965
  endpoint,
18864
18966
  workspaceId: result.workspaceId,
18967
+ workspaceName: result.workspaceName,
18865
18968
  organizationId: result.organizationId,
18969
+ organizationName: result.organizationName,
18970
+ organizationSlug: self?.organization?.slug,
18971
+ keyScope: self?.scope,
18866
18972
  expiresAt: result.expiresAt
18867
18973
  });
18868
18974
  if (result.workspaceName)
@@ -18900,55 +19006,12 @@ async function loginWithPastedKey(initialEndpoint, output, wasFirstLogin) {
18900
19006
  if (!isValid) {
18901
19007
  throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
18902
19008
  }
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
- };
19009
+ const described = await describeKey(apiKey, endpoint, async (rows) => rows.find((w) => w.isDefault)?.id ?? select({
19010
+ message: "Select workspace:",
19011
+ choices: rows.map((w) => ({ name: w.name, value: w.id }))
19012
+ }));
19013
+ saveFullCredentials({ apiKey, endpoint, ...described });
19014
+ return loginResult(apiKey, endpoint, described, wasFirstLogin);
18952
19015
  }
18953
19016
 
18954
19017
  // src/commands/auth.ts
@@ -18978,6 +19041,7 @@ See mutagent login --help for the full description.
18978
19041
  endpoint: options.endpoint,
18979
19042
  browser: options.browser,
18980
19043
  nonInteractive: options.nonInteractive,
19044
+ org: auth.optsWithGlobals().org,
18981
19045
  isJson,
18982
19046
  output
18983
19047
  });
@@ -18990,7 +19054,7 @@ See mutagent login --help for the full description.
18990
19054
  if (result.workspace?.name)
18991
19055
  output.info(`Workspace: ${result.workspace.name}`);
18992
19056
  output.info(`Endpoint: ${result.endpoint}`);
18993
- output.info("Next: mutagent workspaces list --json");
19057
+ output.info(result.workspace ? "Next: mutagent workspaces current" : "Next: mutagent workspaces use <name>");
18994
19058
  }
18995
19059
  if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
18996
19060
  await runPostOnboarding();
@@ -19094,10 +19158,13 @@ Examples:
19094
19158
  ${chalk5.dim("$")} mutagent login ${chalk5.dim("# In a terminal, choose browser or API key")}
19095
19159
  ${chalk5.dim("$")} mutagent login --browser ${chalk5.dim("# Sign in with the browser")}
19096
19160
  ${chalk5.dim("$")} mutagent login --non-interactive ${chalk5.dim("# Browser sign-in without the method prompt (a person approves in the browser)")}
19161
+ ${chalk5.dim("$")} mutagent login --org <slug> ${chalk5.dim("# Sign in for another organization (preselected on the approval page)")}
19097
19162
  ${chalk5.dim("$")} MUTAGENT_API_KEY=mt_... mutagent login --json ${chalk5.dim("# Sign in with a key (CI)")}
19098
19163
 
19099
19164
  Browser sign-in opens app.mutagent.io, where you sign in or sign up, finish onboarding
19100
19165
  and approve this CLI. The CLI waits up to 5 minutes and saves the credentials.
19166
+ The key works in every workspace you are a member of in the approved organization, for
19167
+ 30 days. The approved workspace is selected; switch with mutagent workspaces use <name>.
19101
19168
 
19102
19169
  When MUTAGENT_API_KEY is set, login skips the browser and validates the key.
19103
19170
 
@@ -19108,8 +19175,8 @@ Environment:
19108
19175
  CI=true Same as --non-interactive
19109
19176
 
19110
19177
  Then:
19111
- ${chalk5.dim("$")} mutagent auth status
19112
- ${chalk5.dim("$")} mutagent workspaces list --json
19178
+ ${chalk5.dim("$")} mutagent workspaces current
19179
+ ${chalk5.dim("$")} mutagent workspaces use <name>
19113
19180
 
19114
19181
  mutagent auth login runs the same sign-in.
19115
19182
  `).action(async (options) => {
@@ -19120,6 +19187,7 @@ mutagent auth login runs the same sign-in.
19120
19187
  endpoint: options.endpoint,
19121
19188
  browser: options.browser,
19122
19189
  nonInteractive: options.nonInteractive,
19190
+ org: login.optsWithGlobals().org,
19123
19191
  isJson,
19124
19192
  output
19125
19193
  });
@@ -19132,7 +19200,7 @@ mutagent auth login runs the same sign-in.
19132
19200
  if (result.workspace?.name)
19133
19201
  output.info(`Workspace: ${result.workspace.name}`);
19134
19202
  output.info(`Endpoint: ${result.endpoint}`);
19135
- output.info("Next: mutagent workspaces list --json");
19203
+ output.info(result.workspace ? "Next: mutagent workspaces current" : "Next: mutagent workspaces use <name>");
19136
19204
  }
19137
19205
  if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
19138
19206
  await runPostOnboarding();
@@ -19150,163 +19218,163 @@ mutagent auth login runs the same sign-in.
19150
19218
 
19151
19219
  // src/commands/config.ts
19152
19220
  init_config();
19221
+ import { Command as Command5 } from "commander";
19222
+ import chalk7 from "chalk";
19223
+ init_errors();
19224
+ init_organization_scope();
19225
+
19226
+ // src/commands/workspaces.ts
19227
+ init_sdk_client();
19153
19228
  import { Command as Command4 } from "commander";
19154
19229
  import chalk6 from "chalk";
19155
19230
  init_errors();
19156
- init_sdk_client();
19231
+ init_config();
19157
19232
  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", `
19233
+ async function useWorkspace(value2) {
19234
+ const apiKey = getApiKey();
19235
+ if (!apiKey)
19236
+ throw new AuthenticationError;
19237
+ if (envKeyReplacesFile())
19238
+ throw envKeyHasNoSelection();
19239
+ const tenancy = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19240
+ const config = loadConfig();
19241
+ const workspace = await findWorkspace(value2, {
19242
+ apiKey,
19243
+ endpoint: tenancy.endpoint,
19244
+ keyScope: tenancy.keyScope,
19245
+ organization: tenancy.organization,
19246
+ workspaceId: config.workspaceOrigin === "selection" ? tenancy.workspaceId : undefined,
19247
+ workspaceName: config.workspaceOrigin === "selection" ? tenancy.workspaceName : undefined
19248
+ });
19249
+ setWorkspaceSelection(workspace.id, workspace.name);
19250
+ resetSDKClient();
19251
+ const org = tenancy.organization;
19252
+ return { workspace, organization: org ? { id: org.id, name: org.name } : null };
19253
+ }
19254
+ function createWorkspacesCommand() {
19255
+ const workspaces = new Command4("workspaces").alias("workspace").description("List, select and inspect workspaces").addHelpText("after", `
19161
19256
  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>
19257
+ ${chalk6.dim("$")} mutagent workspaces list
19258
+ ${chalk6.dim("$")} mutagent workspaces use <name|id> ${chalk6.dim("# Every later command works in this workspace")}
19259
+ ${chalk6.dim("$")} mutagent workspaces current --json
19260
+ ${chalk6.dim("$")} mutagent providers list --workspace <name|id> ${chalk6.dim("# One command in another workspace")}
19261
+
19262
+ The CLI key from \`mutagent login\` works in every workspace you are a member of in its
19263
+ organization. Workspace precedence: --workspace, then MUTAGENT_WORKSPACE_ID, then the
19264
+ workspace chosen with \`workspaces use\`.
19265
+ To work in another organization: mutagent login --org <slug>
19266
+ Create or change workspaces in the dashboard: https://app.mutagent.io
19166
19267
  `);
19167
- config.command("list").description("List all configuration").addHelpText("after", `
19168
- 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", `
19268
+ 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", `
19182
19269
  Examples:
19183
- ${chalk6.dim("$")} mutagent config get endpoint
19184
- ${chalk6.dim("$")} mutagent config get defaultWorkspace
19185
- ${chalk6.dim("$")} mutagent config get apiKey --json
19270
+ ${chalk6.dim("$")} mutagent workspaces list
19271
+ ${chalk6.dim("$")} mutagent workspaces list --limit 10 --json
19186
19272
 
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);
19273
+ Lists only workspaces you are a member of. Works even when the selected workspace is no
19274
+ longer available, so you can pick another with: mutagent workspaces use <name>
19275
+ `).action(async (options) => {
19276
+ const isJson = getJsonFlag(workspaces);
19191
19277
  const output = new OutputFormatter(isJson ? "json" : "table");
19192
19278
  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;
19279
+ const apiKey = getApiKey();
19280
+ if (!apiKey)
19281
+ throw new AuthenticationError;
19282
+ const tenancy = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19283
+ const rows = await listKeyWorkspaces(apiKey, tenancy.endpoint, tenancy);
19284
+ const offset = options.offset ? parseInt(options.offset, 10) : 0;
19285
+ const limit = parseInt(options.limit, 10) || 50;
19286
+ const page = rows.slice(offset, offset + limit);
19287
+ const selected = tenancy.workspaceId;
19201
19288
  if (isJson) {
19202
- output.output({ [key]: displayValue });
19289
+ output.output({
19290
+ data: page.map((w) => ({
19291
+ id: w.id,
19292
+ name: w.name,
19293
+ slug: w.slug,
19294
+ isDefault: w.isDefault,
19295
+ selected: w.id === selected,
19296
+ _links: workspaceLinks(w.id)
19297
+ })),
19298
+ total: rows.length
19299
+ });
19203
19300
  } else {
19204
- output.info(displayValue);
19301
+ output.output(page.map((w) => ({
19302
+ selected: w.id === selected ? "*" : "",
19303
+ id: w.id,
19304
+ name: w.name,
19305
+ slug: w.slug,
19306
+ url: workspaceLink(w.id)
19307
+ })));
19205
19308
  }
19206
19309
  } catch (error3) {
19207
19310
  handleError(error3, isJson);
19208
19311
  }
19209
19312
  });
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", `
19214
- Examples:
19215
- ${chalk6.dim("$")} mutagent config set workspace <workspace-id>
19216
-
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", `
19313
+ workspaces.command("use").description("Select the workspace every later command works in").argument("<name|id>", "Workspace name or id").addHelpText("after", `
19230
19314
  Examples:
19231
- ${chalk6.dim("$")} mutagent config set org <org-id>
19315
+ ${chalk6.dim("$")} mutagent workspaces use staging
19316
+ ${chalk6.dim("$")} mutagent workspaces use <workspace-id> --json
19232
19317
 
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);
19318
+ Resolves against your workspaces in the key's organization and saves the choice. No
19319
+ browser and no prompt. A workspace in another organization needs a key for that
19320
+ organization: mutagent login --org <slug>. Two workspaces with the same name: pass the id.
19321
+ `).action(async (value2) => {
19322
+ const isJson = getJsonFlag(workspaces);
19238
19323
  const output = new OutputFormatter(isJson ? "json" : "table");
19239
19324
  try {
19240
- await assertUsableOrganizationId(id, {
19241
- apiKey: getApiKey(),
19242
- endpoint: loadConfig().endpoint ?? "https://api.mutagent.io",
19243
- fetchOrganizations
19244
- });
19245
- setDefaultOrganization(id);
19246
- resetSDKClient();
19325
+ const result = await useWorkspace(value2);
19247
19326
  if (isJson) {
19248
- output.output({ success: true, defaultOrganization: id });
19327
+ output.output({ success: true, ...result, _links: workspaceLinks(result.workspace.id) });
19249
19328
  } else {
19250
- output.success(`Default organization set to ${id}`);
19329
+ output.success(`Workspace: ${result.workspace.name} (${result.workspace.id})`);
19251
19330
  }
19252
19331
  } catch (error3) {
19253
19332
  handleError(error3, isJson);
19254
19333
  }
19255
19334
  });
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", `
19335
+ workspaces.command("current").description("Show the key's scope, organization and the verified workspace").addHelpText("after", `
19267
19336
  Examples:
19268
- ${chalk7.dim("$")} mutagent workspaces list
19269
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id>
19337
+ ${chalk6.dim("$")} mutagent workspaces current
19338
+ ${chalk6.dim("$")} mutagent workspaces current --json
19270
19339
 
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) => {
19340
+ Asks the server (GET /api/api-keys/self) with the workspace this command would use, so
19341
+ the answer is verified, not read from local config.
19342
+ `).action(async () => {
19279
19343
  const isJson = getJsonFlag(workspaces);
19280
19344
  const output = new OutputFormatter(isJson ? "json" : "table");
19281
19345
  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);
19346
+ const apiKey = getApiKey();
19347
+ if (!apiKey)
19348
+ throw new AuthenticationError;
19349
+ const tenancy = await resolveKeyTenancy(apiKey);
19350
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
19351
+ throw noWorkspaceSelected();
19352
+ const headers = tenancyHeaders({
19353
+ keyScope: tenancy.keyScope,
19354
+ workspaceId: tenancy.workspaceId,
19355
+ workspaceOrigin: tenancy.workspaceOrigin,
19356
+ organizationId: tenancy.organization?.id
19357
+ });
19358
+ const self = await fetchKeySelf(apiKey, tenancy.endpoint, { headers, ctx: tenancy });
19359
+ if (!envKeyReplacesFile())
19360
+ recordKeyScope(self);
19361
+ if (!self.workspace && self.scope === "organization")
19362
+ throw noWorkspaceSelected();
19290
19363
  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 });
19364
+ output.output({
19365
+ scope: self.scope,
19366
+ organization: self.organization,
19367
+ workspace: self.workspace,
19368
+ expiresAt: self.expiresAt ?? null
19369
+ });
19300
19370
  } 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);
19371
+ output.info(`Key scope: ${self.scope}`);
19372
+ if (self.organization)
19373
+ output.info(`Organization: ${self.organization.name} (${self.organization.id})`);
19374
+ if (self.workspace)
19375
+ output.info(`Workspace: ${self.workspace.name} (${self.workspace.id})`);
19376
+ if (self.expiresAt)
19377
+ output.info(`Key expires: ${self.expiresAt}`);
19310
19378
  }
19311
19379
  } catch (error3) {
19312
19380
  handleError(error3, isJson);
@@ -19314,8 +19382,8 @@ Examples:
19314
19382
  });
19315
19383
  workspaces.command("get").description("Get workspace details").argument("<id>", "Workspace ID").addHelpText("after", `
19316
19384
  Examples:
19317
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id>
19318
- ${chalk7.dim("$")} mutagent workspaces get <workspace-id> --json
19385
+ ${chalk6.dim("$")} mutagent workspaces get <workspace-id>
19386
+ ${chalk6.dim("$")} mutagent workspaces get <workspace-id> --json
19319
19387
  `).action(async (id) => {
19320
19388
  const isJson = getJsonFlag(workspaces);
19321
19389
  const output = new OutputFormatter(isJson ? "json" : "table");
@@ -19345,6 +19413,115 @@ Examples:
19345
19413
  return workspaces;
19346
19414
  }
19347
19415
 
19416
+ // src/commands/config.ts
19417
+ var VALID_CONFIG_KEYS = ["apiKey", "endpoint", "format", "timeout", "defaultWorkspace", "workspaceName", "defaultOrganization", "organizationName", "keyScope"];
19418
+ function createConfigCommand() {
19419
+ const config = new Command5("config").description("Manage CLI configuration").addHelpText("after", `
19420
+ Examples:
19421
+ ${chalk7.dim("$")} mutagent config list
19422
+ ${chalk7.dim("$")} mutagent config get endpoint
19423
+ ${chalk7.dim("$")} mutagent config set workspace <name|id> ${chalk7.dim("# Same as: mutagent workspaces use")}
19424
+ `);
19425
+ config.command("list").description("List all configuration").addHelpText("after", `
19426
+ Examples:
19427
+ ${chalk7.dim("$")} mutagent config list
19428
+ ${chalk7.dim("$")} mutagent config list --json
19429
+ `).action(() => {
19430
+ const isJson = getJsonFlag(config);
19431
+ const output = new OutputFormatter(isJson ? "json" : "table");
19432
+ const cfg = loadConfig();
19433
+ const display = {
19434
+ ...cfg,
19435
+ apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined
19436
+ };
19437
+ output.output(display);
19438
+ });
19439
+ config.command("get").description("Get configuration value").argument("<key>", `Configuration key (${VALID_CONFIG_KEYS.join(", ")})`).addHelpText("after", `
19440
+ Examples:
19441
+ ${chalk7.dim("$")} mutagent config get endpoint
19442
+ ${chalk7.dim("$")} mutagent config get defaultWorkspace
19443
+ ${chalk7.dim("$")} mutagent config get apiKey --json
19444
+
19445
+ ${chalk7.dim(`Keys: ${VALID_CONFIG_KEYS.join(", ")}`)}
19446
+ ${chalk7.dim("A key with no value set is reported as unknown.")}
19447
+ `).action((key) => {
19448
+ const isJson = getJsonFlag(config);
19449
+ const output = new OutputFormatter(isJson ? "json" : "table");
19450
+ try {
19451
+ const cfg = loadConfig();
19452
+ if (!(key in cfg)) {
19453
+ throw new MutagentError("INVALID_ARGUMENTS", `Unknown config key: "${key}"`, `Valid keys: ${VALID_CONFIG_KEYS.join(", ")}
19454
+ ` + "Example: mutagent config get endpoint");
19455
+ }
19456
+ const value2 = cfg[key];
19457
+ const stringValue = typeof value2 === "object" ? JSON.stringify(value2) : String(value2 ?? "");
19458
+ const displayValue = key === "apiKey" && value2 ? `${stringValue.slice(0, 8)}...` : stringValue;
19459
+ if (isJson) {
19460
+ output.output({ [key]: displayValue });
19461
+ } else {
19462
+ output.info(displayValue);
19463
+ }
19464
+ } catch (error3) {
19465
+ handleError(error3, isJson);
19466
+ }
19467
+ });
19468
+ const set2 = new Command5("set").description("Set configuration value").action(() => {
19469
+ set2.help();
19470
+ });
19471
+ 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", `
19472
+ Examples:
19473
+ ${chalk7.dim("$")} mutagent config set workspace staging
19474
+ ${chalk7.dim("$")} mutagent config set workspace <workspace-id>
19475
+
19476
+ ${chalk7.dim("Runs mutagent workspaces use: resolves the name among your workspaces and saves it.")}
19477
+ `).action(async (value2) => {
19478
+ const isJson = getJsonFlag(config);
19479
+ const output = new OutputFormatter(isJson ? "json" : "table");
19480
+ try {
19481
+ const result = await useWorkspace(value2);
19482
+ if (isJson) {
19483
+ output.output({ success: true, defaultWorkspace: result.workspace.id, ...result });
19484
+ } else {
19485
+ output.success(`Workspace: ${result.workspace.name} (${result.workspace.id})`);
19486
+ }
19487
+ } catch (error3) {
19488
+ handleError(error3, isJson);
19489
+ }
19490
+ });
19491
+ 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", `
19492
+ Examples:
19493
+ ${chalk7.dim("$")} mutagent config set org <org-id|slug>
19494
+
19495
+ ${chalk7.dim("The CLI key belongs to one organization. The same organization: nothing to do.")}
19496
+ ${chalk7.dim("Another organization: sign in for it with mutagent login --org <id|slug>.")}
19497
+ `).action(async (value2) => {
19498
+ const isJson = getJsonFlag(config);
19499
+ const output = new OutputFormatter(isJson ? "json" : "table");
19500
+ try {
19501
+ const apiKey = getApiKey();
19502
+ if (!apiKey)
19503
+ throw new AuthenticationError;
19504
+ setOrgAssertion(value2);
19505
+ const { organization } = await resolveKeyTenancy(apiKey, { resolveOverride: false });
19506
+ if (!organization) {
19507
+ throw new MutagentError("ORG_UNKNOWN", "This key's organization is unknown.", "Run: mutagent workspaces current");
19508
+ }
19509
+ if (!matchesOrganization(value2, organization)) {
19510
+ throw new TenancyError("ORG_MISMATCH", `Your key is for ${organization.name}.`, `Run: mutagent login --org ${value2}`);
19511
+ }
19512
+ if (isJson) {
19513
+ output.output({ success: true, organization });
19514
+ } else {
19515
+ output.success(`Organization: ${organization.name} (${organization.id}), from the key`);
19516
+ }
19517
+ } catch (error3) {
19518
+ handleError(error3, isJson);
19519
+ }
19520
+ });
19521
+ config.addCommand(set2);
19522
+ return config;
19523
+ }
19524
+
19348
19525
  // src/commands/providers/index.ts
19349
19526
  init_sdk_client();
19350
19527
  import { Command as Command6 } from "commander";
@@ -19837,7 +20014,7 @@ anthropic for an endpoint that speaks the Anthropic API; unset, the server defau
19837
20014
  The base URL must be a public address: hosts ending in .internal, .local, .localhost or
19838
20015
  .home.arpa are refused.
19839
20016
 
19840
- Adds the LLM provider to the configured workspace (mutagent config set workspace <id>).
20017
+ Adds the LLM provider to the configured workspace (mutagent workspaces use <name>, or --workspace for one command).
19841
20018
  The key is encrypted and never returned.
19842
20019
  Workspace default model: when the workspace has none, adding a keyed LLM provider sets
19843
20020
  one on the server. An existing workspace default model is never changed.
@@ -19875,7 +20052,7 @@ AI Agent Directive:
19875
20052
  validateApiFormat(providerType, options.apiFormat);
19876
20053
  }
19877
20054
  const client = await getSDKClient();
19878
- const workspaceId = client.requireUnambiguousWorkspace();
20055
+ const workspaceId = client.requireSelectedWorkspace();
19879
20056
  const credentialFields = collectCredentialFields(options);
19880
20057
  if (secretWireName !== "apiKey")
19881
20058
  credentialFields[secretWireName] = secret;
@@ -20787,7 +20964,7 @@ function statusGlyph(result) {
20787
20964
  }
20788
20965
  }
20789
20966
  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", `
20967
+ 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
20968
  Examples:
20792
20969
  ${chalk11.dim("$")} mutagent providers mirror
20793
20970
  ${chalk11.dim("$")} mutagent providers mirror --json ${chalk11.dim("Show the plan without sending anything")}
@@ -20816,6 +20993,9 @@ default model is never changed. Change it with
20816
20993
 
20817
20994
  Mirroring does not test the keys: check \`mutagent helix models\`, then run a model.
20818
20995
 
20996
+ --workspace <name|id> still works here: it is the global flag and targets this run only
20997
+ (mutagent workspaces use <name> changes the default).
20998
+
20819
20999
  Requires \`mutagent login\` and a workspace. Keys are sent only to the configured
20820
21000
  endpoint and are never printed. Consent is asked once per run; without a terminal,
20821
21001
  --yes is required. Without it the run exits 2 (with --json it returns the plan).
@@ -20835,12 +21015,9 @@ AI Agent Directive:
20835
21015
  try {
20836
21016
  const apiKey = getApiKey();
20837
21017
  const config = loadConfig();
20838
- const workspaceId = options.workspace ?? config.defaultWorkspace;
21018
+ const workspaceId = config.defaultWorkspace;
20839
21019
  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;
21020
+ 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
21021
  }
20845
21022
  if (!HELIX_CHANNELS2.includes(options.channel)) {
20846
21023
  throw new MutagentError("INVALID_OPTION", `Unknown channel '${options.channel}'.`, `Use --channel ${HELIX_CHANNELS2.join(" or ")}`);
@@ -20851,7 +21028,7 @@ AI Agent Directive:
20851
21028
  for (const secret of localCredentialSecrets(store.entries))
20852
21029
  registerSecret(secret);
20853
21030
  const client = await getSDKClient();
20854
- const target = client.requireUnambiguousWorkspace();
21031
+ const target = client.requireSelectedWorkspace();
20855
21032
  const rows = (await client.listProviders({ limit: 1000 })).data.map((row) => ({
20856
21033
  id: row.id,
20857
21034
  name: row.name,
@@ -21782,22 +21959,28 @@ exits — run \`mutagent login\` first, then re-run \`mutagent init\`.
21782
21959
 
21783
21960
  \`\`\`bash
21784
21961
  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
21962
+ mutagent config get <key> --json # keys: apiKey, endpoint, format, timeout, defaultWorkspace,
21963
+ # workspaceName, defaultOrganization, organizationName, keyScope
21964
+ mutagent config set workspace <name|id> --json # same as: mutagent workspaces use
21965
+ mutagent config set org <id|slug> --json # confirms the key's organization; another org exits 1
21789
21966
  \`\`\`
21790
21967
 
21791
21968
  ---
21792
21969
 
21793
- ## 4. workspaces (read-only)
21970
+ ## 4. workspaces
21794
21971
 
21795
21972
  \`\`\`bash
21796
- mutagent workspaces list --json # list workspaces
21973
+ mutagent workspaces list --json # your workspaces in the key's organization (selected: true)
21974
+ mutagent workspaces use <name|id> --json # select the workspace later commands work in (no browser, no prompt)
21975
+ mutagent workspaces current --json # key scope, organization, verified workspace, key expiry
21797
21976
  mutagent workspaces get <id> --json # workspace details
21798
21977
  \`\`\`
21799
21978
 
21800
- To make a workspace the default, use \`mutagent config set workspace <id>\`.
21979
+ The login key works in every workspace the user is a member of in its organization.
21980
+ For one command in another workspace, pass \`--workspace <name|id>\` (it beats
21981
+ \`MUTAGENT_WORKSPACE_ID\`, which beats the selection). For another organization:
21982
+ \`mutagent login --org <slug>\`. A tenancy error is one line ending in the fix command:
21983
+ run that command, never retry the same request.
21801
21984
 
21802
21985
  ---
21803
21986
 
@@ -22008,51 +22191,33 @@ It never prompts. Without a sign-in it prints what to run and exits.
22008
22191
  } else {
22009
22192
  const config = (deps.loadConfig ?? loadConfig)();
22010
22193
  const endpoint = config.endpoint ?? "https://api.mutagent.io";
22011
- let workspace = config.defaultWorkspace;
22194
+ const workspace = config.defaultWorkspace;
22012
22195
  let workspaceValidation;
22013
- let resolvedOrgId;
22014
22196
  try {
22015
22197
  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
- }
22198
+ if (apiKey && workspace) {
22199
+ const list = deps.listWorkspaces ?? (async (key) => {
22200
+ const { listKeyWorkspaces: listKeyWorkspaces2, resolveKeyTenancy: resolveKeyTenancy2 } = await Promise.resolve().then(() => (init_organization_scope(), exports_organization_scope));
22201
+ const tenancy = await resolveKeyTenancy2(key, { resolveOverride: false });
22202
+ return listKeyWorkspaces2(key, tenancy.endpoint, tenancy);
22203
+ });
22204
+ const found = (await list(apiKey)).find((w) => w.id === workspace);
22205
+ workspaceValidation = { id: workspace, name: found?.name, found: found !== undefined };
22040
22206
  }
22041
22207
  } catch {}
22042
22208
  rcConfig = {
22043
22209
  endpoint,
22044
- ...workspace ? { defaultWorkspace: workspace } : {},
22045
- ...resolvedOrgId ? { defaultOrganization: resolvedOrgId } : {}
22210
+ ...workspace ? { defaultWorkspace: workspace } : {}
22046
22211
  };
22047
22212
  writeRcConfig(rcConfig, cwd);
22048
22213
  if (!isJson) {
22049
22214
  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
- }
22215
+ if (workspaceValidation?.found) {
22216
+ output.info(`Workspace: ${workspaceValidation.name ?? workspaceValidation.id} (${workspaceValidation.id})`);
22217
+ } else if (workspaceValidation) {
22218
+ output.warn(`Your saved workspace ${workspaceValidation.id} is not among your workspaces. Run: mutagent workspaces use <name>`);
22219
+ } else if (!workspace) {
22220
+ output.warn("No workspace selected. Run: mutagent workspaces use <name>");
22056
22221
  }
22057
22222
  }
22058
22223
  }
@@ -22236,22 +22401,8 @@ function parseTranscript(raw) {
22236
22401
  // src/commands/hooks/api.ts
22237
22402
  init_config();
22238
22403
  init_known_secrets();
22404
+ init_organization_scope();
22239
22405
  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
22406
  async function sendBatchTrace(traces) {
22256
22407
  const apiKey = getApiKey();
22257
22408
  if (!apiKey) {
@@ -22265,12 +22416,7 @@ async function sendBatchTrace(traces) {
22265
22416
  "x-api-key": apiKey,
22266
22417
  "Content-Type": "application/json"
22267
22418
  };
22268
- const tenancy = resolveTenancyHeaders(config);
22269
- if (!tenancy.ok) {
22270
- process.stderr.write(tenancy.warning);
22271
- return;
22272
- }
22273
- Object.assign(headers, tenancy.headers);
22419
+ Object.assign(headers, configTenancyHeaders(config));
22274
22420
  const controller = new AbortController;
22275
22421
  const timeout = setTimeout(() => {
22276
22422
  controller.abort();
@@ -23282,6 +23428,7 @@ import { join as join18, dirname as dirname5 } from "path";
23282
23428
  import { fileURLToPath as fileURLToPath3 } from "url";
23283
23429
  init_errors();
23284
23430
  init_config();
23431
+ init_organization_scope();
23285
23432
 
23286
23433
  // src/lib/transcript.ts
23287
23434
  init_errors();
@@ -23691,15 +23838,12 @@ function buildAutoContext(harness) {
23691
23838
  nodeVersion: process.version
23692
23839
  };
23693
23840
  }
23694
- async function postToServer(payload, endpoint, apiKey, workspaceId, organizationId) {
23841
+ async function postToServer(payload, endpoint, apiKey, tenancy) {
23695
23842
  const headers = {
23696
23843
  "Content-Type": "application/json",
23697
- "x-api-key": apiKey
23844
+ "x-api-key": apiKey,
23845
+ ...tenancy
23698
23846
  };
23699
- if (workspaceId)
23700
- headers["x-workspace-id"] = workspaceId;
23701
- if (organizationId)
23702
- headers["x-organization-id"] = organizationId;
23703
23847
  let response;
23704
23848
  try {
23705
23849
  response = await fetch(`${endpoint}/api/feedback`, {
@@ -23823,7 +23967,7 @@ Or set an API key: mutagent config set apiKey <key>`);
23823
23967
  const payload = buildPayload(feedbackBody, category, options.title, options.session, transcript);
23824
23968
  const config = loadConfig();
23825
23969
  const endpoint = config.endpoint ?? "https://api.mutagent.io";
23826
- const result = await postToServer(payload, endpoint, apiKey, config.defaultWorkspace, config.defaultOrganization);
23970
+ const result = await postToServer(payload, endpoint, apiKey, configTenancyHeaders(config));
23827
23971
  if (isJson) {
23828
23972
  output.output({
23829
23973
  success: true,
@@ -24657,6 +24801,7 @@ init_errors();
24657
24801
 
24658
24802
  // src/lib/sandbox-api.ts
24659
24803
  init_config();
24804
+ init_organization_scope();
24660
24805
  init_errors();
24661
24806
 
24662
24807
  // src/lib/sandbox-token.ts
@@ -24748,7 +24893,7 @@ async function exchangeToken(input2, deps = {}) {
24748
24893
  }
24749
24894
  if (response.status === 403) {
24750
24895
  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);
24896
+ 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
24897
  }
24753
24898
  if (!response.ok) {
24754
24899
  const detail = scrubKnown(messageFrom(await response.text().catch(() => "")));
@@ -24821,22 +24966,25 @@ async function withStoppingRetry(send, sleep2 = (ms) => new Promise((resolve9) =
24821
24966
 
24822
24967
  // src/lib/sandbox-api.ts
24823
24968
  init_known_secrets();
24824
- function resolveConnection() {
24969
+ async function resolveConnection() {
24825
24970
  const apiKey = getApiKey();
24826
24971
  if (!apiKey)
24827
24972
  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 };
24973
+ const tenancy = await resolveKeyTenancy(apiKey, { probe: envKeyReplacesFile() });
24974
+ if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
24975
+ throw noWorkspaceSelected();
24976
+ const workspaceId = requireWorkspace(tenancy.workspaceId);
24977
+ const headers = tenancyHeaders({
24978
+ keyScope: tenancy.keyScope,
24979
+ workspaceId,
24980
+ workspaceOrigin: tenancy.workspaceOrigin,
24981
+ organizationId: tenancy.organization?.id
24982
+ });
24983
+ return { endpoint: tenancy.endpoint, apiKey, workspaceId, headers };
24836
24984
  }
24837
24985
  function requireWorkspace(workspaceId) {
24838
24986
  if (workspaceId === undefined || workspaceId.trim() === "") {
24839
- throw new WorkspaceContextError("A workspace is required: sandbox access is minted per workspace, not per account.");
24987
+ throw new WorkspaceContextError("No workspace selected: sandbox access is minted per workspace, not per account.");
24840
24988
  }
24841
24989
  return workspaceId.trim();
24842
24990
  }
@@ -24852,7 +25000,7 @@ async function fetchWithTokenRetry(url, init, baseHeaders, transport) {
24852
25000
  return attempt(true);
24853
25001
  }
24854
25002
  async function sandboxFetch(path, init, deps = {}) {
24855
- const connection = resolveConnection();
25003
+ const connection = await resolveConnection();
24856
25004
  const { endpoint } = connection;
24857
25005
  try {
24858
25006
  const response = await fetchWithTokenRetry(`${endpoint}${path}`, init, { ...connection.headers, ...toHeaderRecord(init.headers) }, {
@@ -28091,7 +28239,7 @@ Examples:
28091
28239
  ${chalk34.dim("$")} mutagent sandbox ls --json
28092
28240
 
28093
28241
  Lists the sandboxes in the configured workspace
28094
- (mutagent config set workspace <workspace-id>).
28242
+ (mutagent workspaces use <name>).
28095
28243
  An empty list means none are running.
28096
28244
 
28097
28245
  AI Agent Directive:
@@ -28323,7 +28471,7 @@ AI Agent Directive:
28323
28471
  spawned sandbox runs until it is removed or stopped by the idle policy: confirm
28324
28472
  with the user before 'spawn' and before 'rm', and tell them the sandbox ID.
28325
28473
 
28326
- Requires a workspace: mutagent config set workspace <workspace-id>
28474
+ Requires a workspace: mutagent workspaces use <name> (or --workspace for one command)
28327
28475
  `);
28328
28476
  registerSpawnCommand(sandbox);
28329
28477
  registerLsCommand(sandbox);
@@ -28459,7 +28607,7 @@ Values are write-only. \`show\` prints each entry's name and a sha256[:8] finger
28459
28607
  Names: Environments use letters, digits, '.', '_' and '-' (up to 64); variables use
28460
28608
  A-Z, 0-9 and _, not starting with a digit. An Environment holds up to 64 KiB.
28461
28609
 
28462
- Requires a workspace: mutagent config set workspace <workspace-id>
28610
+ Requires a workspace: mutagent workspaces use <name> (or --workspace for one command)
28463
28611
 
28464
28612
  AI Agent Directive:
28465
28613
  Use 'set' to create or update — it creates the Environment when absent and
@@ -28694,6 +28842,7 @@ function printEntries(environment) {
28694
28842
 
28695
28843
  // src/bin/cli.ts
28696
28844
  init_config();
28845
+ init_organization_scope();
28697
28846
 
28698
28847
  // src/lib/brand.ts
28699
28848
  var BRAND = {
@@ -28762,7 +28911,7 @@ ${lines.join(`
28762
28911
  }
28763
28912
 
28764
28913
  // src/lib/global-version-flag.ts
28765
- var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint"]);
28914
+ var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint", "--workspace", "--org"]);
28766
28915
  function findSubcommandIndex(rawArgs) {
28767
28916
  for (let i = 0;i < rawArgs.length; i++) {
28768
28917
  const arg = rawArgs[i];
@@ -28862,7 +29011,7 @@ To pass the Mutagent platform key instead, put it before the command (mutagent -
28862
29011
  function rootValueOptionRefusal(option, label) {
28863
29012
  const command = label.replace(/^mutagent /, "");
28864
29013
  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" : "";
29014
+ const env = option === "--endpoint" ? " or export MUTAGENT_ENDPOINT" : option === "--workspace" ? " or export MUTAGENT_WORKSPACE_ID" : "";
28866
29015
  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
29016
  For the forwarded command, put it after --: ${label} … -- … ${option} <value>.`, 2);
28868
29017
  }
@@ -29254,7 +29403,7 @@ var program = new Command18;
29254
29403
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
29255
29404
 
29256
29405
  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({
29406
+ 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; 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
29407
  sortSubcommands: true,
29259
29408
  showGlobalOptions: true
29260
29409
  });
@@ -29272,9 +29421,11 @@ ${chalk37.yellow("Managed agents:")}
29272
29421
  mutagent agent --help ${chalk37.dim("check, pack, run, deploy, activate, list, inspect, retire")}
29273
29422
 
29274
29423
  ${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")}
29424
+ mutagent workspaces use <name> ${chalk37.dim("Choose the workspace every later command works in")}
29425
+ mutagent workspaces current ${chalk37.dim("The key's scope, organization and the verified workspace")}
29426
+ mutagent workspaces list ${chalk37.dim("Your workspaces in the key's organization")}
29427
+ --workspace <name|id> ${chalk37.dim("One command in another workspace (then MUTAGENT_WORKSPACE_ID, then the selection)")}
29428
+ mutagent login --org <slug> ${chalk37.dim("Sign in for another organization")}
29278
29429
  mutagent providers list ${chalk37.dim("LLM providers in the workspace")}
29279
29430
  mutagent helix models ${chalk37.dim("Models Helix can use in the cloud, and the workspace default model")}
29280
29431
  mutagent env ls ${chalk37.dim("Environments: variables and secrets for tools")}
@@ -29337,7 +29488,7 @@ var rootOptionError = forwardedRootOptionRefusal(rawArgs);
29337
29488
  if (rootOptionError !== undefined) {
29338
29489
  handleError(rootOptionError, rawArgs.includes("--json"));
29339
29490
  }
29340
- program.hook("preAction", (thisCommand) => {
29491
+ program.hook("preAction", (thisCommand, actionCommand) => {
29341
29492
  const globalOpts = thisCommand.optsWithGlobals();
29342
29493
  if (globalOpts.apiKey && !process.env.MUTAGENT_API_KEY) {
29343
29494
  process.env.MUTAGENT_API_KEY = globalOpts.apiKey;
@@ -29345,6 +29496,13 @@ program.hook("preAction", (thisCommand) => {
29345
29496
  if (globalOpts.endpoint && !process.env.MUTAGENT_ENDPOINT) {
29346
29497
  process.env.MUTAGENT_ENDPOINT = globalOpts.endpoint;
29347
29498
  }
29499
+ if (globalOpts.workspace) {
29500
+ process.env.MUTAGENT_WORKSPACE_ID = globalOpts.workspace;
29501
+ setWorkspaceFlag(globalOpts.workspace);
29502
+ }
29503
+ if (actionCommand.name() !== "login") {
29504
+ setOrgAssertion(globalOpts.org);
29505
+ }
29348
29506
  if (globalOpts.nonInteractive || process.env.CI === "true" || !process.stdin.isTTY) {
29349
29507
  process.env.MUTAGENT_NON_INTERACTIVE = "true";
29350
29508
  }
@@ -29388,4 +29546,4 @@ if (isAgentInvocation(rawArgs)) {
29388
29546
  program.parse(argvForCommander, { from: "user" });
29389
29547
  }
29390
29548
 
29391
- //# debugId=DD369C05E2A6AE2D64756E2164756E21
29549
+ //# debugId=005A5989779CDE4E64756E2164756E21