@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.
- package/dist/bin/cli.js +908 -750
- package/dist/index.js +479 -402
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -255,6 +255,164 @@ var init_known_secrets = __esm(() => {
|
|
|
255
255
|
known = new Set;
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
// src/lib/config.ts
|
|
259
|
+
import { cosmiconfigSync } from "cosmiconfig";
|
|
260
|
+
import { z } from "zod";
|
|
261
|
+
import { homedir } from "os";
|
|
262
|
+
import { join } from "path";
|
|
263
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
264
|
+
function parseJsonSafe(content, schema) {
|
|
265
|
+
try {
|
|
266
|
+
const parsed = JSON.parse(content);
|
|
267
|
+
return schema.parse(parsed);
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function loadConfig() {
|
|
273
|
+
const explorer = cosmiconfigSync("mutagent");
|
|
274
|
+
const result = explorer.search();
|
|
275
|
+
const rcConfig = result?.config;
|
|
276
|
+
let credentials = null;
|
|
277
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
278
|
+
credentials = parseJsonSafe(readFileSync(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
|
|
279
|
+
}
|
|
280
|
+
const envKey = process.env.MUTAGENT_API_KEY;
|
|
281
|
+
const fileDescribesKey = envKey === undefined || envKey === credentials?.apiKey;
|
|
282
|
+
const envWorkspace = process.env.MUTAGENT_WORKSPACE_ID?.trim() ?? "";
|
|
283
|
+
const fileSelection = fileDescribesKey ? credentials?.defaultWorkspace : undefined;
|
|
284
|
+
const selected = fileSelection ?? rcConfig?.defaultWorkspace;
|
|
285
|
+
const defaultWorkspace = envWorkspace !== "" ? envWorkspace : selected;
|
|
286
|
+
const merged = {
|
|
287
|
+
providers: {},
|
|
288
|
+
environment: "production",
|
|
289
|
+
...rcConfig,
|
|
290
|
+
...credentials,
|
|
291
|
+
apiKey: envKey ?? credentials?.apiKey ?? rcConfig?.apiKey,
|
|
292
|
+
endpoint: process.env.MUTAGENT_ENDPOINT ?? credentials?.endpoint ?? rcConfig?.endpoint,
|
|
293
|
+
defaultWorkspace,
|
|
294
|
+
workspaceOrigin: envWorkspace !== "" ? "override" : selected ? "selection" : undefined,
|
|
295
|
+
workspaceName: envWorkspace === "" && fileSelection !== undefined && fileSelection === selected ? credentials?.workspaceName : undefined,
|
|
296
|
+
defaultOrganization: fileDescribesKey ? credentials?.defaultOrganization ?? rcConfig?.defaultOrganization : undefined,
|
|
297
|
+
organizationName: fileDescribesKey ? credentials?.organizationName : undefined,
|
|
298
|
+
organizationSlug: fileDescribesKey ? credentials?.organizationSlug : undefined,
|
|
299
|
+
keyScope: fileDescribesKey ? credentials?.keyScope : undefined
|
|
300
|
+
};
|
|
301
|
+
return configSchema.parse(merged);
|
|
302
|
+
}
|
|
303
|
+
function readCredentialsFile() {
|
|
304
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
305
|
+
return {};
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
308
|
+
} catch {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function updateCredentials(patch) {
|
|
313
|
+
if (!existsSync(CREDENTIALS_DIR)) {
|
|
314
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
const updated = Object.fromEntries(Object.entries({ ...readCredentialsFile(), ...patch }).filter(([key, value]) => value !== undefined && key !== "defaultWorkspaceSource"));
|
|
317
|
+
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
|
|
318
|
+
}
|
|
319
|
+
function saveCredentials(apiKey, endpoint) {
|
|
320
|
+
if (!existsSync(CREDENTIALS_DIR)) {
|
|
321
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
322
|
+
}
|
|
323
|
+
const config = loadConfig();
|
|
324
|
+
const credentials = {
|
|
325
|
+
apiKey,
|
|
326
|
+
endpoint: endpoint ?? config.endpoint
|
|
327
|
+
};
|
|
328
|
+
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
|
|
329
|
+
}
|
|
330
|
+
function saveFullCredentials(creds) {
|
|
331
|
+
const existing = readCredentialsFile();
|
|
332
|
+
updateCredentials({
|
|
333
|
+
apiKey: creds.apiKey,
|
|
334
|
+
endpoint: creds.endpoint ?? existing.endpoint ?? "https://api.mutagent.io",
|
|
335
|
+
defaultWorkspace: creds.workspaceId,
|
|
336
|
+
workspaceName: creds.workspaceName,
|
|
337
|
+
defaultOrganization: creds.organizationId,
|
|
338
|
+
organizationName: creds.organizationName,
|
|
339
|
+
organizationSlug: creds.organizationSlug,
|
|
340
|
+
keyScope: creds.keyScope,
|
|
341
|
+
expiresAt: creds.expiresAt
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function clearCredentials() {
|
|
345
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
346
|
+
writeSecureFile(CREDENTIALS_FILE, JSON.stringify({}));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function hasCredentials() {
|
|
350
|
+
if (process.env.MUTAGENT_API_KEY)
|
|
351
|
+
return true;
|
|
352
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
353
|
+
return false;
|
|
354
|
+
const credentials = parseJsonSafe(readFileSync(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
|
|
355
|
+
return credentials?.apiKey !== undefined && credentials.apiKey !== "";
|
|
356
|
+
}
|
|
357
|
+
function envKeyReplacesFile() {
|
|
358
|
+
const envKey = process.env.MUTAGENT_API_KEY;
|
|
359
|
+
if (envKey === undefined)
|
|
360
|
+
return false;
|
|
361
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
362
|
+
return true;
|
|
363
|
+
const credentials = parseJsonSafe(readFileSync(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
|
|
364
|
+
return envKey !== credentials?.apiKey;
|
|
365
|
+
}
|
|
366
|
+
function getApiKey() {
|
|
367
|
+
const apiKey = process.env.MUTAGENT_API_KEY ?? loadConfig().apiKey;
|
|
368
|
+
registerSecret(apiKey);
|
|
369
|
+
return apiKey;
|
|
370
|
+
}
|
|
371
|
+
function setWorkspaceSelection(workspaceId, workspaceName) {
|
|
372
|
+
updateCredentials({ defaultWorkspace: workspaceId, workspaceName });
|
|
373
|
+
}
|
|
374
|
+
function recordKeyScope(self) {
|
|
375
|
+
updateCredentials({
|
|
376
|
+
keyScope: self.scope,
|
|
377
|
+
defaultOrganization: self.organization?.id,
|
|
378
|
+
organizationName: self.organization?.name,
|
|
379
|
+
organizationSlug: self.organization?.slug,
|
|
380
|
+
...self.scope === "workspace" && self.workspace ? { defaultWorkspace: self.workspace.id, workspaceName: self.workspace.name } : {}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
var keyScopeSchema, configSchema, credentialsSchema, CREDENTIALS_DIR, CREDENTIALS_FILE;
|
|
384
|
+
var init_config = __esm(() => {
|
|
385
|
+
init_secure_file();
|
|
386
|
+
init_known_secrets();
|
|
387
|
+
keyScopeSchema = z.enum(["organization", "workspace", "user"]);
|
|
388
|
+
configSchema = z.object({
|
|
389
|
+
apiKey: z.string().optional(),
|
|
390
|
+
endpoint: z.string().default("https://api.mutagent.io"),
|
|
391
|
+
format: z.enum(["table", "json"]).default("table"),
|
|
392
|
+
timeout: z.number().default(30000),
|
|
393
|
+
defaultWorkspace: z.string().optional(),
|
|
394
|
+
workspaceOrigin: z.enum(["override", "selection"]).optional(),
|
|
395
|
+
workspaceName: z.string().optional(),
|
|
396
|
+
defaultOrganization: z.string().optional(),
|
|
397
|
+
organizationName: z.string().optional(),
|
|
398
|
+
organizationSlug: z.string().optional(),
|
|
399
|
+
keyScope: keyScopeSchema.optional()
|
|
400
|
+
});
|
|
401
|
+
credentialsSchema = z.object({
|
|
402
|
+
apiKey: z.string().optional(),
|
|
403
|
+
endpoint: z.string().optional(),
|
|
404
|
+
defaultWorkspace: z.string().optional(),
|
|
405
|
+
workspaceName: z.string().optional(),
|
|
406
|
+
defaultOrganization: z.string().optional(),
|
|
407
|
+
organizationName: z.string().optional(),
|
|
408
|
+
organizationSlug: z.string().optional(),
|
|
409
|
+
keyScope: keyScopeSchema.optional(),
|
|
410
|
+
expiresAt: z.string().optional()
|
|
411
|
+
}).loose();
|
|
412
|
+
CREDENTIALS_DIR = join(homedir(), ".config", "mutagent");
|
|
413
|
+
CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
|
|
414
|
+
});
|
|
415
|
+
|
|
258
416
|
// src/lib/errors.ts
|
|
259
417
|
function createErrorFromUnknown(error) {
|
|
260
418
|
if (error instanceof MutagentError) {
|
|
@@ -278,7 +436,7 @@ function handleError(error, isJson) {
|
|
|
278
436
|
}
|
|
279
437
|
if (error instanceof WorkspaceContextError) {
|
|
280
438
|
jsonOutput.remediation = {
|
|
281
|
-
setWorkspace: "mutagent
|
|
439
|
+
setWorkspace: "mutagent workspaces use <name>",
|
|
282
440
|
listWorkspaces: "mutagent workspaces list"
|
|
283
441
|
};
|
|
284
442
|
}
|
|
@@ -300,7 +458,7 @@ function handleError(error, isJson) {
|
|
|
300
458
|
console.log(JSON.stringify(scrubKnownDeep(jsonOutput), null, 2));
|
|
301
459
|
} else {
|
|
302
460
|
console.error(`Error: ${scrubKnown(error.message)}`);
|
|
303
|
-
if (error instanceof AuthenticationError) {
|
|
461
|
+
if (error.name === "TenancyError") {} else if (error instanceof AuthenticationError) {
|
|
304
462
|
console.error("");
|
|
305
463
|
console.error(AUTH_REMEDIATION_MESSAGE);
|
|
306
464
|
} else if (error instanceof WorkspaceContextError) {
|
|
@@ -403,14 +561,14 @@ var init_errors = __esm(() => {
|
|
|
403
561
|
}
|
|
404
562
|
};
|
|
405
563
|
WORKSPACE_REMEDIATION_MESSAGE = [
|
|
406
|
-
"
|
|
407
|
-
" mutagent
|
|
408
|
-
" mutagent workspaces list
|
|
564
|
+
"No workspace selected. To fix:",
|
|
565
|
+
" mutagent workspaces use <name> # Select the workspace every later command works in",
|
|
566
|
+
" mutagent workspaces list # Your workspaces"
|
|
409
567
|
].join(`
|
|
410
568
|
`);
|
|
411
569
|
WorkspaceContextError = class WorkspaceContextError extends MutagentError {
|
|
412
570
|
constructor(message) {
|
|
413
|
-
super("WORKSPACE_REQUIRED", message ?? "Workspace context is required but not configured", "Run: mutagent
|
|
571
|
+
super("WORKSPACE_REQUIRED", message ?? "Workspace context is required but not configured", "Run: mutagent workspaces use <name>", 3);
|
|
414
572
|
}
|
|
415
573
|
};
|
|
416
574
|
ValidationError = class ValidationError extends MutagentError {
|
|
@@ -444,6 +602,272 @@ var init_errors = __esm(() => {
|
|
|
444
602
|
};
|
|
445
603
|
});
|
|
446
604
|
|
|
605
|
+
// src/lib/organization-scope.ts
|
|
606
|
+
var exports_organization_scope = {};
|
|
607
|
+
__export(exports_organization_scope, {
|
|
608
|
+
tenancyHeaders: () => tenancyHeaders,
|
|
609
|
+
tenancyFailure: () => tenancyFailure,
|
|
610
|
+
setWorkspaceFlag: () => setWorkspaceFlag,
|
|
611
|
+
setOrgAssertion: () => setOrgAssertion,
|
|
612
|
+
resolveKeyTenancy: () => resolveKeyTenancy,
|
|
613
|
+
otherOrganizationHint: () => otherOrganizationHint,
|
|
614
|
+
noWorkspaceSelected: () => noWorkspaceSelected,
|
|
615
|
+
matchesOrganization: () => matchesOrganization,
|
|
616
|
+
looksLikeWorkspaceId: () => looksLikeWorkspaceId,
|
|
617
|
+
listKeyWorkspaces: () => listKeyWorkspaces,
|
|
618
|
+
findWorkspace: () => findWorkspace,
|
|
619
|
+
fetchOrganizations: () => fetchOrganizations,
|
|
620
|
+
fetchKeySelf: () => fetchKeySelf,
|
|
621
|
+
envKeyHasNoSelection: () => envKeyHasNoSelection,
|
|
622
|
+
configTenancyHeaders: () => configTenancyHeaders,
|
|
623
|
+
TenancyError: () => TenancyError,
|
|
624
|
+
TENANCY_DENIED: () => TENANCY_DENIED
|
|
625
|
+
});
|
|
626
|
+
function looksLikeWorkspaceId(value) {
|
|
627
|
+
return UUID_PATTERN.test(value);
|
|
628
|
+
}
|
|
629
|
+
function tenancyHeaders(t) {
|
|
630
|
+
const headers = {};
|
|
631
|
+
if (!t.workspaceId)
|
|
632
|
+
return headers;
|
|
633
|
+
if (t.keyScope === "workspace") {
|
|
634
|
+
if (t.workspaceOrigin === "override")
|
|
635
|
+
headers["x-workspace-id"] = t.workspaceId;
|
|
636
|
+
return headers;
|
|
637
|
+
}
|
|
638
|
+
headers["x-workspace-id"] = t.workspaceId;
|
|
639
|
+
if (t.keyScope === "user" && t.organizationId)
|
|
640
|
+
headers["x-organization-id"] = t.organizationId;
|
|
641
|
+
return headers;
|
|
642
|
+
}
|
|
643
|
+
function configTenancyHeaders(config) {
|
|
644
|
+
return tenancyHeaders({
|
|
645
|
+
keyScope: config.keyScope,
|
|
646
|
+
workspaceId: config.defaultWorkspace,
|
|
647
|
+
workspaceOrigin: config.workspaceOrigin,
|
|
648
|
+
organizationId: config.defaultOrganization
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function parseBody(body) {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(body);
|
|
654
|
+
if (parsed && typeof parsed === "object")
|
|
655
|
+
return parsed;
|
|
656
|
+
} catch {}
|
|
657
|
+
return {};
|
|
658
|
+
}
|
|
659
|
+
function tenancyFailure(status, body, ctx) {
|
|
660
|
+
const parsed = parseBody(body);
|
|
661
|
+
if (status === 401) {
|
|
662
|
+
return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login", 2);
|
|
663
|
+
}
|
|
664
|
+
if (status === 403 && parsed.code === TENANCY_DENIED) {
|
|
665
|
+
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
666
|
+
if (ctx.workspaceOrigin === "selection") {
|
|
667
|
+
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
668
|
+
}
|
|
669
|
+
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
670
|
+
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
671
|
+
}
|
|
672
|
+
if (status === 403 && ctx.keyScope === "workspace" && ctx.workspaceOrigin === "override") {
|
|
673
|
+
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)");
|
|
674
|
+
}
|
|
675
|
+
if (status === 400 && (parsed.message ?? body).toLowerCase().includes("x-workspace-id")) {
|
|
676
|
+
return noWorkspaceSelected();
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
function noWorkspaceSelected() {
|
|
681
|
+
const fix = envKeyReplacesFile() ? ENV_KEY_WORKSPACE_FIX : "Run: mutagent workspaces use <name>";
|
|
682
|
+
return new TenancyError("WORKSPACE_REQUIRED", "No workspace selected.", fix, 3);
|
|
683
|
+
}
|
|
684
|
+
function envKeyHasNoSelection() {
|
|
685
|
+
return new TenancyError("WORKSPACE_FROM_ENV", "With MUTAGENT_API_KEY set, the workspace comes from --workspace or MUTAGENT_WORKSPACE_ID.", ENV_KEY_WORKSPACE_FIX);
|
|
686
|
+
}
|
|
687
|
+
async function tenancyFetch(url, init) {
|
|
688
|
+
try {
|
|
689
|
+
return await fetch(url, init);
|
|
690
|
+
} catch {
|
|
691
|
+
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");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function fetchKeySelf(apiKey, endpoint, opts = {}) {
|
|
695
|
+
const response = await tenancyFetch(`${endpoint}/api/api-keys/self`, {
|
|
696
|
+
headers: { "x-api-key": apiKey, ...opts.headers }
|
|
697
|
+
});
|
|
698
|
+
if (!response.ok) {
|
|
699
|
+
const body = await response.text().catch(() => "");
|
|
700
|
+
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");
|
|
701
|
+
}
|
|
702
|
+
return await response.json();
|
|
703
|
+
}
|
|
704
|
+
async function fetchOrganizations(apiKey, endpoint) {
|
|
705
|
+
try {
|
|
706
|
+
const response = await fetch(`${endpoint}/api/organizations`, {
|
|
707
|
+
headers: { "x-api-key": apiKey }
|
|
708
|
+
});
|
|
709
|
+
if (!response.ok)
|
|
710
|
+
return [];
|
|
711
|
+
const data = await response.json();
|
|
712
|
+
return data.data ?? [];
|
|
713
|
+
} catch {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function matchesOrganization(value, org) {
|
|
718
|
+
if (!org)
|
|
719
|
+
return false;
|
|
720
|
+
return value === org.id || org.slug !== undefined && value === org.slug;
|
|
721
|
+
}
|
|
722
|
+
function otherOrganizationHint(wanted, keyOrg, orgs) {
|
|
723
|
+
const others = orgs.filter((o) => o.id !== keyOrg?.id);
|
|
724
|
+
const where = keyOrg ? ` in ${keyOrg.name}` : "";
|
|
725
|
+
const what = `No workspace "${wanted}"${where}.`;
|
|
726
|
+
const named = others.map((o) => `${o.name} (${o.slug ?? o.id})`).join(", ");
|
|
727
|
+
if (others.length === 1 && others[0]) {
|
|
728
|
+
const only = others[0];
|
|
729
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organization: ${named}.`, `To switch: mutagent login --org ${only.slug ?? only.id}`);
|
|
730
|
+
}
|
|
731
|
+
if (others.length > 1) {
|
|
732
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organizations: ${named}.`, "To switch: mutagent login --org <slug>");
|
|
733
|
+
}
|
|
734
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", what, "See: mutagent workspaces list");
|
|
735
|
+
}
|
|
736
|
+
function setOrgAssertion(value) {
|
|
737
|
+
const trimmed = value?.trim();
|
|
738
|
+
orgAssertion = trimmed === "" ? undefined : trimmed;
|
|
739
|
+
}
|
|
740
|
+
function setWorkspaceFlag(value) {
|
|
741
|
+
const trimmed = value?.trim();
|
|
742
|
+
workspaceFlag = trimmed === "" ? undefined : trimmed;
|
|
743
|
+
}
|
|
744
|
+
function endpointOf(configured) {
|
|
745
|
+
const trimmed = configured?.trim();
|
|
746
|
+
return (trimmed === undefined || trimmed === "" ? "https://api.mutagent.io" : trimmed).replace(/\/+$/, "");
|
|
747
|
+
}
|
|
748
|
+
async function probeKeyScope(apiKey, endpoint, workspaceNamed) {
|
|
749
|
+
const fromFile = !envKeyReplacesFile();
|
|
750
|
+
if (!fromFile && workspaceNamed && orgAssertion === undefined)
|
|
751
|
+
return;
|
|
752
|
+
try {
|
|
753
|
+
const self = await fetchKeySelf(apiKey, endpoint);
|
|
754
|
+
if (fromFile)
|
|
755
|
+
recordKeyScope(self);
|
|
756
|
+
return self;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (error instanceof MutagentError && (error.code === "AUTH_EXPIRED" || error.code === "REQUEST_FAILED"))
|
|
759
|
+
throw error;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function enforceOrgAssertion(org) {
|
|
764
|
+
if (orgAssertion === undefined)
|
|
765
|
+
return;
|
|
766
|
+
if (org === undefined) {
|
|
767
|
+
throw new TenancyError("ORG_UNKNOWN", "This key's organization is unknown, so --org cannot be checked.", "Run: mutagent workspaces current");
|
|
768
|
+
}
|
|
769
|
+
if (!matchesOrganization(orgAssertion, org)) {
|
|
770
|
+
throw new TenancyError("ORG_MISMATCH", `Your key is for ${org.name}.`, `Run: mutagent login --org ${orgAssertion}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function resolveKeyTenancy(apiKey, opts = {}) {
|
|
774
|
+
const config = loadConfig();
|
|
775
|
+
const endpoint = endpointOf(config.endpoint);
|
|
776
|
+
let keyScope = config.keyScope;
|
|
777
|
+
let organization = config.defaultOrganization ? { id: config.defaultOrganization, name: config.organizationName ?? config.defaultOrganization, slug: config.organizationSlug } : undefined;
|
|
778
|
+
let workspaceId = config.defaultWorkspace;
|
|
779
|
+
let workspaceName = config.workspaceName;
|
|
780
|
+
const workspaceOrigin = config.workspaceOrigin;
|
|
781
|
+
if (keyScope === undefined && (opts.probe !== false || orgAssertion !== undefined)) {
|
|
782
|
+
const self = await probeKeyScope(apiKey, endpoint, workspaceId !== undefined);
|
|
783
|
+
if (self) {
|
|
784
|
+
keyScope = self.scope;
|
|
785
|
+
organization = self.organization ?? undefined;
|
|
786
|
+
if (self.scope === "workspace" && self.workspace && workspaceOrigin !== "override") {
|
|
787
|
+
workspaceId = self.workspace.id;
|
|
788
|
+
workspaceName = self.workspace.name;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
enforceOrgAssertion(organization);
|
|
793
|
+
if (opts.resolveOverride !== false && workspaceFlag !== undefined && workspaceId === workspaceFlag && !looksLikeWorkspaceId(workspaceId)) {
|
|
794
|
+
const found = await findWorkspace(workspaceId, { apiKey, endpoint, keyScope, organization, workspaceId: config.defaultWorkspace, workspaceName });
|
|
795
|
+
workspaceId = found.id;
|
|
796
|
+
workspaceName = found.name;
|
|
797
|
+
process.env.MUTAGENT_WORKSPACE_ID = found.id;
|
|
798
|
+
}
|
|
799
|
+
return { endpoint, keyScope, organization, workspaceId, workspaceName, workspaceOrigin };
|
|
800
|
+
}
|
|
801
|
+
async function listKeyWorkspaces(apiKey, endpoint, t) {
|
|
802
|
+
const headers = { "x-api-key": apiKey };
|
|
803
|
+
if (t.keyScope === "user" && t.organization)
|
|
804
|
+
headers["x-organization-id"] = t.organization.id;
|
|
805
|
+
const response = await tenancyFetch(`${endpoint}/api/workspaces`, { headers });
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
const body = await response.text().catch(() => "");
|
|
808
|
+
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");
|
|
809
|
+
}
|
|
810
|
+
const data = await response.json();
|
|
811
|
+
return data.workspaces ?? [];
|
|
812
|
+
}
|
|
813
|
+
async function findWorkspace(wanted, t) {
|
|
814
|
+
if (t.keyScope === "workspace") {
|
|
815
|
+
if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
|
|
816
|
+
return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
|
|
817
|
+
}
|
|
818
|
+
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)");
|
|
819
|
+
}
|
|
820
|
+
const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
|
|
821
|
+
const byId = rows.find((w) => w.id === wanted);
|
|
822
|
+
if (byId)
|
|
823
|
+
return { id: byId.id, name: byId.name };
|
|
824
|
+
let matches = rows.filter((w) => w.name === wanted || w.slug === wanted);
|
|
825
|
+
if (matches.length === 0)
|
|
826
|
+
matches = rows.filter((w) => w.name.toLowerCase() === wanted.toLowerCase());
|
|
827
|
+
if (matches.length === 1 && matches[0])
|
|
828
|
+
return { id: matches[0].id, name: matches[0].name };
|
|
829
|
+
if (matches.length > 1) {
|
|
830
|
+
throw new TenancyError("WORKSPACE_AMBIGUOUS", `Several workspaces are named "${wanted}": ${matches.map((w) => `${w.name} (${w.id})`).join(", ")}.`, "Run: mutagent workspaces use <id>");
|
|
831
|
+
}
|
|
832
|
+
const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
|
|
833
|
+
throw otherOrganizationHint(wanted, t.organization, orgs);
|
|
834
|
+
}
|
|
835
|
+
var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag;
|
|
836
|
+
var init_organization_scope = __esm(() => {
|
|
837
|
+
init_errors();
|
|
838
|
+
init_config();
|
|
839
|
+
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
840
|
+
TenancyError = class TenancyError extends MutagentError {
|
|
841
|
+
constructor(code, what, fix, exitCode = 1) {
|
|
842
|
+
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""), exitCode);
|
|
843
|
+
this.name = "TenancyError";
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// src/lib/provider-request.ts
|
|
849
|
+
function buildCreateProviderBody(data) {
|
|
850
|
+
const body = {
|
|
851
|
+
name: data.name,
|
|
852
|
+
provider: data.provider,
|
|
853
|
+
isDefault: data.isDefault ?? false
|
|
854
|
+
};
|
|
855
|
+
if (data.apiKey)
|
|
856
|
+
body.apiKey = data.apiKey;
|
|
857
|
+
if (data.baseUrl)
|
|
858
|
+
body.baseUrl = data.baseUrl;
|
|
859
|
+
if (data.hostedFamily)
|
|
860
|
+
body.hostedFamily = data.hostedFamily;
|
|
861
|
+
if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
|
|
862
|
+
body.credentialFields = data.credentialFields;
|
|
863
|
+
}
|
|
864
|
+
if (data.metadata && Object.keys(data.metadata).length > 0)
|
|
865
|
+
body.metadata = data.metadata;
|
|
866
|
+
if (data.supportedModels !== undefined)
|
|
867
|
+
body.supportedModels = data.supportedModels;
|
|
868
|
+
return body;
|
|
869
|
+
}
|
|
870
|
+
|
|
447
871
|
// src/lib/sdk-debug.ts
|
|
448
872
|
function isDebugEnabled(env = process.env) {
|
|
449
873
|
return TRUTHY.has((env.MUTAGENT_DEBUG ?? "").trim().toLowerCase());
|
|
@@ -538,282 +962,6 @@ var init_sdk_debug = __esm(() => {
|
|
|
538
962
|
};
|
|
539
963
|
});
|
|
540
964
|
|
|
541
|
-
// src/lib/organization-scope.ts
|
|
542
|
-
function isWellFormedOrganizationId(id) {
|
|
543
|
-
return ORGANIZATION_ID_PATTERN.test(id);
|
|
544
|
-
}
|
|
545
|
-
function isOrganizationScopeMismatch(status, body) {
|
|
546
|
-
if (status !== 403)
|
|
547
|
-
return false;
|
|
548
|
-
return body.toLowerCase().includes("organization scope mismatch");
|
|
549
|
-
}
|
|
550
|
-
async function lookupOrganizations(apiKey, endpoint) {
|
|
551
|
-
try {
|
|
552
|
-
const response = await fetch(`${endpoint}/api/organizations`, {
|
|
553
|
-
headers: { "x-api-key": apiKey }
|
|
554
|
-
});
|
|
555
|
-
if (!response.ok)
|
|
556
|
-
return [];
|
|
557
|
-
const data = await response.json();
|
|
558
|
-
return data.data ?? [];
|
|
559
|
-
} catch {
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
async function fetchOrganizations(apiKey, endpoint) {
|
|
564
|
-
return lookupOrganizations(apiKey, endpoint);
|
|
565
|
-
}
|
|
566
|
-
async function assertUsableOrganizationId(id, lookup) {
|
|
567
|
-
const orgs = lookup.apiKey ? await lookup.fetchOrganizations(lookup.apiKey, lookup.endpoint).catch(() => []) : [];
|
|
568
|
-
if (orgs.length === 0) {
|
|
569
|
-
if (isWellFormedOrganizationId(id))
|
|
570
|
-
return;
|
|
571
|
-
throw new MutagentError("INVALID_ORGANIZATION_ID", `"${id}" is not an organization id.`, `Organization ids look like org_xxxxxxxxxxxxxxxxxxxxx.
|
|
572
|
-
` + "Log in so the id can be checked against your account: mutagent auth login --browser");
|
|
573
|
-
}
|
|
574
|
-
if (orgs.some((org) => org.id === id))
|
|
575
|
-
return;
|
|
576
|
-
throw new MutagentError("UNKNOWN_ORGANIZATION", `"${id}" is not an organization this API key can access.`, `Available: ${orgs.map((org) => org.id).join(", ")}
|
|
577
|
-
` + "A stored organization that does not match the key makes every request fail with 403.");
|
|
578
|
-
}
|
|
579
|
-
function createOrgScopeRecoveringFetcher(hooks) {
|
|
580
|
-
let recoveryUsed = false;
|
|
581
|
-
async function recover() {
|
|
582
|
-
if (recoveryUsed)
|
|
583
|
-
return;
|
|
584
|
-
recoveryUsed = true;
|
|
585
|
-
const stale = hooks.currentOrganization();
|
|
586
|
-
if (!stale)
|
|
587
|
-
return;
|
|
588
|
-
hooks.adoptOrganization(undefined);
|
|
589
|
-
hooks.forgetStoredOrganization?.();
|
|
590
|
-
const orgs = await lookupOrganizations(hooks.apiKey, hooks.endpoint);
|
|
591
|
-
const resolved = orgs[0]?.id;
|
|
592
|
-
if (!resolved || resolved === stale)
|
|
593
|
-
return;
|
|
594
|
-
hooks.adoptOrganization(resolved);
|
|
595
|
-
if (isDebugEnabled()) {
|
|
596
|
-
process.stderr.write(`[mutagent] Stored organization was rejected by the API; re-resolved and retried once.
|
|
597
|
-
`);
|
|
598
|
-
}
|
|
599
|
-
return resolved;
|
|
600
|
-
}
|
|
601
|
-
return async function fetchWithOrgRecovery(input, init) {
|
|
602
|
-
const retryTemplate = input instanceof Request ? input.clone() : undefined;
|
|
603
|
-
const response = init == null ? await fetch(input) : await fetch(input, init);
|
|
604
|
-
if (response.status !== 403)
|
|
605
|
-
return response;
|
|
606
|
-
const body = await response.clone().text().catch(() => "");
|
|
607
|
-
if (!isOrganizationScopeMismatch(response.status, body))
|
|
608
|
-
return response;
|
|
609
|
-
const resolved = await recover();
|
|
610
|
-
if (!resolved)
|
|
611
|
-
return response;
|
|
612
|
-
if (retryTemplate) {
|
|
613
|
-
retryTemplate.headers.set("x-organization-id", resolved);
|
|
614
|
-
return fetch(retryTemplate);
|
|
615
|
-
}
|
|
616
|
-
const headers = new Headers(init?.headers);
|
|
617
|
-
headers.set("x-organization-id", resolved);
|
|
618
|
-
return fetch(input, { ...init, headers });
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
var ORGANIZATION_ID_PATTERN;
|
|
622
|
-
var init_organization_scope = __esm(() => {
|
|
623
|
-
init_errors();
|
|
624
|
-
init_sdk_debug();
|
|
625
|
-
ORGANIZATION_ID_PATTERN = /^org_[A-Za-z0-9_-]{8,}$/;
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// src/lib/config.ts
|
|
629
|
-
import { cosmiconfigSync } from "cosmiconfig";
|
|
630
|
-
import { z } from "zod";
|
|
631
|
-
import { homedir } from "os";
|
|
632
|
-
import { join } from "path";
|
|
633
|
-
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
634
|
-
function parseJsonSafe(content, schema) {
|
|
635
|
-
try {
|
|
636
|
-
const parsed = JSON.parse(content);
|
|
637
|
-
return schema.parse(parsed);
|
|
638
|
-
} catch {
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
function loadConfig() {
|
|
643
|
-
const explorer = cosmiconfigSync("mutagent");
|
|
644
|
-
const result = explorer.search();
|
|
645
|
-
const rcConfig = result?.config;
|
|
646
|
-
let credentials = null;
|
|
647
|
-
if (existsSync(CREDENTIALS_FILE)) {
|
|
648
|
-
credentials = parseJsonSafe(readFileSync(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
|
|
649
|
-
}
|
|
650
|
-
const merged = {
|
|
651
|
-
providers: {},
|
|
652
|
-
environment: "production",
|
|
653
|
-
...rcConfig,
|
|
654
|
-
...credentials,
|
|
655
|
-
apiKey: process.env.MUTAGENT_API_KEY ?? credentials?.apiKey ?? rcConfig?.apiKey,
|
|
656
|
-
endpoint: process.env.MUTAGENT_ENDPOINT ?? credentials?.endpoint ?? rcConfig?.endpoint,
|
|
657
|
-
defaultWorkspace: process.env.MUTAGENT_WORKSPACE_ID ?? credentials?.defaultWorkspace ?? rcConfig?.defaultWorkspace,
|
|
658
|
-
defaultWorkspaceSource: process.env.MUTAGENT_WORKSPACE_ID ? "user" : credentials?.defaultWorkspace ? credentials.defaultWorkspaceSource ?? "login-inferred" : rcConfig?.defaultWorkspace ? "login-inferred" : undefined,
|
|
659
|
-
defaultOrganization: credentials?.defaultOrganization ?? rcConfig?.defaultOrganization
|
|
660
|
-
};
|
|
661
|
-
return configSchema.parse(merged);
|
|
662
|
-
}
|
|
663
|
-
function saveCredentials(apiKey, endpoint) {
|
|
664
|
-
if (!existsSync(CREDENTIALS_DIR)) {
|
|
665
|
-
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
666
|
-
}
|
|
667
|
-
const config = loadConfig();
|
|
668
|
-
const credentials = {
|
|
669
|
-
apiKey,
|
|
670
|
-
endpoint: endpoint ?? config.endpoint
|
|
671
|
-
};
|
|
672
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
|
|
673
|
-
}
|
|
674
|
-
function saveFullCredentials(creds) {
|
|
675
|
-
if (!existsSync(CREDENTIALS_DIR)) {
|
|
676
|
-
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
677
|
-
}
|
|
678
|
-
let existingCredentials = {};
|
|
679
|
-
if (existsSync(CREDENTIALS_FILE)) {
|
|
680
|
-
try {
|
|
681
|
-
existingCredentials = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
682
|
-
} catch {}
|
|
683
|
-
}
|
|
684
|
-
const existingSource = existingCredentials.defaultWorkspaceSource;
|
|
685
|
-
const workspaceChanged = creds.workspaceId !== undefined && creds.workspaceId !== existingCredentials.defaultWorkspace;
|
|
686
|
-
const source = creds.workspaceId === undefined ? existingSource : workspaceChanged ? "login-inferred" : existingSource ?? "login-inferred";
|
|
687
|
-
const credentials = {
|
|
688
|
-
...existingCredentials,
|
|
689
|
-
apiKey: creds.apiKey,
|
|
690
|
-
endpoint: creds.endpoint ?? existingCredentials.endpoint ?? "https://api.mutagent.io",
|
|
691
|
-
defaultWorkspace: creds.workspaceId ?? existingCredentials.defaultWorkspace,
|
|
692
|
-
defaultWorkspaceSource: source,
|
|
693
|
-
defaultOrganization: creds.organizationId ?? existingCredentials.defaultOrganization,
|
|
694
|
-
expiresAt: creds.expiresAt
|
|
695
|
-
};
|
|
696
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
|
|
697
|
-
}
|
|
698
|
-
function clearCredentials() {
|
|
699
|
-
if (existsSync(CREDENTIALS_FILE)) {
|
|
700
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify({}));
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
function hasCredentials() {
|
|
704
|
-
if (process.env.MUTAGENT_API_KEY)
|
|
705
|
-
return true;
|
|
706
|
-
if (!existsSync(CREDENTIALS_FILE))
|
|
707
|
-
return false;
|
|
708
|
-
const credentials = parseJsonSafe(readFileSync(CREDENTIALS_FILE, "utf-8"), credentialsSchema);
|
|
709
|
-
return credentials?.apiKey !== undefined && credentials.apiKey !== "";
|
|
710
|
-
}
|
|
711
|
-
function getApiKey() {
|
|
712
|
-
const apiKey = process.env.MUTAGENT_API_KEY ?? loadConfig().apiKey;
|
|
713
|
-
registerSecret(apiKey);
|
|
714
|
-
return apiKey;
|
|
715
|
-
}
|
|
716
|
-
function setDefaultWorkspace(workspaceId, source = "user") {
|
|
717
|
-
if (!existsSync(CREDENTIALS_DIR)) {
|
|
718
|
-
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
719
|
-
}
|
|
720
|
-
let existingCredentials = {};
|
|
721
|
-
if (existsSync(CREDENTIALS_FILE)) {
|
|
722
|
-
try {
|
|
723
|
-
existingCredentials = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
724
|
-
} catch {}
|
|
725
|
-
}
|
|
726
|
-
const updated = {
|
|
727
|
-
...existingCredentials,
|
|
728
|
-
defaultWorkspace: workspaceId,
|
|
729
|
-
defaultWorkspaceSource: source
|
|
730
|
-
};
|
|
731
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
|
|
732
|
-
}
|
|
733
|
-
function setDefaultOrganization(organizationId) {
|
|
734
|
-
if (!isWellFormedOrganizationId(organizationId)) {
|
|
735
|
-
throw new MutagentError("INVALID_ORGANIZATION_ID", `"${organizationId}" is not an organization id.`, `Organization ids look like org_xxxxxxxxxxxxxxxxxxxxx.
|
|
736
|
-
` + "List the ones you can use with: mutagent auth status");
|
|
737
|
-
}
|
|
738
|
-
if (!existsSync(CREDENTIALS_DIR)) {
|
|
739
|
-
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
740
|
-
}
|
|
741
|
-
let existingCredentials = {};
|
|
742
|
-
if (existsSync(CREDENTIALS_FILE)) {
|
|
743
|
-
try {
|
|
744
|
-
existingCredentials = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
745
|
-
} catch {}
|
|
746
|
-
}
|
|
747
|
-
const updated = {
|
|
748
|
-
...existingCredentials,
|
|
749
|
-
defaultOrganization: organizationId
|
|
750
|
-
};
|
|
751
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(updated, null, 2));
|
|
752
|
-
}
|
|
753
|
-
function clearDefaultOrganization() {
|
|
754
|
-
if (!existsSync(CREDENTIALS_FILE))
|
|
755
|
-
return;
|
|
756
|
-
let existingCredentials;
|
|
757
|
-
try {
|
|
758
|
-
existingCredentials = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
759
|
-
} catch {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
if (!("defaultOrganization" in existingCredentials))
|
|
763
|
-
return;
|
|
764
|
-
delete existingCredentials.defaultOrganization;
|
|
765
|
-
writeSecureFile(CREDENTIALS_FILE, JSON.stringify(existingCredentials, null, 2));
|
|
766
|
-
}
|
|
767
|
-
var configSchema, credentialsSchema, CREDENTIALS_DIR, CREDENTIALS_FILE;
|
|
768
|
-
var init_config = __esm(() => {
|
|
769
|
-
init_secure_file();
|
|
770
|
-
init_known_secrets();
|
|
771
|
-
init_organization_scope();
|
|
772
|
-
init_errors();
|
|
773
|
-
configSchema = z.object({
|
|
774
|
-
apiKey: z.string().optional(),
|
|
775
|
-
endpoint: z.string().default("https://api.mutagent.io"),
|
|
776
|
-
format: z.enum(["table", "json"]).default("table"),
|
|
777
|
-
timeout: z.number().default(30000),
|
|
778
|
-
defaultWorkspace: z.string().optional(),
|
|
779
|
-
defaultWorkspaceSource: z.enum(["user", "login-inferred"]).optional(),
|
|
780
|
-
defaultOrganization: z.string().optional()
|
|
781
|
-
});
|
|
782
|
-
credentialsSchema = z.object({
|
|
783
|
-
apiKey: z.string().optional(),
|
|
784
|
-
endpoint: z.string().optional(),
|
|
785
|
-
defaultWorkspace: z.string().optional(),
|
|
786
|
-
defaultWorkspaceSource: z.enum(["user", "login-inferred"]).optional(),
|
|
787
|
-
defaultOrganization: z.string().optional(),
|
|
788
|
-
expiresAt: z.string().optional()
|
|
789
|
-
}).loose();
|
|
790
|
-
CREDENTIALS_DIR = join(homedir(), ".config", "mutagent");
|
|
791
|
-
CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
// src/lib/provider-request.ts
|
|
795
|
-
function buildCreateProviderBody(data) {
|
|
796
|
-
const body = {
|
|
797
|
-
name: data.name,
|
|
798
|
-
provider: data.provider,
|
|
799
|
-
isDefault: data.isDefault ?? false
|
|
800
|
-
};
|
|
801
|
-
if (data.apiKey)
|
|
802
|
-
body.apiKey = data.apiKey;
|
|
803
|
-
if (data.baseUrl)
|
|
804
|
-
body.baseUrl = data.baseUrl;
|
|
805
|
-
if (data.hostedFamily)
|
|
806
|
-
body.hostedFamily = data.hostedFamily;
|
|
807
|
-
if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
|
|
808
|
-
body.credentialFields = data.credentialFields;
|
|
809
|
-
}
|
|
810
|
-
if (data.metadata && Object.keys(data.metadata).length > 0)
|
|
811
|
-
body.metadata = data.metadata;
|
|
812
|
-
if (data.supportedModels !== undefined)
|
|
813
|
-
body.supportedModels = data.supportedModels;
|
|
814
|
-
return body;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
965
|
// src/lib/sdk-mappers.ts
|
|
818
966
|
import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
|
|
819
967
|
function isProviderType(value) {
|
|
@@ -902,15 +1050,6 @@ var init_sdk_mappers = __esm(() => {
|
|
|
902
1050
|
});
|
|
903
1051
|
|
|
904
1052
|
// src/lib/sdk-client.ts
|
|
905
|
-
var exports_sdk_client = {};
|
|
906
|
-
__export(exports_sdk_client, {
|
|
907
|
-
validateApiKey: () => validateApiKey,
|
|
908
|
-
resetSDKClient: () => resetSDKClient,
|
|
909
|
-
getSDKClient: () => getSDKClient,
|
|
910
|
-
fetchWorkspaces: () => fetchWorkspaces,
|
|
911
|
-
fetchOrganizations: () => fetchOrganizations,
|
|
912
|
-
MutagentSDK: () => SDKClientWrapper
|
|
913
|
-
});
|
|
914
1053
|
import { Mutagent, HTTPClient } from "@mutagent/sdk";
|
|
915
1054
|
|
|
916
1055
|
class SDKClientWrapper {
|
|
@@ -919,32 +1058,24 @@ class SDKClientWrapper {
|
|
|
919
1058
|
endpoint;
|
|
920
1059
|
workspaceId;
|
|
921
1060
|
organizationId;
|
|
922
|
-
|
|
1061
|
+
tenancy;
|
|
923
1062
|
debugSecrets = new Set;
|
|
924
|
-
recoveringFetch;
|
|
925
1063
|
constructor(opts) {
|
|
926
1064
|
this.apiKey = opts.apiKey;
|
|
927
1065
|
this.endpoint = opts.serverURL ?? "http://localhost:3003";
|
|
928
1066
|
this.workspaceId = opts.workspaceId;
|
|
929
1067
|
this.organizationId = opts.organizationId;
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.organizationId = id;
|
|
939
|
-
},
|
|
940
|
-
forgetStoredOrganization: opts.clearStoredOrganization
|
|
941
|
-
});
|
|
942
|
-
const httpClient = new HTTPClient({ fetcher: this.recoveringFetch });
|
|
1068
|
+
this.tenancy = {
|
|
1069
|
+
keyScope: opts.keyScope,
|
|
1070
|
+
workspaceId: opts.workspaceId,
|
|
1071
|
+
workspaceName: opts.workspaceName,
|
|
1072
|
+
workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
|
|
1073
|
+
organizationName: opts.organizationName
|
|
1074
|
+
};
|
|
1075
|
+
const httpClient = new HTTPClient;
|
|
943
1076
|
httpClient.addHook("beforeRequest", (req) => {
|
|
944
|
-
|
|
945
|
-
req.headers.set(
|
|
946
|
-
if (this.organizationId)
|
|
947
|
-
req.headers.set("x-organization-id", this.organizationId);
|
|
1077
|
+
for (const [name, value] of Object.entries(this.tenancyHeaders()))
|
|
1078
|
+
req.headers.set(name, value);
|
|
948
1079
|
return req;
|
|
949
1080
|
});
|
|
950
1081
|
this.debugSecrets.add(this.apiKey);
|
|
@@ -961,6 +1092,11 @@ class SDKClientWrapper {
|
|
|
961
1092
|
handleError(error) {
|
|
962
1093
|
if (error && typeof error === "object") {
|
|
963
1094
|
const err = error;
|
|
1095
|
+
if (err.statusCode) {
|
|
1096
|
+
const tenancy = tenancyFailure(err.statusCode, err.body ?? "", this.tenancy);
|
|
1097
|
+
if (tenancy)
|
|
1098
|
+
throw tenancy;
|
|
1099
|
+
}
|
|
964
1100
|
if (err.statusCode === 401) {
|
|
965
1101
|
throw new AuthenticationError;
|
|
966
1102
|
}
|
|
@@ -999,28 +1135,26 @@ class SDKClientWrapper {
|
|
|
999
1135
|
} else if (optHeaders) {
|
|
1000
1136
|
extraHeaders = optHeaders;
|
|
1001
1137
|
}
|
|
1002
|
-
const
|
|
1003
|
-
if (this.workspaceId)
|
|
1004
|
-
tenancyHeaders["x-workspace-id"] = this.workspaceId;
|
|
1005
|
-
if (this.organizationId)
|
|
1006
|
-
tenancyHeaders["x-organization-id"] = this.organizationId;
|
|
1007
|
-
const response = await this.recoveringFetch(`${this.endpoint}${path}`, {
|
|
1138
|
+
const response = await fetch(`${this.endpoint}${path}`, {
|
|
1008
1139
|
...options,
|
|
1009
1140
|
headers: {
|
|
1010
1141
|
"x-api-key": this.apiKey,
|
|
1011
1142
|
"Content-Type": "application/json",
|
|
1012
|
-
...tenancyHeaders,
|
|
1143
|
+
...this.tenancyHeaders(),
|
|
1013
1144
|
...extraHeaders
|
|
1014
1145
|
}
|
|
1015
1146
|
});
|
|
1016
1147
|
if (!response.ok) {
|
|
1148
|
+
const error = await response.text();
|
|
1149
|
+
const tenancy = tenancyFailure(response.status, error, this.tenancy);
|
|
1150
|
+
if (tenancy)
|
|
1151
|
+
throw tenancy;
|
|
1017
1152
|
if (response.status === 401) {
|
|
1018
1153
|
throw new AuthenticationError;
|
|
1019
1154
|
}
|
|
1020
1155
|
if (response.status === 403) {
|
|
1021
|
-
throw new ApiError(403,
|
|
1156
|
+
throw new ApiError(403, error || "Access denied");
|
|
1022
1157
|
}
|
|
1023
|
-
const error = await response.text();
|
|
1024
1158
|
const errorLower = error.toLowerCase();
|
|
1025
1159
|
if (errorLower.includes("workspace") && (errorLower.includes("missing") || errorLower.includes("required")) || errorLower.includes("x-workspace-id")) {
|
|
1026
1160
|
throw new WorkspaceContextError(error);
|
|
@@ -1166,11 +1300,9 @@ class SDKClientWrapper {
|
|
|
1166
1300
|
}
|
|
1167
1301
|
async getModelsCatalog() {
|
|
1168
1302
|
try {
|
|
1169
|
-
|
|
1170
|
-
if (!wsId)
|
|
1303
|
+
if (!this.workspaceId)
|
|
1171
1304
|
return { models: [] };
|
|
1172
|
-
|
|
1173
|
-
return await this.request(`/api/providers/catalog${qs}`);
|
|
1305
|
+
return await this.request("/api/providers/catalog");
|
|
1174
1306
|
} catch {
|
|
1175
1307
|
return { models: [] };
|
|
1176
1308
|
}
|
|
@@ -1178,56 +1310,17 @@ class SDKClientWrapper {
|
|
|
1178
1310
|
getCurrentWorkspaceId() {
|
|
1179
1311
|
return this.workspaceId;
|
|
1180
1312
|
}
|
|
1181
|
-
|
|
1182
|
-
return
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (!this.organizationId) {
|
|
1189
|
-
let orgs = await fetchOrganizations(this.apiKey, this.endpoint).catch(() => []);
|
|
1190
|
-
if (orgs.length === 0) {
|
|
1191
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1192
|
-
orgs = await fetchOrganizations(this.apiKey, this.endpoint).catch(() => []);
|
|
1193
|
-
}
|
|
1194
|
-
if (orgs.length > 0 && orgs[0]) {
|
|
1195
|
-
this.organizationId = orgs[0].id;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (this.workspaceResolution === "none" && this.organizationId) {
|
|
1199
|
-
try {
|
|
1200
|
-
const response = await this.sdk.workspaces.listWorkspaces({ includeInactive: false });
|
|
1201
|
-
const workspaces = toWorkspaceList(response.workspaces);
|
|
1202
|
-
if (workspaces.length === 1 && workspaces[0]) {
|
|
1203
|
-
this.workspaceId = workspaces[0].id;
|
|
1204
|
-
this.workspaceResolution = "sole";
|
|
1205
|
-
} else if (workspaces.length > 1) {
|
|
1206
|
-
const defaultWs = workspaces.find((ws) => ws.isDefault);
|
|
1207
|
-
if (defaultWs) {
|
|
1208
|
-
this.workspaceId = defaultWs.id;
|
|
1209
|
-
this.workspaceResolution = "configured";
|
|
1210
|
-
} else {
|
|
1211
|
-
const first = workspaces[0] ?? undefined;
|
|
1212
|
-
if (first) {
|
|
1213
|
-
this.workspaceId = this.workspaceId ?? first.id;
|
|
1214
|
-
this.workspaceResolution = "ambiguous";
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
} catch {}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
getWorkspaceResolution() {
|
|
1222
|
-
return this.workspaceResolution;
|
|
1313
|
+
tenancyHeaders() {
|
|
1314
|
+
return tenancyHeaders({
|
|
1315
|
+
keyScope: this.tenancy.keyScope,
|
|
1316
|
+
workspaceId: this.workspaceId,
|
|
1317
|
+
workspaceOrigin: this.tenancy.workspaceOrigin,
|
|
1318
|
+
organizationId: this.organizationId
|
|
1319
|
+
});
|
|
1223
1320
|
}
|
|
1224
|
-
|
|
1225
|
-
if (!this.workspaceId
|
|
1226
|
-
throw
|
|
1227
|
-
}
|
|
1228
|
-
if (this.workspaceResolution === "ambiguous") {
|
|
1229
|
-
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)");
|
|
1230
|
-
}
|
|
1321
|
+
requireSelectedWorkspace() {
|
|
1322
|
+
if (!this.workspaceId)
|
|
1323
|
+
throw noWorkspaceSelected();
|
|
1231
1324
|
return this.workspaceId;
|
|
1232
1325
|
}
|
|
1233
1326
|
async testProvider(id) {
|
|
@@ -1241,7 +1334,7 @@ class SDKClientWrapper {
|
|
|
1241
1334
|
}
|
|
1242
1335
|
}
|
|
1243
1336
|
async createProvider(data) {
|
|
1244
|
-
this.
|
|
1337
|
+
this.requireSelectedWorkspace();
|
|
1245
1338
|
registerSecret(data.apiKey);
|
|
1246
1339
|
registerSecretFields(data.credentialFields);
|
|
1247
1340
|
const body = buildCreateProviderBody(data);
|
|
@@ -1256,7 +1349,7 @@ class SDKClientWrapper {
|
|
|
1256
1349
|
}
|
|
1257
1350
|
}
|
|
1258
1351
|
async updateProvider(id, data) {
|
|
1259
|
-
const workspaceId = this.
|
|
1352
|
+
const workspaceId = this.requireSelectedWorkspace();
|
|
1260
1353
|
if (data.apiKey)
|
|
1261
1354
|
this.debugSecrets.add(data.apiKey);
|
|
1262
1355
|
registerSecret(data.apiKey);
|
|
@@ -1295,7 +1388,7 @@ class SDKClientWrapper {
|
|
|
1295
1388
|
}
|
|
1296
1389
|
}
|
|
1297
1390
|
async deleteProvider(id) {
|
|
1298
|
-
const workspaceId = this.
|
|
1391
|
+
const workspaceId = this.requireSelectedWorkspace();
|
|
1299
1392
|
try {
|
|
1300
1393
|
await this.sdk.llmProviders.deleteProviderConfig({ id });
|
|
1301
1394
|
return { workspaceId };
|
|
@@ -1310,21 +1403,18 @@ async function getSDKClient() {
|
|
|
1310
1403
|
if (!apiKey) {
|
|
1311
1404
|
throw new AuthenticationError;
|
|
1312
1405
|
}
|
|
1313
|
-
const
|
|
1406
|
+
const tenancy = await resolveKeyTenancy(apiKey);
|
|
1407
|
+
if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
|
|
1408
|
+
throw noWorkspaceSelected();
|
|
1314
1409
|
sdkClient = new SDKClientWrapper({
|
|
1315
1410
|
apiKey,
|
|
1316
|
-
serverURL:
|
|
1317
|
-
workspaceId:
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
if (isDebugEnabled()) {
|
|
1324
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1325
|
-
process.stderr.write(`[mutagent] Context resolution failed: ${message}
|
|
1326
|
-
`);
|
|
1327
|
-
}
|
|
1411
|
+
serverURL: tenancy.endpoint,
|
|
1412
|
+
workspaceId: tenancy.workspaceId,
|
|
1413
|
+
workspaceName: tenancy.workspaceName,
|
|
1414
|
+
workspaceOrigin: tenancy.workspaceOrigin,
|
|
1415
|
+
organizationId: tenancy.organization?.id,
|
|
1416
|
+
organizationName: tenancy.organization?.name,
|
|
1417
|
+
keyScope: tenancy.keyScope
|
|
1328
1418
|
});
|
|
1329
1419
|
}
|
|
1330
1420
|
return sdkClient;
|
|
@@ -1342,19 +1432,6 @@ async function validateApiKey(apiKey, endpoint) {
|
|
|
1342
1432
|
return false;
|
|
1343
1433
|
}
|
|
1344
1434
|
}
|
|
1345
|
-
async function fetchWorkspaces(apiKey, endpoint, orgId) {
|
|
1346
|
-
try {
|
|
1347
|
-
const response = await fetch(`${endpoint}/api/workspaces`, {
|
|
1348
|
-
headers: { "x-api-key": apiKey, "x-organization-id": orgId }
|
|
1349
|
-
});
|
|
1350
|
-
if (!response.ok)
|
|
1351
|
-
return [];
|
|
1352
|
-
const data = await response.json();
|
|
1353
|
-
return data.workspaces ?? [];
|
|
1354
|
-
} catch {
|
|
1355
|
-
return [];
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
1435
|
var sdkClient = null;
|
|
1359
1436
|
var init_sdk_client = __esm(() => {
|
|
1360
1437
|
init_errors();
|
|
@@ -1587,4 +1664,4 @@ export {
|
|
|
1587
1664
|
ApiError
|
|
1588
1665
|
};
|
|
1589
1666
|
|
|
1590
|
-
//# debugId=
|
|
1667
|
+
//# debugId=E203EEB55C5F59D464756E2164756E21
|