@hasna/skills 0.3.0 → 0.4.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 +94 -10
- package/bin/index.js +1398 -571
- package/bin/mcp.js +899 -211
- package/bin/server.js +14 -5
- package/bin/worker.js +2 -3
- package/dist/cli/commands/remote-account.d.ts +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +770 -210
- package/dist/lib/auth-store.d.ts +1 -1
- package/dist/lib/fleet-credentials.d.ts +8 -2
- package/dist/lib/instance-credentials-race.fixture.d.ts +1 -0
- package/dist/lib/instance-credentials.d.ts +13 -0
- package/dist/lib/mcp-contracts.d.ts +4 -0
- package/dist/lib/portable-skills-files.d.ts +4 -0
- package/dist/lib/portable-skills-types.d.ts +2 -0
- package/dist/lib/remote-account.d.ts +42 -0
- package/dist/lib/remote-auth.d.ts +32 -0
- package/dist/lib/remote-client.d.ts +56 -1
- package/dist/lib/remote-customer-operations.d.ts +106 -0
- package/dist/lib/remote-files.d.ts +21 -0
- package/dist/lib/run-routing.d.ts +1 -0
- package/dist/lib/run-state.d.ts +3 -0
- package/dist/mcp/remote-customer-tools.d.ts +2 -0
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +1057 -280
- package/dist/sdk/runs.d.ts +4 -4
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1221,6 +1221,16 @@ function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDecl
|
|
|
1221
1221
|
return true;
|
|
1222
1222
|
return false;
|
|
1223
1223
|
}
|
|
1224
|
+
function frontmatterString(raw) {
|
|
1225
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
1226
|
+
try {
|
|
1227
|
+
const decoded = JSON.parse(raw);
|
|
1228
|
+
if (typeof decoded === "string")
|
|
1229
|
+
return decoded;
|
|
1230
|
+
} catch {}
|
|
1231
|
+
}
|
|
1232
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
1233
|
+
}
|
|
1224
1234
|
function parseSkillFrontmatter(content) {
|
|
1225
1235
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1226
1236
|
if (!match)
|
|
@@ -1240,12 +1250,12 @@ function parseSkillFrontmatter(content) {
|
|
|
1240
1250
|
const tags = [];
|
|
1241
1251
|
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
|
|
1242
1252
|
i++;
|
|
1243
|
-
tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
|
|
1253
|
+
tags.push(frontmatterString(lines[i].replace(/^\s+-\s+/, "").trim()));
|
|
1244
1254
|
}
|
|
1245
1255
|
result.tags = tags;
|
|
1246
1256
|
continue;
|
|
1247
1257
|
}
|
|
1248
|
-
const value = rawValue
|
|
1258
|
+
const value = frontmatterString(rawValue);
|
|
1249
1259
|
if (!value)
|
|
1250
1260
|
continue;
|
|
1251
1261
|
if (key === "name")
|
|
@@ -1878,8 +1888,8 @@ function createInstructionManifest(name, options) {
|
|
|
1878
1888
|
description: options.description,
|
|
1879
1889
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
1880
1890
|
displayName: displayName(name),
|
|
1881
|
-
category: "Development Tools",
|
|
1882
|
-
tags: ["custom", name],
|
|
1891
|
+
category: options.category ?? "Development Tools",
|
|
1892
|
+
tags: options.tags ?? ["custom", name],
|
|
1883
1893
|
kind: "instruction",
|
|
1884
1894
|
inputs: [],
|
|
1885
1895
|
commands: [],
|
|
@@ -1894,16 +1904,16 @@ function writeInstructionSkillTemplate(skillPath, manifest) {
|
|
|
1894
1904
|
}
|
|
1895
1905
|
function renderInstructionSkillMd(manifest) {
|
|
1896
1906
|
const tags = manifest.tags?.length ? `tags:
|
|
1897
|
-
${manifest.tags.map((tag) => ` - ${tag}`).join(`
|
|
1907
|
+
${manifest.tags.map((tag) => ` - ${yamlString(tag)}`).join(`
|
|
1898
1908
|
`)}
|
|
1899
1909
|
` : "";
|
|
1900
1910
|
return `---
|
|
1901
1911
|
name: ${manifest.name}
|
|
1902
|
-
description: ${manifest.description}
|
|
1912
|
+
description: ${yamlString(manifest.description)}
|
|
1903
1913
|
kind: instruction
|
|
1904
1914
|
version: ${manifest.version}
|
|
1905
1915
|
source: custom
|
|
1906
|
-
category: ${manifest.category ?? "Development Tools"}
|
|
1916
|
+
category: ${yamlString(manifest.category ?? "Development Tools")}
|
|
1907
1917
|
${tags}---
|
|
1908
1918
|
|
|
1909
1919
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -1924,8 +1934,8 @@ function createPortableManifest(name, options) {
|
|
|
1924
1934
|
description: options.description,
|
|
1925
1935
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
1926
1936
|
displayName: displayName(name),
|
|
1927
|
-
category: "Development Tools",
|
|
1928
|
-
tags: ["custom", name],
|
|
1937
|
+
category: options.category ?? "Development Tools",
|
|
1938
|
+
tags: options.tags ?? ["custom", name],
|
|
1929
1939
|
inputs: DEFAULT_INPUTS,
|
|
1930
1940
|
commands: [{
|
|
1931
1941
|
name,
|
|
@@ -2116,10 +2126,13 @@ function isExcludedCopyEntry(name, isFirstSegment) {
|
|
|
2116
2126
|
return true;
|
|
2117
2127
|
return false;
|
|
2118
2128
|
}
|
|
2129
|
+
function yamlString(value) {
|
|
2130
|
+
return JSON.stringify(value);
|
|
2131
|
+
}
|
|
2119
2132
|
function renderSkillMd(manifest) {
|
|
2120
2133
|
return `---
|
|
2121
2134
|
name: ${manifest.name}
|
|
2122
|
-
description: ${manifest.description}
|
|
2135
|
+
description: ${yamlString(manifest.description)}
|
|
2123
2136
|
---
|
|
2124
2137
|
|
|
2125
2138
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -2502,11 +2515,11 @@ function scaffoldPortableSkill(name, options = {}) {
|
|
|
2502
2515
|
const kind = options.kind ?? "executable";
|
|
2503
2516
|
const description = options.description ?? `${displayName(skillName)} skill`;
|
|
2504
2517
|
if (kind === "instruction") {
|
|
2505
|
-
const manifest2 = createInstructionManifest(skillName, { description });
|
|
2518
|
+
const manifest2 = createInstructionManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
2506
2519
|
writeInstructionSkillTemplate(skillPath, manifest2);
|
|
2507
2520
|
return { name: skillName, path: skillPath, manifest: manifest2, created: true };
|
|
2508
2521
|
}
|
|
2509
|
-
const manifest = createPortableManifest(skillName, { description });
|
|
2522
|
+
const manifest = createPortableManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
2510
2523
|
writePortableSkillTemplate(skillPath, manifest);
|
|
2511
2524
|
return { name: skillName, path: skillPath, manifest, created: true };
|
|
2512
2525
|
}
|
|
@@ -3950,6 +3963,7 @@ function createSkillRun(params, targetDir = process.cwd()) {
|
|
|
3950
3963
|
startedAt: now.toISOString(),
|
|
3951
3964
|
remote: params.remote ?? false,
|
|
3952
3965
|
...params.remoteRunId ? { remoteRunId: params.remoteRunId } : {},
|
|
3966
|
+
...params.remoteApiOrigin ? { remoteApiOrigin: params.remoteApiOrigin } : {},
|
|
3953
3967
|
...params.costCents !== undefined ? { costCents: params.costCents } : {},
|
|
3954
3968
|
artifacts: [],
|
|
3955
3969
|
paths: {
|
|
@@ -8964,7 +8978,6 @@ async function completePointerCredential(name, pointerResolution, env = process.
|
|
|
8964
8978
|
});
|
|
8965
8979
|
}
|
|
8966
8980
|
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
8967
|
-
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
8968
8981
|
function defaultFleetGatewayBaseUrl(name) {
|
|
8969
8982
|
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
8970
8983
|
}
|
|
@@ -9083,102 +9096,6 @@ function toV1BaseUrl(apiUrl) {
|
|
|
9083
9096
|
url.pathname = `${path}/v1`;
|
|
9084
9097
|
return url.toString().replace(/\/+$/, "");
|
|
9085
9098
|
}
|
|
9086
|
-
class ClientTransportConfigurationError extends Error {
|
|
9087
|
-
appName;
|
|
9088
|
-
sources;
|
|
9089
|
-
constructor(appName, message, sources = []) {
|
|
9090
|
-
super(message);
|
|
9091
|
-
this.name = "ClientTransportConfigurationError";
|
|
9092
|
-
this.appName = appName;
|
|
9093
|
-
this.sources = Object.freeze([...sources]);
|
|
9094
|
-
}
|
|
9095
|
-
}
|
|
9096
|
-
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
9097
|
-
env = snapshotClientEnvironment(name, env);
|
|
9098
|
-
const keys = clientTransportEnvKeys(name);
|
|
9099
|
-
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
9100
|
-
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
9101
|
-
if (blankUrl) {
|
|
9102
|
-
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
9103
|
-
}
|
|
9104
|
-
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
9105
|
-
if (controlledUrl) {
|
|
9106
|
-
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
9107
|
-
}
|
|
9108
|
-
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
9109
|
-
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
9110
|
-
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
9111
|
-
}
|
|
9112
|
-
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
9113
|
-
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
9114
|
-
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
9115
|
-
if (diskConfigUrlHit?.unusable) {
|
|
9116
|
-
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
9117
|
-
}
|
|
9118
|
-
const urlCandidates = [
|
|
9119
|
-
...envUrlHit ? [envUrlHit] : [],
|
|
9120
|
-
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
9121
|
-
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
9122
|
-
];
|
|
9123
|
-
const configuredUrl = urlCandidates[0] ?? null;
|
|
9124
|
-
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
9125
|
-
if (configuredUrl && divergentUrls.length > 0) {
|
|
9126
|
-
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
9127
|
-
}
|
|
9128
|
-
const warnings = [];
|
|
9129
|
-
if (configuredUrl && !envUrlHit) {
|
|
9130
|
-
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
9131
|
-
}
|
|
9132
|
-
const credential = resolveCredential(name, env, options.credentials);
|
|
9133
|
-
if (!credential) {
|
|
9134
|
-
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
9135
|
-
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
9136
|
-
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
9137
|
-
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
9138
|
-
}
|
|
9139
|
-
if (credential.warning)
|
|
9140
|
-
warnings.push(credential.warning);
|
|
9141
|
-
let urlHit;
|
|
9142
|
-
if (configuredUrl) {
|
|
9143
|
-
urlHit = configuredUrl;
|
|
9144
|
-
} else {
|
|
9145
|
-
try {
|
|
9146
|
-
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
9147
|
-
} catch (error) {
|
|
9148
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9149
|
-
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
9150
|
-
}
|
|
9151
|
-
}
|
|
9152
|
-
const apiUrlSource = urlHit.key;
|
|
9153
|
-
let baseUrl;
|
|
9154
|
-
try {
|
|
9155
|
-
baseUrl = toV1BaseUrl(urlHit.value);
|
|
9156
|
-
} catch (error) {
|
|
9157
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9158
|
-
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
9159
|
-
}
|
|
9160
|
-
return {
|
|
9161
|
-
resolution: {
|
|
9162
|
-
transport: "http",
|
|
9163
|
-
transportSource: urlHit.key,
|
|
9164
|
-
baseUrl,
|
|
9165
|
-
apiUrlSource,
|
|
9166
|
-
apiKeyPresent: true,
|
|
9167
|
-
apiKeySource: credential.source,
|
|
9168
|
-
apiKeyTier: credential.tier,
|
|
9169
|
-
misconfigured: false,
|
|
9170
|
-
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9171
|
-
},
|
|
9172
|
-
credential
|
|
9173
|
-
};
|
|
9174
|
-
}
|
|
9175
|
-
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
9176
|
-
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
9177
|
-
}
|
|
9178
|
-
function credentialDiskSourcesForMessage(name, env) {
|
|
9179
|
-
const paths = credentialDiskSources(name, env);
|
|
9180
|
-
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
9181
|
-
}
|
|
9182
9099
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9183
9100
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
9184
9101
|
"host",
|
|
@@ -9188,6 +9105,94 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
9188
9105
|
"x-original-host"
|
|
9189
9106
|
]);
|
|
9190
9107
|
|
|
9108
|
+
// src/lib/instance-credentials.ts
|
|
9109
|
+
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync2, readSync } from "fs";
|
|
9110
|
+
var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
|
|
9111
|
+
function selectedSkillsProfile(env, explicit) {
|
|
9112
|
+
const selected = explicit ?? env.HASNA_PROFILE;
|
|
9113
|
+
if (selected === undefined)
|
|
9114
|
+
return null;
|
|
9115
|
+
const profile = selected.trim();
|
|
9116
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(profile))
|
|
9117
|
+
throw new Error("Invalid Skills credential profile");
|
|
9118
|
+
return profile;
|
|
9119
|
+
}
|
|
9120
|
+
function skillsProfileCredentialFiles(env, explicit) {
|
|
9121
|
+
return credentialDiskSourceList("skills", env, selectedSkillsProfile(env, explicit)).map((source) => source.path);
|
|
9122
|
+
}
|
|
9123
|
+
function fileIdentity(file) {
|
|
9124
|
+
try {
|
|
9125
|
+
return lstatSync3(file);
|
|
9126
|
+
} catch (error) {
|
|
9127
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
9128
|
+
return null;
|
|
9129
|
+
throw new Error("Cannot inspect Skills instance configuration");
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
function unchanged(before, after) {
|
|
9133
|
+
return before === null || after === null ? before === after : ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode", "uid"].every((key) => before[key] === after[key]);
|
|
9134
|
+
}
|
|
9135
|
+
function captureSkillsCredentialFiles(files) {
|
|
9136
|
+
const identities = files.map((file) => [file, fileIdentity(file)]);
|
|
9137
|
+
return () => {
|
|
9138
|
+
if (identities.some(([file, before]) => !unchanged(before, fileIdentity(file)))) {
|
|
9139
|
+
throw new Error("Skills instance configuration changed while resolving credentials; retry without sending a credential");
|
|
9140
|
+
}
|
|
9141
|
+
};
|
|
9142
|
+
}
|
|
9143
|
+
function readMetadataText(file) {
|
|
9144
|
+
let fd;
|
|
9145
|
+
try {
|
|
9146
|
+
fd = openSync2(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
9147
|
+
} catch (error) {
|
|
9148
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
9149
|
+
return null;
|
|
9150
|
+
throw new Error("Cannot safely read Skills instance configuration");
|
|
9151
|
+
}
|
|
9152
|
+
try {
|
|
9153
|
+
const before = fstatSync2(fd);
|
|
9154
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
9155
|
+
if (!before.isFile() || ![256, 384].includes(before.mode & 4095) || uid !== undefined && before.uid !== uid || before.size > 64 * 1024) {
|
|
9156
|
+
throw new Error("Unsafe Skills instance configuration; expected a bounded owner-only regular file");
|
|
9157
|
+
}
|
|
9158
|
+
const bytes = Buffer.alloc(64 * 1024 + 1);
|
|
9159
|
+
let length = 0;
|
|
9160
|
+
while (length < bytes.length) {
|
|
9161
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
9162
|
+
if (!count)
|
|
9163
|
+
break;
|
|
9164
|
+
length += count;
|
|
9165
|
+
}
|
|
9166
|
+
if (length > 64 * 1024 || !unchanged(before, fstatSync2(fd)) || !unchanged(before, fileIdentity(file))) {
|
|
9167
|
+
throw new Error("Skills instance configuration changed while reading");
|
|
9168
|
+
}
|
|
9169
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
9170
|
+
} finally {
|
|
9171
|
+
closeSync2(fd);
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
function readSkillsInstanceMetadata(file) {
|
|
9175
|
+
const text = readMetadataText(file);
|
|
9176
|
+
if (text === null)
|
|
9177
|
+
return {};
|
|
9178
|
+
const values = new Map;
|
|
9179
|
+
for (const line of text.split(/\r?\n/)) {
|
|
9180
|
+
const match = /^\s*(?:export\s+)?(HASNA_SKILLS_API_URL|SKILLS_API_URL|HASNA_SKILLS_BOUND_API_URL)\s*=\s*(.*)$/.exec(line);
|
|
9181
|
+
if (!match)
|
|
9182
|
+
continue;
|
|
9183
|
+
let value = match[2].trim();
|
|
9184
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
|
|
9185
|
+
value = value.slice(1, -1);
|
|
9186
|
+
if (!value || /[\x00-\x20\x7f]/.test(value) || values.has(match[1]))
|
|
9187
|
+
throw new Error("Invalid Skills instance configuration");
|
|
9188
|
+
values.set(match[1], value);
|
|
9189
|
+
}
|
|
9190
|
+
const urls = [values.get("HASNA_SKILLS_API_URL"), values.get("SKILLS_API_URL")].filter(Boolean);
|
|
9191
|
+
if (new Set(urls).size > 1)
|
|
9192
|
+
throw new Error("Skills API URL aliases disagree");
|
|
9193
|
+
return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9191
9196
|
// src/lib/fleet-credentials.ts
|
|
9192
9197
|
var SKILLS_APP = "skills";
|
|
9193
9198
|
var ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
|
|
@@ -9204,9 +9209,6 @@ class SkillsFleetCredentialError extends Error {
|
|
|
9204
9209
|
this.code = code;
|
|
9205
9210
|
}
|
|
9206
9211
|
}
|
|
9207
|
-
function isClientTransportConfigurationError(error) {
|
|
9208
|
-
return error instanceof ClientTransportConfigurationError || typeof error === "object" && error !== null && error.name === "ClientTransportConfigurationError";
|
|
9209
|
-
}
|
|
9210
9212
|
function isCredentialResolutionError(error) {
|
|
9211
9213
|
return error instanceof CredentialResolutionError || typeof error === "object" && error !== null && error.name === "CredentialResolutionError";
|
|
9212
9214
|
}
|
|
@@ -9217,6 +9219,9 @@ function asSkillsFleetCredentialError(error) {
|
|
|
9217
9219
|
}
|
|
9218
9220
|
function normalizeSkillsApiOrigin(apiUrl) {
|
|
9219
9221
|
const url = new URL(apiUrl);
|
|
9222
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
9223
|
+
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
9224
|
+
}
|
|
9220
9225
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
9221
9226
|
if (pathname === "/api" || pathname === "/api/v1") {
|
|
9222
9227
|
url.pathname = "/";
|
|
@@ -9227,11 +9232,24 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
9227
9232
|
}
|
|
9228
9233
|
return url.toString().replace(/\/+$/, "");
|
|
9229
9234
|
}
|
|
9230
|
-
function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
if (value)
|
|
9234
|
-
|
|
9235
|
+
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
9236
|
+
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
9237
|
+
for (const entry of declared) {
|
|
9238
|
+
if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
|
|
9239
|
+
throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
|
|
9240
|
+
}
|
|
9241
|
+
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
|
|
9242
|
+
if (new Set(normalized.map((entry) => entry.value)).size > 1)
|
|
9243
|
+
throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
|
|
9244
|
+
if (normalized[0])
|
|
9245
|
+
return normalized[0];
|
|
9246
|
+
if (selectedSkillsProfile(env, profile)) {
|
|
9247
|
+
for (const file of skillsProfileCredentialFiles(env, profile)) {
|
|
9248
|
+
const metadata = readSkillsInstanceMetadata(file);
|
|
9249
|
+
if (metadata.apiUrl || metadata.binding)
|
|
9250
|
+
return { value: metadata.apiUrl ?? metadata.binding, source: file };
|
|
9251
|
+
}
|
|
9252
|
+
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
|
|
9235
9253
|
}
|
|
9236
9254
|
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
9237
9255
|
if (fromKeychain)
|
|
@@ -9245,7 +9263,7 @@ function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
|
9245
9263
|
return null;
|
|
9246
9264
|
}
|
|
9247
9265
|
function skillsCredentialFiles(env = process.env) {
|
|
9248
|
-
return
|
|
9266
|
+
return skillsProfileCredentialFiles(env);
|
|
9249
9267
|
}
|
|
9250
9268
|
function skillsCredentialFilePath(env = process.env) {
|
|
9251
9269
|
const paths = skillsCredentialFiles(env);
|
|
@@ -9264,7 +9282,11 @@ function noticeLocalSkillsMode(write = (line) => console.error(line)) {
|
|
|
9264
9282
|
}
|
|
9265
9283
|
function resolveSkillsFleet(env = process.env, options = {}) {
|
|
9266
9284
|
try {
|
|
9267
|
-
|
|
9285
|
+
const snapshot = snapshotSkillsEnvironment(env);
|
|
9286
|
+
const resolved = resolveSkillsFleetOrThrow(snapshot, snapshotSkillsOptions(env, options));
|
|
9287
|
+
if (resolved.mode === "local" && env === process.env)
|
|
9288
|
+
noticeLocalSkillsMode();
|
|
9289
|
+
return resolved;
|
|
9268
9290
|
} catch (error) {
|
|
9269
9291
|
const translated = asSkillsFleetCredentialError(error);
|
|
9270
9292
|
if (translated)
|
|
@@ -9272,38 +9294,46 @@ function resolveSkillsFleet(env = process.env, options = {}) {
|
|
|
9272
9294
|
throw error;
|
|
9273
9295
|
}
|
|
9274
9296
|
}
|
|
9275
|
-
function
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
const configured2 = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9283
|
-
const credential2 = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9284
|
-
if (!configured2 && !credential2) {
|
|
9285
|
-
if (env === process.env)
|
|
9286
|
-
noticeLocalSkillsMode();
|
|
9287
|
-
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
9288
|
-
}
|
|
9289
|
-
if (configured2 && !credential2) {
|
|
9290
|
-
throw new SkillsFleetCredentialError(`${configured2.source} points this CLI at a Skills service but no API key resolved \u2014 ` + `refusing to run locally instead. Looked in the Keychain item ` + `hasna.credentials.${SKILLS_APP}.api-key, then ${skillsCredentialFiles(env).join(" or ") || "no credentials file (no HOME)"}, ` + `then ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
9297
|
+
function snapshotSkillsEnvironment(env) {
|
|
9298
|
+
const snapshot = {};
|
|
9299
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(env))) {
|
|
9300
|
+
if (!("value" in descriptor)) {
|
|
9301
|
+
if (/^(?:HASNA_|SKILLS_|HOME$|USER$)/.test(key))
|
|
9302
|
+
throw new SkillsFleetCredentialError("Accessor-backed Skills configuration is unsupported");
|
|
9303
|
+
continue;
|
|
9291
9304
|
}
|
|
9292
|
-
|
|
9305
|
+
snapshot[key] = descriptor.value;
|
|
9293
9306
|
}
|
|
9294
|
-
|
|
9295
|
-
|
|
9307
|
+
return Object.freeze(snapshot);
|
|
9308
|
+
}
|
|
9309
|
+
function snapshotSkillsOptions(env, options) {
|
|
9310
|
+
if (env !== process.env)
|
|
9311
|
+
return options;
|
|
9312
|
+
return { ...options, credentials: { ...options.credentials, keychain: {
|
|
9313
|
+
...options.credentials?.keychain,
|
|
9314
|
+
enabled: options.credentials?.keychain?.enabled ?? true
|
|
9315
|
+
} } };
|
|
9316
|
+
}
|
|
9317
|
+
function resolveSkillsFleetOrThrow(env, options) {
|
|
9318
|
+
const assertFilesUnchanged = captureSkillsCredentialFiles(skillsProfileCredentialFiles(env, options.credentials?.profile));
|
|
9319
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
9296
9320
|
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9297
9321
|
if (!credential) {
|
|
9298
|
-
|
|
9322
|
+
if (!configured)
|
|
9323
|
+
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
9324
|
+
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 hasna.credentials.skills.api-key, ${skillsCredentialFiles(env).join(" or ") || "no credentials file"}, and ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
9299
9325
|
}
|
|
9326
|
+
const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
|
|
9327
|
+
toV1BaseUrl(apiOrigin);
|
|
9328
|
+
assertCredentialInstance(credential, apiOrigin, env, options);
|
|
9329
|
+
assertFilesUnchanged();
|
|
9300
9330
|
const base = {
|
|
9301
9331
|
mode: "hosted",
|
|
9302
9332
|
apiOrigin,
|
|
9303
|
-
apiUrlSource:
|
|
9304
|
-
apiKeySource:
|
|
9305
|
-
apiKeyTier:
|
|
9306
|
-
warning:
|
|
9333
|
+
apiUrlSource: configured?.source ?? "default",
|
|
9334
|
+
apiKeySource: credential.source,
|
|
9335
|
+
apiKeyTier: credential.tier,
|
|
9336
|
+
warning: credential.warning
|
|
9307
9337
|
};
|
|
9308
9338
|
if (credential.tier === "pointer") {
|
|
9309
9339
|
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
@@ -9313,19 +9343,37 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
9313
9343
|
}
|
|
9314
9344
|
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
9315
9345
|
}
|
|
9346
|
+
function assertCredentialInstance(credential, apiOrigin, env, options) {
|
|
9347
|
+
let bound;
|
|
9348
|
+
if (credential.tier === "disk" || credential.tier === "profile") {
|
|
9349
|
+
const metadata = readSkillsInstanceMetadata(credential.source);
|
|
9350
|
+
bound = metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
9351
|
+
} else if (credential.tier === "keychain") {
|
|
9352
|
+
bound = keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
9353
|
+
}
|
|
9354
|
+
if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
|
|
9355
|
+
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");
|
|
9356
|
+
}
|
|
9357
|
+
}
|
|
9316
9358
|
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
9317
|
-
|
|
9359
|
+
return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
|
|
9360
|
+
}
|
|
9361
|
+
async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
9362
|
+
const snapshotEnv = snapshotSkillsEnvironment(env);
|
|
9363
|
+
const fleet = resolveSkillsFleet(snapshotEnv, snapshotSkillsOptions(env, options));
|
|
9364
|
+
if (fleet.mode === "local" && env === process.env)
|
|
9365
|
+
noticeLocalSkillsMode();
|
|
9318
9366
|
if (fleet.mode !== "hosted")
|
|
9319
9367
|
return null;
|
|
9320
9368
|
if (fleet.apiKey)
|
|
9321
|
-
return fleet.apiKey;
|
|
9369
|
+
return { ...fleet, apiKey: fleet.apiKey };
|
|
9322
9370
|
const pointer = fleet.apiKeyPointer;
|
|
9323
9371
|
if (!pointer) {
|
|
9324
9372
|
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
|
|
9325
9373
|
}
|
|
9326
9374
|
let completed;
|
|
9327
9375
|
try {
|
|
9328
|
-
completed = await completePointerCredential(SKILLS_APP, pointer,
|
|
9376
|
+
completed = await completePointerCredential(SKILLS_APP, pointer, snapshotEnv);
|
|
9329
9377
|
} catch (error) {
|
|
9330
9378
|
const translated = asSkillsFleetCredentialError(error);
|
|
9331
9379
|
if (translated)
|
|
@@ -9335,7 +9383,7 @@ async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
|
9335
9383
|
if (!completed.apiKey?.trim()) {
|
|
9336
9384
|
throw new SkillsFleetCredentialError(`${credentialPointerEnvKey(SKILLS_APP)} names a vault item that produced an empty Skills API key \u2014 ` + `refusing to send an unauthenticated request.`);
|
|
9337
9385
|
}
|
|
9338
|
-
return completed.apiKey;
|
|
9386
|
+
return { ...fleet, apiKey: completed.apiKey, apiKeyPointer: null };
|
|
9339
9387
|
}
|
|
9340
9388
|
async function requireSkillsApiKey(action = "This command", env = process.env, options = {}) {
|
|
9341
9389
|
const apiKey = await resolveSkillsApiKey(env, options);
|
|
@@ -9343,22 +9391,19 @@ async function requireSkillsApiKey(action = "This command", env = process.env, o
|
|
|
9343
9391
|
throw new MissingSkillsFleetError(action);
|
|
9344
9392
|
return apiKey;
|
|
9345
9393
|
}
|
|
9346
|
-
function stripV1(baseUrl) {
|
|
9347
|
-
return baseUrl.replace(/\/v1$/, "").replace(/\/+$/, "");
|
|
9348
|
-
}
|
|
9349
9394
|
async function skillsCredentialOrReason(env = process.env, options = {}) {
|
|
9350
9395
|
try {
|
|
9351
|
-
const
|
|
9352
|
-
return
|
|
9396
|
+
const connection = await resolveSkillsConnection(env, options);
|
|
9397
|
+
return connection ? { apiKey: connection.apiKey, apiOrigin: connection.apiOrigin, reason: null } : { apiKey: null, apiOrigin: null, reason: null };
|
|
9353
9398
|
} catch (error) {
|
|
9354
9399
|
if (error instanceof SkillsFleetCredentialError || error?.name === "SkillsFleetCredentialError") {
|
|
9355
|
-
return { apiKey: null, reason: error.message };
|
|
9400
|
+
return { apiKey: null, apiOrigin: null, reason: error.message };
|
|
9356
9401
|
}
|
|
9357
9402
|
throw error;
|
|
9358
9403
|
}
|
|
9359
9404
|
}
|
|
9360
9405
|
function resolveSkillsApiOrigin(env = process.env, options = {}) {
|
|
9361
|
-
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9406
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
9362
9407
|
if (configured) {
|
|
9363
9408
|
toV1BaseUrl(configured.value);
|
|
9364
9409
|
return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
|
|
@@ -10181,6 +10226,141 @@ function pickNumber(record, key) {
|
|
|
10181
10226
|
return typeof value === "number" && Number.isFinite(value) ? { [key]: value } : {};
|
|
10182
10227
|
}
|
|
10183
10228
|
|
|
10229
|
+
// src/lib/remote-account.ts
|
|
10230
|
+
class RemoteCreditApprovalError extends Error {
|
|
10231
|
+
requiredCredits;
|
|
10232
|
+
maximumCredits;
|
|
10233
|
+
code = "CREDIT_APPROVAL_REQUIRED";
|
|
10234
|
+
constructor(requiredCredits, maximumCredits) {
|
|
10235
|
+
super(`This run requires ${requiredCredits} credits; the approved maximum is ${maximumCredits}. Quote the run and explicitly approve its cost.`);
|
|
10236
|
+
this.requiredCredits = requiredCredits;
|
|
10237
|
+
this.maximumCredits = maximumCredits;
|
|
10238
|
+
this.name = "RemoteCreditApprovalError";
|
|
10239
|
+
}
|
|
10240
|
+
}
|
|
10241
|
+
function creditCount(value) {
|
|
10242
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 2147483647) {
|
|
10243
|
+
throw new Error("The Skills server returned an invalid credit count");
|
|
10244
|
+
}
|
|
10245
|
+
return value;
|
|
10246
|
+
}
|
|
10247
|
+
function parseRemoteRunQuote(value) {
|
|
10248
|
+
const quote = object(value);
|
|
10249
|
+
const pricing = object(quote.pricing);
|
|
10250
|
+
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
10251
|
+
throw new Error("Invalid quoted skill");
|
|
10252
|
+
const costCents = creditCount(pricing.costCredits ?? pricing.costCents);
|
|
10253
|
+
if (pricing.costCredits !== undefined && pricing.costCents !== undefined && pricing.costCents !== costCents)
|
|
10254
|
+
throw new Error("Inconsistent quoted credit count");
|
|
10255
|
+
if (quote.availability && object(quote.availability).status !== "available")
|
|
10256
|
+
throw new Error("This skill is unavailable for remote execution");
|
|
10257
|
+
return { ...quote, skill: quote.skill, pricing: { ...pricing, costCredits: costCents, costCents, formattedCost: `${costCents} credits` } };
|
|
10258
|
+
}
|
|
10259
|
+
function parseRemoteCreditPacks(value) {
|
|
10260
|
+
if (!Array.isArray(value))
|
|
10261
|
+
throw new Error("Invalid credit pack response");
|
|
10262
|
+
const ids = new Set;
|
|
10263
|
+
return value.map((value2) => {
|
|
10264
|
+
const row = object(value2);
|
|
10265
|
+
const counts = [row.credits, row.creditsCents, row.amountCents].filter((value3) => value3 !== undefined).map(creditCount);
|
|
10266
|
+
if (!counts.length || counts[0] === 0 || counts.some((count) => count !== counts[0]))
|
|
10267
|
+
throw new Error("Inconsistent credit pack counts");
|
|
10268
|
+
const credits = counts[0];
|
|
10269
|
+
const id = row.id ?? `credits_${credits}`;
|
|
10270
|
+
if (typeof id !== "string" || !/^[a-z0-9][a-z0-9_-]{0,99}$/.test(id) || ids.has(id))
|
|
10271
|
+
throw new Error("Invalid credit pack ID");
|
|
10272
|
+
if (id.startsWith("credits_") && id !== `credits_${credits}`)
|
|
10273
|
+
throw new Error("Inconsistent credit pack ID");
|
|
10274
|
+
ids.add(id);
|
|
10275
|
+
return { id, credits, ...row.expiresInDays === undefined ? {} : { expiresInDays: creditCount(row.expiresInDays) } };
|
|
10276
|
+
});
|
|
10277
|
+
}
|
|
10278
|
+
function parseRemoteBillingStatus(value) {
|
|
10279
|
+
const row = object(value);
|
|
10280
|
+
const counts = [row.creditBalance, row.balanceCents].filter((value2) => value2 !== undefined).map(creditCount);
|
|
10281
|
+
if (!counts.length || counts.some((count) => count !== counts[0]))
|
|
10282
|
+
throw new Error("Inconsistent credit balance");
|
|
10283
|
+
return {
|
|
10284
|
+
creditBalance: counts[0],
|
|
10285
|
+
formattedCreditBalance: `${counts[0]} credits`,
|
|
10286
|
+
...typeof row.plan === "string" ? { plan: row.plan } : {},
|
|
10287
|
+
...typeof row.hasPaymentMethod === "boolean" ? { hasPaymentMethod: row.hasPaymentMethod } : {}
|
|
10288
|
+
};
|
|
10289
|
+
}
|
|
10290
|
+
function parseRemoteCheckout(value) {
|
|
10291
|
+
const row = object(value);
|
|
10292
|
+
if (typeof row.url !== "string")
|
|
10293
|
+
throw new Error("Invalid checkout URL");
|
|
10294
|
+
const url = new URL(row.url);
|
|
10295
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
10296
|
+
throw new Error("Invalid checkout URL");
|
|
10297
|
+
return { url: row.url };
|
|
10298
|
+
}
|
|
10299
|
+
function object(value) {
|
|
10300
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10301
|
+
throw new Error("Invalid Skills server response");
|
|
10302
|
+
return value;
|
|
10303
|
+
}
|
|
10304
|
+
|
|
10305
|
+
// src/lib/remote-files.ts
|
|
10306
|
+
import { createHash as createHash3 } from "crypto";
|
|
10307
|
+
var MAX_REMOTE_FILE_BYTES = 64 * 1024 * 1024;
|
|
10308
|
+
function describeRemoteFiles(files) {
|
|
10309
|
+
if (files.length > 10)
|
|
10310
|
+
throw new Error("At most 10 input files are supported");
|
|
10311
|
+
const names = new Set;
|
|
10312
|
+
let total = 0;
|
|
10313
|
+
return files.map((file) => {
|
|
10314
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(file.name) || file.name === "." || file.name === ".." || names.has(file.name))
|
|
10315
|
+
throw new Error("Input file names must be unique safe basenames");
|
|
10316
|
+
names.add(file.name);
|
|
10317
|
+
total += file.bytes.byteLength;
|
|
10318
|
+
if (file.bytes.byteLength > 20 * 1024 * 1024 || total > 50 * 1024 * 1024)
|
|
10319
|
+
throw new Error("Input files exceed the supported size limit");
|
|
10320
|
+
return { name: file.name, sizeBytes: file.bytes.byteLength, sha256: sha256(file.bytes), contentType: file.contentType ?? "application/octet-stream" };
|
|
10321
|
+
});
|
|
10322
|
+
}
|
|
10323
|
+
function sha256(bytes) {
|
|
10324
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
10325
|
+
}
|
|
10326
|
+
async function readBoundedResponse(response, maximum) {
|
|
10327
|
+
if (!Number.isSafeInteger(maximum) || maximum < 0 || maximum > MAX_REMOTE_FILE_BYTES)
|
|
10328
|
+
throw new Error("Invalid artifact size limit");
|
|
10329
|
+
const length = response.headers.get("content-length");
|
|
10330
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
10331
|
+
await response.body?.cancel();
|
|
10332
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
10333
|
+
}
|
|
10334
|
+
const reader = response.body?.getReader();
|
|
10335
|
+
if (!reader)
|
|
10336
|
+
return new Uint8Array;
|
|
10337
|
+
const chunks = [];
|
|
10338
|
+
let size = 0;
|
|
10339
|
+
try {
|
|
10340
|
+
while (true) {
|
|
10341
|
+
const next = await reader.read();
|
|
10342
|
+
if (next.done)
|
|
10343
|
+
break;
|
|
10344
|
+
size += next.value.byteLength;
|
|
10345
|
+
if (size > maximum)
|
|
10346
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
10347
|
+
chunks.push(next.value);
|
|
10348
|
+
}
|
|
10349
|
+
} catch (error) {
|
|
10350
|
+
await reader.cancel().catch(() => {});
|
|
10351
|
+
throw error;
|
|
10352
|
+
} finally {
|
|
10353
|
+
reader.releaseLock();
|
|
10354
|
+
}
|
|
10355
|
+
const bytes = new Uint8Array(size);
|
|
10356
|
+
let offset = 0;
|
|
10357
|
+
for (const chunk of chunks) {
|
|
10358
|
+
bytes.set(chunk, offset);
|
|
10359
|
+
offset += chunk.byteLength;
|
|
10360
|
+
}
|
|
10361
|
+
return bytes;
|
|
10362
|
+
}
|
|
10363
|
+
|
|
10184
10364
|
// src/lib/remote-client.ts
|
|
10185
10365
|
class RemoteRouteUnsupportedError extends Error {
|
|
10186
10366
|
path;
|
|
@@ -10209,13 +10389,16 @@ class RemoteRequestError extends Error {
|
|
|
10209
10389
|
class RemoteSkillsClient {
|
|
10210
10390
|
apiUrl;
|
|
10211
10391
|
apiKey;
|
|
10392
|
+
capabilities;
|
|
10212
10393
|
constructor(apiKey, apiUrl = getApiUrl()) {
|
|
10213
10394
|
this.apiKey = apiKey;
|
|
10214
|
-
this.apiUrl = apiUrl
|
|
10395
|
+
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
10215
10396
|
}
|
|
10216
10397
|
async request(path, options) {
|
|
10217
10398
|
return fetch(`${this.apiUrl}${path}`, {
|
|
10218
10399
|
...options,
|
|
10400
|
+
redirect: "error",
|
|
10401
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
10219
10402
|
headers: {
|
|
10220
10403
|
Authorization: `Bearer ${this.apiKey}`,
|
|
10221
10404
|
"Content-Type": "application/json",
|
|
@@ -10238,8 +10421,7 @@ class RemoteSkillsClient {
|
|
|
10238
10421
|
return response;
|
|
10239
10422
|
}
|
|
10240
10423
|
async listSkills() {
|
|
10241
|
-
|
|
10242
|
-
return res.json();
|
|
10424
|
+
return this.arrayResponse("/api/v1/skills");
|
|
10243
10425
|
}
|
|
10244
10426
|
async getSkillMd(slug) {
|
|
10245
10427
|
const res = await this.request(`/api/v1/skills/${slug}/skill.md`);
|
|
@@ -10261,39 +10443,217 @@ class RemoteSkillsClient {
|
|
|
10261
10443
|
} catch {}
|
|
10262
10444
|
return { status: res.status, body };
|
|
10263
10445
|
}
|
|
10264
|
-
async submitRun(slug, input, args) {
|
|
10265
|
-
|
|
10446
|
+
async submitRun(slug, input, args, approval = {}) {
|
|
10447
|
+
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
10448
|
+
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
10449
|
+
if (approval.maxCostCents !== undefined)
|
|
10450
|
+
creditCount(approval.maxCostCents);
|
|
10451
|
+
if (approval.maxCredits !== undefined)
|
|
10452
|
+
creditCount(approval.maxCredits);
|
|
10453
|
+
if (approval.maxCredits !== undefined && approval.maxCostCents !== undefined && approval.maxCredits !== approval.maxCostCents)
|
|
10454
|
+
throw new Error("Credit approval fields disagree");
|
|
10455
|
+
const res = await this.request(`/api/v1/runs/${encodeURIComponent(slug)}`, {
|
|
10266
10456
|
method: "POST",
|
|
10267
|
-
body: JSON.stringify({
|
|
10457
|
+
body: JSON.stringify({
|
|
10458
|
+
input,
|
|
10459
|
+
args,
|
|
10460
|
+
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
10461
|
+
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
10462
|
+
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
10463
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
10464
|
+
})
|
|
10268
10465
|
});
|
|
10269
10466
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
10270
10467
|
}
|
|
10468
|
+
async quoteRun(slug, input = {}, args = []) {
|
|
10469
|
+
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
10470
|
+
method: "POST",
|
|
10471
|
+
body: JSON.stringify({ input, args })
|
|
10472
|
+
});
|
|
10473
|
+
return parseRemoteRunQuote(await response.json());
|
|
10474
|
+
}
|
|
10475
|
+
getCapabilities() {
|
|
10476
|
+
if (!this.capabilities)
|
|
10477
|
+
this.capabilities = (async () => {
|
|
10478
|
+
const value = await (await this.requestNewRoute("/api/v1/capabilities")).json();
|
|
10479
|
+
if (value.contractVersion !== 1 || value.apiVersion !== 1 || !Array.isArray(value.capabilities) || value.capabilities.some((item) => typeof item !== "string"))
|
|
10480
|
+
throw new Error("Unsupported Skills server capability contract");
|
|
10481
|
+
const billing = value.billing;
|
|
10482
|
+
return { contractVersion: 1, apiVersion: 1, capabilities: value.capabilities, ...billing ? { billing } : {} };
|
|
10483
|
+
})();
|
|
10484
|
+
return this.capabilities;
|
|
10485
|
+
}
|
|
10486
|
+
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
10487
|
+
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
10488
|
+
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
10489
|
+
throw new Error("Credit approval fields disagree");
|
|
10490
|
+
const quote = await this.quoteRun(slug, input, args);
|
|
10491
|
+
if (quote.pricing.costCents > maximum)
|
|
10492
|
+
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
10493
|
+
const capabilities = await this.getCapabilities();
|
|
10494
|
+
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
10495
|
+
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
10496
|
+
}
|
|
10497
|
+
return this.submitRun(quote.skill, input, args, { ...approval, maxCredits: maximum, maxCostCents: maximum });
|
|
10498
|
+
}
|
|
10499
|
+
async getIdentity() {
|
|
10500
|
+
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
10501
|
+
}
|
|
10502
|
+
async listApiKeys() {
|
|
10503
|
+
return this.arrayResponse("/api/auth/keys");
|
|
10504
|
+
}
|
|
10505
|
+
async createApiKey(name, scopes) {
|
|
10506
|
+
if (!name.trim() || name.length > 100)
|
|
10507
|
+
throw new Error("API key name must be 1-100 characters");
|
|
10508
|
+
const value = await (await this.requestNewRoute("/api/auth/keys", { method: "POST", body: JSON.stringify({ name, ...scopes ? { scopes } : {} }) })).json();
|
|
10509
|
+
if (!value || typeof value.key !== "string" || !value.key.trim())
|
|
10510
|
+
throw new Error("The server did not return a created API key");
|
|
10511
|
+
return value;
|
|
10512
|
+
}
|
|
10513
|
+
async revokeApiKey(keyId) {
|
|
10514
|
+
return (await this.requestNewRoute(`/api/auth/keys/${encodeURIComponent(keyId)}`, { method: "DELETE" })).json();
|
|
10515
|
+
}
|
|
10516
|
+
async getBillingStatus() {
|
|
10517
|
+
return parseRemoteBillingStatus(await (await this.requestNewRoute("/api/v1/billing/status")).json());
|
|
10518
|
+
}
|
|
10519
|
+
async listCreditPacks() {
|
|
10520
|
+
return parseRemoteCreditPacks(await (await this.requestNewRoute("/api/v1/billing/credits")).json());
|
|
10521
|
+
}
|
|
10522
|
+
async createCreditCheckout(packId) {
|
|
10523
|
+
const packs = await this.listCreditPacks();
|
|
10524
|
+
if (!packs.some((pack) => pack.id === packId))
|
|
10525
|
+
throw new Error("Choose a credit pack returned by skills credits packs");
|
|
10526
|
+
return parseRemoteCheckout(await (await this.requestNewRoute("/api/v1/billing/credits", {
|
|
10527
|
+
method: "POST",
|
|
10528
|
+
body: JSON.stringify({ packId })
|
|
10529
|
+
})).json());
|
|
10530
|
+
}
|
|
10531
|
+
async getUsage() {
|
|
10532
|
+
return this.arrayResponse("/api/v1/billing/usage");
|
|
10533
|
+
}
|
|
10534
|
+
async listInvoices() {
|
|
10535
|
+
return this.arrayResponse("/api/v1/billing/invoices");
|
|
10536
|
+
}
|
|
10537
|
+
async createBillingCheckout() {
|
|
10538
|
+
return this.checkoutResponse("/api/v1/billing/checkout");
|
|
10539
|
+
}
|
|
10540
|
+
async createBillingPortal() {
|
|
10541
|
+
return this.checkoutResponse("/api/v1/billing/portal");
|
|
10542
|
+
}
|
|
10543
|
+
async cancelRun(runId) {
|
|
10544
|
+
return this.controlRun(runId, "cancel");
|
|
10545
|
+
}
|
|
10546
|
+
async resumeRun(runId) {
|
|
10547
|
+
return this.controlRun(runId, "resume");
|
|
10548
|
+
}
|
|
10549
|
+
async controlRun(runId, action) {
|
|
10550
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/${action}`, { method: "POST", body: "{}" });
|
|
10551
|
+
return normalizeRemoteSkillRunContract(await response.json());
|
|
10552
|
+
}
|
|
10553
|
+
async checkoutResponse(path) {
|
|
10554
|
+
return parseRemoteCheckout(await (await this.requestNewRoute(path, { method: "POST", body: "{}" })).json());
|
|
10555
|
+
}
|
|
10556
|
+
async arrayResponse(path) {
|
|
10557
|
+
const rows = await (await this.requestNewRoute(path)).json();
|
|
10558
|
+
if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
|
|
10559
|
+
throw new Error("Invalid Skills server list response");
|
|
10560
|
+
return rows;
|
|
10561
|
+
}
|
|
10271
10562
|
async getRun(runId) {
|
|
10272
|
-
const
|
|
10273
|
-
|
|
10563
|
+
const path = `/api/v1/runs/${encodeURIComponent(runId)}`;
|
|
10564
|
+
const res = await this.request(path);
|
|
10565
|
+
if (res.status === 404)
|
|
10274
10566
|
return null;
|
|
10567
|
+
if (!res.ok)
|
|
10568
|
+
throw new RemoteRequestError(path, res.status, res.statusText);
|
|
10275
10569
|
return normalizeRemoteSkillRunContract(await res.json());
|
|
10276
10570
|
}
|
|
10277
10571
|
async getRunLogs(runId) {
|
|
10278
|
-
|
|
10279
|
-
if (!res.ok)
|
|
10280
|
-
return [];
|
|
10281
|
-
const payload = await res.json();
|
|
10282
|
-
return Array.isArray(payload) ? payload : [];
|
|
10572
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/logs`);
|
|
10283
10573
|
}
|
|
10284
10574
|
async listRuns(limit = 20) {
|
|
10285
|
-
|
|
10286
|
-
|
|
10575
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
10576
|
+
throw new Error("Run limit must be an integer from 1 to 100");
|
|
10577
|
+
return this.arrayResponse(`/api/v1/runs?limit=${limit}`);
|
|
10287
10578
|
}
|
|
10288
10579
|
async getRunArtifacts(runId) {
|
|
10289
|
-
|
|
10290
|
-
return res.json();
|
|
10580
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts`);
|
|
10291
10581
|
}
|
|
10292
10582
|
async downloadRunArtifact(runId, artifactId) {
|
|
10293
|
-
return this.request(`/api/v1/runs/${runId}/artifacts/${artifactId}/download`, {
|
|
10583
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
10294
10584
|
method: "GET"
|
|
10295
10585
|
});
|
|
10296
10586
|
}
|
|
10587
|
+
async getVerifiedRunArtifact(runId, artifactId, maximumBytes = MAX_REMOTE_FILE_BYTES) {
|
|
10588
|
+
const artifacts = await this.getRunArtifacts(runId);
|
|
10589
|
+
const artifact = artifacts.find((row) => row.id === artifactId);
|
|
10590
|
+
if (!artifact)
|
|
10591
|
+
throw new Error("Run artifact not found");
|
|
10592
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0 || artifact.byteSize > maximumBytes || typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256))
|
|
10593
|
+
throw new Error("The server does not provide valid artifact integrity metadata");
|
|
10594
|
+
const response = await this.downloadRunArtifact(runId, artifactId);
|
|
10595
|
+
if (!response.ok)
|
|
10596
|
+
throw new RemoteRequestError("artifact download", response.status, response.statusText);
|
|
10597
|
+
const bytes = await readBoundedResponse(response, artifact.byteSize);
|
|
10598
|
+
if (bytes.byteLength !== artifact.byteSize || sha256(bytes) !== artifact.sha256)
|
|
10599
|
+
throw new Error("Artifact integrity verification failed");
|
|
10600
|
+
return { id: artifactId, fileName: String(artifact.fileName ?? artifactId), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
10601
|
+
}
|
|
10602
|
+
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
10603
|
+
const inputFiles = describeRemoteFiles(files);
|
|
10604
|
+
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
10605
|
+
throw new Error("The configured server does not support input uploads");
|
|
10606
|
+
const run = await this.submitQuotedRun(slug, input, args, { ...approval, inputFiles });
|
|
10607
|
+
if (run.error || !run.id || !files.length)
|
|
10608
|
+
return run;
|
|
10609
|
+
const pastUploads = (status) => typeof status === "string" && [
|
|
10610
|
+
"running",
|
|
10611
|
+
"completed",
|
|
10612
|
+
"failed",
|
|
10613
|
+
"cancelled",
|
|
10614
|
+
"expired",
|
|
10615
|
+
"pending_approval",
|
|
10616
|
+
"approved",
|
|
10617
|
+
"waiting"
|
|
10618
|
+
].includes(status);
|
|
10619
|
+
if (pastUploads(run.status))
|
|
10620
|
+
return run;
|
|
10621
|
+
try {
|
|
10622
|
+
await this.uploadRunFiles(run.id, files);
|
|
10623
|
+
} catch {
|
|
10624
|
+
try {
|
|
10625
|
+
const current = await this.getRun(run.id);
|
|
10626
|
+
if (current && pastUploads(current.status))
|
|
10627
|
+
return current;
|
|
10628
|
+
} catch {}
|
|
10629
|
+
let cancellationRequested = false;
|
|
10630
|
+
try {
|
|
10631
|
+
await this.cancelRun(run.id);
|
|
10632
|
+
cancellationRequested = true;
|
|
10633
|
+
} catch {}
|
|
10634
|
+
throw new Error(`Input upload failed for run ${run.id}; ${cancellationRequested ? "cancellation requested" : "check its status and cancel the run"}`);
|
|
10635
|
+
}
|
|
10636
|
+
return run;
|
|
10637
|
+
}
|
|
10638
|
+
async uploadRunFiles(runId, files) {
|
|
10639
|
+
const descriptors = describeRemoteFiles(files);
|
|
10640
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/uploads`, { method: "POST", body: JSON.stringify({ files: descriptors }) });
|
|
10641
|
+
const payload = await response.json();
|
|
10642
|
+
if (!Array.isArray(payload.files) || payload.files.length !== files.length || new Set(payload.files.map((file) => file.name)).size !== files.length)
|
|
10643
|
+
throw new Error("Invalid input upload response");
|
|
10644
|
+
for (const file of files) {
|
|
10645
|
+
const upload = payload.files.find((row) => row.name === file.name);
|
|
10646
|
+
if (!upload)
|
|
10647
|
+
throw new Error("Missing input upload URL");
|
|
10648
|
+
const url = new URL(upload.uploadUrl);
|
|
10649
|
+
if (url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
10650
|
+
throw new Error("Unsafe input upload URL");
|
|
10651
|
+
const uploaded = await fetch(url, { method: "PUT", body: file.bytes, headers: { "Content-Type": file.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
|
|
10652
|
+
if (!uploaded.ok)
|
|
10653
|
+
throw new Error("Input upload failed");
|
|
10654
|
+
await uploaded.body?.cancel();
|
|
10655
|
+
}
|
|
10656
|
+
}
|
|
10297
10657
|
async publishSkill(manifest, bundle, ifMatch) {
|
|
10298
10658
|
const form = new FormData;
|
|
10299
10659
|
form.set("manifest", JSON.stringify(manifest));
|
|
@@ -10306,7 +10666,9 @@ class RemoteSkillsClient {
|
|
|
10306
10666
|
return fetch(`${this.apiUrl}/api/v1/skills`, {
|
|
10307
10667
|
method: "POST",
|
|
10308
10668
|
headers,
|
|
10309
|
-
body: form
|
|
10669
|
+
body: form,
|
|
10670
|
+
redirect: "error",
|
|
10671
|
+
signal: AbortSignal.timeout(15000)
|
|
10310
10672
|
});
|
|
10311
10673
|
}
|
|
10312
10674
|
async deleteSkill(slug) {
|
|
@@ -10362,12 +10724,13 @@ class RemoteSkillsClient {
|
|
|
10362
10724
|
if (!Array.isArray(payload)) {
|
|
10363
10725
|
throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
|
|
10364
10726
|
}
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10727
|
+
const isName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
10728
|
+
if (payload.every(isName))
|
|
10729
|
+
return payload;
|
|
10730
|
+
if (payload.every((tag) => tag !== null && typeof tag === "object" && !Array.isArray(tag) && isName(tag.name) && Number.isSafeInteger(tag.count) && tag.count >= 0)) {
|
|
10731
|
+
return payload.map((tag) => tag.name);
|
|
10369
10732
|
}
|
|
10370
|
-
|
|
10733
|
+
throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name, or every element must be a counted tag record)");
|
|
10371
10734
|
}
|
|
10372
10735
|
async skillsByTag(tag) {
|
|
10373
10736
|
const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
|
|
@@ -10470,14 +10833,8 @@ function normalizeUpdatedSincePage(payload) {
|
|
|
10470
10833
|
return { skills, nextCursor };
|
|
10471
10834
|
}
|
|
10472
10835
|
async function createRemoteSkillsClient(env = process.env) {
|
|
10473
|
-
const
|
|
10474
|
-
|
|
10475
|
-
return null;
|
|
10476
|
-
const apiKey = await resolveSkillsApiKey(env);
|
|
10477
|
-
if (!apiKey) {
|
|
10478
|
-
throw new Error("A Skills authority resolved but no API key did. Sign in with: skills auth login");
|
|
10479
|
-
}
|
|
10480
|
-
return new RemoteSkillsClient(apiKey, fleet.apiOrigin);
|
|
10836
|
+
const connection = await resolveSkillsConnection(env);
|
|
10837
|
+
return connection ? new RemoteSkillsClient(connection.apiKey, connection.apiOrigin) : null;
|
|
10481
10838
|
}
|
|
10482
10839
|
// src/lib/scheduler.ts
|
|
10483
10840
|
import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
@@ -10688,7 +11045,7 @@ import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkd
|
|
|
10688
11045
|
import { dirname as dirname6, join as join17 } from "path";
|
|
10689
11046
|
|
|
10690
11047
|
// src/lib/revision.ts
|
|
10691
|
-
import { createHash as
|
|
11048
|
+
import { createHash as createHash4 } from "crypto";
|
|
10692
11049
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
10693
11050
|
function revisionIdOf(content) {
|
|
10694
11051
|
const canonical = JSON.stringify({
|
|
@@ -10704,14 +11061,14 @@ function revisionIdOf(content) {
|
|
|
10704
11061
|
bundleSha256: content.bundleSha256 ?? null,
|
|
10705
11062
|
bundleByteSize: content.bundleByteSize ?? null
|
|
10706
11063
|
});
|
|
10707
|
-
return
|
|
11064
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
10708
11065
|
}
|
|
10709
11066
|
function revisionIdOfRecord(record) {
|
|
10710
11067
|
return revisionIdOf(record);
|
|
10711
11068
|
}
|
|
10712
11069
|
|
|
10713
11070
|
// src/lib/skill-bundle.ts
|
|
10714
|
-
import { createHash as
|
|
11071
|
+
import { createHash as createHash5 } from "crypto";
|
|
10715
11072
|
import { readFileSync as readFileSync14, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
|
|
10716
11073
|
import { join as join16, relative as relative3 } from "path";
|
|
10717
11074
|
var BLOCK = 512;
|
|
@@ -10865,7 +11222,7 @@ function ownBytes(view) {
|
|
|
10865
11222
|
return out;
|
|
10866
11223
|
}
|
|
10867
11224
|
function sha256Hex(bytes) {
|
|
10868
|
-
return
|
|
11225
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
10869
11226
|
}
|
|
10870
11227
|
function collectSkillBundleEntries(dir) {
|
|
10871
11228
|
const entries = [];
|
|
@@ -11586,7 +11943,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
|
|
|
11586
11943
|
// package.json
|
|
11587
11944
|
var package_default = {
|
|
11588
11945
|
name: "@hasna/skills",
|
|
11589
|
-
version: "0.
|
|
11946
|
+
version: "0.4.0",
|
|
11590
11947
|
description: "Skills library for AI coding agents",
|
|
11591
11948
|
type: "module",
|
|
11592
11949
|
bin: {
|
|
@@ -11645,8 +12002,7 @@ var package_default = {
|
|
|
11645
12002
|
"verify:release": "bun run scripts/release-guard.ts",
|
|
11646
12003
|
prepare: "bun run build:js",
|
|
11647
12004
|
prepack: "bun run build && bun run verify:release",
|
|
11648
|
-
prepublishOnly: "bun run typecheck && bun run test"
|
|
11649
|
-
postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
|
|
12005
|
+
prepublishOnly: "bun run typecheck && bun run test"
|
|
11650
12006
|
},
|
|
11651
12007
|
keywords: [
|
|
11652
12008
|
"skills",
|
|
@@ -11775,6 +12131,24 @@ function buildDocs(name) {
|
|
|
11775
12131
|
best: docs.skillMd ?? docs.readme ?? docs.claudeMd ?? null
|
|
11776
12132
|
};
|
|
11777
12133
|
}
|
|
12134
|
+
// src/lib/remote-customer-operations.ts
|
|
12135
|
+
var REMOTE_CUSTOMER_OPERATIONS = [
|
|
12136
|
+
{ name: "get_account", title: "Get Account Identity", parameter: null, mutates: false, invoke: (client) => client.getIdentity() },
|
|
12137
|
+
{ name: "get_server_capabilities", title: "Get Server Capabilities", parameter: null, mutates: false, invoke: (client) => client.getCapabilities() },
|
|
12138
|
+
{ name: "list_remote_skills", title: "List Remote Skills", parameter: null, mutates: false, invoke: (client) => client.listSkills() },
|
|
12139
|
+
{ name: "get_billing_status", title: "Get Billing Status", parameter: null, mutates: false, invoke: (client) => client.getBillingStatus() },
|
|
12140
|
+
{ name: "list_credit_packs", title: "List Credit Packs", parameter: null, mutates: false, invoke: (client) => client.listCreditPacks() },
|
|
12141
|
+
{ name: "create_credit_checkout", title: "Create Credit Checkout", parameter: "pack_id", mutates: true, invoke: (client, value) => client.createCreditCheckout(value) },
|
|
12142
|
+
{ name: "get_billing_usage", title: "Get Billing Usage", parameter: null, mutates: false, invoke: (client) => client.getUsage() },
|
|
12143
|
+
{ name: "list_invoices", title: "List Invoices", parameter: null, mutates: false, invoke: (client) => client.listInvoices() },
|
|
12144
|
+
{ name: "create_billing_checkout", title: "Create Billing Checkout", parameter: null, mutates: true, invoke: (client) => client.createBillingCheckout() },
|
|
12145
|
+
{ name: "create_billing_portal", title: "Create Billing Portal", parameter: null, mutates: true, invoke: (client) => client.createBillingPortal() },
|
|
12146
|
+
{ name: "get_run_logs", title: "Get Run Logs", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunLogs(value) },
|
|
12147
|
+
{ name: "cancel_run", title: "Cancel Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.cancelRun(value) },
|
|
12148
|
+
{ name: "resume_run", title: "Resume Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.resumeRun(value) },
|
|
12149
|
+
{ name: "list_run_artifacts", title: "List Run Artifacts", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunArtifacts(value) }
|
|
12150
|
+
];
|
|
12151
|
+
|
|
11778
12152
|
// src/lib/mcp-contracts.ts
|
|
11779
12153
|
var MCP_CONTRACT_SCHEMA_VERSION = 1;
|
|
11780
12154
|
var stringSchema = (description) => ({
|
|
@@ -12166,11 +12540,44 @@ var toolContracts = [
|
|
|
12166
12540
|
dependencies: objectSchema({}, [], "Package dependencies.", true)
|
|
12167
12541
|
})
|
|
12168
12542
|
},
|
|
12543
|
+
{
|
|
12544
|
+
name: "list_api_keys",
|
|
12545
|
+
title: "List API Keys",
|
|
12546
|
+
description: "List keys using fresh email OTP reauthentication.",
|
|
12547
|
+
params: ["email", "code"],
|
|
12548
|
+
category: "execution",
|
|
12549
|
+
sideEffects: "local-process-or-remote-run",
|
|
12550
|
+
stable: true,
|
|
12551
|
+
inputSchema: objectSchema({ email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["email", "code"]),
|
|
12552
|
+
outputSchema: arraySchema(objectSchema({}, [], "API key metadata", true))
|
|
12553
|
+
},
|
|
12554
|
+
{
|
|
12555
|
+
name: "revoke_api_key",
|
|
12556
|
+
title: "Revoke API Key",
|
|
12557
|
+
description: "Revoke a key using fresh email OTP reauthentication.",
|
|
12558
|
+
params: ["key_id", "email", "code"],
|
|
12559
|
+
category: "execution",
|
|
12560
|
+
sideEffects: "local-process-or-remote-run",
|
|
12561
|
+
stable: true,
|
|
12562
|
+
inputSchema: objectSchema({ key_id: stringSchema("API key ID"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["key_id", "email", "code"]),
|
|
12563
|
+
outputSchema: objectSchema({}, [], "Revocation result", true)
|
|
12564
|
+
},
|
|
12565
|
+
{
|
|
12566
|
+
name: "create_api_key",
|
|
12567
|
+
title: "Create API Key",
|
|
12568
|
+
description: "Create a key with fresh email OTP reauthentication; returns the secret once.",
|
|
12569
|
+
params: ["name", "email", "code", "scopes?"],
|
|
12570
|
+
category: "execution",
|
|
12571
|
+
sideEffects: "local-process-or-remote-run",
|
|
12572
|
+
stable: true,
|
|
12573
|
+
inputSchema: objectSchema({ name: stringSchema("Key name"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" }, scopes: arraySchema(stringSchema("Scope")) }, ["name", "email", "code"]),
|
|
12574
|
+
outputSchema: objectSchema({}, [], "Created key and one-time secret", true)
|
|
12575
|
+
},
|
|
12169
12576
|
{
|
|
12170
12577
|
name: "run_skill",
|
|
12171
12578
|
title: "Run Skill",
|
|
12172
12579
|
description: "Run a skill locally or through a configured remote runner. Returns compact stdout/stderr previews and run summaries by default; pass detail:true for full records.",
|
|
12173
|
-
params: ["name", "input?", "args?", "detail?"],
|
|
12580
|
+
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "idempotency_key?", "files?"],
|
|
12174
12581
|
category: "execution",
|
|
12175
12582
|
sideEffects: "local-process-or-remote-run",
|
|
12176
12583
|
stable: true,
|
|
@@ -12178,7 +12585,12 @@ var toolContracts = [
|
|
|
12178
12585
|
name: skillNameInput,
|
|
12179
12586
|
input: runInputSchema,
|
|
12180
12587
|
args: runArgsSchema,
|
|
12181
|
-
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." }
|
|
12588
|
+
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." },
|
|
12589
|
+
remote: { type: "boolean", description: "Use the configured server catalog." },
|
|
12590
|
+
maxCredits: { type: "integer", minimum: 0, description: "Maximum explicitly approved integer credits; omitted permits only free remote runs." },
|
|
12591
|
+
maxCostCents: { type: "integer", minimum: 0, description: "Legacy alias for maxCredits; both must agree." },
|
|
12592
|
+
idempotency_key: { type: "string", pattern: "^[A-Za-z0-9._:-]{1,128}$", description: "Stable retry key for the same remote submission." },
|
|
12593
|
+
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Inline remote inputs, at most 1 MiB combined." }
|
|
12182
12594
|
}, ["name"]),
|
|
12183
12595
|
outputSchema: runOutputSchema
|
|
12184
12596
|
},
|
|
@@ -12445,7 +12857,40 @@ var toolContracts = [
|
|
|
12445
12857
|
outputSchema: objectSchema({}, [], "Feedback save result.", true)
|
|
12446
12858
|
}
|
|
12447
12859
|
];
|
|
12448
|
-
var
|
|
12860
|
+
var remoteCustomerContracts = REMOTE_CUSTOMER_OPERATIONS.map((operation) => ({
|
|
12861
|
+
name: operation.name,
|
|
12862
|
+
title: operation.title,
|
|
12863
|
+
description: `${operation.title} on the configured server; unavailable capabilities fail explicitly.`,
|
|
12864
|
+
params: operation.parameter ? [operation.parameter] : [],
|
|
12865
|
+
category: "execution",
|
|
12866
|
+
sideEffects: operation.mutates ? "local-process-or-remote-run" : "none",
|
|
12867
|
+
stable: true,
|
|
12868
|
+
inputSchema: objectSchema(operation.parameter ? { [operation.parameter]: stringSchema("Server resource identifier.") } : {}, operation.parameter ? [operation.parameter] : []),
|
|
12869
|
+
outputSchema: { oneOf: [objectSchema({}, [], "Server response.", true), { type: "array", items: objectSchema({}, [], "Server record.", true) }] }
|
|
12870
|
+
}));
|
|
12871
|
+
remoteCustomerContracts.push({
|
|
12872
|
+
name: "quote_skill",
|
|
12873
|
+
title: "Quote Remote Skill",
|
|
12874
|
+
description: "Get a server credit quote without submitting a run.",
|
|
12875
|
+
params: ["name", "input?", "args?"],
|
|
12876
|
+
category: "execution",
|
|
12877
|
+
sideEffects: "none",
|
|
12878
|
+
stable: true,
|
|
12879
|
+
inputSchema: objectSchema({ name: skillNameInput, input: runInputSchema, args: runArgsSchema }, ["name"]),
|
|
12880
|
+
outputSchema: objectSchema({ skill: stringSchema("Canonical server skill."), pricing: objectSchema({}, [], "Quoted integer credits.", true) }, ["skill", "pricing"], undefined, true)
|
|
12881
|
+
});
|
|
12882
|
+
remoteCustomerContracts.push({
|
|
12883
|
+
name: "download_run_artifact",
|
|
12884
|
+
title: "Download Verified Run Artifact",
|
|
12885
|
+
description: "Return verified artifact bytes as base64, bounded to 1 MiB.",
|
|
12886
|
+
params: ["run_id", "artifact_id"],
|
|
12887
|
+
category: "execution",
|
|
12888
|
+
sideEffects: "none",
|
|
12889
|
+
stable: true,
|
|
12890
|
+
inputSchema: objectSchema({ run_id: stringSchema("Run identifier."), artifact_id: stringSchema("Artifact identifier.") }, ["run_id", "artifact_id"]),
|
|
12891
|
+
outputSchema: objectSchema({ id: stringSchema("Artifact identifier."), fileName: stringSchema("Artifact file name."), base64: stringSchema("Verified bytes."), sha256: stringSchema("SHA256 digest."), byteSize: { type: "integer", minimum: 0 } }, ["id", "fileName", "base64", "sha256", "byteSize"])
|
|
12892
|
+
});
|
|
12893
|
+
var contracts = [...toolContracts, ...remoteCustomerContracts].sort((a, b) => a.name.localeCompare(b.name));
|
|
12449
12894
|
var resourceContracts = [
|
|
12450
12895
|
{
|
|
12451
12896
|
uri: "skills://mcp/contracts",
|
|
@@ -12664,7 +13109,7 @@ function isApiMode(env = process.env) {
|
|
|
12664
13109
|
}
|
|
12665
13110
|
}
|
|
12666
13111
|
// src/lib/native-storage.ts
|
|
12667
|
-
import { createHash as
|
|
13112
|
+
import { createHash as createHash6, createHmac as createHmac2 } from "crypto";
|
|
12668
13113
|
import {
|
|
12669
13114
|
existsSync as existsSync17,
|
|
12670
13115
|
mkdirSync as mkdirSync11,
|
|
@@ -12840,7 +13285,7 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
|
12840
13285
|
files.push({
|
|
12841
13286
|
path: relativePath,
|
|
12842
13287
|
sizeBytes: bytes.byteLength,
|
|
12843
|
-
sha256:
|
|
13288
|
+
sha256: createHash6("sha256").update(bytes).digest("hex"),
|
|
12844
13289
|
...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
|
|
12845
13290
|
});
|
|
12846
13291
|
}
|
|
@@ -12865,7 +13310,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
12865
13310
|
continue;
|
|
12866
13311
|
}
|
|
12867
13312
|
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
12868
|
-
const hash =
|
|
13313
|
+
const hash = createHash6("sha256").update(bytes).digest("hex");
|
|
12869
13314
|
if (hash !== file.sha256) {
|
|
12870
13315
|
throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
|
|
12871
13316
|
}
|
|
@@ -13197,7 +13642,7 @@ function toIsoString(value) {
|
|
|
13197
13642
|
return Number.isNaN(date.getTime()) ? value : date.toISOString();
|
|
13198
13643
|
}
|
|
13199
13644
|
function sha256Hex2(value) {
|
|
13200
|
-
return
|
|
13645
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
13201
13646
|
}
|
|
13202
13647
|
function normalizeHeaders(headers) {
|
|
13203
13648
|
const result = {};
|
|
@@ -13245,7 +13690,7 @@ function toArrayBuffer(bytes) {
|
|
|
13245
13690
|
return buffer;
|
|
13246
13691
|
}
|
|
13247
13692
|
// src/lib/station-snapshot.ts
|
|
13248
|
-
import { createHash as
|
|
13693
|
+
import { createHash as createHash7 } from "crypto";
|
|
13249
13694
|
import {
|
|
13250
13695
|
copyFileSync as copyFileSync2,
|
|
13251
13696
|
mkdirSync as mkdirSync12,
|
|
@@ -13420,7 +13865,7 @@ function validateStationId(stationId) {
|
|
|
13420
13865
|
}
|
|
13421
13866
|
}
|
|
13422
13867
|
function sha256File(filePath) {
|
|
13423
|
-
return
|
|
13868
|
+
return createHash7("sha256").update(readFileSync17(filePath)).digest("hex");
|
|
13424
13869
|
}
|
|
13425
13870
|
function scanHome(definition, homesRoot) {
|
|
13426
13871
|
const homePath = homePathFor(definition, homesRoot);
|
|
@@ -13545,7 +13990,7 @@ function writeStationSnapshot(options) {
|
|
|
13545
13990
|
copyFileSync2(plan.source.fullPath, destination);
|
|
13546
13991
|
written += 1;
|
|
13547
13992
|
}
|
|
13548
|
-
const
|
|
13993
|
+
const unchanged2 = plans.length - untouched.length;
|
|
13549
13994
|
const manifest = {
|
|
13550
13995
|
schema: STATION_SYNC_MANIFEST_SCHEMA,
|
|
13551
13996
|
stationId: options.stationId,
|
|
@@ -13553,7 +13998,7 @@ function writeStationSnapshot(options) {
|
|
|
13553
13998
|
producer: STATION_SNAPSHOT_PRODUCER,
|
|
13554
13999
|
stats: {
|
|
13555
14000
|
written,
|
|
13556
|
-
unchanged,
|
|
14001
|
+
unchanged: unchanged2,
|
|
13557
14002
|
files: plans.length,
|
|
13558
14003
|
bytes: totalBytes
|
|
13559
14004
|
},
|
|
@@ -13566,12 +14011,12 @@ function writeStationSnapshot(options) {
|
|
|
13566
14011
|
return {
|
|
13567
14012
|
...base,
|
|
13568
14013
|
mode: "populate",
|
|
13569
|
-
stats: { files: plans.length, bytes: totalBytes, written, unchanged },
|
|
14014
|
+
stats: { files: plans.length, bytes: totalBytes, written, unchanged: unchanged2 },
|
|
13570
14015
|
manifestPath
|
|
13571
14016
|
};
|
|
13572
14017
|
}
|
|
13573
14018
|
// src/lib/station-hydrate.ts
|
|
13574
|
-
import { createHash as
|
|
14019
|
+
import { createHash as createHash8 } from "crypto";
|
|
13575
14020
|
import {
|
|
13576
14021
|
copyFileSync as copyFileSync3,
|
|
13577
14022
|
mkdirSync as mkdirSync13,
|
|
@@ -13777,7 +14222,7 @@ function skillSha256(skill) {
|
|
|
13777
14222
|
return sha256File(skill.files[0].winner.fullPath);
|
|
13778
14223
|
}
|
|
13779
14224
|
const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
|
|
13780
|
-
return
|
|
14225
|
+
return createHash8("sha256").update(joined.sort().join(`
|
|
13781
14226
|
`)).digest("hex");
|
|
13782
14227
|
}
|
|
13783
14228
|
function writeStationHydration(options) {
|
|
@@ -13840,7 +14285,7 @@ function writeStationHydration(options) {
|
|
|
13840
14285
|
copyFileSync3(entry.fullPath, entry.destination);
|
|
13841
14286
|
written += 1;
|
|
13842
14287
|
}
|
|
13843
|
-
const
|
|
14288
|
+
const unchanged2 = plan.totalFiles - written;
|
|
13844
14289
|
const hydration = {
|
|
13845
14290
|
schema: STATION_HYDRATION_MANIFEST_SCHEMA,
|
|
13846
14291
|
stationId: options.stationId,
|
|
@@ -13851,7 +14296,7 @@ function writeStationHydration(options) {
|
|
|
13851
14296
|
stats: {
|
|
13852
14297
|
idents: plan.winners.length,
|
|
13853
14298
|
written,
|
|
13854
|
-
unchanged,
|
|
14299
|
+
unchanged: unchanged2,
|
|
13855
14300
|
files: plan.totalFiles,
|
|
13856
14301
|
bytes: plan.totalBytes
|
|
13857
14302
|
},
|
|
@@ -13864,10 +14309,122 @@ function writeStationHydration(options) {
|
|
|
13864
14309
|
return {
|
|
13865
14310
|
...base,
|
|
13866
14311
|
mode: "apply",
|
|
13867
|
-
stats: { ...base.stats, written, unchanged },
|
|
14312
|
+
stats: { ...base.stats, written, unchanged: unchanged2 },
|
|
13868
14313
|
manifestPath: hydrationManifestPath
|
|
13869
14314
|
};
|
|
13870
14315
|
}
|
|
14316
|
+
// src/lib/remote-auth.ts
|
|
14317
|
+
var MAX_ERROR_DETAIL_LENGTH = 200;
|
|
14318
|
+
|
|
14319
|
+
class HostedApiError extends Error {
|
|
14320
|
+
status;
|
|
14321
|
+
code;
|
|
14322
|
+
detail;
|
|
14323
|
+
endpoint;
|
|
14324
|
+
apiUrl;
|
|
14325
|
+
constructor(message, options = {}) {
|
|
14326
|
+
super(message);
|
|
14327
|
+
this.name = "HostedApiError";
|
|
14328
|
+
this.status = options.status;
|
|
14329
|
+
this.code = options.code;
|
|
14330
|
+
this.detail = options.detail;
|
|
14331
|
+
this.endpoint = options.endpoint;
|
|
14332
|
+
this.apiUrl = options.apiUrl;
|
|
14333
|
+
}
|
|
14334
|
+
}
|
|
14335
|
+
async function requestAuthApi(instance, path, options) {
|
|
14336
|
+
const url = normalizeSkillsApiOrigin(instance);
|
|
14337
|
+
const safeUrl = url;
|
|
14338
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${safeUrl}${path}`;
|
|
14339
|
+
let res;
|
|
14340
|
+
try {
|
|
14341
|
+
res = await fetch(`${url}${path}`, {
|
|
14342
|
+
...options,
|
|
14343
|
+
redirect: "error",
|
|
14344
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
14345
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
14346
|
+
});
|
|
14347
|
+
} catch (err) {
|
|
14348
|
+
throw new HostedApiError(`Unable to reach the Skills API: ${err.message}`, {
|
|
14349
|
+
endpoint,
|
|
14350
|
+
apiUrl: safeUrl
|
|
14351
|
+
});
|
|
14352
|
+
}
|
|
14353
|
+
const text = await res.text();
|
|
14354
|
+
const body = text ? parseJsonBody(text) : {};
|
|
14355
|
+
if (!res.ok) {
|
|
14356
|
+
const record = isRecord4(body) ? body : {};
|
|
14357
|
+
const detail = typeof record.detail === "string" ? record.detail : undefined;
|
|
14358
|
+
const error = typeof record.error === "string" ? record.error : undefined;
|
|
14359
|
+
const code = typeof record.code === "string" ? record.code : undefined;
|
|
14360
|
+
throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
|
|
14361
|
+
status: res.status,
|
|
14362
|
+
code,
|
|
14363
|
+
detail,
|
|
14364
|
+
endpoint,
|
|
14365
|
+
apiUrl: safeUrl
|
|
14366
|
+
});
|
|
14367
|
+
}
|
|
14368
|
+
return body;
|
|
14369
|
+
}
|
|
14370
|
+
function parseJsonBody(text) {
|
|
14371
|
+
try {
|
|
14372
|
+
return JSON.parse(text);
|
|
14373
|
+
} catch {
|
|
14374
|
+
return { detail: condenseErrorBody(text) };
|
|
14375
|
+
}
|
|
14376
|
+
}
|
|
14377
|
+
function condenseErrorBody(text) {
|
|
14378
|
+
const stripped = /<[a-z!/]/i.test(text) ? text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ") : text;
|
|
14379
|
+
const collapsed = stripped.replace(/\s+/g, " ").trim();
|
|
14380
|
+
if (collapsed.length <= MAX_ERROR_DETAIL_LENGTH)
|
|
14381
|
+
return collapsed;
|
|
14382
|
+
return `${collapsed.slice(0, MAX_ERROR_DETAIL_LENGTH - 1).trimEnd()}\u2026`;
|
|
14383
|
+
}
|
|
14384
|
+
function isRecord4(value) {
|
|
14385
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
14386
|
+
}
|
|
14387
|
+
|
|
14388
|
+
class RemoteSkillsAuthClient {
|
|
14389
|
+
apiOrigin;
|
|
14390
|
+
constructor(apiUrl) {
|
|
14391
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
14392
|
+
}
|
|
14393
|
+
requestCode(email) {
|
|
14394
|
+
return this.request("/api/auth/login", { method: "POST", body: JSON.stringify({ email }) });
|
|
14395
|
+
}
|
|
14396
|
+
verifyCode(email, code) {
|
|
14397
|
+
return this.request("/api/auth/verify", { method: "POST", body: JSON.stringify({ email, code }) });
|
|
14398
|
+
}
|
|
14399
|
+
startDevice() {
|
|
14400
|
+
return this.request("/api/auth/device/start", { method: "POST", body: JSON.stringify({ client: "skills-sdk" }) });
|
|
14401
|
+
}
|
|
14402
|
+
pollDevice(deviceCode) {
|
|
14403
|
+
return this.request("/api/auth/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) });
|
|
14404
|
+
}
|
|
14405
|
+
async sessionClient(email, code) {
|
|
14406
|
+
if (!email.includes("@") || !/^\d{6}$/.test(code))
|
|
14407
|
+
throw new Error("Fresh email and six-digit verification code are required to manage API keys");
|
|
14408
|
+
const login = await this.verifyCode(email, code);
|
|
14409
|
+
if (!login || typeof login.token !== "string" || !login.token)
|
|
14410
|
+
throw new Error("The server did not return an authorized account session");
|
|
14411
|
+
return new RemoteSkillsClient(login.token, this.apiOrigin);
|
|
14412
|
+
}
|
|
14413
|
+
async createApiKey(email, code, name, scopes) {
|
|
14414
|
+
return (await this.sessionClient(email, code)).createApiKey(name, scopes);
|
|
14415
|
+
}
|
|
14416
|
+
async listApiKeys(email, code) {
|
|
14417
|
+
return (await this.sessionClient(email, code)).listApiKeys();
|
|
14418
|
+
}
|
|
14419
|
+
async revokeApiKey(email, code, keyId) {
|
|
14420
|
+
return (await this.sessionClient(email, code)).revokeApiKey(keyId);
|
|
14421
|
+
}
|
|
14422
|
+
request(path, options) {
|
|
14423
|
+
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
|
|
14424
|
+
throw new Error("Unsupported authentication operation");
|
|
14425
|
+
return requestAuthApi(this.apiOrigin, path, options);
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
13871
14428
|
export {
|
|
13872
14429
|
writeStationSnapshot,
|
|
13873
14430
|
writeStationHydration,
|
|
@@ -14079,8 +14636,10 @@ export {
|
|
|
14079
14636
|
SKILLS_API_KEY_ENV,
|
|
14080
14637
|
SKILLS,
|
|
14081
14638
|
RemoteSkillsClient,
|
|
14639
|
+
RemoteSkillsAuthClient,
|
|
14082
14640
|
RemoteRouteUnsupportedError,
|
|
14083
14641
|
RemoteRequestError,
|
|
14642
|
+
RemoteCreditApprovalError,
|
|
14084
14643
|
REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
14085
14644
|
REFUSED_SCANNER_FLAGGED,
|
|
14086
14645
|
PullSkillError,
|
|
@@ -14090,6 +14649,7 @@ export {
|
|
|
14090
14649
|
PORTABLE_SKILL_DEFAULT_VERSION,
|
|
14091
14650
|
MissingSkillsFleetError,
|
|
14092
14651
|
MCP_CONTRACT_SCHEMA_VERSION,
|
|
14652
|
+
HostedApiError,
|
|
14093
14653
|
DEFAULT_EXPORT_DIR,
|
|
14094
14654
|
CATEGORIES,
|
|
14095
14655
|
BASIC_SKILL_NAMES,
|