skilld-sdk 0.1.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 +179 -0
- package/dist/contract.d.mts +6816 -0
- package/dist/contract.mjs +2440 -0
- package/dist/index.d.mts +97 -0
- package/dist/index.mjs +253 -0
- package/generated/openapi.v1.json +10244 -0
- package/package.json +48 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { OperationInput, OperationOutput, SkilldV1ErrorCode, SkilldV1OperationDefinition, SkilldV1Problem, SkilldV1Protocol, SkilldV1ProtocolLike, skilldV1Protocol } from "./contract.mjs";
|
|
2
|
+
//#region src/client.d.ts
|
|
3
|
+
type Result<TValue, TError> = {
|
|
4
|
+
_tag: 'Ok';
|
|
5
|
+
value: TValue;
|
|
6
|
+
requestId?: string;
|
|
7
|
+
} | {
|
|
8
|
+
_tag: 'Err';
|
|
9
|
+
error: TError;
|
|
10
|
+
};
|
|
11
|
+
/** The input failed the operation's schema, so nothing was sent. */
|
|
12
|
+
interface RequestFailure {
|
|
13
|
+
_tag: 'RequestFailure';
|
|
14
|
+
operationId: string;
|
|
15
|
+
location: 'input' | 'params' | 'query' | 'body';
|
|
16
|
+
issues: readonly unknown[];
|
|
17
|
+
}
|
|
18
|
+
/** skilld.dev answered with a problem the contract declares. */
|
|
19
|
+
interface ApiFailure {
|
|
20
|
+
_tag: 'ApiFailure';
|
|
21
|
+
operationId: string;
|
|
22
|
+
code: SkilldV1ErrorCode;
|
|
23
|
+
status: number;
|
|
24
|
+
title: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
requestId?: string;
|
|
27
|
+
retryable: boolean;
|
|
28
|
+
problem: SkilldV1Problem;
|
|
29
|
+
}
|
|
30
|
+
/** skilld.dev answered with something the contract does not allow. Report it with the request ID. */
|
|
31
|
+
interface ContractFailure {
|
|
32
|
+
_tag: 'ContractFailure';
|
|
33
|
+
operationId: string;
|
|
34
|
+
status: number;
|
|
35
|
+
message: string;
|
|
36
|
+
issues: readonly unknown[];
|
|
37
|
+
requestId?: string;
|
|
38
|
+
}
|
|
39
|
+
/** The request never got an answer. */
|
|
40
|
+
interface TransportFailure {
|
|
41
|
+
_tag: 'TransportFailure';
|
|
42
|
+
operationId: string;
|
|
43
|
+
reason: 'aborted' | 'credential' | 'network';
|
|
44
|
+
message: string;
|
|
45
|
+
retryable: boolean;
|
|
46
|
+
cause?: unknown;
|
|
47
|
+
}
|
|
48
|
+
type SkilldFailure = RequestFailure | ApiFailure | ContractFailure | TransportFailure;
|
|
49
|
+
type FetchImplementation = (input: string, init: RequestInit) => Promise<Response>;
|
|
50
|
+
type TokenResolver = string | (() => string | undefined | Promise<string | undefined>);
|
|
51
|
+
interface RetryOptions {
|
|
52
|
+
/** Attempts per call, the first one included. Default 3. */
|
|
53
|
+
maxAttempts?: number;
|
|
54
|
+
baseDelayMs?: number;
|
|
55
|
+
maxDelayMs?: number;
|
|
56
|
+
sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
interface CreateSkilldClientOptions {
|
|
59
|
+
/** Default `https://skilld.dev`. */
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
/**
|
|
62
|
+
* A skilld token: `skilld auth login` stores one, and skilld.dev/me/cli-tokens/new
|
|
63
|
+
* creates one. Only account operations need it.
|
|
64
|
+
*/
|
|
65
|
+
token?: TokenResolver;
|
|
66
|
+
fetch?: FetchImplementation;
|
|
67
|
+
headers?: HeadersInit;
|
|
68
|
+
/** Send the skilld.dev sign-in cookie. For a page served from skilld.dev only. */
|
|
69
|
+
credentials?: RequestCredentials;
|
|
70
|
+
retry?: RetryOptions;
|
|
71
|
+
}
|
|
72
|
+
interface CallOptions {
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
}
|
|
75
|
+
/** Calls whose input has no required location take the input as optional. */
|
|
76
|
+
type EmptyInput = Record<never, never>;
|
|
77
|
+
type OperationCall<TOperation extends SkilldV1OperationDefinition> = EmptyInput extends OperationInput<TOperation> ? (input?: OperationInput<TOperation>, options?: CallOptions) => Promise<Result<OperationOutput<TOperation>, SkilldFailure>> : (input: OperationInput<TOperation>, options?: CallOptions) => Promise<Result<OperationOutput<TOperation>, SkilldFailure>>;
|
|
78
|
+
type DomainClient<TOperations extends Readonly<Record<string, SkilldV1OperationDefinition>>> = { [TKey in keyof TOperations]: OperationCall<TOperations[TKey]>; };
|
|
79
|
+
type ProtocolClient<TProtocol extends SkilldV1ProtocolLike> = { [TName in keyof TProtocol['registries']]: DomainClient<TProtocol['registries'][TName]['operations']>; } & {
|
|
80
|
+
execute: <const TOperation extends SkilldV1OperationDefinition>(operation: TOperation, input?: OperationInput<TOperation>, options?: CallOptions) => Promise<Result<OperationOutput<TOperation>, SkilldFailure>>;
|
|
81
|
+
};
|
|
82
|
+
type SkilldClient = ProtocolClient<SkilldV1Protocol>;
|
|
83
|
+
/**
|
|
84
|
+
* A typed client for the skilld API. Every call answers a `Result`: check
|
|
85
|
+
* `_tag` before you read `value`. Nothing throws for an expected failure.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const skilld = createSkilldClient({ token: process.env.SKILLD_TOKEN })
|
|
89
|
+
* const found = await skilld.skills.search({ query: { q: 'tailwind' } })
|
|
90
|
+
* if (found._tag === 'Err')
|
|
91
|
+
* throw new Error(found.error._tag)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
declare function createSkilldClient(options?: CreateSkilldClientOptions): SkilldClient;
|
|
95
|
+
declare function createProtocolClient<const TProtocol extends SkilldV1ProtocolLike>(protocol: TProtocol, options?: CreateSkilldClientOptions): ProtocolClient<TProtocol>;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { ApiFailure, CallOptions, ContractFailure, CreateSkilldClientOptions, DomainClient, FetchImplementation, OperationCall, type OperationInput, type OperationOutput, ProtocolClient, RequestFailure, Result, RetryOptions, SkilldClient, SkilldFailure, type SkilldV1ErrorCode, type SkilldV1OperationDefinition, type SkilldV1Problem, type SkilldV1Protocol, TokenResolver, TransportFailure, createProtocolClient, createSkilldClient, skilldV1Protocol };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { SKILLD_V1_ERROR_STATUS, SKILLD_V1_IMPLICIT_ERRORS, SKILLD_V1_RESPONSE_HEADERS, SKILLD_V1_RETRYABLE_ERRORS, buildOperationPath, listOperations, problemSchema, skilldV1Protocol } from "./contract.mjs";
|
|
2
|
+
//#region src/client.ts
|
|
3
|
+
function err(error) {
|
|
4
|
+
return {
|
|
5
|
+
_tag: "Err",
|
|
6
|
+
error
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function defaultSleep(milliseconds, signal) {
|
|
10
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const handle = setTimeout(() => {
|
|
13
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14
|
+
resolve();
|
|
15
|
+
}, milliseconds);
|
|
16
|
+
function onAbort() {
|
|
17
|
+
clearTimeout(handle);
|
|
18
|
+
reject(signal?.reason);
|
|
19
|
+
}
|
|
20
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function resolveRetry(options) {
|
|
24
|
+
const maxAttempts = options?.maxAttempts ?? 3;
|
|
25
|
+
const baseDelayMs = options?.baseDelayMs ?? 200;
|
|
26
|
+
const maxDelayMs = options?.maxDelayMs ?? 5e3;
|
|
27
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 10) throw new TypeError("retry.maxAttempts must be an integer from 1 to 10.");
|
|
28
|
+
if (baseDelayMs < 0 || maxDelayMs < baseDelayMs) throw new TypeError("retry delays must be nonnegative, and maxDelayMs must be at least baseDelayMs.");
|
|
29
|
+
return {
|
|
30
|
+
maxAttempts,
|
|
31
|
+
baseDelayMs,
|
|
32
|
+
maxDelayMs,
|
|
33
|
+
sleep: options?.sleep ?? defaultSleep
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function retryDelay(attempt, retryAfter, retry) {
|
|
37
|
+
const exponential = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
38
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
39
|
+
const requested = Number.isFinite(seconds) && seconds >= 0 ? Math.ceil(seconds * 1e3) : 0;
|
|
40
|
+
return Math.min(Math.max(exponential, requested), 6e4);
|
|
41
|
+
}
|
|
42
|
+
function appendQuery(path, query) {
|
|
43
|
+
if (!query) return path;
|
|
44
|
+
const search = new URLSearchParams();
|
|
45
|
+
for (const key of Object.keys(query).sort()) {
|
|
46
|
+
const value = query[key];
|
|
47
|
+
if (value === void 0 || value === null) continue;
|
|
48
|
+
for (const item of Array.isArray(value) ? value : [value]) search.append(key, String(item));
|
|
49
|
+
}
|
|
50
|
+
const serialized = search.toString();
|
|
51
|
+
return serialized ? `${path}?${serialized}` : path;
|
|
52
|
+
}
|
|
53
|
+
function prepare(operation, input) {
|
|
54
|
+
const source = input ?? {};
|
|
55
|
+
if (typeof source !== "object" || Array.isArray(source)) return err({
|
|
56
|
+
_tag: "RequestFailure",
|
|
57
|
+
operationId: operation.id,
|
|
58
|
+
location: "input",
|
|
59
|
+
issues: ["input must be an object"]
|
|
60
|
+
});
|
|
61
|
+
const extra = Object.keys(source).filter((key) => key !== "params" && key !== "query" && key !== "body");
|
|
62
|
+
if (extra.length > 0) return err({
|
|
63
|
+
_tag: "RequestFailure",
|
|
64
|
+
operationId: operation.id,
|
|
65
|
+
location: "input",
|
|
66
|
+
issues: [`unknown input keys: ${extra.join(", ")}`]
|
|
67
|
+
});
|
|
68
|
+
const parsed = {};
|
|
69
|
+
for (const location of [
|
|
70
|
+
"params",
|
|
71
|
+
"query",
|
|
72
|
+
"body"
|
|
73
|
+
]) {
|
|
74
|
+
const schema = operation.request[location];
|
|
75
|
+
const supplied = source[location] !== void 0;
|
|
76
|
+
if (!schema) {
|
|
77
|
+
if (supplied) return err({
|
|
78
|
+
_tag: "RequestFailure",
|
|
79
|
+
operationId: operation.id,
|
|
80
|
+
location,
|
|
81
|
+
issues: [`${operation.id} takes no ${location}`]
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const result = schema.safeParse(supplied ? source[location] : location === "body" ? void 0 : {});
|
|
86
|
+
if (!result.success) return err({
|
|
87
|
+
_tag: "RequestFailure",
|
|
88
|
+
operationId: operation.id,
|
|
89
|
+
location,
|
|
90
|
+
issues: result.error.issues
|
|
91
|
+
});
|
|
92
|
+
parsed[location] = result.data;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
_tag: "Ok",
|
|
96
|
+
path: appendQuery(buildOperationPath(operation, parsed.params), parsed.query),
|
|
97
|
+
body: parsed.body === void 0 ? void 0 : JSON.stringify(parsed.body)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function readJson(response) {
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
if (!text) return {
|
|
103
|
+
_tag: "Ok",
|
|
104
|
+
value: null
|
|
105
|
+
};
|
|
106
|
+
return Promise.resolve().then(() => ({
|
|
107
|
+
_tag: "Ok",
|
|
108
|
+
value: JSON.parse(text)
|
|
109
|
+
})).catch((cause) => ({
|
|
110
|
+
_tag: "Err",
|
|
111
|
+
cause
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
async function resolveToken(token) {
|
|
115
|
+
return (typeof token === "function" ? await token() : token)?.trim() || void 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A typed client for the skilld API. Every call answers a `Result`: check
|
|
119
|
+
* `_tag` before you read `value`. Nothing throws for an expected failure.
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* const skilld = createSkilldClient({ token: process.env.SKILLD_TOKEN })
|
|
123
|
+
* const found = await skilld.skills.search({ query: { q: 'tailwind' } })
|
|
124
|
+
* if (found._tag === 'Err')
|
|
125
|
+
* throw new Error(found.error._tag)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
function createSkilldClient(options = {}) {
|
|
129
|
+
return createProtocolClient(skilldV1Protocol, options);
|
|
130
|
+
}
|
|
131
|
+
function createProtocolClient(protocol, options = {}) {
|
|
132
|
+
const retry = resolveRetry(options.retry);
|
|
133
|
+
const baseUrl = (options.baseUrl ?? "https://skilld.dev").replace(/\/+$/, "");
|
|
134
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
135
|
+
if (typeof fetchImplementation !== "function") throw new TypeError("createSkilldClient needs a fetch implementation in this runtime.");
|
|
136
|
+
const registered = new Map(listOperations(protocol).map(({ operation }) => [operation.id, operation]));
|
|
137
|
+
async function execute(operation, input, callOptions = {}) {
|
|
138
|
+
const known = registered.get(operation.id);
|
|
139
|
+
if (!known || known.method !== operation.method || known.path !== operation.path) return err({
|
|
140
|
+
_tag: "RequestFailure",
|
|
141
|
+
operationId: operation.id,
|
|
142
|
+
location: "input",
|
|
143
|
+
issues: [`${operation.id} is not part of this client's protocol`]
|
|
144
|
+
});
|
|
145
|
+
const prepared = prepare(operation, input);
|
|
146
|
+
if (prepared._tag === "Err") return prepared;
|
|
147
|
+
const canRetry = operation.semantics.kind === "query" || operation.semantics.retry === "idempotent";
|
|
148
|
+
const declared = /* @__PURE__ */ new Set([...operation.errors, ...SKILLD_V1_IMPLICIT_ERRORS]);
|
|
149
|
+
for (let attempt = 1;; attempt++) {
|
|
150
|
+
const headers = new Headers(options.headers);
|
|
151
|
+
headers.set("accept", "application/json");
|
|
152
|
+
if (prepared.body !== void 0) headers.set("content-type", "application/json");
|
|
153
|
+
const token = await Promise.resolve(options.token).then(resolveToken).then((value) => ({
|
|
154
|
+
_tag: "Ok",
|
|
155
|
+
value
|
|
156
|
+
}), (cause) => ({
|
|
157
|
+
_tag: "Err",
|
|
158
|
+
cause
|
|
159
|
+
}));
|
|
160
|
+
if (token._tag === "Err") return err({
|
|
161
|
+
_tag: "TransportFailure",
|
|
162
|
+
operationId: operation.id,
|
|
163
|
+
reason: "credential",
|
|
164
|
+
message: "Could not resolve the skilld token.",
|
|
165
|
+
retryable: false,
|
|
166
|
+
cause: token.cause
|
|
167
|
+
});
|
|
168
|
+
if (token.value) headers.set("authorization", `Bearer ${token.value}`);
|
|
169
|
+
const sent = await fetchImplementation(`${baseUrl}${prepared.path}`, {
|
|
170
|
+
method: operation.method,
|
|
171
|
+
headers,
|
|
172
|
+
body: prepared.body,
|
|
173
|
+
signal: callOptions.signal,
|
|
174
|
+
credentials: options.credentials
|
|
175
|
+
}).then((response) => ({
|
|
176
|
+
_tag: "Ok",
|
|
177
|
+
response
|
|
178
|
+
}), (cause) => ({
|
|
179
|
+
_tag: "Err",
|
|
180
|
+
cause
|
|
181
|
+
}));
|
|
182
|
+
if (sent._tag === "Err") {
|
|
183
|
+
const aborted = callOptions.signal?.aborted === true;
|
|
184
|
+
if (!aborted && canRetry && attempt < retry.maxAttempts) {
|
|
185
|
+
if (await retry.sleep(retryDelay(attempt, null, retry), callOptions.signal).then(() => true, () => false)) continue;
|
|
186
|
+
}
|
|
187
|
+
return err({
|
|
188
|
+
_tag: "TransportFailure",
|
|
189
|
+
operationId: operation.id,
|
|
190
|
+
reason: aborted ? "aborted" : "network",
|
|
191
|
+
message: `${operation.id}: ${aborted ? "request aborted" : "network request failed"}`,
|
|
192
|
+
retryable: !aborted && canRetry,
|
|
193
|
+
cause: sent.cause
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const response = sent.response;
|
|
197
|
+
const requestId = response.headers.get(SKILLD_V1_RESPONSE_HEADERS.requestId) ?? void 0;
|
|
198
|
+
const contractFailure = (message, issues = []) => err({
|
|
199
|
+
_tag: "ContractFailure",
|
|
200
|
+
operationId: operation.id,
|
|
201
|
+
status: response.status,
|
|
202
|
+
message,
|
|
203
|
+
issues,
|
|
204
|
+
requestId
|
|
205
|
+
});
|
|
206
|
+
if (response.status === operation.response.status) {
|
|
207
|
+
const body = operation.response.body;
|
|
208
|
+
if (body === null) {
|
|
209
|
+
await response.body?.cancel();
|
|
210
|
+
return {
|
|
211
|
+
_tag: "Ok",
|
|
212
|
+
value: null,
|
|
213
|
+
requestId
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const payload = await readJson(response);
|
|
217
|
+
if (payload._tag === "Err") return contractFailure(`${operation.id}: the answer was not JSON`, [payload.cause]);
|
|
218
|
+
const parsed = body.client.safeParse(payload.value);
|
|
219
|
+
return parsed.success ? {
|
|
220
|
+
_tag: "Ok",
|
|
221
|
+
value: parsed.data,
|
|
222
|
+
requestId
|
|
223
|
+
} : contractFailure(`${operation.id}: the answer broke its schema`, parsed.error.issues);
|
|
224
|
+
}
|
|
225
|
+
if (response.ok) return contractFailure(`${operation.id}: undeclared success status ${response.status}`);
|
|
226
|
+
const payload = await readJson(response);
|
|
227
|
+
const problem = payload._tag === "Ok" ? problemSchema.client.safeParse(payload.value) : null;
|
|
228
|
+
if (!problem?.success) return contractFailure(`${operation.id}: status ${response.status} without a problem body`);
|
|
229
|
+
const { code } = problem.data;
|
|
230
|
+
if (!declared.has(code) || SKILLD_V1_ERROR_STATUS[code] !== response.status) return contractFailure(`${operation.id}: undeclared problem ${code} with status ${response.status}`);
|
|
231
|
+
const failure = {
|
|
232
|
+
_tag: "ApiFailure",
|
|
233
|
+
operationId: operation.id,
|
|
234
|
+
code,
|
|
235
|
+
status: response.status,
|
|
236
|
+
title: problem.data.title,
|
|
237
|
+
detail: problem.data.detail,
|
|
238
|
+
requestId,
|
|
239
|
+
retryable: canRetry && SKILLD_V1_RETRYABLE_ERRORS.has(code),
|
|
240
|
+
problem: problem.data
|
|
241
|
+
};
|
|
242
|
+
if (failure.retryable && attempt < retry.maxAttempts) {
|
|
243
|
+
if (await retry.sleep(retryDelay(attempt, response.headers.get(SKILLD_V1_RESPONSE_HEADERS.retryAfter), retry), callOptions.signal).then(() => true, () => false)) continue;
|
|
244
|
+
}
|
|
245
|
+
return err(failure);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const client = { execute };
|
|
249
|
+
for (const [name, registry] of Object.entries(protocol.registries)) client[name] = Object.fromEntries(Object.entries(registry.operations).map(([key, operation]) => [key, (input, callOptions) => execute(operation, input, callOptions)]));
|
|
250
|
+
return client;
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { createProtocolClient, createSkilldClient, skilldV1Protocol };
|