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.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +189 -0
  3. package/dist/api/agents.d.ts +87 -0
  4. package/dist/api/agents.js +263 -0
  5. package/dist/auth.d.ts +6 -0
  6. package/dist/auth.js +11 -0
  7. package/dist/client.d.ts +13 -0
  8. package/dist/client.js +18 -0
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.js +43 -0
  11. package/dist/exceptions.d.ts +25 -0
  12. package/dist/exceptions.js +73 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/index.js +9 -0
  15. package/dist/integration_test.js +686 -0
  16. package/dist/models/agent.d.ts +78 -0
  17. package/dist/models/agent.js +40 -0
  18. package/dist/models/file.d.ts +30 -0
  19. package/dist/models/file.js +85 -0
  20. package/dist/models/job.d.ts +37 -0
  21. package/dist/models/job.js +133 -0
  22. package/dist/models/responses.d.ts +71 -0
  23. package/dist/models/responses.js +36 -0
  24. package/dist/models/user.d.ts +6 -0
  25. package/dist/models/user.js +1 -0
  26. package/dist/src/api/agents.js +269 -0
  27. package/dist/src/auth.js +15 -0
  28. package/dist/src/client.js +22 -0
  29. package/dist/src/config.js +47 -0
  30. package/dist/src/exceptions.js +86 -0
  31. package/dist/src/models/agent.js +45 -0
  32. package/dist/src/models/file.js +92 -0
  33. package/dist/src/models/job.js +138 -0
  34. package/dist/src/models/responses.js +42 -0
  35. package/dist/src/models/user.js +2 -0
  36. package/dist/src/utils/fileDetection.js +46 -0
  37. package/dist/src/utils/httpClient.js +236 -0
  38. package/dist/src/utils/pagination.js +18 -0
  39. package/dist/utils/fileDetection.d.ts +11 -0
  40. package/dist/utils/fileDetection.js +38 -0
  41. package/dist/utils/httpClient.d.ts +30 -0
  42. package/dist/utils/httpClient.js +229 -0
  43. package/dist/utils/pagination.d.ts +3 -0
  44. package/dist/utils/pagination.js +14 -0
  45. 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,3 @@
1
+ export declare class PaginationHelper {
2
+ static buildQueryParams(organizationId: string, page?: number, pageSize?: number, extra?: Record<string, unknown>): Record<string, string>;
3
+ }
@@ -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
+