Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,4 @@ dist
# AI Assistant settings (local only)
.claude/
.grok/
.gemini_security/
34 changes: 34 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Termly-cli Fork Notes

This fork of `termly-cli` introduces several enhancements to support advanced multi-agent workflows and improved terminal session management.

## Main Changes

### 1. Multi-Agent Batch Support
Added a new `batch` command that allows starting multiple AI agents simultaneously using a single pairing code.
- **Command**: `termly batch <tool[:label]> [tool[:label]...]`
- **Example**: `termly batch klam:frontend klam:backend pi:research`
- **How it works**: It generates one pairing code and registers all requested tools with the backend. When the user pairs in the browser, all agents are connected at once.

### 2. Batch Registration Protocol
- Implemented a new batch registration endpoint `/api/pairing/batch` in `lib/commands/batch.js`.
- Includes a fallback mechanism to sequential registration if the backend does not yet support the batch endpoint.

### 3. Dynamic Session ID Support
- Updated `WebSocketManager` and `PTYManager` to handle dynamic session ID reassignment from the backend.
- This ensures that if the backend assigns a canonical session ID during pairing, the CLI correctly updates its local tracking and PTY management.

### 4. PTY & Session Management Refactoring
- Improved `PTYManager` stability for concurrent sessions.
- Enhanced `CircularBuffer` handling to manage larger terminal outputs (increased to 1MB).
- Better cleanup logic for batch sessions on `SIGINT` and `SIGTERM`.

### 5. CLI Improvements
- Added `batch` command to `bin/cli.js`.
- Updated `list`, `status`, `stop` commands to better handle and display multiple active sessions.
- Improved QR code UI for batch sessions.

## Project Structure Changes
- **New File**: `lib/commands/batch.js` - Core logic for multi-agent orchestration.
- **Modified**: `lib/session/pty-manager.js`, `lib/network/websocket.js` - Refactored for better concurrency and dynamic ID support.
- **Modified**: `lib/ai-tools/registry.js` - Added helper for tool lookups by key.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Termly CLI
# Termly CLI (Forked)

Access your AI coding assistants from any device. Works with Claude Code, Aider, GitHub Copilot, and any terminal-based AI tool.

## What's New in v1.9
> [!NOTE]
> **Fork Features:** This fork adds a new `batch` command for starting multiple AI agents with a single pairing code.
> - `termly batch klam:frontend klam:backend`
> - **See [FORK.md](./FORK.md) for full details.**

## What's New in v1.9 (Upstream)

- 🎯 **Pi Coding Agent** - Support for minimal AI coding agent with extensions and 15+ LLM providers
- 🚀 **Kilo Code CLI** - Full TUI support for agentic engineering CLI with 500+ models
Expand Down
35 changes: 34 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ const startCommand = require('../lib/commands/start');
const statusCommand = require('../lib/commands/status');
const stopCommand = require('../lib/commands/stop');
const listCommand = require('../lib/commands/list');
const batchCommand = require('../lib/commands/batch');
const toolsCommand = require('../lib/commands/tools');
const configCommand = require('../lib/commands/config');
const cleanupCommand = require('../lib/commands/cleanup');
const serverCommand = require('../lib/commands/server');

const program = new Command();

Expand All @@ -35,18 +37,31 @@ program
.command('start [directory]', { isDefault: true })
.description('Start AI tool with remote access')
.option('--ai <tool>', 'Specify AI tool to use')
.option('--ai-args <args>', 'Additional arguments for AI tool (e.g., "--continue" for Claude Code)')
.option('--label <name>', 'Unique label for this agent (allows multiple in same directory)')
.option('--multi', 'Bypass all existing session checks')
.option('--ai-args <args>', 'Additional arguments for AI tool')
.option('--continue', 'Continue previous session (supported by Claude Code)')
.option('--no-auto-detect', 'Disable AI tool auto-detection')
.option('--debug', 'Enable debug logging')
.action(async (directory, options) => {
await startCommand(directory, options);
});

// Batch command
program
.command('batch <tools...>')
.description('Start multiple AI tools under one pairing session')
.option('--ai-args <args>', 'Additional arguments for AI tools')
.action(async (tools, options) => {
await batchCommand(tools, options);
});

