automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
938 lines (935 loc) • 35.4 kB
JavaScript
/**
* Automagik Forge Backend Client
* Type-safe TypeScript client for all Forge API endpoints
*
* Complete API documentation for all 80+ endpoints with full parameter details
*/
/**
* Complete Forge Backend Client
* Provides type-safe access to all Automagik Forge API endpoints
*/
class ForgeClient {
constructor(baseUrl, token) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.token = token;
}
async request(method, path, options) {
const url = new URL(`${this.baseUrl}/api${path}`);
if (options?.query) {
Object.entries(options.query).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
const response = await fetch(url.toString(), {
method,
headers: {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
},
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`[${response.status}] ${response.statusText}: ${errorBody}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'API request failed');
}
// For DELETE operations, data field might not exist
return result.data !== undefined ? result.data : null;
}
// ============================================================================
// HEALTH & SYSTEM
// ============================================================================
/**
* GET /health
* Simple health check endpoint
* @returns 200 OK if service is healthy
*/
async healthCheck() {
const url = `${this.baseUrl}/health`;
const res = await fetch(url);
return res.json();
}
// ============================================================================
// AUTHENTICATION - GitHub OAuth Device Flow
// ============================================================================
/**
* POST /api/auth/github/device/start
* Start GitHub device authentication flow
* Returns device_code and user_code for user to authorize
* @returns Device code and user code for OAuth
*/
async authGithubDeviceStart() {
return this.request('POST', '/auth/github/device/start');
}
/**
* POST /api/auth/github/device/poll
* Poll for GitHub device flow authorization completion
* Call this repeatedly until user authorizes or timeout
* @param deviceCode - Device code from start request
* @returns GitHub token when authorized
*/
async authGithubDevicePoll(deviceCode) {
return this.request('POST', '/auth/github/device/poll', {
body: { device_code: deviceCode },
});
}
/**
* GET /api/auth/github/check
* Verify if GitHub token is valid
* @returns User info if token is valid
*/
async authGithubCheck() {
return this.request('GET', '/auth/github/check');
}
// ============================================================================
// CONFIGURATION - User Preferences & MCP Settings
// ============================================================================
/**
* GET /api/info
* Get comprehensive user system info including profiles and capabilities
* @returns User config, executor profiles, and system capabilities
*/
async getSystemInfo() {
return this.request('GET', '/info');
}
/**
* GET /api/config
* Get user configuration (git preferences, theme, notifications, etc)
* @returns Current user configuration
*/
async getConfig() {
return this.request('GET', '/config');
}
/**
* PUT /api/config
* Update user configuration
* @param config - Partial config to update
* @returns Updated configuration
*/
async updateConfig(config) {
return this.request('PUT', '/config', { body: config });
}
/**
* GET /api/profiles
* Get executor profiles configuration (Claude, Gemini, etc)
* @returns All executor profile configurations
*/
async getExecutorProfiles() {
// Prefer /api/info for executor profiles (includes executors in stable format)
try {
const info = await this.request('GET', '/info');
const executors = info?.data?.executors || info?.executors;
if (executors) {
// Return normalized format: { executors: {...} }
return { executors };
}
} catch (error) {
// Fallback to /api/profiles if /api/info fails
}
// Fallback: /api/profiles returns { content: "<JSON_STRING>", path: "..." }
// (request() method already unwraps .data, so we get the content field directly)
const profiles = await this.request('GET', '/profiles');
if (profiles?.content && typeof profiles.content === 'string') {
// Parse JSON string and return
return JSON.parse(profiles.content);
}
return profiles;
}
/**
* PUT /api/profiles
* Update executor profiles configuration
* @param profiles - Executor profiles object with `executors` key
* @returns Updated profiles
*/
async updateExecutorProfiles(profiles) {
// Parse profiles if string, API expects raw object not wrapped
const profilesObj = typeof profiles === 'string' ? JSON.parse(profiles) : profiles;
return this.request('PUT', '/profiles', { body: profilesObj });
}
/**
* GET /api/mcp-config?executor={executor}
* Get MCP (Model Context Protocol) servers for specific executor
* @param executor - Executor type (CLAUDE_CODE, GEMINI, CODEX, etc)
* @returns MCP server configuration for executor
*/
async getMcpConfig(executor) {
return this.request('GET', '/mcp-config', { query: { executor } });
}
/**
* POST /api/mcp-config?executor={executor}
* Update MCP servers configuration for executor
* @param executor - Executor type
* @param config - MCP server configuration
* @returns Updated MCP configuration
*/
async updateMcpConfig(executor, config) {
return this.request('POST', '/mcp-config', {
body: config,
query: { executor },
});
}
/**
* GET /api/sounds/{sound}
* Get notification sound file
* @param sound - Sound file name (ABSTRACT_SOUND1, ROOSTER, etc)
* @returns Audio file content
*/
async getNotificationSound(sound) {
const url = `${this.baseUrl}/api/sounds/${sound}`;
return fetch(url).then(r => r.blob());
}
// ============================================================================
// PROJECTS - Create, Read, Update, Delete Projects
// ============================================================================
/**
* GET /api/projects
* List all projects in the workspace
* @returns Array of all projects
*/
async listProjects() {
return this.request('GET', '/projects');
}
/**
* POST /api/projects
* Create a new project
* @param project - Project creation details (name, repo path, scripts)
* @returns Created project with ID
*/
async createProject(project) {
return this.request('POST', '/projects', { body: project });
}
/**
* GET /api/projects/{id}
* Get project details by ID
* @param id - Project UUID
* @returns Project details
*/
async getProject(id) {
return this.request('GET', `/projects/${id}`);
}
/**
* PUT /api/projects/{id}
* Update project details
* @param id - Project UUID
* @param updates - Fields to update (name, setup_script, dev_script, etc)
* @returns Updated project
*/
async updateProject(id, updates) {
return this.request('PUT', `/projects/${id}`, { body: updates });
}
/**
* DELETE /api/projects/{id}
* Delete a project (removes from DB, not filesystem)
* @param id - Project UUID
*/
async deleteProject(id) {
await this.request('DELETE', `/projects/${id}`);
}
/**
* GET /api/projects/{id}/branches
* Get all git branches in project repository
* @param id - Project UUID
* @returns Array of branch names
*/
async listProjectBranches(id) {
return this.request('GET', `/projects/${id}/branches`);
}
/**
* GET /api/projects/{id}/search?q={query}&mode={mode}
* Search files in project repository
* @param id - Project UUID
* @param query - Search term (filename, directory name, or full path)
* @param mode - Search mode: "FileName" | "DirectoryName" | "FullPath"
* @returns Array of matching files with paths and metadata
*/
async searchProjectFiles(id, query, mode = 'FileName') {
return this.request('GET', `/projects/${id}/search`, {
query: { q: query, mode },
});
}
/**
* POST /api/projects/{id}/open-editor
* Open project in user's configured code editor
* @param id - Project UUID
* @param request - Optional editor request (file path, line number)
*/
async openProjectInEditor(id, request) {
await this.request('POST', `/projects/${id}/open-editor`, { body: request });
}
// ============================================================================
// TASKS - Create, Read, Update, Delete Tasks
// ============================================================================
/**
* GET /api/projects/{project_id}/tasks
* List all tasks in a project
* @param projectId - Project UUID
* @returns Array of tasks with attempt status info
*/
async listTasks(projectId) {
return this.request('GET', `/projects/${projectId}/tasks`);
}
/**
* POST /api/projects/{project_id}/tasks
* Create a new task (not started)
* @param projectId - Project UUID
* @param task - Task creation details (title, description)
* @returns Created task with ID
*/
async createTask(projectId, task) {
return this.request('POST', `/projects/${projectId}/tasks`, { body: task });
}
/**
* POST /api/tasks/create-and-start
* Create a task AND immediately start task attempt (all-in-one)
* Faster than create + start separately
* @param request - Full request with task object, executor_profile_id, and base_branch
* @returns New task with attempt started
*/
async createAndStartTask(request) {
return this.request('POST', `/tasks/create-and-start`, {
body: request,
});
}
/**
* GET /api/projects/{project_id}/tasks/{task_id}
* Get task details
* @param projectId - Project UUID
* @param taskId - Task UUID
* @returns Task with all details and attempt status
*/
async getTask(projectId, taskId) {
return this.request('GET', `/projects/${projectId}/tasks/${taskId}`);
}
/**
* PUT /api/projects/{project_id}/tasks/{task_id}
* Update task details (title, description, status, images)
* @param projectId - Project UUID
* @param taskId - Task UUID
* @param updates - Fields to update
* @returns Updated task
*/
async updateTask(projectId, taskId, updates) {
return this.request('PUT', `/projects/${projectId}/tasks/${taskId}`, { body: updates });
}
/**
* DELETE /api/projects/{project_id}/tasks/{task_id}
* Delete a task (async cleanup)
* Returns 202 Accepted - deletion happens in background
* @param projectId - Project UUID
* @param taskId - Task UUID
*/
async deleteTask(projectId, taskId) {
await this.request('DELETE', `/projects/${projectId}/tasks/${taskId}`);
}
// ============================================================================
// TASK ATTEMPTS - AI Agent Execution & Orchestration
// ============================================================================
/**
* GET /api/task-attempts?task_id={task_id}
* List task attempts (executions) for a task
* @param taskId - Optional filter by task UUID
* @returns Array of task attempts
*/
async listTaskAttempts(taskId) {
return this.request('GET', '/task-attempts', { query: taskId ? { task_id: taskId } : {} });
}
/**
* POST /api/task-attempts
* Create a new task attempt (start AI execution)
* This spawns the selected executor to work on the task
* @param request - Executor, base branch, task ID
* @returns New task attempt (execution started)
*/
async createTaskAttempt(request) {
return this.request('POST', '/task-attempts', { body: request });
}
/**
* GET /api/task-attempts/{id}
* Get task attempt details and execution status
* @param id - Task attempt UUID
* @returns Task attempt with status and process info
*/
async getTaskAttempt(id) {
return this.request('GET', `/task-attempts/${id}`);
}
/**
* POST /api/task-attempts/{id}/follow-up
* Send follow-up prompt to running/paused execution
* Executor will use this to continue work
* @param id - Task attempt UUID
* @param prompt - Follow-up message for AI agent
* @returns New execution process
*/
async followUpTaskAttempt(id, prompt) {
return this.request('POST', `/task-attempts/${id}/follow-up`, {
body: { prompt: prompt },
});
}
/**
* POST /api/task-attempts/{id}/replace-process
* Replace the execution process and send new prompt
* Use when you want to switch executors or restart with new instructions
* @param id - Task attempt UUID
* @param request - New executor and prompt
* @returns New execution process
*/
async replaceTaskAttemptProcess(id, request) {
return this.request('POST', `/task-attempts/${id}/replace-process`, { body: request });
}
/**
* GET /api/task-attempts/{id}/branch-status
* Get git branch status for task attempt
* Shows commits ahead/behind, merge conflicts, etc
* @param id - Task attempt UUID
* @returns Branch status with commit info
*/
async getTaskAttemptBranchStatus(id) {
return this.request('GET', `/task-attempts/${id}/branch-status`);
}
/**
* POST /api/task-attempts/{id}/rebase
* Rebase task attempt branch onto new base branch
* Handles merge conflicts automatically if possible
* @param id - Task attempt UUID
* @param baseBranch - New base branch to rebase onto
* @returns Updated branch status
*/
async rebaseTaskAttempt(id, baseBranch) {
return this.request('POST', `/task-attempts/${id}/rebase`, {
body: { base_branch: baseBranch },
});
}
/**
* POST /api/task-attempts/{id}/merge
* Merge task attempt branch to target branch
* @param id - Task attempt UUID
* @returns Merge result
*/
async mergeTaskAttempt(id) {
return this.request('POST', `/task-attempts/${id}/merge`);
}
/**
* POST /api/task-attempts/{id}/push
* Push task attempt branch to GitHub
* @param id - Task attempt UUID
*/
async pushTaskAttemptBranch(id) {
await this.request('POST', `/task-attempts/${id}/push`);
}
/**
* POST /api/task-attempts/{id}/conflicts/abort
* Abort rebase/merge conflict state
* Rolls back to previous clean state
* @param id - Task attempt UUID
*/
async abortTaskAttemptConflicts(id) {
await this.request('POST', `/task-attempts/${id}/conflicts/abort`);
}
/**
* POST /api/task-attempts/{id}/pr
* Create GitHub PR for task attempt branch
* @param id - Task attempt UUID
* @param request - PR title, description, target branch
* @returns Created PR info (URL, number, etc)
*/
async createTaskAttemptPullRequest(id, request) {
return this.request('POST', `/task-attempts/${id}/pr`, { body: request });
}
/**
* POST /api/task-attempts/{id}/pr/attach
* Attach existing GitHub PR to task attempt
* Links task execution to ongoing PR
* @param id - Task attempt UUID
* @param prNumber - GitHub PR number
*/
async attachExistingPullRequest(id, prNumber) {
await this.request('POST', `/task-attempts/${id}/pr/attach`, {
body: { pr_number: prNumber },
});
}
/**
* GET /api/task-attempts/{id}/commit-info?sha={sha}
* Get commit subject by SHA
* @param id - Task attempt UUID
* @param sha - Git commit SHA
* @returns Commit subject and metadata
*/
async getCommitInfo(id, sha) {
return this.request('GET', `/task-attempts/${id}/commit-info`, { query: { sha } });
}
/**
* GET /api/task-attempts/{id}/commit-compare?sha={sha}
* Compare commit against HEAD of task branch
* Shows what changed between commit and current
* @param id - Task attempt UUID
* @param sha - Git commit SHA to compare
* @returns Diff and comparison results
*/
async compareCommitToHead(id, sha) {
return this.request('GET', `/task-attempts/${id}/commit-compare`, { query: { sha } });
}
/**
* GET /api/task-attempts/{id}/children
* Get parent task and child tasks (subtasks)
* Task hierarchy relationships
* @param id - Task attempt UUID
* @returns Parent task and child task list
*/
async getTaskAttemptChildren(id) {
return this.request('GET', `/task-attempts/${id}/children`);
}
/**
* POST /api/task-attempts/{id}/stop
* Stop execution process for attempt
* Halts AI agent work
* @param id - Task attempt UUID
*/
async stopTaskAttemptExecution(id) {
await this.request('POST', `/task-attempts/${id}/stop`);
}
/**
* POST /api/task-attempts/{id}/change-target-branch
* Change target branch for task attempt
* Useful if target was deleted or needs to change
* @param id - Task attempt UUID
* @param targetBranch - New target branch name
*/
async changeTaskAttemptTargetBranch(id, targetBranch) {
await this.request('POST', `/task-attempts/${id}/change-target-branch`, {
body: { target_branch: targetBranch },
});
}
/**
* POST /api/task-attempts/{id}/open-editor
* Open task attempt worktree in code editor
* @param id - Task attempt UUID
* @param request - Optional editor request (file, line)
*/
async openTaskAttemptInEditor(id, request) {
await this.request('POST', `/task-attempts/${id}/open-editor`, { body: request });
}
/**
* POST /api/task-attempts/{id}/start-dev-server
* Start dev server for project
* @param id - Task attempt UUID
*/
async startDevServer(id) {
return this.request('POST', `/task-attempts/${id}/start-dev-server`);
}
/**
* POST /api/task-attempts/{id}/delete-file
* Delete file from task attempt worktree
* @param id - Task attempt UUID
* @param filePath - File path to delete
*/
async deleteTaskAttemptFile(id, filePath) {
await this.request('POST', `/task-attempts/${id}/delete-file`, {
query: { file_path: filePath },
});
}
// ============================================================================
// DRAFTS - Save & Manage Draft Code Changes
// ============================================================================
/**
* POST /api/task-attempts/{id}/draft?type={type}
* Save draft code changes for task attempt
* Drafts are queued for later execution
* @param id - Task attempt UUID
* @param type - Draft type (IMPLEMENTATION | TEST | etc)
* @param content - Draft content (JSON, code, etc)
*/
async saveDraft(id, type, content) {
return this.request('POST', `/task-attempts/${id}/draft`, {
body: content,
query: { type },
});
}
/**
* GET /api/task-attempts/{id}/draft?type={type}
* Get saved draft for task attempt
* @param id - Task attempt UUID
* @param type - Draft type
* @returns Draft content
*/
async getDraft(id, type) {
return this.request('GET', `/task-attempts/${id}/draft`, { query: { type } });
}
/**
* DELETE /api/task-attempts/{id}/draft?type={type}
* Delete saved draft
* @param id - Task attempt UUID
* @param type - Draft type
*/
async deleteDraft(id, type) {
await this.request('DELETE', `/task-attempts/${id}/draft`, { query: { type } });
}
/**
* POST /api/task-attempts/{id}/draft/queue?type={type}
* Queue draft for execution
* AI agent will execute draft in next follow-up
* @param id - Task attempt UUID
* @param type - Draft type
* @param request - Queue configuration
*/
async queueDraftExecution(id, type, request) {
await this.request('POST', `/task-attempts/${id}/draft/queue`, {
body: request,
query: { type },
});
}
// ============================================================================
// EXECUTION PROCESSES - Log Streaming & Process Management
// ============================================================================
/**
* GET /api/execution-processes?task_attempt_id={id}&show_soft_deleted={bool}
* List execution processes for task attempt
* @param taskAttemptId - Task attempt UUID
* @param showSoftDeleted - Include soft-deleted processes
* @returns Array of execution processes with logs
*/
async listExecutionProcesses(taskAttemptId, showSoftDeleted = false) {
return this.request('GET', '/execution-processes', {
query: { task_attempt_id: taskAttemptId, show_soft_deleted: showSoftDeleted },
});
}
/**
* GET /api/execution-processes/{id}
* Get execution process details
* @param id - Execution process UUID
* @returns Process with status and output
*/
async getExecutionProcess(id) {
return this.request('GET', `/execution-processes/${id}`);
}
/**
* POST /api/execution-processes/{id}/stop
* Stop execution process
* Sends SIGTERM to running process
* @param id - Execution process UUID
*/
async stopExecutionProcess(id) {
await this.request('POST', `/execution-processes/${id}/stop`);
}
// ============================================================================
// TASK TEMPLATES - Reusable Task Definitions
// ============================================================================
/**
* GET /api/templates?global={bool}&project_id={id}
* List task templates (global and project-specific)
* @param global - Filter by global templates
* @param projectId - Filter by project
* @returns Array of task templates
*/
async listTaskTemplates(options) {
return this.request('GET', '/templates', { query: options || {} });
}
/**
* POST /api/templates
* Create a task template
* Templates can be used to quickly create similar tasks
* @param template - Template definition
* @returns Created template
*/
async createTaskTemplate(template) {
return this.request('POST', '/templates', { body: template });
}
/**
* GET /api/templates/{template_id}
* Get task template details
* @param templateId - Template UUID
* @returns Template definition
*/
async getTaskTemplate(templateId) {
return this.request('GET', `/templates/${templateId}`);
}
/**
* PUT /api/templates/{template_id}
* Update task template
* @param templateId - Template UUID
* @param updates - Template updates
* @returns Updated template
*/
async updateTaskTemplate(templateId, updates) {
return this.request('PUT', `/templates/${templateId}`, { body: updates });
}
/**
* DELETE /api/templates/{template_id}
* Delete task template
* @param templateId - Template UUID
*/
async deleteTaskTemplate(templateId) {
await this.request('DELETE', `/templates/${templateId}`);
}
// ============================================================================
// IMAGES - Upload & Manage Task Images
// ============================================================================
/**
* POST /api/images/upload
* Upload image (20MB limit)
* @param file - Image file to upload
* @returns Created image with ID and URL
*/
async uploadImage(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.baseUrl}/api/images/upload`, {
method: 'POST',
headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
body: formData,
});
if (!response.ok)
throw new Error(`Upload failed: ${response.statusText}`);
const result = await response.json();
if (!result.success)
throw new Error(result.message);
return result.data;
}
/**
* POST /api/images/task/{task_id}/upload
* Upload image for specific task
* @param taskId - Task UUID
* @param file - Image file
* @returns Created image associated with task
*/
async uploadTaskImage(taskId, file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.baseUrl}/api/images/task/${taskId}/upload`, {
method: 'POST',
headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
body: formData,
});
if (!response.ok)
throw new Error(`Upload failed: ${response.statusText}`);
const result = await response.json();
if (!result.success)
throw new Error(result.message);
return result.data;
}
/**
* GET /api/images/{id}/file
* Get image file by ID
* @param id - Image UUID
* @returns Image blob
*/
async getImageFile(id) {
const response = await fetch(`${this.baseUrl}/api/images/${id}/file`, {
headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
});
if (!response.ok)
throw new Error('Failed to fetch image');
return response.blob();
}
/**
* GET /api/images/task/{task_id}
* Get all images for a task
* @param taskId - Task UUID
* @returns Array of images
*/
async getTaskImages(taskId) {
return this.request('GET', `/images/task/${taskId}`);
}
/**
* DELETE /api/images/{id}
* Delete image
* @param id - Image UUID
*/
async deleteImage(id) {
await this.request('DELETE', `/images/${id}`);
}
// ============================================================================
// APPROVALS - Approval Requests & Responses
// ============================================================================
/**
* POST /api/approvals/create
* Create approval request
* Pauses execution waiting for user approval
* @param request - Approval context and options
* @returns Approval ID
*/
async createApprovalRequest(request) {
return this.request('POST', '/approvals/create', { body: request });
}
/**
* GET /api/approvals/{id}/status
* Get approval status (pending, approved, rejected)
* @param id - Approval UUID
* @returns Approval status
*/
async getApprovalStatus(id) {
return this.request('GET', `/approvals/${id}/status`);
}
/**
* POST /api/approvals/{id}/respond
* Respond to approval request (approve/reject)
* @param id - Approval UUID
* @param approved - True to approve, false to reject
* @param comment - Optional comment
*/
async respondToApprovalRequest(id, approved, comment) {
await this.request('POST', `/approvals/${id}/respond`, {
body: { approved, comment },
});
}
/**
* GET /api/approvals/pending
* Get list of pending approvals
* @returns Array of pending approval requests
*/
async getPendingApprovals() {
return this.request('GET', '/approvals/pending');
}
// ============================================================================
// CONTAINERS - Container Reference Resolution
// ============================================================================
/**
* GET /api/containers/info?ref={ref}
* Resolve container reference to IDs
* @param ref - Container reference
* @returns Container IDs and metadata
*/
async getContainerInfo(ref) {
return this.request('GET', '/containers/info', { query: { ref } });
}
// ============================================================================
// FILESYSTEM - Directory Browsing & Git Repo Discovery
// ============================================================================
/**
* GET /api/filesystem/directory?path={path}
* List directory contents
* @param path - Directory path (optional)
* @returns Directory contents
*/
async listDirectory(path) {
return this.request('GET', '/filesystem/directory', { query: path ? { path } : {} });
}
/**
* GET /api/filesystem/git-repos?path={path}
* List git repositories in directory
* @param path - Search path (optional)
* @returns Array of git repo paths
*/
async listGitRepositories(path) {
return this.request('GET', '/filesystem/git-repos', { query: path ? { path } : {} });
}
// ============================================================================
// FORGE AGENTS - Master Orchestrators (Wish, Forge, Review)
// ============================================================================
/**
* GET /api/forge/agents?project_id={project_id}&agent_type={agent_type}
* Get forge agents (master orchestrators) for a project
* Each agent has ONE fixed task per project
* @param projectId - Project UUID
* @param agentType - Agent type filter ('wish' | 'forge' | 'review')
* @returns Array of forge agents
*/
async getForgeAgents(projectId, agentType) {
const query = { project_id: projectId };
if (agentType) {
query.agent_type = agentType;
}
return this.request('GET', '/forge/agents', { query });
}
/**
* POST /api/forge/agents
* Create a new forge agent (and its fixed task)
* @param projectId - Project UUID
* @param agentType - Agent type ('wish' | 'forge' | 'review')
* @returns Created agent with task_id
*/
async createForgeAgent(projectId, agentType) {
return this.request('POST', '/forge/agents', {
body: {
project_id: projectId,
agent_type: agentType,
},
});
}
// ============================================================================
// SERVER-SENT EVENTS - Real-time Updates
// ============================================================================
/**
* GET /api/events
* Subscribe to Server-Sent Events stream
* Receive real-time updates about tasks, processes, etc
* @returns EventSource for streaming updates
*/
subscribeToEvents() {
return new EventSource(`${this.baseUrl}/api/events`, {
withCredentials: !!this.token,
});
}
// ============================================================================
// WEBSOCKET STREAMING - Real-time Log & Diff Streaming
// ============================================================================
/**
* WS /api/tasks/stream/ws?project_id={project_id}
* WebSocket stream of tasks in real-time
* Receive updates when tasks are created, updated, etc
* @param projectId - Project UUID
* @returns WebSocket URL for connection
*/
getTasksStreamUrl(projectId) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/tasks/stream/ws?project_id=${projectId}`;
}
/**
* WS /api/execution-processes/stream/ws?task_attempt_id={id}
* WebSocket stream of execution processes
* Receive updates about process status, cancellation, etc
* @param taskAttemptId - Task attempt UUID
* @returns WebSocket URL for connection
*/
getExecutionProcessesStreamUrl(taskAttemptId) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/execution-processes/stream/ws?task_attempt_id=${taskAttemptId}`;
}
/**
* WS /api/execution-processes/{id}/raw-logs/ws
* WebSocket stream of raw process logs (stdout/stderr)
* @param processId - Execution process UUID
* @returns WebSocket URL for connection
*/
getRawLogsStreamUrl(processId) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/execution-processes/${processId}/raw-logs/ws`;
}
/**
* WS /api/execution-processes/{id}/normalized-logs/ws
* WebSocket stream of normalized/parsed logs
* @param processId - Execution process UUID
* @returns WebSocket URL for connection
*/
getNormalizedLogsStreamUrl(processId) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/execution-processes/${processId}/normalized-logs/ws`;
}
/**
* WS /api/task-attempts/{id}/diff/ws?stats_only={bool}
* WebSocket stream of task attempt diffs
* Streams file changes as they happen
* @param attemptId - Task attempt UUID
* @param statsOnly - Only show statistics, not full diff
* @returns WebSocket URL for connection
*/
getTaskDiffStreamUrl(attemptId, statsOnly = false) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/task-attempts/${attemptId}/diff/ws?stats_only=${statsOnly}`;
}
/**
* WS /api/drafts/stream/ws
* WebSocket stream of project drafts in real-time
* @param projectId - Project UUID
* @returns WebSocket URL for connection
*/
getDraftsStreamUrl(projectId) {
const protocol = this.baseUrl.startsWith('https') ? 'wss' : 'ws';
return `${protocol}://${new URL(this.baseUrl).host}/api/drafts/stream/ws?project_id=${projectId}`;
}
}
// CommonJS exports
module.exports = { ForgeClient };
module.exports.default = ForgeClient;