feat/codegen: added initial outline for sdk, added api keys table & view (#126)

* feat[sdk]: added scaffolding for sdk pkg

* feat[sdk]: simplified usage of tools for agent block

* fix[sdk]: added api key directly into AgentOptions

* fix[sdk]: simplified the conditional block by removing the code input since we don't need it

* feat[sdk]: added account-level api keys & db table to manage them
This commit is contained in:
Waleed Latif
2025-03-18 17:49:50 -07:00
committed by GitHub
parent ed5f8a93dd
commit eb5d01956e
43 changed files with 10263 additions and 7 deletions
+223
View File
@@ -0,0 +1,223 @@
# Sim Studio SDK
The Sim Studio SDK allows developers to create, manage, and deploy workflows programmatically.
## Installation
```bash
npm install @sim-studio/sdk
```
## Usage
### Creating a Simple Workflow
```typescript
import { SimStudio, AgentBlock, FunctionBlock } from '@sim-studio/sdk';
// Initialize the SDK
const simStudio = new SimStudio({
apiKey: 'your-api-key',
});
// Create a workflow
const workflow = simStudio.createWorkflow(
'Simple Workflow',
'A simple workflow that uses an agent to generate content'
);
// Create an agent block
const agentBlock = new AgentBlock({
model: 'claude-3-7-sonnet',
prompt: 'Write a blog post about <input.topic>',
systemPrompt: 'You are a professional content writer.',
});
// Create a function block to format the content
const formatterBlock = new FunctionBlock({
code: `
function formatContent(input) {
return {
title: "Blog: " + input.topic,
content: input.content,
wordCount: input.content.split(' ').length
};
}
`
});
// Add blocks to the workflow
workflow.addBlock(agentBlock);
workflow.addBlock(formatterBlock);
// Connect blocks
const starterBlock = workflow.getStarterBlock();
workflow.connect(starterBlock.id, agentBlock.id);
workflow.connect(agentBlock.id, formatterBlock.id);
// Build and save the workflow
const builtWorkflow = workflow.build();
simStudio.saveWorkflow(builtWorkflow)
.then(savedWorkflow => {
console.log(`Workflow saved with ID: ${savedWorkflow.id}`);
});
```
### Using Tools with Agents
Agent blocks can use tools for enhanced capabilities. You can simply reference built-in tools by ID or define custom tools:
```typescript
import { SimStudio, AgentBlock } from '@sim-studio/sdk';
// Initialize the SDK
const simStudio = new SimStudio({
apiKey: 'your-api-key',
});
// Create a workflow
const workflow = simStudio.createWorkflow('Research Assistant');
// Create an agent block
const researchAgent = new AgentBlock({
model: 'claude-3-opus',
prompt: 'Research the topic "{{input.topic}}" and provide a comprehensive summary.',
systemPrompt: 'You are a research assistant.',
tools: [] // Will be populated with tool references
});
// Reference built-in tools by their IDs
// The system will automatically transform these references into tool definitions
researchAgent.data.toolReferences = ['tavily_search', 'serper_search'];
// Configure tool settings with required parameters
researchAgent.data.toolSettings = {
tavily_search: {
apiKey: 'your-tavily-api-key'
},
serper_search: {
apiKey: 'your-serper-api-key'
}
};
// For custom tools, you can define them directly
const customTool = {
name: 'findCompany',
description: 'Find information about a company by name',
parameters: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company'
}
},
required: ['companyName']
}
};
// Add the custom tool to the agent
researchAgent.data.tools.push(customTool);
// Add to workflow
workflow.addBlock(researchAgent);
// Connect to starter
const starterBlock = workflow.getStarterBlock();
workflow.connect(starterBlock.id, researchAgent.id);
// Build and save
const builtWorkflow = workflow.build();
```
### Creating Conditional Workflows
You can create workflows with conditional branching using the ConditionBlock:
```typescript
import { SimStudio, AgentBlock, ConditionBlock } from '@sim-studio/sdk';
// Create a condition block
const conditionBlock = new ConditionBlock({
conditions: [
{ expression: 'input.score > 0.8', id: 'high' },
{ expression: 'input.score > 0.5', id: 'medium' },
{ expression: 'true', id: 'low' }
]
});
// Connect condition outcomes to different blocks
workflow.connect(
conditionBlock.id,
highQualityBlock.id,
{ sourceHandle: 'condition-high' }
);
workflow.connect(
conditionBlock.id,
mediumQualityBlock.id,
{ sourceHandle: 'condition-medium' }
);
workflow.connect(
conditionBlock.id,
lowQualityBlock.id,
{ sourceHandle: 'condition-low' }
);
```
## API Reference
### SimStudio
- `createWorkflow(name, description?)`: Creates a new workflow builder
- `saveWorkflow(workflow)`: Saves a workflow and returns the saved workflow with ID
- `deployWorkflow(workflowId, options?)`: Deploys a workflow
- `executeWorkflow(workflowId, input)`: Executes a workflow with the given input
### WorkflowBuilder
- `addBlock(block)`: Adds a block to the workflow
- `connect(sourceId, targetId, options?)`: Connects two blocks
- `getStarterBlock()`: Gets the starter block
- `build()`: Builds the workflow
### Block Types
- `AgentBlock`: Agent that can generate content using LLMs
- `FunctionBlock`: Executes JavaScript code
- `ConditionBlock`: Routes based on conditions
- `RouterBlock`: Routes based on agent decisions
- `ApiBlock`: Makes HTTP requests
- `EvaluatorBlock`: Evaluates content quality
## Examples
See the [examples](./examples) directory for more examples of using the SDK.
## Features
- Create and manage workflows programmatically
- Build complex workflows with various block types
- Execute workflows and retrieve results
- Deploy workflows as API endpoints
- Schedule workflows for automatic execution
## Block Types
The SDK supports all Sim Studio block types:
- **Agent**: LLM-powered operations
- **Function**: Custom JavaScript code execution
- **Condition**: Branching based on logical expressions
- **Router**: Dynamic path selection
- **API**: HTTP requests
- **Evaluator**: LLM-based output assessment
## Documentation
For detailed documentation, visit [docs.simstudio.dev](https://docs.simstudio.dev)
## License
MIT
+87
View File
@@ -0,0 +1,87 @@
import { SimStudio, AgentBlock, FunctionBlock } from '../src'
/**
* Creates a simple workflow with Sim Studio SDK
*/
async function createBasicWorkflow() {
const simStudio = new SimStudio({
apiKey: 'your-api-key',
})
// Create a new workflow with a name and description
const workflowBuilder = simStudio.createWorkflow(
'Content Creation Workflow',
'Generate and optimize content for blog posts'
)
// Add blocks
const contentGeneratorBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Write a detailed blog post about {{input.topic}}',
systemPrompt: 'You are a professional content writer with expertise in creating engaging blog posts.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Content Generator')
const formatterBlock = new FunctionBlock({
code: `
function formatContent(input) {
// Simple formatting function
const title = input.topic.toUpperCase()
const content = input.content
const wordCount = content.split(' ').length
return {
title,
content,
wordCount,
timestamp: new Date().toISOString()
}
}
`,
}).setName('Formatter')
const seoOptimizerBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: 'Optimize this content for SEO: {{input.content}}',
systemPrompt: 'You are an SEO expert. Add relevant keywords, improve readability, and optimize for search engines.',
temperature: 0.4,
apiKey: 'your-agent-api-key'
}).setName('SEO Optimizer')
// Add blocks to workflow
workflowBuilder
.addBlock(contentGeneratorBlock)
.addBlock(formatterBlock)
.addBlock(seoOptimizerBlock)
// Connect blocks
const starterBlock = workflowBuilder.getStarterBlock()
workflowBuilder
.connect(starterBlock.id, contentGeneratorBlock.id)
.connect(contentGeneratorBlock.id, formatterBlock.id)
.connect(formatterBlock.id, seoOptimizerBlock.id)
// Build the workflow
const workflow = workflowBuilder.build()
// Execute the workflow
try {
const workflowId = '123456' // In a real scenario, this would be the ID returned from saveWorkflow
const result = await simStudio.executeWorkflow(workflowId, {
topic: 'The benefits of AI for content creation'
})
} catch (error) {
console.error('Error executing workflow:', error)
}
return workflow
}
// Run the example
if (require.main === module) {
createBasicWorkflow().catch(console.error)
}
export default createBasicWorkflow
@@ -0,0 +1,88 @@
import { SimStudio } from '../src'
import { AgentBlock } from '../src/blocks/agent'
import { FunctionBlock } from '../src/blocks/function'
/**
* Example showing how to build and execute a workflow
*/
async function buildExecuteWorkflowExample() {
try {
const simStudio = new SimStudio({
apiKey: 'your-api-key', // Replace with your actual API key
})
// Create a workflow
const workflow = simStudio.createWorkflow(
'Content Creator',
'Generate and format a blog post from a topic'
)
// Create an agent block for content generation
const contentAgentBlock = new AgentBlock({
model: 'claude-3-opus',
prompt: 'Generate a blog post about {{input.topic}}. Include an introduction, 3 main points, and a conclusion.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Content Generator')
// Create a function block for formatting
const formatterBlock = new FunctionBlock({
code: `
function formatContent(input) {
const content = input.content
const formattedContent = {
title: "Blog Post: " + input.topic,
body: content,
date: new Date().toISOString().split('T')[0],
wordCount: content.split(' ').length
}
return formattedContent
}
`
}).setName('Content Formatter')
// Add blocks to the workflow
workflow.addBlock(contentAgentBlock)
workflow.addBlock(formatterBlock)
// Connect blocks
const starterBlock = workflow.getStarterBlock()
workflow.connect(starterBlock.id, contentAgentBlock.id)
workflow.connect(contentAgentBlock.id, formatterBlock.id)
// Build the workflow
const builtWorkflow = workflow.build()
// Save the workflow
const savedWorkflow = await simStudio.saveWorkflow(builtWorkflow)
// Deploy the workflow
if (savedWorkflow.id) {
const deployment = await simStudio.deployWorkflow(savedWorkflow.id)
// Execute the workflow
const executionInput = {
topic: 'The Future of Artificial Intelligence'
}
const executionResult = await simStudio.executeWorkflow(
savedWorkflow.id,
executionInput
)
return executionResult
} else {
return null
}
} catch (error) {
console.error('Error in workflow execution:', error)
throw error
}
}
// Run the example
if (require.main === module) {
buildExecuteWorkflowExample().catch(console.error)
}
export default buildExecuteWorkflowExample
@@ -0,0 +1,152 @@
import { SimStudio } from '../src'
import { AgentBlock } from '../src/blocks/agent'
import { ConditionBlock } from '../src/blocks/condition'
import { FunctionBlock } from '../src/blocks/function'
/**
* Example showing how to create a workflow with conditional branches
* Customer service workflow that routes inquiries to different departments
*/
async function conditionalWorkflowExample() {
try {
const simStudio = new SimStudio({
apiKey: 'your-api-key', // Replace with your actual API key
})
// Create a workflow for handling customer inquiries
const workflow = simStudio.createWorkflow(
'Customer Inquiry Router',
'Routes customer inquiries to appropriate departments based on content'
)
// Create a classifier agent block
const classifierBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: `
Analyze the customer inquiry: "{{input.inquiry}}"
Determine which category it belongs to:
- technical: Technical support requests or product troubleshooting
- billing: Questions about billing, subscriptions, or payments
- general: General inquiries, feedback, or other topics
Output ONLY the category name as your response: technical, billing, or general.
`,
systemPrompt: 'You are a customer inquiry classifier. Categorize inquiries into technical, billing, or general categories.',
temperature: 0.3,
apiKey: 'your-agent-api-key' // Include apiKey directly
}).setName('Inquiry Classifier')
// Create a condition block that routes based on the classification
const conditionData = {
conditions: [
{ expression: 'input.classification === "technical"', id: 'technical' },
{ expression: 'input.classification === "billing"', id: 'billing' },
{ expression: 'input.classification === "general"', id: 'general' }
]
}
const conditionBlock = new ConditionBlock(conditionData).setName('Route Inquiry')
// Create department-specific handler blocks
const technicalSupportBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Provide technical support for the following inquiry: {{input.inquiry}}',
systemPrompt: 'You are a technical support specialist. Provide detailed technical assistance.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Technical Support')
const billingSupportBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Address the following billing inquiry: {{input.inquiry}}',
systemPrompt: 'You are a billing specialist. Help customers with payment and subscription issues.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Billing Support')
const generalSupportBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Address the following general inquiry: {{input.inquiry}}',
systemPrompt: 'You are a customer support agent. Provide helpful and friendly assistance.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('General Support')
// Add response formatter
const formatterBlock = new FunctionBlock({
code: `
function formatResponse(input) {
return {
inquiry: input.inquiry,
department: input.classification || "unknown",
response: input.content,
timestamp: new Date().toISOString()
}
}
`
}).setName('Response Formatter')
// Add all blocks to the workflow
workflow
.addBlock(classifierBlock)
.addBlock(conditionBlock)
.addBlock(technicalSupportBlock)
.addBlock(billingSupportBlock)
.addBlock(generalSupportBlock)
.addBlock(formatterBlock)
// Connect the blocks
const starterBlock = workflow.getStarterBlock()
// Connect starter to classifier
workflow.connect(starterBlock.id, classifierBlock.id)
// Connect classifier to condition
workflow.connect(classifierBlock.id, conditionBlock.id)
// Connect condition outcomes to department-specific blocks
workflow.connect(
conditionBlock.id,
technicalSupportBlock.id,
{ sourceHandle: 'condition-technical' }
)
workflow.connect(
conditionBlock.id,
billingSupportBlock.id,
{ sourceHandle: 'condition-billing' }
)
workflow.connect(
conditionBlock.id,
generalSupportBlock.id,
{ sourceHandle: 'condition-general' }
)
// Connect all support blocks to the formatter
workflow.connect(technicalSupportBlock.id, formatterBlock.id)
workflow.connect(billingSupportBlock.id, formatterBlock.id)
workflow.connect(generalSupportBlock.id, formatterBlock.id)
// Build the workflow
const builtWorkflow = workflow.build()
console.log('Conditional workflow built successfully')
// Example of executing the workflow
const workflowId = '123456' // In a real scenario, this would be the ID returned from saveWorkflow
console.log(`To execute this workflow with an inquiry: simStudio.executeWorkflow('${workflowId}', { inquiry: "I can't login to my account" })`)
return builtWorkflow
} catch (error) {
console.error('Error creating conditional workflow:', error)
throw error
}
}
// Run the example
if (require.main === module) {
conditionalWorkflowExample().catch(console.error)
}
export default conditionalWorkflowExample
+149
View File
@@ -0,0 +1,149 @@
import { SimStudio, AgentBlock, FunctionBlock, ConditionBlock, ApiBlock } from '../src'
// Initialize the SDK
const simStudio = new SimStudio({
apiKey: 'your-api-key',
})
async function createAndDeployWorkflow() {
// Create a workflow
const workflowBuilder = simStudio.createWorkflow(
'Customer Support Workflow',
'Automatically categorize and respond to customer inquiries'
)
// Create blocks
const agentBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: '{{input.message}}',
systemPrompt: 'You are a helpful customer support assistant.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Analyze Request')
const categoryBlock = new FunctionBlock({
code: `
function categorize(input) {
const message = input.content.toLowerCase()
if (message.includes('refund')) return { category: 'refund' }
if (message.includes('shipping')) return { category: 'shipping' }
return { category: 'general' }
}
return categorize(input)
`
}).setName('Categorize Request')
const routerBlock = new ConditionBlock({
conditions: [
{ id: 'refund', expression: 'input.category === "refund"' },
{ id: 'shipping', expression: 'input.category === "shipping"' },
{ id: 'general', expression: 'true' }
]
}).setName('Route Request')
const refundBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: 'Create a response for this refund request: {{input.message}}',
systemPrompt: 'You are a refund specialist.',
apiKey: 'your-agent-api-key'
}).setName('Handle Refund')
const shippingBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: 'Create a response for this shipping request: {{input.message}}',
systemPrompt: 'You are a shipping specialist.',
apiKey: 'your-agent-api-key'
}).setName('Handle Shipping')
const generalBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: 'Create a response for this general request: {{input.message}}',
systemPrompt: 'You are a general support specialist.',
apiKey: 'your-agent-api-key'
}).setName('Handle General')
const slackNotificationBlock = new ApiBlock({
url: 'https://hooks.slack.com/services/your-webhook-url',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: {
text: 'New customer support request processed: {{input.category}}'
}
}).setName('Send Slack Notification')
// Add blocks to workflow
workflowBuilder
.addBlock(agentBlock)
.addBlock(categoryBlock)
.addBlock(routerBlock)
.addBlock(refundBlock)
.addBlock(shippingBlock)
.addBlock(generalBlock)
.addBlock(slackNotificationBlock)
// Connect blocks
const starterBlock = workflowBuilder.getStarterBlock()
if (!starterBlock) {
throw new Error("Starter block not found in workflow")
}
workflowBuilder
.connect(starterBlock.id, agentBlock.id)
.connect(agentBlock.id, categoryBlock.id)
.connect(categoryBlock.id, routerBlock.id)
.connect(routerBlock.id, refundBlock.id, { sourceHandle: 'condition-refund' })
.connect(routerBlock.id, shippingBlock.id, { sourceHandle: 'condition-shipping' })
.connect(routerBlock.id, generalBlock.id, { sourceHandle: 'condition-general' })
.connect(refundBlock.id, slackNotificationBlock.id)
.connect(shippingBlock.id, slackNotificationBlock.id)
.connect(generalBlock.id, slackNotificationBlock.id)
// Position blocks for visual layout
workflowBuilder
.positionBlock(starterBlock.id, 0, 0)
.positionBlock(agentBlock.id, 200, 0)
.positionBlock(categoryBlock.id, 400, 0)
.positionBlock(routerBlock.id, 600, 0)
.positionBlock(refundBlock.id, 800, -100)
.positionBlock(shippingBlock.id, 800, 0)
.positionBlock(generalBlock.id, 800, 100)
.positionBlock(slackNotificationBlock.id, 1000, 0)
// Save the workflow
const workflow = await simStudio.saveWorkflow(workflowBuilder.build())
if (!workflow.id) {
throw new Error("Workflow ID was not returned from the server")
}
// Deploy the workflow
const deployment = await simStudio.deployWorkflow(workflow.id, {
isPublic: true,
authentication: 'api_key',
rateLimit: 100,
})
// Schedule the workflow to run daily
const schedule = await simStudio.scheduleWorkflow(workflow.id, {
cron: '0 9 * * 1-5', // 9 AM on weekdays
timezone: 'America/New_York',
enabled: true,
})
// Execute the workflow with sample input
const result = await simStudio.executeWorkflow(workflow.id, {
message: "I haven't received my order yet and it's been 2 weeks. Can you help me track my package?"
})
return {
workflowId: workflow.id,
deploymentUrl: deployment.url,
scheduleId: schedule.id,
executionResult: result
}
}
// Run the example
createAndDeployWorkflow().catch(console.error)
@@ -0,0 +1,101 @@
import { SimStudio, AgentBlock, WorkflowCodeGenerator } from '../src'
import * as fs from 'fs'
import * as path from 'path'
const __dirname = path.resolve()
/**
* Example demonstrating how to generate TypeScript code from a workflow
*/
async function generateWorkflowCode() {
const simStudio = new SimStudio({
apiKey: 'your-api-key',
})
const workflowBuilder = simStudio.createWorkflow(
'Content Creation Workflow',
'Generate and optimize content for blog posts'
)
// Add blocks
const ideaGeneratorBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Generate 5 blog post ideas about {{input.topic}}',
systemPrompt: 'You are a creative content strategist.',
temperature: 0.8,
apiKey: 'your-agent-api-key'
}).setName('Generate Ideas')
const outlineBlock = new AgentBlock({
model: 'claude-3-sonnet',
prompt: 'Create a detailed outline for this blog post idea: {{input.selectedIdea}}',
systemPrompt: 'You are a skilled content outliner.',
temperature: 0.7,
apiKey: 'your-agent-api-key'
}).setName('Create Outline')
const draftBlock = new AgentBlock({
model: 'claude-3-opus',
prompt: 'Write a blog post based on this outline: {{input.outline}}',
systemPrompt: 'You are a professional blog writer with expertise in creating engaging content.',
temperature: 0.6,
maxTokens: 2500,
apiKey: 'your-agent-api-key'
}).setName('Write Draft')
const optimizeBlock = new AgentBlock({
model: 'claude-3-haiku',
prompt: 'Optimize this blog post for SEO: {{input.draft}}',
systemPrompt: 'You are an SEO expert. Add relevant keywords, improve readability, and optimize for search engines.',
temperature: 0.4,
apiKey: 'your-agent-api-key'
}).setName('Optimize for SEO')
// Add blocks to workflow
workflowBuilder
.addBlock(ideaGeneratorBlock)
.addBlock(outlineBlock)
.addBlock(draftBlock)
.addBlock(optimizeBlock)
// Connect blocks
const starterBlock = workflowBuilder.getStarterBlock()
workflowBuilder
.connect(starterBlock.id, ideaGeneratorBlock.id)
.connect(ideaGeneratorBlock.id, outlineBlock.id)
.connect(outlineBlock.id, draftBlock.id)
.connect(draftBlock.id, optimizeBlock.id)
// Build the workflow
const workflow = workflowBuilder.build()
// Generate TypeScript code
const codeGenerator = new WorkflowCodeGenerator(workflow, {
includeComments: true,
prettyPrint: true,
addGenerationInfo: true,
exportAsModule: true,
mainFunctionName: 'createContentWorkflow',
moduleFormat: 'commonjs',
})
const generatedCode = codeGenerator.generate()
// Save the generated code to a file
const outputDir = path.join(__dirname, 'examples', 'output')
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
const outputFile = path.join(outputDir, 'content-workflow.ts')
fs.writeFileSync(outputFile, generatedCode)
return {
workflow,
generatedCode,
outputFile,
}
}
// Run the example
generateWorkflowCode().catch(console.error)
+99
View File
@@ -0,0 +1,99 @@
import { SimStudio } from '../src'
import { AgentBlock } from '../src/blocks/agent'
/**
* Example showing how to use tools with agent blocks
*/
async function usingToolsExample() {
const simStudio = new SimStudio({
apiKey: 'your-api-key', // Replace with your actual API key
})
// Create a workflow
const workflow = simStudio.createWorkflow(
'Research Assistant',
'Research a topic and summarize findings'
)
// Create the agent block with tools
const researchAgentBlock = new AgentBlock({
model: 'claude-3-opus',
prompt: 'Research the topic "{{input.topic}}" and provide a comprehensive summary. Use the search tools to gather information.',
systemPrompt: 'You are a research assistant that can search the web for information and compile comprehensive reports.',
temperature: 0.5,
tools: [], // Will be populated with tool references
apiKey: 'your-agent-api-key' // Now included directly in options
}).setName('Research Agent')
// Reference built-in tools by their IDs
// The system will automatically transform these references into tool definitions
researchAgentBlock.data.toolReferences = ['tavily_search', 'serper_search']
// Configure tool settings with required parameters
researchAgentBlock.data.toolSettings = {
// Each tool gets its required parameters
tavily_search: {
apiKey: 'your-tavily-api-key'
},
serper_search: {
apiKey: 'your-serper-api-key'
}
}
// Add the agent to the workflow
workflow.addBlock(researchAgentBlock)
// For custom tools, users can still define them directly if needed
// This is equivalent to when a user creates a custom tool in the UI
const customTool = {
name: 'findCompany',
description: 'Find information about a company by name',
parameters: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to find information about'
},
detailLevel: {
type: 'string',
enum: ['basic', 'detailed'],
description: 'The level of detail to return',
default: 'basic'
}
},
required: ['companyName']
}
}
// Add the custom tool to the agent
if (!researchAgentBlock.data.tools) {
researchAgentBlock.data.tools = []
}
researchAgentBlock.data.tools.push(customTool)
console.log('Added tools to the agent')
// Connect the starter block to the agent
const starterBlock = workflow.getStarterBlock()
workflow.connect(starterBlock.id, researchAgentBlock.id)
// Build the workflow
const builtWorkflow = workflow.build()
console.log('Workflow built successfully with search tools')
// In a real scenario, you would save and deploy the workflow
console.log('To execute this workflow, you would:')
console.log('1. Save the workflow: simStudio.saveWorkflow(workflow)')
console.log('2. Deploy the workflow: simStudio.deployWorkflow(workflowId)')
console.log('3. Execute the workflow: simStudio.executeWorkflow(workflowId, { topic: "AI ethics" })')
return builtWorkflow
}
// Run the example
if (require.main === module) {
usingToolsExample().catch(console.error)
}
export default usingToolsExample
+5173
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@sim-studio/sdk",
"version": "0.1.0",
"description": "Official SDK for Sim Studio - build, deploy, and manage agentic workflows",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prebuild": "ts-node src/scripts/sync-tools.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint 'src/**/*.ts'",
"sync-tools": "ts-node src/scripts/sync-tools.ts"
},
"dependencies": {
"axios": "^1.6.2"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/node": "^18.15.0",
"dotenv": "^16.4.7",
"eslint": "^8.36.0",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
}
}
+88
View File
@@ -0,0 +1,88 @@
import { Block } from './base'
import { Tool } from '../tools'
export interface AgentOptions {
model: string
prompt: string
temperature?: number
maxTokens?: number
systemPrompt?: string
tools?: AgentTool[]
apiKey: string
}
export interface AgentTool {
name: string
description: string
parameters: Record<string, any>
}
/**
* Agent block for LLM-powered operations
*/
export class AgentBlock extends Block {
// Define required parameters for agent blocks
static requiredParameters: string[] = ['model', 'prompt']
constructor(options: AgentOptions) {
super('agent', options)
this.metadata.id = 'agent'
}
/**
* Set the model to use for this agent
*/
setModel(model: string): this {
this.data.model = model
return this
}
/**
* Set the prompt for this agent
*/
setPrompt(prompt: string): this {
this.data.prompt = prompt
return this
}
/**
* Set the system prompt for this agent
*/
setSystemPrompt(systemPrompt: string): this {
this.data.systemPrompt = systemPrompt
return this
}
/**
* Set the temperature for response generation
*/
setTemperature(temperature: number): this {
this.data.temperature = temperature
return this
}
/**
* Set the maximum token count for response
*/
setMaxTokens(maxTokens: number): this {
this.data.maxTokens = maxTokens
return this
}
/**
* Add a tool for the agent to use
*/
addTool(tool: Tool | AgentTool): this {
if (!this.data.tools) {
this.data.tools = []
}
// Convert Tool to AgentTool if needed
const agentTool: AgentTool = 'parameters' in tool
? { name: tool.name, description: tool.description, parameters: tool.parameters }
: { name: tool.id, description: tool.description, parameters: tool.schema }
this.data.tools.push(agentTool)
return this
}
}
+145
View File
@@ -0,0 +1,145 @@
import { Block } from './base'
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
export interface ApiOptions {
url: string
method: HttpMethod
headers?: Record<string, string>
body?: any
query?: Record<string, string>
timeout?: number
authentication?: {
type: 'basic' | 'bearer' | 'api_key'
credentials?: {
username?: string
password?: string
token?: string
key?: string
value?: string
in?: 'header' | 'query'
}
}
}
/**
* API block for making HTTP requests
*/
export class ApiBlock extends Block {
constructor(options: ApiOptions) {
super('api', options)
this.metadata.id = 'api'
}
/**
* Set the request URL
*/
setUrl(url: string): this {
this.data.url = url
return this
}
/**
* Set the HTTP method
*/
setMethod(method: HttpMethod): this {
this.data.method = method
return this
}
/**
* Set request headers
*/
setHeaders(headers: Record<string, string>): this {
this.data.headers = headers
return this
}
/**
* Add a header
*/
addHeader(key: string, value: string): this {
if (!this.data.headers) {
this.data.headers = {}
}
this.data.headers[key] = value
return this
}
/**
* Set the request body
*/
setBody(body: any): this {
this.data.body = body
return this
}
/**
* Set query parameters
*/
setQueryParams(query: Record<string, string>): this {
this.data.query = query
return this
}
/**
* Add a query parameter
*/
addQueryParam(key: string, value: string): this {
if (!this.data.query) {
this.data.query = {}
}
this.data.query[key] = value
return this
}
/**
* Set request timeout
*/
setTimeout(timeout: number): this {
this.data.timeout = timeout
return this
}
/**
* Configure basic authentication
*/
setBasicAuth(username: string, password: string): this {
this.data.authentication = {
type: 'basic',
credentials: {
username,
password
}
}
return this
}
/**
* Configure bearer token authentication
*/
setBearerAuth(token: string): this {
this.data.authentication = {
type: 'bearer',
credentials: {
token
}
}
return this
}
/**
* Configure API key authentication
*/
setApiKeyAuth(key: string, value: string, location: 'header' | 'query' = 'header'): this {
this.data.authentication = {
type: 'api_key',
credentials: {
key,
value,
in: location
}
}
return this
}
}
+147
View File
@@ -0,0 +1,147 @@
import { BlockType, BlockData } from '../types'
import { Tool } from '../tools'
import { generateId as generateUniqueId } from '../utils'
import { blockRequiredParameters, getToolRequiredParameters } from '../generated'
/**
* Base class for all workflow blocks
*/
export abstract class Block {
id: string
type: BlockType
data: BlockData
enabled: boolean
metadata: {
name?: string
description?: string
[key: string]: any
}
private tools: Tool[] = []
// Define static property for required parameters that subclasses should override
static requiredParameters: string[] = []
constructor(type: BlockType, data: BlockData = {}) {
this.id = generateUniqueId()
this.type = type
this.data = data
this.enabled = true
this.metadata = {}
// Validate required parameters
this.validateRequiredParameters()
}
/**
* Validate that all required parameters are present
*/
private validateRequiredParameters(): void {
// First check static requirements from the class (if any)
const staticRequiredParams = (this.constructor as typeof Block).requiredParameters
if (staticRequiredParams.length > 0) {
const missingParams = staticRequiredParams.filter(param => this.data[param] === undefined)
if (missingParams.length > 0) {
throw new Error(`Missing required parameters for ${this.type} block: ${missingParams.join(', ')}`)
}
}
// Then check the dynamically generated requirements from the main app
const dynamicRequiredParams = blockRequiredParameters[this.type] || []
if (dynamicRequiredParams.length > 0) {
const missingParams = dynamicRequiredParams.filter(param => this.data[param] === undefined)
if (missingParams.length > 0) {
throw new Error(`Missing required parameters for ${this.type} block: ${missingParams.join(', ')}`)
}
}
}
/**
* Set the block's name
*/
setName(name: string): this {
this.metadata.name = name
return this
}
/**
* Set the block's description
*/
setDescription(description: string): this {
this.metadata.description = description
return this
}
/**
* Enable or disable the block
*/
setEnabled(enabled: boolean): this {
this.enabled = enabled
return this
}
/**
* Set custom metadata
*/
setMetadata(key: string, value: any): this {
this.metadata[key] = value
return this
}
/**
* Add a tool to this block
*/
addTool(tool: Tool): this {
this.tools.push(tool)
// If this is an agent block, update the data.tools array
if (this.type === 'agent' && Array.isArray(this.data.tools)) {
// Validate required tool parameters
const requiredParams = getToolRequiredParameters(tool.id)
if (requiredParams.length > 0) {
const toolSettings = this.data.toolSettings?.[tool.id] || {}
const missingParams = requiredParams.filter(param =>
toolSettings[param] === undefined
)
if (missingParams.length > 0) {
throw new Error(`Missing required parameters for tool '${tool.id}': ${missingParams.join(', ')}`)
}
}
this.data.tools.push({
name: tool.id,
description: tool.description,
parameters: tool.schema
})
}
return this
}
/**
* Get all tools attached to this block
*/
getTools(): Tool[] {
return this.tools
}
/**
* Serialize the block for API requests
*/
toJSON(): any {
return {
id: this.id,
type: this.type,
data: this.data,
enabled: this.enabled,
metadata: this.metadata,
}
}
}
/**
* Generate a unique ID for blocks
*/
function generateId(): string {
return Math.random().toString(36).substring(2, 9)
}
+52
View File
@@ -0,0 +1,52 @@
import { Block } from './base'
export interface Condition {
id: string
expression: string
description?: string
}
export interface ConditionOptions {
conditions: Condition[]
}
/**
* Condition block for branching workflows based on logical expressions
*/
export class ConditionBlock extends Block {
constructor(options: ConditionOptions) {
super('condition', options)
this.metadata.id = 'condition'
}
/**
* Add a condition branch
*/
addCondition(condition: Condition): this {
if (!this.data.conditions) {
this.data.conditions = []
}
this.data.conditions.push(condition)
return this
}
/**
* Remove a condition by ID
*/
removeCondition(conditionId: string): this {
if (this.data.conditions) {
this.data.conditions = this.data.conditions.filter(
(c: Condition) => c.id !== conditionId
)
}
return this
}
/**
* Set all conditions at once
*/
setConditions(conditions: Condition[]): this {
this.data.conditions = conditions
return this
}
}
+71
View File
@@ -0,0 +1,71 @@
import { Block } from './base'
export interface EvaluatorOptions {
criteria: Criterion[]
model?: string
systemPrompt?: string
}
export interface Criterion {
id: string
name: string
description: string
weight?: number
}
/**
* Evaluator block for assessing outputs using LLM-based evaluation
*/
export class EvaluatorBlock extends Block {
constructor(options: EvaluatorOptions) {
super('evaluator', options)
this.metadata.id = 'evaluator'
}
/**
* Set the evaluation criteria
*/
setCriteria(criteria: Criterion[]): this {
this.data.criteria = criteria
return this
}
/**
* Add an evaluation criterion
*/
addCriterion(criterion: Criterion): this {
if (!this.data.criteria) {
this.data.criteria = []
}
this.data.criteria.push(criterion)
return this
}
/**
* Remove a criterion by ID
*/
removeCriterion(criterionId: string): this {
if (this.data.criteria) {
this.data.criteria = this.data.criteria.filter(
(c: Criterion) => c.id !== criterionId
)
}
return this
}
/**
* Set the model to use for evaluation
*/
setModel(model: string): this {
this.data.model = model
return this
}
/**
* Set the system prompt for evaluation
*/
setSystemPrompt(systemPrompt: string): this {
this.data.systemPrompt = systemPrompt
return this
}
}
+41
View File
@@ -0,0 +1,41 @@
import { Block } from './base'
export interface FunctionOptions {
code: string
timeout?: number
environment?: Record<string, string>
}
/**
* Function block for custom JavaScript code execution
*/
export class FunctionBlock extends Block {
constructor(options: FunctionOptions) {
super('function', options)
this.metadata.id = 'function'
}
/**
* Set the JavaScript code to execute
*/
setCode(code: string): this {
this.data.code = code
return this
}
/**
* Set the execution timeout in milliseconds
*/
setTimeout(timeout: number): this {
this.data.timeout = timeout
return this
}
/**
* Set environment variables for function execution
*/
setEnvironment(env: Record<string, string>): this {
this.data.environment = env
return this
}
}
+8
View File
@@ -0,0 +1,8 @@
export * from './base'
export * from './agent'
export * from './function'
export * from './condition'
export * from './router'
export * from './api'
export * from './evaluator'
export * from './starter'
+126
View File
@@ -0,0 +1,126 @@
import { Block } from './base'
import { availableBlocks, toolToBlockMap } from '../generated/registry'
/**
* Registry for all available block types
*/
export class BlockRegistry {
private static blocks: Map<string, new (...args: any[]) => Block> = new Map()
private static blockOptions: Map<string, any> = new Map()
/**
* Register a block type
*/
static register<T extends Block>(
id: string,
blockClass: new (...args: any[]) => T,
options?: {
category?: string
description?: string
icon?: string
defaultData?: any
schema?: any
}
): void {
this.blocks.set(id, blockClass)
if (options) {
this.blockOptions.set(id, options)
}
}
/**
* Create a new block instance by ID
*/
static create(id: string, data?: any): Block | null {
const BlockClass = this.blocks.get(id)
if (!BlockClass) {
return null
}
return new BlockClass(data)
}
/**
* Check if a block type is registered
*/
static has(id: string): boolean {
return this.blocks.has(id)
}
/**
* Get all registered block types
*/
static getAll(): string[] {
return Array.from(this.blocks.keys())
}
/**
* Get block options for a specific block type
*/
static getOptions(id: string): any {
return this.blockOptions.get(id) || {}
}
/**
* Check if a block is available in the main application
*/
static isAvailableInMainApp(id: string): boolean {
return availableBlocks.includes(id)
}
/**
* Get the block type for a tool
*/
static getBlockTypeForTool(toolId: string): string | undefined {
return toolToBlockMap[toolId]
}
/**
* Get all available block types from the main application
*/
static getAllAvailableBlocks(): string[] {
return [...availableBlocks]
}
}
/**
* Decorator for registering a block class with the registry
*/
export function registerBlock(
id: string,
options?: {
category?: string
description?: string
icon?: string
defaultData?: any
schema?: any
}
): ClassDecorator {
return (target: any) => {
BlockRegistry.register(id, target, options)
return target
}
}
/**
* Interface for block imports from the main application
*/
export interface BlockImport {
id: string
blockClass: new (...args: any[]) => Block
options?: {
category?: string
description?: string
icon?: string
defaultData?: any
schema?: any
}
}
/**
* Import blocks from the main application
*/
export function importBlocks(blocks: BlockImport[]): void {
blocks.forEach(block => {
BlockRegistry.register(block.id, block.blockClass, block.options)
})
}
+70
View File
@@ -0,0 +1,70 @@
import { Block } from './base'
export interface Route {
id: string
targetId: string
description?: string
}
export interface RouterOptions {
routes: Route[]
defaultRouteId?: string
routingExpression?: string
}
/**
* Router block for dynamic path selection in workflows
*/
export class RouterBlock extends Block {
constructor(options: RouterOptions) {
super('router', options)
this.metadata.id = 'router'
}
/**
* Add a route
*/
addRoute(route: Route): this {
if (!this.data.routes) {
this.data.routes = []
}
this.data.routes.push(route)
return this
}
/**
* Remove a route by ID
*/
removeRoute(routeId: string): this {
if (this.data.routes) {
this.data.routes = this.data.routes.filter(
(r: Route) => r.id !== routeId
)
}
return this
}
/**
* Set all routes at once
*/
setRoutes(routes: Route[]): this {
this.data.routes = routes
return this
}
/**
* Set the default route ID
*/
setDefaultRoute(routeId: string): this {
this.data.defaultRouteId = routeId
return this
}
/**
* Set the routing expression
*/
setRoutingExpression(expression: string): this {
this.data.routingExpression = expression
return this
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Block } from './base'
export interface StarterOptions {
input?: Record<string, any>
}
/**
* Starter block for workflow entry point
*/
export class StarterBlock extends Block {
constructor(options: StarterOptions = {}) {
super('starter', options)
this.metadata.id = 'starter'
this.setName('Start')
}
/**
* Set the default input for this workflow
*/
setInput(input: Record<string, any>): this {
this.data.input = input
return this
}
}
+366
View File
@@ -0,0 +1,366 @@
import { Workflow, Block, Connection } from '../types'
/**
* Options for code generation
*/
export interface CodeGenerationOptions {
/**
* Include comments in generated code
*/
includeComments?: boolean
/**
* Format the code with proper indentation
*/
prettyPrint?: boolean
/**
* Add a timestamp and generator information
*/
addGenerationInfo?: boolean
/**
* Export workflow as a module
*/
exportAsModule?: boolean
/**
* Name of the main function
*/
mainFunctionName?: string
/**
* ESNext or CommonJS module format
*/
moduleFormat?: 'esm' | 'commonjs'
}
/**
* Generates TypeScript code from a workflow
*/
export class WorkflowCodeGenerator {
private workflow: Workflow
private options: Required<CodeGenerationOptions>
constructor(workflow: Workflow, options: CodeGenerationOptions = {}) {
this.workflow = workflow
this.options = {
includeComments: options.includeComments ?? true,
prettyPrint: options.prettyPrint ?? true,
addGenerationInfo: options.addGenerationInfo ?? true,
exportAsModule: options.exportAsModule ?? true,
mainFunctionName: options.mainFunctionName ?? this.sanitizeName(workflow.name || 'workflow'),
moduleFormat: options.moduleFormat ?? 'esm',
}
}
/**
* Generate TypeScript code representing the workflow
*/
generate(): string {
const parts: string[] = []
// Add generation info if requested
if (this.options.addGenerationInfo) {
parts.push(this.generateHeader())
}
// Add imports
parts.push(this.generateImports())
// Add block declarations
parts.push(this.generateBlockDeclarations())
// Add main function
parts.push(this.generateMainFunction())
// Join all parts
return parts.join('\n\n')
}
/**
* Generate header comment with generation info
*/
private generateHeader(): string {
const date = new Date().toISOString()
return [
'/**',
' * This code was generated from a Sim Studio workflow',
` * Workflow: ${this.workflow.name || 'Unnamed workflow'}`,
` * Generated: ${date}`,
' */',
].join('\n')
}
/**
* Generate imports section
*/
private generateImports(): string {
if (this.options.moduleFormat === 'esm') {
return [
"import { SimStudio } from '@sim-studio/sdk'",
this.getBlockImports(),
].join('\n')
} else {
return [
"const { SimStudio } = require('@sim-studio/sdk')",
this.getBlockImports('commonjs'),
].join('\n')
}
}
/**
* Generate imports for block types used in the workflow
*/
private getBlockImports(format: 'esm' | 'commonjs' = 'esm'): string {
const blockTypes = new Set<string>()
for (const block of this.workflow.blocks) {
const blockType = this.getBlockClassName(block)
if (blockType && blockType !== 'StarterBlock') {
blockTypes.add(blockType)
}
}
if (blockTypes.size === 0) {
return ''
}
const blockTypesList = Array.from(blockTypes).sort()
if (format === 'esm') {
return `import { ${blockTypesList.join(', ')} } from '@sim-studio/sdk'`
} else {
return `const { ${blockTypesList.join(', ')} } = require('@sim-studio/sdk')`
}
}
/**
* Generate declarations for all blocks
*/
private generateBlockDeclarations(): string {
return this.workflow.blocks
.filter(block => block.enabled !== false && block.metadata?.id !== 'starter')
.map(block => this.generateBlockDeclaration(block))
.join('\n\n')
}
/**
* Generate the main function that creates and executes the workflow
*/
private generateMainFunction(): string {
const lines: string[] = []
// Function signature
if (this.options.exportAsModule) {
lines.push(`export async function ${this.options.mainFunctionName}(input = {}) {`)
} else {
lines.push(`async function ${this.options.mainFunctionName}(input = {}) {`)
}
// Initialize SDK
lines.push(' // Initialize the SDK')
lines.push(" const simStudio = new SimStudio()")
lines.push('')
// Create workflow
lines.push(' // Create a new workflow')
lines.push(` const workflowBuilder = simStudio.createWorkflow('${this.workflow.name || 'Generated Workflow'}')`)
lines.push('')
// Add blocks
lines.push(' // Add blocks to the workflow')
for (const block of this.workflow.blocks) {
if (block.metadata?.id !== 'starter' && block.enabled !== false) {
const varName = this.getBlockVariableName(block)
lines.push(` workflowBuilder.addBlock(${varName})`)
}
}
lines.push('')
// Get starter block
lines.push(' // Get the starter block')
lines.push(' const starterBlock = workflowBuilder.getStarterBlock()')
lines.push('')
// Connect blocks
lines.push(' // Connect blocks')
const connections = this.generateConnectionsCode()
connections.forEach(conn => lines.push(` ${conn}`))
lines.push('')
// Create loops if any
if (this.workflow.loops && Object.keys(this.workflow.loops).length > 0) {
lines.push(' // Create loops')
for (const [loopId, loop] of Object.entries(this.workflow.loops)) {
const nodesArray = JSON.stringify(loop.nodes)
lines.push(` workflowBuilder.createLoop(${nodesArray}, ${loop.maxIterations}${loop.iterationVariable ? `, '${loop.iterationVariable}'` : ''})`)
}
lines.push('')
}
// Save workflow
lines.push(' // Save and execute the workflow')
lines.push(' const workflow = await workflowBuilder.build()')
lines.push(' return workflow')
// Close function
lines.push('}')
// Add example if not a module
if (!this.options.exportAsModule) {
lines.push('')
lines.push(`// Execute the workflow with sample input`)
lines.push(`${this.options.mainFunctionName}({`)
lines.push(` // Add your input here`)
lines.push(`}).then(console.log).catch(console.error)`)
}
return lines.join('\n')
}
/**
* Generate a single block declaration
*/
private generateBlockDeclaration(block: Block): string {
const varName = this.getBlockVariableName(block)
const className = this.getBlockClassName(block)
const blockData = JSON.stringify(block.data, null, 2)
.replace(/"([^"]+)":/g, '$1:') // Convert "key": to key:
.replace(/"/g, "'") // Convert double quotes to single quotes
let declaration = `const ${varName} = new ${className}(${blockData})`
// Add name and description if available
if (block.metadata?.name) {
declaration += `.setName('${block.metadata.name.replace(/'/g, "\\'")}')`
}
if (block.metadata?.description) {
declaration += `.setDescription('${block.metadata.description.replace(/'/g, "\\'")}')`
}
return declaration + ''
}
/**
* Generate code for all connections
*/
private generateConnectionsCode(): string[] {
return this.workflow.connections.map(conn => {
let sourceId = conn.source
let targetId = conn.target
// Try to use variable names when possible
const sourceBlock = this.workflow.blocks.find(b => b.id === conn.source)
const targetBlock = this.workflow.blocks.find(b => b.id === conn.target)
if (sourceBlock?.metadata?.id === 'starter') {
sourceId = 'starterBlock.id'
} else if (sourceBlock) {
sourceId = `${this.getBlockVariableName(sourceBlock)}.id`
}
if (targetBlock) {
targetId = `${this.getBlockVariableName(targetBlock)}.id`
}
let options = ''
if (conn.sourceHandle || conn.targetHandle) {
const optionsObj: Record<string, string> = {}
if (conn.sourceHandle) optionsObj.sourceHandle = `'${conn.sourceHandle}'`
if (conn.targetHandle) optionsObj.targetHandle = `'${conn.targetHandle}'`
options = `, { ${Object.entries(optionsObj).map(([k, v]) => `${k}: ${v}`).join(', ')} }`
}
return `workflowBuilder.connect(${sourceId}, ${targetId}${options})`
})
}
/**
* Generate a variable name for a block
*/
private getBlockVariableName(block: Block): string {
if (block.metadata?.id === 'starter') {
return 'starterBlock'
}
// Use the block name if available
if (block.metadata?.name) {
return this.sanitizeName(block.metadata.name, true)
}
// Fall back to block type + ID
return `${block.type}Block_${block.id}`
}
/**
* Get the class name for a block
*/
private getBlockClassName(block: Block): string | null {
const blockTypeId = block.metadata?.id || block.type
// Map block types to class names
const blockTypeToClass: Record<string, string> = {
'starter': 'StarterBlock',
'agent': 'AgentBlock',
'function': 'FunctionBlock',
'condition': 'ConditionBlock',
'router': 'RouterBlock',
'api': 'ApiBlock',
'evaluator': 'EvaluatorBlock',
'openai': 'OpenAIBlock',
'tavily': 'TavilyBlock',
'slack': 'SlackBlock',
'jina': 'JinaBlock',
'serper': 'SerperBlock',
'pinecone': 'PineconeBlock',
'github': 'GitHubBlock',
'gmail': 'GmailBlock',
'notion': 'NotionBlock',
'sheets': 'SheetsBlock',
'drive': 'DriveBlock',
'docs': 'DocsBlock',
'x': 'XBlock'
}
return blockTypeToClass[blockTypeId] || `${this.capitalize(blockTypeId)}Block`
}
/**
* Generate a sanitized name that can be used as a variable name
*/
private sanitizeName(name: string, camelCase: boolean = false): string {
// Replace non-alphanumeric characters with underscores
let sanitized = name.replace(/[^a-zA-Z0-9]/g, '_')
// Ensure it starts with a letter
if (!/^[a-zA-Z]/.test(sanitized)) {
sanitized = 'block_' + sanitized
}
// Convert to camelCase if requested
if (camelCase) {
sanitized = sanitized.replace(/_([a-zA-Z])/g, (_, char) => char.toUpperCase())
sanitized = sanitized.charAt(0).toLowerCase() + sanitized.slice(1)
}
return sanitized
}
/**
* Capitalize the first letter of a string
*/
private capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
/**
* Convenience function to generate TypeScript code from a workflow
*/
export function generateCode(workflow: Workflow, options: CodeGenerationOptions = {}): string {
const generator = new WorkflowCodeGenerator(workflow, options)
return generator.generate()
}
+170
View File
@@ -0,0 +1,170 @@
import axios, { AxiosInstance } from 'axios'
import { SimStudioConfig, Workflow, ExecutionResult, DeploymentOptions, DeploymentResult, ScheduleOptions, ScheduleResult } from './types'
import { WorkflowBuilder } from './workflows/builder'
import { BlockRegistry, BlockImport } from './blocks/registry'
import { Tool, ToolRegistry } from './tools'
/**
* Main SDK class for Sim Studio
*/
export class SimStudio {
private client: AxiosInstance
private config: Required<SimStudioConfig>
/**
* Create a new Sim Studio SDK instance
*/
constructor(config: SimStudioConfig = {}) {
this.config = {
apiKey: config.apiKey ?? process.env.SIM_STUDIO_API_KEY ?? '',
baseUrl: config.baseUrl ?? process.env.SIM_STUDIO_API_URL ?? 'https://api.simstudio.dev',
timeout: config.timeout ?? 30000,
}
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.apiKey}`,
},
})
}
/**
* Create a new workflow builder
*/
createWorkflow(name: string, description?: string): WorkflowBuilder {
return new WorkflowBuilder(name, description)
}
/**
* Get a workflow by ID
*/
async getWorkflow(id: string): Promise<Workflow> {
const response = await this.client.get(`/workflows/${id}`)
return response.data
}
/**
* List all workflows
*/
async listWorkflows(params: { limit?: number; offset?: number } = {}): Promise<Workflow[]> {
const response = await this.client.get('/workflows', { params })
return response.data
}
/**
* Save a workflow
*/
async saveWorkflow(workflow: Workflow): Promise<Workflow> {
if (workflow.id) {
const response = await this.client.put(`/workflows/${workflow.id}`, workflow)
return response.data
} else {
const response = await this.client.post('/workflows', workflow)
return response.data
}
}
/**
* Delete a workflow
*/
async deleteWorkflow(id: string): Promise<void> {
await this.client.delete(`/workflows/${id}`)
}
/**
* Execute a workflow
*/
async executeWorkflow(id: string, input?: Record<string, any>): Promise<ExecutionResult> {
const response = await this.client.post(`/workflows/${id}/execute`, { input })
return response.data
}
/**
* Deploy a workflow as an API endpoint
*/
async deployWorkflow(id: string, options: DeploymentOptions = {}): Promise<DeploymentResult> {
const response = await this.client.post(`/workflows/${id}/deploy`, options)
return response.data
}
/**
* Get deployment details
*/
async getDeployment(workflowId: string): Promise<DeploymentResult> {
const response = await this.client.get(`/workflows/${workflowId}/deployment`)
return response.data
}
/**
* Remove a workflow deployment
*/
async undeployWorkflow(workflowId: string): Promise<void> {
await this.client.delete(`/workflows/${workflowId}/deployment`)
}
/**
* Schedule a workflow
*/
async scheduleWorkflow(workflowId: string, options: ScheduleOptions): Promise<ScheduleResult> {
const response = await this.client.post(`/workflows/${workflowId}/schedule`, options)
return response.data
}
/**
* Get a workflow schedule
*/
async getSchedule(scheduleId: string): Promise<ScheduleResult> {
const response = await this.client.get(`/schedules/${scheduleId}`)
return response.data
}
/**
* List schedules for a workflow
*/
async listSchedules(workflowId: string): Promise<ScheduleResult[]> {
const response = await this.client.get(`/workflows/${workflowId}/schedules`)
return response.data
}
/**
* Delete a schedule
*/
async deleteSchedule(scheduleId: string): Promise<void> {
await this.client.delete(`/schedules/${scheduleId}`)
}
/**
* Register blocks from the main application
*/
static registerBlocks(blocks: BlockImport[]): void {
blocks.forEach(block => {
BlockRegistry.register(block.id, block.blockClass, block.options)
})
}
/**
* Register tools from the main application
*/
static registerTools(tools: Tool[]): void {
tools.forEach(tool => {
ToolRegistry.register(tool)
})
}
/**
* Get all registered blocks
*/
static getRegisteredBlocks(): string[] {
return BlockRegistry.getAll()
}
/**
* Get all registered tools
*/
static getRegisteredTools(): Tool[] {
return ToolRegistry.getAll()
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Type declarations for the generated registry
*/
/**
* List of all available tools in the main application
*/
export declare const availableTools: string[]
/**
* List of all available blocks in the main application
*/
export declare const availableBlocks: string[]
/**
* Map of tool IDs to their block types
*/
export declare const toolToBlockMap: Record<string, string>
/**
* Required parameters for each tool
*/
export declare const toolRequiredParameters: Record<string, string[]>
/**
* Required parameters for each block
*/
export declare const blockRequiredParameters: Record<string, string[]>
/**
* Check if a tool is available
*/
export declare function isToolAvailable(toolId: string): boolean
/**
* Check if a block is available
*/
export declare function isBlockAvailable(blockId: string): boolean
/**
* Get the block type for a tool
*/
export declare function getBlockTypeForTool(toolId: string): string | undefined
/**
* Get required parameters for a tool
*/
export declare function getToolRequiredParameters(toolId: string): string[]
/**
* Get required parameters for a block
*/
export declare function getBlockRequiredParameters(blockId: string): string[]
+1
View File
@@ -0,0 +1 @@
export * from './registry';
+278
View File
@@ -0,0 +1,278 @@
/**
* This file is auto-generated. Do not edit directly.
* Generated at 2025-03-18T23:55:40.504Z
*/
/**
* List of all available tools in the main application
*/
export const availableTools = [
"openai_embeddings",
"http_request",
"hubspot_contacts",
"salesforce_opportunities",
"function_execute",
"webcontainer_execute",
"vision_tool",
"firecrawl_scrape",
"jina_readurl",
"slack_message",
"github_repoinfo",
"serper_search",
"tavily_search",
"tavily_extract",
"supabase_query",
"supabase_insert",
"supabase_update",
"youtube_search",
"notion_read",
"notion_write",
"gmail_send",
"gmail_read",
"gmail_search",
"whatsapp_send_message",
"x_write",
"x_read",
"x_search",
"x_user",
"pinecone_fetch",
"pinecone_generate_embeddings",
"pinecone_search_text",
"pinecone_search_vector",
"pinecone_upsert_text",
"github_pr",
"github_comment",
"exa_search",
"exa_get_contents",
"exa_find_similar_links",
"exa_answer",
"reddit_hot_posts",
"google_drive_download",
"google_drive_list",
"google_drive_upload",
"google_docs_read",
"google_docs_write",
"google_docs_create",
"google_sheets_read",
"google_sheets_write",
"google_sheets_update",
"guesty_reservation",
"guesty_guest"
]
/**
* List of all available blocks in the main application
*/
export const availableBlocks = [
"agent",
"api",
"condition",
"evaluator",
"exa",
"firecrawl",
"function",
"github",
"gmail",
"google_docs",
"google_drive",
"google_sheets",
"jina",
"notion",
"openai",
"pinecone",
"reddit",
"router",
"serper",
"slack",
"starter",
"supabase",
"tavily",
"translate",
"vision",
"whatsapp",
"x",
"youtube"
]
/**
* Map of tool IDs to their block types
*/
export const toolToBlockMap: Record<string, string> = {
'openai_embeddings': 'OpenaiEmbeddingsBlock',
'http_request': 'HttpRequestBlock',
'hubspot_contacts': 'HubspotContactsBlock',
'salesforce_opportunities': 'SalesforceOpportunitiesBlock',
'function_execute': 'FunctionExecuteBlock',
'webcontainer_execute': 'WebcontainerExecuteBlock',
'vision_tool': 'VisionToolBlock',
'firecrawl_scrape': 'FirecrawlScrapeBlock',
'jina_readurl': 'JinaReadurlBlock',
'slack_message': 'SlackMessageBlock',
'github_repoinfo': 'GithubRepoinfoBlock',
'serper_search': 'SerperSearchBlock',
'tavily_search': 'TavilySearchBlock',
'tavily_extract': 'TavilyExtractBlock',
'supabase_query': 'SupabaseQueryBlock',
'supabase_insert': 'SupabaseInsertBlock',
'supabase_update': 'SupabaseUpdateBlock',
'youtube_search': 'YoutubeSearchBlock',
'notion_read': 'NotionReadBlock',
'notion_write': 'NotionWriteBlock',
'gmail_send': 'GmailSendBlock',
'gmail_read': 'GmailReadBlock',
'gmail_search': 'GmailSearchBlock',
'whatsapp_send_message': 'WhatsappSendMessageBlock',
'x_write': 'XWriteBlock',
'x_read': 'XReadBlock',
'x_search': 'XSearchBlock',
'x_user': 'XUserBlock',
'pinecone_fetch': 'PineconeFetchBlock',
'pinecone_generate_embeddings': 'PineconeGenerateEmbeddingsBlock',
'pinecone_search_text': 'PineconeSearchTextBlock',
'pinecone_search_vector': 'PineconeSearchVectorBlock',
'pinecone_upsert_text': 'PineconeUpsertTextBlock',
'github_pr': 'GithubPrBlock',
'github_comment': 'GithubCommentBlock',
'exa_search': 'ExaSearchBlock',
'exa_get_contents': 'ExaGetContentsBlock',
'exa_find_similar_links': 'ExaFindSimilarLinksBlock',
'exa_answer': 'ExaAnswerBlock',
'reddit_hot_posts': 'RedditHotPostsBlock',
'google_drive_download': 'GoogleDriveDownloadBlock',
'google_drive_list': 'GoogleDriveListBlock',
'google_drive_upload': 'GoogleDriveUploadBlock',
'google_docs_read': 'GoogleDocsReadBlock',
'google_docs_write': 'GoogleDocsWriteBlock',
'google_docs_create': 'GoogleDocsCreateBlock',
'google_sheets_read': 'GoogleSheetsReadBlock',
'google_sheets_write': 'GoogleSheetsWriteBlock',
'google_sheets_update': 'GoogleSheetsUpdateBlock',
'guesty_reservation': 'GuestyReservationBlock',
'guesty_guest': 'GuestyGuestBlock'
}
/**
* Required parameters for each tool
*/
export const toolRequiredParameters: Record<string, string[]> = {
'openai_embeddings': [],
'http_request': [],
'hubspot_contacts': ["apiKey"],
'salesforce_opportunities': ["apiKey"],
'function_execute': [],
'webcontainer_execute': [],
'vision_tool': ["apiKey"],
'firecrawl_scrape': ["apiKey"],
'jina_readurl': ["apiKey"],
'slack_message': ["apiKey"],
'github_repoinfo': ["apiKey"],
'serper_search': ["apiKey"],
'tavily_search': ["apiKey"],
'tavily_extract': ["apiKey"],
'supabase_query': [],
'supabase_insert': [],
'supabase_update': [],
'youtube_search': ["apiKey"],
'notion_read': ["pageId","apiKey"],
'notion_write': ["pageId","apiKey"],
'gmail_send': [],
'gmail_read': [],
'gmail_search': [],
'whatsapp_send_message': [],
'x_write': ["apiKey"],
'x_read': ["apiKey","tweetId"],
'x_search': ["apiKey"],
'x_user': ["apiKey"],
'pinecone_fetch': ["apiKey","indexHost","ids"],
'pinecone_generate_embeddings': ["apiKey"],
'pinecone_search_text': ["apiKey","indexHost"],
'pinecone_search_vector': ["apiKey","indexHost"],
'pinecone_upsert_text': ["apiKey","indexHost"],
'github_pr': ["apiKey"],
'github_comment': ["apiKey"],
'exa_search': ["apiKey"],
'exa_get_contents': ["apiKey"],
'exa_find_similar_links': ["apiKey"],
'exa_answer': ["apiKey"],
'reddit_hot_posts': [],
'google_drive_download': [],
'google_drive_list': [],
'google_drive_upload': [],
'google_docs_read': [],
'google_docs_write': [],
'google_docs_create': [],
'google_sheets_read': [],
'google_sheets_write': [],
'google_sheets_update': [],
'guesty_reservation': ["apiKey"],
'guesty_guest': ["apiKey"]
}
/**
* Required parameters for each block
*/
export const blockRequiredParameters: Record<string, string[]> = {
'agent': ["model","apiKey"],
'api': ["url","method"],
'condition': [],
'evaluator': ["metrics","model","apiKey","content"],
'exa': ["operation","apiKey"],
'firecrawl': ["apiKey","url"],
'function': [],
'github': ["operation","owner","repo","apiKey"],
'gmail': ["operation","credential"],
'google_docs': ["operation","credential"],
'google_drive': ["operation","credential"],
'google_sheets': ["operation","credential"],
'jina': ["url","apiKey"],
'notion': ["pageId","operation","apiKey"],
'openai': ["input","apiKey"],
'pinecone': ["operation","apiKey"],
'reddit': ["subreddit"],
'router': ["prompt","model","apiKey"],
'serper': ["query","apiKey"],
'slack': ["apiKey","channel","text"],
'starter': [],
'supabase': ["operation","credential","projectId","table"],
'tavily': ["operation","apiKey"],
'translate': ["context","targetLanguage","apiKey","systemPrompt"],
'vision': ["apiKey","imageUrl"],
'whatsapp': ["phoneNumber","message","phoneNumberId","accessToken"],
'x': ["operation","apiKey"],
'youtube': ["apiKey","query"]
}
/**
* Check if a tool is available
*/
export function isToolAvailable(toolId: string): boolean {
return availableTools.includes(toolId)
}
/**
* Check if a block is available
*/
export function isBlockAvailable(blockId: string): boolean {
return availableBlocks.includes(blockId)
}
/**
* Get the block type for a tool
*/
export function getBlockTypeForTool(toolId: string): string | undefined {
return toolToBlockMap[toolId]
}
/**
* Get required parameters for a tool
*/
export function getToolRequiredParameters(toolId: string): string[] {
return toolRequiredParameters[toolId] || []
}
/**
* Get required parameters for a block
*/
export function getBlockRequiredParameters(blockId: string): string[] {
return blockRequiredParameters[blockId] || []
}
+16
View File
@@ -0,0 +1,16 @@
export { SimStudio } from './core'
export { WorkflowBuilder } from './workflows/builder'
export { Block as BlockBase } from './blocks/base'
export { AgentBlock } from './blocks/agent'
export { FunctionBlock } from './blocks/function'
export { ConditionBlock } from './blocks/condition'
export { RouterBlock } from './blocks/router'
export { ApiBlock } from './blocks/api'
export { EvaluatorBlock } from './blocks/evaluator'
export { StarterBlock } from './blocks/starter'
export { WorkflowCodeGenerator } from './code-generator'
export * from './utils'
export * from './types'
export * from './blocks/registry'
export * from './tools'
+555
View File
@@ -0,0 +1,555 @@
import * as fs from 'fs'
import * as path from 'path'
import * as ts from 'typescript'
import * as dotenv from 'dotenv'
// Load environment variables from .env file
dotenv.config()
console.log('SIM_APP_PATH:', process.env.SIM_APP_PATH)
/**
* Script to synchronize tools and blocks from the main app to the SDK
* This script should be run as part of the build process
*/
// Paths to look for the main app files
const POSSIBLE_MAIN_APP_PATHS = [
// Path from environment variable (should be the most reliable)
process.env.SIM_APP_PATH ? path.join(process.env.SIM_APP_PATH, 'app/tools') : '',
process.env.SIM_APP_PATH ? path.join(process.env.SIM_APP_PATH, 'app/blocks') : '',
// Direct relative paths from the script's location
'../../sim/app/tools',
'../../sim/app/blocks',
// From project root
'../sim/app/tools',
'../sim/app/blocks',
// From monorepo root
'../../sim/app/tools',
'../../sim/app/blocks',
// Path based on common monorepo structure
'../../../sim/app/tools',
'../../../sim/app/blocks',
].filter(Boolean)
const OUTPUT_REGISTRY_PATH = '../generated/registry.ts'
export async function syncFromMainApp() {
try {
// Ensure output directory exists
const outputDir = path.resolve(__dirname, '../generated')
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// Find the directories
const toolsDir = findDir('tools')
const blocksDir = findDir('blocks')
console.log("Tools directory:", toolsDir)
console.log("Blocks directory:", blocksDir)
// Extract tools and blocks
const tools = toolsDir ? extractToolsFromDir(toolsDir) : new Map<string, string[]>()
const blocks = blocksDir ? extractBlocksFromDir(blocksDir) : new Map<string, string[]>()
// Get the registry data
const toolRegistry = toolsDir ? extractToolsRegistry(path.join(toolsDir, 'index.ts')) : []
const blockRegistry = blocksDir ? extractBlocksRegistry(path.join(blocksDir, 'index.ts')) : []
// Combine the information
const combinedTools = mergeToolsInfo(toolRegistry, tools)
const combinedBlocks = mergeBlocksInfo(blockRegistry, blocks)
// Generate registry file
generateRegistryFile(combinedTools, combinedBlocks)
console.log('Successfully synchronized tools and blocks from main app')
if (!toolsDir) console.log('Warning: Tools directory not found')
if (!blocksDir) console.log('Warning: Blocks directory not found')
} catch (error) {
console.error('Error synchronizing from main app:', error)
// Generate empty registry if failed
generateRegistryFile([], [])
}
}
function findDir(type: 'tools' | 'blocks'): string | null {
const possiblePaths = POSSIBLE_MAIN_APP_PATHS.filter(p => p.includes(`/${type}`))
for (const possiblePath of possiblePaths) {
const fullPath = path.resolve(__dirname, possiblePath)
if (fs.existsSync(fullPath)) {
console.log(`Found ${type} directory at: ${fullPath}`)
return fullPath
}
}
console.warn(`${type} directory not found in any of the possible locations`)
return null
}
function extractToolsRegistry(filePath: string): string[] {
try {
if (!fs.existsSync(filePath)) {
console.warn(`Registry file not found at ${filePath}`)
return []
}
const fileContent = fs.readFileSync(filePath, 'utf8')
const sourceFile = ts.createSourceFile(
'index.ts',
fileContent,
ts.ScriptTarget.Latest,
true
)
// Find the tools registry object
const tools: string[] = []
function visit(node: ts.Node) {
if (
ts.isVariableDeclaration(node) &&
node.name.getText() === 'tools' &&
node.initializer &&
ts.isObjectLiteralExpression(node.initializer)
) {
// Extract tool IDs from the object properties
for (const property of node.initializer.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
tools.push(property.name.text)
}
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
console.log(`Extracted ${tools.length} tools from registry`)
return tools
} catch (error) {
console.error('Error extracting tools from registry:', error)
return []
}
}
function extractBlocksRegistry(filePath: string): string[] {
try {
if (!fs.existsSync(filePath)) {
console.warn(`Registry file not found at ${filePath}`)
return []
}
const fileContent = fs.readFileSync(filePath, 'utf8')
const sourceFile = ts.createSourceFile(
'index.ts',
fileContent,
ts.ScriptTarget.Latest,
true
)
// Find the blocks registry object
const blocks: string[] = []
function visit(node: ts.Node) {
if (
ts.isVariableDeclaration(node) &&
node.name.getText() === 'blocks' &&
node.initializer &&
ts.isObjectLiteralExpression(node.initializer)
) {
// Extract block IDs from the object properties
for (const property of node.initializer.properties) {
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
blocks.push(property.name.text)
}
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
console.log(`Extracted ${blocks.length} blocks from registry`)
return blocks
} catch (error) {
console.error('Error extracting blocks from registry:', error)
return []
}
}
function extractToolsFromDir(dirPath: string): Map<string, string[]> {
const toolParams = new Map<string, string[]>()
// Check subdirectories first
const subdirs = fs.readdirSync(dirPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
console.log(`Found ${subdirs.length} subdirectories in tools directory`)
// Process each subdirectory
for (const subdir of subdirs) {
if (subdir === 'node_modules' || subdir === '__test-utils__') continue
const subdirPath = path.join(dirPath, subdir)
// Look for all .ts files (but not .test.ts)
const files = fs.readdirSync(subdirPath)
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
for (const file of files) {
const filePath = path.join(subdirPath, file)
const extractedTools = extractToolDefinitionFromFile(filePath)
// Merge the results
for (const [toolId, params] of extractedTools.entries()) {
toolParams.set(toolId, params)
}
}
}
// Also process root .ts files
const rootFiles = fs.readdirSync(dirPath)
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts') && file !== 'index.ts')
for (const file of rootFiles) {
const filePath = path.join(dirPath, file)
const extractedTools = extractToolDefinitionFromFile(filePath)
// Merge the results
for (const [toolId, params] of extractedTools.entries()) {
toolParams.set(toolId, params)
}
}
console.log(`Found tool definitions with required parameters for: ${Array.from(toolParams.keys()).join(', ')}`)
return toolParams
}
function extractBlocksFromDir(dirPath: string): Map<string, string[]> {
const blockParams = new Map<string, string[]>()
// Check for blocks subdirectory specifically
const blocksDir = path.join(dirPath, 'blocks')
if (fs.existsSync(blocksDir) && fs.statSync(blocksDir).isDirectory()) {
console.log('Found blocks subdirectory')
// Look for all .ts files (but not .test.ts)
const files = fs.readdirSync(blocksDir)
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
for (const file of files) {
const filePath = path.join(blocksDir, file)
const extractedBlocks = extractBlockDefinitionFromFile(filePath)
// Merge the results
for (const [blockId, params] of extractedBlocks.entries()) {
blockParams.set(blockId, params)
}
}
}
// Also process root .ts files
const rootFiles = fs.readdirSync(dirPath)
.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts') && file !== 'index.ts')
for (const file of rootFiles) {
const filePath = path.join(dirPath, file)
const extractedBlocks = extractBlockDefinitionFromFile(filePath)
// Merge the results
for (const [blockId, params] of extractedBlocks.entries()) {
blockParams.set(blockId, params)
}
}
console.log(`Found block definitions with required parameters for: ${Array.from(blockParams.keys()).join(', ')}`)
return blockParams
}
function extractToolDefinitionFromFile(filePath: string): Map<string, string[]> {
const toolParams = new Map<string, string[]>()
try {
const fileContent = fs.readFileSync(filePath, 'utf8')
const sourceFile = ts.createSourceFile(
path.basename(filePath),
fileContent,
ts.ScriptTarget.Latest,
true
)
// Look for tool definitions (objects with id property)
ts.forEachChild(sourceFile, node => {
if (ts.isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations) {
if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
let toolId = ''
// Find the tool ID
for (const prop of declaration.initializer.properties) {
if (
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === 'id' &&
ts.isStringLiteral(prop.initializer)
) {
toolId = prop.initializer.text
break
}
}
if (!toolId) continue
// Find required parameters
const requiredParams: string[] = []
for (const prop of declaration.initializer.properties) {
if (
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
(prop.name.text === 'params' || prop.name.text === 'parameters') &&
ts.isObjectLiteralExpression(prop.initializer)
) {
// Examine each parameter
for (const param of prop.initializer.properties) {
if (ts.isPropertyAssignment(param) && ts.isIdentifier(param.name)) {
const paramName = param.name.text
if (ts.isObjectLiteralExpression(param.initializer)) {
// Look for requiredForToolCall: true
for (const paramProp of param.initializer.properties) {
if (
ts.isPropertyAssignment(paramProp) &&
ts.isIdentifier(paramProp.name) &&
paramProp.name.text === 'requiredForToolCall' &&
(
(paramProp.initializer.kind === ts.SyntaxKind.TrueKeyword) ||
(ts.isStringLiteral(paramProp.initializer) && paramProp.initializer.text === 'true')
)
) {
requiredParams.push(paramName)
console.log(`Found requiredForToolCall parameter '${paramName}' for tool '${toolId}' in ${filePath}`)
break
}
}
}
}
}
}
}
if (requiredParams.length > 0) {
toolParams.set(toolId, [...new Set(requiredParams)])
}
}
}
}
})
return toolParams
} catch (error) {
console.error(`Error extracting tool definitions from ${filePath}:`, error)
return toolParams
}
}
function extractBlockDefinitionFromFile(filePath: string): Map<string, string[]> {
const blockParams = new Map<string, string[]>()
try {
const fileContent = fs.readFileSync(filePath, 'utf8')
const sourceFile = ts.createSourceFile(
path.basename(filePath),
fileContent,
ts.ScriptTarget.Latest,
true
)
// Look for block definitions (objects with type property)
ts.forEachChild(sourceFile, node => {
if (ts.isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations) {
if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
let blockId = ''
// Find the block type
for (const prop of declaration.initializer.properties) {
if (
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === 'type' &&
ts.isStringLiteral(prop.initializer)
) {
blockId = prop.initializer.text
break
}
}
if (!blockId) continue
// Find required parameters
const requiredParams: string[] = []
for (const prop of declaration.initializer.properties) {
if (
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === 'inputs' &&
ts.isObjectLiteralExpression(prop.initializer)
) {
// Examine each parameter
for (const param of prop.initializer.properties) {
if (ts.isPropertyAssignment(param) && ts.isIdentifier(param.name)) {
const paramName = param.name.text
if (ts.isObjectLiteralExpression(param.initializer)) {
// Look for required: true
for (const paramProp of param.initializer.properties) {
if (
ts.isPropertyAssignment(paramProp) &&
ts.isIdentifier(paramProp.name) &&
paramProp.name.text === 'required' &&
(
(paramProp.initializer.kind === ts.SyntaxKind.TrueKeyword) ||
(ts.isStringLiteral(paramProp.initializer) && paramProp.initializer.text === 'true')
)
) {
requiredParams.push(paramName)
console.log(`Found required parameter '${paramName}' for block '${blockId}' in ${filePath}`)
break
}
}
}
}
}
}
}
if (requiredParams.length > 0) {
blockParams.set(blockId, [...new Set(requiredParams)])
}
}
}
}
})
return blockParams
} catch (error) {
console.error(`Error extracting block definitions from ${filePath}:`, error)
return blockParams
}
}
function mergeToolsInfo(registry: string[], toolParams: Map<string, string[]>): { id: string, requiredParams: string[] }[] {
return registry.map(id => ({
id,
requiredParams: toolParams.get(id) || []
}))
}
function mergeBlocksInfo(registry: string[], blockParams: Map<string, string[]>): { id: string, requiredParams: string[] }[] {
return registry.map(id => ({
id,
requiredParams: blockParams.get(id) || []
}))
}
function generateRegistryFile(
tools: { id: string, requiredParams: string[] }[],
blocks: { id: string, requiredParams: string[] }[]
) {
const outputPath = path.resolve(__dirname, OUTPUT_REGISTRY_PATH)
const content = `/**
* This file is auto-generated. Do not edit directly.
* Generated at ${new Date().toISOString()}
*/
/**
* List of all available tools in the main application
*/
export const availableTools = ${JSON.stringify(tools.map(t => t.id), null, 2)}
/**
* List of all available blocks in the main application
*/
export const availableBlocks = ${JSON.stringify(blocks.map(b => b.id), null, 2)}
/**
* Map of tool IDs to their block types
*/
export const toolToBlockMap: Record<string, string> = {
${tools.map(tool => {
// Convert tool_name to ToolNameBlock format
const blockName = tool.id.split('_')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('') + 'Block'
return ` '${tool.id}': '${blockName}'`
}).join(',\n')}
}
/**
* Required parameters for each tool
*/
export const toolRequiredParameters: Record<string, string[]> = {
${tools.map(tool => ` '${tool.id}': ${JSON.stringify(tool.requiredParams)}`).join(',\n')}
}
/**
* Required parameters for each block
*/
export const blockRequiredParameters: Record<string, string[]> = {
${blocks.map(block => ` '${block.id}': ${JSON.stringify(block.requiredParams)}`).join(',\n')}
}
/**
* Check if a tool is available
*/
export function isToolAvailable(toolId: string): boolean {
return availableTools.includes(toolId)
}
/**
* Check if a block is available
*/
export function isBlockAvailable(blockId: string): boolean {
return availableBlocks.includes(blockId)
}
/**
* Get the block type for a tool
*/
export function getBlockTypeForTool(toolId: string): string | undefined {
return toolToBlockMap[toolId]
}
/**
* Get required parameters for a tool
*/
export function getToolRequiredParameters(toolId: string): string[] {
return toolRequiredParameters[toolId] || []
}
/**
* Get required parameters for a block
*/
export function getBlockRequiredParameters(blockId: string): string[] {
return blockRequiredParameters[blockId] || []
}
`
fs.writeFileSync(outputPath, content)
console.log(`Generated registry file at ${outputPath}`)
}
// Run the sync if this file is executed directly
if (require.main === module) {
syncFromMainApp().catch(console.error)
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Tool registry for Sim Studio SDK
*/
import { availableTools } from '../generated/index'
export interface Tool {
id: string
name: string
description: string
schema: any
parameters?: any
requiredParameters?: string[] // Parameters that must be provided for the tool to function
execute(params: any): Promise<any>
}
/**
* Registry for all available tool types
*/
export class ToolRegistry {
private static tools: Map<string, Tool> = new Map()
/**
* Register a tool
*/
static register(tool: Tool): void {
this.tools.set(tool.id, tool)
}
/**
* Get a tool by ID
*/
static get(id: string): Tool | undefined {
return this.tools.get(id)
}
/**
* Check if a tool is registered
*/
static has(id: string): boolean {
return this.tools.has(id)
}
/**
* Get all registered tools
*/
static getAll(): Tool[] {
return Array.from(this.tools.values())
}
/**
* Import tools from the main application
*/
static importTools(tools: Tool[]): void {
tools.forEach(tool => {
this.register(tool)
})
}
/**
* Check if a tool is available in the main application
*/
static isAvailableInMainApp(id: string): boolean {
return availableTools.includes(id)
}
/**
* Get all available tool IDs from the main application
*/
static getAllAvailableTools(): string[] {
return [...availableTools]
}
}
/**
* Create a tool definition
*/
export function createTool(tool: Tool): Tool {
return tool
}
export * from './registry'
+45
View File
@@ -0,0 +1,45 @@
import { Tool } from './index'
/**
* Interface for tool imports from the main application
*/
export interface ToolImport {
id: string
name: string
description: string
schema: any
execute: (params: any) => Promise<any>
}
/**
* Create a dynamic adapter for importing tools from the main application
* This serves as a bridge between the main app's tools and our SDK
*/
export function createToolAdapter(tool: ToolImport): Tool {
return {
id: tool.id,
name: tool.name,
description: tool.description,
schema: tool.schema,
execute: tool.execute
}
}
/**
* Create a tool factory for a specific tool type
*/
export function createToolFactory<T extends Record<string, any>>(
id: string,
name: string,
description: string,
schema: any,
executeFunction: (params: T) => Promise<any>
): (params?: Partial<T>) => Tool {
return (params: Partial<T> = {}) => ({
id,
name,
description,
schema,
execute: (inputParams: any) => executeFunction({ ...params, ...inputParams })
})
}
+127
View File
@@ -0,0 +1,127 @@
/**
* Core types for the Sim Studio SDK
*/
// API Configuration
export interface SimStudioConfig {
apiKey?: string
baseUrl?: string
timeout?: number
}
// Workflow definition types
export interface Workflow {
id?: string
name: string
description?: string
blocks: Block[]
connections: Connection[]
loops?: Record<string, Loop>
metadata?: Record<string, any>
}
export interface Block {
id: string
type: BlockType
data: BlockData
position?: Position
enabled?: boolean
metadata?: {
name?: string
description?: string
id?: string
[key: string]: any
}
}
// Core block types
export type CoreBlockType =
| 'starter'
| 'agent'
| 'function'
| 'condition'
| 'router'
| 'api'
| 'evaluator'
| 'generic'
// Custom block types can be any string
export type BlockType = CoreBlockType | string
export interface BlockData {
[key: string]: any
}
export interface Connection {
id?: string
source: string
target: string
sourceHandle?: string
targetHandle?: string
}
export interface Loop {
nodes: string[]
maxIterations: number
iterationVariable?: string
}
export interface Position {
x: number
y: number
}
// Execution result types
export interface ExecutionResult {
success: boolean
output: any
error?: string
logs?: ExecutionLog[]
metadata?: {
startTime?: string
endTime?: string
duration?: number
}
}
export interface ExecutionLog {
blockId: string
blockName?: string
blockType?: string
startedAt: string
endedAt: string
durationMs: number
success: boolean
output?: any
error?: string
}
// Deployment types
export interface DeploymentOptions {
isPublic?: boolean
authentication?: 'none' | 'api_key' | 'oauth'
rateLimit?: number
}
export interface DeploymentResult {
id: string
url: string
status: 'active' | 'inactive'
createdAt: string
}
// Schedule types
export interface ScheduleOptions {
cron: string
timezone?: string
input?: Record<string, any>
enabled?: boolean
}
export interface ScheduleResult {
id: string
workflowId: string
cron: string
nextRunAt: string
status: 'active' | 'inactive'
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Utility functions for Sim Studio SDK
*/
/**
* Safely parse a JSON string
*/
export function safeJsonParse<T>(jsonString: string, fallback: T): T {
try {
return JSON.parse(jsonString) as T
} catch (error) {
return fallback
}
}
/**
* Create a template string with variable interpolation
*/
export function template(str: string, variables: Record<string, any>): string {
return str.replace(/\{\{([^}]+)\}\}/g, (_, path) => {
const trimmedPath = path.trim()
return getValueByPath(variables, trimmedPath)
})
}
/**
* Get a nested value from an object using dot notation
*/
export function getValueByPath(obj: any, path: string): any {
const parts = path.split('.')
let current = obj
for (const part of parts) {
if (current === null || current === undefined) {
return undefined
}
current = current[part]
}
return current
}
/**
* Generate a unique ID
*/
export function generateId(prefix = ''): string {
return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`
}
/**
* Deep clone an object
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (Array.isArray(obj)) {
return obj.map(deepClone) as unknown as T
}
const clone: Record<string, any> = {}
for (const [key, value] of Object.entries(obj as Record<string, any>)) {
clone[key] = deepClone(value)
}
return clone as T
}
/**
* Check if two objects are deeply equal
*/
export function deepEquals(a: any, b: any): boolean {
if (a === b) {
return true
}
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return false
}
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) {
return false
}
return keysA.every(key => keysB.includes(key) && deepEquals(a[key], b[key]))
}
/**
* Format a date string in ISO format
*/
export function formatDate(date: Date = new Date()): string {
return date.toISOString()
}
/**
* Convert seconds to a human-readable duration
*/
export function formatDuration(seconds: number): string {
if (seconds < 60) {
return `${Math.round(seconds)}s`
}
if (seconds < 3600) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.round(seconds % 60)
return `${minutes}m ${remainingSeconds}s`
}
const hours = Math.floor(seconds / 3600)
const remainingMinutes = Math.floor((seconds % 3600) / 60)
return `${hours}h ${remainingMinutes}m`
}
+107
View File
@@ -0,0 +1,107 @@
import { Workflow, Connection, Loop, Block as BlockType } from '../types'
import { Block } from '../blocks/base'
import { StarterBlock } from '../blocks/starter'
/**
* Builder class for creating workflows programmatically
*/
export class WorkflowBuilder {
private workflow: Workflow
private blockMap: Map<string, Block>
/**
* Create a new workflow builder
*/
constructor(name: string, description?: string) {
this.workflow = {
name,
description,
blocks: [],
connections: [],
loops: {},
}
this.blockMap = new Map()
// Add a starter block by default
const starterBlock = new StarterBlock()
this.addBlock(starterBlock)
}
/**
* Add a block to the workflow
*/
addBlock(block: Block): this {
this.blockMap.set(block.id, block)
this.workflow.blocks.push(block.toJSON())
return this
}
/**
* Connect two blocks
*/
connect(sourceId: string, targetId: string, options: {
sourceHandle?: string
targetHandle?: string
} = {}): this {
const connection: Connection = {
source: sourceId,
target: targetId,
...options,
}
this.workflow.connections.push(connection)
return this
}
/**
* Create a loop with specific blocks
*/
createLoop(nodes: string[], maxIterations: number, iterationVariable?: string): this {
const loopId = `loop_${Date.now()}`
const loop: Loop = {
nodes,
maxIterations,
...(iterationVariable ? { iterationVariable } : {})
}
this.workflow.loops = {
...this.workflow.loops,
[loopId]: loop
}
return this
}
/**
* Position a block at specific coordinates
*/
positionBlock(blockId: string, x: number, y: number): this {
const block = this.workflow.blocks.find(b => b.id === blockId)
if (block) {
block.position = { x, y }
}
return this
}
/**
* Get the workflow definition
*/
build(): Workflow {
return this.workflow
}
/**
* Find a block by name
*/
findBlockByName(name: string): BlockType | undefined {
return this.workflow.blocks.find(block => block.metadata?.name === name)
}
/**
* Get the starter block
*/
getStarterBlock(): BlockType {
const starterBlock = this.workflow.blocks.find(block => block.metadata?.id === 'starter')
if (!starterBlock) {
throw new Error('Starter block not found in workflow')
}
return starterBlock
}
}
+1
View File
@@ -0,0 +1 @@
export { WorkflowBuilder } from './builder'
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node", "jest"]
},
"include": [
"src/**/*",
"examples/**/*"
],
"exclude": [
"node_modules",
"**/*.test.ts"
]
}
+47
View File
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server'
import { and, eq } from 'drizzle-orm'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { apiKey } from '@/app/db/schema'
import { db } from '@/db'
const logger = createLogger('ApiKeyRoute')
// DELETE /api/user/api-keys/[id] - Delete an API key
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const { id } = await params
try {
logger.debug(`[${requestId}] Deleting API key: ${id}`)
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const keyId = id
if (!keyId) {
return NextResponse.json({ error: 'API key ID is required' }, { status: 400 })
}
// Delete the API key, ensuring it belongs to the current user
const result = await db
.delete(apiKey)
.where(and(eq(apiKey.id, keyId), eq(apiKey.userId, userId)))
.returning({ id: apiKey.id })
if (!result.length) {
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
}
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Failed to delete API key', { error })
return NextResponse.json({ error: 'Failed to delete API key' }, { status: 500 })
}
}
+90
View File
@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from 'next/server'
import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { apiKey } from '@/app/db/schema'
import { db } from '@/db'
const logger = createLogger('ApiKeysRoute')
// GET /api/user/api-keys - Get all API keys for the current user
export async function GET(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
// Fetch all API keys for this user
const keys = await db
.select({
id: apiKey.id,
name: apiKey.name,
key: apiKey.key,
createdAt: apiKey.createdAt,
lastUsed: apiKey.lastUsed,
expiresAt: apiKey.expiresAt,
})
.from(apiKey)
.where(eq(apiKey.userId, userId))
.orderBy(apiKey.createdAt)
const maskedKeys = keys.map((key) => ({
...key,
key: key.key,
}))
return NextResponse.json({ keys: maskedKeys })
} catch (error) {
logger.error('Failed to fetch API keys', { error })
return NextResponse.json({ error: 'Failed to fetch API keys' }, { status: 500 })
}
}
// POST /api/user/api-keys - Create a new API key
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const body = await request.json()
// Validate the request
const { name } = body
if (!name || typeof name !== 'string') {
return NextResponse.json({ error: 'Invalid request. Name is required.' }, { status: 400 })
}
// Generate an API key - we'll use a prefix to identify this as an API key (sim_)
const keyValue = `sim_${nanoid(32)}`
// Insert the new API key
const [newKey] = await db
.insert(apiKey)
.values({
id: nanoid(),
userId,
name,
key: keyValue,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning({
id: apiKey.id,
name: apiKey.name,
key: apiKey.key,
createdAt: apiKey.createdAt,
})
return NextResponse.json({ key: newKey })
} catch (error) {
logger.error('Failed to create API key', { error })
return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 })
}
}
+1 -4
View File
@@ -1,5 +1,4 @@
import { ConditionalIcon } from '@/components/icons'
import { CodeExecutionOutput } from '@/tools/function/execute'
import { BlockConfig } from '../types'
interface ConditionBlockOutput {
@@ -35,9 +34,7 @@ export const ConditionBlock: BlockConfig<ConditionBlockOutput> = {
tools: {
access: ['function_execute'],
},
inputs: {
code: { type: 'string', required: true },
},
inputs: {},
outputs: {
response: {
type: {
@@ -0,0 +1,13 @@
CREATE TABLE "api_key" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"name" text NOT NULL,
"key" text NOT NULL,
"last_used" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"expires_at" timestamp,
CONSTRAINT "api_key_key_unique" UNIQUE("key")
);
--> statement-breakpoint
ALTER TABLE "api_key" ADD CONSTRAINT "api_key_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,897 @@
{
"id": "f71b2569-b14c-40aa-aae2-a65fd1c1978e",
"prevId": "41e64bff-e76e-4af6-aebd-1d2dd3db72f9",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.api_key": {
"name": "api_key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_used": {
"name": "last_used",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"api_key_user_id_user_id_fk": {
"name": "api_key_user_id_user_id_fk",
"tableFrom": "api_key",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_key_key_unique": {
"name": "api_key_key_unique",
"nullsNotDistinct": false,
"columns": ["key"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.environment": {
"name": "environment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"variables": {
"name": "variables",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"environment_user_id_user_id_fk": {
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"general": {
"name": "general",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"settings_user_id_user_id_fk": {
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.webhook": {
"name": "webhook",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": false
},
"provider_config": {
"name": "provider_config",
"type": "json",
"primaryKey": false,
"notNull": false
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"path_idx": {
"name": "path_idx",
"columns": [
{
"expression": "path",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"webhook_workflow_id_workflow_id_fk": {
"name": "webhook_workflow_id_workflow_id_fk",
"tableFrom": "webhook",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow": {
"name": "workflow",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "json",
"primaryKey": false,
"notNull": true
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'#3972F6'"
},
"last_synced": {
"name": "last_synced",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"is_deployed": {
"name": "is_deployed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"deployed_at": {
"name": "deployed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"api_key": {
"name": "api_key",
"type": "text",
"primaryKey": false,
"notNull": false
},
"collaborators": {
"name": "collaborators",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'[]'"
}
},
"indexes": {},
"foreignKeys": {
"workflow_user_id_user_id_fk": {
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_logs": {
"name": "workflow_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_id": {
"name": "execution_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"duration": {
"name": "duration",
"type": "text",
"primaryKey": false,
"notNull": false
},
"trigger": {
"name": "trigger",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"metadata": {
"name": "metadata",
"type": "json",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_logs_workflow_id_workflow_id_fk": {
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_schedule": {
"name": "workflow_schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cron_expression": {
"name": "cron_expression",
"type": "text",
"primaryKey": false,
"notNull": false
},
"next_run_at": {
"name": "next_run_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_ran_at": {
"name": "last_ran_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"trigger_type": {
"name": "trigger_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_schedule_workflow_id_workflow_id_fk": {
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": ["workflow_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -120,6 +120,13 @@
"when": 1742197359430,
"tag": "0016_cultured_butterfly",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1742342896119,
"tag": "0017_curious_ink",
"breakpoints": true
}
]
}
+13
View File
@@ -145,3 +145,16 @@ export const webhook = pgTable(
}
}
)
export const apiKey = pgTable('api_key', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
key: text('key').notNull().unique(),
lastUsed: timestamp('last_used'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
expiresAt: timestamp('expires_at'),
})
@@ -0,0 +1,370 @@
'use client'
import { useEffect, useState } from 'react'
import { AlertCircle, Check, Copy, KeySquare, Plus, Trash2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
const logger = createLogger('ApiKeys')
interface ApiKeysProps {
onOpenChange?: (open: boolean) => void
}
interface ApiKey {
id: string
name: string
key: string
lastUsed?: string
createdAt: string
expiresAt?: string
}
export function ApiKeys({ onOpenChange }: ApiKeysProps) {
const { data: session } = useSession()
const userId = session?.user?.id
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isCreating, setIsCreating] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
const [newKey, setNewKey] = useState<ApiKey | null>(null)
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
const [deleteKey, setDeleteKey] = useState<ApiKey | null>(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [copySuccess, setCopySuccess] = useState(false)
// Fetch API keys
const fetchApiKeys = async () => {
if (!userId) return
setIsLoading(true)
try {
const response = await fetch('/api/user/api-keys')
if (response.ok) {
const data = await response.json()
setApiKeys(data.keys || [])
}
} catch (error) {
logger.error('Error fetching API keys:', { error })
} finally {
setIsLoading(false)
}
}
// Generate a new API key
const handleCreateKey = async () => {
if (!userId || !newKeyName.trim()) return
setIsCreating(true)
try {
const response = await fetch('/api/user/api-keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newKeyName.trim(),
}),
})
if (response.ok) {
const data = await response.json()
// Show the new key dialog with the API key (only shown once)
setNewKey(data.key)
setShowNewKeyDialog(true)
// Reset form
setNewKeyName('')
// Refresh the keys list
fetchApiKeys()
}
} catch (error) {
logger.error('Error creating API key:', { error })
} finally {
setIsCreating(false)
}
}
// Delete an API key
const handleDeleteKey = async () => {
if (!userId || !deleteKey) return
try {
const response = await fetch(`/api/user/api-keys/${deleteKey.id}`, {
method: 'DELETE',
})
if (response.ok) {
// Refresh the keys list
fetchApiKeys()
// Close the dialog
setShowDeleteDialog(false)
setDeleteKey(null)
}
} catch (error) {
logger.error('Error deleting API key:', { error })
}
}
// Copy API key to clipboard
const copyToClipboard = (key: string) => {
navigator.clipboard.writeText(key)
setCopySuccess(true)
setTimeout(() => setCopySuccess(false), 2000)
}
// Load API keys on mount
useEffect(() => {
if (userId) {
fetchApiKeys()
}
}, [userId])
// Format date
const formatDate = (dateString?: string) => {
if (!dateString) return 'Never'
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">API Keys</h2>
<Button
onClick={() => setIsCreating(true)}
disabled={isLoading}
size="sm"
className="gap-1.5"
>
<Plus className="h-4 w-4" />
Create Key
</Button>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
API keys allow you to authenticate with the Sim SDK. Keep your API keys secure. They have
access to your account and workflows.
</p>
{isLoading ? (
<div className="space-y-3 mt-6">
<KeySkeleton />
<KeySkeleton />
</div>
) : apiKeys.length === 0 ? (
<div className="rounded-md border border-dashed p-8 mt-6">
<div className="flex flex-col items-center justify-center text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<KeySquare className="h-6 w-6 text-primary" />
</div>
<h3 className="mt-4 text-lg font-medium">No API keys yet</h3>
<p className="mt-2 text-sm text-muted-foreground max-w-sm">
You don&apos;t have any API keys yet. Create one to get started with the Sim SDK.
</p>
<Button
variant="default"
className="mt-4"
onClick={() => setIsCreating(true)}
size="sm"
>
<Plus className="h-4 w-4 mr-1.5" /> Create API Key
</Button>
</div>
</div>
) : (
<div className="space-y-4 mt-6">
{apiKeys.map((key) => (
<Card key={key.id} className="p-4 hover:shadow-sm transition-shadow">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="font-medium text-base">{key.name}</h3>
<div className="flex items-center space-x-1">
<p className="text-xs text-muted-foreground">
Created: {formatDate(key.createdAt)} • Last used: {formatDate(key.lastUsed)}
</p>
<div className="text-xs px-1.5 py-0.5 bg-muted/50 rounded font-mono">
•••••{key.key.slice(-6)}
</div>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
setDeleteKey(key)
setShowDeleteDialog(true)
}}
className="text-destructive hover:bg-destructive/10 h-8 w-8"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete key</span>
</Button>
</div>
</Card>
))}
</div>
)}
{/* Create API Key Dialog */}
<Dialog open={isCreating} onOpenChange={setIsCreating}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Create new API key</DialogTitle>
<DialogDescription>
Name your API key to help you identify it later. This key will have access to your
account and workflows.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-3">
<div className="space-y-2">
<Label htmlFor="keyName">API Key Name</Label>
<Input
id="keyName"
placeholder="e.g., Development, Production, etc."
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
className="focus-visible:ring-primary"
/>
</div>
</div>
<DialogFooter className="sm:justify-end gap-2">
<Button variant="outline" onClick={() => setIsCreating(false)}>
Cancel
</Button>
<Button onClick={handleCreateKey} disabled={!newKeyName.trim()}>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* New API Key Dialog */}
<Dialog
open={showNewKeyDialog}
onOpenChange={(open) => {
setShowNewKeyDialog(open)
if (!open) setNewKey(null)
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Your API key has been created</DialogTitle>
<DialogDescription>
This is the only time you will see your API key. Copy it now and store it securely.
</DialogDescription>
</DialogHeader>
{newKey && (
<div className="space-y-4 py-3">
<div className="space-y-2">
<Label>API Key</Label>
<div className="relative">
<Input
readOnly
value={newKey.key}
className="font-mono text-sm pr-10 bg-muted/50 border-slate-300"
/>
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
onClick={() => copyToClipboard(newKey.key)}
>
{copySuccess ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
<span className="sr-only">Copy to clipboard</span>
</Button>
</div>
<p className="text-xs text-muted-foreground mt-1">
For security, we don&apos;t store the complete key. You won&apos;t be able to view
it again.
</p>
</div>
</div>
)}
<DialogFooter className="sm:justify-end">
<Button
onClick={() => {
setShowNewKeyDialog(false)
setNewKey(null)
}}
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Delete API Key</AlertDialogTitle>
<AlertDialogDescription>
{deleteKey && (
<>
Are you sure you want to delete the API key{' '}
<span className="font-semibold">{deleteKey.name}</span>? This action cannot be
undone and any integrations using this key will no longer work.
</>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="sm:justify-end gap-2">
<AlertDialogCancel onClick={() => setDeleteKey(null)}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteKey}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
function KeySkeleton() {
return (
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-5 w-32 mb-2" />
<Skeleton className="h-4 w-48" />
</div>
<Skeleton className="h-8 w-8 rounded-md" />
</div>
</Card>
)
}
@@ -1,9 +1,11 @@
import { Key, KeyRound, Settings, UserCircle } from 'lucide-react'
import { Key, KeyRound, KeySquare, Settings, UserCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
interface SettingsNavigationProps {
activeSection: string
onSectionChange: (section: 'general' | 'environment' | 'account' | 'credentials') => void
onSectionChange: (
section: 'general' | 'environment' | 'account' | 'credentials' | 'apikeys'
) => void
}
const navigationItems = [
@@ -27,6 +29,11 @@ const navigationItems = [
label: 'Credentials',
icon: Key,
},
{
id: 'apikeys',
label: 'API Keys',
icon: KeySquare,
},
] as const
export function SettingsNavigation({ activeSection, onSectionChange }: SettingsNavigationProps) {
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import { Account } from './components/account/account'
import { ApiKeys } from './components/api-keys/api-keys'
import { Credentials } from './components/credentials/credentials'
import { EnvironmentVariables } from './components/environment/environment'
import { General } from './components/general/general'
@@ -16,7 +17,7 @@ interface SettingsModalProps {
onOpenChange: (open: boolean) => void
}
type SettingsSection = 'general' | 'environment' | 'account' | 'credentials'
type SettingsSection = 'general' | 'environment' | 'account' | 'credentials' | 'apikeys'
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
@@ -75,6 +76,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
<div className={cn('h-full', activeSection === 'credentials' ? 'block' : 'hidden')}>
<Credentials onOpenChange={onOpenChange} />
</div>
<div className={cn('h-full', activeSection === 'apikeys' ? 'block' : 'hidden')}>
<ApiKeys onOpenChange={onOpenChange} />
</div>
</div>
</div>
</DialogContent>