@locusai/locus-gateway
v0.26.6
Published
Channel-agnostic message gateway for Locus platform adapters
Readme
@locusai/locus-gateway
Channel-agnostic message gateway for Locus platform adapters. Routes inbound messages from any platform (Telegram, Discord, WhatsApp, etc.) through a unified command pipeline — parsing, tracking, executing, and formatting output per platform capabilities.
Installation
npm install @locusai/locus-gatewayQuick Start
import { Gateway } from "@locusai/locus-gateway";
// Create the gateway
const gateway = new Gateway({
onEvent: (event) => console.log(`[${event.type}]`, event),
});
// Register a platform adapter
gateway.register(myAdapter);
// Start all adapters
await gateway.start();
// Handle an inbound message (adapters call this internally)
await gateway.handleMessage({
platform: "telegram",
sessionId: "chat-123",
userId: "user-456",
text: "/run 42",
});Architecture
Messages flow through a five-stage pipeline:
Adapter → Gateway → Router → Tracker → Executor → Adapter- Adapter receives a platform message and passes it to the gateway
- Router (
CommandRouter) parses text into a structuredParsedCommandorFreeText - Tracker (
CommandTracker) checks concurrency — blocks commands that conflict with running ones in the same exclusivity group - Executor (
CommandExecutor) spawns the Locus CLI viainvokeLocusStream(), supports streaming and buffered output modes - Gateway sends the formatted result back through the originating adapter
Free-text messages (non-commands) are automatically routed to locus exec as prompts.
API Reference
Gateway
Central orchestrator connecting platform adapters to the command pipeline.
import { Gateway } from "@locusai/locus-gateway";
const gateway = new Gateway(options?: GatewayOptions);GatewayOptions
| Property | Type | Description |
|----------|------|-------------|
| onEvent | (event: GatewayEvent) => void | Optional handler for gateway lifecycle events |
Methods
| Method | Description |
|--------|-------------|
| register(adapter) | Register a PlatformAdapter (throws if platform already registered) |
| getAdapter(platform) | Get a registered adapter by platform name |
| handleMessage(message) | Main entry point — parse, route, and execute an InboundMessage |
| start() | Start all registered adapters |
| stop() | Stop all adapters gracefully |
| getRouter() | Access the CommandRouter instance |
| getExecutor() | Access the CommandExecutor instance |
| getTracker() | Access the CommandTracker instance |
CommandRouter
Parses inbound text into structured commands or free-text.
import { CommandRouter } from "@locusai/locus-gateway";
const router = new CommandRouter(prefix?: string); // default: "/"
const parsed = router.parse("/run 42 43");
// → { type: "command", command: "run", args: ["42", "43"], raw: "/run 42 43" }Returns ParsedCommand for slash commands, FreeText for everything else. Handles @botname suffixes (e.g., /run@MyBot → command "run").
CommandExecutor
Executes Locus CLI commands via subprocess, with streaming or buffered output.
import { CommandExecutor, CommandTracker } from "@locusai/locus-gateway";
const tracker = new CommandTracker();
const executor = new CommandExecutor(tracker);
// Buffered execution
const result = await executor.executeLocusCommand(sessionId, "status", []);
// Streaming execution with callbacks
const result = await executor.executeLocusCommand(sessionId, "run", ["42"], {
onStart: async (text) => { /* send initial message, return messageId */ },
onUpdate: async (messageId, text) => { /* edit message with progress */ },
onComplete: async (messageId, text, exitCode) => { /* final update */ },
});StreamCallbacks
| Callback | Description |
|----------|-------------|
| onStart(text) | Called when execution begins. Return a message ID for subsequent edits |
| onUpdate(messageId, text) | Called periodically with updated output (every ~2s) |
| onComplete(messageId, text, exitCode) | Called when execution finishes |
CommandTracker
Tracks active commands per session and enforces exclusivity groups.
import { CommandTracker } from "@locusai/locus-gateway";
const tracker = new CommandTracker();
// Check for conflicts before executing
const conflict = tracker.checkExclusiveConflict(sessionId, "run");
if (conflict) {
console.log(`Blocked by: ${conflict.runningCommand.command}`);
}
// Track a running command
const id = tracker.track(sessionId, "run", ["42"], childProcess);
// List active commands
const active = tracker.getActive(sessionId);
// Kill a specific command or all commands in a session
tracker.kill(sessionId, id);
tracker.killAll(sessionId);
// Untrack when done
tracker.untrack(sessionId, id);Exclusivity Groups
| Group | Commands | Behavior |
|-------|----------|----------|
| workspace | run, plan, iterate, exec | Only one per session |
| git | stage, commit, checkout, stash, pr | Only one per session |
Commands outside these groups can run concurrently.
Formatting Utilities
import { bestFormat, splitMessage, truncate } from "@locusai/locus-gateway";
// Pick the richest format a platform supports (HTML > Markdown > plain)
const format = bestFormat(adapter.capabilities);
// Split long text respecting platform message limits
const chunks = splitMessage(longText, 4096);
// Truncate text with an indicator
const short = truncate(longText, 200);Command Registry
import {
COMMAND_REGISTRY,
STREAMING_COMMANDS,
getCommandDefinition,
} from "@locusai/locus-gateway";
// Look up a command definition
const def = getCommandDefinition("run");
// → { cliArgs: ["run"], streaming: true }
// Check if a command streams
STREAMING_COMMANDS.has("run"); // truePlatform Adapter Interface
To build a custom adapter, implement the PlatformAdapter interface:
import type {
PlatformAdapter,
PlatformCapabilities,
OutboundMessage,
} from "@locusai/locus-gateway";
class MyAdapter implements PlatformAdapter {
readonly platform = "my-platform";
capabilities: PlatformCapabilities = {
supportsEditing: true, // Can edit previously sent messages
supportsInlineButtons: true, // Supports inline action buttons
supportsMarkdown: true, // Supports Markdown formatting
supportsHTML: false, // Supports HTML formatting
supportsFileUpload: false, // Supports file attachments
maxMessageLength: 4096, // Max characters before message splitting
supportsStreaming: true, // Enable streaming via message edits
};
async start(): Promise<void> {
// Initialize platform connection (e.g., connect to API, start polling)
}
async stop(): Promise<void> {
// Clean up resources, disconnect
}
async send(sessionId: string, message: OutboundMessage): Promise<void> {
// Send a message to the platform
}
// Optional — required if supportsEditing is true
async edit?(sessionId: string, messageId: string, message: OutboundMessage): Promise<void> {
// Edit a previously sent message
}
}PlatformCapabilities
| Property | Type | Description |
|----------|------|-------------|
| supportsEditing | boolean | Can edit sent messages (enables streaming via edits) |
| supportsInlineButtons | boolean | Supports inline action buttons |
| supportsMarkdown | boolean | Supports Markdown formatting |
| supportsHTML | boolean | Supports HTML formatting |
| supportsFileUpload | boolean | Supports file uploads/attachments |
| maxMessageLength | number | Max characters per message before splitting |
| supportsStreaming | boolean | Enable real-time streaming output via message edits |
OutboundMessage
| Property | Type | Description |
|----------|------|-------------|
| text | string | Message content |
| format | "plain" \| "markdown" \| "html" | Text format |
| actions? | Action[] | Optional inline buttons/links |
| attachments? | Attachment[] | Optional file attachments |
Gateway Events
Subscribe to lifecycle events via the onEvent option:
const gateway = new Gateway({
onEvent: (event) => {
switch (event.type) {
case "message_received": // Inbound message from a platform
case "command_started": // Command execution began
case "command_completed": // Command finished (includes exitCode)
case "error": // Error with optional context string
}
},
});Related Packages
| Package | Description |
|---------|-------------|
| @locusai/locus-pm2 | Process management for long-running adapter daemons |
| @locusai/locus-telegram | Telegram adapter built on this gateway |
| @locusai/sdk | SDK for building community packages |
