@cmdop/node
v2026.3.301
Published
CMDOP SDK for Node.js - Server-side agent interaction via gRPC
Maintainers
Readme
@cmdop/node
Node.js SDK for CMDOP agent interaction via gRPC.
Installation
npm install @cmdop/node
# or
pnpm add @cmdop/nodeRequirements: Node.js >= 18.0.0
Quick Start
import { CMDOPClient } from '@cmdop/node';
// Local connection (auto-discover agent via ~/.cmdop/agent.info)
const client = CMDOPClient.local();
// Remote connection via cloud relay
const client = CMDOPClient.remote('cmdop_live_xxx');
// Auto-detect: local first, falls back to CMDOP_API_KEY env var
const client = await CMDOPClient.discover();
// Automatic cleanup via Symbol.asyncDispose (TypeScript 5.2+)
await using client = CMDOPClient.local();Services
Terminal
// Create a session
const session = await client.terminal.create({ cols: 120, rows: 40 });
// Send input / resize / signal
await client.terminal.sendInput(session.sessionId, 'ls -la\n');
await client.terminal.resize(session.sessionId, 200, 50);
await client.terminal.signal(session.sessionId, 'SIGINT');
// Run command and wait for output
await client.terminal.setMachine('my-server');
const { output, exitCode } = await client.terminal.execute('echo hello');
console.log(output, exitCode);
// Get the currently active session
const active = await client.terminal.getActiveSession({ hostname: 'my-server' });
// Stream output in real-time (polling-based)
const stream = client.terminal.stream(session.sessionId);
stream.on((event) => {
if (event.type === 'output') process.stdout.write(event.data.toString());
});
await stream.connect();
// Attach to session (bidirectional gRPC stream, SSH-like)
const attach = client.terminal.attach(session.sessionId, { cols: 120, rows: 40 });
attach.on((event) => {
if (event.type === 'sessionReady') console.log('Connected!');
if (event.type === 'output') process.stdout.write(event.data);
if (event.type === 'closed') console.log('Disconnected:', event.reason);
});
await attach.connect();
attach.sendInput('ls -la\n');
attach.sendResize(200, 50);
attach.close();
// List / close
const { sessions } = await client.terminal.list();
await client.terminal.close(session.sessionId);SSH Connect
High-level interactive terminal (like ssh). Handles raw mode, stdin/stdout piping, resize, and Ctrl+D to disconnect.
import { CMDOPClient, sshConnect } from '@cmdop/node';
const client = CMDOPClient.remote('cmdop_live_xxx');
const exitCode = await sshConnect({
client,
hostname: 'my-server',
debug: false, // optional: log gRPC messages to stderr
sessionId: '...', // optional: skip session discovery
});
process.exit(exitCode);Files
// List directory
const result = await client.files.list('/tmp', { pageSize: 50 });
for (const entry of result.entries) {
console.log(entry.name, entry.type, entry.size);
}
// Read / Write
const file = await client.files.read('/tmp/file.txt');
console.log(file.content.toString('utf-8'));
await client.files.write('/tmp/new.txt', 'Hello World');
// Stat / Mkdir / Move / Copy / Delete
await client.files.stat('/tmp/file.txt');
await client.files.mkdir('/tmp/newdir');
await client.files.move('/tmp/a.txt', '/tmp/b.txt');
await client.files.copy('/tmp/src.txt', '/tmp/dst.txt');
await client.files.delete('/tmp/file.txt');
// Search / Archive
const matches = await client.files.search('/tmp', { pattern: '*.log' });
await client.files.archive(['/tmp/dir'], '/tmp/out.zip');Download
Transfer files from the remote agent to local disk.
// Download a remote file
const result = await client.download.downloadFile('/remote/data.csv', '/local/data.csv');
console.log(`Saved ${result.size} bytes in ${result.metrics?.durationMs}ms`);
// Download a URL via the agent (respects agent cookies/auth)
const result = await client.download.downloadUrl(
'https://example.com/report.pdf',
'/local/report.pdf'
);Agent
// One-shot execution
const result = await client.agent.run('List files in /tmp', {
mode: 'terminal', // 'chat' | 'terminal' | 'command' | 'router' | 'planner' | 'browser' | 'scraper' | 'form_filler'
timeoutSeconds: 60,
maxTurns: 10,
maxRetries: 2,
model: 'claude-opus-4-6',
});
console.log(result.text);
console.log(result.usage?.totalTokens);
console.log(result.toolResults);
// Streaming
const stream = client.agent.stream('Explain what ls -la does');
stream.on((event) => {
if (event.type === 'token') process.stdout.write(event.token);
if (event.type === 'tool_start') console.log(`\n[tool: ${event.payload}]`);
if (event.type === 'thinking') console.log(`[thinking: ${event.payload}]`);
});
const result = await stream.start();
stream.cancel(); // cancel if needed
// Structured output
import { z, zodToJsonSchema } from '@cmdop/node';
const FileListSchema = z.object({
files: z.array(z.string()),
total: z.number(),
});
const data = await client.agent.extract<z.infer<typeof FileListSchema>>(
'List files in /tmp',
zodToJsonSchema(FileListSchema)
);
console.log(data.files, data.total);Skills
// List all installed skills
const skills = await client.skills.list();
for (const skill of skills) {
console.log(`${skill.name} (${skill.origin}) — ${skill.description}`);
}
// Show skill details
const detail = await client.skills.show('code-review');
if (detail.found) {
console.log(detail.info?.name, detail.info?.version);
console.log(detail.content); // SKILL.md body
console.log(detail.source); // file path on agent
}
// Run a skill
const result = await client.skills.run('code-review', 'Review my PR changes');
console.log(result.text);
console.log(result.durationMs);
// Structured output
import { z, zodToJsonSchema } from '@cmdop/node';
const ReviewSchema = z.object({
issues: z.array(z.object({ file: z.string(), line: z.number(), message: z.string() })),
summary: z.string(),
});
const review = await client.skills.extract<z.infer<typeof ReviewSchema>>(
'code-review',
'Review the changes',
zodToJsonSchema(ReviewSchema)
);
console.log(review.issues, review.summary);Extract
Dedicated structured data extraction RPC (more reliable than agent.extract()).
import { z } from '@cmdop/node';
const ConfigSchema = z.object({
host: z.string(),
port: z.number(),
database: z.string(),
});
// Zod schema — result is validated and fully typed
const result = await client.extract.runSchema(
'Find the database config in config files',
ConfigSchema
);
console.log(result.data.host);
console.log(result.reasoning);
console.log(result.metrics?.durationMs);
// Raw JSON Schema
const raw = await client.extract.run<{ host: string }>(
'Find the database host',
JSON.stringify({ type: 'object', properties: { host: { type: 'string' } } })
);Browser
// Create a session
const browser = await client.browser.createSession({
startUrl: 'https://example.com',
headless: true,
});
// Navigation
await browser.navigate('https://github.com');
await browser.reload();
await browser.goBack();
await browser.goForward();
// Interaction
await browser.click({ selector: 'button.submit' });
await browser.type('hello world', 'input[name="q"]');
await browser.key('Enter');
await browser.hover('nav a.menu');
await browser.mouseMove(100, 200);
// Scroll
await browser.scrollDown(500);
await browser.scrollUp(500);
await browser.scrollToBottom();
await browser.scrollToTop();
// Wait
await browser.wait({ selector: '.loaded', timeoutMs: 5000 });
await browser.wait({ timeMs: 1000 });
// Read content
const html = await browser.getHTML('main');
const text = await browser.getText('h1');
const state = await browser.getState(); // url, title, scrollY, ...
const info = await browser.getPageInfo(); // url, title, pageHeight, isHttps, cloudflareDetected, ...
// Extract structured data
const values = await browser.extract('a', { attribute: 'href' });
const data = await browser.extractData({
fields: [
{ name: 'title', selector: 'h1', type: 'text' },
{ name: 'price', selector: '.price', type: 'text' },
],
});
// Screenshot (returns Buffer)
const screenshot = await browser.screenshot({ fullPage: true });
// Fetch (runs inside browser context, respects cookies)
const json = await browser.fetchJson<{ id: number }>('/api/me');
const html = await browser.fetchText('https://example.com/page');
// Cookies
const cookies = await browser.getCookies({ domain: 'example.com' });
await browser.setCookies([{ name: 'session', value: 'abc', domain: 'example.com' }]);
// Validate selectors
const valid = await browser.validateSelectors({ selectors: ['h1', '.missing'] });
// Network capture
await browser.networkEnable();
const exchanges = await browser.networkGetExchanges({ urlPattern: '/api/' });
const last = await browser.networkGetLast('/api/user');
const stats = await browser.networkStats();
const har = await browser.networkExportHAR();
await browser.networkClear();
await browser.networkDisable();
// Cleanup
await browser.close();
// Or with Symbol.asyncDispose
await using browser = await client.browser.createSession({ startUrl: 'https://example.com' });Remote Connections
For cloud relay connections, set the session ID before using files, agent, extract, or browser.
const client = CMDOPClient.remote('cmdop_live_xxx');
// Discover available agents
const agents = await CMDOPClient.listAgents('cmdop_live_xxx');
const online = await CMDOPClient.getOnlineAgents('cmdop_live_xxx');
// Route all services to a specific agent
client.setSessionId(online[0].agentId);
// Or route each service to a different machine
await client.files.setMachine('storage-01');
await client.agent.setMachine('gpu-box');
await client.skills.setMachine('gpu-box');
await client.terminal.setMachine('prod-01');Client Properties
const client = CMDOPClient.remote('cmdop_live_xxx');
client.mode // 'local' | 'remote'
client.address // 'grpc.cmdop.com:443'
client.isConnected // false (lazy connection)
client.transport // underlying BaseTransport instance
// Build client from a pre-configured transport
import { RemoteTransport } from '@cmdop/node';
const transport = new RemoteTransport({ apiKey: '...', server: 'custom.host:443' });
const client = CMDOPClient.fromTransport(transport);Configuration
import { configure } from '@cmdop/node';
configure({
connectTimeoutMs: 10_000,
requestTimeoutMs: 30_000,
retryAttempts: 5,
retryTimeoutMs: 30_000,
keepaliveIntervalMs: 25_000,
circuitBreakerFailMax: 5,
circuitBreakerResetMs: 30_000,
maxMessageSize: 64 * 1024 * 1024,
grpcServer: 'grpc.cmdop.com:443',
apiBaseUrl: 'https://api.cmdop.com',
logLevel: 'info',
logJson: false,
});Environment variables:
| Variable | Default | Description |
|---|---|---|
| CMDOP_GRPC_SERVER | grpc.cmdop.com:443 | gRPC server address |
| CMDOP_API_BASE_URL | https://api.cmdop.com | REST API base URL |
| CMDOP_CONNECT_TIMEOUT_MS | 10000 | Connection timeout (ms) |
| CMDOP_REQUEST_TIMEOUT_MS | 30000 | Per-request timeout (ms) |
| CMDOP_RETRY_ATTEMPTS | 5 | Max retry attempts |
| CMDOP_RETRY_TIMEOUT_MS | 30000 | Total retry window (ms) |
| CMDOP_KEEPALIVE_INTERVAL_MS | 25000 | Keepalive ping interval (ms) |
| CMDOP_QUEUE_MAX_SIZE | 1000 | Max streaming queue size |
| CMDOP_CIRCUIT_FAIL_MAX | 5 | Circuit breaker failure threshold |
| CMDOP_CIRCUIT_RESET_TIMEOUT_MS | 30000 | Circuit breaker reset time (ms) |
| CMDOP_MAX_MESSAGE_SIZE | 33554432 | Max gRPC message size (bytes) |
| CMDOP_LOG_LEVEL | info | debug\|info\|warn\|error\|silent |
| CMDOP_LOG_JSON | false | Structured JSON logging |
| CMDOP_API_KEY | — | API key (used by CMDOPClient.discover()) |
Error Handling
import {
CMDOPError,
// Core
ConnectionError,
AuthenticationError,
SessionError,
TimeoutError,
NotFoundError,
PermissionError,
// Extended (Node SDK)
AgentNotRunningError,
StalePortFileError,
ConnectionLostError,
InvalidAPIKeyError,
TokenExpiredError,
AgentError,
AgentOfflineError,
AgentBusyError,
FeatureNotAvailableError,
SessionInterruptedError,
FileTooLargeError,
BrowserError,
BrowserSessionClosedError,
BrowserNavigationError,
BrowserElementNotFoundError,
RateLimitError,
} from '@cmdop/node';
try {
await client.files.read('/etc/shadow');
} catch (error) {
if (error instanceof PermissionError) console.log('Permission denied');
else if (error instanceof NotFoundError) console.log('File not found');
else if (error instanceof AgentOfflineError) console.log('Agent is offline');
else if (error instanceof RateLimitError) console.log('Rate limit exceeded');
else if (error instanceof CMDOPError) console.log(error.message);
}Related Packages
- @cmdop/core — Shared types and errors
- @cmdop/react — React hooks
Links
License
MIT
