@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/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
- function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
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
- for (const key of SKILLS_API_URL_ENV_KEYS) {
7799
- const value = env[key]?.trim();
7800
- if (value)
7801
- return { value, source: key };
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 credentialDiskSources(SKILLS_APP, env);
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
- return resolveSkillsFleetOrThrow(env, options);
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 resolveSkillsFleetOrThrow(env, options) {
7845
- let resolution;
7846
- try {
7847
- resolution = resolveClientTransport(SKILLS_APP, env, { credentials: options.credentials });
7848
- } catch (error2) {
7849
- if (!isClientTransportConfigurationError(error2))
7850
- throw error2;
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
- throw error2;
7865
+ snapshot[key] = descriptor.value;
7862
7866
  }
7863
- const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
7864
- const apiOrigin = configured ? normalizeSkillsApiOrigin(configured.value) : stripV1(resolution.baseUrl);
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
- throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
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: resolution.apiUrlSource ?? (configured?.source ?? "default"),
7873
- apiKeySource: resolution.apiKeySource ?? credential.source,
7874
- apiKeyTier: resolution.apiKeyTier,
7875
- warning: resolution.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
- const fleet = resolveSkillsFleet(env, options);
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, env);
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 apiKey = await resolveSkillsApiKey(env, options);
7921
- return apiKey ? { apiKey, reason: null } : { apiKey: null, reason: null };
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.replace(/\/$/, "");
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
- const res = await this.request("/api/v1/skills");
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
- const res = await this.request(`/api/v1/runs/${slug}`, {
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({ input, args })
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 res = await this.request(`/api/v1/runs/${runId}`);
8188
- if (!res.ok)
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
- const res = await this.request(`/api/v1/runs/${runId}/logs`);
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
- const res = await this.request(`/api/v1/runs?limit=${limit}`);
8201
- return res.json();
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
- const res = await this.request(`/api/v1/runs/${runId}/artifacts`);
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
- for (const tag of payload) {
8281
- if (typeof tag !== "string" || tag.trim().length === 0) {
8282
- throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name)");
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
- return payload;
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 fleet = resolveSkillsFleet(env);
8389
- if (fleet.mode !== "hosted")
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.3.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.replace(/^["']|["']$/g, "");
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 contracts = [...toolContracts].sort((a, b) => a.name.localeCompare(b.name));
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 fleet;
27052
- let apiKey;
27535
+ let connection;
27053
27536
  try {
27054
- fleet = resolveSkillsFleet(env);
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, fleet.apiOrigin ?? undefined);
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
- }, async ({ name, input, args, detail }) => {
27273
- const skill = getSkill(name);
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.submitRun(skillName, runInput, runArgs);
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
- const client = new RemoteSkillsClient2(apiKey);
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 createHash3, createHmac } from "crypto";
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: createHash3("sha256").update(bytes).digest("hex"),
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
- --http run Streamable HTTP transport on 127.0.0.1 (default port 8836)
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")) {