@ampcode/sdk
v0.1.0-20260918210405-g81edbf0
Published
TypeScript SDK for Amp CLI - Build custom AI agents with Amp's capabilities
Readme
Amp TypeScript SDK
Use the Amp SDK to programmatically deploy the Amp agent anywhere you run TypeScript. Execute Amp CLI commands programmatically with full type safety, streaming responses, and complete control over your AI coding agent workflows.
Why use the Amp SDK?
The Amp TypeScript SDK brings the Amp agent directly into your applications with simple, reliable functionality:
- Stream Inputs: Send prompts and messages incrementally to the Amp agent
- Stream Outputs: Receive structured JSON responses (system, assistant, result) as the agent executes tasks
- Multi-turn Conversations: Maintain back-and-forth interactions across multiple executions
- Thread Continuity: Continue an existing thread (latest or by ID) to build stateful agent workflows
- Programmatic Settings: Configure working directories, settings, and tools without user prompts — ideal for automation
- MCP Integration: Extend Amp with custom Model Context Protocol servers and tools
- Custom Skills: Define and use custom agent skills to extend Amp's functionality
What can you build?
The Amp SDK enables a wide range of AI-powered applications:
Development Tools
- Code Review Agent: Automated pull request analysis and feedback
- Documentation Generator: Create and maintain project documentation
- Test Automation: Generate and execute test suites
- Migration Assistant: Help upgrade codebases and refactor legacy code
Workflow Automation
- CI/CD Integration: Smart build and deployment pipelines
- Issue Triage: Automatically categorize and prioritize bug reports
- Code Quality Monitoring: Continuous analysis of code health metrics
- Release Management: Automated changelog generation and version bumping
Quick Start
Installation
# Install the Amp SDK using npm
npm install @ampcode/sdk
# or yarn
yarn add @ampcode/sdk
# Optional: manually install the Amp CLI, already a dependency
npx -y @ampcode/sdk installIf you need to use Amp before Amp Neo, install the legacy SDK release
@ampcode/[email protected]:
npm install @ampcode/[email protected]The SDK requires an Amp CLI version that is at least the version pinned by your SDK release. If your organization installs Amp CLI through Homebrew, Artifactory, or another internal distribution channel, you can keep using that installation as long as its version is new enough.
Once installed, add your API key to the environment. You can access your API key in Security Settings.
export AMP_API_KEY=sgamp_your_api_key_hereYour First Amp Command
Now that you have the SDK installed and your API key set up, you can start using Amp with the execute() function:
import { execute } from '@ampcode/sdk'
// Simple execution - get the final result
for await (const message of execute({ prompt: 'What files are in this directory?' })) {
if (message.type === 'result' && !message.is_error) {
console.log('Result:', message.result)
break
}
}The execute() function only requires that you provide a prompt to get started. The SDK streams messages as the agent works, letting you handle responses and integrate them directly into your application.
Core Concepts
Message Streaming
The SDK streams different types of messages as your agent executes:
for await (const message of execute({ prompt: 'Run tests' })) {
if (message.type === 'system') {
// Session info, available tools, MCP servers
console.log('Available tools:', message.tools)
} else if (message.type === 'assistant') {
// AI responses and tool usage
console.log('Assistant is working...')
} else if (message.type === 'result') {
// Final result (success or error)
console.log('Done:', message.result)
}
}Simple Result Extraction
When you just need the final result without handling streaming:
async function getResult(prompt: string): Promise<string> {
for await (const message of execute({ prompt })) {
if (message.type === 'result') {
if (message.is_error) {
throw new Error(message.error)
}
return message.result
}
}
throw new Error('No result received')
}
// Usage
try {
const result = await getResult('List all TypeScript files in this project')
console.log('Found files:', result)
} catch (error) {
console.error('Failed:', error.message)
}Thread Continuity
Continue conversations across multiple interactions:
// Continue the most recent conversation
for await (const message of execute({
prompt: 'What was the last error you found?',
options: { continue: true },
})) {
if (message.type === 'result') {
console.log(message.result)
}
}
// Continue a specific thread by ID
for await (const message of execute({
prompt: 'Can you update that code we discussed?',
options: { continue: 'T-abc123-def456' },
})) {
if (message.type === 'result') {
console.log(message.result)
}
}Common Configuration
Working Directory
Specify where Amp should run:
for await (const message of execute({
prompt: 'Refactor the auth module',
options: { cwd: './my-project' },
})) {
// Process messages...
}Enable Debug Logging
See what's happening under the hood:
for await (const message of execute({
prompt: 'Analyze this project',
options: {
logLevel: 'debug', // Shows CLI command in console
logFile: './amp-debug.log', // Optional: write logs to file
},
})) {
// Process messages
}Agent Mode
Select which agent mode to use. The mode controls the model, system prompt, and tool selection:
for await (const message of execute({
prompt: 'Quickly fix this typo',
options: {
mode: 'low', // Use low mode for faster responses
},
})) {
// Process messages
}Available modes:
low: Fast, low-cost mode for small, well-defined tasksmedium(default): Default for most tasks, balancing quality, speed, and costhigh: Use for difficult tasks that need deeper reasoning and can tolerate more time and costultra: Use for the hardest open-ended tasks when maximum capability matters more than speed or cost
Reasoning Effort
Set model reasoning effort for supported modes:
for await (const message of execute({
prompt: 'Think carefully and explain your plan before coding.',
options: {
mode: 'medium',
effort: 'high',
},
})) {
if (message.type === 'result') {
console.log(message.result)
break
}
}Available effort levels:
noneminimallowmediumhighxhighmax
Thread Labels
Add labels to threads created by execute().
for await (const message of execute({
prompt: 'Summarize this repo',
options: {
labels: ['sdk', 'summary'],
},
})) {
if (message.type === 'result') {
console.log(message.result)
break
}
}Thread Titles
Set the title when you create a thread. You cannot use title when continuing a thread.
for await (const message of execute({
prompt: 'Review pull request 42',
options: {
executor: 'orb',
title: 'Review acme/widgets#42',
},
})) {
if (message.type === 'result') {
console.log(message.result)
break
}
}Remote Executors
By default the agent runs in the local Amp CLI process. Set executor: 'orb' to run it in an Amp
cloud sandbox, or executor: 'runner' with runnerId to run it on an Amp CLI runner you started
with amp --no-tui --runner-id <id>. In both cases the local process only streams the results.
Requires AMP_API_KEY.
for await (const message of execute({
prompt: 'Run the integration tests and summarize any failures',
options: {
executor: 'runner',
runnerId: 'build-box-1',
runnerDir: '/home/build/code/my-project',
},
})) {
if (message.type === 'result') {
console.log(message.result)
break
}
}runnerDir selects one absolute directory served by the runner. Start the runner
with --dir /home/build/code/my-project, or register the directory through runner discovery or
amp runner dirs add. Omit the option to use the runner's starting directory.
Each thread has one working directory. To run in several directories, make separate execute()
calls. Continuing a thread keeps its existing directory and ignores this option. cwd only sets
the local CLI subprocess directory; it does not select a directory on the runner.
project applies only to executor: 'orb'.
Local-only options (permissions, enabledTools, skills, mcpConfig, dangerouslyAllowAll)
and streaming input prompts do not apply to remote executors. Plugin agent modes must be loaded on
the runner.
Thread Visibility
Control who can see threads created by execute():
for await (const message of execute({
prompt: 'Analyze this private codebase',
options: {
visibility: 'private', // Only you can see this thread
},
})) {
if (message.type === 'result') {
console.log(message.result)
break
}
}Available visibility levels:
workspace(default): Visible to all workspace membersprivate: Only visible to youunlisted: Visible to anyone with the linkgroup: Visible to members of your user group (Enterprise)
Tool Permissions
Use a custom plugin to control tool use. Workspace admins can distribute it as a global workspace plugin.
Advanced Usage
Interactive Progress Tracking
For building user interfaces that show real-time progress:
async function executeWithProgress(prompt: string) {
console.log('Starting task...')
for await (const message of execute({ prompt })) {
if (message.type === 'system' && message.subtype === 'init') {
console.log('Tools available:', message.tools.join(', '))
} else if (message.type === 'assistant') {
// Show tool usage or assistant responses
const content = message.message.content[0]
if (content.type === 'tool_use') {
console.log(`Using ${content.name}...`)
} else if (content.type === 'text') {
console.log('Assistant:', content.text.slice(0, 100) + '...')
}
} else if (message.type === 'result') {
if (message.is_error) {
console.log('Failed:', message.error)
} else {
console.log('Completed successfully!')
console.log(message.result)
}
}
}
}Cancellation and Timeouts
Handle long-running operations gracefully:
async function executeWithTimeout(prompt: string, timeoutMs = 30000) {
const signal = AbortSignal.timeout(timeoutMs)
try {
for await (const message of execute({
prompt,
signal,
})) {
if (message.type === 'result') {
return message.result
}
}
} catch (error) {
if (error.message.includes('aborted')) {
throw new Error(`Operation timed out after ${timeoutMs}ms`)
}
throw error
}
}MCP (Model Context Protocol) Integration
Extend Amp's capabilities with custom tools and data sources:
import { execute, type MCPConfig } from '@ampcode/sdk'
const mcpConfig: MCPConfig = {
playwright: {
command: 'npx',
args: ['-y', '@playwright/mcp@latest', '--headless'],
env: { NODE_ENV: 'production' },
},
database: {
command: 'node',
args: ['./custom-mcp-server.js'],
env: { DB_CONNECTION_STRING: '${DATABASE_URL}' },
},
}
for await (const message of execute({
prompt: 'Test the login flow on staging environment',
options: { mcpConfig },
})) {
if (message.type === 'system') {
console.log(
'MCP Servers:',
message.mcp_servers.map((s) => `${s.name}: ${s.status}`),
)
}
// Handle other messages...
}Prefer ${VAR} placeholders for secrets such as connection strings and API tokens. Amp expands them from its environment when starting each MCP server, so the secret values never need to appear in the configuration itself. Placeholders work in stdio command, args, and env values, and in remote server url and headers values.
To find out more about extending Amp with MCP servers, visit the MCP Configuration section of the docs.
Multi-turn Conversations
Build streaming conversations using async generators:
import { execute, createUserMessage } from '@ampcode/sdk'
async function* generateMessages() {
yield createUserMessage('Start analyzing the codebase')
// Wait for some condition or user input
await new Promise((resolve) => setTimeout(resolve, 1000))
yield createUserMessage('Now focus on the authentication module')
}
for await (const message of execute({
prompt: generateMessages(),
})) {
if (message.type === 'result') {
console.log(message.result)
}
}Settings File Configuration
Configure Amp's behavior with a settings file, like the settings.json. You can provide Amp with a custom settings file you have saved in your project:
import { execute } from '@ampcode/sdk'
// Use a custom settings file
for await (const message of execute({
prompt: 'Deploy the application',
options: {
settingsFile: './settings.json',
logLevel: 'debug',
},
})) {
// Handle messages...
}Example settings.json:
{
"amp.mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
}
},
"amp.tools.disable": ["web_search", "mcp__playwright__browser_resize"]
}To find all available settings, see the Configuration Settings.
Custom Skills
Load custom skills from a specified directory:
for await (const message of execute({
prompt: 'Use my custom deployment skill',
options: {
skills: './my-skills', // Path to custom skills directory
},
})) {
// Process messages
}To learn more about creating custom skills, see the Agent Skills section of the Amp documentation.
Functions
execute()
The main function for executing Amp CLI commands programmatically.
function execute(options: ExecuteOptions): AsyncIterable<StreamMessage>Parameters
options(ExecuteOptions) - Configuration for the execution
Returns
AsyncIterable<StreamMessage>- Stream of messages from the Amp CLI
Example
import { execute } from '@ampcode/sdk'
for await (const message of execute({
prompt: 'Analyze this codebase',
options: {
cwd: './my-project',
},
})) {
if (message.type === 'assistant') {
console.log('Assistant:', message.message.content)
} else if (message.type === 'result') {
console.log('Final result:', message.result)
break
}
}createUserMessage()
Helper function to create properly formatted user input messages for streaming conversations.
function createUserMessage(text: string, options?: { requestId?: string }): UserInputMessageParameters
text(string) - The text content for the user messageoptions.requestId(string, optional) - A 1–256 character ID. Reusing it for a retry on the same thread prevents Amp from creating another user message.
Returns
UserInputMessage- A formatted user input message
Example
import { createUserMessage } from '@ampcode/sdk'
const message = createUserMessage('Analyze this code', { requestId: 'analysis-123' })
console.log(message)
// Output: { type: 'user', requestId: 'analysis-123', message: { role: 'user', content: [{ type: 'text', text: 'Analyze this code' }] } }threads.new()
Create a new empty thread and return its ID.
async function threads.new(options?: ThreadsNewOptions): Promise<string>Parameters
options(ThreadsNewOptions, optional) - Configuration for the new thread
Returns
Promise<string>- The thread ID
Example
import { threads } from '@ampcode/sdk'
// Create a new private thread
const threadId = await threads.new({ visibility: 'private' })
console.log('Created thread:', threadId)threads.markdown()
Get a thread rendered as markdown.
async function threads.markdown(options: ThreadsMarkdownOptions): Promise<string>Parameters
options(ThreadsMarkdownOptions) - Options containing the thread ID
Returns
Promise<string>- The thread content as markdown
Example
import { threads } from '@ampcode/sdk'
// Get thread content as markdown
const markdown = await threads.markdown({ threadId: 'T-abc123-def456' })
console.log(markdown)threads.setMultiplayer()
Open or close a thread for contributions (multiplayer). Only works on orb threads with shared (non-private) visibility, and only for the thread owner.
async function threads.setMultiplayer(options: ThreadsSetMultiplayerOptions): Promise<void>Parameters
options(ThreadsSetMultiplayerOptions) - The thread ID, enabled flag, and optional open-window duration
Returns
Promise<void>
Example
import { threads } from '@ampcode/sdk'
// Open a thread for contributions for the default duration (currently 3 hours)
await threads.setMultiplayer({ threadId: 'T-abc123-def456' })
// Open a thread for contributions for 12 hours
await threads.setMultiplayer({ threadId: 'T-abc123-def456', hours: 12 })
// Close a thread for contributions
await threads.setMultiplayer({ threadId: 'T-abc123-def456', enabled: false })Types
ExecuteOptions
Configuration options for the execute() function.
interface ExecuteOptions {
prompt: string | AsyncIterable<UserInputMessage>
options?: AmpOptions
signal?: AbortSignal
}Properties
| Property | Type | Required | Description |
| --------- | ------------------------------------------- | -------- | -------------------------------------------------------------------------------------------- |
| prompt | string \| AsyncIterable<UserInputMessage> | Yes | The input prompt as a string or async iterable of user messages for multi-turn conversations |
| options | AmpOptions | No | CLI configuration options |
| signal | AbortSignal | No | Signal for cancellation support |
AmpOptions
Configuration options that map to Amp CLI flags.
interface AmpOptions {
cwd?: string
mode?: string // Prefer 'low', 'medium', 'high', or 'ultra'
effort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
noArchiveAfterExecute?: boolean
visibility?: 'private' | 'unlisted' | 'workspace' | 'group'
settingsFile?: string
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'audit'
logFile?: string
mcpConfig?: string | MCPConfig
env?: Record<string, string>
continue?: boolean | string
skills?: string
enabledTools?: string[]
labels?: string[]
thinking?: boolean
executor?: 'local' | 'orb' | 'runner'
runnerId?: string
runnerDir?: string
project?: string
title?: string
}The built-in modes are 'low', 'medium', 'high', and 'ultra'. You can also pass a
string for a custom plugin-defined mode.
Properties
| Property | Type | Default | Description |
| ----------------------- | ------------------------------------------------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------- |
| cwd | string | process.cwd() | Working directory of the local CLI subprocess |
| mode | string | 'medium' | Agent mode. Prefer 'low', 'medium', 'high', or 'ultra'; custom plugin-defined mode strings are also accepted |
| effort | 'none' \| 'minimal' \| 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' | - | Reasoning effort for supported modes |
| noArchiveAfterExecute | boolean | false | Leave new execute threads unarchived after execution completes |
| visibility | 'private' \| 'unlisted' \| 'workspace' \| 'group' | 'workspace' | Thread visibility level |
| settingsFile | string | - | Path to custom settings file |
| logLevel | 'debug' \| 'info' \| 'warn' \| 'error' \| 'audit' | 'info' | Logging verbosity level |
| logFile | string | - | Path to write logs |
| continue | boolean \| string | false | Continue most recent thread (true) or specific thread by ID (string) |
| mcpConfig | string \| MCPConfig | - | MCP server configuration as JSON string, or config object |
| env | Record<string, string> | - | Additional environment variables |
| skills | string | - | Folder path with custom skills |
| enabledTools | string[] | - | Tool name patterns to enable (maps to amp.tools.enable) |
| labels | string[] | - | Labels to add to the thread |
| thinking | boolean | false | Include thinking blocks in the result stream |
| executor | 'local' \| 'orb' \| 'runner' | 'local' | Run in the local CLI process, a remote orb, or an Amp CLI runner |
| runnerId | string | - | Runner ID for a new runner thread; requires executor: 'runner' |
| runnerDir | string | - | Absolute path to one served directory for a new runner thread; requires executor: 'runner' |
| project | string | Inferred | Amp project for a new orb thread; requires executor: 'orb' |
| title | string | - | Title for a new thread, up to 256 characters |
Message Types
The SDK streams various message types during execution. All messages implement the base StreamMessage type.
SystemMessage
Initial message containing session information and available tools.
interface SystemMessage {
type: 'system'
subtype: 'init'
session_id: string
cwd: string
tools: string[]
mcp_servers: Array<{
name: string
status: 'connected' | 'connecting' | 'connection-failed' | 'disabled'
}>
}Properties
| Property | Type | Description |
| ------------- | --------------------------------------- | -------------------------------------------- |
| session_id | string | Unique identifier for this execution session |
| cwd | string | Current working directory |
| tools | string[] | List of available tool names |
| mcp_servers | Array<{name: string, status: string}> | Status of MCP servers |
AssistantMessage
AI assistant responses with text content and tool usage.
interface AssistantMessage {
type: 'assistant'
session_id: string
message: {
id: string
type: 'message'
role: 'assistant'
model: string
content: Array<TextContent | ToolUseContent>
stop_reason: 'end_turn' | 'tool_use' | 'max_tokens' | null
stop_sequence: string | null
usage?: Usage
}
parent_tool_use_id: string | null
}Properties
| Property | Type | Description |
| -------------------- | ---------------- | ------------------------------------------------ |
| session_id | string | Unique identifier for this execution session |
| message | object | The assistant's message content |
| parent_tool_use_id | string \| null | ID of parent tool use if this is a tool response |
UserMessage
User input and tool results.
interface UserMessage {
type: 'user'
session_id: string
message: {
role: 'user'
content: Array<TextContent | ToolResultContent>
}
parent_tool_use_id: string | null
}Properties
| Property | Type | Description |
| -------------------- | ---------------- | ------------------------------------------------ |
| session_id | string | Unique identifier for this execution session |
| message | object | The user's message content |
| parent_tool_use_id | string \| null | ID of parent tool use if this is a tool response |
ResultMessage
Final successful execution result.
interface ResultMessage {
type: 'result'
subtype: 'success'
session_id: string
is_error: false
result: string
duration_ms: number
num_turns: number
usage?: Usage
permission_denials?: string[]
}Properties
| Property | Type | Description |
| -------------------- | ----------------- | -------------------------------------------- |
| session_id | string | Unique identifier for this execution session |
| result | string | The final result from the assistant |
| duration_ms | number | Total execution time in milliseconds |
| num_turns | number | Number of conversation turns |
| usage | Usage | Token usage information |
| permission_denials | string[] | List of permissions that were denied |
ErrorResultMessage
Final error result indicating execution failure.
interface ErrorResultMessage {
type: 'result'
subtype: 'error_during_execution' | 'error_max_turns'
session_id: string
is_error: true
error: string
duration_ms: number
num_turns: number
usage?: Usage
permission_denials?: string[]
}Properties
| Property | Type | Description |
| -------------------- | ----------------- | -------------------------------------------- |
| session_id | string | Unique identifier for this execution session |
| error | string | Error message describing what went wrong |
| duration_ms | number | Total execution time in milliseconds |
| num_turns | number | Number of conversation turns |
| usage | Usage | Token usage information |
| permission_denials | string[] | List of permissions that were denied |
TextContent
Plain text content block.
interface TextContent {
type: 'text'
text: string
}ToolUseContent
Tool execution request.
interface ToolUseContent {
type: 'tool_use'
id: string
name: string
input: Record<string, unknown>
}ToolResultContent
Result from tool execution.
interface ToolResultContent {
type: 'tool_result'
tool_use_id: string
content: string
is_error: boolean
}Usage
Token usage and billing information from API calls.
interface Usage {
input_tokens: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
output_tokens: number
service_tier?: string
}Properties
| Property | Type | Description |
| ----------------------------- | -------- | ---------------------------------- |
| input_tokens | number | Number of input tokens used |
| cache_creation_input_tokens | number | Tokens used for cache creation |
| cache_read_input_tokens | number | Tokens read from cache |
| output_tokens | number | Number of output tokens generated |
| service_tier | string | Service tier used for this request |
Input Types
UserInputMessage
Formatted user input message for streaming conversations.
interface UserInputMessage {
type: 'user'
requestId?: string
message: {
role: 'user'
content: Array<{
type: 'text'
text: string
}>
}
}MCPConfig
Configuration for MCP (Model Context Protocol) servers. Supports both stdio-based and HTTP-based servers.
type MCPConfig = Record<string, MCPServer>
// MCPServer is a union of stdio and HTTP server configurationsMCPServer accepts either a stdio server config (with command) or an HTTP server config (with url):
const mcpConfig: MCPConfig = {
playwright: { command: 'npx', args: ['-y', '@playwright/mcp'] },
remote: { url: 'https://api.example.com/mcp' },
}MCPServer Properties
Stdio server:
| Property | Type | Required | Description |
| ---------- | ------------------------ | -------- | ------------------------------------ |
| command | string | Yes | Command to start the MCP server |
| args | string[] | No | Command line arguments |
| env | Record<string, string> | No | Environment variables for the server |
| disabled | boolean | No | Whether this server is disabled |
HTTP server:
| Property | Type | Required | Description |
| ----------- | ------------------------ | -------- | -------------------------------------- |
| url | string | Yes | URL of the HTTP MCP server |
| headers | Record<string, string> | No | HTTP headers to send with requests |
| transport | string | No | Transport type (e.g., "sse") |
| oauth | object | No | OAuth configuration for authentication |
| disabled | boolean | No | Whether this server is disabled |
OAuth config (for HTTP servers):
| Property | Type | Required | Description |
| -------------- | ---------- | -------- | ----------------------- |
| clientId | string | Yes | OAuth client ID |
| clientSecret | string | No | OAuth client secret |
| authUrl | string | Yes | OAuth authorization URL |
| tokenUrl | string | Yes | OAuth token URL |
| scopes | string[] | No | OAuth scopes |
| redirectUrl | string | No | OAuth redirect URL |
ThreadsNewOptions
Options for creating a new thread.
interface ThreadsNewOptions {
visibility?: 'private' | 'unlisted' | 'workspace' | 'group'
}Properties
| Property | Type | Required | Description |
| ------------ | --------------------------------------------------- | -------- | ----------------- |
| visibility | 'private' \| 'unlisted' \| 'workspace' \| 'group' | No | Thread visibility |
ThreadsMarkdownOptions
Options for getting thread markdown.
interface ThreadsMarkdownOptions {
threadId: string
}Properties
| Property | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------- |
| threadId | string | Yes | The thread ID to get markdown for |
ThreadsSetMultiplayerOptions
Options for opening or closing a thread for contributions (multiplayer). The duration options are summed; the total must be between 5 minutes and 7 days. When enabled is true and no duration is given, the server default (currently 3 hours) is used. Duration options are not allowed when enabled is false.
interface ThreadsSetMultiplayerOptions {
threadId: string
enabled?: boolean
minutes?: number
hours?: number
days?: number
weeks?: number
}Properties
| Property | Type | Required | Description |
| ---------- | --------- | -------- | ------------------------------------------------------------- |
| threadId | string | Yes | The thread ID to update |
| enabled | boolean | No | Whether the thread is open for contributions (default true) |
| minutes | number | No | Open-window duration in minutes |
| hours | number | No | Open-window duration in hours |
| days | number | No | Open-window duration in days |
| weeks | number | No | Open-window duration in weeks |
Requirements
- Node.js 18 or higher
