@eigenpal/sdk 0.5.4 → 0.5.6

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/CHANGELOG.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # @eigenpal/sdk
2
2
 
3
- ## 0.5.4
3
+ ## 0.5.6
4
4
 
5
5
  ### Major Changes
6
6
 
7
7
  - c15ce88: Rename agent API calls to `/v1/agents` and scope execution helpers under their owning workflow or agent.
8
8
 
9
+ ### Minor Changes
10
+
11
+ - d1d3260: `client.workflows.run` (and agent runs) now accept a Node readable stream (`fs.createReadStream('contract.pdf')`) as a file input, inferring the upload filename from the stream's path. Adds a `toFile(content, filename, mimeType?)` helper for attaching a filename to raw bytes (`Buffer`, `ArrayBuffer`, or `Blob`). Streams are drained to bytes before the request is sent, so a retried request can replay the body.
12
+
9
13
  ### Patch Changes
10
14
 
11
15
  - 5909b21: Add execution-scoped agent feedback filters and expected artifact management to the API, CLI, and SDKs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/sdk",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Official TypeScript SDK for the EigenPal API",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -32,7 +32,8 @@ export {
32
32
  EigenpalValidationError,
33
33
  } from './errors';
34
34
 
35
- export type { FileDescriptor, FileInput } from './lib/files';
35
+ export { toFile } from './lib/files';
36
+ export type { FileDescriptor, FileInput, NodeReadableStream } from './lib/files';
36
37
  export type {
37
38
  ListAgentExecutionsOptions,
38
39
  ListAgentsOptions,
package/src/lib/files.ts CHANGED
@@ -1,25 +1,49 @@
1
1
  /**
2
- * File-input helpers for `client.workflows.run()`.
2
+ * File-input helpers for `client.workflows.run()` (and agent runs).
3
3
  *
4
- * Pass a `File`, `Blob`, or explicit `{ content, filename, mimeType }` triple
5
- * as a workflow input value. The SDK auto-detects file values and uploads
6
- * them as `multipart/form-data` matching `curl -F`. No base64 needed.
4
+ * A workflow input value is treated as a file when it is one of:
5
+ * - a Node readable stream `fs.createReadStream('contract.pdf')`
6
+ * - a `File` or `Blob`
7
+ * - a `{ content, filename, mimeType? }` descriptor — build one with `toFile()`
8
+ *
9
+ * File values are auto-detected and uploaded as `multipart/form-data`
10
+ * (matching `curl -F`) — no base64 round-trip.
7
11
  */
8
12
 
13
+ const DEFAULT_MIME = 'application/octet-stream';
14
+
15
+ /** Common file extensions → MIME type, for upload filename inference. */
16
+ const MIME_BY_EXT: Record<string, string> = {
17
+ pdf: 'application/pdf',
18
+ png: 'image/png',
19
+ jpg: 'image/jpeg',
20
+ jpeg: 'image/jpeg',
21
+ gif: 'image/gif',
22
+ webp: 'image/webp',
23
+ txt: 'text/plain',
24
+ csv: 'text/csv',
25
+ json: 'application/json',
26
+ xml: 'application/xml',
27
+ html: 'text/html',
28
+ md: 'text/markdown',
29
+ doc: 'application/msword',
30
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
31
+ xls: 'application/vnd.ms-excel',
32
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
33
+ ppt: 'application/vnd.ms-powerpoint',
34
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
35
+ };
36
+
37
+ /** Guess a MIME type from a filename extension; falls back to octet-stream. */
38
+ function guessMimeType(filename: string): string {
39
+ const ext = filename.includes('.') ? filename.split('.').pop()!.toLowerCase() : '';
40
+ return MIME_BY_EXT[ext] ?? DEFAULT_MIME;
41
+ }
42
+
9
43
  /**
10
- * Explicit file descriptor — raw bytes plus metadata. Use when you have a
11
- * `Buffer` / `Uint8Array` / `ArrayBuffer` and want to set the filename /
12
- * mime type yourself (a bare `Blob` has no filename).
13
- *
14
- * ```ts
15
- * await client.workflows.run('extract-invoice', {
16
- * contract_document: {
17
- * content: buffer,
18
- * filename: 'contract.pdf',
19
- * mimeType: 'application/pdf',
20
- * },
21
- * });
22
- * ```
44
+ * Explicit file descriptor — raw bytes plus metadata. Build one with
45
+ * `toFile()` when you have a `Buffer` / `Uint8Array` / `ArrayBuffer` / `Blob`
46
+ * in memory rather than a path or a stream.
23
47
  */
24
48
  export interface FileDescriptor {
25
49
  content: ArrayBuffer | ArrayBufferView | Blob;
@@ -28,27 +52,63 @@ export interface FileDescriptor {
28
52
  }
29
53
 
30
54
  /**
31
- * Any value that the SDK accepts as a "file" workflow input.
55
+ * A Node readable stream typically `fs.createReadStream('contract.pdf')`.
56
+ * The SDK drains it and infers the upload filename from `path`.
32
57
  */
33
- export type FileInput = Blob | FileDescriptor;
58
+ export interface NodeReadableStream extends AsyncIterable<unknown> {
59
+ /** Source path; used to infer the upload filename. */
60
+ readonly path?: string;
61
+ }
34
62
 
35
- const DEFAULT_MIME = 'application/octet-stream';
63
+ /** Any value the SDK accepts as a "file" workflow input. */
64
+ export type FileInput = Blob | FileDescriptor | NodeReadableStream;
65
+
66
+ /**
67
+ * Attach a filename (and optional MIME type) to raw bytes. The escape hatch
68
+ * for when you have a `Buffer` / `ArrayBuffer` / `Blob` in memory rather than
69
+ * a file path or stream.
70
+ *
71
+ * ```ts
72
+ * await client.workflows.run('extract-invoice', {
73
+ * contract_document: toFile(buffer, 'contract.pdf'),
74
+ * });
75
+ * ```
76
+ */
77
+ export function toFile(
78
+ content: ArrayBuffer | ArrayBufferView | Blob,
79
+ filename: string,
80
+ mimeType?: string
81
+ ): FileDescriptor {
82
+ return { content, filename, mimeType: mimeType ?? guessMimeType(filename) };
83
+ }
84
+
85
+ /** Detect a Node readable stream (`fs.createReadStream`, etc.). */
86
+ function isReadStream(value: unknown): value is NodeReadableStream {
87
+ if (value === null || typeof value !== 'object') return false;
88
+ const v = value as Record<PropertyKey, unknown>;
89
+ return (
90
+ typeof v[Symbol.asyncIterator] === 'function' &&
91
+ (typeof v.pipe === 'function' || typeof v.read === 'function')
92
+ );
93
+ }
94
+
95
+ /** Detect an explicit `{ content, filename }` descriptor. */
96
+ function isFileDescriptor(value: unknown): value is FileDescriptor {
97
+ if (value === null || typeof value !== 'object') return false;
98
+ const v = value as { content?: unknown; filename?: unknown };
99
+ return (
100
+ typeof v.filename === 'string' &&
101
+ v.content != null &&
102
+ (v.content instanceof ArrayBuffer ||
103
+ ArrayBuffer.isView(v.content) ||
104
+ (typeof Blob !== 'undefined' && v.content instanceof Blob))
105
+ );
106
+ }
36
107
 
37
108
  export function isFileInput(value: unknown): value is FileInput {
38
109
  if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
39
- if (value !== null && typeof value === 'object') {
40
- const v = value as { content?: unknown; filename?: unknown };
41
- if (
42
- typeof v.filename === 'string' &&
43
- v.content != null &&
44
- (v.content instanceof ArrayBuffer ||
45
- ArrayBuffer.isView(v.content) ||
46
- (typeof Blob !== 'undefined' && v.content instanceof Blob))
47
- ) {
48
- return true;
49
- }
50
- }
51
- return false;
110
+ if (isReadStream(value)) return true;
111
+ return isFileDescriptor(value);
52
112
  }
53
113
 
54
114
  export function hasFileInput(input: Record<string, unknown> | undefined): boolean {
@@ -59,23 +119,45 @@ export function hasFileInput(input: Record<string, unknown> | undefined): boolea
59
119
  return false;
60
120
  }
61
121
 
62
- /** Convert a `FileInput` to `{ blob, filename }` for `FormData.append`. */
63
- function toBlobAndFilename(file: FileInput): { blob: Blob; filename: string } {
64
- // File extends Blob, so the Blob branch covers it.
122
+ /** Last path segment of a (possibly nested) file path. */
123
+ function basename(path: string): string {
124
+ const segments = path.split(/[\\/]/);
125
+ return segments[segments.length - 1] || 'file';
126
+ }
127
+
128
+ /**
129
+ * Resolve a `FileInput` to a `{ blob, filename }` pair for `FormData.append`.
130
+ *
131
+ * Streams are drained to bytes *here* — eagerly, before the request is sent —
132
+ * so the body can be replayed if the SDK retries the request. (A consumed
133
+ * stream cannot be re-read; a Blob can.)
134
+ */
135
+ async function resolveFileBlob(file: FileInput): Promise<{ blob: Blob; filename: string }> {
136
+ // `File` extends `Blob`, so this branch covers both.
65
137
  if (typeof Blob !== 'undefined' && file instanceof Blob) {
66
- const name = (file as File).name ?? 'file';
67
- return { blob: file, filename: name };
138
+ return { blob: file, filename: (file as File).name || 'file' };
139
+ }
140
+
141
+ // Node readable stream — drain it into a Blob now.
142
+ if (isReadStream(file)) {
143
+ const filename = typeof file.path === 'string' ? basename(file.path) : 'file';
144
+ const parts: BlobPart[] = [];
145
+ for await (const chunk of file) {
146
+ parts.push(chunk as BlobPart);
147
+ }
148
+ return { blob: new Blob(parts, { type: guessMimeType(filename) }), filename };
68
149
  }
150
+
151
+ // Explicit descriptor.
69
152
  const desc = file as FileDescriptor;
70
- const content = desc.content;
71
153
  const type = desc.mimeType ?? DEFAULT_MIME;
72
- let blob: Blob;
73
- if (content instanceof Blob) {
74
- blob = type && content.type !== type ? content.slice(0, content.size, type) : content;
75
- } else {
76
- // ArrayBuffer or ArrayBufferView — wrap in a Blob.
77
- blob = new Blob([content as BlobPart], { type });
154
+ if (!(desc.content instanceof Blob)) {
155
+ // ArrayBuffer / ArrayBufferView — wrap the bytes in a typed Blob.
156
+ return { blob: new Blob([desc.content as BlobPart], { type }), filename: desc.filename };
78
157
  }
158
+ // Already a Blob — reuse it, re-slicing only to apply a different MIME type.
159
+ const blob =
160
+ desc.content.type === type ? desc.content : desc.content.slice(0, desc.content.size, type);
79
161
  return { blob, filename: desc.filename };
80
162
  }
81
163
 
@@ -86,38 +168,46 @@ export interface MultipartParts {
86
168
  fileCount: number;
87
169
  }
88
170
 
171
+ /** Drain `input` into a FormData, appending each detected file as a field. */
172
+ async function appendFiles(
173
+ fd: FormData,
174
+ input: Record<string, unknown> | undefined
175
+ ): Promise<{ scalars: Record<string, unknown>; fileCount: number }> {
176
+ const scalars: Record<string, unknown> = {};
177
+ let fileCount = 0;
178
+ for (const [key, value] of Object.entries(input ?? {})) {
179
+ if (isFileInput(value)) {
180
+ const { blob, filename } = await resolveFileBlob(value);
181
+ fd.append(key, blob, filename);
182
+ fileCount += 1;
183
+ } else {
184
+ scalars[key] = value;
185
+ }
186
+ }
187
+ return { scalars, fileCount };
188
+ }
189
+
89
190
  /**
90
- * Build a `multipart/form-data` body that matches what
91
- * `processMultipartRunBody` on the server expects:
191
+ * Build a `multipart/form-data` body matching what `processMultipartRunBody`
192
+ * on the server expects:
92
193
  *
93
194
  * - Each file in `input` becomes a top-level form field (key = input name).
94
195
  * - Non-file inputs + overrides + trigger go in a `_json` text field.
95
196
  *
96
- * Only top-level file values are extracted. Files nested inside arrays /
97
- * objects keep their position in the JSON sidecar the server doesn't
98
- * support nested file uploads via multipart.
197
+ * Async because stream inputs are drained to bytes here. Only top-level file
198
+ * values are extracted files nested inside arrays / objects keep their
199
+ * position in the JSON sidecar (the server has no nested-upload path).
99
200
  */
100
- export function buildMultipart(args: {
201
+ export async function buildMultipart(args: {
101
202
  input?: Record<string, unknown>;
102
203
  overrides?: { steps?: Record<string, Record<string, unknown>> };
103
204
  trigger?: 'api' | 'cli';
104
- }): MultipartParts {
205
+ }): Promise<MultipartParts> {
105
206
  const fd = new FormData();
106
- const inputScalars: Record<string, unknown> = {};
107
- let fileCount = 0;
108
-
109
- for (const [key, value] of Object.entries(args.input ?? {})) {
110
- if (isFileInput(value)) {
111
- const { blob, filename } = toBlobAndFilename(value);
112
- fd.append(key, blob, filename);
113
- fileCount += 1;
114
- } else {
115
- inputScalars[key] = value;
116
- }
117
- }
207
+ const { scalars, fileCount } = await appendFiles(fd, args.input);
118
208
 
119
209
  const sidecar: Record<string, unknown> = {};
120
- if (Object.keys(inputScalars).length > 0) sidecar.input = inputScalars;
210
+ if (Object.keys(scalars).length > 0) sidecar.input = scalars;
121
211
  if (args.overrides) sidecar.overrides = args.overrides;
122
212
  if (args.trigger) sidecar.trigger = args.trigger;
123
213
  if (Object.keys(sidecar).length > 0) {
@@ -132,23 +222,14 @@ export function buildMultipart(args: {
132
222
  * input object itself, unlike workflow runs where `_json` is a sidecar with
133
223
  * `{ input, overrides, trigger }`.
134
224
  */
135
- export function buildAgentMultipart(input?: Record<string, unknown>): MultipartParts {
225
+ export async function buildAgentMultipart(
226
+ input?: Record<string, unknown>
227
+ ): Promise<MultipartParts> {
136
228
  const fd = new FormData();
137
- const inputScalars: Record<string, unknown> = {};
138
- let fileCount = 0;
139
-
140
- for (const [key, value] of Object.entries(input ?? {})) {
141
- if (isFileInput(value)) {
142
- const { blob, filename } = toBlobAndFilename(value);
143
- fd.append(key, blob, filename);
144
- fileCount += 1;
145
- } else {
146
- inputScalars[key] = value;
147
- }
148
- }
229
+ const { scalars, fileCount } = await appendFiles(fd, input);
149
230
 
150
- if (Object.keys(inputScalars).length > 0) {
151
- fd.append('_json', JSON.stringify(inputScalars));
231
+ if (Object.keys(scalars).length > 0) {
232
+ fd.append('_json', JSON.stringify(scalars));
152
233
  }
153
234
 
154
235
  return { formData: fd, fileCount };
@@ -409,7 +409,7 @@ export class AgentsResource {
409
409
  : {};
410
410
 
411
411
  if (hasFileInput(input)) {
412
- const { formData } = buildAgentMultipart(input);
412
+ const { formData } = await buildAgentMultipart(input);
413
413
  return this.dispatch<RunAgentResponse>(
414
414
  () =>
415
415
  this.client.post({
@@ -136,10 +136,14 @@ export class WorkflowExecutionsResource {
136
136
 
137
137
  // Trigger async — we don't ask the server to wait, since we're polling.
138
138
  const triggerQuery = options.version ? { version: options.version } : {};
139
- const runResult = hasFileInput(input)
140
- ? await this.dispatch<RunWorkflowResponse>(() => {
141
- const { formData } = buildMultipart({ input, overrides: options.overrides });
142
- return this.client.post({
139
+ let runResult: RunWorkflowResponse;
140
+ if (hasFileInput(input)) {
141
+ // Build the multipart body once, up front — `dispatch` may retry the
142
+ // POST, and a drained stream cannot be replayed.
143
+ const { formData } = await buildMultipart({ input, overrides: options.overrides });
144
+ runResult = await this.dispatch<RunWorkflowResponse>(
145
+ () =>
146
+ this.client.post({
143
147
  url: '/api/v1/workflows/{id}/run',
144
148
  path: { id: workflowId },
145
149
  query: triggerQuery,
@@ -147,20 +151,22 @@ export class WorkflowExecutionsResource {
147
151
  bodySerializer: null,
148
152
  headers: { 'Content-Type': null },
149
153
  signal: options.signal,
150
- }) as Promise<OperationResult<RunWorkflowResponse>>;
154
+ }) as Promise<OperationResult<RunWorkflowResponse>>
155
+ );
156
+ } else {
157
+ runResult = await this.dispatch<RunWorkflowResponse>(() =>
158
+ workflowsRun({
159
+ client: this.client,
160
+ path: { id: workflowId },
161
+ query: triggerQuery,
162
+ body: {
163
+ ...(input !== undefined ? { input } : {}),
164
+ ...(options.overrides ? { overrides: options.overrides } : {}),
165
+ },
166
+ signal: options.signal,
151
167
  })
152
- : await this.dispatch<RunWorkflowResponse>(() =>
153
- workflowsRun({
154
- client: this.client,
155
- path: { id: workflowId },
156
- query: triggerQuery,
157
- body: {
158
- ...(input !== undefined ? { input } : {}),
159
- ...(options.overrides ? { overrides: options.overrides } : {}),
160
- },
161
- signal: options.signal,
162
- })
163
- );
168
+ );
169
+ }
164
170
 
165
171
  const { executionId } = runResult;
166
172
 
@@ -96,7 +96,7 @@ export class WorkflowsResource {
96
96
 
97
97
  // File-bearing input → multipart/form-data (no base64 overhead).
98
98
  if (hasFileInput(input)) {
99
- const { formData } = buildMultipart({ input, overrides: options.overrides });
99
+ const { formData } = await buildMultipart({ input, overrides: options.overrides });
100
100
  return this.dispatch<RunWorkflowResponse>(
101
101
  () =>
102
102
  this.client.post({
package/src/telemetry.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  export const SDK_LANGUAGE = 'typescript';
20
20
  // Rewritten at publish time by scripts/render-sdk-versions.sh.
21
21
  // Keep this string literal exactly stable — sed matches on it.
22
- export const SDK_VERSION = '0.5.4';
22
+ export const SDK_VERSION = '0.5.6';
23
23
 
24
24
  function detectRuntime(): string {
25
25
  const g = globalThis as unknown as {