chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Build output
dist/
build/
# Coverage reports
coverage/
*.lcov
# Cache directories
.turbo/
.cache/
.parcel-cache/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# TypeScript
*.tsbuildinfo
# Logs
logs
*.log
+536
View File
@@ -0,0 +1,536 @@
# Sim TypeScript SDK
The official TypeScript/JavaScript SDK for [Sim](https://sim.ai), allowing you to execute workflows programmatically from your applications.
## Installation
```bash
npm install simstudio-ts-sdk
# or
yarn add simstudio-ts-sdk
# or
bun add simstudio-ts-sdk
```
## Quick Start
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
// Initialize the client
const client = new SimStudioClient({
apiKey: 'your-api-key-here',
baseUrl: 'https://sim.ai' // optional, defaults to https://sim.ai
});
// Execute a workflow
try {
const result = await client.executeWorkflow('workflow-id');
console.log('Workflow executed successfully:', result);
} catch (error) {
console.error('Workflow execution failed:', error);
}
```
## API Reference
### SimStudioClient
#### Constructor
```typescript
new SimStudioClient(config: SimStudioConfig)
```
- `config.apiKey` (string): Your Sim API key
- `config.baseUrl` (string, optional): Base URL for the Sim API (defaults to `https://sim.ai`)
#### Methods
##### executeWorkflow(workflowId, input?, options?)
Execute a workflow with optional input data.
```typescript
// With object input (spread at root level of request body)
const result = await client.executeWorkflow('workflow-id', {
message: 'Hello, world!'
});
// With primitive input (wrapped as { input: value })
const result = await client.executeWorkflow('workflow-id', 'NVDA');
// With options
const result = await client.executeWorkflow('workflow-id', { message: 'Hello' }, {
timeout: 60000
});
```
**Parameters:**
- `workflowId` (string): The ID of the workflow to execute
- `input` (any, optional): Input data to pass to the workflow. Objects are spread at the root level, primitives/arrays are wrapped in `{ input: value }`. File objects are automatically converted to base64.
- `options` (ExecutionOptions, optional):
- `timeout` (number): Timeout in milliseconds (default: 30000)
- `stream` (boolean): Enable streaming responses
- `selectedOutputs` (string[]): Block outputs to stream (e.g., `["agent1.content"]`)
- `async` (boolean): Execute asynchronously and return execution ID
**Returns:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
##### getWorkflowStatus(workflowId)
Get the status of a workflow (deployment status, etc.).
```typescript
const status = await client.getWorkflowStatus('workflow-id');
console.log('Is deployed:', status.isDeployed);
```
**Parameters:**
- `workflowId` (string): The ID of the workflow
**Returns:** `Promise<WorkflowStatus>`
##### validateWorkflow(workflowId)
Validate that a workflow is ready for execution.
```typescript
const isReady = await client.validateWorkflow('workflow-id');
if (isReady) {
// Workflow is deployed and ready
}
```
**Parameters:**
- `workflowId` (string): The ID of the workflow
**Returns:** `Promise<boolean>`
##### executeWorkflowSync(workflowId, input?, options?)
Execute a workflow and poll for completion (useful for long-running workflows).
```typescript
const result = await client.executeWorkflowSync('workflow-id', { data: 'some input' }, {
timeout: 60000
});
```
**Parameters:**
- `workflowId` (string): The ID of the workflow to execute
- `input` (any, optional): Input data to pass to the workflow
- `options` (ExecutionOptions, optional):
- `timeout` (number): Timeout for the initial request in milliseconds
**Returns:** `Promise<WorkflowExecutionResult>`
##### getJobStatus(jobId)
Get the status of an async job.
```typescript
const status = await client.getJobStatus('job-id-from-async-execution');
console.log('Job status:', status);
```
**Parameters:**
- `jobId` (string): The job ID returned from async execution
**Returns:** `Promise<any>`
##### executeWithRetry(workflowId, input?, options?, retryOptions?)
Execute a workflow with automatic retry on rate limit errors.
```typescript
const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
timeout: 30000
}, {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2
});
```
**Parameters:**
- `workflowId` (string): The ID of the workflow to execute
- `input` (any, optional): Input data to pass to the workflow
- `options` (ExecutionOptions, optional): Execution options
- `retryOptions` (RetryOptions, optional):
- `maxRetries` (number): Maximum retry attempts (default: 3)
- `initialDelay` (number): Initial delay in ms (default: 1000)
- `maxDelay` (number): Maximum delay in ms (default: 30000)
- `backoffMultiplier` (number): Backoff multiplier (default: 2)
**Returns:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
##### getRateLimitInfo()
Get current rate limit information from the last API response.
```typescript
const rateInfo = client.getRateLimitInfo();
if (rateInfo) {
console.log('Remaining requests:', rateInfo.remaining);
}
```
**Returns:** `RateLimitInfo | null`
##### getUsageLimits()
Get current usage limits and quota information.
```typescript
const limits = await client.getUsageLimits();
console.log('Current usage:', limits.usage);
```
**Returns:** `Promise<UsageLimits>`
##### setApiKey(apiKey)
Update the API key.
```typescript
client.setApiKey('new-api-key');
```
##### setBaseUrl(baseUrl)
Update the base URL.
```typescript
client.setBaseUrl('https://my-custom-domain.com');
```
## Types
### WorkflowExecutionResult
```typescript
interface WorkflowExecutionResult {
success: boolean;
output?: any;
error?: string;
logs?: any[];
metadata?: {
duration?: number;
executionId?: string;
[key: string]: any;
};
traceSpans?: any[];
totalDuration?: number;
}
```
### LargeValueRef
Oversized execution values may be returned as a versioned reference inside `output`, `logs`, streaming events, or async job status responses.
The `key` field is an opaque execution-scoped server storage pointer, not a client-readable download URL.
```typescript
interface LargeValueRef {
__simLargeValueRef: true;
version: 1;
id: string;
kind: 'array' | 'object' | 'string' | 'json';
size: number;
key?: string;
executionId?: string;
preview?: unknown;
}
```
### WorkflowStatus
```typescript
interface WorkflowStatus {
isDeployed: boolean;
deployedAt?: string;
needsRedeployment: boolean;
}
```
### SimStudioError
```typescript
class SimStudioError extends Error {
code?: string;
status?: number;
}
```
### AsyncExecutionResult
```typescript
interface AsyncExecutionResult {
success: boolean;
jobId: string;
statusUrl: string;
executionId?: string;
message: string;
async: true;
}
```
### RateLimitInfo
```typescript
interface RateLimitInfo {
limit: number;
remaining: number;
reset: number;
retryAfter?: number;
}
```
### UsageLimits
```typescript
interface UsageLimits {
success: boolean;
rateLimit: {
sync: {
isLimited: boolean;
limit: number;
remaining: number;
resetAt: string;
};
async: {
isLimited: boolean;
limit: number;
remaining: number;
resetAt: string;
};
authType: string;
};
usage: {
currentPeriodCost: number;
limit: number;
plan: string;
};
}
```
### ExecutionOptions
```typescript
interface ExecutionOptions {
timeout?: number;
stream?: boolean;
selectedOutputs?: string[];
async?: boolean;
}
```
### RetryOptions
```typescript
interface RetryOptions {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
backoffMultiplier?: number;
}
```
## Examples
### Basic Workflow Execution
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
async function runWorkflow() {
try {
// Check if workflow is ready
const isReady = await client.validateWorkflow('my-workflow-id');
if (!isReady) {
throw new Error('Workflow is not deployed or ready');
}
// Execute the workflow
const result = await client.executeWorkflow('my-workflow-id', {
message: 'Process this data',
userId: '12345'
});
if (result.success) {
console.log('Output:', result.output);
console.log('Duration:', result.metadata?.duration);
} else {
console.error('Workflow failed:', result.error);
}
} catch (error) {
console.error('Error:', error);
}
}
runWorkflow();
```
### Error Handling
```typescript
import { SimStudioClient, SimStudioError } from 'simstudio-ts-sdk';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
async function executeWithErrorHandling() {
try {
const result = await client.executeWorkflow('workflow-id');
return result;
} catch (error) {
if (error instanceof SimStudioError) {
switch (error.code) {
case 'UNAUTHORIZED':
console.error('Invalid API key');
break;
case 'TIMEOUT':
console.error('Workflow execution timed out');
break;
case 'USAGE_LIMIT_EXCEEDED':
console.error('Usage limit exceeded');
break;
case 'INVALID_JSON':
console.error('Invalid JSON in request body');
break;
default:
console.error('Workflow error:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
```
### Environment Configuration
```typescript
// Using environment variables
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!,
baseUrl: process.env.SIM_BASE_URL // optional
});
```
### File Upload
File objects are automatically detected and converted to base64 format. Include them in your input under the field name matching your workflow's API trigger input format:
The SDK converts File objects to this format:
```typescript
{
type: 'file',
data: 'data:mime/type;base64,base64data',
name: 'filename',
mime: 'mime/type'
}
```
Alternatively, you can manually provide files using the URL format:
```typescript
{
type: 'url',
data: 'https://example.com/file.pdf',
name: 'file.pdf',
mime: 'application/pdf'
}
```
```typescript
import { SimStudioClient } from 'simstudio-ts-sdk';
import fs from 'fs';
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!
});
// Node.js: Read file and create File object
const fileBuffer = fs.readFileSync('./document.pdf');
const file = new File([fileBuffer], 'document.pdf', { type: 'application/pdf' });
// Include files under the field name from your API trigger's input format
const result = await client.executeWorkflow('workflow-id', {
documents: [file], // Field name must match your API trigger's file input field
instructions: 'Process this document'
});
// Browser: From file input
const handleFileUpload = async (event: Event) => {
const inputEl = event.target as HTMLInputElement;
const files = Array.from(inputEl.files || []);
const result = await client.executeWorkflow('workflow-id', {
attachments: files, // Field name must match your API trigger's file input field
query: 'Analyze these files'
});
};
```
## Getting Your API Key
1. Log in to your [Sim](https://sim.ai) account
2. Navigate to your workflow
3. Click on "Deploy" to deploy your workflow
4. Select or create an API key during the deployment process
5. Copy the API key to use in your application
## Development
### Running Tests
To run the tests locally:
1. Clone the repository and navigate to the TypeScript SDK directory:
```bash
cd packages/ts-sdk
```
2. Install dependencies:
```bash
bun install
```
3. Run the tests:
```bash
bun run test
```
### Building
Build the TypeScript SDK:
```bash
bun run build
```
This will compile TypeScript files to JavaScript and generate type declarations in the `dist/` directory.
### Development Mode
For development with auto-rebuild:
```bash
bun run dev
```
## Requirements
- Node.js 18+
- TypeScript 5.0+ (for TypeScript projects)
## License
Apache-2.0
+164
View File
@@ -0,0 +1,164 @@
import { SimStudioClient, SimStudioError } from '../src/index'
// Example 1: Basic workflow execution
async function basicExample() {
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!,
baseUrl: 'https://sim.ai',
})
try {
// Execute a workflow without input
const result = await client.executeWorkflow('your-workflow-id')
if (result.success) {
console.log('✅ Workflow executed successfully!')
console.log('Output:', result.output)
console.log('Duration:', result.metadata?.duration, 'ms')
} else {
console.log('❌ Workflow failed:', result.error)
}
} catch (error) {
if (error instanceof SimStudioError) {
console.error('SDK Error:', error.message, 'Code:', error.code)
} else {
console.error('Unexpected error:', error)
}
}
}
// Example 2: Workflow execution with input data
async function withInputExample() {
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!,
})
try {
const result = await client.executeWorkflow('your-workflow-id', {
input: {
message: 'Hello from SDK!',
userId: '12345',
data: {
type: 'analysis',
parameters: {
includeMetadata: true,
format: 'json',
},
},
},
timeout: 60000, // 60 seconds
})
if (result.success) {
console.log('✅ Workflow executed successfully!')
console.log('Output:', result.output)
if (result.metadata?.duration) {
console.log('Duration:', result.metadata.duration, 'ms')
}
} else {
console.log('❌ Workflow failed:', result.error)
}
} catch (error) {
if (error instanceof SimStudioError) {
console.error('SDK Error:', error.message, 'Code:', error.code)
} else {
console.error('Unexpected error:', error)
}
}
}
// Example 3: Workflow validation and status checking
async function statusExample() {
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!,
})
try {
// Check if workflow is ready
const isReady = await client.validateWorkflow('your-workflow-id')
console.log('Workflow ready:', isReady)
// Get detailed status
const status = await client.getWorkflowStatus('your-workflow-id')
console.log('Status:', {
deployed: status.isDeployed,
needsRedeployment: status.needsRedeployment,
deployedAt: status.deployedAt,
})
if (status.isDeployed) {
// Execute the workflow
const result = await client.executeWorkflow('your-workflow-id')
if (result.success) {
console.log('✅ Workflow executed successfully!')
console.log('Output:', result.output)
} else {
console.log('❌ Workflow failed:', result.error)
}
}
} catch (error) {
if (error instanceof SimStudioError) {
console.error('SDK Error:', error.message, 'Code:', error.code)
} else {
console.error('Unexpected error:', error)
}
}
}
// Example 4: Workflow execution with streaming
async function streamingExample() {
const client = new SimStudioClient({
apiKey: process.env.SIM_API_KEY!,
})
try {
const result = await client.executeWorkflow('your-workflow-id', {
input: {
message: 'Count to five',
},
stream: true,
selectedOutputs: ['agent1.content'], // Use blockName.attribute format
timeout: 60000,
})
if (result.success) {
console.log('✅ Workflow executed successfully!')
console.log('Output:', result.output)
console.log('Duration:', result.metadata?.duration, 'ms')
} else {
console.log('❌ Workflow failed:', result.error)
}
} catch (error) {
if (error instanceof SimStudioError) {
console.error('SDK Error:', error.message, 'Code:', error.code)
} else {
console.error('Unexpected error:', error)
}
}
}
// Run examples
if (require.main === module) {
async function runExamples() {
console.log('🚀 Running Sim SDK Examples\n')
try {
await basicExample()
console.log('\n✅ Basic example completed')
await withInputExample()
console.log('\n✅ Input example completed')
await statusExample()
console.log('\n✅ Status example completed')
await streamingExample()
console.log('\n✅ Streaming example completed')
} catch (error) {
console.error('Error running examples:', error)
}
}
runExamples()
}
+63
View File
@@ -0,0 +1,63 @@
{
"name": "simstudio-ts-sdk",
"version": "0.1.2",
"description": "Sim SDK - Execute workflows programmatically",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"type-check": "tsc --noEmit",
"lint": "biome check --write --unsafe .",
"lint:check": "biome check .",
"format": "biome format --write .",
"format:check": "biome format .",
"dev:watch": "tsc --watch",
"prepublishOnly": "bun run build",
"test": "vitest run",
"test:watch": "vitest"
},
"files": [
"dist"
],
"keywords": [
"simstudio",
"ai",
"workflow",
"sdk",
"api",
"automation",
"typescript"
],
"author": "Sim",
"license": "Apache-2.0",
"dependencies": {
"node-fetch": "^3.3.2"
},
"devDependencies": {
"@sim/tsconfig": "workspace:*",
"@types/node": "^20.5.1",
"@vitest/coverage-v8": "^4.1.0",
"typescript": "^7.0.2",
"vitest": "^4.1.0"
},
"engines": {
"node": ">=16"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/simstudioai/sim.git",
"directory": "packages/ts-sdk"
},
"homepage": "https://sim.ai",
"bugs": {
"url": "https://github.com/simstudioai/sim/issues"
}
}
+658
View File
@@ -0,0 +1,658 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SimStudioClient, SimStudioError } from './index'
vi.mock('node-fetch', () => ({
default: vi.fn(),
}))
describe('SimStudioClient', () => {
let client: SimStudioClient
beforeEach(() => {
client = new SimStudioClient({
apiKey: 'test-api-key',
baseUrl: 'https://test.sim.ai',
})
vi.clearAllMocks()
})
describe('constructor', () => {
it('should create a client with correct configuration', () => {
expect(client).toBeInstanceOf(SimStudioClient)
})
it('should use default base URL when not provided', () => {
const defaultClient = new SimStudioClient({
apiKey: 'test-api-key',
})
expect(defaultClient).toBeInstanceOf(SimStudioClient)
})
})
describe('setApiKey', () => {
it('should update the API key', () => {
const newApiKey = 'new-api-key'
client.setApiKey(newApiKey)
// Verify the method exists
expect(client.setApiKey).toBeDefined()
// Verify the API key was actually updated
expect((client as any).apiKey).toBe(newApiKey)
})
})
describe('setBaseUrl', () => {
it('should update the base URL', () => {
const newBaseUrl = 'https://new.sim.ai'
client.setBaseUrl(newBaseUrl)
expect((client as any).baseUrl).toBe(newBaseUrl)
})
it('should strip trailing slash from base URL', () => {
const urlWithSlash = 'https://test.sim.ai/'
client.setBaseUrl(urlWithSlash)
// Verify the trailing slash was actually stripped
expect((client as any).baseUrl).toBe('https://test.sim.ai')
})
})
describe('validateWorkflow', () => {
it('should return false when workflow status request fails', async () => {
const fetch = await import('node-fetch')
vi.mocked(fetch.default).mockRejectedValue(new Error('Network error'))
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(false)
})
it('should return true when workflow is deployed', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
isDeployed: true,
deployedAt: '2023-01-01T00:00:00Z',
needsRedeployment: false,
}),
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(true)
})
it('should return false when workflow is not deployed', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
isDeployed: false,
deployedAt: null,
needsRedeployment: true,
}),
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.validateWorkflow('test-workflow-id')
expect(result).toBe(false)
})
})
describe('executeWorkflow - async execution', () => {
it('should return AsyncExecutionResult when async is true', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 202,
json: vi.fn().mockResolvedValue({
success: true,
jobId: 'job-123',
statusUrl: 'https://test.sim.ai/api/jobs/job-123',
message: 'Workflow execution queued',
async: true,
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.executeWorkflow(
'workflow-id',
{ message: 'Hello' },
{ async: true }
)
expect(result).toHaveProperty('jobId', 'job-123')
expect(result).toHaveProperty('statusUrl', 'https://test.sim.ai/api/jobs/job-123')
expect(result).toHaveProperty('async', true)
// Verify headers were set correctly
const calls = vi.mocked(fetch.default).mock.calls
expect(calls[0][1]?.headers).toMatchObject({
'X-Execution-Mode': 'async',
})
})
it('should return WorkflowExecutionResult when async is false', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: { result: 'completed' },
logs: [],
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.executeWorkflow(
'workflow-id',
{ message: 'Hello' },
{ async: false }
)
expect(result).toHaveProperty('success', true)
expect(result).toHaveProperty('output')
expect(result).not.toHaveProperty('jobId')
})
it('should not set X-Execution-Mode header when async is undefined', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', { message: 'Hello' })
const calls = vi.mocked(fetch.default).mock.calls
expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode')
})
})
describe('getJobStatus', () => {
it('should fetch job status with correct endpoint', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
success: true,
taskId: 'task-123',
status: 'completed',
metadata: {
startedAt: '2024-01-01T00:00:00Z',
completedAt: '2024-01-01T00:01:00Z',
duration: 60000,
},
output: { result: 'done' },
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.getJobStatus('task-123')
expect(result).toHaveProperty('taskId', 'task-123')
expect(result).toHaveProperty('status', 'completed')
expect(result).toHaveProperty('output')
// Verify correct endpoint was called
const calls = vi.mocked(fetch.default).mock.calls
expect(calls[0][0]).toBe('https://test.sim.ai/api/jobs/task-123')
})
it('should handle job not found error', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
json: vi.fn().mockResolvedValue({
error: 'Job not found',
code: 'JOB_NOT_FOUND',
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await expect(client.getJobStatus('invalid-task')).rejects.toThrow(SimStudioError)
await expect(client.getJobStatus('invalid-task')).rejects.toThrow('Job not found')
})
})
describe('executeWithRetry', () => {
it('should succeed on first attempt when no rate limit', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: { result: 'success' },
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.executeWithRetry('workflow-id', { message: 'test' })
expect(result).toHaveProperty('success', true)
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1)
})
it('should retry on rate limit error', async () => {
const fetch = await import('node-fetch')
// First call returns 429, second call succeeds
const rateLimitResponse = {
ok: false,
status: 429,
statusText: 'Too Many Requests',
json: vi.fn().mockResolvedValue({
error: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED',
}),
headers: {
get: vi.fn((header: string) => {
if (header === 'retry-after') return '1'
if (header === 'x-ratelimit-limit') return '100'
if (header === 'x-ratelimit-remaining') return '0'
if (header === 'x-ratelimit-reset') return String(Math.floor(Date.now() / 1000) + 60)
return null
}),
},
}
const successResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: { result: 'success' },
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default)
.mockResolvedValueOnce(rateLimitResponse as any)
.mockResolvedValueOnce(successResponse as any)
const result = await client.executeWithRetry(
'workflow-id',
{ message: 'test' },
{},
{ maxRetries: 3, initialDelay: 10 }
)
expect(result).toHaveProperty('success', true)
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(2)
})
it('should throw after max retries exceeded', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 429,
statusText: 'Too Many Requests',
json: vi.fn().mockResolvedValue({
error: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED',
}),
headers: {
get: vi.fn((header: string) => {
if (header === 'retry-after') return '1'
return null
}),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await expect(
client.executeWithRetry(
'workflow-id',
{ message: 'test' },
{},
{ maxRetries: 2, initialDelay: 10 }
)
).rejects.toThrow('Rate limit exceeded')
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(3) // Initial + 2 retries
})
it('should not retry on non-rate-limit errors', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: vi.fn().mockResolvedValue({
error: 'Server error',
code: 'INTERNAL_ERROR',
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await expect(client.executeWithRetry('workflow-id', { message: 'test' })).rejects.toThrow(
'Server error'
)
expect(vi.mocked(fetch.default)).toHaveBeenCalledTimes(1) // No retries
})
})
describe('getRateLimitInfo', () => {
it('should return null when no rate limit info available', () => {
const info = client.getRateLimitInfo()
expect(info).toBeNull()
})
it('should return rate limit info after API call', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({ success: true, output: {} }),
headers: {
get: vi.fn((header: string) => {
if (header === 'x-ratelimit-limit') return '100'
if (header === 'x-ratelimit-remaining') return '95'
if (header === 'x-ratelimit-reset') return '1704067200'
return null
}),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', {})
const info = client.getRateLimitInfo()
expect(info).not.toBeNull()
expect(info?.limit).toBe(100)
expect(info?.remaining).toBe(95)
expect(info?.reset).toBe(1704067200)
})
})
describe('getUsageLimits', () => {
it('should fetch usage limits with correct structure', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
success: true,
rateLimit: {
sync: {
isLimited: false,
requestsPerMinute: 100,
maxBurst: 200,
remaining: 95,
resetAt: '2024-01-01T01:00:00Z',
},
async: {
isLimited: false,
requestsPerMinute: 50,
maxBurst: 100,
remaining: 48,
resetAt: '2024-01-01T01:00:00Z',
},
authType: 'api',
},
usage: {
currentPeriodCost: 1.23,
limit: 100.0,
plan: 'pro',
},
storage: {
usedBytes: 1024,
limitBytes: 10240,
percentUsed: 10,
},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
const result = await client.getUsageLimits()
expect(result.success).toBe(true)
expect(result.rateLimit.sync.requestsPerMinute).toBe(100)
expect(result.rateLimit.sync.maxBurst).toBe(200)
expect(result.rateLimit.async.requestsPerMinute).toBe(50)
expect(result.usage.currentPeriodCost).toBe(1.23)
expect(result.usage.plan).toBe('pro')
expect(result.storage.usedBytes).toBe(1024)
expect(result.storage.percentUsed).toBe(10)
// Verify correct endpoint was called
const calls = vi.mocked(fetch.default).mock.calls
expect(calls[0][0]).toBe('https://test.sim.ai/api/users/me/usage-limits')
})
it('should handle unauthorized error', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: false,
status: 401,
statusText: 'Unauthorized',
json: vi.fn().mockResolvedValue({
error: 'Invalid API key',
code: 'UNAUTHORIZED',
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await expect(client.getUsageLimits()).rejects.toThrow(SimStudioError)
await expect(client.getUsageLimits()).rejects.toThrow('Invalid API key')
})
})
describe('executeWorkflow - streaming with selectedOutputs', () => {
it('should include stream and selectedOutputs in request body', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow(
'workflow-id',
{ message: 'test' },
{ stream: true, selectedOutputs: ['agent1.content', 'agent2.content'] }
)
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('message', 'test')
expect(requestBody).toHaveProperty('stream', true)
expect(requestBody).toHaveProperty('selectedOutputs')
expect(requestBody.selectedOutputs).toEqual(['agent1.content', 'agent2.content'])
})
})
describe('executeWorkflow - primitive and array inputs', () => {
it('should wrap primitive string input in input field', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', 'NVDA')
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input', 'NVDA')
expect(requestBody).not.toHaveProperty('0') // Should not spread string characters
})
it('should wrap primitive number input in input field', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', 42)
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input', 42)
})
it('should wrap array input in input field', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', ['NVDA', 'AAPL', 'GOOG'])
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('input')
expect(requestBody.input).toEqual(['NVDA', 'AAPL', 'GOOG'])
expect(requestBody).not.toHaveProperty('0') // Should not spread array
})
it('should spread object input at root level', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', { ticker: 'NVDA', quantity: 100 })
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
expect(requestBody).toHaveProperty('ticker', 'NVDA')
expect(requestBody).toHaveProperty('quantity', 100)
expect(requestBody).not.toHaveProperty('input') // Should not wrap in input field
})
it('should handle null input as no input (empty body)', async () => {
const fetch = await import('node-fetch')
const mockResponse = {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
success: true,
output: {},
}),
headers: {
get: vi.fn().mockReturnValue(null),
},
}
vi.mocked(fetch.default).mockResolvedValue(mockResponse as any)
await client.executeWorkflow('workflow-id', null)
const calls = vi.mocked(fetch.default).mock.calls
const requestBody = JSON.parse(calls[0][1]?.body as string)
// null treated as "no input" - sends empty body (consistent with Python SDK)
expect(requestBody).toEqual({})
})
})
})
describe('SimStudioError', () => {
it('should create error with message', () => {
const error = new SimStudioError('Test error')
expect(error.message).toBe('Test error')
expect(error.name).toBe('SimStudioError')
})
it('should create error with code and status', () => {
const error = new SimStudioError('Test error', 'TEST_CODE', 400)
expect(error.message).toBe('Test error')
expect(error.code).toBe('TEST_CODE')
expect(error.status).toBe(400)
})
})
+519
View File
@@ -0,0 +1,519 @@
import fetch from 'node-fetch'
export interface SimStudioConfig {
apiKey: string
baseUrl?: string
}
export interface LargeValueRef {
__simLargeValueRef: true
version: 1
id: string
kind: 'array' | 'object' | 'string' | 'json'
size: number
/** Opaque execution-scoped server storage key. This is not a download URL. */
key?: string
executionId?: string
preview?: unknown
}
export interface WorkflowExecutionResult {
success: boolean
output?: any
error?: string
logs?: any[]
metadata?: {
duration?: number
executionId?: string
[key: string]: any
}
traceSpans?: any[]
totalDuration?: number
}
export interface WorkflowStatus {
isDeployed: boolean
isPublished?: boolean
deployedAt?: string
needsRedeployment: boolean
}
export interface ExecutionOptions {
timeout?: number
stream?: boolean
selectedOutputs?: string[]
async?: boolean
}
export interface AsyncExecutionResult {
success: boolean
jobId: string
statusUrl: string
executionId?: string
message: string
async: true
}
export interface JobStatusResult {
taskId: string
status: string
metadata?: Record<string, unknown>
output?: unknown
error?: string
}
export interface RateLimitInfo {
limit: number
remaining: number
reset: number
retryAfter?: number
}
export interface RetryOptions {
maxRetries?: number
initialDelay?: number
maxDelay?: number
backoffMultiplier?: number
}
export interface UsageLimits {
success: boolean
rateLimit: {
sync: {
isLimited: boolean
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
async: {
isLimited: boolean
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
authType: string
}
usage: {
currentPeriodCost: number
limit: number
plan: string
}
storage: {
usedBytes: number
limitBytes: number
percentUsed: number
}
}
export class SimStudioError extends Error {
public code?: string
public status?: number
constructor(message: string, code?: string, status?: number) {
super(message)
this.name = 'SimStudioError'
this.code = code
this.status = status
}
}
/**
* Remove trailing slashes from a URL
* Uses string operations instead of regex to prevent ReDoS attacks
* @param url - The URL to normalize
* @returns URL without trailing slashes
*/
function normalizeBaseUrl(url: string): string {
let normalized = url
while (normalized.endsWith('/')) {
normalized = normalized.slice(0, -1)
}
return normalized
}
export class SimStudioClient {
private apiKey: string
private baseUrl: string
private rateLimitInfo: RateLimitInfo | null = null
constructor(config: SimStudioConfig) {
this.apiKey = config.apiKey
this.baseUrl = normalizeBaseUrl(config.baseUrl || 'https://sim.ai')
}
/**
* Convert File objects in input to API format (base64)
* Recursively processes nested objects and arrays
*/
private async convertFilesToBase64(
value: any,
visited: WeakSet<object> = new WeakSet()
): Promise<any> {
if (typeof File !== 'undefined' && value instanceof File) {
const arrayBuffer = await value.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64 = buffer.toString('base64')
return {
type: 'file',
data: `data:${value.type || 'application/octet-stream'};base64,${base64}`,
name: value.name,
mime: value.type || 'application/octet-stream',
}
}
if (Array.isArray(value)) {
if (visited.has(value)) {
return '[Circular]'
}
visited.add(value)
const result = await Promise.all(
value.map((item) => this.convertFilesToBase64(item, visited))
)
visited.delete(value)
return result
}
if (value !== null && typeof value === 'object') {
if (visited.has(value)) {
return '[Circular]'
}
visited.add(value)
const converted: any = {}
for (const [key, val] of Object.entries(value)) {
converted[key] = await this.convertFilesToBase64(val, visited)
}
visited.delete(value)
return converted
}
return value
}
/**
* Execute a workflow with optional input data
* @param workflowId - The ID of the workflow to execute
* @param input - Input data to pass to the workflow (object, primitive, or array)
* @param options - Execution options (timeout, stream, async, etc.)
*/
async executeWorkflow(
workflowId: string,
input?: any,
options: ExecutionOptions = {}
): Promise<WorkflowExecutionResult | AsyncExecutionResult> {
const url = `${this.baseUrl}/api/workflows/${workflowId}/execute`
const { timeout = 30000, stream, selectedOutputs, async } = options
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('TIMEOUT')), timeout)
})
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey,
}
if (async) {
headers['X-Execution-Mode'] = 'async'
}
let jsonBody: any = {}
if (input !== undefined && input !== null) {
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
jsonBody = { ...input }
} else {
jsonBody = { input }
}
}
jsonBody = await this.convertFilesToBase64(jsonBody)
if (stream !== undefined) {
jsonBody.stream = stream
}
if (selectedOutputs !== undefined) {
jsonBody.selectedOutputs = selectedOutputs
}
const fetchPromise = fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(jsonBody),
})
const response = await Promise.race([fetchPromise, timeoutPromise])
this.updateRateLimitInfo(response)
if (response.status === 429) {
const retryAfter = this.rateLimitInfo?.retryAfter || 1000
throw new SimStudioError(
`Rate limit exceeded. Retry after ${retryAfter}ms`,
'RATE_LIMIT_EXCEEDED',
429
)
}
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as unknown as any
throw new SimStudioError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
errorData.code,
response.status
)
}
const result = await response.json()
return result as WorkflowExecutionResult | AsyncExecutionResult
} catch (error: any) {
if (error instanceof SimStudioError) {
throw error
}
if (error.message === 'TIMEOUT') {
throw new SimStudioError(`Workflow execution timed out after ${timeout}ms`, 'TIMEOUT')
}
throw new SimStudioError(error?.message || 'Failed to execute workflow', 'EXECUTION_ERROR')
}
}
/**
* Get the status of a workflow (deployment status, etc.)
*/
async getWorkflowStatus(workflowId: string): Promise<WorkflowStatus> {
const url = `${this.baseUrl}/api/workflows/${workflowId}/status`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': this.apiKey,
},
})
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as unknown as any
throw new SimStudioError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
errorData.code,
response.status
)
}
const result = await response.json()
return result as WorkflowStatus
} catch (error: any) {
if (error instanceof SimStudioError) {
throw error
}
throw new SimStudioError(error?.message || 'Failed to get workflow status', 'STATUS_ERROR')
}
}
/**
* Execute a workflow synchronously (ensures non-async mode)
* @param workflowId - The ID of the workflow to execute
* @param input - Input data to pass to the workflow
* @param options - Execution options (timeout, stream, etc.)
*/
async executeWorkflowSync(
workflowId: string,
input?: any,
options: ExecutionOptions = {}
): Promise<WorkflowExecutionResult> {
const syncOptions = { ...options, async: false }
return this.executeWorkflow(workflowId, input, syncOptions) as Promise<WorkflowExecutionResult>
}
/**
* Validate that a workflow is ready for execution
*/
async validateWorkflow(workflowId: string): Promise<boolean> {
try {
const status = await this.getWorkflowStatus(workflowId)
return status.isDeployed
} catch (error) {
return false
}
}
/**
* Set a new API key
*/
setApiKey(apiKey: string): void {
this.apiKey = apiKey
}
/**
* Set a new base URL
*/
setBaseUrl(baseUrl: string): void {
this.baseUrl = normalizeBaseUrl(baseUrl)
}
/**
* Get the status of an async job
* @param taskId The job ID returned from async execution
*/
async getJobStatus(taskId: string): Promise<JobStatusResult> {
const url = `${this.baseUrl}/api/jobs/${taskId}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': this.apiKey,
},
})
this.updateRateLimitInfo(response)
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as unknown as any
throw new SimStudioError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
errorData.code,
response.status
)
}
const result = await response.json()
return result as JobStatusResult
} catch (error: any) {
if (error instanceof SimStudioError) {
throw error
}
throw new SimStudioError(error?.message || 'Failed to get job status', 'STATUS_ERROR')
}
}
/**
* Execute workflow with automatic retry on rate limit
* @param workflowId - The ID of the workflow to execute
* @param input - Input data to pass to the workflow
* @param options - Execution options (timeout, stream, async, etc.)
* @param retryOptions - Retry configuration (maxRetries, delays, etc.)
*/
async executeWithRetry(
workflowId: string,
input?: any,
options: ExecutionOptions = {},
retryOptions: RetryOptions = {}
): Promise<WorkflowExecutionResult | AsyncExecutionResult> {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffMultiplier = 2,
} = retryOptions
let lastError: SimStudioError | null = null
let delay = initialDelay
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.executeWorkflow(workflowId, input, options)
} catch (error: any) {
if (!(error instanceof SimStudioError) || error.code !== 'RATE_LIMIT_EXCEEDED') {
throw error
}
lastError = error
if (attempt === maxRetries) {
break
}
const waitTime =
error.status === 429 && this.rateLimitInfo?.retryAfter
? this.rateLimitInfo.retryAfter
: Math.min(delay, maxDelay)
// standalone package — cannot depend on @sim/utils
const jitter = waitTime * (0.75 + Math.random() * 0.5)
await new Promise((resolve) => setTimeout(resolve, jitter))
delay *= backoffMultiplier
}
}
throw lastError || new SimStudioError('Max retries exceeded', 'MAX_RETRIES_EXCEEDED')
}
/**
* Get current rate limit information
*/
getRateLimitInfo(): RateLimitInfo | null {
return this.rateLimitInfo
}
/**
* Update rate limit info from response headers
* @private
*/
private updateRateLimitInfo(response: any): void {
const limit = response.headers.get('x-ratelimit-limit')
const remaining = response.headers.get('x-ratelimit-remaining')
const reset = response.headers.get('x-ratelimit-reset')
const retryAfter = response.headers.get('retry-after')
const resetTime = reset
? /^\d+$/.test(reset)
? Number.parseInt(reset, 10)
: Date.parse(reset)
: Number.NaN
if (limit || remaining || reset) {
this.rateLimitInfo = {
limit: limit ? Number.parseInt(limit, 10) : 0,
remaining: remaining ? Number.parseInt(remaining, 10) : 0,
reset: Number.isNaN(resetTime) ? 0 : resetTime,
retryAfter: retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined,
}
}
}
/**
* Get current usage limits and quota information
*/
async getUsageLimits(): Promise<UsageLimits> {
const url = `${this.baseUrl}/api/users/me/usage-limits`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': this.apiKey,
},
})
this.updateRateLimitInfo(response)
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as unknown as any
throw new SimStudioError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
errorData.code,
response.status
)
}
const result = await response.json()
return result as UsageLimits
} catch (error: any) {
if (error instanceof SimStudioError) {
throw error
}
throw new SimStudioError(error?.message || 'Failed to get usage limits', 'USAGE_ERROR')
}
}
}
export { SimStudioClient as default }
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "@sim/tsconfig/library-build.json",
"compilerOptions": {
"target": "ES2020",
"module": "nodenext",
"lib": ["ES2020"],
"moduleResolution": "nodenext",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
exclude: ['**/node_modules/**', '**/dist/**'],
},
resolve: {
conditions: ['node', 'default'],
},
})