@hasna/skills 0.9.19 → 0.10.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
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.9.19",
20022
+ version: "0.10.0",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -20921,7 +20921,7 @@ function toV1BaseUrl(apiUrl) {
20921
20921
  url2.pathname = `${path}/v1`;
20922
20922
  return url2.toString().replace(/\/+$/, "");
20923
20923
  }
20924
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", MAX_PACKAGE_BYTES, 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, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
20924
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", MAX_PACKAGE_BYTES, 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, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
20925
20925
  var init_transport = __esm(() => {
20926
20926
  MAX_PACKAGE_BYTES = 1024 * 1024;
20927
20927
  CredentialResolutionError = class CredentialResolutionError extends Error {
@@ -20954,6 +20954,16 @@ var init_transport = __esm(() => {
20954
20954
  requireSecretsSdk = createRequire(import.meta.url);
20955
20955
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
20956
20956
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
20957
+ ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
20958
+ appName;
20959
+ sources;
20960
+ constructor(appName, message, sources = []) {
20961
+ super(message);
20962
+ this.name = "ClientTransportConfigurationError";
20963
+ this.appName = appName;
20964
+ this.sources = Object.freeze([...sources]);
20965
+ }
20966
+ };
20957
20967
  IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
20958
20968
  AUTHORITY_OVERRIDE_HEADERS = new Set([
20959
20969
  "host",
@@ -21050,6 +21060,12 @@ function readSkillsInstanceMetadata(file2) {
21050
21060
  throw new Error("Skills API URL aliases disagree");
21051
21061
  return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
21052
21062
  }
21063
+ function skillsFileHasCredential(file2) {
21064
+ const text = readMetadataText(file2);
21065
+ if (text === null)
21066
+ return false;
21067
+ return /^\s*(?:export\s+)?(?:HASNA_SKILLS_API_KEY|SKILLS_API_KEY|HASNA_SKILLS_API_KEY_REF)\s*=\s*\S/m.test(text);
21068
+ }
21053
21069
  var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
21054
21070
  var init_instance_credentials = __esm(() => {
21055
21071
  init_transport();
@@ -21081,6 +21097,9 @@ var init_local_opt_in = __esm(() => {
21081
21097
  SKILLS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_SKILLS_LOCAL", "SKILLS_LOCAL"];
21082
21098
  });
21083
21099
 
21100
+ // src/lib/product-default.ts
21101
+ var SKILLS_PRODUCT_DEFAULT_ORIGIN = "https://skills.md";
21102
+
21084
21103
  // src/lib/fleet-credentials.ts
21085
21104
  var exports_fleet_credentials = {};
21086
21105
  __export(exports_fleet_credentials, {
@@ -21089,6 +21108,7 @@ __export(exports_fleet_credentials, {
21089
21108
  skillsCredentialFilePath: () => skillsCredentialFilePath,
21090
21109
  skillsApiRequestUrl: () => skillsApiRequestUrl,
21091
21110
  selectsSkillsLocalMode: () => selectsSkillsLocalMode,
21111
+ resolveSkillsSignInOrigin: () => resolveSkillsSignInOrigin,
21092
21112
  resolveSkillsFleet: () => resolveSkillsFleet,
21093
21113
  resolveSkillsConnection: () => resolveSkillsConnection,
21094
21114
  resolveSkillsApiOrigin: () => resolveSkillsApiOrigin,
@@ -21101,9 +21121,12 @@ __export(exports_fleet_credentials, {
21101
21121
  normalizeSkillsApiOrigin: () => normalizeSkillsApiOrigin,
21102
21122
  isSkillsLocalOptIn: () => isSkillsLocalOptIn,
21103
21123
  isSkillsFleetCredentialError: () => isSkillsFleetCredentialError,
21124
+ gatewaySignInRefusal: () => gatewaySignInRefusal,
21125
+ credentialPairing: () => credentialPairing,
21104
21126
  configuredSkillsApiUrl: () => configuredSkillsApiUrl,
21105
21127
  SkillsFleetCredentialError: () => SkillsFleetCredentialError,
21106
21128
  SKILLS_LOCAL_OPT_IN_ENV_KEYS: () => SKILLS_LOCAL_OPT_IN_ENV_KEYS,
21129
+ SKILLS_CREDENTIAL_TIERS: () => SKILLS_CREDENTIAL_TIERS,
21107
21130
  SKILLS_APP: () => SKILLS_APP,
21108
21131
  SKILLS_API_URL_ENV_KEYS: () => SKILLS_API_URL_ENV_KEYS,
21109
21132
  SKILLS_API_URL_ENV: () => SKILLS_API_URL_ENV,
@@ -21112,6 +21135,9 @@ __export(exports_fleet_credentials, {
21112
21135
  REDACTED_VAULT_REFERENCE: () => REDACTED_VAULT_REFERENCE,
21113
21136
  MissingSkillsFleetError: () => MissingSkillsFleetError
21114
21137
  });
21138
+ function isClientTransportConfigurationError(error61) {
21139
+ return error61 instanceof ClientTransportConfigurationError || typeof error61 === "object" && error61 !== null && error61.name === "ClientTransportConfigurationError";
21140
+ }
21115
21141
  function isCredentialResolutionError(error61) {
21116
21142
  return error61 instanceof CredentialResolutionError || typeof error61 === "object" && error61 !== null && error61.name === "CredentialResolutionError";
21117
21143
  }
@@ -21166,7 +21192,7 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
21166
21192
  if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
21167
21193
  throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
21168
21194
  }
21169
- const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
21195
+ const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key, trust: "environment" }));
21170
21196
  if (new Set(normalized.map((entry) => entry.value)).size > 1)
21171
21197
  throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
21172
21198
  if (normalized[0])
@@ -21175,19 +21201,19 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
21175
21201
  for (const file2 of skillsProfileCredentialFiles(env, profile)) {
21176
21202
  const metadata = readSkillsInstanceMetadata(file2);
21177
21203
  if (metadata.apiUrl || metadata.binding)
21178
- return { value: metadata.apiUrl ?? metadata.binding, source: file2 };
21204
+ return { value: metadata.apiUrl ?? metadata.binding, source: file2, trust: "file" };
21179
21205
  }
21180
- return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
21206
+ return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default", trust: "default" };
21181
21207
  }
21182
21208
  const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
21183
21209
  if (fromKeychain)
21184
- return { value: fromKeychain.value.trim(), source: fromKeychain.source };
21210
+ return { value: fromKeychain.value.trim(), source: fromKeychain.source, trust: "keychain" };
21185
21211
  const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
21186
21212
  if (fromDisk?.unusable) {
21187
21213
  throw new SkillsFleetCredentialError(`${fromDisk.key} in ${fromDisk.path} is declared but blank or malformed; ` + `a Skills authority must be a valid https URL (or an exact loopback http URL).`, "INVALID_API_URL");
21188
21214
  }
21189
21215
  if (fromDisk)
21190
- return { value: fromDisk.value.trim(), source: fromDisk.path };
21216
+ return { value: fromDisk.value.trim(), source: fromDisk.path, trust: "file" };
21191
21217
  return null;
21192
21218
  }
21193
21219
  function skillsCredentialFiles(env = process.env) {
@@ -21252,13 +21278,13 @@ function resolveSkillsFleetOrThrow(env, options) {
21252
21278
  const credential = resolveCredential(SKILLS_APP, env, options.credentials);
21253
21279
  if (!credential) {
21254
21280
  if (!configured) {
21255
- throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills auth login`);
21281
+ throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills login`);
21256
21282
  }
21257
- 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 ${credentialLocations(env)}. Sign in with: skills auth login`);
21283
+ 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 ${credentialLocations(env)}. Sign in with: skills login`);
21258
21284
  }
21259
21285
  const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
21260
21286
  toV1BaseUrl(apiOrigin);
21261
- assertCredentialInstance(credential, apiOrigin, env, options);
21287
+ assertCredentialInstancePairing(credential, configured, apiOrigin, env, options);
21262
21288
  assertFilesUnchanged();
21263
21289
  const base = {
21264
21290
  mode: "hosted",
@@ -21272,7 +21298,7 @@ function resolveSkillsFleetOrThrow(env, options) {
21272
21298
  return { ...base, apiKey: null, apiKeyPointer: credential };
21273
21299
  }
21274
21300
  if (!credential.apiKey.trim()) {
21275
- throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills auth login`);
21301
+ throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills login`);
21276
21302
  }
21277
21303
  return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
21278
21304
  }
@@ -21280,17 +21306,49 @@ function credentialLocations(env) {
21280
21306
  const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
21281
21307
  return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
21282
21308
  }
21283
- function assertCredentialInstance(credential, apiOrigin, env, options) {
21284
- let bound;
21285
- if (credential.tier === "disk" || credential.tier === "profile" || credential.tier === "pointer" && credential.diskCandidates.includes(credential.source)) {
21286
- const metadata = readSkillsInstanceMetadata(credential.source);
21287
- bound = metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
21288
- } else if (credential.tier === "keychain") {
21289
- bound = keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
21309
+ function credentialPairing(credential, env, options = {}) {
21310
+ const fileBinding = (file2) => {
21311
+ const metadata = readSkillsInstanceMetadata(file2);
21312
+ return { kind: "bound", instance: normalizeSkillsApiOrigin(metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP)) };
21313
+ };
21314
+ const tier = credential.tier;
21315
+ switch (tier) {
21316
+ case "disk":
21317
+ case "profile":
21318
+ return fileBinding(credential.source);
21319
+ case "pointer":
21320
+ return credential.diskCandidates.includes(credential.source) ? fileBinding(credential.source) : { kind: "unbound" };
21321
+ case "keychain":
21322
+ return {
21323
+ kind: "bound",
21324
+ instance: normalizeSkillsApiOrigin(keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP))
21325
+ };
21326
+ case "env":
21327
+ case "override":
21328
+ case "argument":
21329
+ return { kind: "unbound" };
21330
+ default: {
21331
+ const unclassified = tier;
21332
+ throw new SkillsFleetCredentialError(`The Skills credential from ${credential.source} has an unclassified tier (${String(unclassified)}); refusing to send it.`, "INSTANCE_CREDENTIAL_MISMATCH");
21333
+ }
21290
21334
  }
21291
- if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
21335
+ }
21336
+ function assertCredentialInstancePairing(credential, configured, apiOrigin, env, options) {
21337
+ const pairing = credentialPairing(credential, env, options);
21338
+ if (pairing.kind === "bound") {
21339
+ if (pairing.instance === apiOrigin)
21340
+ return;
21292
21341
  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");
21293
21342
  }
21343
+ if (apiOrigin === normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
21344
+ return;
21345
+ if (configured?.trust === "environment")
21346
+ return;
21347
+ const next = [
21348
+ `set ${SKILLS_API_URL_ENV} alongside ${credential.source}`,
21349
+ `unset ${credential.source}, then run: skills login`
21350
+ ];
21351
+ throw new SkillsFleetCredentialError(`${credential.source} records no Skills server of its own, so it is only sent to the internal gateway or to a URL ` + `from the environment (${SKILLS_API_URL_ENV}); the Skills URL here comes from ${configured?.source ?? "nowhere"}. ` + `No credential was sent. To use your own server, ${next[0]}; otherwise ${next[1]}.`, "INSTANCE_CREDENTIAL_MISMATCH", next);
21294
21352
  }
21295
21353
  async function resolveSkillsApiKey(env = process.env, options = {}) {
21296
21354
  return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
@@ -21307,7 +21365,7 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
21307
21365
  return { ...fleet, apiKey: fleet.apiKey };
21308
21366
  const pointer = fleet.apiKeyPointer;
21309
21367
  if (!pointer) {
21310
- throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
21368
+ throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills login`);
21311
21369
  }
21312
21370
  let completed;
21313
21371
  try {
@@ -21364,13 +21422,84 @@ function requireSkillsApiOrigin(action = "This command", env = process.env, opti
21364
21422
  throw new MissingSkillsFleetError(action);
21365
21423
  return resolved.origin;
21366
21424
  }
21425
+ function gatewaySignInRefusal(target) {
21426
+ if (normalizeSkillsApiOrigin(target.origin) !== normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
21427
+ return null;
21428
+ const cause = target.credentialSource ? `${target.credentialSource} holds a Skills credential for the internal gateway` : target.source === "--url" ? "--url names the internal gateway" : `${target.source} selects the internal gateway`;
21429
+ const unset = target.credentialSource ?? (target.source === "--url" ? null : target.source);
21430
+ return new SkillsFleetCredentialError(`${cause}, which has no sign-in service. Nothing was sent.` + (unset ? ` To sign in to the default server instead, first unset ${unset} (or remove it from where it is stored), then run: skills login.` : ""), "GATEWAY_AUTH_UNAVAILABLE", unset ? [`unset ${unset}`, "skills login"] : undefined);
21431
+ }
21432
+ function explicitSignInOrigin(url2) {
21433
+ if (!url2.trim() || /[\x00-\x1f\x7f]/.test(url2)) {
21434
+ throw new SkillsFleetCredentialError("--url is blank or contains control characters", "INVALID_API_URL");
21435
+ }
21436
+ let origin;
21437
+ try {
21438
+ origin = normalizeSkillsApiOrigin(url2.trim());
21439
+ } catch (error61) {
21440
+ if (isSkillsFleetCredentialError(error61))
21441
+ throw error61;
21442
+ throw new SkillsFleetCredentialError("--url is not a valid URL", "INVALID_API_URL");
21443
+ }
21444
+ try {
21445
+ toV1BaseUrl(origin);
21446
+ } catch (error61) {
21447
+ if (isClientTransportConfigurationError(error61))
21448
+ throw new SkillsFleetCredentialError(error61.message, "INVALID_API_URL");
21449
+ throw error61;
21450
+ }
21451
+ return origin;
21452
+ }
21453
+ function resolveSkillsSignInOrigin(env = process.env, options = {}, explicitUrl) {
21454
+ try {
21455
+ const snapshot = snapshotSkillsEnvironment(env);
21456
+ const resolvedOptions = snapshotSkillsOptions(env, options);
21457
+ const profile = resolvedOptions.credentials?.profile;
21458
+ const configured = configuredSkillsApiUrl(snapshot, resolvedOptions.credentials?.keychain, profile);
21459
+ const declared = configured && configured.source !== "default" ? configured : null;
21460
+ if (explicitUrl !== undefined) {
21461
+ const origin = explicitSignInOrigin(explicitUrl);
21462
+ const storedHere = declared ? skillsProfileCredentialFiles(snapshot, profile).includes(declared.source) : false;
21463
+ if (declared && !storedHere && normalizeSkillsApiOrigin(declared.value) !== origin) {
21464
+ throw new SkillsFleetCredentialError(`--url ${origin} conflicts with ${declared.source}, which outranks the saved configuration and would ` + `shadow the key this sign-in stores. Unset ${declared.source} or pass the same URL.`, "INVALID_API_URL");
21465
+ }
21466
+ return { origin, source: "--url" };
21467
+ }
21468
+ if (declared) {
21469
+ toV1BaseUrl(declared.value);
21470
+ return { origin: normalizeSkillsApiOrigin(declared.value), source: declared.source };
21471
+ }
21472
+ let credential;
21473
+ try {
21474
+ credential = resolveCredential(SKILLS_APP, snapshot, resolvedOptions.credentials);
21475
+ } catch (error61) {
21476
+ const files = selectedSkillsProfile(snapshot, profile) ? skillsProfileCredentialFiles(snapshot, profile) : [];
21477
+ if (!isCredentialResolutionError(error61) || files.length === 0 || files.some(skillsFileHasCredential))
21478
+ throw error61;
21479
+ credential = null;
21480
+ }
21481
+ if (credential) {
21482
+ const pairing = credentialPairing(credential, snapshot, resolvedOptions);
21483
+ const origin = pairing.kind === "bound" ? pairing.instance : normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP));
21484
+ return { origin, source: "default", credentialSource: credential.source };
21485
+ }
21486
+ return { origin: SKILLS_PRODUCT_DEFAULT_ORIGIN, source: "default" };
21487
+ } catch (error61) {
21488
+ const translated = asSkillsFleetCredentialError(error61);
21489
+ if (translated)
21490
+ throw translated;
21491
+ if (isClientTransportConfigurationError(error61))
21492
+ throw new SkillsFleetCredentialError(error61.message, "INVALID_API_URL");
21493
+ throw error61;
21494
+ }
21495
+ }
21367
21496
  function requireSkillsFleet(action = "This command", env = process.env, options = {}) {
21368
21497
  const fleet = resolveSkillsFleet(env, options);
21369
21498
  if (fleet.mode === "hosted")
21370
21499
  return fleet;
21371
21500
  throw new MissingSkillsFleetError(action);
21372
21501
  }
21373
- 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, REDACTED_VAULT_REFERENCE = "[redacted vault reference]", localNoticePrinted = false, MissingSkillsFleetError;
21502
+ var SKILLS_APP = "skills", ENV_KEYS, SKILLS_API_URL_ENV_KEYS, SKILLS_API_KEY_ENV_KEYS, SKILLS_API_URL_ENV, SKILLS_API_KEY_ENV, SKILLS_CREDENTIAL_TIERS, SkillsFleetCredentialError, REDACTED_VAULT_REFERENCE = "[redacted vault reference]", localNoticePrinted = false, MissingSkillsFleetError;
21374
21503
  var init_fleet_credentials = __esm(() => {
21375
21504
  init_transport();
21376
21505
  init_instance_credentials();
@@ -21381,18 +21510,22 @@ var init_fleet_credentials = __esm(() => {
21381
21510
  SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
21382
21511
  SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
21383
21512
  SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
21513
+ SKILLS_CREDENTIAL_TIERS = ["argument", "override", "pointer", "profile", "keychain", "disk", "env"];
21384
21514
  SkillsFleetCredentialError = class SkillsFleetCredentialError extends Error {
21385
21515
  code;
21386
- constructor(message, code = "MISSING_API_CREDENTIAL") {
21516
+ next;
21517
+ constructor(message, code = "MISSING_API_CREDENTIAL", next) {
21387
21518
  super(message);
21388
21519
  this.name = "SkillsFleetCredentialError";
21389
21520
  this.code = code;
21521
+ if (next)
21522
+ this.next = next;
21390
21523
  }
21391
21524
  };
21392
21525
  MissingSkillsFleetError = class MissingSkillsFleetError extends Error {
21393
21526
  code = "MISSING_API_URL";
21394
21527
  constructor(action = "This command") {
21395
- super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills auth login, or set ${SKILLS_API_KEY_ENV} ` + `(add the Keychain item hasna.credentials.${SKILLS_APP}.api-key, or write ~/.hasna/skills/config/credentials). ` + `Point at your own instance with ${SKILLS_API_URL_ENV}, or run: skills setup --api-url <your Skills instance origin>`);
21528
+ super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills login, or set ${SKILLS_API_KEY_ENV} ` + `(add the Keychain item hasna.credentials.${SKILLS_APP}.api-key, or write ~/.hasna/skills/config/credentials). ` + `Point at your own instance with ${SKILLS_API_URL_ENV}, or run: skills setup --api-url <your Skills instance origin>`);
21396
21529
  this.name = "MissingSkillsFleetError";
21397
21530
  }
21398
21531
  };
@@ -49905,7 +50038,7 @@ var init_api_url = __esm(() => {
49905
50038
  init_fleet_credentials();
49906
50039
  init_fleet_credentials();
49907
50040
  API_URL_ENV_VAR = SKILLS_API_URL_ENV;
49908
- MISSING_API_URL_HINT = `run: skills auth login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
50041
+ MISSING_API_URL_HINT = `run: skills login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
49909
50042
  });
49910
50043
 
49911
50044
  // src/lib/discovery.ts
@@ -53006,7 +53139,7 @@ class RemoteSkillsAuthClient {
53006
53139
  return (await this.sessionClient(email4, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
53007
53140
  }
53008
53141
  request(path, options) {
53009
- if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
53142
+ if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami", "/api/auth/logout"].includes(path))
53010
53143
  throw new Error("Unsupported authentication operation");
53011
53144
  return requestAuthApi(this.apiOrigin, path, options);
53012
53145
  }
@@ -55241,7 +55374,7 @@ async function readSurface(body) {
55241
55374
  return await body();
55242
55375
  } catch (error61) {
55243
55376
  if (isSkillsFleetCredentialError(error61))
55244
- return mcpError("AUTH_REQUIRED", error61.message, ["skills auth login"]);
55377
+ return mcpError("AUTH_REQUIRED", error61.message, ["skills login"]);
55245
55378
  throw error61;
55246
55379
  }
55247
55380
  }
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.9.19",
10
+ version: "0.10.0",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.9.19",
24125
+ version: "0.10.0",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
@@ -36906,13 +36906,15 @@ var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
36906
36906
  var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
36907
36907
  var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
36908
36908
  var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
36909
-
36910
36909
  class SkillsFleetCredentialError extends Error {
36911
36910
  code;
36912
- constructor(message, code = "MISSING_API_CREDENTIAL") {
36911
+ next;
36912
+ constructor(message, code = "MISSING_API_CREDENTIAL", next2) {
36913
36913
  super(message);
36914
36914
  this.name = "SkillsFleetCredentialError";
36915
36915
  this.code = code;
36916
+ if (next2)
36917
+ this.next = next2;
36916
36918
  }
36917
36919
  }
36918
36920
  function normalizeSkillsApiOrigin(apiUrl) {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.9.19",
24128
+ version: "0.10.0",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -1,2 +1,4 @@
1
1
  import { Command } from "commander";
2
+ /** `skills login`, `skills logout`, `skills whoami` — the account verbs, at the top level. */
3
+ export declare function registerAccountCommands(parent: Command): void;
2
4
  export declare function registerAuth(parent: Command): void;
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ /** The TUI's account commands: `/login [url]`, `/logout`, `/whoami`, `/help`. */
3
+ export type AccountAction = "login" | "logout" | "whoami" | "help";
4
+ export declare const SLASH_COMMANDS: ReadonlyArray<{
5
+ usage: string;
6
+ description: string;
7
+ }>;
8
+ interface AccountViewProps {
9
+ action: AccountAction;
10
+ /** `/login <url>`: sign in to this server instead of the default. */
11
+ argument?: string;
12
+ /** Leave the view; `changed` is true when the stored credential changed. */
13
+ onDone: (changed: boolean) => void;
14
+ }
15
+ export declare function AccountView({ action, argument, onDone }: AccountViewProps): React.JSX.Element;
16
+ export {};
@@ -1,7 +1,24 @@
1
1
  import React from "react";
2
+ /**
3
+ * What the read gate said when the TUI opened (and after every sign-in or
4
+ * sign-out). A signed-out TUI still renders — that is where `/login` lives —
5
+ * but it offers no catalog: data views stay behind the same fail-closed gate
6
+ * as every other surface.
7
+ */
8
+ export type TuiAccess = {
9
+ state: "hosted";
10
+ origin: string;
11
+ } | {
12
+ state: "local";
13
+ } | {
14
+ state: "signed-out";
15
+ reason: string;
16
+ code?: string;
17
+ };
2
18
  interface AppProps {
3
19
  initialSkills?: string[];
4
20
  overwrite?: boolean;
21
+ initialAccess?: TuiAccess;
5
22
  }
6
- export declare function App({ initialSkills, overwrite }: AppProps): React.JSX.Element;
23
+ export declare function App({ initialSkills, overwrite, initialAccess }: AppProps): React.JSX.Element;
7
24
  export {};
@@ -4,6 +4,8 @@ interface SearchViewProps {
4
4
  onToggle: (name: string) => void;
5
5
  onConfirm: () => void;
6
6
  onBack: () => void;
7
+ /** A query starting with "/" is a slash command, run on Enter instead of searched. */
8
+ onCommand?: (line: string) => void;
7
9
  }
8
- export declare function SearchView({ selected, onToggle, onConfirm, onBack, }: SearchViewProps): React.JSX.Element;
10
+ export declare function SearchView({ selected, onToggle, onConfirm, onBack, onCommand, }: SearchViewProps): React.JSX.Element;
9
11
  export {};
@@ -0,0 +1,3 @@
1
+ /** Called once by the program's preAction hook, before `--profile` is applied. */
2
+ export declare function recordEnvironmentProfile(value: string | undefined): void;
3
+ export declare function getEnvironmentProfile(): string | undefined;
package/dist/index.js CHANGED
@@ -9611,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9611
9611
  // package.json
9612
9612
  var package_default = {
9613
9613
  name: "@hasna/skills",
9614
- version: "0.9.19",
9614
+ version: "0.10.0",
9615
9615
  description: "Skills library for AI coding agents",
9616
9616
  type: "module",
9617
9617
  bin: {
@@ -10684,13 +10684,15 @@ var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
10684
10684
  var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
10685
10685
  var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
10686
10686
  var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
10687
-
10688
10687
  class SkillsFleetCredentialError extends Error {
10689
10688
  code;
10690
- constructor(message, code = "MISSING_API_CREDENTIAL") {
10689
+ next;
10690
+ constructor(message, code = "MISSING_API_CREDENTIAL", next) {
10691
10691
  super(message);
10692
10692
  this.name = "SkillsFleetCredentialError";
10693
10693
  this.code = code;
10694
+ if (next)
10695
+ this.next = next;
10694
10696
  }
10695
10697
  }
10696
10698
  function isCredentialResolutionError(error) {
@@ -10748,7 +10750,7 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
10748
10750
  if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
10749
10751
  throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
10750
10752
  }
10751
- const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
10753
+ const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key, trust: "environment" }));
10752
10754
  if (new Set(normalized.map((entry) => entry.value)).size > 1)
10753
10755
  throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
10754
10756
  if (normalized[0])
@@ -10757,19 +10759,19 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
10757
10759
  for (const file of skillsProfileCredentialFiles(env, profile)) {
10758
10760
  const metadata = readSkillsInstanceMetadata(file);
10759
10761
  if (metadata.apiUrl || metadata.binding)
10760
- return { value: metadata.apiUrl ?? metadata.binding, source: file };
10762
+ return { value: metadata.apiUrl ?? metadata.binding, source: file, trust: "file" };
10761
10763
  }
10762
- return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
10764
+ return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default", trust: "default" };
10763
10765
  }
10764
10766
  const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
10765
10767
  if (fromKeychain)
10766
- return { value: fromKeychain.value.trim(), source: fromKeychain.source };
10768
+ return { value: fromKeychain.value.trim(), source: fromKeychain.source, trust: "keychain" };
10767
10769
  const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
10768
10770
  if (fromDisk?.unusable) {
10769
10771
  throw new SkillsFleetCredentialError(`${fromDisk.key} in ${fromDisk.path} is declared but blank or malformed; ` + `a Skills authority must be a valid https URL (or an exact loopback http URL).`, "INVALID_API_URL");
10770
10772
  }
10771
10773
  if (fromDisk)
10772
- return { value: fromDisk.value.trim(), source: fromDisk.path };
10774
+ return { value: fromDisk.value.trim(), source: fromDisk.path, trust: "file" };
10773
10775
  return null;
10774
10776
  }
10775
10777
  function skillsCredentialFiles(env = process.env) {
@@ -10832,13 +10834,13 @@ function resolveSkillsFleetOrThrow(env, options) {
10832
10834
  const credential = resolveCredential(SKILLS_APP, env, options.credentials);
10833
10835
  if (!credential) {
10834
10836
  if (!configured) {
10835
- throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills auth login`);
10837
+ throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills login`);
10836
10838
  }
10837
- 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 ${credentialLocations(env)}. Sign in with: skills auth login`);
10839
+ 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 ${credentialLocations(env)}. Sign in with: skills login`);
10838
10840
  }
10839
10841
  const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
10840
10842
  toV1BaseUrl(apiOrigin);
10841
- assertCredentialInstance(credential, apiOrigin, env, options);
10843
+ assertCredentialInstancePairing(credential, configured, apiOrigin, env, options);
10842
10844
  assertFilesUnchanged();
10843
10845
  const base = {
10844
10846
  mode: "hosted",
@@ -10852,7 +10854,7 @@ function resolveSkillsFleetOrThrow(env, options) {
10852
10854
  return { ...base, apiKey: null, apiKeyPointer: credential };
10853
10855
  }
10854
10856
  if (!credential.apiKey.trim()) {
10855
- throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills auth login`);
10857
+ throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills login`);
10856
10858
  }
10857
10859
  return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
10858
10860
  }
@@ -10860,17 +10862,49 @@ function credentialLocations(env) {
10860
10862
  const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
10861
10863
  return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
10862
10864
  }
10863
- function assertCredentialInstance(credential, apiOrigin, env, options) {
10864
- let bound;
10865
- if (credential.tier === "disk" || credential.tier === "profile" || credential.tier === "pointer" && credential.diskCandidates.includes(credential.source)) {
10866
- const metadata = readSkillsInstanceMetadata(credential.source);
10867
- bound = metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
10868
- } else if (credential.tier === "keychain") {
10869
- bound = keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
10865
+ function credentialPairing(credential, env, options = {}) {
10866
+ const fileBinding = (file) => {
10867
+ const metadata = readSkillsInstanceMetadata(file);
10868
+ return { kind: "bound", instance: normalizeSkillsApiOrigin(metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP)) };
10869
+ };
10870
+ const tier = credential.tier;
10871
+ switch (tier) {
10872
+ case "disk":
10873
+ case "profile":
10874
+ return fileBinding(credential.source);
10875
+ case "pointer":
10876
+ return credential.diskCandidates.includes(credential.source) ? fileBinding(credential.source) : { kind: "unbound" };
10877
+ case "keychain":
10878
+ return {
10879
+ kind: "bound",
10880
+ instance: normalizeSkillsApiOrigin(keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP))
10881
+ };
10882
+ case "env":
10883
+ case "override":
10884
+ case "argument":
10885
+ return { kind: "unbound" };
10886
+ default: {
10887
+ const unclassified = tier;
10888
+ throw new SkillsFleetCredentialError(`The Skills credential from ${credential.source} has an unclassified tier (${String(unclassified)}); refusing to send it.`, "INSTANCE_CREDENTIAL_MISMATCH");
10889
+ }
10870
10890
  }
10871
- if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
10891
+ }
10892
+ function assertCredentialInstancePairing(credential, configured, apiOrigin, env, options) {
10893
+ const pairing = credentialPairing(credential, env, options);
10894
+ if (pairing.kind === "bound") {
10895
+ if (pairing.instance === apiOrigin)
10896
+ return;
10872
10897
  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");
10873
10898
  }
10899
+ if (apiOrigin === normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
10900
+ return;
10901
+ if (configured?.trust === "environment")
10902
+ return;
10903
+ const next = [
10904
+ `set ${SKILLS_API_URL_ENV} alongside ${credential.source}`,
10905
+ `unset ${credential.source}, then run: skills login`
10906
+ ];
10907
+ throw new SkillsFleetCredentialError(`${credential.source} records no Skills server of its own, so it is only sent to the internal gateway or to a URL ` + `from the environment (${SKILLS_API_URL_ENV}); the Skills URL here comes from ${configured?.source ?? "nowhere"}. ` + `No credential was sent. To use your own server, ${next[0]}; otherwise ${next[1]}.`, "INSTANCE_CREDENTIAL_MISMATCH", next);
10874
10908
  }
10875
10909
  async function resolveSkillsApiKey(env = process.env, options = {}) {
10876
10910
  return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
@@ -10887,7 +10921,7 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
10887
10921
  return { ...fleet, apiKey: fleet.apiKey };
10888
10922
  const pointer = fleet.apiKeyPointer;
10889
10923
  if (!pointer) {
10890
- throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
10924
+ throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills login`);
10891
10925
  }
10892
10926
  let completed;
10893
10927
  try {
@@ -10954,7 +10988,7 @@ function requireSkillsFleet(action = "This command", env = process.env, options
10954
10988
  class MissingSkillsFleetError extends Error {
10955
10989
  code = "MISSING_API_URL";
10956
10990
  constructor(action = "This command") {
10957
- super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills auth login, or set ${SKILLS_API_KEY_ENV} ` + `(add the Keychain item hasna.credentials.${SKILLS_APP}.api-key, or write ~/.hasna/skills/config/credentials). ` + `Point at your own instance with ${SKILLS_API_URL_ENV}, or run: skills setup --api-url <your Skills instance origin>`);
10991
+ super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills login, or set ${SKILLS_API_KEY_ENV} ` + `(add the Keychain item hasna.credentials.${SKILLS_APP}.api-key, or write ~/.hasna/skills/config/credentials). ` + `Point at your own instance with ${SKILLS_API_URL_ENV}, or run: skills setup --api-url <your Skills instance origin>`);
10958
10992
  this.name = "MissingSkillsFleetError";
10959
10993
  }
10960
10994
  }
@@ -36498,7 +36532,7 @@ function date4(params) {
36498
36532
  }
36499
36533
  // src/lib/api-url.ts
36500
36534
  var API_URL_ENV_VAR = SKILLS_API_URL_ENV;
36501
- var MISSING_API_URL_HINT = `run: skills auth login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
36535
+ var MISSING_API_URL_HINT = `run: skills login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
36502
36536
  function resolveApiUrl(env = process.env, options = {}) {
36503
36537
  const fleet = resolveSkillsFleet(env, options);
36504
36538
  return fleet.mode === "hosted" ? fleet.apiOrigin : undefined;
@@ -40659,7 +40693,7 @@ class RemoteSkillsAuthClient {
40659
40693
  return (await this.sessionClient(email4, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
40660
40694
  }
40661
40695
  request(path, options) {
40662
- if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
40696
+ if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami", "/api/auth/logout"].includes(path))
40663
40697
  throw new Error("Unsupported authentication operation");
40664
40698
  return requestAuthApi(this.apiOrigin, path, options);
40665
40699
  }