ai-code-connect 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -0
- package/dist/adapters/base.d.ts +57 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +28 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +45 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +201 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +31 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +168 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +131 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/persistent-pty.d.ts +154 -0
- package/dist/persistent-pty.d.ts.map +1 -0
- package/dist/persistent-pty.js +477 -0
- package/dist/persistent-pty.js.map +1 -0
- package/dist/sdk-session.d.ts +65 -0
- package/dist/sdk-session.d.ts.map +1 -0
- package/dist/sdk-session.js +1238 -0
- package/dist/sdk-session.js.map +1 -0
- package/dist/utils.d.ts +49 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +166 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +54 -0
- package/dist/version.js.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# AIC² - AI Code Connect
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
█████╗ ██╗ ██████╗ ██████╗
|
|
5
|
+
██╔══██╗██║██╔════╝ ╚════██╗
|
|
6
|
+
██║ ██║██║██║ █████╔╝
|
|
7
|
+
███████║██║██║ ██╔═══╝
|
|
8
|
+
██╔══██║██║██║ ███████╗
|
|
9
|
+
██║ ██║██║██║ ╚══════╝
|
|
10
|
+
██║ ██║██║╚██████╗
|
|
11
|
+
╚═╝ ╚═╝╚═╝ ╚═════╝
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A CLI tool that connects **Claude Code** and **Gemini CLI**, eliminating manual copy-paste between AI coding assistants.
|
|
15
|
+
|
|
16
|
+
**AIC²** = **A**I **C**ode **C**onnect (the two C's = ²)
|
|
17
|
+
|
|
18
|
+
## The Problem
|
|
19
|
+
|
|
20
|
+
When working with multiple AI coding tools:
|
|
21
|
+
1. Ask Gemini for a proposal
|
|
22
|
+
2. Copy the response
|
|
23
|
+
3. Paste into Claude for review
|
|
24
|
+
4. Copy Claude's feedback
|
|
25
|
+
5. Paste back to Gemini...
|
|
26
|
+
|
|
27
|
+
This is tedious and breaks your flow.
|
|
28
|
+
|
|
29
|
+
## The Solution
|
|
30
|
+
|
|
31
|
+
`aic` bridges both tools in a single interactive session with:
|
|
32
|
+
- **Persistent sessions** - Both tools remember context
|
|
33
|
+
- **One-command forwarding** - Send responses between tools instantly
|
|
34
|
+
- **Interactive mode** - Full access to slash commands and approvals
|
|
35
|
+
- **Detach/reattach** - Keep tools running in background
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g ai-code-connect
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it! The `aic` command is now available globally.
|
|
44
|
+
|
|
45
|
+
### Alternative: Install from Source
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/jacob-bd/ai-code-connect.git
|
|
49
|
+
cd ai-code-connect
|
|
50
|
+
npm install
|
|
51
|
+
npm run build
|
|
52
|
+
npm link
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Prerequisites
|
|
56
|
+
|
|
57
|
+
Install both AI CLI tools:
|
|
58
|
+
|
|
59
|
+
- **Claude Code**: `npm install -g @anthropic-ai/claude-code`
|
|
60
|
+
- **Gemini CLI**: `npm install -g @google/gemini-cli`
|
|
61
|
+
|
|
62
|
+
Verify:
|
|
63
|
+
```bash
|
|
64
|
+
aic tools
|
|
65
|
+
# Should show both as "✓ available"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
aic
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it! This launches the interactive session.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### Basic Commands
|
|
79
|
+
|
|
80
|
+
| Command | Description |
|
|
81
|
+
|---------|-------------|
|
|
82
|
+
| `/claude` | Switch to Claude Code |
|
|
83
|
+
| `/claude -i` | Switch to Claude Code and enter interactive mode |
|
|
84
|
+
| `/gemini` | Switch to Gemini CLI |
|
|
85
|
+
| `/gemini -i` | Switch to Gemini CLI and enter interactive mode |
|
|
86
|
+
| `/i` | Enter interactive mode (full tool access) |
|
|
87
|
+
| `/forward` | Forward last response to other tool (auto-selects if 2 tools) |
|
|
88
|
+
| `/forward [tool]` | Forward to specific tool (required if 3+ tools) |
|
|
89
|
+
| `/forward [tool] [msg]` | Forward with additional context |
|
|
90
|
+
| `/forward -i [tool]` | Forward and stay in interactive mode |
|
|
91
|
+
| `/forwardi [tool]` | Same as `/forward -i` (alias: `/fwdi`) |
|
|
92
|
+
| `/history` | Show conversation history |
|
|
93
|
+
| `/status` | Show running processes |
|
|
94
|
+
| `/clear` | Clear sessions and history |
|
|
95
|
+
| `/quit` or `/cya` | Exit |
|
|
96
|
+
|
|
97
|
+
### Tool Slash Commands
|
|
98
|
+
|
|
99
|
+
Use double slash (`//`) to run tool-specific slash commands:
|
|
100
|
+
|
|
101
|
+
| Input | What Happens |
|
|
102
|
+
|-------|--------------|
|
|
103
|
+
| `//cost` | Opens interactive mode, runs `/cost`, you see output |
|
|
104
|
+
| `//status` | Opens interactive mode, runs `/status`, you can interact |
|
|
105
|
+
| `//config` | Opens interactive mode, runs `/config`, full control |
|
|
106
|
+
|
|
107
|
+
When you type `//command`:
|
|
108
|
+
1. AIC enters interactive mode with the tool
|
|
109
|
+
2. Sends the `/command` for you
|
|
110
|
+
3. You see the full output and can interact
|
|
111
|
+
4. Press `Ctrl+]` when done to return to AIC
|
|
112
|
+
|
|
113
|
+
This approach ensures you can fully view and interact with commands like `/status` that show interactive UIs.
|
|
114
|
+
|
|
115
|
+
### Command Menu
|
|
116
|
+
|
|
117
|
+
Type `/` to see a command menu. Use ↓ arrow to select, or keep typing.
|
|
118
|
+
|
|
119
|
+
### Example Session
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
❯ claude → How should I implement caching for this API?
|
|
123
|
+
|
|
124
|
+
⠹ Claude is thinking...
|
|
125
|
+
I suggest implementing a Redis-based caching layer...
|
|
126
|
+
|
|
127
|
+
❯ claude → /forward What do you think of this approach?
|
|
128
|
+
|
|
129
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
130
|
+
↗ Forwarding from Claude Code → Gemini CLI
|
|
131
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
132
|
+
Gemini CLI responds:
|
|
133
|
+
|
|
134
|
+
The Redis approach is solid. I'd also consider...
|
|
135
|
+
|
|
136
|
+
❯ gemini → /claude
|
|
137
|
+
● Switched to Claude Code
|
|
138
|
+
|
|
139
|
+
❯ claude → Can you implement that?
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Interactive Mode
|
|
143
|
+
|
|
144
|
+
For full tool access (approvals, multi-turn interactions, etc.):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
❯ claude → /i
|
|
148
|
+
|
|
149
|
+
▶ Starting Claude Code interactive mode...
|
|
150
|
+
Press Ctrl+] to detach • /exit to terminate
|
|
151
|
+
|
|
152
|
+
> (interact with Claude directly)
|
|
153
|
+
> (press Ctrl+]) # Detach back to aic
|
|
154
|
+
|
|
155
|
+
⏸ Detached from Claude Code (still running)
|
|
156
|
+
Use /i to re-attach
|
|
157
|
+
|
|
158
|
+
❯ claude → /i # Re-attach to same session
|
|
159
|
+
↩ Re-attaching to Claude Code...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Key bindings in interactive mode:**
|
|
163
|
+
- `Ctrl+]` - Detach (tool keeps running)
|
|
164
|
+
- `/exit` - Terminate the tool session
|
|
165
|
+
|
|
166
|
+
> **Tip:** Use `//status` or `//cost` to quickly run tool commands—AIC will enter interactive mode, run the command, and you press `Ctrl+]` when done.
|
|
167
|
+
|
|
168
|
+
> **Note:** Messages exchanged while in interactive mode (after `/i`) are not captured for forwarding. Use regular mode for conversations you want to forward between tools.
|
|
169
|
+
|
|
170
|
+
### Interactive Forwarding
|
|
171
|
+
|
|
172
|
+
When forwarding a message that might trigger permissions or require interaction (e.g., code edits, file changes), use `/forwardi` or `/forward -i` to stay in interactive mode:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
❯ claude → /forwardi gemini
|
|
176
|
+
|
|
177
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
178
|
+
↗ Forwarding from Claude Code → Gemini CLI (interactive)
|
|
179
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
180
|
+
Gemini CLI responds:
|
|
181
|
+
|
|
182
|
+
> I'll implement those changes. Allow me to edit src/api.ts? [y/n]: y
|
|
183
|
+
> (you can respond to prompts)
|
|
184
|
+
> (press Ctrl+] when done to return to aic)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This is useful when:
|
|
188
|
+
- The AI might request permission to modify files
|
|
189
|
+
- You need to approve or deny actions
|
|
190
|
+
- The response requires multi-turn interaction
|
|
191
|
+
|
|
192
|
+
### Session Persistence
|
|
193
|
+
|
|
194
|
+
Sessions persist automatically within an AIC² session:
|
|
195
|
+
- **Claude**: Uses unique session IDs (`--session-id` / `--resume`) isolated from other Claude instances
|
|
196
|
+
- **Gemini**: Uses `--resume latest` flag
|
|
197
|
+
|
|
198
|
+
Your conversation context is maintained across messages within the same AIC² session.
|
|
199
|
+
|
|
200
|
+
## CLI Options
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
aic # Launch interactive session
|
|
204
|
+
aic tools # List available AI tools
|
|
205
|
+
aic config default # Show current default tool
|
|
206
|
+
aic config default gemini # Set Gemini as default tool
|
|
207
|
+
aic --version # Show version
|
|
208
|
+
aic --help # Show help
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Configuration
|
|
212
|
+
|
|
213
|
+
### Default Tool
|
|
214
|
+
|
|
215
|
+
Set which tool loads by default when you start AIC²:
|
|
216
|
+
|
|
217
|
+
**Option 1: CLI command**
|
|
218
|
+
```bash
|
|
219
|
+
aic config default gemini
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Option 2: Inside AIC²**
|
|
223
|
+
```
|
|
224
|
+
❯ claude → /default gemini
|
|
225
|
+
✓ Default tool set to "gemini". Will be used on next launch.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Option 3: Environment variable (temporary override)**
|
|
229
|
+
```bash
|
|
230
|
+
AIC_DEFAULT_TOOL=gemini aic
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Configuration is stored in `~/.aic/config.json`.
|
|
234
|
+
|
|
235
|
+
## Architecture
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
src/
|
|
239
|
+
├── adapters/
|
|
240
|
+
│ ├── base.ts # ToolAdapter interface & registry
|
|
241
|
+
│ ├── claude.ts # Claude Code adapter
|
|
242
|
+
│ ├── gemini.ts # Gemini CLI adapter
|
|
243
|
+
│ ├── index.ts # Exports all adapters
|
|
244
|
+
│ └── template.ts.example # Template for new adapters
|
|
245
|
+
├── sdk-session.ts # Interactive session & command handling
|
|
246
|
+
├── persistent-pty.ts # Persistent PTY management for tools
|
|
247
|
+
├── index.ts # CLI entry point
|
|
248
|
+
├── config.ts # Configuration management (~/.aic/)
|
|
249
|
+
├── utils.ts # Utilities (command execution, etc.)
|
|
250
|
+
└── version.ts # Version from package.json
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Adding New Tools
|
|
254
|
+
|
|
255
|
+
AIC² is modular. To add a new AI CLI (e.g., OpenAI Codex):
|
|
256
|
+
|
|
257
|
+
1. Copy the template: `cp src/adapters/template.ts.example src/adapters/codex.ts`
|
|
258
|
+
2. Implement the `ToolAdapter` interface
|
|
259
|
+
3. Register in `src/adapters/index.ts` and `src/index.ts`
|
|
260
|
+
4. Add to `src/sdk-session.ts`
|
|
261
|
+
|
|
262
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
|
|
263
|
+
|
|
264
|
+
## Features
|
|
265
|
+
|
|
266
|
+
- ✅ **Colorful UI** - ASCII banner, colored prompts, status indicators
|
|
267
|
+
- ✅ **Rainbow animations** - Animated rainbow effect on slash commands
|
|
268
|
+
- ✅ **Spinner** - Visual feedback while waiting for responses
|
|
269
|
+
- ✅ **Session persistence** - Context maintained across messages
|
|
270
|
+
- ✅ **Interactive mode** - Full tool access with detach/reattach
|
|
271
|
+
- ✅ **Command menu** - Type `/` for autocomplete suggestions
|
|
272
|
+
- ✅ **Forward responses** - One command to send between tools
|
|
273
|
+
- ✅ **Modular adapters** - Easy to add new AI tools
|
|
274
|
+
- ✅ **Cross-platform** - Works on macOS, Linux, and Windows
|
|
275
|
+
- ✅ **Request locking** - Prevents concurrent request issues
|
|
276
|
+
- ✅ **Memory safe** - Conversation history limits prevent memory leaks
|
|
277
|
+
|
|
278
|
+
## Development
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Development mode
|
|
282
|
+
npm run dev
|
|
283
|
+
|
|
284
|
+
# Build
|
|
285
|
+
npm run build
|
|
286
|
+
|
|
287
|
+
# Run
|
|
288
|
+
aic
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Testing
|
|
292
|
+
|
|
293
|
+
AIC² uses [Vitest](https://vitest.dev/) for testing.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Run tests once
|
|
297
|
+
npm test
|
|
298
|
+
|
|
299
|
+
# Run tests in watch mode (re-runs on file changes)
|
|
300
|
+
npm run test:watch
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### What's Tested
|
|
304
|
+
|
|
305
|
+
| File | Tests | Description |
|
|
306
|
+
|------|-------|-------------|
|
|
307
|
+
| `src/utils.test.ts` | 17 | Pure utility functions: `stripAnsi`, `truncate`, `formatResponse` |
|
|
308
|
+
| `src/config.test.ts` | 18 | Config loading, saving, defaults, environment variable handling |
|
|
309
|
+
|
|
310
|
+
### Adding Tests
|
|
311
|
+
|
|
312
|
+
Test files live alongside source files with a `.test.ts` suffix:
|
|
313
|
+
- `src/utils.ts` → `src/utils.test.ts`
|
|
314
|
+
- `src/config.ts` → `src/config.test.ts`
|
|
315
|
+
|
|
316
|
+
Tests are excluded from the build output (`dist/`) but are committed to git.
|
|
317
|
+
|
|
318
|
+
## Vibe Coding Alert
|
|
319
|
+
|
|
320
|
+
Full transparency: this project was built by a non-developer using AI coding assistants (yes, the very tools this project connects). If you're an experienced developer or architect, you might look at this codebase and wince. That's okay.
|
|
321
|
+
|
|
322
|
+
The goal here was to scratch an itch and learn along the way. The code works, but it's likely missing patterns, optimizations, or elegance that only years of experience can provide.
|
|
323
|
+
|
|
324
|
+
**This is where you come in.** If you see something that makes you cringe, please consider contributing rather than just closing the tab. This is open source specifically because human expertise is irreplaceable. Whether it's refactoring, performance improvements, better error handling, or architectural guidance - PRs and issues are welcome.
|
|
325
|
+
|
|
326
|
+
Think of it as a chance to mentor an AI-assisted developer through code review. We all benefit when experienced developers share their knowledge.
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base interface for AI CLI tool adapters
|
|
3
|
+
*/
|
|
4
|
+
export interface SendOptions {
|
|
5
|
+
/** Working directory for the tool */
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** Whether to continue the previous session (default: true) */
|
|
8
|
+
continueSession?: boolean;
|
|
9
|
+
/** Timeout in milliseconds */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** Keep stdin open after command (for interactive sessions) */
|
|
12
|
+
keepStdinOpen?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolAdapter {
|
|
15
|
+
/** Unique name identifier for the tool */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** Display name for the tool */
|
|
18
|
+
readonly displayName: string;
|
|
19
|
+
/** ANSI color code for the tool (e.g., '\x1b[96m' for bright cyan) */
|
|
20
|
+
readonly color: string;
|
|
21
|
+
/** Regex pattern to detect when tool is showing its input prompt (ready for input) */
|
|
22
|
+
readonly promptPattern: RegExp;
|
|
23
|
+
/** Fallback timeout in ms - if no output for this long, assume response complete */
|
|
24
|
+
readonly idleTimeout: number;
|
|
25
|
+
/** Time in ms to wait for tool to start before sending commands (first launch) */
|
|
26
|
+
readonly startupDelay: number;
|
|
27
|
+
/** Check if the tool is installed and available */
|
|
28
|
+
isAvailable(): Promise<boolean>;
|
|
29
|
+
/** Send a prompt to the tool and get a response */
|
|
30
|
+
send(prompt: string, options?: SendOptions): Promise<string>;
|
|
31
|
+
/** Reset conversation context */
|
|
32
|
+
resetContext(): void;
|
|
33
|
+
/** Get the command that would be executed (for debugging) */
|
|
34
|
+
getCommand(prompt: string, options?: SendOptions): string[];
|
|
35
|
+
/** Get the command to start an interactive session */
|
|
36
|
+
getInteractiveCommand(options?: SendOptions): string[];
|
|
37
|
+
/** Get arguments for starting a persistent PTY session */
|
|
38
|
+
getPersistentArgs(): string[];
|
|
39
|
+
/** Clean response output - remove UI noise like spinners, prompts, status lines */
|
|
40
|
+
cleanResponse(rawOutput: string): string;
|
|
41
|
+
/** Check if there's an active session with this tool */
|
|
42
|
+
hasSession(): boolean;
|
|
43
|
+
/** Set whether there's an active session (for persistence) */
|
|
44
|
+
setHasSession(value: boolean): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Registry of available tool adapters
|
|
48
|
+
*/
|
|
49
|
+
export declare class AdapterRegistry {
|
|
50
|
+
private adapters;
|
|
51
|
+
register(adapter: ToolAdapter): void;
|
|
52
|
+
get(name: string): ToolAdapter | undefined;
|
|
53
|
+
getAll(): ToolAdapter[];
|
|
54
|
+
getNames(): string[];
|
|
55
|
+
getAvailable(): Promise<ToolAdapter[]>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/adapters/base.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,gCAAgC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,sFAAsF;IACtF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,mDAAmD;IACnD,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAEhC,mDAAmD;IACnD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7D,iCAAiC;IACjC,YAAY,IAAI,IAAI,CAAC;IAErB,6DAA6D;IAC7D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CAAC;IAE5D,sDAAsD;IACtD,qBAAqB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CAAC;IAEvD,0DAA0D;IAC1D,iBAAiB,IAAI,MAAM,EAAE,CAAC;IAE9B,mFAAmF;IACnF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzC,wDAAwD;IACxD,UAAU,IAAI,OAAO,CAAC;IAEtB,8DAA8D;IAC9D,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAuC;IAEvD,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAIpC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI1C,MAAM,IAAI,WAAW,EAAE;IAIvB,QAAQ,IAAI,MAAM,EAAE;IAId,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;CAS7C"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of available tool adapters
|
|
3
|
+
*/
|
|
4
|
+
export class AdapterRegistry {
|
|
5
|
+
adapters = new Map();
|
|
6
|
+
register(adapter) {
|
|
7
|
+
this.adapters.set(adapter.name, adapter);
|
|
8
|
+
}
|
|
9
|
+
get(name) {
|
|
10
|
+
return this.adapters.get(name);
|
|
11
|
+
}
|
|
12
|
+
getAll() {
|
|
13
|
+
return Array.from(this.adapters.values());
|
|
14
|
+
}
|
|
15
|
+
getNames() {
|
|
16
|
+
return Array.from(this.adapters.keys());
|
|
17
|
+
}
|
|
18
|
+
async getAvailable() {
|
|
19
|
+
const available = [];
|
|
20
|
+
for (const adapter of this.adapters.values()) {
|
|
21
|
+
if (await adapter.isAvailable()) {
|
|
22
|
+
available.push(adapter);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return available;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/adapters/base.ts"],"names":[],"mappings":"AA6DA;;GAEG;AACH,MAAM,OAAO,eAAe;IAClB,QAAQ,GAA6B,IAAI,GAAG,EAAE,CAAC;IAEvD,QAAQ,CAAC,OAAoB;QAC3B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,MAAM;QACJ,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,QAAQ;QACN,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,SAAS,GAAkB,EAAE,CAAC;QACpC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,MAAM,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;gBAChC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ToolAdapter, SendOptions } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Claude Code CLI
|
|
4
|
+
*
|
|
5
|
+
* Claude Code supports:
|
|
6
|
+
* - Non-interactive mode via -p/--print flag
|
|
7
|
+
* - Output formats: text, json, stream-json
|
|
8
|
+
* - Session continuation via --session-id (isolated from other sessions in same directory)
|
|
9
|
+
*/
|
|
10
|
+
export declare class ClaudeAdapter implements ToolAdapter {
|
|
11
|
+
readonly name = "claude";
|
|
12
|
+
readonly displayName = "Claude Code";
|
|
13
|
+
readonly color = "\u001B[96m";
|
|
14
|
+
readonly promptPattern: RegExp;
|
|
15
|
+
readonly idleTimeout = 3000;
|
|
16
|
+
readonly startupDelay = 2500;
|
|
17
|
+
private hasActiveSession;
|
|
18
|
+
private sessionId;
|
|
19
|
+
private sessionCreated;
|
|
20
|
+
isAvailable(): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Get or create a unique session ID for aic's Claude sessions.
|
|
23
|
+
* This isolates aic from other Claude Code sessions in the same directory.
|
|
24
|
+
*/
|
|
25
|
+
private getOrCreateSessionId;
|
|
26
|
+
/**
|
|
27
|
+
* Get session args for Claude commands.
|
|
28
|
+
* First call uses --session-id to CREATE the session.
|
|
29
|
+
* Subsequent calls use --resume to CONTINUE the session (avoids "already in use" errors).
|
|
30
|
+
*/
|
|
31
|
+
private getSessionArgs;
|
|
32
|
+
getCommand(prompt: string, options?: SendOptions): string[];
|
|
33
|
+
getInteractiveCommand(options?: SendOptions): string[];
|
|
34
|
+
getPersistentArgs(): string[];
|
|
35
|
+
cleanResponse(rawOutput: string): string;
|
|
36
|
+
send(prompt: string, options?: SendOptions): Promise<string>;
|
|
37
|
+
resetContext(): void;
|
|
38
|
+
/** Check if there's an active session */
|
|
39
|
+
hasSession(): boolean;
|
|
40
|
+
/** Mark that a session exists (for loading from persisted state) */
|
|
41
|
+
setHasSession(value: boolean): void;
|
|
42
|
+
/** Get current session ID (for debugging/logging) */
|
|
43
|
+
getSessionId(): string | null;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/adapters/claude.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAGrD;;;;;;;GAOG;AACH,qBAAa,aAAc,YAAW,WAAW;IAC/C,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,iBAAiB;IACrC,QAAQ,CAAC,KAAK,gBAAc;IAI5B,QAAQ,CAAC,aAAa,SAAwB;IAI9C,QAAQ,CAAC,WAAW,QAAQ;IAG5B,QAAQ,CAAC,YAAY,QAAQ;IAE7B,OAAO,CAAC,gBAAgB,CAAS;IAIjC,OAAO,CAAC,SAAS,CAAuB;IAGxC,OAAO,CAAC,cAAc,CAAS;IAEzB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAIrC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;;;OAIG;IACH,OAAO,CAAC,cAAc;IActB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE;IA0B3D,qBAAqB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE;IAStD,iBAAiB,IAAI,MAAM,EAAE;IAK7B,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAyFlC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBlE,YAAY,IAAI,IAAI;IAOpB,yCAAyC;IACzC,UAAU,IAAI,OAAO;IAIrB,oEAAoE;IACpE,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAInC,qDAAqD;IACrD,YAAY,IAAI,MAAM,GAAG,IAAI;CAG9B"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { runCommand, commandExists } from '../utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for Claude Code CLI
|
|
5
|
+
*
|
|
6
|
+
* Claude Code supports:
|
|
7
|
+
* - Non-interactive mode via -p/--print flag
|
|
8
|
+
* - Output formats: text, json, stream-json
|
|
9
|
+
* - Session continuation via --session-id (isolated from other sessions in same directory)
|
|
10
|
+
*/
|
|
11
|
+
export class ClaudeAdapter {
|
|
12
|
+
name = 'claude';
|
|
13
|
+
displayName = 'Claude Code';
|
|
14
|
+
color = '\x1b[96m'; // brightCyan
|
|
15
|
+
// Claude shows ❯ character when ready for input (with possible trailing chars like tool name)
|
|
16
|
+
// Match: ❯ followed by optional text and arrow or end of string/line
|
|
17
|
+
promptPattern = /❯\s*(\w*\s*→)?.*$/m;
|
|
18
|
+
// Fallback: if no output for 3 seconds, assume response complete
|
|
19
|
+
// Claude Code can have brief pauses between output chunks during response generation
|
|
20
|
+
idleTimeout = 3000;
|
|
21
|
+
// Claude starts relatively quickly (~2.5 seconds for first launch)
|
|
22
|
+
startupDelay = 2500;
|
|
23
|
+
hasActiveSession = false;
|
|
24
|
+
// Unique session ID for aic's Claude sessions - prevents collision with
|
|
25
|
+
// other Claude Code sessions running in the same directory
|
|
26
|
+
sessionId = null;
|
|
27
|
+
// Track if we've used the session ID at least once (to know whether to use --session-id or --resume)
|
|
28
|
+
sessionCreated = false;
|
|
29
|
+
async isAvailable() {
|
|
30
|
+
return commandExists('claude');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get or create a unique session ID for aic's Claude sessions.
|
|
34
|
+
* This isolates aic from other Claude Code sessions in the same directory.
|
|
35
|
+
*/
|
|
36
|
+
getOrCreateSessionId() {
|
|
37
|
+
if (!this.sessionId) {
|
|
38
|
+
this.sessionId = randomUUID();
|
|
39
|
+
}
|
|
40
|
+
return this.sessionId;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get session args for Claude commands.
|
|
44
|
+
* First call uses --session-id to CREATE the session.
|
|
45
|
+
* Subsequent calls use --resume to CONTINUE the session (avoids "already in use" errors).
|
|
46
|
+
*/
|
|
47
|
+
getSessionArgs() {
|
|
48
|
+
const sessionId = this.getOrCreateSessionId();
|
|
49
|
+
if (!this.sessionCreated) {
|
|
50
|
+
// First call - create the session with this ID
|
|
51
|
+
// Mark as created immediately so subsequent calls use --resume
|
|
52
|
+
// (even if this command fails, the session ID is reserved)
|
|
53
|
+
this.sessionCreated = true;
|
|
54
|
+
return ['--session-id', sessionId];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Subsequent calls - resume the existing session
|
|
58
|
+
return ['--resume', sessionId];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getCommand(prompt, options) {
|
|
62
|
+
// For slash commands, run without -p to access Claude's internal commands
|
|
63
|
+
const isSlashCommand = prompt.startsWith('/');
|
|
64
|
+
const args = [];
|
|
65
|
+
if (!isSlashCommand) {
|
|
66
|
+
args.push('-p'); // Print mode for regular prompts
|
|
67
|
+
// Note: We intentionally don't enable --tools or --permission-mode here
|
|
68
|
+
// This makes print mode read-only (no file edits without user consent)
|
|
69
|
+
// Use /i (interactive mode) for full tool access with approvals
|
|
70
|
+
}
|
|
71
|
+
// Use session args to isolate and continue aic's sessions
|
|
72
|
+
if (options?.continueSession !== false) {
|
|
73
|
+
args.push(...this.getSessionArgs());
|
|
74
|
+
}
|
|
75
|
+
// Add the prompt as the last argument (only for non-slash commands in print mode)
|
|
76
|
+
if (!isSlashCommand) {
|
|
77
|
+
args.push(prompt);
|
|
78
|
+
}
|
|
79
|
+
return ['claude', ...args];
|
|
80
|
+
}
|
|
81
|
+
getInteractiveCommand(options) {
|
|
82
|
+
const args = [];
|
|
83
|
+
// Use session args to maintain isolated session
|
|
84
|
+
if (options?.continueSession !== false) {
|
|
85
|
+
args.push(...this.getSessionArgs());
|
|
86
|
+
}
|
|
87
|
+
return ['claude', ...args];
|
|
88
|
+
}
|
|
89
|
+
getPersistentArgs() {
|
|
90
|
+
// Use session args for PTY - will create or resume as appropriate
|
|
91
|
+
return this.getSessionArgs();
|
|
92
|
+
}
|
|
93
|
+
cleanResponse(rawOutput) {
|
|
94
|
+
let output = rawOutput;
|
|
95
|
+
// Remove all ANSI escape sequences first
|
|
96
|
+
output = output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
97
|
+
output = output.replace(/\x1b\[\??\d+[hl]/g, '');
|
|
98
|
+
output = output.replace(/\x1b\[\d* ?q/g, '');
|
|
99
|
+
output = output.replace(/\x1b\][^\x07]*\x07/g, ''); // OSC sequences
|
|
100
|
+
// Remove spinner frames
|
|
101
|
+
output = output.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '');
|
|
102
|
+
// Remove box drawing characters
|
|
103
|
+
output = output.replace(/[╭╮╰╯│─┌┐└┘├┤┬┴┼║═╔╗╚╝╠╣╦╩╬▐▛▜▝▘]/g, '');
|
|
104
|
+
// Remove common status line patterns (e.g., "Reading file...", "Thinking...")
|
|
105
|
+
output = output.replace(/^(Reading|Writing|Analyzing|Thinking|Searching|Running).*$/gm, '');
|
|
106
|
+
// Remove the prompt line itself (❯ followed by tool name → )
|
|
107
|
+
output = output.replace(/❯\s*[a-z]+\s*→.*$/gm, '');
|
|
108
|
+
output = output.replace(/❯\s*$/gm, '');
|
|
109
|
+
// Remove Claude Code UI elements
|
|
110
|
+
output = output.replace(/^\s*Claude Code v[\d.]+\s*$/gm, '');
|
|
111
|
+
output = output.replace(/^\s*Opus.*API Usage.*$/gm, '');
|
|
112
|
+
output = output.replace(/^\s*\/model to try.*$/gm, '');
|
|
113
|
+
output = output.replace(/^\s*~\/.*$/gm, ''); // Directory paths
|
|
114
|
+
output = output.replace(/^\s*\?\s*for shortcuts\s*$/gm, '');
|
|
115
|
+
output = output.replace(/^\s*Try ".*"\s*$/gm, '');
|
|
116
|
+
// Remove typing hints and keyboard shortcuts help
|
|
117
|
+
output = output.replace(/You can also use Ctrl\+P.*history.*$/gm, '');
|
|
118
|
+
output = output.replace(/\(esc to cancel.*\)/g, '');
|
|
119
|
+
output = output.replace(/^\s*no sandbox.*$/gm, '');
|
|
120
|
+
output = output.replace(/^\s*auto\s*$/gm, '');
|
|
121
|
+
// Remove welcome screen elements
|
|
122
|
+
output = output.replace(/^\s*Welcome back!\s*$/gm, '');
|
|
123
|
+
output = output.replace(/^\s*Tips for getting started\s*$/gm, '');
|
|
124
|
+
output = output.replace(/^\s*Run \/init.*$/gm, '');
|
|
125
|
+
output = output.replace(/^\s*Recent activity\s*$/gm, '');
|
|
126
|
+
output = output.replace(/^\s*No recent activity\s*$/gm, '');
|
|
127
|
+
// Remove tool use indicators (⏺ Read, ⏺ Write, etc.) but keep content
|
|
128
|
+
output = output.replace(/^⏺\s+(Read|Write|Edit|Bash|Glob|Grep)\(.*\)\s*$/gm, '');
|
|
129
|
+
output = output.replace(/^\s*⎿\s+.*$/gm, ''); // Tool result indicators
|
|
130
|
+
// Extract response content - Claude responses often start with ⏺
|
|
131
|
+
const responseBlocks = [];
|
|
132
|
+
const lines = output.split('\n');
|
|
133
|
+
let inResponse = false;
|
|
134
|
+
let currentBlock = [];
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
// Start of a response block (⏺ without tool name after it)
|
|
138
|
+
if (trimmed.startsWith('⏺') && !trimmed.match(/^⏺\s+(Read|Write|Edit|Bash|Glob|Grep)/)) {
|
|
139
|
+
if (currentBlock.length > 0) {
|
|
140
|
+
responseBlocks.push(currentBlock.join('\n'));
|
|
141
|
+
}
|
|
142
|
+
currentBlock = [trimmed.replace(/^⏺\s*/, '')];
|
|
143
|
+
inResponse = true;
|
|
144
|
+
}
|
|
145
|
+
else if (inResponse && trimmed.length > 0) {
|
|
146
|
+
// Continue capturing response content
|
|
147
|
+
if (!trimmed.match(/^[\s│─╭╮╰╯]*$/) && trimmed.length > 2) {
|
|
148
|
+
currentBlock.push(line);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (trimmed.length === 0 && inResponse) {
|
|
152
|
+
currentBlock.push('');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (currentBlock.length > 0) {
|
|
156
|
+
responseBlocks.push(currentBlock.join('\n'));
|
|
157
|
+
}
|
|
158
|
+
// If we found response blocks, use those; otherwise use cleaned output
|
|
159
|
+
if (responseBlocks.length > 0) {
|
|
160
|
+
output = responseBlocks.join('\n\n');
|
|
161
|
+
}
|
|
162
|
+
// Final cleanup
|
|
163
|
+
output = output.replace(/\n{3,}/g, '\n\n');
|
|
164
|
+
output = output.replace(/^\s+$/gm, '');
|
|
165
|
+
return output.trim();
|
|
166
|
+
}
|
|
167
|
+
async send(prompt, options) {
|
|
168
|
+
// For print mode (-p), use non-interactive runCommand to avoid messing with stdin
|
|
169
|
+
const args = this.getCommand(prompt, options).slice(1); // Remove 'claude' from start
|
|
170
|
+
const result = await runCommand('claude', args, {
|
|
171
|
+
cwd: options?.cwd || process.cwd(),
|
|
172
|
+
});
|
|
173
|
+
if (result.exitCode !== 0) {
|
|
174
|
+
const errorMsg = result.stderr.trim() || result.stdout.trim() || 'Unknown error';
|
|
175
|
+
throw new Error(`Claude Code exited with code ${result.exitCode}: ${errorMsg}`);
|
|
176
|
+
}
|
|
177
|
+
// Mark that we now have an active session
|
|
178
|
+
this.hasActiveSession = true;
|
|
179
|
+
// Return stdout (already plain text in print mode)
|
|
180
|
+
return result.stdout.trim();
|
|
181
|
+
}
|
|
182
|
+
resetContext() {
|
|
183
|
+
this.hasActiveSession = false;
|
|
184
|
+
// Generate a new session ID on reset to start fresh
|
|
185
|
+
this.sessionId = null;
|
|
186
|
+
this.sessionCreated = false;
|
|
187
|
+
}
|
|
188
|
+
/** Check if there's an active session */
|
|
189
|
+
hasSession() {
|
|
190
|
+
return this.hasActiveSession;
|
|
191
|
+
}
|
|
192
|
+
/** Mark that a session exists (for loading from persisted state) */
|
|
193
|
+
setHasSession(value) {
|
|
194
|
+
this.hasActiveSession = value;
|
|
195
|
+
}
|
|
196
|
+
/** Get current session ID (for debugging/logging) */
|
|
197
|
+
getSessionId() {
|
|
198
|
+
return this.sessionId;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=claude.js.map
|