// Status command
program
.command('status')
.description('Show session status')
.option('--all', 'Show all sessions including stopped')
.option('--label <name>', 'Filter by agent label')
.action(async (options) => {
await statusCommand(options);
});
Expand All @@ -56,6 +71,7 @@ program
.command('stop [session-id]')
.description('Stop session(s)')
.option('--all', 'Stop all sessions')
.option('--label <name>', 'Stop session with specific label')
.action(async (sessionId, options) => {
await stopCommand(sessionId, options);
});
Expand Down Expand Up @@ -103,6 +119,15 @@ program.on('--help', () => {
console.log(' $ termly tools list # List available tools');
console.log(' $ termly status # Show all sessions');
console.log('');
console.log('Multi-Agent Support:');
console.log(' Run multiple agents in the same project using labels:');
console.log(' $ termly --label agent-1 # Start first agent');
console.log(' $ termly --label agent-2 # Start second agent');
console.log('');
console.log('Batching Agents:');
console.log(' Start multiple tools under ONE QR code:');
console.log(' $ termly batch aider claude:agent-2 # Format: tool:label');
console.log('');
console.log('Special modes:');
console.log(' --ai demo Demo mode for testing (no AI agent installation required)');
console.log('');
Expand Down Expand Up @@ -138,5 +163,13 @@ program.on('--help', () => {
console.log('');
});

// Server command — manage local termly server
program
.command('server [action]')
.description('Manage local Termly server (start/stop/status/launch)')
.action(async (action = 'start') => {
await serverCommand(action);
});

// Parse arguments
program.parse(process.argv);
206 changes: 182 additions & 24 deletions lib/ai-tools/registry.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const { spawn } = require('child_process');

// AI Tools Registry
const AI_TOOLS = {
Expand Down Expand Up @@ -31,6 +29,15 @@ const AI_TOOLS = {
website: 'https://openai.com/codex',
checkInstalled: async () => await commandExists('codex')
},
'crush': {
key: 'crush',
command: 'crush',
args: [],
displayName: 'Crush',
description: 'Terminal-first AI coding assistant',
website: 'https://github.com/charmbracelet/crush',
checkInstalled: async () => await commandExists('crush')
},
'github-copilot': {
key: 'github-copilot',
command: 'copilot',
Expand Down Expand Up @@ -175,6 +182,105 @@ const AI_TOOLS = {
website: 'https://kilo.ai',
checkInstalled: async () => await commandExists('kilo')
},
'kimi': {
key: 'kimi',
command: 'kimi',
args: [],
displayName: 'Kimi Code',
description: 'Moonshot AI\'s autonomous coding agent',
website: 'https://github.com/MoonshotAI/kimi-cli',
checkInstalled: async () => await commandExists('kimi')
},
'zai': {
key: 'zai',
command: 'zai',
args: [],
displayName: 'ZAI CLI',
description: 'Zhipu AI\'s conversational agent for GLM models',
website: 'https://z.ai',
checkInstalled: async () => await commandExists('zai')
},
'interpreter': {
key: 'interpreter',
command: 'interpreter',
args: [],
displayName: 'Open Interpreter',
description: 'Natural language interface for your computer',
website: 'https://openinterpreter.com',
checkInstalled: async () => await commandExists('interpreter')
},
'fabric': {
key: 'fabric',
command: 'fabric',
args: [],
displayName: 'Fabric',
description: 'Augmenting humans using AI patterns',
website: 'https://github.com/danielmiessler/fabric',
checkInstalled: async () => await commandExists('fabric')
},
'plandex': {
key: 'plandex',
command: 'plandex',
args: [],
displayName: 'Plandex',
description: 'AI coding agent for complex, multi-stage tasks',
website: 'https://plandex.ai',
checkInstalled: async () => await commandExists('plandex')
},
'shor': {
key: 'shor',
command: 'shor',
args: [],
displayName: 'ShellOracle',
description: 'Intelligent shell command generation',
website: 'https://github.com/djcopley/ShellOracle',
checkInstalled: async () => await commandExists('shor')
},
'gptme': {
key: 'gptme',
command: 'gptme',
args: [],
displayName: 'gptme',
description: 'Personal AI assistant in your terminal',
website: 'https://gptme.org',
checkInstalled: async () => await commandExists('gptme')
},
'hermes': {
key: 'hermes',
command: 'hermes',
args: [],
displayName: 'Hermes Agent',
description: 'Self-improving AI agent by Nous Research with learning loop and multi-platform support',
website: 'https://hermes-agent.nousresearch.com',
checkInstalled: async () => await commandExists('hermes')
},
'mods': {
key: 'mods',
command: 'mods',
args: [],
displayName: 'Mods',
description: 'AI for the command line (by Charm)',
website: 'https://github.com/charmbracelet/mods',
checkInstalled: async () => await commandExists('mods')
},
'vibe': {
key: 'vibe',
command: 'prvibe',
args: [],
displayName: 'Mistral Vibe',
description: 'Mistral Vibe PR workflow CLI',
website: 'https://github.com/mistralai/vibe',
checkInstalled: async () => await commandExists('prvibe')
},
'qodo': {
key: 'qodo',
command: 'qodo',
args: [],
displayName: 'Qodo CLI',
description: 'Quality-focused AI code review and testing',
website: 'https://www.qodo.ai',
checkInstalled: async () => await commandExists('qodo')
},
'demo': {
key: 'demo',
command: 'node',
Expand All @@ -183,41 +289,93 @@ const AI_TOOLS = {
description: 'Interactive demo for testing (no AI installation required)',
website: 'https://termly.dev',
checkInstalled: async () => true // Always available
},
'openclaude': {
key: 'openclaude',
command: 'openclaude',
args: [],
displayName: 'OpenClaude (Profile 1)',
description: 'Claude Code with custom API key profile 1',
website: 'https://docs.termly.dev',
checkInstalled: async () => await commandExists('openclaude')
},
'openclaude2': {
key: 'openclaude2',
command: 'openclaude2',
args: [],
displayName: 'OpenClaude (Profile 2)',
description: 'Claude Code with custom API key profile 2',
website: 'https://docs.termly.dev',
checkInstalled: async () => await commandExists('openclaude2')
},
'openclaude3': {
key: 'openclaude3',
command: 'openclaude3',
args: [],
displayName: 'OpenClaude (Profile 3)',
description: 'Claude Code with custom API key profile 3',
website: 'https://docs.termly.dev',
checkInstalled: async () => await commandExists('openclaude3')
}
};

// Check if command exists
async function commandExists(command) {
try {
return new Promise((resolve) => {
const isWindows = process.platform === 'win32';
const checkCommand = isWindows ? `where ${command}` : `command -v ${command}`;
await execAsync(checkCommand);
return true;
} catch {
return false;
}
const checkCommand = isWindows ? 'where' : 'which';
const args = [command];

const child = spawn(checkCommand, args);

child.on('close', (code) => {
resolve(code === 0);
});

child.on('error', () => {
resolve(false);
});
});
}

// Get tool version
async function getToolVersion(tool) {
const tryVersion = (args) => {
return new Promise((resolve) => {
const child = spawn(tool.command, args);
let output = '';

child.stdout.on('data', (data) => {
output += data.toString();
});

child.stderr.on('data', (data) => {
output += data.toString();
});

child.on('close', (code) => {
if (code === 0) {
resolve(parseVersion(output));
} else {
resolve(null);
}
});

child.on('error', () => {
resolve(null);
});
});
};

// Try --version first
try {
const { stdout } = await execAsync(`${tool.command} --version 2>&1`);
const version = parseVersion(stdout);
if (version !== 'unknown') {
return version;
}
} catch {
// Ignore error, will try -v next
let version = await tryVersion(['--version']);
if (version && version !== 'unknown') {
return version;
}

// Try -v as fallback
try {
const { stdout } = await execAsync(`${tool.command} -v 2>&1`);
return parseVersion(stdout);
} catch {
return 'unknown';
}
version = await tryVersion(['-v']);
return version || 'unknown';
}

// Parse version from output
Expand Down
Loading