@tiny-fish/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/_utils/client.d.ts +40 -0
- package/dist/_utils/client.d.ts.map +1 -0
- package/dist/_utils/client.js +168 -0
- package/dist/_utils/client.js.map +1 -0
- package/dist/_utils/errors.d.ts +109 -0
- package/dist/_utils/errors.d.ts.map +1 -0
- package/dist/_utils/errors.js +123 -0
- package/dist/_utils/errors.js.map +1 -0
- package/dist/_utils/index.d.ts +15 -0
- package/dist/_utils/index.d.ts.map +1 -0
- package/dist/_utils/index.js +14 -0
- package/dist/_utils/index.js.map +1 -0
- package/dist/_utils/resource.d.ts +6 -0
- package/dist/_utils/resource.d.ts.map +1 -0
- package/dist/_utils/resource.js +7 -0
- package/dist/_utils/resource.js.map +1 -0
- package/dist/_utils/sse.d.ts +15 -0
- package/dist/_utils/sse.d.ts.map +1 -0
- package/dist/_utils/sse.js +62 -0
- package/dist/_utils/sse.js.map +1 -0
- package/dist/agent/index.d.ts +47 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +168 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/types.d.ts +163 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +61 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +16 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/runs/index.d.ts +25 -0
- package/dist/runs/index.d.ts.map +1 -0
- package/dist/runs/index.js +47 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/types.d.ts +75 -0
- package/dist/runs/types.d.ts.map +1 -0
- package/dist/runs/types.js +10 -0
- package/dist/runs/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +43 -0
- package/src/_utils/client.ts +219 -0
- package/src/_utils/errors.ts +153 -0
- package/src/_utils/index.ts +31 -0
- package/src/_utils/resource.ts +9 -0
- package/src/_utils/sse.ts +72 -0
- package/src/agent/index.ts +210 -0
- package/src/agent/types.ts +190 -0
- package/src/client.ts +19 -0
- package/src/index.ts +64 -0
- package/src/runs/index.ts +47 -0
- package/src/runs/types.ts +81 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs management request/response types.
|
|
3
|
+
*/
|
|
4
|
+
import type { RunError, RunStatus } from "../agent/types.js";
|
|
5
|
+
/** Sort order for list queries. */
|
|
6
|
+
export declare enum SortDirection {
|
|
7
|
+
ASC = "asc",
|
|
8
|
+
DESC = "desc"
|
|
9
|
+
}
|
|
10
|
+
/** Browser configuration used for a run. */
|
|
11
|
+
export interface BrowserConfig {
|
|
12
|
+
/** Whether proxy was enabled. */
|
|
13
|
+
proxy_enabled: boolean | null;
|
|
14
|
+
/** Country code for proxy. */
|
|
15
|
+
proxy_country_code: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** A single automation run with full details. */
|
|
18
|
+
export interface Run {
|
|
19
|
+
/** Unique identifier for the run. */
|
|
20
|
+
run_id: string;
|
|
21
|
+
/** Current status of the run. */
|
|
22
|
+
status: RunStatus;
|
|
23
|
+
/** Natural language goal for this automation run. */
|
|
24
|
+
goal: string;
|
|
25
|
+
/** ISO 8601 timestamp when run was created. */
|
|
26
|
+
created_at: string;
|
|
27
|
+
/** ISO 8601 timestamp when run started executing. */
|
|
28
|
+
started_at: string | null;
|
|
29
|
+
/** ISO 8601 timestamp when run finished executing. */
|
|
30
|
+
finished_at: string | null;
|
|
31
|
+
/** Number of steps taken during the automation. */
|
|
32
|
+
num_of_steps: number;
|
|
33
|
+
/** Extracted data from the automation run. `null` if not completed or failed. */
|
|
34
|
+
result: Record<string, unknown> | null;
|
|
35
|
+
/** Error details. `null` if the run succeeded or is still running. */
|
|
36
|
+
error: RunError | null;
|
|
37
|
+
/** URL to watch live browser session (available while running). */
|
|
38
|
+
streaming_url: string | null;
|
|
39
|
+
/** Browser configuration used for the run. */
|
|
40
|
+
browser_config: BrowserConfig | null;
|
|
41
|
+
}
|
|
42
|
+
/** Pagination metadata for list responses. */
|
|
43
|
+
export interface PaginationInfo {
|
|
44
|
+
/** Total number of runs matching the current filters. */
|
|
45
|
+
total: number;
|
|
46
|
+
/** Whether there are more results after this page. */
|
|
47
|
+
has_more: boolean;
|
|
48
|
+
/** Cursor for fetching next page. `null` if no more results. */
|
|
49
|
+
next_cursor: string | null;
|
|
50
|
+
}
|
|
51
|
+
/** Paginated list of automation runs. */
|
|
52
|
+
export interface RunListResponse {
|
|
53
|
+
/** Array of runs. */
|
|
54
|
+
data: Run[];
|
|
55
|
+
/** Pagination information. */
|
|
56
|
+
pagination: PaginationInfo;
|
|
57
|
+
}
|
|
58
|
+
/** Parameters for listing runs. */
|
|
59
|
+
export interface RunListParams {
|
|
60
|
+
/** Pagination cursor from a previous response's `next_cursor` field. */
|
|
61
|
+
cursor?: string;
|
|
62
|
+
/** Maximum number of runs to return. */
|
|
63
|
+
limit?: number;
|
|
64
|
+
/** Filter by run status. */
|
|
65
|
+
status?: RunStatus;
|
|
66
|
+
/** Filter by goal text. */
|
|
67
|
+
goal?: string;
|
|
68
|
+
/** Filter runs created after this ISO timestamp. */
|
|
69
|
+
created_after?: string;
|
|
70
|
+
/** Filter runs created before this ISO timestamp. */
|
|
71
|
+
created_before?: string;
|
|
72
|
+
/** Sort order — `"asc"` or `"desc"`. */
|
|
73
|
+
sort_direction?: SortDirection;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runs/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE7D,mCAAmC;AACnC,oBAAY,aAAa;IACxB,GAAG,QAAQ;IACX,IAAI,SAAS;CACb;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC7B,iCAAiC;IACjC,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,8BAA8B;IAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,iDAAiD;AACjD,MAAM,WAAW,GAAG;IACnB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,sDAAsD;IACtD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,mDAAmD;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,sEAAsE;IACtE,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,8CAA8C;IAC9C,cAAc,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAED,8CAA8C;AAC9C,MAAM,WAAW,cAAc;IAC9B,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,yCAAyC;AACzC,MAAM,WAAW,eAAe;IAC/B,qBAAqB;IACrB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,8BAA8B;IAC9B,UAAU,EAAE,cAAc,CAAC;CAC3B;AAED,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC7B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs management request/response types.
|
|
3
|
+
*/
|
|
4
|
+
/** Sort order for list queries. */
|
|
5
|
+
export var SortDirection;
|
|
6
|
+
(function (SortDirection) {
|
|
7
|
+
SortDirection["ASC"] = "asc";
|
|
8
|
+
SortDirection["DESC"] = "desc";
|
|
9
|
+
})(SortDirection || (SortDirection = {}));
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/runs/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,mCAAmC;AACnC,MAAM,CAAN,IAAY,aAGX;AAHD,WAAY,aAAa;IACxB,4BAAW,CAAA;IACX,8BAAa,CAAA;AACd,CAAC,EAHW,aAAa,KAAb,aAAa,QAGxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tiny-fish/sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TinyFish TypeScript SDK — State-of-the-art web agents in an API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"generate:version": "bun scripts/generate-version.ts",
|
|
23
|
+
"prebuild": "bun run generate:version",
|
|
24
|
+
"build": "bunx tsc -p tsconfig.build.json",
|
|
25
|
+
"prepack": "bun run build",
|
|
26
|
+
"typecheck": "bunx tsc --noEmit",
|
|
27
|
+
"lint": "biome check src/ tests/ scripts/ playground/",
|
|
28
|
+
"format": "biome format --write src/ tests/ scripts/ playground/",
|
|
29
|
+
"test": "bun test"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@biomejs/biome": "^1.9.0",
|
|
33
|
+
"@types/bun": "^1.3.10",
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"p-retry": "^7.1.1"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org/",
|
|
41
|
+
"access": "restricted"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base HTTP client — handles auth, headers, retries, and error mapping.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pRetry, { AbortError } from "p-retry";
|
|
6
|
+
|
|
7
|
+
import { VERSION } from "../version.js";
|
|
8
|
+
import {
|
|
9
|
+
APIConnectionError,
|
|
10
|
+
APIStatusError,
|
|
11
|
+
APITimeoutError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
BadRequestError,
|
|
14
|
+
ConflictError,
|
|
15
|
+
InternalServerError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
PermissionDeniedError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
RequestTimeoutError,
|
|
20
|
+
SDKError,
|
|
21
|
+
UnprocessableEntityError,
|
|
22
|
+
} from "./errors.js";
|
|
23
|
+
|
|
24
|
+
export interface ClientOptions {
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
baseURL?: string;
|
|
27
|
+
timeout?: number;
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_BASE_URL = "https://agent.tinyfish.ai";
|
|
32
|
+
const DEFAULT_TIMEOUT = 600_000; // 10 minutes, matches Python SDK
|
|
33
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
34
|
+
|
|
35
|
+
export class BaseClient {
|
|
36
|
+
readonly apiKey: string;
|
|
37
|
+
readonly baseURL: string;
|
|
38
|
+
readonly timeout: number;
|
|
39
|
+
readonly maxRetries: number;
|
|
40
|
+
|
|
41
|
+
constructor(options: ClientOptions = {}) {
|
|
42
|
+
const apiKey = options.apiKey ?? process.env["TINYFISH_API_KEY"];
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
throw new SDKError(
|
|
45
|
+
"Missing API key. Pass `apiKey` to the constructor or set the TINYFISH_API_KEY environment variable.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.apiKey = apiKey;
|
|
50
|
+
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
|
|
51
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
52
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected _buildHeaders(): Record<string, string> {
|
|
56
|
+
return {
|
|
57
|
+
"X-API-Key": this.apiKey,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
"User-Agent": `tinyfish-typescript/${VERSION}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Core request method with retry and error mapping.
|
|
66
|
+
* All public methods (get, post, postStream) delegate here.
|
|
67
|
+
*/
|
|
68
|
+
private async _request(method: string, path: string, body?: unknown): Promise<Response> {
|
|
69
|
+
const url = `${this.baseURL}${path}`;
|
|
70
|
+
const headers = this._buildHeaders();
|
|
71
|
+
|
|
72
|
+
return pRetry(
|
|
73
|
+
async () => {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
method,
|
|
77
|
+
headers,
|
|
78
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
79
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sdkError = await this._makeStatusError(response);
|
|
87
|
+
|
|
88
|
+
// Non-retryable status codes abort immediately
|
|
89
|
+
const status = response.status;
|
|
90
|
+
if (status !== 408 && status !== 429 && status < 500) {
|
|
91
|
+
throw new AbortError(sdkError);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Retryable — let p-retry handle backoff
|
|
95
|
+
throw sdkError;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Re-throw AbortError and SDK errors as-is
|
|
98
|
+
if (error instanceof AbortError || error instanceof SDKError) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// AbortSignal.timeout throws a DOMException with name "TimeoutError"
|
|
103
|
+
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
104
|
+
throw new APITimeoutError(`Request timed out after ${this.timeout}ms`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Network failures (DNS, refused, etc.)
|
|
108
|
+
throw new APIConnectionError(
|
|
109
|
+
error instanceof Error ? error.message : "Connection failed",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
retries: this.maxRetries,
|
|
115
|
+
minTimeout: 500,
|
|
116
|
+
maxTimeout: 8_000,
|
|
117
|
+
factor: 2,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extracts a human-readable error message from the response body.
|
|
124
|
+
* Tries JSON body → error.message → message, falls back to text, then HTTP status.
|
|
125
|
+
* Matches Python SDK's _parse_error_message.
|
|
126
|
+
*/
|
|
127
|
+
private async _parseErrorMessage(response: Response): Promise<string> {
|
|
128
|
+
try {
|
|
129
|
+
const text = await response.text();
|
|
130
|
+
try {
|
|
131
|
+
const body = JSON.parse(text) as Record<string, unknown> | null | undefined;
|
|
132
|
+
const errorObj = body?.["error"] as Record<string, unknown> | null | undefined;
|
|
133
|
+
const msg = (errorObj?.["message"] as string) ?? (body?.["message"] as string);
|
|
134
|
+
if (typeof msg === "string" && msg.length > 0) {
|
|
135
|
+
return msg;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Not JSON — fall through to use raw text
|
|
139
|
+
}
|
|
140
|
+
if (text.length > 0) {
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Body read failed entirely
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return `HTTP ${response.status}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Maps HTTP status codes to typed error classes. Matches Python SDK's _make_status_error. */
|
|
151
|
+
private async _makeStatusError(response: Response): Promise<SDKError> {
|
|
152
|
+
const msg = await this._parseErrorMessage(response);
|
|
153
|
+
const opts = { response };
|
|
154
|
+
|
|
155
|
+
switch (response.status) {
|
|
156
|
+
case 400:
|
|
157
|
+
return new BadRequestError(msg, opts);
|
|
158
|
+
case 401:
|
|
159
|
+
return new AuthenticationError(msg, opts);
|
|
160
|
+
case 403:
|
|
161
|
+
return new PermissionDeniedError(msg, opts);
|
|
162
|
+
case 404:
|
|
163
|
+
return new NotFoundError(msg, opts);
|
|
164
|
+
case 408:
|
|
165
|
+
return new RequestTimeoutError(msg, opts);
|
|
166
|
+
case 409:
|
|
167
|
+
return new ConflictError(msg, opts);
|
|
168
|
+
case 422:
|
|
169
|
+
return new UnprocessableEntityError(msg, opts);
|
|
170
|
+
case 429:
|
|
171
|
+
return new RateLimitError(msg, opts);
|
|
172
|
+
default:
|
|
173
|
+
if (response.status >= 500) {
|
|
174
|
+
return new InternalServerError(msg, {
|
|
175
|
+
response,
|
|
176
|
+
statusCode: response.status,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return new APIStatusError(msg, {
|
|
180
|
+
response,
|
|
181
|
+
statusCode: response.status,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async get<T>(
|
|
187
|
+
path: string,
|
|
188
|
+
options?: { params?: Record<string, string | number | boolean | undefined> },
|
|
189
|
+
): Promise<T> {
|
|
190
|
+
let url = path;
|
|
191
|
+
if (options?.params) {
|
|
192
|
+
const filtered: Record<string, string> = {};
|
|
193
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
194
|
+
if (value !== undefined) {
|
|
195
|
+
filtered[key] = String(value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const searchParams = new URLSearchParams(filtered);
|
|
199
|
+
url = `${path}?${searchParams.toString()}`;
|
|
200
|
+
}
|
|
201
|
+
const response = await this._request("GET", url);
|
|
202
|
+
return (await response.json()) as T;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async post<T>(path: string, options?: { json?: unknown }): Promise<T> {
|
|
206
|
+
const response = await this._request("POST", path, options?.json);
|
|
207
|
+
return (await response.json()) as T;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async postStream(path: string, options?: { json?: unknown }): Promise<ReadableStream<string>> {
|
|
211
|
+
const response = await this._request("POST", path, options?.json);
|
|
212
|
+
|
|
213
|
+
if (!response.body) {
|
|
214
|
+
throw new SDKError("Response body is null — server did not return a stream");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return response.body.pipeThrough(new TextDecoderStream());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exception hierarchy for SDK errors.
|
|
3
|
+
*
|
|
4
|
+
* Hierarchy:
|
|
5
|
+
* SDKError
|
|
6
|
+
* +- SSEParseError
|
|
7
|
+
* +- APIError
|
|
8
|
+
* +- APIConnectionError
|
|
9
|
+
* | +- APITimeoutError
|
|
10
|
+
* +- APIStatusError
|
|
11
|
+
* +- BadRequestError (400)
|
|
12
|
+
* +- AuthenticationError (401)
|
|
13
|
+
* +- PermissionDeniedError (403)
|
|
14
|
+
* +- NotFoundError (404)
|
|
15
|
+
* +- RequestTimeoutError (408)
|
|
16
|
+
* +- ConflictError (409)
|
|
17
|
+
* +- UnprocessableEntityError (422)
|
|
18
|
+
* +- RateLimitError (429)
|
|
19
|
+
* +- InternalServerError (500+)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export class SDKError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "SDKError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SSEParseError extends SDKError {
|
|
30
|
+
readonly line: string;
|
|
31
|
+
|
|
32
|
+
constructor(message: string, line: string) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "SSEParseError";
|
|
35
|
+
this.line = line;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class APIError extends SDKError {
|
|
40
|
+
readonly request: Request | undefined;
|
|
41
|
+
readonly response: Response | undefined;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
message: string,
|
|
45
|
+
options?: { request?: Request | undefined; response?: Response | undefined },
|
|
46
|
+
) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "APIError";
|
|
49
|
+
this.request = options?.request;
|
|
50
|
+
this.response = options?.response;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class APIConnectionError extends APIError {
|
|
55
|
+
constructor(message: string, options?: { request?: Request | undefined }) {
|
|
56
|
+
super(message, options);
|
|
57
|
+
this.name = "APIConnectionError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class APITimeoutError extends APIConnectionError {
|
|
62
|
+
constructor(message: string, options?: { request?: Request | undefined }) {
|
|
63
|
+
super(message, options);
|
|
64
|
+
this.name = "APITimeoutError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class APIStatusError extends APIError {
|
|
69
|
+
readonly statusCode: number;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
message: string,
|
|
73
|
+
options: { response: Response; statusCode: number; request?: Request | undefined },
|
|
74
|
+
) {
|
|
75
|
+
super(message, { request: options.request, response: options.response });
|
|
76
|
+
this.name = "APIStatusError";
|
|
77
|
+
this.statusCode = options.statusCode;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class BadRequestError extends APIStatusError {
|
|
82
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
83
|
+
super(message, { ...options, statusCode: 400 });
|
|
84
|
+
this.name = "BadRequestError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class AuthenticationError extends APIStatusError {
|
|
89
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
90
|
+
super(message, { ...options, statusCode: 401 });
|
|
91
|
+
this.name = "AuthenticationError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class PermissionDeniedError extends APIStatusError {
|
|
96
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
97
|
+
super(message, { ...options, statusCode: 403 });
|
|
98
|
+
this.name = "PermissionDeniedError";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class NotFoundError extends APIStatusError {
|
|
103
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
104
|
+
super(message, { ...options, statusCode: 404 });
|
|
105
|
+
this.name = "NotFoundError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class RequestTimeoutError extends APIStatusError {
|
|
110
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
111
|
+
super(message, { ...options, statusCode: 408 });
|
|
112
|
+
this.name = "RequestTimeoutError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class ConflictError extends APIStatusError {
|
|
117
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
118
|
+
super(message, { ...options, statusCode: 409 });
|
|
119
|
+
this.name = "ConflictError";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class UnprocessableEntityError extends APIStatusError {
|
|
124
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
125
|
+
super(message, { ...options, statusCode: 422 });
|
|
126
|
+
this.name = "UnprocessableEntityError";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class RateLimitError extends APIStatusError {
|
|
131
|
+
constructor(message: string, options: { response: Response; request?: Request | undefined }) {
|
|
132
|
+
super(message, { ...options, statusCode: 429 });
|
|
133
|
+
this.name = "RateLimitError";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class InternalServerError extends APIStatusError {
|
|
138
|
+
constructor(
|
|
139
|
+
message: string,
|
|
140
|
+
options: {
|
|
141
|
+
response: Response;
|
|
142
|
+
statusCode?: number | undefined;
|
|
143
|
+
request?: Request | undefined;
|
|
144
|
+
},
|
|
145
|
+
) {
|
|
146
|
+
super(message, {
|
|
147
|
+
response: options.response,
|
|
148
|
+
statusCode: options.statusCode ?? 500,
|
|
149
|
+
request: options.request,
|
|
150
|
+
});
|
|
151
|
+
this.name = "InternalServerError";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable SDK foundation.
|
|
3
|
+
*
|
|
4
|
+
* Provides base classes for building HTTP API SDKs:
|
|
5
|
+
* - Client: HTTP client with auth and request plumbing
|
|
6
|
+
* - Resource: Base class for API resource groupings
|
|
7
|
+
* - Errors: Complete error hierarchy
|
|
8
|
+
* - SSE: Server-sent events parser
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { BaseClient } from "./client.js";
|
|
12
|
+
export type { ClientOptions } from "./client.js";
|
|
13
|
+
export { APIResource } from "./resource.js";
|
|
14
|
+
export { parseSSEStream } from "./sse.js";
|
|
15
|
+
export {
|
|
16
|
+
SDKError,
|
|
17
|
+
SSEParseError,
|
|
18
|
+
APIError,
|
|
19
|
+
APIConnectionError,
|
|
20
|
+
APITimeoutError,
|
|
21
|
+
APIStatusError,
|
|
22
|
+
BadRequestError,
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
PermissionDeniedError,
|
|
25
|
+
NotFoundError,
|
|
26
|
+
RequestTimeoutError,
|
|
27
|
+
ConflictError,
|
|
28
|
+
UnprocessableEntityError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
InternalServerError,
|
|
31
|
+
} from "./errors.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE line-stream parser.
|
|
3
|
+
* Converts a ReadableStream of text chunks into parsed JSON event objects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SSEParseError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
const DATA_PREFIX = "data:";
|
|
9
|
+
|
|
10
|
+
/** Split a ReadableStream<string> of arbitrary text chunks into lines. */
|
|
11
|
+
async function* splitLines(stream: ReadableStream<string>): AsyncGenerator<string> {
|
|
12
|
+
const reader = stream.getReader();
|
|
13
|
+
let buffer = "";
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done) break;
|
|
19
|
+
|
|
20
|
+
buffer += value;
|
|
21
|
+
const lines = buffer.split("\n");
|
|
22
|
+
// Last element is either empty (if chunk ended with \n) or a partial line
|
|
23
|
+
buffer = lines.pop() ?? "";
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
yield line;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Flush any remaining partial line
|
|
31
|
+
if (buffer.length > 0) {
|
|
32
|
+
yield buffer;
|
|
33
|
+
}
|
|
34
|
+
} finally {
|
|
35
|
+
reader.releaseLock();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse an SSE text stream into JSON event objects.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors Python SDK's `parse_sse_line_stream`:
|
|
43
|
+
* - Strips each line
|
|
44
|
+
* - Skips empty lines and comment lines (`:` prefix)
|
|
45
|
+
* - Extracts `data:` payloads and parses as JSON
|
|
46
|
+
* - Ignores `event:`, `id:`, `retry:` lines
|
|
47
|
+
*/
|
|
48
|
+
export async function* parseSSEStream(
|
|
49
|
+
stream: ReadableStream<string>,
|
|
50
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
51
|
+
for await (const rawLine of splitLines(stream)) {
|
|
52
|
+
const line = rawLine.trim();
|
|
53
|
+
|
|
54
|
+
// Skip empty lines and comments
|
|
55
|
+
if (!line || line.startsWith(":")) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse data lines
|
|
60
|
+
if (line.startsWith(DATA_PREFIX)) {
|
|
61
|
+
const dataStr = line.slice(DATA_PREFIX.length).trim();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
yield JSON.parse(dataStr) as Record<string, unknown>;
|
|
65
|
+
} catch {
|
|
66
|
+
throw new SSEParseError("Malformed JSON in SSE event", line);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ignore event:, id:, retry: lines
|
|
71
|
+
}
|
|
72
|
+
}
|