@hasna/skills 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -57,7 +57,12 @@ import { join as join2, dirname } from "path";
57
57
  // src/lib/retired-settings.ts
58
58
  var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
59
59
  var RETIRED_CONFIG_KEYS = {
60
- mode: "apiUrl"
60
+ mode: "a configured API origin",
61
+ apiUrl: "skills setup --api-url <origin>"
62
+ };
63
+ var RETIRED_CONFIG_KEY_REASONS = {
64
+ mode: "Deployment modes were removed: a Skills client either resolves a credential " + "or it does not, and that is the whole of it.",
65
+ apiUrl: "The service address is no longer kept in this app's config file: it is read from " + "HASNA_SKILLS_API_URL, then the macOS Keychain item hasna.credentials.skills.api-url, " + "then ~/.hasna/skills/config/credentials, then the fleet gateway."
61
66
  };
62
67
 
63
68
  class RetiredSettingError extends Error {
@@ -96,7 +101,7 @@ function assertNoRetiredConfigKeys(config, source) {
96
101
  for (const [key, replacement] of Object.entries(RETIRED_CONFIG_KEYS)) {
97
102
  if (!(key in config))
98
103
  continue;
99
- throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` + "Deployment modes were removed: a Skills client either has an API origin " + "configured or it does not, and that is the whole of it. " + `Use "${replacement}" instead (skills config set ${replacement} <origin>), and ` + `remove the old key with: skills config unset ${key}. ` + "Refused rather than ignored, because silently dropping it would leave an " + "operator believing they had pointed this install at a server.");
104
+ throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` + `${RETIRED_CONFIG_KEY_REASONS[key] ?? ""} ` + `Use ${replacement} instead, and remove the old key with: skills config unset ${key}. ` + "Refused rather than ignored, because silently dropping it would leave an " + "operator believing they had pointed this install at a server.");
100
105
  }
101
106
  }
102
107
 
@@ -224,7 +229,7 @@ var ENUM_KEYS = {
224
229
  defaultScope: ["global", "project"],
225
230
  format: ["compact", "json", "csv"]
226
231
  };
227
- var STRING_KEYS = ["apiUrl", "extensionsDir"];
232
+ var STRING_KEYS = ["extensionsDir"];
228
233
  function validKeys() {
229
234
  return [...Object.keys(ENUM_KEYS), ...STRING_KEYS];
230
235
  }
@@ -255,16 +260,6 @@ function normalizeConfigValue(key, value) {
255
260
  const allowed = allowedValues(key);
256
261
  if (allowed)
257
262
  return allowed.includes(value) ? value : undefined;
258
- if (key === "apiUrl") {
259
- try {
260
- const url = new URL(value);
261
- if (url.protocol !== "http:" && url.protocol !== "https:")
262
- return;
263
- return value.replace(/\/+$/, "");
264
- } catch {
265
- return;
266
- }
267
- }
268
263
  if (key === "extensionsDir")
269
264
  return value.trim() ? value : undefined;
270
265
  return;
@@ -334,7 +329,7 @@ function saveConfig(key, value, scope = "project") {
334
329
  const normalized = normalizeConfigValue(key, value);
335
330
  if (normalized === undefined) {
336
331
  const allowed = allowedValues(key);
337
- throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected ${key === "apiUrl" ? "an http(s) URL" : "a non-empty path"}`);
332
+ throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected a non-empty path`);
338
333
  }
339
334
  const filePath = getConfigPath(scope);
340
335
  let existing = {};
@@ -1842,7 +1837,7 @@ function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)
1842
1837
  const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
1843
1838
  const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
1844
1839
  const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
1845
- const version = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
1840
+ const version = readDeclaredSkillVersion(skillPath) ?? PORTABLE_SKILL_DEFAULT_VERSION;
1846
1841
  const kind = parseSkillKind(stringField(jsonManifest, "kind") ?? frontmatter?.kind);
1847
1842
  const commands = parseManifestCommands(jsonManifest) ?? (kind === "instruction" ? [] : inferPackageCommands(pkg, name)) ?? [];
