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.
Files changed (42) hide show
  1. package/README.md +330 -0
  2. package/dist/adapters/base.d.ts +57 -0
  3. package/dist/adapters/base.d.ts.map +1 -0
  4. package/dist/adapters/base.js +28 -0
  5. package/dist/adapters/base.js.map +1 -0
  6. package/dist/adapters/claude.d.ts +45 -0
  7. package/dist/adapters/claude.d.ts.map +1 -0
  8. package/dist/adapters/claude.js +201 -0
  9. package/dist/adapters/claude.js.map +1 -0
  10. package/dist/adapters/gemini.d.ts +31 -0
  11. package/dist/adapters/gemini.d.ts.map +1 -0
  12. package/dist/adapters/gemini.js +168 -0
  13. package/dist/adapters/gemini.js.map +1 -0
  14. package/dist/adapters/index.d.ts +5 -0
  15. package/dist/adapters/index.d.ts.map +1 -0
  16. package/dist/adapters/index.js +4 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/config.d.ts +59 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +131 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +102 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/persistent-pty.d.ts +154 -0
  27. package/dist/persistent-pty.d.ts.map +1 -0
  28. package/dist/persistent-pty.js +477 -0
  29. package/dist/persistent-pty.js.map +1 -0
  30. package/dist/sdk-session.d.ts +65 -0
  31. package/dist/sdk-session.d.ts.map +1 -0
  32. package/dist/sdk-session.js +1238 -0
  33. package/dist/sdk-session.js.map +1 -0
  34. package/dist/utils.d.ts +49 -0
  35. package/dist/utils.d.ts.map +1 -0
  36. package/dist/utils.js +166 -0
  37. package/dist/utils.js.map +1 -0
  38. package/dist/version.d.ts +13 -0
  39. package/dist/version.d.ts.map +1 -0
  40. package/dist/version.js +54 -0
  41. package/dist/version.js.map +1 -0
  42. 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