AIC² uses a modular adapter pattern that makes it easy to add new AI CLI tools.
src/
├── adapters/
│ ├── base.ts # ToolAdapter interface & registry
│ ├── claude.ts # Claude Code adapter
│ ├── gemini.ts # Gemini CLI adapter
│ ├── index.ts # Exports all adapters
│ └── template.ts.example # Template for new adapters
├── sdk-session.ts # Interactive session & command handling
├── persistent-pty.ts # Persistent PTY management for tools
├── index.ts # CLI entry point
├── config.ts # Configuration management (~/.aic/)
├── utils.ts # Utility functions
└── version.ts # Version (reads from package.json)
git clone https://github.com/jacob-bd/ai-code-connect.git
cd ai-code-connect
npm install
npm run build
npm link # Makes 'aic' available globally| Command | Description |
|---|---|
npm run dev |
Run in development mode (tsx) |
npm run build |
Compile TypeScript |
npm test |
Run tests |
npm run test:watch |
Run tests in watch mode |
npx tsc --noEmit |
Type-check without building |
Copy the template and create your adapter:
cp src/adapters/template.ts.example src/adapters/codex.tsEdit src/adapters/codex.ts. The ToolAdapter interface requires these properties and methods:
import { ToolAdapter, SendOptions } from './base.js';
import { runCommand, commandExists } from '../utils.js';
export class CodexAdapter implements ToolAdapter {
// Required properties
readonly name = 'codex'; // Used in /codex command
readonly displayName = 'OpenAI Codex'; // Shown in UI
readonly color = '\x1b[92m'; // ANSI color (brightGreen)
readonly promptPattern = /^>\s*$/m; // Regex to detect input prompt
readonly idleTimeout = 2000; // Fallback timeout (ms)
readonly startupDelay = 3000; // First launch delay (ms)
private hasActiveSession = false;
async isAvailable(): Promise<boolean> {
return commandExists('codex');
}
getCommand(prompt: string, options?: SendOptions): string[] {
const args: string[] = ['--print'];
if (this.hasActiveSession) args.push('--continue');
args.push(prompt);
return ['codex', ...args];
}
getInteractiveCommand(options?: SendOptions): string[] {
return this.hasActiveSession ? ['codex', '--continue'] : ['codex'];
}
getPersistentArgs(): string[] {
return this.hasActiveSession ? ['--continue'] : [];
}
cleanResponse(rawOutput: string): string {
// Remove ANSI codes, spinners, prompts - see template for details
return rawOutput.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').trim();
}
async send(prompt: string, options?: SendOptions): Promise<string> {
const result = await runCommand('codex', this.getCommand(prompt, options).slice(1));
this.hasActiveSession = true;
return result.stdout.trim();
}
resetContext(): void { this.hasActiveSession = false; }
hasSession(): boolean { return this.hasActiveSession; }
setHasSession(value: boolean): void { this.hasActiveSession = value; }
}See src/adapters/template.ts.example for a complete, well-documented implementation.
Edit src/adapters/index.ts:
export { ToolAdapter, SendOptions, AdapterRegistry } from './base.js';
export { ClaudeAdapter } from './claude.js';
export { GeminiAdapter } from './gemini.js';
export { CodexAdapter } from './codex.js'; // Add this lineEdit src/index.ts:
import { AdapterRegistry, ClaudeAdapter, GeminiAdapter, CodexAdapter } from './adapters/index.js';
const registry = new AdapterRegistry();
registry.register(new ClaudeAdapter());
registry.register(new GeminiAdapter());
registry.register(new CodexAdapter()); // Add this lineEdit src/sdk-session.ts to add the new tool's switch command.
Using AI-assisted development: If you're using Claude Code or similar, just say:
"Add a new tool called 'codex' for OpenAI Codex CLI. Follow the pattern used for claude and gemini in sdk-session.ts"
Manual steps:
- Add to handleMetaCommand switch (search for
case 'gemini'):
case 'codex':
this.activeTool = 'codex';
console.log(`${colors.green}●${colors.reset} Switched to ${colors.brightGreen}OpenAI Codex${colors.reset}`);
break;- Add to command autocomplete (search for
const commands =):
const commands = ['/claude', '/gemini', '/codex', ...];- Add to splash screen commands (search for
commandsLeft):
` ${rainbowText('/codex', 2)} Switch to OpenAI Codex`,The adapter registry handles tool discovery automatically - no need to modify sendToTool() or add session flags manually.
npm run build
aic tools # Verify new tool shows up
aic # Test switching to new tool with /codexEach CLI tool handles session continuation differently:
- Claude Code:
--session-id <uuid>for first call,--resume <uuid>for subsequent calls - Gemini CLI:
--resume latestflag - Your tool: Check your tool's documentation for session/conversation continuation flags
The /forward command behavior changes based on how many tools are registered:
- 2 tools:
/forwardauto-selects the other tool (no argument needed) - 3+ tools: User must specify target:
/forward <tool> [message]
This is handled automatically—no additional code needed when adding tools.
For interactive mode (/i), the tool needs to support running in a PTY.
Most CLI tools do, but check if yours has any special requirements.
Pick a distinctive color for your tool's UI elements.
Available: brightCyan, brightMagenta, brightYellow, brightGreen, brightBlue, brightRed
See the template file for a complete working example:
src/adapters/template.ts.example
Version is managed in package.json only. The src/version.ts file reads from package.json at runtime, ensuring a single source of truth.
// src/version.ts - DO NOT hardcode versions elsewhere
import { VERSION } from './version.js';- TypeScript: Use strict types, avoid
any - Imports: Use
.jsextensions for relative imports (ESM requirement) - Async: Use
async/awaitover raw promises - Constants: Extract magic numbers to named constants
- Cleanup: Always use
try/finallyfor resource cleanup
Tests live alongside source files with .test.ts suffix:
npm test # Run once
npm run test:watch # Watch modeAdd tests for any new utility functions. Integration tests for adapters are optional but appreciated.
- Input validation: Validate command names before shell execution
- No shell interpolation: Use
spawn()with arrays, notexec()with strings - File permissions: Config files should use mode
0o600
The adapter pattern is designed to be flexible. If your tool has unique requirements,
you can extend the ToolAdapter interface or add tool-specific methods.
Open an issue at github.com/jacob-bd/ai-code-connect/issues for questions or feature requests.