@hasna/skills 0.9.20 → 0.10.1

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.20",
20022
+ version: "0.10.1",
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",
@@ -20964,10 +20974,16 @@ var init_transport = __esm(() => {
20964
20974
  ]);
20965
20975
  });
20966
20976
 
20977
+ // src/lib/cli-credential-profile.ts
20978
+ function selectedCliSkillsCredentialProfile() {
20979
+ return cliCredentialProfile;
20980
+ }
20981
+ var cliCredentialProfile;
20982
+
20967
20983
  // src/lib/instance-credentials.ts
20968
20984
  import { closeSync as closeSync3, constants, fstatSync as fstatSync3, lstatSync as lstatSync2, openSync as openSync3, readSync } from "fs";
20969
20985
  function selectedSkillsProfile(env, explicit) {
20970
- const selected = explicit ?? env.HASNA_PROFILE;
20986
+ const selected = explicit ?? selectedCliSkillsCredentialProfile() ?? env.HASNA_PROFILE;
20971
20987
  if (selected === undefined)
20972
20988
  return null;
20973
20989
  const profile = selected.trim();
@@ -21050,6 +21066,12 @@ function readSkillsInstanceMetadata(file2) {
21050
21066
  throw new Error("Skills API URL aliases disagree");
21051
21067
  return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
21052
21068
  }
21069
+ function skillsFileHasCredential(file2) {
21070
+ const text = readMetadataText(file2);
21071
+ if (text === null)
21072
+ return false;
21073
+ return /^\s*(?:export\s+)?(?:HASNA_SKILLS_API_KEY|SKILLS_API_KEY|HASNA_SKILLS_API_KEY_REF)\s*=\s*\S/m.test(text);
21074
+ }
21053
21075
  var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
21054
21076
  var init_instance_credentials = __esm(() => {
21055
21077
  init_transport();
@@ -21081,6 +21103,9 @@ var init_local_opt_in = __esm(() => {
21081
21103
  SKILLS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_SKILLS_LOCAL", "SKILLS_LOCAL"];
21082
21104
  });
21083
21105
 
21106
+ // src/lib/product-default.ts
21107
+ var SKILLS_PRODUCT_DEFAULT_ORIGIN = "https://skills.md";
21108
+
21084
21109
  // src/lib/fleet-credentials.ts
21085
21110
  var exports_fleet_credentials = {};
21086
21111
  __export(exports_fleet_credentials, {
@@ -21089,6 +21114,7 @@ __export(exports_fleet_credentials, {
21089
21114
  skillsCredentialFilePath: () => skillsCredentialFilePath,
21090
21115
  skillsApiRequestUrl: () => skillsApiRequestUrl,
21091
21116
  selectsSkillsLocalMode: () => selectsSkillsLocalMode,
21117
+ resolveSkillsSignInOrigin: () => resolveSkillsSignInOrigin,
21092
21118
  resolveSkillsFleet: () => resolveSkillsFleet,
21093
21119
  resolveSkillsConnection: () => resolveSkillsConnection,
21094
21120
  resolveSkillsApiOrigin: () => resolveSkillsApiOrigin,
@@ -21101,9 +21127,12 @@ __export(exports_fleet_credentials, {
21101
21127
  normalizeSkillsApiOrigin: () => normalizeSkillsApiOrigin,
21102
21128
  isSkillsLocalOptIn: () => isSkillsLocalOptIn,
21103
21129
  isSkillsFleetCredentialError: () => isSkillsFleetCredentialError,
21130
+ gatewaySignInRefusal: () => gatewaySignInRefusal,
21131
+ credentialPairing: () => credentialPairing,
21104
21132
  configuredSkillsApiUrl: () => configuredSkillsApiUrl,
21105
21133
  SkillsFleetCredentialError: () => SkillsFleetCredentialError,
21106
21134
  SKILLS_LOCAL_OPT_IN_ENV_KEYS: () => SKILLS_LOCAL_OPT_IN_ENV_KEYS,
21135
+ SKILLS_CREDENTIAL_TIERS: () => SKILLS_CREDENTIAL_TIERS,
21107
21136
  SKILLS_APP: () => SKILLS_APP,
21108
21137
  SKILLS_API_URL_ENV_KEYS: () => SKILLS_API_URL_ENV_KEYS,
21109
21138
  SKILLS_API_URL_ENV: () => SKILLS_API_URL_ENV,
@@ -21112,6 +21141,9 @@ __export(exports_fleet_credentials, {
21112
21141
  REDACTED_VAULT_REFERENCE: () => REDACTED_VAULT_REFERENCE,
21113
21142
  MissingSkillsFleetError: () => MissingSkillsFleetError
21114
21143
  });
21144
+ function isClientTransportConfigurationError(error61) {
21145
+ return error61 instanceof ClientTransportConfigurationError || typeof error61 === "object" && error61 !== null && error61.name === "ClientTransportConfigurationError";
21146
+ }
21115
21147
  function isCredentialResolutionError(error61) {
21116
21148
  return error61 instanceof CredentialResolutionError || typeof error61 === "object" && error61 !== null && error61.name === "CredentialResolutionError";
21117
21149
  }
@@ -21166,7 +21198,7 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
21166
21198
  if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
21167
21199
  throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
21168
21200
  }
21169
- const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
21201
+ const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key, trust: "environment" }));
21170
21202
  if (new Set(normalized.map((entry) => entry.value)).size > 1)
21171
21203
  throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
21172
21204
  if (normalized[0])
@@ -21175,19 +21207,19 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
21175
21207
  for (const file2 of skillsProfileCredentialFiles(env, profile)) {
21176
21208
  const metadata = readSkillsInstanceMetadata(file2);
21177
21209
  if (metadata.apiUrl || metadata.binding)
21178
- return { value: metadata.apiUrl ?? metadata.binding, source: file2 };
21210
+ return { value: metadata.apiUrl ?? metadata.binding, source: file2, trust: "file" };
21179
21211
  }
21180
- return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
21212
+ return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default", trust: "default" };
21181
21213
  }
21182
21214
  const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
21183
21215
  if (fromKeychain)
21184
- return { value: fromKeychain.value.trim(), source: fromKeychain.source };
21216
+ return { value: fromKeychain.value.trim(), source: fromKeychain.source, trust: "keychain" };
21185
21217
  const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
21186
21218
  if (fromDisk?.unusable) {
21187
21219
  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
21220
  }
21189
21221
  if (fromDisk)
21190
- return { value: fromDisk.value.trim(), source: fromDisk.path };
21222
+ return { value: fromDisk.value.trim(), source: fromDisk.path, trust: "file" };
21191
21223
  return null;
21192
21224
  }
21193
21225
  function skillsCredentialFiles(env = process.env) {
@@ -21237,11 +21269,13 @@ function snapshotSkillsEnvironment(env) {
21237
21269
  return Object.freeze(snapshot);
21238
21270
  }
21239
21271
  function snapshotSkillsOptions(env, options) {
21272
+ const profile = options.credentials?.profile ?? selectedCliSkillsCredentialProfile();
21273
+ const credentials = profile === undefined ? options.credentials : { ...options.credentials, profile };
21240
21274
  if (env !== process.env)
21241
- return options;
21242
- return { ...options, credentials: { ...options.credentials, keychain: {
21243
- ...options.credentials?.keychain,
21244
- enabled: options.credentials?.keychain?.enabled ?? true
21275
+ return credentials === options.credentials ? options : { ...options, credentials };
21276
+ return { ...options, credentials: { ...credentials, keychain: {
21277
+ ...credentials?.keychain,
21278
+ enabled: credentials?.keychain?.enabled ?? true
21245
21279
  } } };
21246
21280
  }
21247
21281
  function resolveSkillsFleetOrThrow(env, options) {
@@ -21252,13 +21286,13 @@ function resolveSkillsFleetOrThrow(env, options) {
21252
21286
  const credential = resolveCredential(SKILLS_APP, env, options.credentials);
21253
21287
  if (!credential) {
21254
21288
  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`);
21289
+ 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
21290
  }
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`);
21291
+ 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
21292
  }
21259
21293
  const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
21260
21294
  toV1BaseUrl(apiOrigin);
21261
- assertCredentialInstance(credential, apiOrigin, env, options);
21295
+ assertCredentialInstancePairing(credential, configured, apiOrigin, env, options);
21262
21296
  assertFilesUnchanged();
21263
21297
  const base = {
21264
21298
  mode: "hosted",
@@ -21272,7 +21306,7 @@ function resolveSkillsFleetOrThrow(env, options) {
21272
21306
  return { ...base, apiKey: null, apiKeyPointer: credential };
21273
21307
  }
21274
21308
  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`);
21309
+ 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
21310
  }
21277
21311
  return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
21278
21312
  }
@@ -21280,25 +21314,58 @@ function credentialLocations(env) {
21280
21314
  const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
21281
21315
  return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
21282
21316
  }
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);
21317
+ function credentialPairing(credential, env, options = {}) {
21318
+ const fileBinding = (file2) => {
21319
+ const metadata = readSkillsInstanceMetadata(file2);
21320
+ return { kind: "bound", instance: normalizeSkillsApiOrigin(metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP)) };
21321
+ };
21322
+ const tier = credential.tier;
21323
+ switch (tier) {
21324
+ case "disk":
21325
+ case "profile":
21326
+ return fileBinding(credential.source);
21327
+ case "pointer":
21328
+ return credential.diskCandidates.includes(credential.source) ? fileBinding(credential.source) : { kind: "unbound" };
21329
+ case "keychain":
21330
+ return {
21331
+ kind: "bound",
21332
+ instance: normalizeSkillsApiOrigin(keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP))
21333
+ };
21334
+ case "env":
21335
+ case "override":
21336
+ case "argument":
21337
+ return { kind: "unbound" };
21338
+ default: {
21339
+ const unclassified = tier;
21340
+ throw new SkillsFleetCredentialError(`The Skills credential from ${credential.source} has an unclassified tier (${String(unclassified)}); refusing to send it.`, "INSTANCE_CREDENTIAL_MISMATCH");
21341
+ }
21290
21342
  }
21291
- if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
21343
+ }
21344
+ function assertCredentialInstancePairing(credential, configured, apiOrigin, env, options) {
21345
+ const pairing = credentialPairing(credential, env, options);
21346
+ if (pairing.kind === "bound") {
21347
+ if (pairing.instance === apiOrigin)
21348
+ return;
21292
21349
  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
21350
  }
21351
+ if (apiOrigin === normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
21352
+ return;
21353
+ if (configured?.trust === "environment")
21354
+ return;
21355
+ const next = [
21356
+ `set ${SKILLS_API_URL_ENV} alongside ${credential.source}`,
21357
+ `unset ${credential.source}, then run: skills login`
21358
+ ];
21359
+ 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
21360
  }
21295
21361
  async function resolveSkillsApiKey(env = process.env, options = {}) {
21296
21362
  return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
21297
21363
  }
21298
21364
  async function resolveSkillsConnection(env = process.env, options = {}) {
21299
21365
  const snapshotEnv = snapshotSkillsEnvironment(env);
21300
- const assertFilesUnchanged = selectsSkillsLocalMode(snapshotEnv) ? () => {} : captureSkillsCredentialFiles(skillsProfileCredentialFiles(snapshotEnv, options.credentials?.profile));
21301
- const fleet = resolveSkillsFleet(snapshotEnv, snapshotSkillsOptions(env, options));
21366
+ const resolvedOptions = snapshotSkillsOptions(env, options);
21367
+ const assertFilesUnchanged = selectsSkillsLocalMode(snapshotEnv) ? () => {} : captureSkillsCredentialFiles(skillsProfileCredentialFiles(snapshotEnv, resolvedOptions.credentials?.profile));
21368
+ const fleet = resolveSkillsFleet(snapshotEnv, resolvedOptions);
21302
21369
  if (fleet.mode === "local" && env === process.env)
21303
21370
  noticeLocalSkillsMode();
21304
21371
  if (fleet.mode !== "hosted")
@@ -21307,7 +21374,7 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
21307
21374
  return { ...fleet, apiKey: fleet.apiKey };
21308
21375
  const pointer = fleet.apiKeyPointer;
21309
21376
  if (!pointer) {
21310
- throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
21377
+ throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills login`);
21311
21378
  }
21312
21379
  let completed;
21313
21380
  try {
@@ -21364,13 +21431,84 @@ function requireSkillsApiOrigin(action = "This command", env = process.env, opti
21364
21431
  throw new MissingSkillsFleetError(action);
21365
21432
  return resolved.origin;
21366
21433
  }
21434
+ function gatewaySignInRefusal(target) {
21435
+ if (normalizeSkillsApiOrigin(target.origin) !== normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
21436
+ return null;
21437
+ 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`;
21438
+ const unset = target.credentialSource ?? (target.source === "--url" ? null : target.source);
21439
+ 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);
21440
+ }
21441
+ function explicitSignInOrigin(url2) {
21442
+ if (!url2.trim() || /[\x00-\x1f\x7f]/.test(url2)) {
21443
+ throw new SkillsFleetCredentialError("--url is blank or contains control characters", "INVALID_API_URL");
21444
+ }
21445
+ let origin;
21446
+ try {
21447
+ origin = normalizeSkillsApiOrigin(url2.trim());
21448
+ } catch (error61) {
21449
+ if (isSkillsFleetCredentialError(error61))
21450
+ throw error61;
21451
+ throw new SkillsFleetCredentialError("--url is not a valid URL", "INVALID_API_URL");
21452
+ }
21453
+ try {
21454
+ toV1BaseUrl(origin);
21455
+ } catch (error61) {
21456
+ if (isClientTransportConfigurationError(error61))
21457
+ throw new SkillsFleetCredentialError(error61.message, "INVALID_API_URL");
21458
+ throw error61;
21459
+ }
21460
+ return origin;
21461
+ }
21462
+ function resolveSkillsSignInOrigin(env = process.env, options = {}, explicitUrl) {
21463
+ try {
21464
+ const snapshot = snapshotSkillsEnvironment(env);
21465
+ const resolvedOptions = snapshotSkillsOptions(env, options);
21466
+ const profile = resolvedOptions.credentials?.profile;
21467
+ const configured = configuredSkillsApiUrl(snapshot, resolvedOptions.credentials?.keychain, profile);
21468
+ const declared = configured && configured.source !== "default" ? configured : null;
21469
+ if (explicitUrl !== undefined) {
21470
+ const origin = explicitSignInOrigin(explicitUrl);
21471
+ const storedHere = declared ? skillsProfileCredentialFiles(snapshot, profile).includes(declared.source) : false;
21472
+ if (declared && !storedHere && normalizeSkillsApiOrigin(declared.value) !== origin) {
21473
+ 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");
21474
+ }
21475
+ return { origin, source: "--url" };
21476
+ }
21477
+ if (declared) {
21478
+ toV1BaseUrl(declared.value);
21479
+ return { origin: normalizeSkillsApiOrigin(declared.value), source: declared.source };
21480
+ }
21481
+ let credential;
21482
+ try {
21483
+ credential = resolveCredential(SKILLS_APP, snapshot, resolvedOptions.credentials);
21484
+ } catch (error61) {
21485
+ const files = selectedSkillsProfile(snapshot, profile) ? skillsProfileCredentialFiles(snapshot, profile) : [];
21486
+ if (!isCredentialResolutionError(error61) || files.length === 0 || files.some(skillsFileHasCredential))
21487
+ throw error61;
21488
+ credential = null;
21489
+ }
21490
+ if (credential) {
21491
+ const pairing = credentialPairing(credential, snapshot, resolvedOptions);
21492
+ const origin = pairing.kind === "bound" ? pairing.instance : normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP));
21493
+ return { origin, source: "default", credentialSource: credential.source };
21494
+ }
21495
+ return { origin: SKILLS_PRODUCT_DEFAULT_ORIGIN, source: "default" };
21496
+ } catch (error61) {
21497
+ const translated = asSkillsFleetCredentialError(error61);
21498
+ if (translated)
21499
+ throw translated;
21500
+ if (isClientTransportConfigurationError(error61))
21501
+ throw new SkillsFleetCredentialError(error61.message, "INVALID_API_URL");
21502
+ throw error61;
21503
+ }
21504
+ }
21367
21505
  function requireSkillsFleet(action = "This command", env = process.env, options = {}) {
21368
21506
  const fleet = resolveSkillsFleet(env, options);
21369
21507
  if (fleet.mode === "hosted")
21370
21508
  return fleet;
21371
21509
  throw new MissingSkillsFleetError(action);
21372
21510
  }
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;
21511
+ 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
21512
  var init_fleet_credentials = __esm(() => {
21375
21513
  init_transport();
21376
21514
  init_instance_credentials();
@@ -21381,18 +21519,22 @@ var init_fleet_credentials = __esm(() => {
21381
21519
  SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
21382
21520
  SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
21383
21521
  SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
21522
+ SKILLS_CREDENTIAL_TIERS = ["argument", "override", "pointer", "profile", "keychain", "disk", "env"];
21384
21523
  SkillsFleetCredentialError = class SkillsFleetCredentialError extends Error {
21385
21524
  code;
21386
- constructor(message, code = "MISSING_API_CREDENTIAL") {
21525
+ next;
21526
+ constructor(message, code = "MISSING_API_CREDENTIAL", next) {
21387
21527
  super(message);
21388
21528
  this.name = "SkillsFleetCredentialError";
21389
21529
  this.code = code;
21530
+ if (next)
21531
+ this.next = next;
21390
21532
  }
21391
21533
  };
21392
21534
  MissingSkillsFleetError = class MissingSkillsFleetError extends Error {
21393
21535
  code = "MISSING_API_URL";
21394
21536
  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>`);
21537
+ 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
21538
  this.name = "MissingSkillsFleetError";
21397
21539
  }
21398
21540
  };
@@ -49905,7 +50047,7 @@ var init_api_url = __esm(() => {
49905
50047
  init_fleet_credentials();
49906
50048
  init_fleet_credentials();
49907
50049
  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>`;
50050
+ 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
50051
  });
49910
50052
 
49911
50053
  // src/lib/discovery.ts
@@ -53006,7 +53148,7 @@ class RemoteSkillsAuthClient {
53006
53148
  return (await this.sessionClient(email4, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
53007
53149
  }
53008
53150
  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))
53151
+ 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
53152
  throw new Error("Unsupported authentication operation");
53011
53153
  return requestAuthApi(this.apiOrigin, path, options);
53012
53154
  }
@@ -55241,7 +55383,7 @@ async function readSurface(body) {
55241
55383
  return await body();
55242
55384
  } catch (error61) {
55243
55385
  if (isSkillsFleetCredentialError(error61))
55244
- return mcpError("AUTH_REQUIRED", error61.message, ["skills auth login"]);
55386
+ return mcpError("AUTH_REQUIRED", error61.message, ["skills login"]);
55245
55387
  throw error61;
55246
55388
  }
55247
55389
  }
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.20",
10
+ version: "0.10.1",
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.20",
24125
+ version: "0.10.1",
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.20",
24128
+ version: "0.10.1",
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. */
2
+ export declare function recordEnvironmentProfile(value: string | undefined): void;
3
+ export declare function getEnvironmentProfile(): string | undefined;