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