1848
1843
  return {
@@ -1866,6 +1861,15 @@ function parseSkillKind(value) {
1866
1861
  return value;
1867
1862
  return;
1868
1863
  }
1864
+ function readDeclaredSkillVersion(skillPath) {
1865
+ const skillJsonPath = join6(skillPath, "skill.json");
1866
+ const skillMdPath = join6(skillPath, "SKILL.md");
1867
+ const pkgPath = join6(skillPath, "package.json");
1868
+ const jsonManifest = existsSync6(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
1869
+ const frontmatter = existsSync6(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
1870
+ const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
1871
+ return stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version);
1872
+ }
1869
1873
  function createInstructionManifest(name, options) {
1870
1874
  return {
1871
1875
  $schema: PORTABLE_SKILL_SCHEMA,
@@ -4181,7 +4185,8 @@ function getSkillRequirements(name) {
4181
4185
  }
4182
4186
  }
4183
4187
  envVars.delete("SKILL_API_KEY");
4184
- envVars.add("SKILLS_API_KEY");
4188
+ envVars.delete("SKILLS_API_KEY");
4189
+ envVars.add("HASNA_SKILLS_API_KEY");
4185
4190
  }
4186
4191
  const systemDeps = new Set;
4187
4192
  const depPatterns = [
@@ -8400,65 +8405,815 @@ var coerce = {
8400
8405
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
8401
8406
  };
8402
8407
  var NEVER = INVALID;
8403
- // src/lib/api-url.ts
8404
- var API_URL_ENV_VAR = "SKILLS_API_URL";
8405
- var API_URL_CONFIG_KEY = "apiUrl";
8406
- var MISSING_API_URL_HINT = `set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
8408
+ // ../contracts/dist/client/transport.js
8409
+ import { isIP } from "net";
8410
+ import { spawnSync } from "child_process";
8411
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync12 } from "fs";
8412
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
8413
+ import { createRequire } from "module";
8414
+ import { hostname as osHostname } from "os";
8415
+ import { isAbsolute as isAbsolute3, join as join14 } from "path";
8416
+ function envToken(name) {
8417
+ return name.toUpperCase().replace(/-/g, "_");
8418
+ }
8419
+ function clientTransportEnvKeys(name) {
8420
+ const envSegment = envToken(name);
8421
+ return {
8422
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
8423
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
8424
+ };
8425
+ }
8426
+ function credentialOverrideEnvKey(name) {
8427
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
8428
+ }
8429
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
8430
+ function credentialPointerEnvKey(name) {
8431
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
8432
+ }
8407
8433
 
8408
- class MissingApiUrlError extends Error {
8409
- code = "MISSING_API_URL";
8410
- constructor(action = "This command") {
8411
- super(`${action} requires a Skills API URL and none is configured \u2014 ${MISSING_API_URL_HINT}`);
8412
- this.name = "MissingApiUrlError";
8434
+ class CredentialResolutionError extends Error {
8435
+ appName;
8436
+ attempted;
8437
+ constructor(appName, message, attempted) {
8438
+ super(message);
8439
+ this.name = "CredentialResolutionError";
8440
+ this.appName = appName;
8441
+ this.attempted = attempted;
8413
8442
  }
8414
8443
  }
8415
- function resolveApiUrl(config = loadConfig(), env = process.env) {
8416
- const raw = env[API_URL_ENV_VAR] || config[API_URL_CONFIG_KEY];
8417
- const trimmed = raw?.trim().replace(/\/+$/, "");
8418
- return trimmed || undefined;
8444
+
8445
+ class CredentialFileUnsafeError extends Error {
8446
+ path;
8447
+ constructor(path, reason) {
8448
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
8449
+ this.name = "CredentialFileUnsafeError";
8450
+ this.path = path;
8451
+ }
8419
8452
  }
8420
- function requireApiUrl(action = "This command", config, env) {
8421
- const resolved = resolveApiUrl(config ?? loadConfig(), env ?? process.env);
8422
- if (!resolved)
8423
- throw new MissingApiUrlError(action);
8424
- return resolved;
8453
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
8454
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
8455
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
8456
+ var HASNA_HOME_DIR = ".hasna";
8457
+ var CONFIG_SUBDIR = "config";
8458
+ var CREDENTIALS_FILE = "credentials";
8459
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
8460
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
8461
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
8462
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
8463
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
8464
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
8465
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
8466
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
8467
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
8468
+ function homeDir(env) {
8469
+ const home = env.HOME?.trim();
8470
+ return home ? home : null;
8471
+ }
8472
+ function absoluteOverride(env, key) {
8473
+ const value = env[key]?.trim();
8474
+ return value && isAbsolute3(value) ? value : null;
8475
+ }
8476
+ function hasnaHomeDir(env) {
8477
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
8478
+ if (override)
8479
+ return override;
8480
+ const home = homeDir(env);
8481
+ return home ? join14(home, HASNA_HOME_DIR) : null;
8482
+ }
8483
+ function appConfigDir(name, env) {
8484
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
8485
+ if (configRoot)
8486
+ return join14(configRoot, name);
8487
+ const root = hasnaHomeDir(env);
8488
+ return root ? join14(root, name, CONFIG_SUBDIR) : null;
8489
+ }
8490
+ function credentialDiskSourceList(name, env, profile = null) {
8491
+ if (!SAFE_APP_SLUG.test(name))
8492
+ return [];
8493
+ const directory = appConfigDir(name, env);
8494
+ if (!directory)
8495
+ return [];
8496
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
8497
+ return [{ path: join14(directory, file), tier: "disk" }];
8425
8498
  }
8426
-
8427
- // src/lib/auth-store.ts
8428
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync } from "fs";
8429
- import { dirname as dirname6, join as join14 } from "path";
8430
- import { homedir as homedir4 } from "os";
8431
- function getAuthFilePath() {
8432
- return join14(getDataDir(), "auth.json");
8499
+ function credentialDiskSources(name, env) {
8500
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
8501
+ }
8502
+ function profileDiskSources(name, env, profile) {
8503
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
8504
+ }
8505
+ function parseEnvFile(text) {
8506
+ const values = new Map;
8507
+ const unusable = new Set;
8508
+ for (const rawLine of text.split(/\r?\n/)) {
8509
+ const line = rawLine.trim();
8510
+ if (line.length === 0 || line.startsWith("#"))
8511
+ continue;
8512
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
8513
+ const equals = withoutExport.indexOf("=");
8514
+ if (equals <= 0)
8515
+ continue;
8516
+ const key = withoutExport.slice(0, equals).trim();
8517
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
8518
+ continue;
8519
+ let value = withoutExport.slice(equals + 1).trim();
8520
+ const quote = value[0];
8521
+ if (quote === '"' || quote === "'") {
8522
+ if (value.length < 2 || !value.endsWith(quote)) {
8523
+ unusable.add(key);
8524
+ continue;
8525
+ }
8526
+ value = value.slice(1, -1);
8527
+ }
8528
+ if (value.trim().length === 0) {
8529
+ unusable.add(key);
8530
+ continue;
8531
+ }
8532
+ if (values.has(key) && values.get(key) !== value)
8533
+ unusable.add(key);
8534
+ values.set(key, value);
8535
+ }
8536
+ return { values, unusable };
8433
8537
  }
8434
- function legacyAuthFilePath() {
8435
- return join14(process.env["HOME"] || process.env["USERPROFILE"] || homedir4(), ".skills", "auth.json");
8538
+ function configFileModeAllowed(mode) {
8539
+ const permissions = mode & 4095;
8540
+ return permissions === 256 || permissions === 384;
8436
8541
  }
8437
- var cachedConfig;
8438
- function getAuthConfig() {
8439
- if (cachedConfig !== undefined)
8440
- return cachedConfig;
8542
+ function configFileReadsCoherent(before, after) {
8543
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
8544
+ }
8545
+ function readAppConfigFile(path) {
8546
+ const unsafe = (reason) => {
8547
+ throw new CredentialFileUnsafeError(path, reason);
8548
+ };
8549
+ let fd = -1;
8441
8550
  try {
8442
- const file = existsSync14(getAuthFilePath()) ? getAuthFilePath() : legacyAuthFilePath();
8443
- const raw = readFileSync12(file, "utf-8");
8444
- const config = JSON.parse(raw);
8445
- if (!config.apiKey) {
8446
- cachedConfig = null;
8551
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
8552
+ } catch (error) {
8553
+ const code = error.code;
8554
+ if (code === "ENOENT" || code === "ENOTDIR")
8447
8555
  return null;
8556
+ if (code === "ELOOP")
8557
+ unsafe("the path is a symlink");
8558
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
8559
+ }
8560
+ try {
8561
+ const before = fstatSync(fd);
8562
+ if (!before.isFile())
8563
+ unsafe("the path is not a regular file");
8564
+ if (!configFileModeAllowed(before.mode)) {
8565
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
8566
+ }
8567
+ const uid = process.getuid?.() ?? process.geteuid?.();
8568
+ if (uid !== undefined && before.uid !== uid)
8569
+ unsafe("the file is not owned by the current user");
8570
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
8571
+ unsafe("the file exceeds the size limit");
8572
+ const bytes = readFileSync12(fd);
8573
+ const after = fstatSync(fd);
8574
+ if (!configFileReadsCoherent(before, after)) {
8575
+ unsafe("the file changed while being read");
8576
+ }
8577
+ return parseEnvFile(bytes.toString("utf8"));
8578
+ } finally {
8579
+ if (fd !== -1)
8580
+ closeSync(fd);
8581
+ }
8582
+ }
8583
+ function readCredentialFile(path, apiKeyKeys) {
8584
+ const parsed = readAppConfigFile(path);
8585
+ if (!parsed)
8586
+ return null;
8587
+ for (const key of apiKeyKeys) {
8588
+ if (parsed.unusable.has(key)) {
8589
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
8448
8590
  }
8449
- cachedConfig = config;
8450
- return config;
8451
- } catch {
8452
- cachedConfig = null;
8591
+ }
8592
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
8593
+ if (new Set(values).size > 1) {
8594
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
8595
+ }
8596
+ return values[0] ?? null;
8597
+ }
8598
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
8599
+ function appConfigDiskValue(name, env, keys) {
8600
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
8601
+ if (wanted.length === 0)
8602
+ return null;
8603
+ for (const path of credentialDiskSources(name, env)) {
8604
+ const parsed = readAppConfigFile(path);
8605
+ if (!parsed)
8606
+ continue;
8607
+ if (wanted.some((key) => parsed.unusable.has(key))) {
8608
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
8609
+ }
8610
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
8611
+ if (new Set(values).size > 1)
8612
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
8613
+ for (const key of wanted) {
8614
+ if (parsed.unusable.has(key))
8615
+ return { key, value: "", path, unusable: true };
8616
+ const value = parsed.values.get(key)?.trim();
8617
+ if (value)
8618
+ return { key, value, path };
8619
+ }
8620
+ }
8621
+ return null;
8622
+ }
8623
+ function assertUsableCredential(appName, source, value) {
8624
+ if (VAULT_POINTER_SHAPE.test(value)) {
8625
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
8626
+ }
8627
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
8628
+ return;
8629
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
8630
+ }
8631
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
8632
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
8633
+ function sealCredential(fields) {
8634
+ const { apiKey } = fields;
8635
+ const visible = {
8636
+ tier: fields.tier,
8637
+ source: fields.source,
8638
+ deliberate: fields.deliberate,
8639
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
8640
+ warning: fields.warning
8641
+ };
8642
+ const sealed = { ...visible };
8643
+ Object.defineProperty(sealed, "apiKey", {
8644
+ value: apiKey,
8645
+ enumerable: false,
8646
+ writable: false,
8647
+ configurable: false
8648
+ });
8649
+ if (fields.pointerVaultKey !== undefined) {
8650
+ Object.defineProperty(sealed, "pointerVaultKey", {
8651
+ value: fields.pointerVaultKey,
8652
+ enumerable: false,
8653
+ writable: false,
8654
+ configurable: false
8655
+ });
8656
+ }
8657
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
8658
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
8659
+ enumerable: false,
8660
+ writable: false,
8661
+ configurable: false
8662
+ });
8663
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
8664
+ value: true,
8665
+ enumerable: false,
8666
+ writable: false,
8667
+ configurable: false
8668
+ });
8669
+ return Object.freeze(sealed);
8670
+ }
8671
+ function firstEnvValue(env, keys) {
8672
+ for (const key of keys) {
8673
+ if (!Object.prototype.hasOwnProperty.call(env, key))
8674
+ continue;
8675
+ const value = env[key]?.trim();
8676
+ if (value)
8677
+ return { key, value };
8678
+ }
8679
+ return null;
8680
+ }
8681
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
8682
+ function isAmbientEnvironment(env) {
8683
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
8684
+ }
8685
+ function defaultKeychainRunner(argv) {
8686
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
8687
+ encoding: "utf8",
8688
+ stdio: ["ignore", "pipe", "pipe"],
8689
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
8690
+ });
8691
+ return {
8692
+ status: result.status,
8693
+ stdout: result.stdout ?? "",
8694
+ stderr: result.error ? result.error.message : result.stderr ?? ""
8695
+ };
8696
+ }
8697
+ function keychainTierEnabled(env, options) {
8698
+ if ((options.platform ?? process.platform) !== "darwin")
8699
+ return false;
8700
+ if (options.enabled !== undefined)
8701
+ return options.enabled;
8702
+ return options.run !== undefined || isAmbientEnvironment(env);
8703
+ }
8704
+ function keychainAccount(env, options) {
8705
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
8706
+ if (station)
8707
+ return station;
8708
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
8709
+ if (host)
8710
+ return host;
8711
+ const user = env.USER?.trim();
8712
+ return user || null;
8713
+ }
8714
+ function keychainFailureHint(text) {
8715
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
8716
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
8717
+ return clean ? `: ${clean}` : "";
8718
+ }
8719
+ function readKeychainItem(name, env, kind, options) {
8720
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
8721
+ return null;
8722
+ const account = keychainAccount(env, options);
8723
+ if (!account)
8724
+ return null;
8725
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
8726
+ const source = `keychain:${service}@${account}`;
8727
+ const run = options.run ?? defaultKeychainRunner;
8728
+ let result;
8729
+ try {
8730
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
8731
+ } catch (error) {
8732
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
8733
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
8734
+ }
8735
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
8453
8736
  return null;
8737
+ if (result.status !== 0) {
8738
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
8739
+ }
8740
+ const value = result.stdout.trim();
8741
+ if (!value) {
8742
+ throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
8743
+ }
8744
+ return { value, source };
8745
+ }
8746
+ function keychainConfigValue(name, env, options = {}) {
8747
+ return readKeychainItem(name, env, "api-url", options);
8748
+ }
8749
+ function snapshotClientEnvironment(name, env) {
8750
+ const keys = clientTransportEnvKeys(name);
8751
+ const ambient = isAmbientEnvironment(env);
8752
+ const snapshot = Object.create(null);
8753
+ for (const key of [
8754
+ ...keys.apiUrlKeys,
8755
+ ...keys.apiKeyKeys,
8756
+ credentialOverrideEnvKey(name),
8757
+ credentialPointerEnvKey(name),
8758
+ CREDENTIAL_PROFILE_ENV_KEY,
8759
+ "HOME",
8760
+ HASNA_HOME_ENV_KEY,
8761
+ HASNA_CONFIG_HOME_ENV_KEY,
8762
+ KEYCHAIN_STATION_ENV_KEY,
8763
+ "USER"
8764
+ ]) {
8765
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
8766
+ if (!descriptor)
8767
+ continue;
8768
+ if (!("value" in descriptor)) {
8769
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
8770
+ }
8771
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
8772
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
8773
+ }
8774
+ snapshot[key] = descriptor.value;
8775
+ }
8776
+ if (ambient) {
8777
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
8778
+ value: true,
8779
+ enumerable: false,
8780
+ writable: false,
8781
+ configurable: false
8782
+ });
8783
+ }
8784
+ return Object.freeze(snapshot);
8785
+ }
8786
+ function resolveCredential(name, env, options = {}) {
8787
+ env = snapshotClientEnvironment(name, env);
8788
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
8789
+ const diskPaths = credentialDiskSources(name, env);
8790
+ if (options.apiKey !== undefined) {
8791
+ const explicitKey = options.apiKey.trim();
8792
+ if (!explicitKey) {
8793
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
8794
+ }
8795
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
8796
+ return sealCredential({
8797
+ apiKey: explicitKey,
8798
+ tier: "argument",
8799
+ source: "explicit apiKey argument",
8800
+ deliberate: true,
8801
+ diskCandidates: diskPaths,
8802
+ warning: null
8803
+ });
8804
+ }
8805
+ const overrideKeyName = credentialOverrideEnvKey(name);
8806
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
8807
+ if (overrideRaw !== undefined) {
8808
+ const override = overrideRaw.trim();
8809
+ if (!override) {
8810
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
8811
+ }
8812
+ assertUsableCredential(name, overrideKeyName, override);
8813
+ return sealCredential({
8814
+ apiKey: override,
8815
+ tier: "override",
8816
+ source: overrideKeyName,
8817
+ deliberate: true,
8818
+ diskCandidates: diskPaths,
8819
+ warning: null
8820
+ });
8821
+ }
8822
+ const pointerKeyName = credentialPointerEnvKey(name);
8823
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
8824
+ if (pointerRaw !== undefined) {
8825
+ const pointer = pointerRaw.trim();
8826
+ if (!pointer) {
8827
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
8828
+ }
8829
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
8830
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
8831
+ }
8832
+ return sealCredential({
8833
+ apiKey: "",
8834
+ pointerVaultKey: pointer,
8835
+ tier: "pointer",
8836
+ source: pointerKeyName,
8837
+ deliberate: true,
8838
+ diskCandidates: diskPaths,
8839
+ warning: null
8840
+ });
8841
+ }
8842
+ if (options.profile !== undefined && !options.profile.trim()) {
8843
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
8844
+ }
8845
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
8846
+ if (profileRaw !== undefined && !profileRaw.trim()) {
8847
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
8848
+ }
8849
+ const profile = options.profile?.trim() || profileRaw?.trim();
8850
+ if (profile) {
8851
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
8852
+ if (!SAFE_PROFILE.test(profile)) {
8853
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
8854
+ }
8855
+ const paths = profileDiskSources(name, env, profile);
8856
+ for (const path of paths) {
8857
+ const value = readCredentialFile(path, apiKeyKeys);
8858
+ if (value) {
8859
+ assertUsableCredential(name, path, value);
8860
+ return sealCredential({
8861
+ apiKey: value,
8862
+ tier: "profile",
8863
+ source: path,
8864
+ deliberate: true,
8865
+ diskCandidates: paths,
8866
+ warning: null
8867
+ });
8868
+ }
8869
+ }
8870
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
8871
+ }
8872
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
8873
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
8874
+ if (blankEnv) {
8875
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
8876
+ }
8877
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
8878
+ throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
8879
+ }
8880
+ const envHit = firstEnvValue(env, apiKeyKeys);
8881
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
8882
+ if (keychainHit) {
8883
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
8884
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
8885
+ return sealCredential({
8886
+ apiKey: keychainHit.value,
8887
+ tier: "keychain",
8888
+ source: keychainHit.source,
8889
+ deliberate: false,
8890
+ diskCandidates: diskPaths,
8891
+ warning
8892
+ });
8893
+ }
8894
+ const diskSourceList = credentialDiskSourceList(name, env, null);
8895
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
8896
+ if (diskHits.length > 0) {
8897
+ const winner = diskHits[0];
8898
+ assertUsableCredential(name, winner.src.path, winner.value);
8899
+ const divergentSources = [
8900
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
8901
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
8902
+ ];
8903
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
8904
+ return sealCredential({
8905
+ apiKey: winner.value,
8906
+ tier: winner.src.tier,
8907
+ source: winner.src.path,
8908
+ deliberate: false,
8909
+ diskCandidates: diskPaths,
8910
+ warning
8911
+ });
8912
+ }
8913
+ if (envHit) {
8914
+ assertUsableCredential(name, envHit.key, envHit.value);
8915
+ return sealCredential({
8916
+ apiKey: envHit.value,
8917
+ tier: "env",
8918
+ source: envHit.key,
8919
+ deliberate: false,
8920
+ diskCandidates: diskPaths,
8921
+ warning: null
8922
+ });
8923
+ }
8924
+ return null;
8925
+ }
8926
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
8927
+ var requireSecretsSdk = createRequire(import.meta.url);
8928
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
8929
+ const vaultKey = pointerResolution.pointerVaultKey;
8930
+ const pointerEnvKey = pointerResolution.source;
8931
+ if (!vaultKey) {
8932
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
8933
+ }
8934
+ let secretsSdk;
8935
+ try {
8936
+ secretsSdk = requireSecretsSdk(SECRETS_PACKAGE_SPECIFIER);
8937
+ } catch {
8938
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
8939
+ }
8940
+ let client;
8941
+ try {
8942
+ client = secretsSdk.createSecretsClientFromEnv(env);
8943
+ } catch {
8944
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
8945
+ }
8946
+ let secret;
8947
+ try {
8948
+ secret = await client.getSecret({ key: vaultKey });
8949
+ } catch {
8950
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
8951
+ }
8952
+ const value = secret.value;
8953
+ if (!value) {
8954
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
8955
+ }
8956
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
8957
+ return sealCredential({
8958
+ apiKey: value,
8959
+ tier: "pointer",
8960
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
8961
+ deliberate: true,
8962
+ diskCandidates: pointerResolution.diskCandidates,
8963
+ warning: null
8964
+ });
8965
+ }
8966
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
8967
+ var DEFAULT_AUTHORITY_SOURCE = "default";
8968
+ function defaultFleetGatewayBaseUrl(name) {
8969
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
8970
+ }
8971
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
8972
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
8973
+ function isValidDnsDomain(value) {
8974
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
8975
+ return false;
8454
8976
  }
8977
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
8455
8978
  }
8456
- function getApiKey() {
8457
- if (process.env.SKILLS_API_KEY)
8458
- return process.env.SKILLS_API_KEY;
8459
- if (process.env.SKILL_API_KEY)
8460
- return process.env.SKILL_API_KEY;
8461
- return getAuthConfig()?.apiKey || null;
8979
+ function validateAppSlug(name) {
8980
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
8981
+ throw new Error("App name must be one lowercase DNS label.");
8982
+ }
8983
+ return name;
8984
+ }
8985
+ function rawAuthority(value) {
8986
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
8987
+ if (!match)
8988
+ throw new Error("API URL must be absolute.");
8989
+ const afterScheme = value.slice(match[0].length);
8990
+ const boundary = afterScheme.search(/[/?#]/);
8991
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
8992
+ if (!authority)
8993
+ throw new Error("API URL must include a hostname.");
8994
+ return authority;
8995
+ }
8996
+ function assertCanonicalPort(port) {
8997
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
8998
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
8999
+ }
9000
+ const numericPort = Number(port);
9001
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
9002
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
9003
+ }
9004
+ }
9005
+ function canonicalAuthorityHostname(authority) {
9006
+ let rawHostname;
9007
+ if (authority.startsWith("[")) {
9008
+ const closingBracket = authority.indexOf("]");
9009
+ if (closingBracket === -1) {
9010
+ throw new Error("API URL authority must contain a canonical hostname.");
9011
+ }
9012
+ rawHostname = authority.slice(0, closingBracket + 1);
9013
+ const portSuffix = authority.slice(closingBracket + 1);
9014
+ if (portSuffix) {
9015
+ if (!portSuffix.startsWith(":")) {
9016
+ throw new Error("API URL authority must contain a canonical hostname and port.");
9017
+ }
9018
+ assertCanonicalPort(portSuffix.slice(1));
9019
+ }
9020
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
9021
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
9022
+ }
9023
+ } else {
9024
+ const firstColon = authority.indexOf(":");
9025
+ const lastColon = authority.lastIndexOf(":");
9026
+ if (firstColon !== lastColon) {
9027
+ throw new Error("IPv6 API URL authorities must use brackets.");
9028
+ }
9029
+ if (lastColon !== -1) {
9030
+ const port = authority.slice(lastColon + 1);
9031
+ assertCanonicalPort(port);
9032
+ rawHostname = authority.slice(0, lastColon);
9033
+ } else {
9034
+ rawHostname = authority;
9035
+ }
9036
+ const ipVersion = isIP(rawHostname);
9037
+ const numericAddressParts = rawHostname.split(".");
9038
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
9039
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
9040
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
9041
+ }
9042
+ }
9043
+ return rawHostname.toLowerCase();
9044
+ }
9045
+ function isDeliberateLoopbackHttpAuthority(authority) {
9046
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
9047
+ }
9048
+ function toV1BaseUrl(apiUrl) {
9049
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
9050
+ throw new Error("API URL must not contain ASCII control characters.");
9051
+ }
9052
+ const input = apiUrl.trim();
9053
+ const authority = rawAuthority(input);
9054
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
9055
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
9056
+ }
9057
+ const canonicalHostname = canonicalAuthorityHostname(authority);
9058
+ const url = new URL(input);
9059
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
9060
+ throw new Error("API URL must use http or https.");
9061
+ }
9062
+ if (url.username || url.password) {
9063
+ throw new Error("API URL must not include credentials.");
9064
+ }
9065
+ if (!url.hostname || url.hostname.endsWith(".")) {
9066
+ throw new Error("API URL must include a canonical hostname.");
9067
+ }
9068
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
9069
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
9070
+ }
9071
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
9072
+ throw new Error("API URL must not use IDN or punycode hostnames.");
9073
+ }
9074
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
9075
+ throw new Error("API URL may use http only for an exact loopback authority.");
9076
+ }
9077
+ if (url.search || url.hash) {
9078
+ throw new Error("API URL must not include a query string or fragment.");
9079
+ }
9080
+ let path = url.pathname.replace(/\/+$/, "");
9081
+ if (path.endsWith("/v1"))
9082
+ path = path.slice(0, -"/v1".length);
9083
+ url.pathname = `${path}/v1`;
9084
+ return url.toString().replace(/\/+$/, "");
9085
+ }
9086
+ class ClientTransportConfigurationError extends Error {
9087
+ appName;
9088
+ sources;
9089
+ constructor(appName, message, sources = []) {
9090
+ super(message);
9091
+ this.name = "ClientTransportConfigurationError";
9092
+ this.appName = appName;
9093
+ this.sources = Object.freeze([...sources]);
9094
+ }
9095
+ }
9096
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
9097
+ env = snapshotClientEnvironment(name, env);
9098
+ const keys = clientTransportEnvKeys(name);
9099
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
9100
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
9101
+ if (blankUrl) {
9102
+ 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]);
9103
+ }
9104
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
9105
+ if (controlledUrl) {
9106
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
9107
+ }
9108
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
9109
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
9110
+ 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));
9111
+ }
9112
+ const envUrlHit = usableUrlEntries[0] ?? null;
9113
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
9114
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
9115
+ if (diskConfigUrlHit?.unusable) {
9116
+ 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]);
9117
+ }
9118
+ const urlCandidates = [
9119
+ ...envUrlHit ? [envUrlHit] : [],
9120
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
9121
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
9122
+ ];
9123
+ const configuredUrl = urlCandidates[0] ?? null;
9124
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
9125
+ if (configuredUrl && divergentUrls.length > 0) {
9126
+ 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));
9127
+ }
9128
+ const warnings = [];
9129
+ if (configuredUrl && !envUrlHit) {
9130
+ 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.`);
9131
+ }
9132
+ const credential = resolveCredential(name, env, options.credentials);
9133
+ if (!credential) {
9134
+ const diskHint = credentialDiskSourcesForMessage(name, env);
9135
+ 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`;
9136
+ 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.`);
9137
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
9138
+ }
9139
+ if (credential.warning)
9140
+ warnings.push(credential.warning);
9141
+ let urlHit;
9142
+ if (configuredUrl) {
9143
+ urlHit = configuredUrl;
9144
+ } else {
9145
+ try {
9146
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
9147
+ } catch (error) {
9148
+ const message = error instanceof Error ? error.message : String(error);
9149
+ 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]]);
9150
+ }
9151
+ }
9152
+ const apiUrlSource = urlHit.key;
9153
+ let baseUrl;
9154
+ try {
9155
+ baseUrl = toV1BaseUrl(urlHit.value);
9156
+ } catch (error) {
9157
+ const message = error instanceof Error ? error.message : String(error);
9158
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
9159
+ }
9160
+ return {
9161
+ resolution: {
9162
+ transport: "http",
9163
+ transportSource: urlHit.key,
9164
+ baseUrl,
9165
+ apiUrlSource,
9166
+ apiKeyPresent: true,
9167
+ apiKeySource: credential.source,
9168
+ apiKeyTier: credential.tier,
9169
+ misconfigured: false,
9170
+ warning: warnings.length > 0 ? warnings.join(" ") : null
9171
+ },
9172
+ credential
9173
+ };
9174
+ }
9175
+ function resolveClientTransport(name, env = process.env, options = {}) {
9176
+ return resolveClientTransportSnapshot(name, env, options).resolution;
9177
+ }
9178
+ function credentialDiskSourcesForMessage(name, env) {
9179
+ const paths = credentialDiskSources(name, env);
9180
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
9181
+ }
9182
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
9183
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
9184
+ "host",
9185
+ ":authority",
9186
+ "forwarded",
9187
+ "x-forwarded-host",
9188
+ "x-original-host"
9189
+ ]);
9190
+
9191
+ // src/lib/fleet-credentials.ts
9192
+ var SKILLS_APP = "skills";
9193
+ var ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
9194
+ var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
9195
+ var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
9196
+ var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
9197
+ var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
9198
+
9199
+ class SkillsFleetCredentialError extends Error {
9200
+ code;
9201
+ constructor(message, code = "MISSING_API_CREDENTIAL") {
9202
+ super(message);
9203
+ this.name = "SkillsFleetCredentialError";
9204
+ this.code = code;
9205
+ }
9206
+ }
9207
+ function isClientTransportConfigurationError(error) {
9208
+ return error instanceof ClientTransportConfigurationError || typeof error === "object" && error !== null && error.name === "ClientTransportConfigurationError";
9209
+ }
9210
+ function isCredentialResolutionError(error) {
9211
+ return error instanceof CredentialResolutionError || typeof error === "object" && error !== null && error.name === "CredentialResolutionError";
9212
+ }
9213
+ function asSkillsFleetCredentialError(error) {
9214
+ if (!isCredentialResolutionError(error))
9215
+ return null;
9216
+ return new SkillsFleetCredentialError(error.message, "MISSING_API_CREDENTIAL");
8462
9217
  }
