@mutagent/cli 0.1.379 → 0.1.381
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +945 -760
- package/dist/index.js +515 -411
- 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,290 @@ 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
|
+
unreachableServerError: () => unreachableServerError,
|
|
609
|
+
tenancyHeaders: () => tenancyHeaders,
|
|
610
|
+
tenancyFailure: () => tenancyFailure,
|
|
611
|
+
setWorkspaceFlag: () => setWorkspaceFlag,
|
|
612
|
+
setOrgAssertion: () => setOrgAssertion,
|
|
613
|
+
resolveKeyTenancy: () => resolveKeyTenancy,
|
|
614
|
+
otherOrganizationHint: () => otherOrganizationHint,
|
|
615
|
+
orgMismatch: () => orgMismatch,
|
|
616
|
+
noWorkspaceSelected: () => noWorkspaceSelected,
|
|
617
|
+
matchesOrganization: () => matchesOrganization,
|
|
618
|
+
looksLikeWorkspaceId: () => looksLikeWorkspaceId,
|
|
619
|
+
listKeyWorkspaces: () => listKeyWorkspaces,
|
|
620
|
+
findWorkspace: () => findWorkspace,
|
|
621
|
+
fetchOrganizations: () => fetchOrganizations,
|
|
622
|
+
fetchKeySelf: () => fetchKeySelf,
|
|
623
|
+
envKeyHasNoSelection: () => envKeyHasNoSelection,
|
|
624
|
+
deadKeyError: () => deadKeyError,
|
|
625
|
+
configTenancyHeaders: () => configTenancyHeaders,
|
|
626
|
+
TenancyError: () => TenancyError,
|
|
627
|
+
TENANCY_DENIED: () => TENANCY_DENIED
|
|
628
|
+
});
|
|
629
|
+
function looksLikeWorkspaceId(value) {
|
|
630
|
+
return UUID_PATTERN.test(value);
|
|
631
|
+
}
|
|
632
|
+
function tenancyHeaders(t) {
|
|
633
|
+
const headers = {};
|
|
634
|
+
if (!t.workspaceId)
|
|
635
|
+
return headers;
|
|
636
|
+
if (t.keyScope === "workspace") {
|
|
637
|
+
if (t.workspaceOrigin === "override")
|
|
638
|
+
headers["x-workspace-id"] = t.workspaceId;
|
|
639
|
+
return headers;
|
|
640
|
+
}
|
|
641
|
+
headers["x-workspace-id"] = t.workspaceId;
|
|
642
|
+
if (t.keyScope === "user" && t.organizationId)
|
|
643
|
+
headers["x-organization-id"] = t.organizationId;
|
|
644
|
+
return headers;
|
|
645
|
+
}
|
|
646
|
+
function configTenancyHeaders(config) {
|
|
647
|
+
return tenancyHeaders({
|
|
648
|
+
keyScope: config.keyScope,
|
|
649
|
+
workspaceId: config.defaultWorkspace,
|
|
650
|
+
workspaceOrigin: config.workspaceOrigin,
|
|
651
|
+
organizationId: config.defaultOrganization
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function parseBody(body) {
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(body);
|
|
657
|
+
if (parsed && typeof parsed === "object")
|
|
658
|
+
return parsed;
|
|
659
|
+
} catch {}
|
|
660
|
+
return {};
|
|
661
|
+
}
|
|
662
|
+
function deadKeyError() {
|
|
663
|
+
return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login", 2);
|
|
664
|
+
}
|
|
665
|
+
function tenancyFailure(status, body, ctx) {
|
|
666
|
+
const parsed = parseBody(body);
|
|
667
|
+
if (status === 401)
|
|
668
|
+
return deadKeyError();
|
|
669
|
+
if (status === 403 && parsed.code === TENANCY_DENIED) {
|
|
670
|
+
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
671
|
+
if (ctx.workspaceOrigin === "selection") {
|
|
672
|
+
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
673
|
+
}
|
|
674
|
+
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
675
|
+
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
676
|
+
}
|
|
677
|
+
if (status === 403 && ctx.keyScope === "workspace" && ctx.workspaceOrigin === "override") {
|
|
678
|
+
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)");
|
|
679
|
+
}
|
|
680
|
+
if (status === 400 && (parsed.message ?? body).toLowerCase().includes("x-workspace-id")) {
|
|
681
|
+
return noWorkspaceSelected();
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
function noWorkspaceSelected() {
|
|
686
|
+
const fix = envKeyReplacesFile() ? ENV_KEY_WORKSPACE_FIX : "Run: mutagent workspaces use <name>";
|
|
687
|
+
return new TenancyError("WORKSPACE_REQUIRED", "No workspace selected.", fix, 3);
|
|
688
|
+
}
|
|
689
|
+
function envKeyHasNoSelection() {
|
|
690
|
+
return new TenancyError("WORKSPACE_FROM_ENV", "With MUTAGENT_API_KEY set, the workspace comes from --workspace or MUTAGENT_WORKSPACE_ID.", ENV_KEY_WORKSPACE_FIX);
|
|
691
|
+
}
|
|
692
|
+
async function tenancyFetch(url, init) {
|
|
693
|
+
try {
|
|
694
|
+
return await fetch(url, init);
|
|
695
|
+
} catch {
|
|
696
|
+
throw unreachableServerError(url);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function unreachableServerError(url) {
|
|
700
|
+
let origin = url;
|
|
701
|
+
try {
|
|
702
|
+
origin = new URL(url).origin;
|
|
703
|
+
} catch {}
|
|
704
|
+
return new MutagentError("REQUEST_FAILED", `Unable to connect to the Mutagent server at ${origin}.`, "Check your connection and the endpoint: mutagent config get endpoint");
|
|
705
|
+
}
|
|
706
|
+
async function fetchKeySelf(apiKey, endpoint, opts = {}) {
|
|
707
|
+
const response = await tenancyFetch(`${endpoint}/api/api-keys/self`, {
|
|
708
|
+
headers: { "x-api-key": apiKey, ...opts.headers }
|
|
709
|
+
});
|
|
710
|
+
if (!response.ok) {
|
|
711
|
+
const body = await response.text().catch(() => "");
|
|
712
|
+
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");
|
|
713
|
+
}
|
|
714
|
+
return await response.json();
|
|
715
|
+
}
|
|
716
|
+
async function fetchOrganizations(apiKey, endpoint) {
|
|
717
|
+
try {
|
|
718
|
+
const response = await fetch(`${endpoint}/api/organizations`, {
|
|
719
|
+
headers: { "x-api-key": apiKey }
|
|
720
|
+
});
|
|
721
|
+
if (!response.ok)
|
|
722
|
+
return [];
|
|
723
|
+
const data = await response.json();
|
|
724
|
+
return data.data ?? [];
|
|
725
|
+
} catch {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function matchesOrganization(value, org) {
|
|
730
|
+
if (!org)
|
|
731
|
+
return false;
|
|
732
|
+
if (value === org.id || org.slug !== undefined && value === org.slug)
|
|
733
|
+
return true;
|
|
734
|
+
return org.name !== undefined && value.toLowerCase() === org.name.toLowerCase();
|
|
735
|
+
}
|
|
736
|
+
function orgMismatch(value, org) {
|
|
737
|
+
const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
|
|
738
|
+
return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${value}`);
|
|
739
|
+
}
|
|
740
|
+
function otherOrganizationHint(wanted, keyOrg, orgs) {
|
|
741
|
+
const others = orgs.filter((o) => o.id !== keyOrg?.id);
|
|
742
|
+
const where = keyOrg ? ` in ${keyOrg.name}` : "";
|
|
743
|
+
const what = `No workspace "${wanted}"${where}.`;
|
|
744
|
+
const named = others.map((o) => `${o.name} (${o.slug ?? o.id})`).join(", ");
|
|
745
|
+
if (others.length === 1 && others[0]) {
|
|
746
|
+
const only = others[0];
|
|
747
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organization: ${named}.`, `To switch: mutagent login --org ${only.slug ?? only.id}`);
|
|
748
|
+
}
|
|
749
|
+
if (others.length > 1) {
|
|
750
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", `${what} Your other organizations: ${named}.`, "To switch: mutagent login --org <slug>");
|
|
751
|
+
}
|
|
752
|
+
return new TenancyError("WORKSPACE_NOT_FOUND", what, "See: mutagent workspaces list");
|
|
753
|
+
}
|
|
754
|
+
function setOrgAssertion(value) {
|
|
755
|
+
const trimmed = value?.trim();
|
|
756
|
+
orgAssertion = trimmed === "" ? undefined : trimmed;
|
|
757
|
+
}
|
|
758
|
+
function setWorkspaceFlag(value) {
|
|
759
|
+
const trimmed = value?.trim();
|
|
760
|
+
workspaceFlag = trimmed === "" ? undefined : trimmed;
|
|
761
|
+
}
|
|
762
|
+
function endpointOf(configured) {
|
|
763
|
+
const trimmed = configured?.trim();
|
|
764
|
+
return (trimmed === undefined || trimmed === "" ? "https://api.mutagent.io" : trimmed).replace(/\/+$/, "");
|
|
765
|
+
}
|
|
766
|
+
async function probeKeyScope(apiKey, endpoint, workspaceNamed) {
|
|
767
|
+
const fromFile = !envKeyReplacesFile();
|
|
768
|
+
if (!fromFile && workspaceNamed && orgAssertion === undefined)
|
|
769
|
+
return;
|
|
770
|
+
try {
|
|
771
|
+
const self = await fetchKeySelf(apiKey, endpoint);
|
|
772
|
+
if (fromFile)
|
|
773
|
+
recordKeyScope(self);
|
|
774
|
+
return self;
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (error instanceof MutagentError && (error.code === "AUTH_EXPIRED" || error.code === "REQUEST_FAILED"))
|
|
777
|
+
throw error;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function enforceOrgAssertion(org) {
|
|
782
|
+
if (orgAssertion === undefined)
|
|
783
|
+
return;
|
|
784
|
+
if (org === undefined) {
|
|
785
|
+
throw new TenancyError("ORG_UNKNOWN", "This key's organization is unknown, so --org cannot be checked.", "Run: mutagent workspaces current");
|
|
786
|
+
}
|
|
787
|
+
if (!matchesOrganization(orgAssertion, org)) {
|
|
788
|
+
throw orgMismatch(orgAssertion, org);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function resolveKeyTenancy(apiKey, opts = {}) {
|
|
792
|
+
const config = loadConfig();
|
|
793
|
+
const endpoint = endpointOf(config.endpoint);
|
|
794
|
+
let keyScope = config.keyScope;
|
|
795
|
+
let organization = config.defaultOrganization ? { id: config.defaultOrganization, name: config.organizationName ?? config.defaultOrganization, slug: config.organizationSlug } : undefined;
|
|
796
|
+
let workspaceId = config.defaultWorkspace;
|
|
797
|
+
let workspaceName = config.workspaceName;
|
|
798
|
+
const workspaceOrigin = config.workspaceOrigin;
|
|
799
|
+
if (keyScope === undefined && (opts.probe !== false || orgAssertion !== undefined)) {
|
|
800
|
+
const self = await probeKeyScope(apiKey, endpoint, workspaceId !== undefined);
|
|
801
|
+
if (self) {
|
|
802
|
+
keyScope = self.scope;
|
|
803
|
+
organization = self.organization ?? undefined;
|
|
804
|
+
if (self.scope === "workspace" && self.workspace && workspaceOrigin !== "override") {
|
|
805
|
+
workspaceId = self.workspace.id;
|
|
806
|
+
workspaceName = self.workspace.name;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
enforceOrgAssertion(organization);
|
|
811
|
+
if (opts.resolveOverride !== false && workspaceFlag !== undefined && workspaceId === workspaceFlag && !looksLikeWorkspaceId(workspaceId)) {
|
|
812
|
+
const found = await findWorkspace(workspaceId, { apiKey, endpoint, keyScope, organization, workspaceId: config.defaultWorkspace, workspaceName });
|
|
813
|
+
workspaceId = found.id;
|
|
814
|
+
workspaceName = found.name;
|
|
815
|
+
process.env.MUTAGENT_WORKSPACE_ID = found.id;
|
|
816
|
+
}
|
|
817
|
+
return { endpoint, keyScope, organization, workspaceId, workspaceName, workspaceOrigin };
|
|
818
|
+
}
|
|
819
|
+
async function listKeyWorkspaces(apiKey, endpoint, t) {
|
|
820
|
+
const headers = { "x-api-key": apiKey };
|
|
821
|
+
if (t.keyScope === "user" && t.organization)
|
|
822
|
+
headers["x-organization-id"] = t.organization.id;
|
|
823
|
+
const response = await tenancyFetch(`${endpoint}/api/workspaces`, { headers });
|
|
824
|
+
if (!response.ok) {
|
|
825
|
+
const body = await response.text().catch(() => "");
|
|
826
|
+
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");
|
|
827
|
+
}
|
|
828
|
+
const data = await response.json();
|
|
829
|
+
return data.workspaces ?? [];
|
|
830
|
+
}
|
|
831
|
+
async function findWorkspace(wanted, t) {
|
|
832
|
+
if (t.keyScope === "workspace") {
|
|
833
|
+
if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
|
|
834
|
+
return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
|
|
835
|
+
}
|
|
836
|
+
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)");
|
|
837
|
+
}
|
|
838
|
+
const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
|
|
839
|
+
const byId = rows.find((w) => w.id === wanted);
|
|
840
|
+
if (byId)
|
|
841
|
+
return { id: byId.id, name: byId.name };
|
|
842
|
+
let matches = rows.filter((w) => w.name === wanted || w.slug === wanted);
|
|
843
|
+
if (matches.length === 0)
|
|
844
|
+
matches = rows.filter((w) => w.name.toLowerCase() === wanted.toLowerCase());
|
|
845
|
+
if (matches.length === 1 && matches[0])
|
|
846
|
+
return { id: matches[0].id, name: matches[0].name };
|
|
847
|
+
if (matches.length > 1) {
|
|
848
|
+
throw new TenancyError("WORKSPACE_AMBIGUOUS", `Several workspaces are named "${wanted}": ${matches.map((w) => `${w.name} (${w.id})`).join(", ")}.`, "Run: mutagent workspaces use <id>");
|
|
849
|
+
}
|
|
850
|
+
const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
|
|
851
|
+
throw otherOrganizationHint(wanted, t.organization, orgs);
|
|
852
|
+
}
|
|
853
|
+
var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag;
|
|
854
|
+
var init_organization_scope = __esm(() => {
|
|
855
|
+
init_errors();
|
|
856
|
+
init_config();
|
|
857
|
+
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
858
|
+
TenancyError = class TenancyError extends MutagentError {
|
|
859
|
+
constructor(code, what, fix, exitCode = 1) {
|
|
860
|
+
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""), exitCode);
|
|
861
|
+
this.name = "TenancyError";
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// src/lib/provider-request.ts
|
|
867
|
+
function buildCreateProviderBody(data) {
|
|
868
|
+
const body = {
|
|
869
|
+
name: data.name,
|
|
870
|
+
provider: data.provider,
|
|
871
|
+
isDefault: data.isDefault ?? false
|
|
872
|
+
};
|
|
873
|
+
if (data.apiKey)
|
|
874
|
+
body.apiKey = data.apiKey;
|
|
875
|
+
if (data.baseUrl)
|
|
876
|
+
body.baseUrl = data.baseUrl;
|
|
877
|
+
if (data.hostedFamily)
|
|
878
|
+
body.hostedFamily = data.hostedFamily;
|
|
879
|
+
if (data.credentialFields && Object.keys(data.credentialFields).length > 0) {
|
|
880
|
+
body.credentialFields = data.credentialFields;
|
|
881
|
+
}
|
|
882
|
+
if (data.metadata && Object.keys(data.metadata).length > 0)
|
|
883
|
+
body.metadata = data.metadata;
|
|
884
|
+
if (data.supportedModels !== undefined)
|
|
885
|
+
body.supportedModels = data.supportedModels;
|
|
886
|
+
return body;
|
|
887
|
+
}
|
|
888
|
+
|
|
447
889
|
// src/lib/sdk-debug.ts
|
|
448
890
|
function isDebugEnabled(env = process.env) {
|
|
449
891
|
return TRUTHY.has((env.MUTAGENT_DEBUG ?? "").trim().toLowerCase());
|
|
@@ -538,282 +980,6 @@ var init_sdk_debug = __esm(() => {
|
|
|
538
980
|
};
|
|
539
981
|
});
|
|
540
982
|
|
|
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
983
|
// src/lib/sdk-mappers.ts
|
|
818
984
|
import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
|
|
819
985
|
function isProviderType(value) {
|
|
@@ -902,15 +1068,6 @@ var init_sdk_mappers = __esm(() => {
|
|
|
902
1068
|
});
|
|
903
1069
|
|
|
904
1070
|
// 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
1071
|
import { Mutagent, HTTPClient } from "@mutagent/sdk";
|
|
915
1072
|
|
|
916
1073
|
class SDKClientWrapper {
|
|
@@ -919,32 +1076,24 @@ class SDKClientWrapper {
|
|
|
919
1076
|
endpoint;
|
|
920
1077
|
workspaceId;
|
|
921
1078
|
organizationId;
|
|
922
|
-
|
|
1079
|
+
tenancy;
|
|
923
1080
|
debugSecrets = new Set;
|
|
924
|
-
recoveringFetch;
|
|
925
1081
|
constructor(opts) {
|
|
926
1082
|
this.apiKey = opts.apiKey;
|
|
927
1083
|
this.endpoint = opts.serverURL ?? "http://localhost:3003";
|
|
928
1084
|
this.workspaceId = opts.workspaceId;
|
|
929
1085
|
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 });
|
|
1086
|
+
this.tenancy = {
|
|
1087
|
+
keyScope: opts.keyScope,
|
|
1088
|
+
workspaceId: opts.workspaceId,
|
|
1089
|
+
workspaceName: opts.workspaceName,
|
|
1090
|
+
workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
|
|
1091
|
+
organizationName: opts.organizationName
|
|
1092
|
+
};
|
|
1093
|
+
const httpClient = new HTTPClient;
|
|
943
1094
|
httpClient.addHook("beforeRequest", (req) => {
|
|
944
|
-
|
|
945
|
-
req.headers.set(
|
|
946
|
-
if (this.organizationId)
|
|
947
|
-
req.headers.set("x-organization-id", this.organizationId);
|
|
1095
|
+
for (const [name, value] of Object.entries(this.tenancyHeaders()))
|
|
1096
|
+
req.headers.set(name, value);
|
|
948
1097
|
return req;
|
|
949
1098
|
});
|
|
950
1099
|
this.debugSecrets.add(this.apiKey);
|
|
@@ -959,8 +1108,16 @@ class SDKClientWrapper {
|
|
|
959
1108
|
});
|
|
960
1109
|
}
|
|
961
1110
|
handleError(error) {
|
|
1111
|
+
if (error instanceof Error && SDK_TRANSPORT_ERRORS.has(error.name)) {
|
|
1112
|
+
throw unreachableServerError(this.endpoint);
|
|
1113
|
+
}
|
|
962
1114
|
if (error && typeof error === "object") {
|
|
963
1115
|
const err = error;
|
|
1116
|
+
if (err.statusCode) {
|
|
1117
|
+
const tenancy = tenancyFailure(err.statusCode, err.body ?? "", this.tenancy);
|
|
1118
|
+
if (tenancy)
|
|
1119
|
+
throw tenancy;
|
|
1120
|
+
}
|
|
964
1121
|
if (err.statusCode === 401) {
|
|
965
1122
|
throw new AuthenticationError;
|
|
966
1123
|
}
|
|
@@ -981,7 +1138,7 @@ class SDKClientWrapper {
|
|
|
981
1138
|
}
|
|
982
1139
|
if (error instanceof TypeError && typeof error.message === "string") {
|
|
983
1140
|
if (error.message.includes("fetch") || error.message.includes("network") || error.message.includes("ECONNREFUSED")) {
|
|
984
|
-
throw
|
|
1141
|
+
throw unreachableServerError(this.endpoint);
|
|
985
1142
|
}
|
|
986
1143
|
}
|
|
987
1144
|
if (error instanceof Error) {
|
|
@@ -999,28 +1156,31 @@ class SDKClientWrapper {
|
|
|
999
1156
|
} else if (optHeaders) {
|
|
1000
1157
|
extraHeaders = optHeaders;
|
|
1001
1158
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1159
|
+
let response;
|
|
1160
|
+
try {
|
|
1161
|
+
response = await fetch(`${this.endpoint}${path}`, {
|
|
1162
|
+
...options,
|
|
1163
|
+
headers: {
|
|
1164
|
+
"x-api-key": this.apiKey,
|
|
1165
|
+
"Content-Type": "application/json",
|
|
1166
|
+
...this.tenancyHeaders(),
|
|
1167
|
+
...extraHeaders
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
} catch {
|
|
1171
|
+
throw unreachableServerError(this.endpoint);
|
|
1172
|
+
}
|
|
1016
1173
|
if (!response.ok) {
|
|
1174
|
+
const error = await response.text();
|
|
1175
|
+
const tenancy = tenancyFailure(response.status, error, this.tenancy);
|
|
1176
|
+
if (tenancy)
|
|
1177
|
+
throw tenancy;
|
|
1017
1178
|
if (response.status === 401) {
|
|
1018
1179
|
throw new AuthenticationError;
|
|
1019
1180
|
}
|
|
1020
1181
|
if (response.status === 403) {
|
|
1021
|
-
throw new ApiError(403,
|
|
1182
|
+
throw new ApiError(403, error || "Access denied");
|
|
1022
1183
|
}
|
|
1023
|
-
const error = await response.text();
|
|
1024
1184
|
const errorLower = error.toLowerCase();
|
|
1025
1185
|
if (errorLower.includes("workspace") && (errorLower.includes("missing") || errorLower.includes("required")) || errorLower.includes("x-workspace-id")) {
|
|
1026
1186
|
throw new WorkspaceContextError(error);
|
|
@@ -1166,11 +1326,9 @@ class SDKClientWrapper {
|
|
|
1166
1326
|
}
|
|
1167
1327
|
async getModelsCatalog() {
|
|
1168
1328
|
try {
|
|
1169
|
-
|
|
1170
|
-
if (!wsId)
|
|
1329
|
+
if (!this.workspaceId)
|
|
1171
1330
|
return { models: [] };
|
|
1172
|
-
|
|
1173
|
-
return await this.request(`/api/providers/catalog${qs}`);
|
|
1331
|
+
return await this.request("/api/providers/catalog");
|
|
1174
1332
|
} catch {
|
|
1175
1333
|
return { models: [] };
|
|
1176
1334
|
}
|
|
@@ -1178,56 +1336,17 @@ class SDKClientWrapper {
|
|
|
1178
1336
|
getCurrentWorkspaceId() {
|
|
1179
1337
|
return this.workspaceId;
|
|
1180
1338
|
}
|
|
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;
|
|
1339
|
+
tenancyHeaders() {
|
|
1340
|
+
return tenancyHeaders({
|
|
1341
|
+
keyScope: this.tenancy.keyScope,
|
|
1342
|
+
workspaceId: this.workspaceId,
|
|
1343
|
+
workspaceOrigin: this.tenancy.workspaceOrigin,
|
|
1344
|
+
organizationId: this.organizationId
|
|
1345
|
+
});
|
|
1223
1346
|
}
|
|
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
|
-
}
|
|
1347
|
+
requireSelectedWorkspace() {
|
|
1348
|
+
if (!this.workspaceId)
|
|
1349
|
+
throw noWorkspaceSelected();
|
|
1231
1350
|
return this.workspaceId;
|
|
1232
1351
|
}
|
|
1233
1352
|
async testProvider(id) {
|
|
@@ -1241,7 +1360,7 @@ class SDKClientWrapper {
|
|
|
1241
1360
|
}
|
|
1242
1361
|
}
|
|
1243
1362
|
async createProvider(data) {
|
|
1244
|
-
this.
|
|
1363
|
+
this.requireSelectedWorkspace();
|
|
1245
1364
|
registerSecret(data.apiKey);
|
|
1246
1365
|
registerSecretFields(data.credentialFields);
|
|
1247
1366
|
const body = buildCreateProviderBody(data);
|
|
@@ -1256,7 +1375,7 @@ class SDKClientWrapper {
|
|
|
1256
1375
|
}
|
|
1257
1376
|
}
|
|
1258
1377
|
async updateProvider(id, data) {
|
|
1259
|
-
const workspaceId = this.
|
|
1378
|
+
const workspaceId = this.requireSelectedWorkspace();
|
|
1260
1379
|
if (data.apiKey)
|
|
1261
1380
|
this.debugSecrets.add(data.apiKey);
|
|
1262
1381
|
registerSecret(data.apiKey);
|
|
@@ -1295,7 +1414,7 @@ class SDKClientWrapper {
|
|
|
1295
1414
|
}
|
|
1296
1415
|
}
|
|
1297
1416
|
async deleteProvider(id) {
|
|
1298
|
-
const workspaceId = this.
|
|
1417
|
+
const workspaceId = this.requireSelectedWorkspace();
|
|
1299
1418
|
try {
|
|
1300
1419
|
await this.sdk.llmProviders.deleteProviderConfig({ id });
|
|
1301
1420
|
return { workspaceId };
|
|
@@ -1310,21 +1429,18 @@ async function getSDKClient() {
|
|
|
1310
1429
|
if (!apiKey) {
|
|
1311
1430
|
throw new AuthenticationError;
|
|
1312
1431
|
}
|
|
1313
|
-
const
|
|
1432
|
+
const tenancy = await resolveKeyTenancy(apiKey);
|
|
1433
|
+
if (tenancy.keyScope === "organization" && !tenancy.workspaceId)
|
|
1434
|
+
throw noWorkspaceSelected();
|
|
1314
1435
|
sdkClient = new SDKClientWrapper({
|
|
1315
1436
|
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
|
-
}
|
|
1437
|
+
serverURL: tenancy.endpoint,
|
|
1438
|
+
workspaceId: tenancy.workspaceId,
|
|
1439
|
+
workspaceName: tenancy.workspaceName,
|
|
1440
|
+
workspaceOrigin: tenancy.workspaceOrigin,
|
|
1441
|
+
organizationId: tenancy.organization?.id,
|
|
1442
|
+
organizationName: tenancy.organization?.name,
|
|
1443
|
+
keyScope: tenancy.keyScope
|
|
1328
1444
|
});
|
|
1329
1445
|
}
|
|
1330
1446
|
return sdkClient;
|
|
@@ -1342,20 +1458,7 @@ async function validateApiKey(apiKey, endpoint) {
|
|
|
1342
1458
|
return false;
|
|
1343
1459
|
}
|
|
1344
1460
|
}
|
|
1345
|
-
|
|
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
|
-
var sdkClient = null;
|
|
1461
|
+
var SDK_TRANSPORT_ERRORS, sdkClient = null;
|
|
1359
1462
|
var init_sdk_client = __esm(() => {
|
|
1360
1463
|
init_errors();
|
|
1361
1464
|
init_config();
|
|
@@ -1364,6 +1467,7 @@ var init_sdk_client = __esm(() => {
|
|
|
1364
1467
|
init_known_secrets();
|
|
1365
1468
|
init_sdk_mappers();
|
|
1366
1469
|
init_organization_scope();
|
|
1470
|
+
SDK_TRANSPORT_ERRORS = new Set(["ConnectionError", "RequestTimeoutError", "RequestAbortedError", "UnexpectedClientError"]);
|
|
1367
1471
|
});
|
|
1368
1472
|
|
|
1369
1473
|
// src/index.ts
|
|
@@ -1587,4 +1691,4 @@ export {
|
|
|
1587
1691
|
ApiError
|
|
1588
1692
|
};
|
|
1589
1693
|
|
|
1590
|
-
//# debugId=
|
|
1694
|
+
//# debugId=6C0EFE359421B56964756E2164756E21
|