@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/README.md +80 -22
- package/bin/index.js +5081 -3944
- package/bin/mcp.js +175 -33
- package/bin/migrate.js +1 -1
- package/bin/server.js +5 -3
- package/bin/worker.js +1 -1
- package/dist/cli/commands/auth.d.ts +2 -0
- package/dist/cli/components/AccountView.d.ts +16 -0
- package/dist/cli/components/App.d.ts +18 -1
- package/dist/cli/components/SearchView.d.ts +3 -1
- package/dist/cli/profile-selection.d.ts +3 -0
- package/dist/index.js +76 -31
- package/dist/lib/auth-store.d.ts +48 -4
- package/dist/lib/cli-credential-profile.d.ts +2 -0
- package/dist/lib/fleet-credentials.d.ts +87 -3
- package/dist/lib/instance-credentials.d.ts +6 -0
- package/dist/lib/product-default.d.ts +13 -0
- package/dist/lib/sign-in.d.ts +213 -0
- package/dist/lib/vendor-host-policy.d.ts +26 -0
- package/dist/lib/vendor-host-url.d.ts +1 -1
- package/dist/sdk/index.js +75 -30
- package/package.json +1 -1
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.
|
|
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: { ...
|
|
21243
|
-
...
|
|
21244
|
-
enabled:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
21287
|
-
|
|
21288
|
-
|
|
21289
|
-
|
|
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
|
-
|
|
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
|
|
21301
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
55386
|
+
return mcpError("AUTH_REQUIRED", error61.message, ["skills login"]);
|
|
55245
55387
|
throw error61;
|
|
55246
55388
|
}
|
|
55247
55389
|
}
|
package/bin/migrate.js
CHANGED
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.
|
|
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
|
-
|
|
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.
|
|
24128
|
+
version: "0.10.1",
|
|
24129
24129
|
description: "Skills library for AI coding agents",
|
|
24130
24130
|
type: "module",
|
|
24131
24131
|
bin: {
|
|
@@ -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 {};
|