@hasna/skills 0.3.0 → 0.4.0
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/README.md +94 -10
- package/bin/index.js +1398 -571
- package/bin/mcp.js +899 -211
- package/bin/server.js +14 -5
- package/bin/worker.js +2 -3
- package/dist/cli/commands/remote-account.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +770 -210
- package/dist/lib/auth-store.d.ts +1 -1
- package/dist/lib/fleet-credentials.d.ts +8 -2
- package/dist/lib/instance-credentials-race.fixture.d.ts +1 -0
- package/dist/lib/instance-credentials.d.ts +13 -0
- package/dist/lib/mcp-contracts.d.ts +4 -0
- package/dist/lib/portable-skills-files.d.ts +4 -0
- package/dist/lib/portable-skills-types.d.ts +2 -0
- package/dist/lib/remote-account.d.ts +42 -0
- package/dist/lib/remote-auth.d.ts +32 -0
- package/dist/lib/remote-client.d.ts +56 -1
- package/dist/lib/remote-customer-operations.d.ts +106 -0
- package/dist/lib/remote-files.d.ts +21 -0
- package/dist/lib/run-routing.d.ts +1 -0
- package/dist/lib/run-state.d.ts +3 -0
- package/dist/mcp/remote-customer-tools.d.ts +2 -0
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +1057 -280
- package/dist/sdk/runs.d.ts +4 -4
- package/package.json +2 -3
package/bin/mcp.js
CHANGED
|
@@ -7608,93 +7608,7 @@ function toV1BaseUrl(apiUrl) {
|
|
|
7608
7608
|
url.pathname = `${path}/v1`;
|
|
7609
7609
|
return url.toString().replace(/\/+$/, "");
|
|
7610
7610
|
}
|
|
7611
|
-
|
|
7612
|
-
env = snapshotClientEnvironment(name, env);
|
|
7613
|
-
const keys = clientTransportEnvKeys(name);
|
|
7614
|
-
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
7615
|
-
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
7616
|
-
if (blankUrl) {
|
|
7617
|
-
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
7618
|
-
}
|
|
7619
|
-
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
7620
|
-
if (controlledUrl) {
|
|
7621
|
-
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
7622
|
-
}
|
|
7623
|
-
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
7624
|
-
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
7625
|
-
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
7626
|
-
}
|
|
7627
|
-
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
7628
|
-
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
7629
|
-
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
7630
|
-
if (diskConfigUrlHit?.unusable) {
|
|
7631
|
-
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
7632
|
-
}
|
|
7633
|
-
const urlCandidates = [
|
|
7634
|
-
...envUrlHit ? [envUrlHit] : [],
|
|
7635
|
-
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
7636
|
-
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
7637
|
-
];
|
|
7638
|
-
const configuredUrl = urlCandidates[0] ?? null;
|
|
7639
|
-
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
7640
|
-
if (configuredUrl && divergentUrls.length > 0) {
|
|
7641
|
-
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
7642
|
-
}
|
|
7643
|
-
const warnings = [];
|
|
7644
|
-
if (configuredUrl && !envUrlHit) {
|
|
7645
|
-
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
7646
|
-
}
|
|
7647
|
-
const credential = resolveCredential(name, env, options.credentials);
|
|
7648
|
-
if (!credential) {
|
|
7649
|
-
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
7650
|
-
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
7651
|
-
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
7652
|
-
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
7653
|
-
}
|
|
7654
|
-
if (credential.warning)
|
|
7655
|
-
warnings.push(credential.warning);
|
|
7656
|
-
let urlHit;
|
|
7657
|
-
if (configuredUrl) {
|
|
7658
|
-
urlHit = configuredUrl;
|
|
7659
|
-
} else {
|
|
7660
|
-
try {
|
|
7661
|
-
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
7662
|
-
} catch (error2) {
|
|
7663
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7664
|
-
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
7665
|
-
}
|
|
7666
|
-
}
|
|
7667
|
-
const apiUrlSource = urlHit.key;
|
|
7668
|
-
let baseUrl;
|
|
7669
|
-
try {
|
|
7670
|
-
baseUrl = toV1BaseUrl(urlHit.value);
|
|
7671
|
-
} catch (error2) {
|
|
7672
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7673
|
-
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
7674
|
-
}
|
|
7675
|
-
return {
|
|
7676
|
-
resolution: {
|
|
7677
|
-
transport: "http",
|
|
7678
|
-
transportSource: urlHit.key,
|
|
7679
|
-
baseUrl,
|
|
7680
|
-
apiUrlSource,
|
|
7681
|
-
apiKeyPresent: true,
|
|
7682
|
-
apiKeySource: credential.source,
|
|
7683
|
-
apiKeyTier: credential.tier,
|
|
7684
|
-
misconfigured: false,
|
|
7685
|
-
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
7686
|
-
},
|
|
7687
|
-
credential
|
|
7688
|
-
};
|
|
7689
|
-
}
|
|
7690
|
-
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
7691
|
-
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
7692
|
-
}
|
|
7693
|
-
function credentialDiskSourcesForMessage(name, env) {
|
|
7694
|
-
const paths = credentialDiskSources(name, env);
|
|
7695
|
-
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7696
|
-
}
|
|
7697
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
7611
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
7698
7612
|
var init_transport = __esm(() => {
|
|
7699
7613
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
7700
7614
|
appName;
|
|
@@ -7727,16 +7641,6 @@ var init_transport = __esm(() => {
|
|
|
7727
7641
|
requireSecretsSdk = createRequire(import.meta.url);
|
|
7728
7642
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7729
7643
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7730
|
-
ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
|
|
7731
|
-
appName;
|
|
7732
|
-
sources;
|
|
7733
|
-
constructor(appName, message, sources = []) {
|
|
7734
|
-
super(message);
|
|
7735
|
-
this.name = "ClientTransportConfigurationError";
|
|
7736
|
-
this.appName = appName;
|
|
7737
|
-
this.sources = Object.freeze([...sources]);
|
|
7738
|
-
}
|
|
7739
|
-
};
|
|
7740
7644
|
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7741
7645
|
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
7742
7646
|
"host",
|
|
@@ -7747,6 +7651,97 @@ var init_transport = __esm(() => {
|
|
|
7747
7651
|
]);
|
|
7748
7652
|
});
|
|
7749
7653
|
|
|
7654
|
+
// src/lib/instance-credentials.ts
|
|
7655
|
+
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync2, readSync } from "fs";
|
|
7656
|
+
function selectedSkillsProfile(env, explicit) {
|
|
7657
|
+
const selected = explicit ?? env.HASNA_PROFILE;
|
|
7658
|
+
if (selected === undefined)
|
|
7659
|
+
return null;
|
|
7660
|
+
const profile = selected.trim();
|
|
7661
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(profile))
|
|
7662
|
+
throw new Error("Invalid Skills credential profile");
|
|
7663
|
+
return profile;
|
|
7664
|
+
}
|
|
7665
|
+
function skillsProfileCredentialFiles(env, explicit) {
|
|
7666
|
+
return credentialDiskSourceList("skills", env, selectedSkillsProfile(env, explicit)).map((source) => source.path);
|
|
7667
|
+
}
|
|
7668
|
+
function fileIdentity(file) {
|
|
7669
|
+
try {
|
|
7670
|
+
return lstatSync3(file);
|
|
7671
|
+
} catch (error2) {
|
|
7672
|
+
if (["ENOENT", "ENOTDIR"].includes(error2.code ?? ""))
|
|
7673
|
+
return null;
|
|
7674
|
+
throw new Error("Cannot inspect Skills instance configuration");
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
function unchanged(before, after) {
|
|
7678
|
+
return before === null || after === null ? before === after : ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode", "uid"].every((key) => before[key] === after[key]);
|
|
7679
|
+
}
|
|
7680
|
+
function captureSkillsCredentialFiles(files) {
|
|
7681
|
+
const identities = files.map((file) => [file, fileIdentity(file)]);
|
|
7682
|
+
return () => {
|
|
7683
|
+
if (identities.some(([file, before]) => !unchanged(before, fileIdentity(file)))) {
|
|
7684
|
+
throw new Error("Skills instance configuration changed while resolving credentials; retry without sending a credential");
|
|
7685
|
+
}
|
|
7686
|
+
};
|
|
7687
|
+
}
|
|
7688
|
+
function readMetadataText(file) {
|
|
7689
|
+
let fd;
|
|
7690
|
+
try {
|
|
7691
|
+
fd = openSync2(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
7692
|
+
} catch (error2) {
|
|
7693
|
+
if (["ENOENT", "ENOTDIR"].includes(error2.code ?? ""))
|
|
7694
|
+
return null;
|
|
7695
|
+
throw new Error("Cannot safely read Skills instance configuration");
|
|
7696
|
+
}
|
|
7697
|
+
try {
|
|
7698
|
+
const before = fstatSync2(fd);
|
|
7699
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
7700
|
+
if (!before.isFile() || ![256, 384].includes(before.mode & 4095) || uid !== undefined && before.uid !== uid || before.size > 64 * 1024) {
|
|
7701
|
+
throw new Error("Unsafe Skills instance configuration; expected a bounded owner-only regular file");
|
|
7702
|
+
}
|
|
7703
|
+
const bytes = Buffer.alloc(64 * 1024 + 1);
|
|
7704
|
+
let length = 0;
|
|
7705
|
+
while (length < bytes.length) {
|
|
7706
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
7707
|
+
if (!count)
|
|
7708
|
+
break;
|
|
7709
|
+
length += count;
|
|
7710
|
+
}
|
|
7711
|
+
if (length > 64 * 1024 || !unchanged(before, fstatSync2(fd)) || !unchanged(before, fileIdentity(file))) {
|
|
7712
|
+
throw new Error("Skills instance configuration changed while reading");
|
|
7713
|
+
}
|
|
7714
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
7715
|
+
} finally {
|
|
7716
|
+
closeSync2(fd);
|
|
7717
|
+
}
|
|
7718
|
+
}
|
|
7719
|
+
function readSkillsInstanceMetadata(file) {
|
|
7720
|
+
const text = readMetadataText(file);
|
|
7721
|
+
if (text === null)
|
|
7722
|
+
return {};
|
|
7723
|
+
const values = new Map;
|
|
7724
|
+
for (const line of text.split(/\r?\n/)) {
|
|
7725
|
+
const match = /^\s*(?:export\s+)?(HASNA_SKILLS_API_URL|SKILLS_API_URL|HASNA_SKILLS_BOUND_API_URL)\s*=\s*(.*)$/.exec(line);
|
|
7726
|
+
if (!match)
|
|
7727
|
+
continue;
|
|
7728
|
+
let value = match[2].trim();
|
|
7729
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
|
|
7730
|
+
value = value.slice(1, -1);
|
|
7731
|
+
if (!value || /[\x00-\x20\x7f]/.test(value) || values.has(match[1]))
|
|
7732
|
+
throw new Error("Invalid Skills instance configuration");
|
|
7733
|
+
values.set(match[1], value);
|
|
7734
|
+
}
|
|
7735
|
+
const urls = [values.get("HASNA_SKILLS_API_URL"), values.get("SKILLS_API_URL")].filter(Boolean);
|
|
7736
|
+
if (new Set(urls).size > 1)
|
|
7737
|
+
throw new Error("Skills API URL aliases disagree");
|
|
7738
|
+
return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
|
|
7739
|
+
}
|
|
7740
|
+
var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
|
|
7741
|
+
var init_instance_credentials = __esm(() => {
|
|
7742
|
+
init_transport();
|
|
7743
|
+
});
|
|
7744
|
+
|
|
7750
7745
|
// src/lib/fleet-credentials.ts
|
|
7751
7746
|
var exports_fleet_credentials = {};
|
|
7752
7747
|
__export(exports_fleet_credentials, {
|
|
@@ -7754,6 +7749,7 @@ __export(exports_fleet_credentials, {
|
|
|
7754
7749
|
skillsCredentialFiles: () => skillsCredentialFiles,
|
|
7755
7750
|
skillsCredentialFilePath: () => skillsCredentialFilePath,
|
|
7756
7751
|
resolveSkillsFleet: () => resolveSkillsFleet,
|
|
7752
|
+
resolveSkillsConnection: () => resolveSkillsConnection,
|
|
7757
7753
|
resolveSkillsApiOrigin: () => resolveSkillsApiOrigin,
|
|
7758
7754
|
resolveSkillsApiKey: () => resolveSkillsApiKey,
|
|
7759
7755
|
resetLocalSkillsModeNotice: () => resetLocalSkillsModeNotice,
|
|
@@ -7771,9 +7767,6 @@ __export(exports_fleet_credentials, {
|
|
|
7771
7767
|
SKILLS_API_KEY_ENV: () => SKILLS_API_KEY_ENV,
|
|
7772
7768
|
MissingSkillsFleetError: () => MissingSkillsFleetError
|
|
7773
7769
|
});
|
|
7774
|
-
function isClientTransportConfigurationError(error2) {
|
|
7775
|
-
return error2 instanceof ClientTransportConfigurationError || typeof error2 === "object" && error2 !== null && error2.name === "ClientTransportConfigurationError";
|
|
7776
|
-
}
|
|
7777
7770
|
function isCredentialResolutionError(error2) {
|
|
7778
7771
|
return error2 instanceof CredentialResolutionError || typeof error2 === "object" && error2 !== null && error2.name === "CredentialResolutionError";
|
|
7779
7772
|
}
|
|
@@ -7784,6 +7777,9 @@ function asSkillsFleetCredentialError(error2) {
|
|
|
7784
7777
|
}
|
|
7785
7778
|
function normalizeSkillsApiOrigin(apiUrl) {
|
|
7786
7779
|
const url = new URL(apiUrl);
|
|
7780
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
7781
|
+
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
7782
|
+
}
|
|
7787
7783
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
7788
7784
|
if (pathname === "/api" || pathname === "/api/v1") {
|
|
7789
7785
|
url.pathname = "/";
|
|
@@ -7794,11 +7790,24 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
7794
7790
|
}
|
|
7795
7791
|
return url.toString().replace(/\/+$/, "");
|
|
7796
7792
|
}
|
|
7797
|
-
function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
if (value)
|
|
7801
|
-
|
|
7793
|
+
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
7794
|
+
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
7795
|
+
for (const entry of declared) {
|
|
7796
|
+
if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
|
|
7797
|
+
throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
|
|
7798
|
+
}
|
|
7799
|
+
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
|
|
7800
|
+
if (new Set(normalized.map((entry) => entry.value)).size > 1)
|
|
7801
|
+
throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
|
|
7802
|
+
if (normalized[0])
|
|
7803
|
+
return normalized[0];
|
|
7804
|
+
if (selectedSkillsProfile(env, profile)) {
|
|
7805
|
+
for (const file of skillsProfileCredentialFiles(env, profile)) {
|
|
7806
|
+
const metadata = readSkillsInstanceMetadata(file);
|
|
7807
|
+
if (metadata.apiUrl || metadata.binding)
|
|
7808
|
+
return { value: metadata.apiUrl ?? metadata.binding, source: file };
|
|
7809
|
+
}
|
|
7810
|
+
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
|
|
7802
7811
|
}
|
|
7803
7812
|
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
7804
7813
|
if (fromKeychain)
|
|
@@ -7812,7 +7821,7 @@ function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
|
7812
7821
|
return null;
|
|
7813
7822
|
}
|
|
7814
7823
|
function skillsCredentialFiles(env = process.env) {
|
|
7815
|
-
return
|
|
7824
|
+
return skillsProfileCredentialFiles(env);
|
|
7816
7825
|
}
|
|
7817
7826
|
function skillsCredentialFilePath(env = process.env) {
|
|
7818
7827
|
const paths = skillsCredentialFiles(env);
|
|
@@ -7833,7 +7842,11 @@ function resetLocalSkillsModeNotice() {
|
|
|
7833
7842
|
}
|
|
7834
7843
|
function resolveSkillsFleet(env = process.env, options = {}) {
|
|
7835
7844
|
try {
|
|
7836
|
-
|
|
7845
|
+
const snapshot = snapshotSkillsEnvironment(env);
|
|
7846
|
+
const resolved = resolveSkillsFleetOrThrow(snapshot, snapshotSkillsOptions(env, options));
|
|
7847
|
+
if (resolved.mode === "local" && env === process.env)
|
|
7848
|
+
noticeLocalSkillsMode();
|
|
7849
|
+
return resolved;
|
|
7837
7850
|
} catch (error2) {
|
|
7838
7851
|
const translated = asSkillsFleetCredentialError(error2);
|
|
7839
7852
|
if (translated)
|
|
@@ -7841,38 +7854,46 @@ function resolveSkillsFleet(env = process.env, options = {}) {
|
|
|
7841
7854
|
throw error2;
|
|
7842
7855
|
}
|
|
7843
7856
|
}
|
|
7844
|
-
function
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
const configured2 = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
7852
|
-
const credential2 = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
7853
|
-
if (!configured2 && !credential2) {
|
|
7854
|
-
if (env === process.env)
|
|
7855
|
-
noticeLocalSkillsMode();
|
|
7856
|
-
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
7857
|
-
}
|
|
7858
|
-
if (configured2 && !credential2) {
|
|
7859
|
-
throw new SkillsFleetCredentialError(`${configured2.source} points this CLI at a Skills service but no API key resolved \u2014 ` + `refusing to run locally instead. Looked in the Keychain item ` + `hasna.credentials.${SKILLS_APP}.api-key, then ${skillsCredentialFiles(env).join(" or ") || "no credentials file (no HOME)"}, ` + `then ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
7857
|
+
function snapshotSkillsEnvironment(env) {
|
|
7858
|
+
const snapshot = {};
|
|
7859
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(env))) {
|
|
7860
|
+
if (!("value" in descriptor)) {
|
|
7861
|
+
if (/^(?:HASNA_|SKILLS_|HOME$|USER$)/.test(key))
|
|
7862
|
+
throw new SkillsFleetCredentialError("Accessor-backed Skills configuration is unsupported");
|
|
7863
|
+
continue;
|
|
7860
7864
|
}
|
|
7861
|
-
|
|
7865
|
+
snapshot[key] = descriptor.value;
|
|
7862
7866
|
}
|
|
7863
|
-
|
|
7864
|
-
|
|
7867
|
+
return Object.freeze(snapshot);
|
|
7868
|
+
}
|
|
7869
|
+
function snapshotSkillsOptions(env, options) {
|
|
7870
|
+
if (env !== process.env)
|
|
7871
|
+
return options;
|
|
7872
|
+
return { ...options, credentials: { ...options.credentials, keychain: {
|
|
7873
|
+
...options.credentials?.keychain,
|
|
7874
|
+
enabled: options.credentials?.keychain?.enabled ?? true
|
|
7875
|
+
} } };
|
|
7876
|
+
}
|
|
7877
|
+
function resolveSkillsFleetOrThrow(env, options) {
|
|
7878
|
+
const assertFilesUnchanged = captureSkillsCredentialFiles(skillsProfileCredentialFiles(env, options.credentials?.profile));
|
|
7879
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
7865
7880
|
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
7866
7881
|
if (!credential) {
|
|
7867
|
-
|
|
7882
|
+
if (!configured)
|
|
7883
|
+
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
7884
|
+
throw new SkillsFleetCredentialError(`${configured.source} points this CLI at a Skills service but no API key resolved \u2014 refusing to run locally instead. ` + `Looked in hasna.credentials.skills.api-key, ${skillsCredentialFiles(env).join(" or ") || "no credentials file"}, and ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
7868
7885
|
}
|
|
7886
|
+
const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
|
|
7887
|
+
toV1BaseUrl(apiOrigin);
|
|
7888
|
+
assertCredentialInstance(credential, apiOrigin, env, options);
|
|
7889
|
+
assertFilesUnchanged();
|
|
7869
7890
|
const base = {
|
|
7870
7891
|
mode: "hosted",
|
|
7871
7892
|
apiOrigin,
|
|
7872
|
-
apiUrlSource:
|
|
7873
|
-
apiKeySource:
|
|
7874
|
-
apiKeyTier:
|
|
7875
|
-
warning:
|
|
7893
|
+
apiUrlSource: configured?.source ?? "default",
|
|
7894
|
+
apiKeySource: credential.source,
|
|
7895
|
+
apiKeyTier: credential.tier,
|
|
7896
|
+
warning: credential.warning
|
|
7876
7897
|
};
|
|
7877
7898
|
if (credential.tier === "pointer") {
|
|
7878
7899
|
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
@@ -7882,19 +7903,37 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
7882
7903
|
}
|
|
7883
7904
|
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
7884
7905
|
}
|
|
7906
|
+
function assertCredentialInstance(credential, apiOrigin, env, options) {
|
|
7907
|
+
let bound;
|
|
7908
|
+
if (credential.tier === "disk" || credential.tier === "profile") {
|
|
7909
|
+
const metadata = readSkillsInstanceMetadata(credential.source);
|
|
7910
|
+
bound = metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
7911
|
+
} else if (credential.tier === "keychain") {
|
|
7912
|
+
bound = keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
7913
|
+
}
|
|
7914
|
+
if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
|
|
7915
|
+
throw new SkillsFleetCredentialError("The selected Skills API does not match this credential's instance. Select its profile or sign in to the new instance; no credential was sent.", "INSTANCE_CREDENTIAL_MISMATCH");
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7885
7918
|
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
7886
|
-
|
|
7919
|
+
return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
|
|
7920
|
+
}
|
|
7921
|
+
async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
7922
|
+
const snapshotEnv = snapshotSkillsEnvironment(env);
|
|
7923
|
+
const fleet = resolveSkillsFleet(snapshotEnv, snapshotSkillsOptions(env, options));
|
|
7924
|
+
if (fleet.mode === "local" && env === process.env)
|
|
7925
|
+
noticeLocalSkillsMode();
|
|
7887
7926
|
if (fleet.mode !== "hosted")
|
|
7888
7927
|
return null;
|
|
7889
7928
|
if (fleet.apiKey)
|
|
7890
|
-
return fleet.apiKey;
|
|
7929
|
+
return { ...fleet, apiKey: fleet.apiKey };
|
|
7891
7930
|
const pointer = fleet.apiKeyPointer;
|
|
7892
7931
|
if (!pointer) {
|
|
7893
7932
|
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
|
|
7894
7933
|
}
|
|
7895
7934
|
let completed;
|
|
7896
7935
|
try {
|
|
7897
|
-
completed = await completePointerCredential(SKILLS_APP, pointer,
|
|
7936
|
+
completed = await completePointerCredential(SKILLS_APP, pointer, snapshotEnv);
|
|
7898
7937
|
} catch (error2) {
|
|
7899
7938
|
const translated = asSkillsFleetCredentialError(error2);
|
|
7900
7939
|
if (translated)
|
|
@@ -7904,7 +7943,7 @@ async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
|
7904
7943
|
if (!completed.apiKey?.trim()) {
|
|
7905
7944
|
throw new SkillsFleetCredentialError(`${credentialPointerEnvKey(SKILLS_APP)} names a vault item that produced an empty Skills API key \u2014 ` + `refusing to send an unauthenticated request.`);
|
|
7906
7945
|
}
|
|
7907
|
-
return completed.apiKey;
|
|
7946
|
+
return { ...fleet, apiKey: completed.apiKey, apiKeyPointer: null };
|
|
7908
7947
|
}
|
|
7909
7948
|
async function requireSkillsApiKey(action = "This command", env = process.env, options = {}) {
|
|
7910
7949
|
const apiKey = await resolveSkillsApiKey(env, options);
|
|
@@ -7912,22 +7951,19 @@ async function requireSkillsApiKey(action = "This command", env = process.env, o
|
|
|
7912
7951
|
throw new MissingSkillsFleetError(action);
|
|
7913
7952
|
return apiKey;
|
|
7914
7953
|
}
|
|
7915
|
-
function stripV1(baseUrl) {
|
|
7916
|
-
return baseUrl.replace(/\/v1$/, "").replace(/\/+$/, "");
|
|
7917
|
-
}
|
|
7918
7954
|
async function skillsCredentialOrReason(env = process.env, options = {}) {
|
|
7919
7955
|
try {
|
|
7920
|
-
const
|
|
7921
|
-
return
|
|
7956
|
+
const connection = await resolveSkillsConnection(env, options);
|
|
7957
|
+
return connection ? { apiKey: connection.apiKey, apiOrigin: connection.apiOrigin, reason: null } : { apiKey: null, apiOrigin: null, reason: null };
|
|
7922
7958
|
} catch (error2) {
|
|
7923
7959
|
if (error2 instanceof SkillsFleetCredentialError || error2?.name === "SkillsFleetCredentialError") {
|
|
7924
|
-
return { apiKey: null, reason: error2.message };
|
|
7960
|
+
return { apiKey: null, apiOrigin: null, reason: error2.message };
|
|
7925
7961
|
}
|
|
7926
7962
|
throw error2;
|
|
7927
7963
|
}
|
|
7928
7964
|
}
|
|
7929
7965
|
function resolveSkillsApiOrigin(env = process.env, options = {}) {
|
|
7930
|
-
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
7966
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
7931
7967
|
if (configured) {
|
|
7932
7968
|
toV1BaseUrl(configured.value);
|
|
7933
7969
|
return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
|
|
@@ -7950,6 +7986,7 @@ function requireSkillsFleet(action = "This command", env = process.env, options
|
|
|
7950
7986
|
var SKILLS_APP = "skills", ENV_KEYS, SKILLS_API_URL_ENV_KEYS, SKILLS_API_KEY_ENV_KEYS, SKILLS_API_URL_ENV, SKILLS_API_KEY_ENV, SkillsFleetCredentialError, localNoticePrinted = false, MissingSkillsFleetError;
|
|
7951
7987
|
var init_fleet_credentials = __esm(() => {
|
|
7952
7988
|
init_transport();
|
|
7989
|
+
init_instance_credentials();
|
|
7953
7990
|
ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
|
|
7954
7991
|
SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
|
|
7955
7992
|
SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
|
|
@@ -8102,15 +8139,181 @@ var init_blog_article = __esm(() => {
|
|
|
8102
8139
|
ARTICLE_LENGTHS = ["short", "medium", "long"];
|
|
8103
8140
|
});
|
|
8104
8141
|
|
|
8142
|
+
// src/lib/remote-files.ts
|
|
8143
|
+
var exports_remote_files = {};
|
|
8144
|
+
__export(exports_remote_files, {
|
|
8145
|
+
sha256: () => sha256,
|
|
8146
|
+
readBoundedResponse: () => readBoundedResponse,
|
|
8147
|
+
describeRemoteFiles: () => describeRemoteFiles,
|
|
8148
|
+
decodeRemoteFiles: () => decodeRemoteFiles,
|
|
8149
|
+
MAX_REMOTE_FILE_BYTES: () => MAX_REMOTE_FILE_BYTES
|
|
8150
|
+
});
|
|
8151
|
+
import { createHash as createHash3 } from "crypto";
|
|
8152
|
+
function describeRemoteFiles(files) {
|
|
8153
|
+
if (files.length > 10)
|
|
8154
|
+
throw new Error("At most 10 input files are supported");
|
|
8155
|
+
const names = new Set;
|
|
8156
|
+
let total = 0;
|
|
8157
|
+
return files.map((file) => {
|
|
8158
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(file.name) || file.name === "." || file.name === ".." || names.has(file.name))
|
|
8159
|
+
throw new Error("Input file names must be unique safe basenames");
|
|
8160
|
+
names.add(file.name);
|
|
8161
|
+
total += file.bytes.byteLength;
|
|
8162
|
+
if (file.bytes.byteLength > 20 * 1024 * 1024 || total > 50 * 1024 * 1024)
|
|
8163
|
+
throw new Error("Input files exceed the supported size limit");
|
|
8164
|
+
return { name: file.name, sizeBytes: file.bytes.byteLength, sha256: sha256(file.bytes), contentType: file.contentType ?? "application/octet-stream" };
|
|
8165
|
+
});
|
|
8166
|
+
}
|
|
8167
|
+
function sha256(bytes) {
|
|
8168
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
8169
|
+
}
|
|
8170
|
+
async function readBoundedResponse(response, maximum) {
|
|
8171
|
+
if (!Number.isSafeInteger(maximum) || maximum < 0 || maximum > MAX_REMOTE_FILE_BYTES)
|
|
8172
|
+
throw new Error("Invalid artifact size limit");
|
|
8173
|
+
const length = response.headers.get("content-length");
|
|
8174
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
8175
|
+
await response.body?.cancel();
|
|
8176
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
8177
|
+
}
|
|
8178
|
+
const reader = response.body?.getReader();
|
|
8179
|
+
if (!reader)
|
|
8180
|
+
return new Uint8Array;
|
|
8181
|
+
const chunks = [];
|
|
8182
|
+
let size = 0;
|
|
8183
|
+
try {
|
|
8184
|
+
while (true) {
|
|
8185
|
+
const next = await reader.read();
|
|
8186
|
+
if (next.done)
|
|
8187
|
+
break;
|
|
8188
|
+
size += next.value.byteLength;
|
|
8189
|
+
if (size > maximum)
|
|
8190
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
8191
|
+
chunks.push(next.value);
|
|
8192
|
+
}
|
|
8193
|
+
} catch (error2) {
|
|
8194
|
+
await reader.cancel().catch(() => {});
|
|
8195
|
+
throw error2;
|
|
8196
|
+
} finally {
|
|
8197
|
+
reader.releaseLock();
|
|
8198
|
+
}
|
|
8199
|
+
const bytes = new Uint8Array(size);
|
|
8200
|
+
let offset = 0;
|
|
8201
|
+
for (const chunk of chunks) {
|
|
8202
|
+
bytes.set(chunk, offset);
|
|
8203
|
+
offset += chunk.byteLength;
|
|
8204
|
+
}
|
|
8205
|
+
return bytes;
|
|
8206
|
+
}
|
|
8207
|
+
function decodeRemoteFiles(files) {
|
|
8208
|
+
let total = 0;
|
|
8209
|
+
const decoded = files.map((file) => {
|
|
8210
|
+
if (file.base64.length > 1398104 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.base64))
|
|
8211
|
+
throw new Error("Invalid inline input encoding");
|
|
8212
|
+
const bytes = Buffer.from(file.base64, "base64");
|
|
8213
|
+
total += bytes.byteLength;
|
|
8214
|
+
if (total > 1024 * 1024 || bytes.toString("base64") !== file.base64)
|
|
8215
|
+
throw new Error("Inline inputs must total at most 1 MiB");
|
|
8216
|
+
return { name: file.name, bytes, contentType: file.contentType };
|
|
8217
|
+
});
|
|
8218
|
+
describeRemoteFiles(decoded);
|
|
8219
|
+
return decoded;
|
|
8220
|
+
}
|
|
8221
|
+
var MAX_REMOTE_FILE_BYTES;
|
|
8222
|
+
var init_remote_files = __esm(() => {
|
|
8223
|
+
MAX_REMOTE_FILE_BYTES = 64 * 1024 * 1024;
|
|
8224
|
+
});
|
|
8225
|
+
|
|
8105
8226
|
// src/lib/auth-store.ts
|
|
8106
8227
|
function getApiUrl(action, env = process.env, options = {}) {
|
|
8107
8228
|
return requireSkillsApiOrigin(action, env, options);
|
|
8108
8229
|
}
|
|
8109
8230
|
var init_auth_store = __esm(() => {
|
|
8231
|
+
init_transport();
|
|
8232
|
+
init_instance_credentials();
|
|
8110
8233
|
init_fleet_credentials();
|
|
8234
|
+
init_transport();
|
|
8111
8235
|
init_fleet_credentials();
|
|
8112
8236
|
});
|
|
8113
8237
|
|
|
8238
|
+
// src/lib/remote-account.ts
|
|
8239
|
+
function creditCount(value) {
|
|
8240
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 2147483647) {
|
|
8241
|
+
throw new Error("The Skills server returned an invalid credit count");
|
|
8242
|
+
}
|
|
8243
|
+
return value;
|
|
8244
|
+
}
|
|
8245
|
+
function parseRemoteRunQuote(value) {
|
|
8246
|
+
const quote = object4(value);
|
|
8247
|
+
const pricing = object4(quote.pricing);
|
|
8248
|
+
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
8249
|
+
throw new Error("Invalid quoted skill");
|
|
8250
|
+
const costCents = creditCount(pricing.costCredits ?? pricing.costCents);
|
|
8251
|
+
if (pricing.costCredits !== undefined && pricing.costCents !== undefined && pricing.costCents !== costCents)
|
|
8252
|
+
throw new Error("Inconsistent quoted credit count");
|
|
8253
|
+
if (quote.availability && object4(quote.availability).status !== "available")
|
|
8254
|
+
throw new Error("This skill is unavailable for remote execution");
|
|
8255
|
+
return { ...quote, skill: quote.skill, pricing: { ...pricing, costCredits: costCents, costCents, formattedCost: `${costCents} credits` } };
|
|
8256
|
+
}
|
|
8257
|
+
function parseRemoteCreditPacks(value) {
|
|
8258
|
+
if (!Array.isArray(value))
|
|
8259
|
+
throw new Error("Invalid credit pack response");
|
|
8260
|
+
const ids = new Set;
|
|
8261
|
+
return value.map((value2) => {
|
|
8262
|
+
const row = object4(value2);
|
|
8263
|
+
const counts = [row.credits, row.creditsCents, row.amountCents].filter((value3) => value3 !== undefined).map(creditCount);
|
|
8264
|
+
if (!counts.length || counts[0] === 0 || counts.some((count) => count !== counts[0]))
|
|
8265
|
+
throw new Error("Inconsistent credit pack counts");
|
|
8266
|
+
const credits = counts[0];
|
|
8267
|
+
const id = row.id ?? `credits_${credits}`;
|
|
8268
|
+
if (typeof id !== "string" || !/^[a-z0-9][a-z0-9_-]{0,99}$/.test(id) || ids.has(id))
|
|
8269
|
+
throw new Error("Invalid credit pack ID");
|
|
8270
|
+
if (id.startsWith("credits_") && id !== `credits_${credits}`)
|
|
8271
|
+
throw new Error("Inconsistent credit pack ID");
|
|
8272
|
+
ids.add(id);
|
|
8273
|
+
return { id, credits, ...row.expiresInDays === undefined ? {} : { expiresInDays: creditCount(row.expiresInDays) } };
|
|
8274
|
+
});
|
|
8275
|
+
}
|
|
8276
|
+
function parseRemoteBillingStatus(value) {
|
|
8277
|
+
const row = object4(value);
|
|
8278
|
+
const counts = [row.creditBalance, row.balanceCents].filter((value2) => value2 !== undefined).map(creditCount);
|
|
8279
|
+
if (!counts.length || counts.some((count) => count !== counts[0]))
|
|
8280
|
+
throw new Error("Inconsistent credit balance");
|
|
8281
|
+
return {
|
|
8282
|
+
creditBalance: counts[0],
|
|
8283
|
+
formattedCreditBalance: `${counts[0]} credits`,
|
|
8284
|
+
...typeof row.plan === "string" ? { plan: row.plan } : {},
|
|
8285
|
+
...typeof row.hasPaymentMethod === "boolean" ? { hasPaymentMethod: row.hasPaymentMethod } : {}
|
|
8286
|
+
};
|
|
8287
|
+
}
|
|
8288
|
+
function parseRemoteCheckout(value) {
|
|
8289
|
+
const row = object4(value);
|
|
8290
|
+
if (typeof row.url !== "string")
|
|
8291
|
+
throw new Error("Invalid checkout URL");
|
|
8292
|
+
const url = new URL(row.url);
|
|
8293
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
8294
|
+
throw new Error("Invalid checkout URL");
|
|
8295
|
+
return { url: row.url };
|
|
8296
|
+
}
|
|
8297
|
+
function object4(value) {
|
|
8298
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8299
|
+
throw new Error("Invalid Skills server response");
|
|
8300
|
+
return value;
|
|
8301
|
+
}
|
|
8302
|
+
var RemoteCreditApprovalError;
|
|
8303
|
+
var init_remote_account = __esm(() => {
|
|
8304
|
+
RemoteCreditApprovalError = class RemoteCreditApprovalError extends Error {
|
|
8305
|
+
requiredCredits;
|
|
8306
|
+
maximumCredits;
|
|
8307
|
+
code = "CREDIT_APPROVAL_REQUIRED";
|
|
8308
|
+
constructor(requiredCredits, maximumCredits) {
|
|
8309
|
+
super(`This run requires ${requiredCredits} credits; the approved maximum is ${maximumCredits}. Quote the run and explicitly approve its cost.`);
|
|
8310
|
+
this.requiredCredits = requiredCredits;
|
|
8311
|
+
this.maximumCredits = maximumCredits;
|
|
8312
|
+
this.name = "RemoteCreditApprovalError";
|
|
8313
|
+
}
|
|
8314
|
+
};
|
|
8315
|
+
});
|
|
8316
|
+
|
|
8114
8317
|
// src/lib/remote-client.ts
|
|
8115
8318
|
var exports_remote_client = {};
|
|
8116
8319
|
__export(exports_remote_client, {
|
|
@@ -8124,13 +8327,16 @@ __export(exports_remote_client, {
|
|
|
8124
8327
|
class RemoteSkillsClient {
|
|
8125
8328
|
apiUrl;
|
|
8126
8329
|
apiKey;
|
|
8330
|
+
capabilities;
|
|
8127
8331
|
constructor(apiKey, apiUrl = getApiUrl()) {
|
|
8128
8332
|
this.apiKey = apiKey;
|
|
8129
|
-
this.apiUrl = apiUrl
|
|
8333
|
+
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
8130
8334
|
}
|
|
8131
8335
|
async request(path, options) {
|
|
8132
8336
|
return fetch(`${this.apiUrl}${path}`, {
|
|
8133
8337
|
...options,
|
|
8338
|
+
redirect: "error",
|
|
8339
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
8134
8340
|
headers: {
|
|
8135
8341
|
Authorization: `Bearer ${this.apiKey}`,
|
|
8136
8342
|
"Content-Type": "application/json",
|
|
@@ -8153,8 +8359,7 @@ class RemoteSkillsClient {
|
|
|
8153
8359
|
return response;
|
|
8154
8360
|
}
|
|
8155
8361
|
async listSkills() {
|
|
8156
|
-
|
|
8157
|
-
return res.json();
|
|
8362
|
+
return this.arrayResponse("/api/v1/skills");
|
|
8158
8363
|
}
|
|
8159
8364
|
async getSkillMd(slug) {
|
|
8160
8365
|
const res = await this.request(`/api/v1/skills/${slug}/skill.md`);
|
|
@@ -8176,39 +8381,217 @@ class RemoteSkillsClient {
|
|
|
8176
8381
|
} catch {}
|
|
8177
8382
|
return { status: res.status, body };
|
|
8178
8383
|
}
|
|
8179
|
-
async submitRun(slug, input, args) {
|
|
8180
|
-
|
|
8384
|
+
async submitRun(slug, input, args, approval = {}) {
|
|
8385
|
+
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
8386
|
+
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
8387
|
+
if (approval.maxCostCents !== undefined)
|
|
8388
|
+
creditCount(approval.maxCostCents);
|
|
8389
|
+
if (approval.maxCredits !== undefined)
|
|
8390
|
+
creditCount(approval.maxCredits);
|
|
8391
|
+
if (approval.maxCredits !== undefined && approval.maxCostCents !== undefined && approval.maxCredits !== approval.maxCostCents)
|
|
8392
|
+
throw new Error("Credit approval fields disagree");
|
|
8393
|
+
const res = await this.request(`/api/v1/runs/${encodeURIComponent(slug)}`, {
|
|
8181
8394
|
method: "POST",
|
|
8182
|
-
body: JSON.stringify({
|
|
8395
|
+
body: JSON.stringify({
|
|
8396
|
+
input,
|
|
8397
|
+
args,
|
|
8398
|
+
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
8399
|
+
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
8400
|
+
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
8401
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
8402
|
+
})
|
|
8183
8403
|
});
|
|
8184
8404
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
8185
8405
|
}
|
|
8406
|
+
async quoteRun(slug, input = {}, args = []) {
|
|
8407
|
+
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
8408
|
+
method: "POST",
|
|
8409
|
+
body: JSON.stringify({ input, args })
|
|
8410
|
+
});
|
|
8411
|
+
return parseRemoteRunQuote(await response.json());
|
|
8412
|
+
}
|
|
8413
|
+
getCapabilities() {
|
|
8414
|
+
if (!this.capabilities)
|
|
8415
|
+
this.capabilities = (async () => {
|
|
8416
|
+
const value = await (await this.requestNewRoute("/api/v1/capabilities")).json();
|
|
8417
|
+
if (value.contractVersion !== 1 || value.apiVersion !== 1 || !Array.isArray(value.capabilities) || value.capabilities.some((item) => typeof item !== "string"))
|
|
8418
|
+
throw new Error("Unsupported Skills server capability contract");
|
|
8419
|
+
const billing = value.billing;
|
|
8420
|
+
return { contractVersion: 1, apiVersion: 1, capabilities: value.capabilities, ...billing ? { billing } : {} };
|
|
8421
|
+
})();
|
|
8422
|
+
return this.capabilities;
|
|
8423
|
+
}
|
|
8424
|
+
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
8425
|
+
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
8426
|
+
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
8427
|
+
throw new Error("Credit approval fields disagree");
|
|
8428
|
+
const quote = await this.quoteRun(slug, input, args);
|
|
8429
|
+
if (quote.pricing.costCents > maximum)
|
|
8430
|
+
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
8431
|
+
const capabilities = await this.getCapabilities();
|
|
8432
|
+
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
8433
|
+
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
8434
|
+
}
|
|
8435
|
+
return this.submitRun(quote.skill, input, args, { ...approval, maxCredits: maximum, maxCostCents: maximum });
|
|
8436
|
+
}
|
|
8437
|
+
async getIdentity() {
|
|
8438
|
+
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
8439
|
+
}
|
|
8440
|
+
async listApiKeys() {
|
|
8441
|
+
return this.arrayResponse("/api/auth/keys");
|
|
8442
|
+
}
|
|
8443
|
+
async createApiKey(name, scopes) {
|
|
8444
|
+
if (!name.trim() || name.length > 100)
|
|
8445
|
+
throw new Error("API key name must be 1-100 characters");
|
|
8446
|
+
const value = await (await this.requestNewRoute("/api/auth/keys", { method: "POST", body: JSON.stringify({ name, ...scopes ? { scopes } : {} }) })).json();
|
|
8447
|
+
if (!value || typeof value.key !== "string" || !value.key.trim())
|
|
8448
|
+
throw new Error("The server did not return a created API key");
|
|
8449
|
+
return value;
|
|
8450
|
+
}
|
|
8451
|
+
async revokeApiKey(keyId) {
|
|
8452
|
+
return (await this.requestNewRoute(`/api/auth/keys/${encodeURIComponent(keyId)}`, { method: "DELETE" })).json();
|
|
8453
|
+
}
|
|
8454
|
+
async getBillingStatus() {
|
|
8455
|
+
return parseRemoteBillingStatus(await (await this.requestNewRoute("/api/v1/billing/status")).json());
|
|
8456
|
+
}
|
|
8457
|
+
async listCreditPacks() {
|
|
8458
|
+
return parseRemoteCreditPacks(await (await this.requestNewRoute("/api/v1/billing/credits")).json());
|
|
8459
|
+
}
|
|
8460
|
+
async createCreditCheckout(packId) {
|
|
8461
|
+
const packs = await this.listCreditPacks();
|
|
8462
|
+
if (!packs.some((pack) => pack.id === packId))
|
|
8463
|
+
throw new Error("Choose a credit pack returned by skills credits packs");
|
|
8464
|
+
return parseRemoteCheckout(await (await this.requestNewRoute("/api/v1/billing/credits", {
|
|
8465
|
+
method: "POST",
|
|
8466
|
+
body: JSON.stringify({ packId })
|
|
8467
|
+
})).json());
|
|
8468
|
+
}
|
|
8469
|
+
async getUsage() {
|
|
8470
|
+
return this.arrayResponse("/api/v1/billing/usage");
|
|
8471
|
+
}
|
|
8472
|
+
async listInvoices() {
|
|
8473
|
+
return this.arrayResponse("/api/v1/billing/invoices");
|
|
8474
|
+
}
|
|
8475
|
+
async createBillingCheckout() {
|
|
8476
|
+
return this.checkoutResponse("/api/v1/billing/checkout");
|
|
8477
|
+
}
|
|
8478
|
+
async createBillingPortal() {
|
|
8479
|
+
return this.checkoutResponse("/api/v1/billing/portal");
|
|
8480
|
+
}
|
|
8481
|
+
async cancelRun(runId) {
|
|
8482
|
+
return this.controlRun(runId, "cancel");
|
|
8483
|
+
}
|
|
8484
|
+
async resumeRun(runId) {
|
|
8485
|
+
return this.controlRun(runId, "resume");
|
|
8486
|
+
}
|
|
8487
|
+
async controlRun(runId, action) {
|
|
8488
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/${action}`, { method: "POST", body: "{}" });
|
|
8489
|
+
return normalizeRemoteSkillRunContract(await response.json());
|
|
8490
|
+
}
|
|
8491
|
+
async checkoutResponse(path) {
|
|
8492
|
+
return parseRemoteCheckout(await (await this.requestNewRoute(path, { method: "POST", body: "{}" })).json());
|
|
8493
|
+
}
|
|
8494
|
+
async arrayResponse(path) {
|
|
8495
|
+
const rows = await (await this.requestNewRoute(path)).json();
|
|
8496
|
+
if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
|
|
8497
|
+
throw new Error("Invalid Skills server list response");
|
|
8498
|
+
return rows;
|
|
8499
|
+
}
|
|
8186
8500
|
async getRun(runId) {
|
|
8187
|
-
const
|
|
8188
|
-
|
|
8501
|
+
const path = `/api/v1/runs/${encodeURIComponent(runId)}`;
|
|
8502
|
+
const res = await this.request(path);
|
|
8503
|
+
if (res.status === 404)
|
|
8189
8504
|
return null;
|
|
8505
|
+
if (!res.ok)
|
|
8506
|
+
throw new RemoteRequestError(path, res.status, res.statusText);
|
|
8190
8507
|
return normalizeRemoteSkillRunContract(await res.json());
|
|
8191
8508
|
}
|
|
8192
8509
|
async getRunLogs(runId) {
|
|
8193
|
-
|
|
8194
|
-
if (!res.ok)
|
|
8195
|
-
return [];
|
|
8196
|
-
const payload = await res.json();
|
|
8197
|
-
return Array.isArray(payload) ? payload : [];
|
|
8510
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/logs`);
|
|
8198
8511
|
}
|
|
8199
8512
|
async listRuns(limit = 20) {
|
|
8200
|
-
|
|
8201
|
-
|
|
8513
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
8514
|
+
throw new Error("Run limit must be an integer from 1 to 100");
|
|
8515
|
+
return this.arrayResponse(`/api/v1/runs?limit=${limit}`);
|
|
8202
8516
|
}
|
|
8203
8517
|
async getRunArtifacts(runId) {
|
|
8204
|
-
|
|
8205
|
-
return res.json();
|
|
8518
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts`);
|
|
8206
8519
|
}
|
|
8207
8520
|
async downloadRunArtifact(runId, artifactId) {
|
|
8208
|
-
return this.request(`/api/v1/runs/${runId}/artifacts/${artifactId}/download`, {
|
|
8521
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
8209
8522
|
method: "GET"
|
|
8210
8523
|
});
|
|
8211
8524
|
}
|
|
8525
|
+
async getVerifiedRunArtifact(runId, artifactId, maximumBytes = MAX_REMOTE_FILE_BYTES) {
|
|
8526
|
+
const artifacts = await this.getRunArtifacts(runId);
|
|
8527
|
+
const artifact = artifacts.find((row) => row.id === artifactId);
|
|
8528
|
+
if (!artifact)
|
|
8529
|
+
throw new Error("Run artifact not found");
|
|
8530
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0 || artifact.byteSize > maximumBytes || typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256))
|
|
8531
|
+
throw new Error("The server does not provide valid artifact integrity metadata");
|
|
8532
|
+
const response = await this.downloadRunArtifact(runId, artifactId);
|
|
8533
|
+
if (!response.ok)
|
|
8534
|
+
throw new RemoteRequestError("artifact download", response.status, response.statusText);
|
|
8535
|
+
const bytes = await readBoundedResponse(response, artifact.byteSize);
|
|
8536
|
+
if (bytes.byteLength !== artifact.byteSize || sha256(bytes) !== artifact.sha256)
|
|
8537
|
+
throw new Error("Artifact integrity verification failed");
|
|
8538
|
+
return { id: artifactId, fileName: String(artifact.fileName ?? artifactId), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
8539
|
+
}
|
|
8540
|
+
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
8541
|
+
const inputFiles = describeRemoteFiles(files);
|
|
8542
|
+
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
8543
|
+
throw new Error("The configured server does not support input uploads");
|
|
8544
|
+
const run = await this.submitQuotedRun(slug, input, args, { ...approval, inputFiles });
|
|
8545
|
+
if (run.error || !run.id || !files.length)
|
|
8546
|
+
return run;
|
|
8547
|
+
const pastUploads = (status) => typeof status === "string" && [
|
|
8548
|
+
"running",
|
|
8549
|
+
"completed",
|
|
8550
|
+
"failed",
|
|
8551
|
+
"cancelled",
|
|
8552
|
+
"expired",
|
|
8553
|
+
"pending_approval",
|
|
8554
|
+
"approved",
|
|
8555
|
+
"waiting"
|
|
8556
|
+
].includes(status);
|
|
8557
|
+
if (pastUploads(run.status))
|
|
8558
|
+
return run;
|
|
8559
|
+
try {
|
|
8560
|
+
await this.uploadRunFiles(run.id, files);
|
|
8561
|
+
} catch {
|
|
8562
|
+
try {
|
|
8563
|
+
const current = await this.getRun(run.id);
|
|
8564
|
+
if (current && pastUploads(current.status))
|
|
8565
|
+
return current;
|
|
8566
|
+
} catch {}
|
|
8567
|
+
let cancellationRequested = false;
|
|
8568
|
+
try {
|
|
8569
|
+
await this.cancelRun(run.id);
|
|
8570
|
+
cancellationRequested = true;
|
|
8571
|
+
} catch {}
|
|
8572
|
+
throw new Error(`Input upload failed for run ${run.id}; ${cancellationRequested ? "cancellation requested" : "check its status and cancel the run"}`);
|
|
8573
|
+
}
|
|
8574
|
+
return run;
|
|
8575
|
+
}
|
|
8576
|
+
async uploadRunFiles(runId, files) {
|
|
8577
|
+
const descriptors = describeRemoteFiles(files);
|
|
8578
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/uploads`, { method: "POST", body: JSON.stringify({ files: descriptors }) });
|
|
8579
|
+
const payload = await response.json();
|
|
8580
|
+
if (!Array.isArray(payload.files) || payload.files.length !== files.length || new Set(payload.files.map((file) => file.name)).size !== files.length)
|
|
8581
|
+
throw new Error("Invalid input upload response");
|
|
8582
|
+
for (const file of files) {
|
|
8583
|
+
const upload = payload.files.find((row) => row.name === file.name);
|
|
8584
|
+
if (!upload)
|
|
8585
|
+
throw new Error("Missing input upload URL");
|
|
8586
|
+
const url = new URL(upload.uploadUrl);
|
|
8587
|
+
if (url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
8588
|
+
throw new Error("Unsafe input upload URL");
|
|
8589
|
+
const uploaded = await fetch(url, { method: "PUT", body: file.bytes, headers: { "Content-Type": file.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
|
|
8590
|
+
if (!uploaded.ok)
|
|
8591
|
+
throw new Error("Input upload failed");
|
|
8592
|
+
await uploaded.body?.cancel();
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8212
8595
|
async publishSkill(manifest, bundle, ifMatch) {
|
|
8213
8596
|
const form = new FormData;
|
|
8214
8597
|
form.set("manifest", JSON.stringify(manifest));
|
|
@@ -8221,7 +8604,9 @@ class RemoteSkillsClient {
|
|
|
8221
8604
|
return fetch(`${this.apiUrl}/api/v1/skills`, {
|
|
8222
8605
|
method: "POST",
|
|
8223
8606
|
headers,
|
|
8224
|
-
body: form
|
|
8607
|
+
body: form,
|
|
8608
|
+
redirect: "error",
|
|
8609
|
+
signal: AbortSignal.timeout(15000)
|
|
8225
8610
|
});
|
|
8226
8611
|
}
|
|
8227
8612
|
async deleteSkill(slug) {
|
|
@@ -8277,12 +8662,13 @@ class RemoteSkillsClient {
|
|
|
8277
8662
|
if (!Array.isArray(payload)) {
|
|
8278
8663
|
throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
|
|
8279
8664
|
}
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8665
|
+
const isName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
8666
|
+
if (payload.every(isName))
|
|
8667
|
+
return payload;
|
|
8668
|
+
if (payload.every((tag) => tag !== null && typeof tag === "object" && !Array.isArray(tag) && isName(tag.name) && Number.isSafeInteger(tag.count) && tag.count >= 0)) {
|
|
8669
|
+
return payload.map((tag) => tag.name);
|
|
8284
8670
|
}
|
|
8285
|
-
|
|
8671
|
+
throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name, or every element must be a counted tag record)");
|
|
8286
8672
|
}
|
|
8287
8673
|
async skillsByTag(tag) {
|
|
8288
8674
|
const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
|
|
@@ -8385,14 +8771,8 @@ function normalizeUpdatedSincePage(payload) {
|
|
|
8385
8771
|
return { skills, nextCursor };
|
|
8386
8772
|
}
|
|
8387
8773
|
async function createRemoteSkillsClient(env = process.env) {
|
|
8388
|
-
const
|
|
8389
|
-
|
|
8390
|
-
return null;
|
|
8391
|
-
const apiKey = await resolveSkillsApiKey(env);
|
|
8392
|
-
if (!apiKey) {
|
|
8393
|
-
throw new Error("A Skills authority resolved but no API key did. Sign in with: skills auth login");
|
|
8394
|
-
}
|
|
8395
|
-
return new RemoteSkillsClient(apiKey, fleet.apiOrigin);
|
|
8774
|
+
const connection = await resolveSkillsConnection(env);
|
|
8775
|
+
return connection ? new RemoteSkillsClient(connection.apiKey, connection.apiOrigin) : null;
|
|
8396
8776
|
}
|
|
8397
8777
|
function createRemoteSkillsClientReadOnly(env = process.env) {
|
|
8398
8778
|
return createRemoteSkillsClient(env);
|
|
@@ -8401,6 +8781,8 @@ var RemoteRouteUnsupportedError, RemoteRequestError;
|
|
|
8401
8781
|
var init_remote_client = __esm(() => {
|
|
8402
8782
|
init_auth_store();
|
|
8403
8783
|
init_fleet_credentials();
|
|
8784
|
+
init_remote_account();
|
|
8785
|
+
init_remote_files();
|
|
8404
8786
|
RemoteRouteUnsupportedError = class RemoteRouteUnsupportedError extends Error {
|
|
8405
8787
|
path;
|
|
8406
8788
|
status;
|
|
@@ -13941,7 +14323,7 @@ class StdioServerTransport {
|
|
|
13941
14323
|
// package.json
|
|
13942
14324
|
var package_default = {
|
|
13943
14325
|
name: "@hasna/skills",
|
|
13944
|
-
version: "0.
|
|
14326
|
+
version: "0.4.0",
|
|
13945
14327
|
description: "Skills library for AI coding agents",
|
|
13946
14328
|
type: "module",
|
|
13947
14329
|
bin: {
|
|
@@ -14000,8 +14382,7 @@ var package_default = {
|
|
|
14000
14382
|
"verify:release": "bun run scripts/release-guard.ts",
|
|
14001
14383
|
prepare: "bun run build:js",
|
|
14002
14384
|
prepack: "bun run build && bun run verify:release",
|
|
14003
|
-
prepublishOnly: "bun run typecheck && bun run test"
|
|
14004
|
-
postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
|
|
14385
|
+
prepublishOnly: "bun run typecheck && bun run test"
|
|
14005
14386
|
},
|
|
14006
14387
|
keywords: [
|
|
14007
14388
|
"skills",
|
|
@@ -22993,6 +23374,16 @@ function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDecl
|
|
|
22993
23374
|
return true;
|
|
22994
23375
|
return false;
|
|
22995
23376
|
}
|
|
23377
|
+
function frontmatterString(raw) {
|
|
23378
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
23379
|
+
try {
|
|
23380
|
+
const decoded = JSON.parse(raw);
|
|
23381
|
+
if (typeof decoded === "string")
|
|
23382
|
+
return decoded;
|
|
23383
|
+
} catch {}
|
|
23384
|
+
}
|
|
23385
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
23386
|
+
}
|
|
22996
23387
|
function parseSkillFrontmatter(content) {
|
|
22997
23388
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
22998
23389
|
if (!match)
|
|
@@ -23012,12 +23403,12 @@ function parseSkillFrontmatter(content) {
|
|
|
23012
23403
|
const tags = [];
|
|
23013
23404
|
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
|
|
23014
23405
|
i++;
|
|
23015
|
-
tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
|
|
23406
|
+
tags.push(frontmatterString(lines[i].replace(/^\s+-\s+/, "").trim()));
|
|
23016
23407
|
}
|
|
23017
23408
|
result.tags = tags;
|
|
23018
23409
|
continue;
|
|
23019
23410
|
}
|
|
23020
|
-
const value = rawValue
|
|
23411
|
+
const value = frontmatterString(rawValue);
|
|
23021
23412
|
if (!value)
|
|
23022
23413
|
continue;
|
|
23023
23414
|
if (key === "name")
|
|
@@ -23626,8 +24017,8 @@ function createInstructionManifest(name, options) {
|
|
|
23626
24017
|
description: options.description,
|
|
23627
24018
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
23628
24019
|
displayName: displayName(name),
|
|
23629
|
-
category: "Development Tools",
|
|
23630
|
-
tags: ["custom", name],
|
|
24020
|
+
category: options.category ?? "Development Tools",
|
|
24021
|
+
tags: options.tags ?? ["custom", name],
|
|
23631
24022
|
kind: "instruction",
|
|
23632
24023
|
inputs: [],
|
|
23633
24024
|
commands: [],
|
|
@@ -23642,16 +24033,16 @@ function writeInstructionSkillTemplate(skillPath, manifest) {
|
|
|
23642
24033
|
}
|
|
23643
24034
|
function renderInstructionSkillMd(manifest) {
|
|
23644
24035
|
const tags = manifest.tags?.length ? `tags:
|
|
23645
|
-
${manifest.tags.map((tag) => ` - ${tag}`).join(`
|
|
24036
|
+
${manifest.tags.map((tag) => ` - ${yamlString(tag)}`).join(`
|
|
23646
24037
|
`)}
|
|
23647
24038
|
` : "";
|
|
23648
24039
|
return `---
|
|
23649
24040
|
name: ${manifest.name}
|
|
23650
|
-
description: ${manifest.description}
|
|
24041
|
+
description: ${yamlString(manifest.description)}
|
|
23651
24042
|
kind: instruction
|
|
23652
24043
|
version: ${manifest.version}
|
|
23653
24044
|
source: custom
|
|
23654
|
-
category: ${manifest.category ?? "Development Tools"}
|
|
24045
|
+
category: ${yamlString(manifest.category ?? "Development Tools")}
|
|
23655
24046
|
${tags}---
|
|
23656
24047
|
|
|
23657
24048
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -23672,8 +24063,8 @@ function createPortableManifest(name, options) {
|
|
|
23672
24063
|
description: options.description,
|
|
23673
24064
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
23674
24065
|
displayName: displayName(name),
|
|
23675
|
-
category: "Development Tools",
|
|
23676
|
-
tags: ["custom", name],
|
|
24066
|
+
category: options.category ?? "Development Tools",
|
|
24067
|
+
tags: options.tags ?? ["custom", name],
|
|
23677
24068
|
inputs: DEFAULT_INPUTS,
|
|
23678
24069
|
commands: [{
|
|
23679
24070
|
name,
|
|
@@ -23864,10 +24255,13 @@ function isExcludedCopyEntry(name, isFirstSegment) {
|
|
|
23864
24255
|
return true;
|
|
23865
24256
|
return false;
|
|
23866
24257
|
}
|
|
24258
|
+
function yamlString(value) {
|
|
24259
|
+
return JSON.stringify(value);
|
|
24260
|
+
}
|
|
23867
24261
|
function renderSkillMd(manifest) {
|
|
23868
24262
|
return `---
|
|
23869
24263
|
name: ${manifest.name}
|
|
23870
|
-
description: ${manifest.description}
|
|
24264
|
+
description: ${yamlString(manifest.description)}
|
|
23871
24265
|
---
|
|
23872
24266
|
|
|
23873
24267
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -24241,11 +24635,11 @@ function scaffoldPortableSkill(name, options = {}) {
|
|
|
24241
24635
|
const kind = options.kind ?? "executable";
|
|
24242
24636
|
const description = options.description ?? `${displayName(skillName)} skill`;
|
|
24243
24637
|
if (kind === "instruction") {
|
|
24244
|
-
const manifest2 = createInstructionManifest(skillName, { description });
|
|
24638
|
+
const manifest2 = createInstructionManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
24245
24639
|
writeInstructionSkillTemplate(skillPath, manifest2);
|
|
24246
24640
|
return { name: skillName, path: skillPath, manifest: manifest2, created: true };
|
|
24247
24641
|
}
|
|
24248
|
-
const manifest = createPortableManifest(skillName, { description });
|
|
24642
|
+
const manifest = createPortableManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
24249
24643
|
writePortableSkillTemplate(skillPath, manifest);
|
|
24250
24644
|
return { name: skillName, path: skillPath, manifest, created: true };
|
|
24251
24645
|
}
|
|
@@ -25265,6 +25659,24 @@ function readIfExists(path) {
|
|
|
25265
25659
|
return null;
|
|
25266
25660
|
}
|
|
25267
25661
|
|
|
25662
|
+
// src/lib/remote-customer-operations.ts
|
|
25663
|
+
var REMOTE_CUSTOMER_OPERATIONS = [
|
|
25664
|
+
{ name: "get_account", title: "Get Account Identity", parameter: null, mutates: false, invoke: (client) => client.getIdentity() },
|
|
25665
|
+
{ name: "get_server_capabilities", title: "Get Server Capabilities", parameter: null, mutates: false, invoke: (client) => client.getCapabilities() },
|
|
25666
|
+
{ name: "list_remote_skills", title: "List Remote Skills", parameter: null, mutates: false, invoke: (client) => client.listSkills() },
|
|
25667
|
+
{ name: "get_billing_status", title: "Get Billing Status", parameter: null, mutates: false, invoke: (client) => client.getBillingStatus() },
|
|
25668
|
+
{ name: "list_credit_packs", title: "List Credit Packs", parameter: null, mutates: false, invoke: (client) => client.listCreditPacks() },
|
|
25669
|
+
{ name: "create_credit_checkout", title: "Create Credit Checkout", parameter: "pack_id", mutates: true, invoke: (client, value) => client.createCreditCheckout(value) },
|
|
25670
|
+
{ name: "get_billing_usage", title: "Get Billing Usage", parameter: null, mutates: false, invoke: (client) => client.getUsage() },
|
|
25671
|
+
{ name: "list_invoices", title: "List Invoices", parameter: null, mutates: false, invoke: (client) => client.listInvoices() },
|
|
25672
|
+
{ name: "create_billing_checkout", title: "Create Billing Checkout", parameter: null, mutates: true, invoke: (client) => client.createBillingCheckout() },
|
|
25673
|
+
{ name: "create_billing_portal", title: "Create Billing Portal", parameter: null, mutates: true, invoke: (client) => client.createBillingPortal() },
|
|
25674
|
+
{ name: "get_run_logs", title: "Get Run Logs", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunLogs(value) },
|
|
25675
|
+
{ name: "cancel_run", title: "Cancel Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.cancelRun(value) },
|
|
25676
|
+
{ name: "resume_run", title: "Resume Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.resumeRun(value) },
|
|
25677
|
+
{ name: "list_run_artifacts", title: "List Run Artifacts", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunArtifacts(value) }
|
|
25678
|
+
];
|
|
25679
|
+
|
|
25268
25680
|
// src/lib/mcp-contracts.ts
|
|
25269
25681
|
var MCP_CONTRACT_SCHEMA_VERSION = 1;
|
|
25270
25682
|
var stringSchema = (description) => ({
|
|
@@ -25656,11 +26068,44 @@ var toolContracts = [
|
|
|
25656
26068
|
dependencies: objectSchema({}, [], "Package dependencies.", true)
|
|
25657
26069
|
})
|
|
25658
26070
|
},
|
|
26071
|
+
{
|
|
26072
|
+
name: "list_api_keys",
|
|
26073
|
+
title: "List API Keys",
|
|
26074
|
+
description: "List keys using fresh email OTP reauthentication.",
|
|
26075
|
+
params: ["email", "code"],
|
|
26076
|
+
category: "execution",
|
|
26077
|
+
sideEffects: "local-process-or-remote-run",
|
|
26078
|
+
stable: true,
|
|
26079
|
+
inputSchema: objectSchema({ email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["email", "code"]),
|
|
26080
|
+
outputSchema: arraySchema(objectSchema({}, [], "API key metadata", true))
|
|
26081
|
+
},
|
|
26082
|
+
{
|
|
26083
|
+
name: "revoke_api_key",
|
|
26084
|
+
title: "Revoke API Key",
|
|
26085
|
+
description: "Revoke a key using fresh email OTP reauthentication.",
|
|
26086
|
+
params: ["key_id", "email", "code"],
|
|
26087
|
+
category: "execution",
|
|
26088
|
+
sideEffects: "local-process-or-remote-run",
|
|
26089
|
+
stable: true,
|
|
26090
|
+
inputSchema: objectSchema({ key_id: stringSchema("API key ID"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["key_id", "email", "code"]),
|
|
26091
|
+
outputSchema: objectSchema({}, [], "Revocation result", true)
|
|
26092
|
+
},
|
|
26093
|
+
{
|
|
26094
|
+
name: "create_api_key",
|
|
26095
|
+
title: "Create API Key",
|
|
26096
|
+
description: "Create a key with fresh email OTP reauthentication; returns the secret once.",
|
|
26097
|
+
params: ["name", "email", "code", "scopes?"],
|
|
26098
|
+
category: "execution",
|
|
26099
|
+
sideEffects: "local-process-or-remote-run",
|
|
26100
|
+
stable: true,
|
|
26101
|
+
inputSchema: objectSchema({ name: stringSchema("Key name"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" }, scopes: arraySchema(stringSchema("Scope")) }, ["name", "email", "code"]),
|
|
26102
|
+
outputSchema: objectSchema({}, [], "Created key and one-time secret", true)
|
|
26103
|
+
},
|
|
25659
26104
|
{
|
|
25660
26105
|
name: "run_skill",
|
|
25661
26106
|
title: "Run Skill",
|
|
25662
26107
|
description: "Run a skill locally or through a configured remote runner. Returns compact stdout/stderr previews and run summaries by default; pass detail:true for full records.",
|
|
25663
|
-
params: ["name", "input?", "args?", "detail?"],
|
|
26108
|
+
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "idempotency_key?", "files?"],
|
|
25664
26109
|
category: "execution",
|
|
25665
26110
|
sideEffects: "local-process-or-remote-run",
|
|
25666
26111
|
stable: true,
|
|
@@ -25668,7 +26113,12 @@ var toolContracts = [
|
|
|
25668
26113
|
name: skillNameInput,
|
|
25669
26114
|
input: runInputSchema,
|
|
25670
26115
|
args: runArgsSchema,
|
|
25671
|
-
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." }
|
|
26116
|
+
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." },
|
|
26117
|
+
remote: { type: "boolean", description: "Use the configured server catalog." },
|
|
26118
|
+
maxCredits: { type: "integer", minimum: 0, description: "Maximum explicitly approved integer credits; omitted permits only free remote runs." },
|
|
26119
|
+
maxCostCents: { type: "integer", minimum: 0, description: "Legacy alias for maxCredits; both must agree." },
|
|
26120
|
+
idempotency_key: { type: "string", pattern: "^[A-Za-z0-9._:-]{1,128}$", description: "Stable retry key for the same remote submission." },
|
|
26121
|
+
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Inline remote inputs, at most 1 MiB combined." }
|
|
25672
26122
|
}, ["name"]),
|
|
25673
26123
|
outputSchema: runOutputSchema
|
|
25674
26124
|
},
|
|
@@ -25935,7 +26385,40 @@ var toolContracts = [
|
|
|
25935
26385
|
outputSchema: objectSchema({}, [], "Feedback save result.", true)
|
|
25936
26386
|
}
|
|
25937
26387
|
];
|
|
25938
|
-
var
|
|
26388
|
+
var remoteCustomerContracts = REMOTE_CUSTOMER_OPERATIONS.map((operation) => ({
|
|
26389
|
+
name: operation.name,
|
|
26390
|
+
title: operation.title,
|
|
26391
|
+
description: `${operation.title} on the configured server; unavailable capabilities fail explicitly.`,
|
|
26392
|
+
params: operation.parameter ? [operation.parameter] : [],
|
|
26393
|
+
category: "execution",
|
|
26394
|
+
sideEffects: operation.mutates ? "local-process-or-remote-run" : "none",
|
|
26395
|
+
stable: true,
|
|
26396
|
+
inputSchema: objectSchema(operation.parameter ? { [operation.parameter]: stringSchema("Server resource identifier.") } : {}, operation.parameter ? [operation.parameter] : []),
|
|
26397
|
+
outputSchema: { oneOf: [objectSchema({}, [], "Server response.", true), { type: "array", items: objectSchema({}, [], "Server record.", true) }] }
|
|
26398
|
+
}));
|
|
26399
|
+
remoteCustomerContracts.push({
|
|
26400
|
+
name: "quote_skill",
|
|
26401
|
+
title: "Quote Remote Skill",
|
|
26402
|
+
description: "Get a server credit quote without submitting a run.",
|
|
26403
|
+
params: ["name", "input?", "args?"],
|
|
26404
|
+
category: "execution",
|
|
26405
|
+
sideEffects: "none",
|
|
26406
|
+
stable: true,
|
|
26407
|
+
inputSchema: objectSchema({ name: skillNameInput, input: runInputSchema, args: runArgsSchema }, ["name"]),
|
|
26408
|
+
outputSchema: objectSchema({ skill: stringSchema("Canonical server skill."), pricing: objectSchema({}, [], "Quoted integer credits.", true) }, ["skill", "pricing"], undefined, true)
|
|
26409
|
+
});
|
|
26410
|
+
remoteCustomerContracts.push({
|
|
26411
|
+
name: "download_run_artifact",
|
|
26412
|
+
title: "Download Verified Run Artifact",
|
|
26413
|
+
description: "Return verified artifact bytes as base64, bounded to 1 MiB.",
|
|
26414
|
+
params: ["run_id", "artifact_id"],
|
|
26415
|
+
category: "execution",
|
|
26416
|
+
sideEffects: "none",
|
|
26417
|
+
stable: true,
|
|
26418
|
+
inputSchema: objectSchema({ run_id: stringSchema("Run identifier."), artifact_id: stringSchema("Artifact identifier.") }, ["run_id", "artifact_id"]),
|
|
26419
|
+
outputSchema: objectSchema({ id: stringSchema("Artifact identifier."), fileName: stringSchema("Artifact file name."), base64: stringSchema("Verified bytes."), sha256: stringSchema("SHA256 digest."), byteSize: { type: "integer", minimum: 0 } }, ["id", "fileName", "base64", "sha256", "byteSize"])
|
|
26420
|
+
});
|
|
26421
|
+
var contracts = [...toolContracts, ...remoteCustomerContracts].sort((a, b) => a.name.localeCompare(b.name));
|
|
25939
26422
|
var resourceContracts = [
|
|
25940
26423
|
{
|
|
25941
26424
|
uri: "skills://mcp/contracts",
|
|
@@ -26869,6 +27352,7 @@ function createSkillRun(params, targetDir = process.cwd()) {
|
|
|
26869
27352
|
startedAt: now.toISOString(),
|
|
26870
27353
|
remote: params.remote ?? false,
|
|
26871
27354
|
...params.remoteRunId ? { remoteRunId: params.remoteRunId } : {},
|
|
27355
|
+
...params.remoteApiOrigin ? { remoteApiOrigin: params.remoteApiOrigin } : {},
|
|
26872
27356
|
...params.costCents !== undefined ? { costCents: params.costCents } : {},
|
|
26873
27357
|
artifacts: [],
|
|
26874
27358
|
paths: {
|
|
@@ -27045,14 +27529,12 @@ function resolveRunRouting(skill, apiKey, apiUrl) {
|
|
|
27045
27529
|
error: `${skill.name} is a server-owned skill. Run: skills auth login`
|
|
27046
27530
|
};
|
|
27047
27531
|
}
|
|
27048
|
-
return { route: "remote", apiKey };
|
|
27532
|
+
return { route: "remote", apiKey, apiOrigin: apiUrl };
|
|
27049
27533
|
}
|
|
27050
27534
|
async function resolveConfiguredRunRouting(skill, env = process.env) {
|
|
27051
|
-
let
|
|
27052
|
-
let apiKey;
|
|
27535
|
+
let connection;
|
|
27053
27536
|
try {
|
|
27054
|
-
|
|
27055
|
-
apiKey = fleet.mode === "hosted" ? await resolveSkillsApiKey(env) : null;
|
|
27537
|
+
connection = await resolveSkillsConnection(env);
|
|
27056
27538
|
} catch (error2) {
|
|
27057
27539
|
const isMissingCredential = (error2 instanceof SkillsFleetCredentialError || error2?.name === "SkillsFleetCredentialError") && error2.code === "MISSING_API_CREDENTIAL";
|
|
27058
27540
|
if (!isMissingCredential)
|
|
@@ -27063,7 +27545,7 @@ async function resolveConfiguredRunRouting(skill, env = process.env) {
|
|
|
27063
27545
|
error: `${skill.name} is a server-owned skill. ${error2.message}`
|
|
27064
27546
|
};
|
|
27065
27547
|
}
|
|
27066
|
-
return resolveRunRouting(skill, apiKey,
|
|
27548
|
+
return resolveRunRouting(skill, connection?.apiKey, connection?.apiOrigin);
|
|
27067
27549
|
}
|
|
27068
27550
|
|
|
27069
27551
|
// src/mcp/operation-tools.ts
|
|
@@ -27267,10 +27749,15 @@ function registerOperationTools(server) {
|
|
|
27267
27749
|
name: exports_external.string(),
|
|
27268
27750
|
input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
|
|
27269
27751
|
args: exports_external.array(exports_external.string()).optional(),
|
|
27270
|
-
detail: exports_external.boolean().optional()
|
|
27271
|
-
|
|
27272
|
-
|
|
27273
|
-
|
|
27752
|
+
detail: exports_external.boolean().optional(),
|
|
27753
|
+
maxCostCents: exports_external.number().int().min(0).max(2147483647).optional().describe("Maximum integer credits explicitly approved by the user for a remote run; omitted permits only free runs"),
|
|
27754
|
+
maxCredits: exports_external.number().int().min(0).max(2147483647).optional(),
|
|
27755
|
+
remote: exports_external.boolean().optional().describe("Use the configured server catalog, including skills not installed locally"),
|
|
27756
|
+
idempotency_key: exports_external.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe("Reuse for the same approved submission after an interrupted response"),
|
|
27757
|
+
files: exports_external.array(exports_external.object({ name: exports_external.string(), base64: exports_external.string().max(1398104), contentType: exports_external.string().optional() })).max(10).optional().describe("Inline remote inputs, at most 1 MiB combined; use CLI or SDK for larger files")
|
|
27758
|
+
}
|
|
27759
|
+
}, async ({ name, input, args, detail, maxCostCents, maxCredits, remote, idempotency_key, files }) => {
|
|
27760
|
+
const skill = remote ? { name, serverOwned: true } : getSkill(name);
|
|
27274
27761
|
if (!skill) {
|
|
27275
27762
|
return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
|
|
27276
27763
|
}
|
|
@@ -27281,17 +27768,26 @@ function registerOperationTools(server) {
|
|
|
27281
27768
|
const skillName = skill.name;
|
|
27282
27769
|
const runInput = input || {};
|
|
27283
27770
|
const runArgs = args || [];
|
|
27284
|
-
if (skillName === ARTICLE_GENERATION_SLUG2) {
|
|
27771
|
+
if (!remote && skillName === ARTICLE_GENERATION_SLUG2) {
|
|
27285
27772
|
const validation = validateBlogArticleRunOptions2(runInput, runArgs, { requireTopic: true });
|
|
27286
27773
|
if (!validation.ok) {
|
|
27287
27774
|
return mcpError("INVALID_BLOG_ARTICLE_OPTIONS", validation.errors.join(" "));
|
|
27288
27775
|
}
|
|
27289
27776
|
}
|
|
27290
27777
|
const routing = await resolveConfiguredRunRouting(skill);
|
|
27778
|
+
if (files?.length && routing.route !== "remote")
|
|
27779
|
+
return mcpError("REMOTE_REQUIRED", "Inline inputs require a remote run");
|
|
27780
|
+
let inputFiles;
|
|
27781
|
+
try {
|
|
27782
|
+
inputFiles = (await Promise.resolve().then(() => (init_remote_files(), exports_remote_files))).decodeRemoteFiles(files ?? []);
|
|
27783
|
+
} catch (error2) {
|
|
27784
|
+
return mcpError("INVALID_INPUT_FILES", error2.message);
|
|
27785
|
+
}
|
|
27291
27786
|
const runContext = createSkillRun({
|
|
27292
27787
|
skill: skillName,
|
|
27293
27788
|
args: runArgs,
|
|
27294
|
-
remote: routing.route === "remote"
|
|
27789
|
+
remote: routing.route === "remote",
|
|
27790
|
+
...routing.route === "remote" ? { remoteApiOrigin: routing.apiOrigin } : {}
|
|
27295
27791
|
});
|
|
27296
27792
|
if (routing.route === "error") {
|
|
27297
27793
|
const error2 = routing.error;
|
|
@@ -27304,8 +27800,8 @@ function registerOperationTools(server) {
|
|
|
27304
27800
|
if (routing.route === "remote") {
|
|
27305
27801
|
try {
|
|
27306
27802
|
const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
|
|
27307
|
-
const client = new RemoteSkillsClient2(routing.apiKey);
|
|
27308
|
-
const run = await client.
|
|
27803
|
+
const client = new RemoteSkillsClient2(routing.apiKey, routing.apiOrigin);
|
|
27804
|
+
const run = await client.submitQuotedRunWithFiles(skillName, runInput, runArgs, inputFiles, { maxCredits, maxCostCents, idempotencyKey: idempotency_key ?? runContext.record.id });
|
|
27309
27805
|
if (run.error) {
|
|
27310
27806
|
writeRunLogs(runContext, "", String(run.error) + `
|
|
27311
27807
|
`);
|
|
@@ -27367,7 +27863,7 @@ function registerOperationTools(server) {
|
|
|
27367
27863
|
}
|
|
27368
27864
|
}, async ({ run_id, detail }) => {
|
|
27369
27865
|
const { skillsCredentialOrReason: skillsCredentialOrReason2 } = await Promise.resolve().then(() => (init_fleet_credentials(), exports_fleet_credentials));
|
|
27370
|
-
const { apiKey, reason } = await skillsCredentialOrReason2();
|
|
27866
|
+
const { apiKey, apiOrigin, reason } = await skillsCredentialOrReason2();
|
|
27371
27867
|
if (!apiKey) {
|
|
27372
27868
|
return mcpError("AUTH_REQUIRED", reason ?? "Remote run status requires API access. Run: skills auth login", ["skills auth login"]);
|
|
27373
27869
|
}
|
|
@@ -27378,7 +27874,9 @@ function registerOperationTools(server) {
|
|
|
27378
27874
|
}
|
|
27379
27875
|
try {
|
|
27380
27876
|
const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
|
|
27381
|
-
|
|
27877
|
+
if (localRun?.remoteApiOrigin && localRun.remoteApiOrigin !== apiOrigin)
|
|
27878
|
+
return mcpError("INSTANCE_MISMATCH", "This run belongs to another Skills instance; select its credential profile");
|
|
27879
|
+
const client = new RemoteSkillsClient2(apiKey, apiOrigin);
|
|
27382
27880
|
const run = await client.getRun(remoteRunId);
|
|
27383
27881
|
if (!run)
|
|
27384
27882
|
return mcpError("RUN_NOT_FOUND", `Remote run '${remoteRunId}' not found`);
|
|
@@ -27987,7 +28485,7 @@ function registerScheduleTools(server) {
|
|
|
27987
28485
|
}
|
|
27988
28486
|
|
|
27989
28487
|
// src/lib/native-storage.ts
|
|
27990
|
-
import { createHash as
|
|
28488
|
+
import { createHash as createHash4, createHmac } from "crypto";
|
|
27991
28489
|
import {
|
|
27992
28490
|
existsSync as existsSync16,
|
|
27993
28491
|
mkdirSync as mkdirSync8,
|
|
@@ -28099,7 +28597,7 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
|
28099
28597
|
files.push({
|
|
28100
28598
|
path: relativePath,
|
|
28101
28599
|
sizeBytes: bytes.byteLength,
|
|
28102
|
-
sha256:
|
|
28600
|
+
sha256: createHash4("sha256").update(bytes).digest("hex"),
|
|
28103
28601
|
...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
|
|
28104
28602
|
});
|
|
28105
28603
|
}
|
|
@@ -28231,6 +28729,194 @@ function registerStorageTools(server) {
|
|
|
28231
28729
|
});
|
|
28232
28730
|
}
|
|
28233
28731
|
|
|
28732
|
+
// src/lib/remote-auth.ts
|
|
28733
|
+
init_remote_client();
|
|
28734
|
+
init_fleet_credentials();
|
|
28735
|
+
var MAX_ERROR_DETAIL_LENGTH = 200;
|
|
28736
|
+
|
|
28737
|
+
class HostedApiError extends Error {
|
|
28738
|
+
status;
|
|
28739
|
+
code;
|
|
28740
|
+
detail;
|
|
28741
|
+
endpoint;
|
|
28742
|
+
apiUrl;
|
|
28743
|
+
constructor(message, options = {}) {
|
|
28744
|
+
super(message);
|
|
28745
|
+
this.name = "HostedApiError";
|
|
28746
|
+
this.status = options.status;
|
|
28747
|
+
this.code = options.code;
|
|
28748
|
+
this.detail = options.detail;
|
|
28749
|
+
this.endpoint = options.endpoint;
|
|
28750
|
+
this.apiUrl = options.apiUrl;
|
|
28751
|
+
}
|
|
28752
|
+
}
|
|
28753
|
+
async function requestAuthApi(instance, path, options) {
|
|
28754
|
+
const url = normalizeSkillsApiOrigin(instance);
|
|
28755
|
+
const safeUrl = url;
|
|
28756
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${safeUrl}${path}`;
|
|
28757
|
+
let res;
|
|
28758
|
+
try {
|
|
28759
|
+
res = await fetch(`${url}${path}`, {
|
|
28760
|
+
...options,
|
|
28761
|
+
redirect: "error",
|
|
28762
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
28763
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
28764
|
+
});
|
|
28765
|
+
} catch (err) {
|
|
28766
|
+
throw new HostedApiError(`Unable to reach the Skills API: ${err.message}`, {
|
|
28767
|
+
endpoint,
|
|
28768
|
+
apiUrl: safeUrl
|
|
28769
|
+
});
|
|
28770
|
+
}
|
|
28771
|
+
const text = await res.text();
|
|
28772
|
+
const body = text ? parseJsonBody(text) : {};
|
|
28773
|
+
if (!res.ok) {
|
|
28774
|
+
const record3 = isRecord4(body) ? body : {};
|
|
28775
|
+
const detail = typeof record3.detail === "string" ? record3.detail : undefined;
|
|
28776
|
+
const error2 = typeof record3.error === "string" ? record3.error : undefined;
|
|
28777
|
+
const code = typeof record3.code === "string" ? record3.code : undefined;
|
|
28778
|
+
throw new HostedApiError(detail || error2 || `${res.status} ${res.statusText}`, {
|
|
28779
|
+
status: res.status,
|
|
28780
|
+
code,
|
|
28781
|
+
detail,
|
|
28782
|
+
endpoint,
|
|
28783
|
+
apiUrl: safeUrl
|
|
28784
|
+
});
|
|
28785
|
+
}
|
|
28786
|
+
return body;
|
|
28787
|
+
}
|
|
28788
|
+
function parseJsonBody(text) {
|
|
28789
|
+
try {
|
|
28790
|
+
return JSON.parse(text);
|
|
28791
|
+
} catch {
|
|
28792
|
+
return { detail: condenseErrorBody(text) };
|
|
28793
|
+
}
|
|
28794
|
+
}
|
|
28795
|
+
function condenseErrorBody(text) {
|
|
28796
|
+
const stripped = /<[a-z!/]/i.test(text) ? text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ") : text;
|
|
28797
|
+
const collapsed = stripped.replace(/\s+/g, " ").trim();
|
|
28798
|
+
if (collapsed.length <= MAX_ERROR_DETAIL_LENGTH)
|
|
28799
|
+
return collapsed;
|
|
28800
|
+
return `${collapsed.slice(0, MAX_ERROR_DETAIL_LENGTH - 1).trimEnd()}\u2026`;
|
|
28801
|
+
}
|
|
28802
|
+
function isRecord4(value) {
|
|
28803
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
28804
|
+
}
|
|
28805
|
+
|
|
28806
|
+
class RemoteSkillsAuthClient {
|
|
28807
|
+
apiOrigin;
|
|
28808
|
+
constructor(apiUrl) {
|
|
28809
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
28810
|
+
}
|
|
28811
|
+
requestCode(email2) {
|
|
28812
|
+
return this.request("/api/auth/login", { method: "POST", body: JSON.stringify({ email: email2 }) });
|
|
28813
|
+
}
|
|
28814
|
+
verifyCode(email2, code) {
|
|
28815
|
+
return this.request("/api/auth/verify", { method: "POST", body: JSON.stringify({ email: email2, code }) });
|
|
28816
|
+
}
|
|
28817
|
+
startDevice() {
|
|
28818
|
+
return this.request("/api/auth/device/start", { method: "POST", body: JSON.stringify({ client: "skills-sdk" }) });
|
|
28819
|
+
}
|
|
28820
|
+
pollDevice(deviceCode) {
|
|
28821
|
+
return this.request("/api/auth/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) });
|
|
28822
|
+
}
|
|
28823
|
+
async sessionClient(email2, code) {
|
|
28824
|
+
if (!email2.includes("@") || !/^\d{6}$/.test(code))
|
|
28825
|
+
throw new Error("Fresh email and six-digit verification code are required to manage API keys");
|
|
28826
|
+
const login = await this.verifyCode(email2, code);
|
|
28827
|
+
if (!login || typeof login.token !== "string" || !login.token)
|
|
28828
|
+
throw new Error("The server did not return an authorized account session");
|
|
28829
|
+
return new RemoteSkillsClient(login.token, this.apiOrigin);
|
|
28830
|
+
}
|
|
28831
|
+
async createApiKey(email2, code, name, scopes) {
|
|
28832
|
+
return (await this.sessionClient(email2, code)).createApiKey(name, scopes);
|
|
28833
|
+
}
|
|
28834
|
+
async listApiKeys(email2, code) {
|
|
28835
|
+
return (await this.sessionClient(email2, code)).listApiKeys();
|
|
28836
|
+
}
|
|
28837
|
+
async revokeApiKey(email2, code, keyId) {
|
|
28838
|
+
return (await this.sessionClient(email2, code)).revokeApiKey(keyId);
|
|
28839
|
+
}
|
|
28840
|
+
request(path, options) {
|
|
28841
|
+
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
|
|
28842
|
+
throw new Error("Unsupported authentication operation");
|
|
28843
|
+
return requestAuthApi(this.apiOrigin, path, options);
|
|
28844
|
+
}
|
|
28845
|
+
}
|
|
28846
|
+
|
|
28847
|
+
// src/mcp/remote-customer-tools.ts
|
|
28848
|
+
init_auth_store();
|
|
28849
|
+
init_remote_client();
|
|
28850
|
+
function registerRemoteCustomerTools(server) {
|
|
28851
|
+
for (const operation of REMOTE_CUSTOMER_OPERATIONS) {
|
|
28852
|
+
const inputSchema = {};
|
|
28853
|
+
if (operation.parameter)
|
|
28854
|
+
inputSchema[operation.parameter] = exports_external.string().min(1);
|
|
28855
|
+
server.registerTool(operation.name, {
|
|
28856
|
+
title: operation.title,
|
|
28857
|
+
description: `${operation.title} on the explicitly configured Skills server. Missing server capabilities return an error. Checkout links require external customer confirmation.`,
|
|
28858
|
+
inputSchema
|
|
28859
|
+
}, async (input) => callRemote((client) => operation.invoke(client, operation.parameter ? String(input[operation.parameter]) : "")));
|
|
28860
|
+
}
|
|
28861
|
+
server.registerTool("list_api_keys", {
|
|
28862
|
+
title: "List API Keys",
|
|
28863
|
+
description: "List account API keys using fresh email OTP reauthentication.",
|
|
28864
|
+
inputSchema: { email: exports_external.string().email(), code: exports_external.string().regex(/^\d{6}$/) }
|
|
28865
|
+
}, async ({ email: email2, code }) => {
|
|
28866
|
+
try {
|
|
28867
|
+
return mcpJson(await new RemoteSkillsAuthClient(getApiUrl("List API keys")).listApiKeys(email2, code));
|
|
28868
|
+
} catch (error2) {
|
|
28869
|
+
return mcpError("KEY_LIST_FAILED", error2.message);
|
|
28870
|
+
}
|
|
28871
|
+
});
|
|
28872
|
+
server.registerTool("revoke_api_key", {
|
|
28873
|
+
title: "Revoke API Key",
|
|
28874
|
+
description: "Revoke an account API key using fresh email OTP reauthentication.",
|
|
28875
|
+
inputSchema: { key_id: exports_external.string().min(1), email: exports_external.string().email(), code: exports_external.string().regex(/^\d{6}$/) }
|
|
28876
|
+
}, async ({ key_id, email: email2, code }) => {
|
|
28877
|
+
try {
|
|
28878
|
+
return mcpJson(await new RemoteSkillsAuthClient(getApiUrl("Revoke API key")).revokeApiKey(email2, code, key_id));
|
|
28879
|
+
} catch (error2) {
|
|
28880
|
+
return mcpError("KEY_REVOKE_FAILED", error2.message);
|
|
28881
|
+
}
|
|
28882
|
+
});
|
|
28883
|
+
server.registerTool("create_api_key", {
|
|
28884
|
+
title: "Create API Key",
|
|
28885
|
+
description: "Create an API key using fresh email OTP reauthentication; returns its secret once. A stored API key cannot grant this authority.",
|
|
28886
|
+
inputSchema: { name: exports_external.string().min(1), email: exports_external.string().email(), code: exports_external.string().regex(/^\d{6}$/), scopes: exports_external.array(exports_external.string()).optional() }
|
|
28887
|
+
}, async ({ name, email: email2, code, scopes }) => {
|
|
28888
|
+
try {
|
|
28889
|
+
return mcpJson(await new RemoteSkillsAuthClient(getApiUrl("Create API key")).createApiKey(email2, code, name, scopes));
|
|
28890
|
+
} catch (error2) {
|
|
28891
|
+
return mcpError("KEY_CREATION_FAILED", error2.message);
|
|
28892
|
+
}
|
|
28893
|
+
});
|
|
28894
|
+
server.registerTool("quote_skill", {
|
|
28895
|
+
title: "Quote Remote Skill",
|
|
28896
|
+
description: "Get the configured server's credit quote without submitting a run.",
|
|
28897
|
+
inputSchema: { name: exports_external.string(), input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(), args: exports_external.array(exports_external.string()).optional() }
|
|
28898
|
+
}, ({ name, input, args }) => callRemote((client) => client.quoteRun(name, input, args)));
|
|
28899
|
+
server.registerTool("download_run_artifact", {
|
|
28900
|
+
title: "Download Verified Run Artifact",
|
|
28901
|
+
description: "Return verified artifact bytes as base64 (at most 1 MiB); use the CLI for larger files.",
|
|
28902
|
+
inputSchema: { run_id: exports_external.string(), artifact_id: exports_external.string() }
|
|
28903
|
+
}, ({ run_id, artifact_id }) => callRemote(async (client) => {
|
|
28904
|
+
const artifact = await client.getVerifiedRunArtifact(run_id, artifact_id, 1024 * 1024);
|
|
28905
|
+
const { bytes, ...metadata } = artifact;
|
|
28906
|
+
return { ...metadata, base64: Buffer.from(bytes).toString("base64") };
|
|
28907
|
+
}));
|
|
28908
|
+
}
|
|
28909
|
+
async function callRemote(action) {
|
|
28910
|
+
try {
|
|
28911
|
+
const client = await createRemoteSkillsClient();
|
|
28912
|
+
if (!client)
|
|
28913
|
+
return mcpError("AUTH_REQUIRED", "Configure a Skills API and sign in with skills auth login");
|
|
28914
|
+
return mcpJson(await action(client));
|
|
28915
|
+
} catch (error2) {
|
|
28916
|
+
return mcpError("REMOTE_REQUEST_FAILED", error2 instanceof Error ? error2.message : "Skills server request failed");
|
|
28917
|
+
}
|
|
28918
|
+
}
|
|
28919
|
+
|
|
28234
28920
|
// src/mcp/server.ts
|
|
28235
28921
|
function buildServer() {
|
|
28236
28922
|
const server = new McpServer({
|
|
@@ -28242,6 +28928,7 @@ function buildServer() {
|
|
|
28242
28928
|
registerScheduleTools(server);
|
|
28243
28929
|
registerStorageTools(server);
|
|
28244
28930
|
registerResourceMetaTools(server);
|
|
28931
|
+
registerRemoteCustomerTools(server);
|
|
28245
28932
|
return server;
|
|
28246
28933
|
}
|
|
28247
28934
|
var server = buildServer();
|
|
@@ -29705,7 +30392,8 @@ MCP server for ${package_default.name}
|
|
|
29705
30392
|
Options:
|
|
29706
30393
|
-V, --version output the version number
|
|
29707
30394
|
-h, --help display help for command
|
|
29708
|
-
--
|
|
30395
|
+
--stdio run newline-delimited JSON-RPC for agent hosts
|
|
30396
|
+
--http run Streamable HTTP transport on 127.0.0.1 (default; port 8836)
|
|
29709
30397
|
--port <n> HTTP port (--http or MCP_HTTP=1)`);
|
|
29710
30398
|
}
|
|
29711
30399
|
if (args.includes("--help") || args.includes("-h")) {
|