roe-typescript 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/LICENSE +201 -0
- package/README.md +189 -0
- package/dist/api/agents.d.ts +87 -0
- package/dist/api/agents.js +263 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +11 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.js +18 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +43 -0
- package/dist/exceptions.d.ts +25 -0
- package/dist/exceptions.js +73 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/integration_test.js +686 -0
- package/dist/models/agent.d.ts +78 -0
- package/dist/models/agent.js +40 -0
- package/dist/models/file.d.ts +30 -0
- package/dist/models/file.js +85 -0
- package/dist/models/job.d.ts +37 -0
- package/dist/models/job.js +133 -0
- package/dist/models/responses.d.ts +71 -0
- package/dist/models/responses.js +36 -0
- package/dist/models/user.d.ts +6 -0
- package/dist/models/user.js +1 -0
- package/dist/src/api/agents.js +269 -0
- package/dist/src/auth.js +15 -0
- package/dist/src/client.js +22 -0
- package/dist/src/config.js +47 -0
- package/dist/src/exceptions.js +86 -0
- package/dist/src/models/agent.js +45 -0
- package/dist/src/models/file.js +92 -0
- package/dist/src/models/job.js +138 -0
- package/dist/src/models/responses.js +42 -0
- package/dist/src/models/user.js +2 -0
- package/dist/src/utils/fileDetection.js +46 -0
- package/dist/src/utils/httpClient.js +236 -0
- package/dist/src/utils/pagination.js +18 -0
- package/dist/utils/fileDetection.d.ts +11 -0
- package/dist/utils/fileDetection.js +38 -0
- package/dist/utils/httpClient.d.ts +30 -0
- package/dist/utils/httpClient.js +229 -0
- package/dist/utils/pagination.d.ts +3 -0
- package/dist/utils/pagination.js +14 -0
- package/package.json +36 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import https from "https";
|
|
6
|
+
import { FileUpload } from "../models/file";
|
|
7
|
+
import { getExceptionForStatusCode, extractErrorMessage } from "../exceptions";
|
|
8
|
+
import { isFilePath, isUuidString } from "./fileDetection";
|
|
9
|
+
// Keep-alive agents for connection reuse (improves performance under load)
|
|
10
|
+
const httpAgent = new http.Agent({ keepAlive: true });
|
|
11
|
+
const httpsAgent = new https.Agent({ keepAlive: true });
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
export class RoeHTTPClient {
|
|
16
|
+
constructor(config, auth) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.auth = auth;
|
|
19
|
+
this.maxRetries = config.maxRetries;
|
|
20
|
+
this.client = axios.create({
|
|
21
|
+
baseURL: config.baseUrl,
|
|
22
|
+
timeout: config.timeoutMs,
|
|
23
|
+
headers: auth.getHeaders(),
|
|
24
|
+
httpAgent,
|
|
25
|
+
httpsAgent,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
close() {
|
|
29
|
+
// no-op; included for parity
|
|
30
|
+
}
|
|
31
|
+
async execute(fn) {
|
|
32
|
+
let attempt = 0;
|
|
33
|
+
let lastError;
|
|
34
|
+
while (attempt <= this.maxRetries) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fn();
|
|
37
|
+
return this.handleResponse(res);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
lastError = err;
|
|
41
|
+
const status = axios.isAxiosError(err) ? err.response?.status : undefined;
|
|
42
|
+
const isNetworkError = axios.isAxiosError(err) && !err.response;
|
|
43
|
+
const retriableStatus = status !== undefined &&
|
|
44
|
+
(status >= 500 || status === 429 || status === 408);
|
|
45
|
+
const shouldRetry = (isNetworkError || retriableStatus) && attempt < this.maxRetries;
|
|
46
|
+
if (!shouldRetry) {
|
|
47
|
+
if (axios.isAxiosError(err) && err.response) {
|
|
48
|
+
const ExceptionClass = getExceptionForStatusCode(err.response.status);
|
|
49
|
+
const data = err.response.data;
|
|
50
|
+
// Use improved error message extraction that handles multiple formats
|
|
51
|
+
const message = extractErrorMessage(data, err.response.status);
|
|
52
|
+
throw new ExceptionClass(message, err.response.status, data ?? null);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
|
|
57
|
+
await sleep(backoffMs);
|
|
58
|
+
}
|
|
59
|
+
attempt += 1;
|
|
60
|
+
}
|
|
61
|
+
if (lastError)
|
|
62
|
+
throw lastError;
|
|
63
|
+
throw new Error("Request failed");
|
|
64
|
+
}
|
|
65
|
+
handleResponse(response) {
|
|
66
|
+
if (response.status >= 200 && response.status < 300) {
|
|
67
|
+
return response.data;
|
|
68
|
+
}
|
|
69
|
+
const ExceptionClass = getExceptionForStatusCode(response.status);
|
|
70
|
+
const data = response.data;
|
|
71
|
+
// Use improved error message extraction that handles multiple formats
|
|
72
|
+
const message = extractErrorMessage(data, response.status);
|
|
73
|
+
throw new ExceptionClass(message, response.status, data ?? null);
|
|
74
|
+
}
|
|
75
|
+
get(url, params) {
|
|
76
|
+
return this.execute(() => this.client.get(url, { params }));
|
|
77
|
+
}
|
|
78
|
+
async getBytes(url, params) {
|
|
79
|
+
const data = await this.execute(() => this.client.get(url, { params, responseType: "arraybuffer" }));
|
|
80
|
+
// Normalize ArrayBuffer/Buffer to Buffer (axios returns ArrayBuffer in browsers)
|
|
81
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
82
|
+
}
|
|
83
|
+
async post(options) {
|
|
84
|
+
const { url, json, formData, files, params } = options;
|
|
85
|
+
const config = { params };
|
|
86
|
+
if (json !== undefined) {
|
|
87
|
+
return this.execute(() => this.client.post(url, json, config));
|
|
88
|
+
}
|
|
89
|
+
// Check if we have files that need retry-safe handling
|
|
90
|
+
const hasFiles = files && Object.keys(files).length > 0;
|
|
91
|
+
if (!hasFiles) {
|
|
92
|
+
// No files - build FormData once (safe for retries)
|
|
93
|
+
const fd = new FormData();
|
|
94
|
+
if (formData) {
|
|
95
|
+
for (const [k, v] of Object.entries(formData)) {
|
|
96
|
+
if (v === undefined || v === null)
|
|
97
|
+
continue;
|
|
98
|
+
fd.append(k, String(v));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
config.headers = { ...(config.headers || {}), ...fd.getHeaders() };
|
|
102
|
+
return this.execute(() => this.client.post(url, fd, config));
|
|
103
|
+
}
|
|
104
|
+
// With files - need to rebuild FormData on each retry attempt
|
|
105
|
+
// because file streams can only be read once
|
|
106
|
+
return this.executeWithFormData(url, formData ?? {}, files, config);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Execute a POST request with FormData that contains files.
|
|
110
|
+
* Rebuilds FormData on each retry attempt to handle stream consumption.
|
|
111
|
+
*/
|
|
112
|
+
async executeWithFormData(url, formData, files, config) {
|
|
113
|
+
// Pre-resolve file paths to avoid async in retry loop
|
|
114
|
+
const resolvedFiles = [];
|
|
115
|
+
for (const [k, v] of Object.entries(files)) {
|
|
116
|
+
if (v instanceof FileUpload) {
|
|
117
|
+
resolvedFiles.push({ key: k, type: "fileUpload", value: v });
|
|
118
|
+
}
|
|
119
|
+
else if (typeof v === "string" && await isFilePath(v)) {
|
|
120
|
+
resolvedFiles.push({ key: k, type: "path", value: v });
|
|
121
|
+
}
|
|
122
|
+
else if (typeof v === "string") {
|
|
123
|
+
resolvedFiles.push({ key: k, type: "string", value: v });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Raw streams cannot be safely retried - warn but allow first attempt
|
|
127
|
+
resolvedFiles.push({ key: k, type: "stream", value: v });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let attempt = 0;
|
|
131
|
+
let lastError;
|
|
132
|
+
while (attempt <= this.maxRetries) {
|
|
133
|
+
try {
|
|
134
|
+
// Build fresh FormData for each attempt
|
|
135
|
+
const fd = new FormData();
|
|
136
|
+
for (const [k, v] of Object.entries(formData)) {
|
|
137
|
+
if (v === undefined || v === null)
|
|
138
|
+
continue;
|
|
139
|
+
fd.append(k, String(v));
|
|
140
|
+
}
|
|
141
|
+
for (const file of resolvedFiles) {
|
|
142
|
+
if (file.type === "fileUpload") {
|
|
143
|
+
const fu = file.value;
|
|
144
|
+
fd.append(file.key, fu.open(), {
|
|
145
|
+
filename: fu.effectiveFilename,
|
|
146
|
+
contentType: fu.effectiveMimeType,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (file.type === "path") {
|
|
150
|
+
fd.append(file.key, fs.createReadStream(file.value));
|
|
151
|
+
}
|
|
152
|
+
else if (file.type === "string") {
|
|
153
|
+
fd.append(file.key, file.value);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Stream - can only be used on first attempt
|
|
157
|
+
if (attempt > 0) {
|
|
158
|
+
throw new Error(`Cannot retry request with consumed stream for field "${file.key}". ` +
|
|
159
|
+
`Use FileUpload or file path instead for retry-safe uploads.`);
|
|
160
|
+
}
|
|
161
|
+
fd.append(file.key, file.value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const reqConfig = { ...config, headers: { ...(config.headers || {}), ...fd.getHeaders() } };
|
|
165
|
+
const res = await this.client.post(url, fd, reqConfig);
|
|
166
|
+
return this.handleResponse(res);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
lastError = err;
|
|
170
|
+
const status = axios.isAxiosError(err) ? err.response?.status : undefined;
|
|
171
|
+
const isNetworkError = axios.isAxiosError(err) && !err.response;
|
|
172
|
+
const retriableStatus = status !== undefined &&
|
|
173
|
+
(status >= 500 || status === 429 || status === 408);
|
|
174
|
+
const shouldRetry = (isNetworkError || retriableStatus) && attempt < this.maxRetries;
|
|
175
|
+
if (!shouldRetry) {
|
|
176
|
+
if (axios.isAxiosError(err) && err.response) {
|
|
177
|
+
const ExceptionClass = getExceptionForStatusCode(err.response.status);
|
|
178
|
+
const data = err.response.data;
|
|
179
|
+
const message = extractErrorMessage(data, err.response.status);
|
|
180
|
+
throw new ExceptionClass(message, err.response.status, data ?? null);
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
|
|
185
|
+
await sleep(backoffMs);
|
|
186
|
+
}
|
|
187
|
+
attempt += 1;
|
|
188
|
+
}
|
|
189
|
+
if (lastError)
|
|
190
|
+
throw lastError;
|
|
191
|
+
throw new Error("Request failed");
|
|
192
|
+
}
|
|
193
|
+
put(url, json, params) {
|
|
194
|
+
return this.execute(() => this.client.put(url, json ?? {}, { params }));
|
|
195
|
+
}
|
|
196
|
+
delete(url, params) {
|
|
197
|
+
return this.execute(() => this.client.delete(url, { params }));
|
|
198
|
+
}
|
|
199
|
+
async postWithDynamicInputs(url, inputs, params) {
|
|
200
|
+
const formData = {};
|
|
201
|
+
const files = {};
|
|
202
|
+
// Process inputs and detect file paths asynchronously
|
|
203
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
204
|
+
if (value instanceof FileUpload) {
|
|
205
|
+
files[key] = value;
|
|
206
|
+
}
|
|
207
|
+
else if (value && typeof value === "object" && typeof value.pipe === "function") {
|
|
208
|
+
// More robust stream detection: check if it's an object with a pipe function
|
|
209
|
+
files[key] = value;
|
|
210
|
+
}
|
|
211
|
+
else if (typeof value === "string") {
|
|
212
|
+
if (isUuidString(value)) {
|
|
213
|
+
formData[key] = value;
|
|
214
|
+
}
|
|
215
|
+
else if (await isFilePath(value)) {
|
|
216
|
+
// Use async file detection to avoid blocking the event loop
|
|
217
|
+
files[key] = value;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
formData[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (value !== undefined && value !== null) {
|
|
224
|
+
formData[key] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return this.post({ url, formData, files, params });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class PaginationHelper {
|
|
2
|
+
static buildQueryParams(organizationId, page, pageSize, extra = {}) {
|
|
3
|
+
const params = { organization_id: organizationId };
|
|
4
|
+
if (page !== undefined)
|
|
5
|
+
params.page = String(page);
|
|
6
|
+
if (pageSize !== undefined)
|
|
7
|
+
params.page_size = String(pageSize);
|
|
8
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
9
|
+
if (v !== undefined && v !== null)
|
|
10
|
+
params[k] = String(v);
|
|
11
|
+
}
|
|
12
|
+
return params;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roe-typescript",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the Roe AI API (feature parity with roe-python).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"lint": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run tests/unit --passWithNoTests",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"prepublishOnly": "npm run lint && npm run test && npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"axios": "^1.7.7",
|
|
23
|
+
"form-data": "^4.0.0",
|
|
24
|
+
"mime-types": "^2.1.35",
|
|
25
|
+
"uuid": "^11.0.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/form-data": "^2.5.0",
|
|
29
|
+
"@types/mime-types": "^2.1.4",
|
|
30
|
+
"@types/uuid": "^10.0.0",
|
|
31
|
+
"@types/node": "^22.9.0",
|
|
32
|
+
"typescript": "^5.6.3",
|
|
33
|
+
"vitest": "^1.6.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|