8463
9218
  function normalizeSkillsApiOrigin(apiUrl) {
8464
9219
  const url = new URL(apiUrl);
@@ -8472,8 +9227,172 @@ function normalizeSkillsApiOrigin(apiUrl) {
8472
9227
  }
8473
9228
  return url.toString().replace(/\/+$/, "");
8474
9229
  }
8475
- function getApiUrl(action) {
8476
- return normalizeSkillsApiOrigin(requireApiUrl(action));
9230
+ function configuredSkillsApiUrl(env = process.env, keychain) {
9231
+ for (const key of SKILLS_API_URL_ENV_KEYS) {
9232
+ const value = env[key]?.trim();
9233
+ if (value)
9234
+ return { value, source: key };
9235
+ }
9236
+ const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
9237
+ if (fromKeychain)
9238
+ return { value: fromKeychain.value.trim(), source: fromKeychain.source };
9239
+ const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
9240
+ if (fromDisk?.unusable) {
9241
+ 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");
9242
+ }
9243
+ if (fromDisk)
9244
+ return { value: fromDisk.value.trim(), source: fromDisk.path };
9245
+ return null;
9246
+ }
9247
+ function skillsCredentialFiles(env = process.env) {
9248
+ return credentialDiskSources(SKILLS_APP, env);
9249
+ }
9250
+ function skillsCredentialFilePath(env = process.env) {
9251
+ const paths = skillsCredentialFiles(env);
9252
+ const path = paths[0];
9253
+ if (!path) {
9254
+ throw new Error("No home directory is set (HOME or HASNA_HOME), so there is nowhere to store a Skills credential.");
9255
+ }
9256
+ return path;
9257
+ }
9258
+ var localNoticePrinted = false;
9259
+ function noticeLocalSkillsMode(write = (line) => console.error(line)) {
9260
+ if (localNoticePrinted)
9261
+ return;
9262
+ localNoticePrinted = true;
9263
+ write(`skills: local mode \u2014 no ${SKILLS_API_KEY_ENV} and no ${SKILLS_API_URL_ENV} resolved, ` + `so this runs on this machine against the bundled corpus. ` + `Sign in with: skills auth login`);
9264
+ }
9265
+ function resolveSkillsFleet(env = process.env, options = {}) {
9266
+ try {
9267
+ return resolveSkillsFleetOrThrow(env, options);
9268
+ } catch (error) {
9269
+ const translated = asSkillsFleetCredentialError(error);
9270
+ if (translated)
9271
+ throw translated;
9272
+ throw error;
9273
+ }
9274
+ }
9275
+ function resolveSkillsFleetOrThrow(env, options) {
9276
+ let resolution;
9277
+ try {
9278
+ resolution = resolveClientTransport(SKILLS_APP, env, { credentials: options.credentials });
9279
+ } catch (error) {
9280
+ if (!isClientTransportConfigurationError(error))
9281
+ throw error;
9282
+ const configured2 = configuredSkillsApiUrl(env, options.credentials?.keychain);
9283
+ const credential2 = resolveCredential(SKILLS_APP, env, options.credentials);
9284
+ if (!configured2 && !credential2) {
9285
+ if (env === process.env)
9286
+ noticeLocalSkillsMode();
9287
+ return { mode: "local", apiOrigin: null, apiKey: null };
9288
+ }
9289
+ if (configured2 && !credential2) {
9290
+ 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`);
9291
+ }
9292
+ throw error;
9293
+ }
9294
+ const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
9295
+ const apiOrigin = configured ? normalizeSkillsApiOrigin(configured.value) : stripV1(resolution.baseUrl);
9296
+ const credential = resolveCredential(SKILLS_APP, env, options.credentials);
9297
+ if (!credential) {
9298
+ throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
9299
+ }
9300
+ const base = {
9301
+ mode: "hosted",
9302
+ apiOrigin,
9303
+ apiUrlSource: resolution.apiUrlSource ?? (configured?.source ?? "default"),
9304
+ apiKeySource: resolution.apiKeySource ?? credential.source,
9305
+ apiKeyTier: resolution.apiKeyTier,
9306
+ warning: resolution.warning
9307
+ };
9308
+ if (credential.tier === "pointer") {
9309
+ return { ...base, apiKey: null, apiKeyPointer: credential };
9310
+ }
9311
+ if (!credential.apiKey.trim()) {
9312
+ 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`);
9313
+ }
9314
+ return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
9315
+ }
9316
+ async function resolveSkillsApiKey(env = process.env, options = {}) {
9317
+ const fleet = resolveSkillsFleet(env, options);
9318
+ if (fleet.mode !== "hosted")
9319
+ return null;
9320
+ if (fleet.apiKey)
9321
+ return fleet.apiKey;
9322
+ const pointer = fleet.apiKeyPointer;
9323
+ if (!pointer) {
9324
+ throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
9325
+ }
9326
+ let completed;
9327
+ try {
9328
+ completed = await completePointerCredential(SKILLS_APP, pointer, env);
9329
+ } catch (error) {
9330
+ const translated = asSkillsFleetCredentialError(error);
9331
+ if (translated)
9332
+ throw translated;
9333
+ throw error;
9334
+ }
9335
+ if (!completed.apiKey?.trim()) {
9336
+ throw new SkillsFleetCredentialError(`${credentialPointerEnvKey(SKILLS_APP)} names a vault item that produced an empty Skills API key \u2014 ` + `refusing to send an unauthenticated request.`);
9337
+ }
9338
+ return completed.apiKey;
9339
+ }
9340
+ async function requireSkillsApiKey(action = "This command", env = process.env, options = {}) {
9341
+ const apiKey = await resolveSkillsApiKey(env, options);
9342
+ if (!apiKey)
9343
+ throw new MissingSkillsFleetError(action);
9344
+ return apiKey;
9345
+ }
9346
+ function stripV1(baseUrl) {
9347
+ return baseUrl.replace(/\/v1$/, "").replace(/\/+$/, "");
9348
+ }
9349
+ async function skillsCredentialOrReason(env = process.env, options = {}) {
9350
+ try {
9351
+ const apiKey = await resolveSkillsApiKey(env, options);
9352
+ return apiKey ? { apiKey, reason: null } : { apiKey: null, reason: null };
9353
+ } catch (error) {
9354
+ if (error instanceof SkillsFleetCredentialError || error?.name === "SkillsFleetCredentialError") {
9355
+ return { apiKey: null, reason: error.message };
9356
+ }
9357
+ throw error;
9358
+ }
9359
+ }
9360
+ function resolveSkillsApiOrigin(env = process.env, options = {}) {
9361
+ const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
9362
+ if (configured) {
9363
+ toV1BaseUrl(configured.value);
9364
+ return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
9365
+ }
9366
+ const fleet = resolveSkillsFleet(env, options);
9367
+ return fleet.mode === "hosted" ? { origin: fleet.apiOrigin, source: fleet.apiUrlSource } : null;
9368
+ }
9369
+ function requireSkillsApiOrigin(action = "This command", env = process.env, options = {}) {
9370
+ const resolved = resolveSkillsApiOrigin(env, options);
9371
+ if (!resolved)
9372
+ throw new MissingSkillsFleetError(action);
9373
+ return resolved.origin;
9374
+ }
9375
+ function requireSkillsFleet(action = "This command", env = process.env, options = {}) {
9376
+ const fleet = resolveSkillsFleet(env, options);
9377
+ if (fleet.mode === "hosted")
9378
+ return fleet;
9379
+ throw new MissingSkillsFleetError(action);
9380
+ }
9381
+
9382
+ class MissingSkillsFleetError extends Error {
9383
+ code = "MISSING_API_URL";
9384
+ constructor(action = "This command") {
9385
+ 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>`);
9386
+ this.name = "MissingSkillsFleetError";
9387
+ }
9388
+ }
9389
+
9390
+ // src/lib/api-url.ts
9391
+ var API_URL_ENV_VAR = SKILLS_API_URL_ENV;
9392
+ 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>`;
9393
+ function resolveApiUrl(env = process.env, options = {}) {
9394
+ const fleet = resolveSkillsFleet(env, options);
9395
+ return fleet.mode === "hosted" ? fleet.apiOrigin : undefined;
8477
9396
  }
8478
9397
 
8479
9398
  // src/lib/discovery.ts
@@ -8601,26 +9520,19 @@ var remoteRegistrySchema = exports_external.union([
8601
9520
  exports_external.object({ skills: exports_external.array(remoteSkillSchema) }),
8602
9521
  exports_external.object({ data: exports_external.array(remoteSkillSchema) })
8603
9522
  ]);
8604
- function getConfiguredApiUrl(config = loadConfig(), env = process.env) {
8605
- return resolveApiUrl(config, env);
9523
+ function getConfiguredApiUrl(env = process.env) {
9524
+ return resolveApiUrl(env);
8606
9525
  }
8607
9526
  function buildSkillsApiUrl(apiUrl, endpoint = "/skills") {
8608
9527
  const url = new URL(apiUrl);
8609
9528
  const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
8610
9529
  const pathname = url.pathname.replace(/\/+$/, "");
8611
- if (pathname.endsWith("/skills")) {
8612
- if (cleanEndpoint === "/skills") {
8613
- url.pathname = pathname;
8614
- } else {
8615
- url.pathname = `${pathname.slice(0, -"/skills".length)}${cleanEndpoint}` || cleanEndpoint;
8616
- }
8617
- return url.toString();
8618
- }
8619
- if (pathname.endsWith("/api") || pathname.endsWith("/api/v1")) {
8620
- url.pathname = `${pathname}${cleanEndpoint}`;
9530
+ const apiBase = /\/api(?:\/v1)?\/skills$/.test(pathname) ? pathname.slice(0, -"/skills".length) : pathname;
9531
+ if (/\/api(?:\/v1)?$/.test(apiBase)) {
9532
+ url.pathname = `${apiBase}${cleanEndpoint}`;
8621
9533
  return url.toString();
8622
9534
  }
8623
- url.pathname = `${pathname}/api/v1${cleanEndpoint}`.replace(/\/{2,}/g, "/");
9535
+ url.pathname = `${apiBase}/api/v1${cleanEndpoint}`.replace(/\/{2,}/g, "/");
8624
9536
  return url.toString();
8625
9537
  }
8626
9538
  function titleize(name) {
@@ -8681,9 +9593,9 @@ function parseRemoteContract(schema, payload, message) {
8681
9593
  throw error;
8682
9594
  }
8683
9595
  }
8684
- function remoteRequestHeaders(options) {
9596
+ async function remoteRequestHeaders(options) {
8685
9597
  const headers = new Headers({ Accept: "application/json" });
8686
- const token = options.authToken !== undefined ? options.authToken : getApiKey();
9598
+ const token = options.authToken !== undefined ? options.authToken : await resolveSkillsApiKey();
8687
9599
  const trimmed = token?.trim();
8688
9600
  if (trimmed)
8689
9601
  headers.set("Authorization", `Bearer ${trimmed}`);
@@ -8691,11 +9603,12 @@ function remoteRequestHeaders(options) {
8691
9603
  }
8692
9604
  async function fetchRemoteJson(url, options) {
8693
9605
  const fetchImpl = options.fetchImpl || fetch;
9606
+ const headers = await remoteRequestHeaders(options);
8694
9607
  const controller = new AbortController;
8695
9608
  const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 1e4);
8696
9609
  try {
8697
9610
  const response = await fetchImpl(url, {
8698
- headers: remoteRequestHeaders(options),
9611
+ headers,
8699
9612
  signal: controller.signal
8700
9613
  });
8701
9614
  if (!response.ok) {
@@ -8709,7 +9622,7 @@ async function fetchRemoteJson(url, options) {
8709
9622
  async function loadRemoteRegistry(options = {}) {
8710
9623
  const apiUrl = options.apiUrl || getConfiguredApiUrl();
8711
9624
  if (!apiUrl) {
8712
- throw new Error("Remote registry requires SKILLS_API_URL or config apiUrl");
9625
+ throw new Error(`Remote registry requires a Skills credential (${SKILLS_API_KEY_ENV}, the Keychain item, or ~/.hasna/skills/config/credentials) and, for your own instance, ${SKILLS_API_URL_ENV}`);
8713
9626
  }
8714
9627
  const url = buildSkillsApiUrl(apiUrl, options.endpoint);
8715
9628
  return parseRemoteRegistryPayload(await fetchRemoteJson(url, options));
@@ -8717,7 +9630,7 @@ async function loadRemoteRegistry(options = {}) {
8717
9630
  async function loadRemoteSkill(name, options = {}) {
8718
9631
  const apiUrl = options.apiUrl || getConfiguredApiUrl();
8719
9632
  if (!apiUrl) {
8720
- throw new Error("Remote registry requires SKILLS_API_URL or config apiUrl");
9633
+ throw new Error(`Remote registry requires a Skills credential (${SKILLS_API_KEY_ENV}, the Keychain item, or ~/.hasna/skills/config/credentials) and, for your own instance, ${SKILLS_API_URL_ENV}`);
8721
9634
  }
8722
9635
  const slug = encodeURIComponent(name);
8723
9636
  const url = buildSkillsApiUrl(apiUrl, options.endpoint ?? `/skills/${slug}`);
@@ -8857,7 +9770,7 @@ var TOOL_PRIMITIVES = [
8857
9770
  cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
8858
9771
  mcpTools: ["get_skill_tool_dependencies", "run_skill"],
8859
9772
  apiSurfaces: ["runSkill", "SkillRunRecord", "RemoteSkillRunContract"],
8860
- envVars: ["SKILLS_API_KEY"],
9773
+ envVars: ["HASNA_SKILLS_API_KEY"],
8861
9774
  outputTypes: ["text", "json", "markdown", "artifact"],
8862
9775
  capabilities: ["completion", "reasoning", "tool-calling", "vision-input", "structured-output"]
8863
9776
  },
@@ -8913,7 +9826,7 @@ var TOOL_PRIMITIVES = [
8913
9826
  cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
8914
9827
  mcpTools: ["get_skill_tool_dependencies", "run_skill"],
8915
9828
  apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
8916
- envVars: ["SKILLS_API_KEY"],
9829
+ envVars: ["HASNA_SKILLS_API_KEY"],
8917
9830
  outputTypes: ["png", "jpeg", "webp", "svg", "zip"],
8918
9831
  capabilities: ["image-generation", "image-analysis", "image-editing", "asset-packaging"]
8919
9832
  },
@@ -8927,7 +9840,7 @@ var TOOL_PRIMITIVES = [
8927
9840
  cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
8928
9841
  mcpTools: ["get_skill_tool_dependencies", "run_skill"],
8929
9842
  apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
8930
- envVars: ["SKILLS_API_KEY"],
9843
+ envVars: ["HASNA_SKILLS_API_KEY"],
8931
9844
  outputTypes: ["mp3", "wav", "txt", "srt", "json", "zip"],
8932
9845
  capabilities: ["transcription", "audio-generation", "voiceover", "audio-cleanup"]
8933
9846
  },
@@ -8941,7 +9854,7 @@ var TOOL_PRIMITIVES = [
8941
9854
  cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
8942
9855
  mcpTools: ["get_skill_tool_dependencies", "run_skill"],
8943
9856
  apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
8944
- envVars: ["SKILLS_API_KEY"],
9857
+ envVars: ["HASNA_SKILLS_API_KEY"],
8945
9858
  outputTypes: ["mp4", "mov", "srt", "png", "json", "zip"],
8946
9859
  capabilities: ["video-generation", "video-analysis", "captioning", "highlight-extraction"]
8947
9860
  },
@@ -8969,7 +9882,7 @@ var TOOL_PRIMITIVES = [
8969
9882
  cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
8970
9883
  mcpTools: ["get_skill_tool_dependencies", "run_skill"],
8971
9884
  apiSurfaces: ["SkillRunContext", "RemoteSkillRunContract"],
8972
- envVars: ["SKILLS_API_KEY"],
9885
+ envVars: ["HASNA_SKILLS_API_KEY"],
8973
9886
  outputTypes: ["json", "artifact"],
8974
9887
  capabilities: ["approval", "external-api", "account-scoped-execution"]
8975
9888
  },
@@ -9011,7 +9924,7 @@ var TOOL_PRIMITIVES = [
9011
9924
  cliCommands: ["skills auth login", "skills run <skill>"],
9012
9925
  mcpTools: ["run_skill"],
9013
9926
  apiSurfaces: ["RemoteSkillsClient", "RemoteSkillRunContract"],
9014
- envVars: ["SKILLS_API_KEY"],
9927
+ envVars: ["HASNA_SKILLS_API_KEY"],
9015
9928
  outputTypes: ["json"],
9016
9929
  capabilities: ["account-auth", "remote-run-submit"]
9017
9930
  }
@@ -9219,6 +10132,11 @@ function primitiveHaystack(primitive) {
9219
10132
  function clone(value) {
9220
10133
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
9221
10134
  }
10135
+ // src/lib/auth-store.ts
10136
+ function getApiUrl(action, env = process.env, options = {}) {
10137
+ return requireSkillsApiOrigin(action, env, options);
10138
+ }
10139
+
9222
10140
  // src/lib/remote-run-contract.ts
9223
10141
  var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
9224
10142
  function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
@@ -9551,21 +10469,25 @@ function normalizeUpdatedSincePage(payload) {
9551
10469
  }
9552
10470
  return { skills, nextCursor };
9553
10471
  }
9554
- function createRemoteSkillsClient() {
9555
- const apiKey = getApiKey();
9556
- if (!apiKey)
10472
+ async function createRemoteSkillsClient(env = process.env) {
10473
+ const fleet = resolveSkillsFleet(env);
10474
+ if (fleet.mode !== "hosted")
9557
10475
  return null;
9558
- return new RemoteSkillsClient(apiKey);
10476
+ const apiKey = await resolveSkillsApiKey(env);
10477
+ if (!apiKey) {
10478
+ throw new Error("A Skills authority resolved but no API key did. Sign in with: skills auth login");
10479
+ }
10480
+ return new RemoteSkillsClient(apiKey, fleet.apiOrigin);
9559
10481
  }
9560
10482
  // src/lib/scheduler.ts
9561
- import { existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "fs";
10483
+ import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
9562
10484
  import { join as join15 } from "path";
9563
10485
  function getSchedulesPath(targetDir = process.cwd()) {
9564
10486
  return join15(targetDir, ".skills", "schedules.json");
9565
10487
  }
9566
10488
  function loadSchedules(targetDir = process.cwd()) {
9567
10489
  const path = getSchedulesPath(targetDir);
9568
- if (existsSync15(path)) {
10490
+ if (existsSync14(path)) {
9569
10491
  try {
9570
10492
  return JSON.parse(readFileSync13(path, "utf-8"));
9571
10493
  } catch {}
@@ -9575,9 +10497,9 @@ function loadSchedules(targetDir = process.cwd()) {
9575
10497
  function saveSchedules(data, targetDir = process.cwd()) {
9576
10498
  const path = getSchedulesPath(targetDir);
9577
10499
  const dir = join15(targetDir, ".skills");
9578
- if (!existsSync15(dir))
9579
- mkdirSync8(dir, { recursive: true });
9580
- writeFileSync8(path, JSON.stringify(data, null, 2));
10500
+ if (!existsSync14(dir))
10501
+ mkdirSync7(dir, { recursive: true });
10502
+ writeFileSync7(path, JSON.stringify(data, null, 2));
9581
10503
  }
9582
10504
  function validateCronField(expr, min, max, label) {
9583
10505
  for (const part of expr.split(",")) {
@@ -9762,8 +10684,8 @@ function recordScheduleRun(id, status, targetDir) {
9762
10684
  saveSchedules(data, targetDir);
9763
10685
  }
9764
10686
  // src/lib/pull.ts
9765
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
9766
- import { dirname as dirname7, join as join17 } from "path";
10687
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
10688
+ import { dirname as dirname6, join as join17 } from "path";
9767
10689
 
9768
10690
  // src/lib/revision.ts
9769
10691
  import { createHash as createHash3 } from "crypto";
@@ -10160,9 +11082,9 @@ class PullSkillError extends Error {
10160
11082
  }
10161
11083
  }
10162
11084
  async function pullSkills(options = {}) {
10163
- const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
11085
+ const client = options.client !== undefined ? options.client : await createRemoteSkillsClient();
10164
11086
  if (!client) {
10165
- throw new PullSkillError("No API key configured, so there is no instance to pull from.", ["Run `skills login`, or set SKILLS_API_KEY and SKILLS_API_URL for this instance."]);
11087
+ throw new PullSkillError("No API key configured, so there is no instance to pull from.", ["Run `skills auth login`, or set HASNA_SKILLS_API_KEY (and HASNA_SKILLS_API_URL for your own instance)."]);
10166
11088
  }
10167
11089
  const signingKey = options.signingKey ?? resolveSigningKey() ?? undefined;
10168
11090
  const targets = await resolveTargetSlugs(client, options);
@@ -10215,7 +11137,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
10215
11137
  }
10216
11138
  if (bundleResponse.status === 404) {
10217
11139
  const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
10218
- if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
11140
+ if (isPublishedInstallMarker(marker)) {
10219
11141
  return { name: slug, success: true, purged: true, removed: false };
10220
11142
  }
10221
11143
  }
@@ -10254,7 +11176,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
10254
11176
  }
10255
11177
  if (!meta?.revisionId) {
10256
11178
  const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
10257
- if (marker && typeof marker.revisionId === "string" && marker.revisionId) {
11179
+ if (isPublishedInstallMarker(marker)) {
10258
11180
  return { name: slug, success: true, purged: true, removed: false };
10259
11181
  }
10260
11182
  }
@@ -10311,7 +11233,7 @@ function provenRevision(meta, slug, bundle) {
10311
11233
  }
10312
11234
  function reconcileTombstone(slug, corpusOptions) {
10313
11235
  const target = join17(getPortableSkillsRoot(corpusOptions), slug);
10314
- if (!existsSync16(join17(target, PULL_MARKER_FILE))) {
11236
+ if (!existsSync15(join17(target, PULL_MARKER_FILE))) {
10315
11237
  return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
10316
11238
  }
10317
11239
  rmSync4(target, { recursive: true, force: true });
@@ -10324,6 +11246,13 @@ function readPullMarker(dir) {
10324
11246
  return null;
10325
11247
  }
10326
11248
  }
11249
+ function isPublishedInstallMarker(marker) {
11250
+ if (!marker)
11251
+ return false;
11252
+ const revisionId = typeof marker.revisionId === "string" ? marker.revisionId : "";
11253
+ const contentHash = typeof marker.contentHash === "string" ? marker.contentHash : "";
11254
+ return revisionId.length > 0 || contentHash.length > 0;
11255
+ }
10327
11256
  function installVerifiedBundle(slug, response, meta, corpusOptions, verify, exact) {
10328
11257
  return response.arrayBuffer().then((buffer) => {
10329
11258
  const verified = verifyBundleResponseBytes(buffer, response, verify);
@@ -10446,17 +11375,17 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
10446
11375
  }
10447
11376
  function installBundleAtomically(name, entries, options = {}, marker = {}) {
10448
11377
  const root = getPortableSkillsRoot(options);
10449
- mkdirSync9(root, { recursive: true });
11378
+ mkdirSync8(root, { recursive: true });
10450
11379
  const target = join17(root, name);
10451
- const created = !existsSync16(target);
11380
+ const created = !existsSync15(target);
10452
11381
  const staging = mkdtempSync3(join17(root, `.pull-${name}-`));
10453
11382
  let moved = false;
10454
11383
  let backup = null;
10455
11384
  try {
10456
11385
  for (const entry of entries) {
10457
11386
  const destination = join17(staging, entry.path);
10458
- mkdirSync9(dirname7(destination), { recursive: true });
10459
- writeFileSync9(destination, entry.bytes, { mode: entry.mode });
11387
+ mkdirSync8(dirname6(destination), { recursive: true });
11388
+ writeFileSync8(destination, entry.bytes, { mode: entry.mode });
10460
11389
  }
10461
11390
  writePullMarker(staging, {
10462
11391
  skill: name,
@@ -10466,7 +11395,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
10466
11395
  ...marker.signature ? { signature: marker.signature } : {},
10467
11396
  ...marker.revisionId ? { revisionId: marker.revisionId } : {}
10468
11397
  });
10469
- if (existsSync16(target)) {
11398
+ if (existsSync15(target)) {
10470
11399
  backup = mkdtempSync3(join17(root, `.pull-backup-${name}-`));
10471
11400
  renameSync3(target, join17(backup, name));
10472
11401
  moved = true;
@@ -10476,7 +11405,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
10476
11405
  rmSync4(backup, { recursive: true, force: true });
10477
11406
  } catch (error) {
10478
11407
  rmSync4(staging, { recursive: true, force: true });
10479
- if (moved && backup && existsSync16(join17(backup, name))) {
11408
+ if (moved && backup && existsSync15(join17(backup, name))) {
10480
11409
  try {
10481
11410
  renameSync3(join17(backup, name), target);
10482
11411
  } catch {}
@@ -10497,7 +11426,7 @@ function writePullMarker(dir, record) {
10497
11426
  ...record.revisionId ? { revisionId: record.revisionId } : {},
10498
11427
  syncedAt: new Date().toISOString()
10499
11428
  };
10500
- writeFileSync9(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
11429
+ writeFileSync8(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
10501
11430
  `);
10502
11431
  }
10503
11432
  async function safeMeta(client, slug) {
@@ -10652,17 +11581,18 @@ function findSkillsParityForMcpTool(tool) {
10652
11581
  return SKILLS_CLI_MCP_PARITY.find((entry) => entry.mcpTools.includes(tool));
10653
11582
  }
10654
11583
  // src/lib/registry-sync.ts
10655
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
10656
- import { dirname as dirname8, relative as relative4 } from "path";
11584
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
11585
+ import { dirname as dirname7, relative as relative4 } from "path";
10657
11586
  // package.json
10658
11587
  var package_default = {
10659
11588
  name: "@hasna/skills",
10660
- version: "0.2.0",
11589
+ version: "0.3.0",
10661
11590
  description: "Skills library for AI coding agents",
10662
11591
  type: "module",
10663
11592
  bin: {
10664
11593
  skills: "bin/index.js",
10665
11594
  "skills-mcp": "bin/mcp.js",
11595
+ "skills-serve": "bin/server.js",
10666
11596
  "skills-server": "bin/server.js",
10667
11597
  "skills-worker": "bin/worker.js",
10668
11598
  "skills-migrate": "bin/migrate.js"
@@ -10748,6 +11678,7 @@ var package_default = {
10748
11678
  dependencies: {
10749
11679
  "@aws-sdk/client-ecs": "^3.1079.0",
10750
11680
  "@aws-sdk/client-s3": "^3.1079.0",
11681
+ "@hasna/contracts": "1.0.1",
10751
11682
  "@hasna/events": "0.1.16",
10752
11683
  "@modelcontextprotocol/sdk": "^1.26.0",
10753
11684
  chalk: "^5.3.0",
@@ -10833,8 +11764,8 @@ function createRegistrySyncArtifact(options = {}) {
10833
11764
  };
10834
11765
  }
10835
11766
  function writeRegistrySyncArtifact(path, artifact) {
10836
- mkdirSync10(dirname8(path), { recursive: true });
10837
- writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
11767
+ mkdirSync9(dirname7(path), { recursive: true });
11768
+ writeFileSync9(path, `${JSON.stringify(artifact, null, 2)}
10838
11769
  `);
10839
11770
  }
10840
11771
  function buildDocs(name) {
@@ -11673,17 +12604,17 @@ function clone2(value) {
11673
12604
  return JSON.parse(JSON.stringify(value));
11674
12605
  }
11675
12606
  // src/lib/feedback.ts
11676
- import { appendFileSync, existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
11677
- import { dirname as dirname9, join as join18 } from "path";
12607
+ import { appendFileSync, existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
12608
+ import { dirname as dirname8, join as join18 } from "path";
11678
12609
  import { Database } from "bun:sqlite";
11679
12610
  function getFeedbackDbPath() {
11680
12611
  return join18(getDataDir(), "skills.db");
11681
12612
  }
11682
12613
  function getFeedbackDb() {
11683
12614
  const dbPath = getFeedbackDbPath();
11684
- const dir = dirname9(dbPath);
11685
- if (!existsSync17(dir))
11686
- mkdirSync11(dir, { recursive: true });
12615
+ const dir = dirname8(dbPath);
12616
+ if (!existsSync16(dir))
12617
+ mkdirSync10(dir, { recursive: true });
11687
12618
  const db = new Database(dbPath);
11688
12619
  db.exec("PRAGMA journal_mode = WAL");
11689
12620
  db.exec([
@@ -11710,9 +12641,9 @@ function saveFeedback(input) {
11710
12641
  const category = input.category ?? "general";
11711
12642
  if (isApiMode()) {
11712
12643
  const path = join18(getDataDir(), "feedback.jsonl");
11713
- const dir = dirname9(path);
11714
- if (!existsSync17(dir))
11715
- mkdirSync11(dir, { recursive: true });
12644
+ const dir = dirname8(path);
12645
+ if (!existsSync16(dir))
12646
+ mkdirSync10(dir, { recursive: true });
11716
12647
  appendFileSync(path, JSON.stringify({ message, category, email: input.email ?? null, agent: input.agent ?? null, version: input.version ?? null, createdAt: new Date().toISOString() }) + `
11717
12648
  `);
11718
12649
  return { saved: true, category, path };
@@ -11726,25 +12657,23 @@ function saveFeedback(input) {
11726
12657
  return { saved: true, category, path: getFeedbackDbPath() };
11727
12658
  }
11728
12659
  function isApiMode(env = process.env) {
11729
- if (env.HASNA_SKILLS_API_URL?.trim())
11730
- return true;
11731
12660
  try {
11732
- return Boolean(resolveApiUrl(undefined, env));
12661
+ return Boolean(resolveApiUrl(env));
11733
12662
  } catch {
11734
- return Boolean(env.SKILLS_API_URL?.trim());
12663
+ return true;
11735
12664
  }
11736
12665
  }
11737
12666
  // src/lib/native-storage.ts
11738
12667
  import { createHash as createHash5, createHmac as createHmac2 } from "crypto";
11739
12668
  import {
11740
- existsSync as existsSync18,
11741
- mkdirSync as mkdirSync12,
12669
+ existsSync as existsSync17,
12670
+ mkdirSync as mkdirSync11,
11742
12671
  readFileSync as readFileSync16,
11743
12672
  readdirSync as readdirSync10,
11744
12673
  statSync as statSync10,
11745
- writeFileSync as writeFileSync11
12674
+ writeFileSync as writeFileSync10
11746
12675
  } from "fs";
11747
- import { dirname as dirname10, join as join19, normalize as normalize3, relative as relative5, sep as sep2 } from "path";
12676
+ import { dirname as dirname9, join as join19, normalize as normalize3, relative as relative5, sep as sep2 } from "path";
11748
12677
  var SKILLS_STORAGE_TABLES = [
11749
12678
  "skills_sync_records",
11750
12679
  "skills_sync_cursors"
@@ -11904,7 +12833,7 @@ function getStorageStatus(options = {}) {
11904
12833
  function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
11905
12834
  const projectStateDir = getProjectStateDir(targetDir);
11906
12835
  const files = [];
11907
- if (existsSync18(projectStateDir)) {
12836
+ if (existsSync17(projectStateDir)) {
11908
12837
  for (const filePath of walkFiles2(projectStateDir)) {
11909
12838
  const bytes = readFileSync16(filePath);
11910
12839
  const relativePath = toPosix(relative5(targetDir, filePath));
@@ -11931,7 +12860,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
11931
12860
  continue;
11932
12861
  }
11933
12862
  const absolutePath = resolveSnapshotPath(targetDir, file.path);
11934
- if (existsSync18(absolutePath) && !options.overwrite) {
12863
+ if (existsSync17(absolutePath) && !options.overwrite) {
11935
12864
  skipped += 1;
11936
12865
  continue;
11937
12866
  }
@@ -11940,8 +12869,8 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
11940
12869
  if (hash !== file.sha256) {
11941
12870
  throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
11942
12871
  }
11943
- mkdirSync12(dirname10(absolutePath), { recursive: true });
11944
- writeFileSync11(absolutePath, bytes);
12872
+ mkdirSync11(dirname9(absolutePath), { recursive: true });
12873
+ writeFileSync10(absolutePath, bytes);
11945
12874
  written += 1;
11946
12875
  }
11947
12876
  return { written, skipped };
@@ -12319,16 +13248,16 @@ function toArrayBuffer(bytes) {
12319
13248
  import { createHash as createHash6 } from "crypto";
12320
13249
  import {
12321
13250
  copyFileSync as copyFileSync2,
12322
- mkdirSync as mkdirSync13,
13251
+ mkdirSync as mkdirSync12,
12323
13252
  readFileSync as readFileSync17,
12324
13253
  statSync as statSync12,
12325
- writeFileSync as writeFileSync12
13254
+ writeFileSync as writeFileSync11
12326
13255
  } from "fs";
12327
- import { dirname as dirname11, isAbsolute as isAbsolute3, relative as relative6, resolve as resolve2, sep as sep4 } from "path";
13256
+ import { dirname as dirname10, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve2, sep as sep4 } from "path";
12328
13257
 
12329
13258
  // src/lib/portable-snapshot-filter.ts
12330
13259
  import { readdirSync as readdirSync11, statSync as statSync11 } from "fs";
12331
- import { homedir as homedir5 } from "os";
13260
+ import { homedir as homedir4 } from "os";
12332
13261
  import { join as join20, sep as sep3 } from "path";
12333
13262
  var SYNC_HOMES = [
12334
13263
  { name: "skills", subClass: "skills", agent: null },
@@ -12420,7 +13349,7 @@ function isPortableWithinSkill(relativeParts) {
12420
13349
  return PORTABLE_SUBDIRS.has(second);
12421
13350
  }
12422
13351
  function homePathFor(definition, homesRoot) {
12423
- const home = homesRoot ?? homedir5();
13352
+ const home = homesRoot ?? homedir4();
12424
13353
  if (definition.subClass === "skills" || definition.subClass === "custom") {
12425
13354
  return join20(skillsDataRootForHome(home), definition.name);
12426
13355
  }
@@ -12590,7 +13519,7 @@ function writeStationSnapshot(options) {
12590
13519
  for (const plan of plans) {
12591
13520
  const destination = resolve2(repoRoot, plan.destination);
12592
13521
  const destinationRelative = relative6(repoRoot, destination);
12593
- if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute3(destinationRelative)) {
13522
+ if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute4(destinationRelative)) {
12594
13523
  throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
12595
13524
  }
12596
13525
  let existingDigest = null;
@@ -12612,7 +13541,7 @@ function writeStationSnapshot(options) {
12612
13541
  let written = 0;
12613
13542
  for (const plan of untouched) {
12614
13543
  const destination = resolve2(repoRoot, plan.destination);
12615
- mkdirSync13(dirname11(destination), { recursive: true });
13544
+ mkdirSync12(dirname10(destination), { recursive: true });
12616
13545
  copyFileSync2(plan.source.fullPath, destination);
12617
13546
  written += 1;
12618
13547
  }
@@ -12631,8 +13560,8 @@ function writeStationSnapshot(options) {
12631
13560
  files: manifestFiles
12632
13561
  };
12633
13562
  const manifestPath = resolve2(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
12634
- mkdirSync13(dirname11(manifestPath), { recursive: true });
12635
- writeFileSync12(manifestPath, `${JSON.stringify(manifest, null, 2)}
13563
+ mkdirSync12(dirname10(manifestPath), { recursive: true });
13564
+ writeFileSync11(manifestPath, `${JSON.stringify(manifest, null, 2)}
12636
13565
  `);
12637
13566
  return {
12638
13567
  ...base,
@@ -12645,13 +13574,13 @@ function writeStationSnapshot(options) {
12645
13574
  import { createHash as createHash7 } from "crypto";
12646
13575
  import {
12647
13576
  copyFileSync as copyFileSync3,
12648
- mkdirSync as mkdirSync14,
13577
+ mkdirSync as mkdirSync13,
12649
13578
  readdirSync as readdirSync12,
12650
13579
  readFileSync as readFileSync18,
12651
13580
  statSync as statSync13,
12652
- writeFileSync as writeFileSync13
13581
+ writeFileSync as writeFileSync12
12653
13582
  } from "fs";
12654
- import { dirname as dirname12, join as join21, resolve as resolve3, sep as sep5 } from "path";
13583
+ import { dirname as dirname11, join as join21, resolve as resolve3, sep as sep5 } from "path";
12655
13584
  var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
12656
13585
  var STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
12657
13586
  var MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
@@ -12907,7 +13836,7 @@ function writeStationHydration(options) {
12907
13836
  }
12908
13837
  let written = 0;
12909
13838
  for (const entry of toWrite) {
12910
- mkdirSync14(dirname12(entry.destination), { recursive: true });
13839
+ mkdirSync13(dirname11(entry.destination), { recursive: true });
12911
13840
  copyFileSync3(entry.fullPath, entry.destination);
12912
13841
  written += 1;
12913
13842
  }
@@ -12928,9 +13857,9 @@ function writeStationHydration(options) {
12928
13857
  },
12929
13858
  skills: resultSkills
12930
13859
  };
12931
- const hydrationManifestPath = join21(dirname12(cacheRoot), `hydration-${options.stationId}.json`);
12932
- mkdirSync14(dirname12(hydrationManifestPath), { recursive: true });
12933
- writeFileSync13(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
13860
+ const hydrationManifestPath = join21(dirname11(cacheRoot), `hydration-${options.stationId}.json`);
13861
+ mkdirSync13(dirname11(hydrationManifestPath), { recursive: true });
13862
+ writeFileSync12(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
12934
13863
  `);
12935
13864
  return {
12936
13865
  ...base,
@@ -12967,6 +13896,9 @@ export {
12967
13896
  summarizeMcpToolContract,
12968
13897
  storageCapabilities,
12969
13898
  skillsPostgresSyncSchemaSql,
13899
+ skillsCredentialOrReason,
13900
+ skillsCredentialFiles,
13901
+ skillsCredentialFilePath,
12970
13902
  skillExists,
12971
13903
  signSkillsAwsV4Request,
12972
13904
  sha256File,
@@ -12983,7 +13915,13 @@ export {
12983
13915
  resolveSyncAgents,
12984
13916
  resolveStorageConfig,
12985
13917
  resolveSkillsNativeStorageConfig,
13918
+ resolveSkillsFleet,
13919
+ resolveSkillsApiOrigin,
13920
+ resolveSkillsApiKey,
12986
13921
  resolveSkillAlias,
13922
+ requireSkillsFleet,
13923
+ requireSkillsApiOrigin,
13924
+ requireSkillsApiKey,
12987
13925
  removeSkillForAgent,
12988
13926
  removeSkill,
12989
13927
  removeSchedule,
@@ -13005,6 +13943,8 @@ export {
13005
13943
  parseSkillFrontmatter,
13006
13944
  parseRemoteSkillPayload,
13007
13945
  parseRemoteRegistryPayload,
13946
+ noticeLocalSkillsMode,
13947
+ normalizeSkillsApiOrigin,
13008
13948
  normalizeSkillSlug,
13009
13949
  normalizeRemoteSkillRunContract,
13010
13950
  normalizePortableSkillName,
@@ -13097,6 +14037,7 @@ export {
13097
14037
  createRegistrySyncArtifact,
13098
14038
  createMcpContractManifest,
13099
14039
  createLocalSkillManifest,
14040
+ configuredSkillsApiUrl,
13100
14041
  computeContentHash,
13101
14042
  completeSkillRun,
13102
14043
  clearRegistryCache,
@@ -13112,6 +14053,7 @@ export {
13112
14053
  StationSnapshotError,
13113
14054
  SkillsS3ObjectStore,
13114
14055
  SkillsPostgresSyncStore,
14056
+ SkillsFleetCredentialError,
13115
14057
  SYNC_MARKER_MANAGED_BY,
13116
14058
  SYNC_MARKER_FILE,
13117
14059
  SYNC_HOMES,
@@ -13130,6 +14072,11 @@ export {
13130
14072
  SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
13131
14073
  SKILLS_NATIVE_STORAGE_ENV,
13132
14074
  SKILLS_CLI_MCP_PARITY,
14075
+ SKILLS_APP,
14076
+ SKILLS_API_URL_ENV_KEYS,
14077
+ SKILLS_API_URL_ENV,
14078
+ SKILLS_API_KEY_ENV_KEYS,
14079
+ SKILLS_API_KEY_ENV,
13133
14080
  SKILLS,
13134
14081
  RemoteSkillsClient,
13135
14082
  RemoteRouteUnsupportedError,
@@ -13141,6 +14088,7 @@ export {
13141
14088
  PORTABLE_SKILL_STANDARD,
13142
14089
  PORTABLE_SKILL_SCHEMA,
13143
14090
  PORTABLE_SKILL_DEFAULT_VERSION,
14091
+ MissingSkillsFleetError,
13144
14092
  MCP_CONTRACT_SCHEMA_VERSION,
13145
14093
  DEFAULT_EXPORT_DIR,
13146
14094
  CATEGORIES,