ai-code-connect 1.0.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 +280 -0
- package/dist/adapters/base.d.ts +43 -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 +23 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +68 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +24 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +59 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -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 +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +104 -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 +98 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk-session.d.ts +52 -0
- package/dist/sdk-session.d.ts.map +1 -0
- package/dist/sdk-session.js +1341 -0
- package/dist/sdk-session.js.map +1 -0
- package/dist/utils.d.ts +43 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +138 -0
- package/dist/utils.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import * as pty from 'node-pty';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import TerminalRenderer from 'marked-terminal';
|
|
6
|
+
import { stripAnsi } from './utils.js';
|
|
7
|
+
import { getDefaultTool, setDefaultTool } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Get the version of a CLI tool
|
|
10
|
+
*/
|
|
11
|
+
function getToolVersion(command) {
|
|
12
|
+
try {
|
|
13
|
+
const output = execSync(`${command} -v 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
14
|
+
// Extract version number (first line, clean up)
|
|
15
|
+
const firstLine = output.split('\n')[0];
|
|
16
|
+
// Handle formats like "2.0.59 (Claude Code)" or just "0.19.1"
|
|
17
|
+
const version = firstLine.split(' ')[0];
|
|
18
|
+
return version || null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Configure marked to render markdown for terminal with colors
|
|
25
|
+
marked.setOptions({
|
|
26
|
+
// @ts-ignore - marked-terminal types not fully compatible
|
|
27
|
+
renderer: new TerminalRenderer({
|
|
28
|
+
// Customize colors
|
|
29
|
+
codespan: (code) => `\x1b[93m${code}\x1b[0m`, // Yellow for inline code
|
|
30
|
+
strong: (text) => `\x1b[1m${text}\x1b[0m`, // Bold
|
|
31
|
+
em: (text) => `\x1b[3m${text}\x1b[0m`, // Italic
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
// Detach key codes - multiple options for compatibility across terminals
|
|
35
|
+
// Traditional raw control characters (used by Terminal.app and others)
|
|
36
|
+
const DETACH_KEYS = {
|
|
37
|
+
CTRL_BRACKET: 0x1d, // Ctrl+] = 0x1D = 29
|
|
38
|
+
CTRL_BACKSLASH: 0x1c, // Ctrl+\ = 0x1C = 28
|
|
39
|
+
CTRL_CARET: 0x1e, // Ctrl+^ = 0x1E = 30 (Ctrl+Shift+6 on US keyboards)
|
|
40
|
+
CTRL_UNDERSCORE: 0x1f, // Ctrl+_ = 0x1F = 31 (Ctrl+Shift+- on US keyboards)
|
|
41
|
+
ESCAPE: 0x1b, // Escape = 0x1B = 27
|
|
42
|
+
};
|
|
43
|
+
// CSI u sequences - modern keyboard protocol used by iTerm2
|
|
44
|
+
// Format: ESC [ <keycode> ; <modifiers> u
|
|
45
|
+
// Modifier 5 = Ctrl (4) + 1
|
|
46
|
+
const CSI_U_DETACH_SEQS = [
|
|
47
|
+
'\x1b[93;5u', // Ctrl+] (keycode 93 = ])
|
|
48
|
+
'\x1b[92;5u', // Ctrl+\ (keycode 92 = \)
|
|
49
|
+
'\x1b[54;5u', // Ctrl+^ / Ctrl+6 (keycode 54 = 6)
|
|
50
|
+
'\x1b[45;5u', // Ctrl+_ / Ctrl+- (keycode 45 = -)
|
|
51
|
+
'\x1b[54;6u', // Ctrl+Shift+6 (modifier 6 = Ctrl+Shift)
|
|
52
|
+
'\x1b[45;6u', // Ctrl+Shift+- (modifier 6 = Ctrl+Shift)
|
|
53
|
+
];
|
|
54
|
+
// Terminal sequences to filter out
|
|
55
|
+
// Focus reporting - sent by terminals when window gains/loses focus
|
|
56
|
+
const FOCUS_IN_SEQ = '\x1b[I'; // ESC [ I - Focus gained
|
|
57
|
+
const FOCUS_OUT_SEQ = '\x1b[O'; // ESC [ O - Focus lost
|
|
58
|
+
// Regex to match terminal response sequences we want to filter
|
|
59
|
+
// These include Device Attributes responses, cursor position reports, etc.
|
|
60
|
+
const TERMINAL_RESPONSE_REGEX = /\x1b\[\??[\d;]*[a-zA-Z]/g;
|
|
61
|
+
// For backwards compatibility
|
|
62
|
+
const DETACH_KEY = '\x1d';
|
|
63
|
+
// Spinner frames
|
|
64
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
65
|
+
// ANSI cursor control
|
|
66
|
+
const cursor = {
|
|
67
|
+
show: '\x1b[?25h',
|
|
68
|
+
hide: '\x1b[?25l',
|
|
69
|
+
blockBlink: '\x1b[1 q',
|
|
70
|
+
blockSteady: '\x1b[2 q',
|
|
71
|
+
underlineBlink: '\x1b[3 q',
|
|
72
|
+
underlineSteady: '\x1b[4 q',
|
|
73
|
+
barBlink: '\x1b[5 q',
|
|
74
|
+
barSteady: '\x1b[6 q',
|
|
75
|
+
};
|
|
76
|
+
// ANSI Color codes
|
|
77
|
+
const colors = {
|
|
78
|
+
reset: '\x1b[0m',
|
|
79
|
+
bold: '\x1b[1m',
|
|
80
|
+
dim: '\x1b[2m',
|
|
81
|
+
// Foreground
|
|
82
|
+
cyan: '\x1b[36m',
|
|
83
|
+
magenta: '\x1b[35m',
|
|
84
|
+
yellow: '\x1b[33m',
|
|
85
|
+
green: '\x1b[32m',
|
|
86
|
+
blue: '\x1b[34m',
|
|
87
|
+
red: '\x1b[31m',
|
|
88
|
+
white: '\x1b[37m',
|
|
89
|
+
gray: '\x1b[90m',
|
|
90
|
+
// Bright foreground
|
|
91
|
+
brightCyan: '\x1b[96m',
|
|
92
|
+
brightMagenta: '\x1b[95m',
|
|
93
|
+
brightYellow: '\x1b[93m',
|
|
94
|
+
brightGreen: '\x1b[92m',
|
|
95
|
+
brightBlue: '\x1b[94m',
|
|
96
|
+
brightWhite: '\x1b[97m',
|
|
97
|
+
// Background
|
|
98
|
+
bgBlue: '\x1b[44m',
|
|
99
|
+
bgMagenta: '\x1b[45m',
|
|
100
|
+
bgCyan: '\x1b[46m',
|
|
101
|
+
};
|
|
102
|
+
// Rainbow colors for animated effect
|
|
103
|
+
const RAINBOW_COLORS = [
|
|
104
|
+
'\x1b[91m', // bright red
|
|
105
|
+
'\x1b[93m', // bright yellow
|
|
106
|
+
'\x1b[92m', // bright green
|
|
107
|
+
'\x1b[96m', // bright cyan
|
|
108
|
+
'\x1b[94m', // bright blue
|
|
109
|
+
'\x1b[95m', // bright magenta
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* Apply rainbow gradient to text (static)
|
|
113
|
+
*/
|
|
114
|
+
function rainbowText(text, offset = 0) {
|
|
115
|
+
let result = '';
|
|
116
|
+
for (let i = 0; i < text.length; i++) {
|
|
117
|
+
const colorIndex = (i + offset) % RAINBOW_COLORS.length;
|
|
118
|
+
result += RAINBOW_COLORS[colorIndex] + text[i];
|
|
119
|
+
}
|
|
120
|
+
return result + colors.reset;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Animate rainbow text in place
|
|
124
|
+
*/
|
|
125
|
+
function animateRainbow(text, duration = 600) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
let offset = 0;
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
const animate = () => {
|
|
130
|
+
const elapsed = Date.now() - startTime;
|
|
131
|
+
if (elapsed >= duration) {
|
|
132
|
+
// Final render
|
|
133
|
+
process.stdout.write('\r' + rainbowText(text, offset) + ' ');
|
|
134
|
+
resolve();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
process.stdout.write('\r' + rainbowText(text, offset));
|
|
138
|
+
offset = (offset + 1) % RAINBOW_COLORS.length;
|
|
139
|
+
setTimeout(animate, 50);
|
|
140
|
+
};
|
|
141
|
+
animate();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Get terminal width (with fallback)
|
|
145
|
+
function getTerminalWidth() {
|
|
146
|
+
return process.stdout.columns || 80;
|
|
147
|
+
}
|
|
148
|
+
// Create a full-width horizontal line
|
|
149
|
+
function fullWidthLine(char = '═', color = colors.dim) {
|
|
150
|
+
const width = getTerminalWidth();
|
|
151
|
+
return `${color}${char.repeat(width)}${colors.reset}`;
|
|
152
|
+
}
|
|
153
|
+
// ASCII Art banner for AIC² (larger version)
|
|
154
|
+
const AIC_BANNER = `
|
|
155
|
+
${colors.brightCyan} ██████╗ ${colors.brightMagenta}██╗${colors.brightYellow} ██████╗${colors.reset} ${colors.dim}²${colors.reset}
|
|
156
|
+
${colors.brightCyan} ██╔══██╗ ${colors.brightMagenta}██║${colors.brightYellow}██╔════╝${colors.reset}
|
|
157
|
+
${colors.brightCyan} ███████║ ${colors.brightMagenta}██║${colors.brightYellow}██║ ${colors.reset}
|
|
158
|
+
${colors.brightCyan} ██╔══██║ ${colors.brightMagenta}██║${colors.brightYellow}██║ ${colors.reset}
|
|
159
|
+
${colors.brightCyan} ██║ ██║ ${colors.brightMagenta}██║${colors.brightYellow}╚██████╗${colors.reset}
|
|
160
|
+
${colors.brightCyan} ╚═╝ ╚═╝ ${colors.brightMagenta}╚═╝${colors.brightYellow} ╚═════╝${colors.reset}
|
|
161
|
+
`;
|
|
162
|
+
const VERSION = 'v1.0.0';
|
|
163
|
+
const AVAILABLE_TOOLS = [
|
|
164
|
+
{ name: 'claude', displayName: 'Claude Code', color: colors.brightCyan },
|
|
165
|
+
{ name: 'gemini', displayName: 'Gemini CLI', color: colors.brightMagenta },
|
|
166
|
+
// Add new tools here, e.g.:
|
|
167
|
+
// { name: 'codex', displayName: 'Codex CLI', color: colors.brightGreen },
|
|
168
|
+
];
|
|
169
|
+
function getToolConfig(name) {
|
|
170
|
+
return AVAILABLE_TOOLS.find(t => t.name === name);
|
|
171
|
+
}
|
|
172
|
+
function getToolColor(name) {
|
|
173
|
+
return getToolConfig(name)?.color || colors.white;
|
|
174
|
+
}
|
|
175
|
+
function getToolDisplayName(name) {
|
|
176
|
+
return getToolConfig(name)?.displayName || name;
|
|
177
|
+
}
|
|
178
|
+
// AIC command definitions (single slash for AIC commands)
|
|
179
|
+
const AIC_COMMANDS = [
|
|
180
|
+
{ value: '/claude', name: `${rainbowText('/claude')} Switch to Claude Code`, description: 'Switch to Claude Code' },
|
|
181
|
+
{ value: '/gemini', name: `${rainbowText('/gemini', 1)} Switch to Gemini CLI`, description: 'Switch to Gemini CLI' },
|
|
182
|
+
{ value: '/i', name: `${rainbowText('/i', 2)} Enter interactive mode`, description: 'Enter interactive mode (Ctrl+] or Ctrl+\\ to detach)' },
|
|
183
|
+
{ value: '/forward', name: `${rainbowText('/forward', 3)} Forward last response`, description: 'Forward response: /forward [tool] [msg]' },
|
|
184
|
+
{ value: '/fwd', name: `${rainbowText('/fwd', 4)} Forward (alias)`, description: 'Forward response: /fwd [tool] [msg]' },
|
|
185
|
+
{ value: '/history', name: `${rainbowText('/history', 4)} Show conversation`, description: 'Show conversation history' },
|
|
186
|
+
{ value: '/status', name: `${rainbowText('/status', 5)} Show running processes`, description: 'Show daemon status' },
|
|
187
|
+
{ value: '/default', name: `${rainbowText('/default', 0)} Set default tool`, description: 'Set default tool: /default <claude|gemini>' },
|
|
188
|
+
{ value: '/help', name: `${rainbowText('/help', 1)} Show help`, description: 'Show available commands' },
|
|
189
|
+
{ value: '/clear', name: `${rainbowText('/clear', 2)} Clear sessions`, description: 'Clear sessions and history' },
|
|
190
|
+
{ value: '/quit', name: `${rainbowText('/quit', 3)} Exit`, description: 'Exit AIC' },
|
|
191
|
+
{ value: '/cya', name: `${rainbowText('/cya', 4)} Exit (alias)`, description: 'Exit AIC' },
|
|
192
|
+
];
|
|
193
|
+
function drawBox(content, width = 50) {
|
|
194
|
+
const top = `${colors.gray}╭${'─'.repeat(width - 2)}╮${colors.reset}`;
|
|
195
|
+
const bottom = `${colors.gray}╰${'─'.repeat(width - 2)}╯${colors.reset}`;
|
|
196
|
+
const lines = content.map(line => {
|
|
197
|
+
const padding = width - 4 - stripAnsiLength(line);
|
|
198
|
+
return `${colors.gray}│${colors.reset} ${line}${' '.repeat(Math.max(0, padding))} ${colors.gray}│${colors.reset}`;
|
|
199
|
+
});
|
|
200
|
+
return [top, ...lines, bottom].join('\n');
|
|
201
|
+
}
|
|
202
|
+
function stripAnsiLength(str) {
|
|
203
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
204
|
+
}
|
|
205
|
+
function colorize(text, color) {
|
|
206
|
+
return `${color}${text}${colors.reset}`;
|
|
207
|
+
}
|
|
208
|
+
class Spinner {
|
|
209
|
+
intervalId = null;
|
|
210
|
+
frameIndex = 0;
|
|
211
|
+
message;
|
|
212
|
+
constructor(message = 'Thinking') {
|
|
213
|
+
this.message = message;
|
|
214
|
+
}
|
|
215
|
+
start() {
|
|
216
|
+
this.frameIndex = 0;
|
|
217
|
+
// Clear any garbage on the current line before starting spinner
|
|
218
|
+
process.stdout.write('\x1b[2K\r');
|
|
219
|
+
process.stdout.write(`\n${SPINNER_FRAMES[0]} ${this.message}...`);
|
|
220
|
+
this.intervalId = setInterval(() => {
|
|
221
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
222
|
+
// Move cursor back and overwrite
|
|
223
|
+
process.stdout.write(`\r${SPINNER_FRAMES[this.frameIndex]} ${this.message}...`);
|
|
224
|
+
}, 80);
|
|
225
|
+
}
|
|
226
|
+
stop() {
|
|
227
|
+
if (this.intervalId) {
|
|
228
|
+
clearInterval(this.intervalId);
|
|
229
|
+
this.intervalId = null;
|
|
230
|
+
// Clear the spinner line
|
|
231
|
+
process.stdout.write('\r' + ' '.repeat(this.message.length + 15) + '\r');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Session with persistent interactive mode support
|
|
237
|
+
* - Regular messages: uses -p (print mode) with --continue/--resume
|
|
238
|
+
* - Interactive mode: persistent PTY process, detach with Ctrl+]
|
|
239
|
+
*/
|
|
240
|
+
export class SDKSession {
|
|
241
|
+
isRunning = false;
|
|
242
|
+
activeTool;
|
|
243
|
+
conversationHistory = [];
|
|
244
|
+
// Session tracking (for print mode)
|
|
245
|
+
claudeHasSession = false;
|
|
246
|
+
geminiHasSession = false;
|
|
247
|
+
// Persistent PTY processes for interactive mode
|
|
248
|
+
runningProcesses = new Map();
|
|
249
|
+
// Buffer to capture interactive mode output for forwarding
|
|
250
|
+
interactiveOutputBuffer = new Map();
|
|
251
|
+
// Working directory
|
|
252
|
+
cwd;
|
|
253
|
+
// Readline interface for input with history
|
|
254
|
+
rl = null;
|
|
255
|
+
inputHistory = [];
|
|
256
|
+
constructor(cwd) {
|
|
257
|
+
this.cwd = cwd || process.cwd();
|
|
258
|
+
// Load default tool from config (or env var)
|
|
259
|
+
this.activeTool = getDefaultTool();
|
|
260
|
+
}
|
|
261
|
+
async start() {
|
|
262
|
+
// Ensure cursor is visible
|
|
263
|
+
process.stdout.write(cursor.show + cursor.blockBlink);
|
|
264
|
+
const width = getTerminalWidth();
|
|
265
|
+
// Get tool versions to check availability
|
|
266
|
+
const claudeVersion = getToolVersion('claude');
|
|
267
|
+
const geminiVersion = getToolVersion('gemini');
|
|
268
|
+
const claudeAvailable = claudeVersion !== null;
|
|
269
|
+
const geminiAvailable = geminiVersion !== null;
|
|
270
|
+
const availableCount = (claudeAvailable ? 1 : 0) + (geminiAvailable ? 1 : 0);
|
|
271
|
+
// Handle no tools available
|
|
272
|
+
if (availableCount === 0) {
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(`${colors.red}✗ No AI tools found!${colors.reset}`);
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(`${colors.dim}AIC² bridges multiple AI CLI tools. Please install both:${colors.reset}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log(` ${colors.brightCyan}Claude Code${colors.reset}: npm install -g @anthropic-ai/claude-code`);
|
|
279
|
+
console.log(` ${colors.brightMagenta}Gemini CLI${colors.reset}: npm install -g @google/gemini-cli`);
|
|
280
|
+
console.log('');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
// Handle only one tool available
|
|
284
|
+
if (availableCount === 1) {
|
|
285
|
+
const availableTool = claudeAvailable ? 'Claude Code' : 'Gemini CLI';
|
|
286
|
+
const availableCmd = claudeAvailable ? 'claude' : 'gemini';
|
|
287
|
+
const missingTool = claudeAvailable ? 'Gemini CLI' : 'Claude Code';
|
|
288
|
+
const missingInstall = claudeAvailable
|
|
289
|
+
? 'npm install -g @google/gemini-cli'
|
|
290
|
+
: 'npm install -g @anthropic-ai/claude-code';
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(`${colors.yellow}⚠ Only ${availableTool} found${colors.reset}`);
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(`${colors.dim}AIC² bridges multiple AI tools - you need both installed.${colors.reset}`);
|
|
295
|
+
console.log(`${colors.dim}Install ${missingTool}:${colors.reset}`);
|
|
296
|
+
console.log(` ${missingInstall}`);
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(`${colors.dim}Or use ${availableTool} directly:${colors.reset} ${availableCmd}`);
|
|
299
|
+
console.log('');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
// Clear screen and show splash
|
|
303
|
+
console.clear();
|
|
304
|
+
// Top separator
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log(fullWidthLine('═'));
|
|
307
|
+
console.log('');
|
|
308
|
+
// Banner with title and connected tools on the right side
|
|
309
|
+
const bannerLines = AIC_BANNER.trim().split('\n');
|
|
310
|
+
const titleLines = [
|
|
311
|
+
`${colors.brightCyan}A${colors.brightMagenta}I${colors.reset} ${colors.brightYellow}C${colors.white}ode${colors.reset} ${colors.brightYellow}C${colors.white}onnect${colors.reset} ${colors.dim}${VERSION}${colors.reset}`,
|
|
312
|
+
'',
|
|
313
|
+
`${colors.dim}Connected Tools:${colors.reset}`,
|
|
314
|
+
claudeVersion
|
|
315
|
+
? `✅ ${colors.brightCyan}Claude Code${colors.reset} ${colors.dim}v${claudeVersion}${colors.reset}`
|
|
316
|
+
: `❌ ${colors.dim}Claude Code (not found)${colors.reset}`,
|
|
317
|
+
geminiVersion
|
|
318
|
+
? `✅ ${colors.brightMagenta}Gemini CLI${colors.reset} ${colors.dim}v${geminiVersion}${colors.reset}`
|
|
319
|
+
: `❌ ${colors.dim}Gemini CLI (not found)${colors.reset}`,
|
|
320
|
+
'',
|
|
321
|
+
`${colors.dim}📁 ${this.cwd}${colors.reset}`,
|
|
322
|
+
];
|
|
323
|
+
// Print banner and title side by side, centered
|
|
324
|
+
const maxLines = Math.max(bannerLines.length, titleLines.length);
|
|
325
|
+
const bannerWidth = 30; // Approximate width of banner
|
|
326
|
+
const gap = 10;
|
|
327
|
+
const maxTitleWidth = Math.max(...titleLines.map(l => stripAnsiLength(l)));
|
|
328
|
+
const totalContentWidth = bannerWidth + gap + maxTitleWidth;
|
|
329
|
+
const leftPadding = Math.max(2, Math.floor((width - totalContentWidth) / 2));
|
|
330
|
+
for (let i = 0; i < maxLines; i++) {
|
|
331
|
+
const bannerLine = bannerLines[i] || '';
|
|
332
|
+
const titleLine = titleLines[i] || '';
|
|
333
|
+
console.log(`${' '.repeat(leftPadding)}${bannerLine}${' '.repeat(Math.max(0, bannerWidth - stripAnsiLength(bannerLine) + gap))}${titleLine}`);
|
|
334
|
+
}
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(fullWidthLine('─'));
|
|
337
|
+
console.log('');
|
|
338
|
+
// Commands in a wider layout (single slash = AIC commands, double slash = tool commands via interactive mode)
|
|
339
|
+
const commandsLeft = [
|
|
340
|
+
` ${rainbowText('/claude')} Switch to Claude Code`,
|
|
341
|
+
` ${rainbowText('/gemini', 1)} Switch to Gemini CLI`,
|
|
342
|
+
` ${rainbowText('/i', 2)} Enter interactive mode`,
|
|
343
|
+
` ${rainbowText('/forward', 3)} Forward response ${colors.dim}[tool] [msg]${colors.reset}`,
|
|
344
|
+
];
|
|
345
|
+
const commandsRight = [
|
|
346
|
+
` ${rainbowText('/history', 4)} Show conversation`,
|
|
347
|
+
` ${rainbowText('/status', 5)} Show running processes`,
|
|
348
|
+
` ${rainbowText('/clear', 0)} Clear sessions`,
|
|
349
|
+
` ${rainbowText('/quit', 1)} Exit ${colors.dim}(or /cya)${colors.reset}`,
|
|
350
|
+
];
|
|
351
|
+
// Print commands side by side if terminal is wide enough
|
|
352
|
+
if (width >= 100) {
|
|
353
|
+
const colWidth = Math.floor(width / 2) - 5;
|
|
354
|
+
for (let i = 0; i < commandsLeft.length; i++) {
|
|
355
|
+
const left = commandsLeft[i] || '';
|
|
356
|
+
const right = commandsRight[i] || '';
|
|
357
|
+
const leftPadded = left + ' '.repeat(Math.max(0, colWidth - stripAnsiLength(left)));
|
|
358
|
+
console.log(`${leftPadded}${right}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Single column for narrow terminals
|
|
363
|
+
commandsLeft.forEach(cmd => console.log(cmd));
|
|
364
|
+
commandsRight.forEach(cmd => console.log(cmd));
|
|
365
|
+
}
|
|
366
|
+
console.log('');
|
|
367
|
+
// Tips section
|
|
368
|
+
console.log(` ${colors.dim}💡 ${colors.brightYellow}//command${colors.dim} opens interactive mode & sends the command. ${colors.white}Use ${colors.brightYellow}Ctrl+]${colors.white}, ${colors.brightYellow}Ctrl+\\${colors.white}, or ${colors.brightYellow}Esc Esc${colors.white} to return to aic²${colors.reset}`);
|
|
369
|
+
console.log(` ${colors.dim}💡 ${colors.brightYellow}Tab${colors.dim}: autocomplete ${colors.brightYellow}↑/↓${colors.dim}: history${colors.reset}`);
|
|
370
|
+
console.log('');
|
|
371
|
+
// Show active tool with full width separator
|
|
372
|
+
const toolColor = this.activeTool === 'claude' ? colors.brightCyan : colors.brightMagenta;
|
|
373
|
+
const toolName = this.activeTool === 'claude' ? 'Claude Code' : 'Gemini CLI';
|
|
374
|
+
console.log(fullWidthLine('═'));
|
|
375
|
+
console.log(` ${colors.green}●${colors.reset} Active: ${toolColor}${toolName}${colors.reset}`);
|
|
376
|
+
console.log(fullWidthLine('─'));
|
|
377
|
+
console.log('');
|
|
378
|
+
this.isRunning = true;
|
|
379
|
+
await this.runLoop();
|
|
380
|
+
}
|
|
381
|
+
getPrompt() {
|
|
382
|
+
const toolColor = this.activeTool === 'claude' ? colors.brightCyan : colors.brightMagenta;
|
|
383
|
+
const toolName = this.activeTool === 'claude' ? 'claude' : 'gemini';
|
|
384
|
+
return `${toolColor}❯ ${toolName}${colors.reset} ${colors.dim}→${colors.reset} `;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Tab completion for / commands
|
|
388
|
+
*/
|
|
389
|
+
completer(line) {
|
|
390
|
+
const commands = ['/claude', '/gemini', '/i', '/forward', '/fwd', '/history', '/status', '/default', '/help', '/clear', '/quit', '/cya'];
|
|
391
|
+
// Only complete if line starts with /
|
|
392
|
+
if (line.startsWith('/')) {
|
|
393
|
+
const hits = commands.filter(c => c.startsWith(line));
|
|
394
|
+
// Show all commands if no specific match, or show matches
|
|
395
|
+
return [hits.length ? hits : commands, line];
|
|
396
|
+
}
|
|
397
|
+
// No completion for regular input
|
|
398
|
+
return [[], line];
|
|
399
|
+
}
|
|
400
|
+
setupReadline() {
|
|
401
|
+
this.rl = createInterface({
|
|
402
|
+
input: process.stdin,
|
|
403
|
+
output: process.stdout,
|
|
404
|
+
completer: this.completer.bind(this),
|
|
405
|
+
history: this.inputHistory,
|
|
406
|
+
historySize: 100,
|
|
407
|
+
prompt: this.getPrompt(),
|
|
408
|
+
});
|
|
409
|
+
// Handle Ctrl+C gracefully
|
|
410
|
+
this.rl.on('SIGINT', () => {
|
|
411
|
+
console.log('\n');
|
|
412
|
+
this.rl?.close();
|
|
413
|
+
this.cleanup().then(() => {
|
|
414
|
+
console.log(`${colors.brightYellow}👋 Goodbye!${colors.reset}\n`);
|
|
415
|
+
process.exit(0);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async runLoop() {
|
|
420
|
+
this.setupReadline();
|
|
421
|
+
await this.promptLoop();
|
|
422
|
+
}
|
|
423
|
+
async promptLoop() {
|
|
424
|
+
while (this.isRunning) {
|
|
425
|
+
const input = await this.readInput();
|
|
426
|
+
if (!input || !input.trim())
|
|
427
|
+
continue;
|
|
428
|
+
const trimmed = input.trim();
|
|
429
|
+
// Add to history (readline handles this, but we track for persistence)
|
|
430
|
+
if (trimmed && !this.inputHistory.includes(trimmed)) {
|
|
431
|
+
this.inputHistory.push(trimmed);
|
|
432
|
+
// Keep history manageable
|
|
433
|
+
if (this.inputHistory.length > 100) {
|
|
434
|
+
this.inputHistory.shift();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Handle double slash - enter interactive mode and send the command
|
|
438
|
+
// e.g., //status -> enters interactive mode, sends /status, user stays in control
|
|
439
|
+
if (trimmed.startsWith('//')) {
|
|
440
|
+
const slashCmd = trimmed.slice(1); // e.g., "/status"
|
|
441
|
+
const toolName = this.activeTool === 'claude' ? 'Claude Code' : 'Gemini CLI';
|
|
442
|
+
// Show rainbow "Entering Interactive Mode" message
|
|
443
|
+
await animateRainbow(`Entering Interactive Mode for ${toolName}...`, 500);
|
|
444
|
+
process.stdout.write('\n');
|
|
445
|
+
await this.enterInteractiveModeWithCommand(slashCmd);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
// Handle AIC meta commands (single slash)
|
|
449
|
+
if (trimmed.startsWith('/')) {
|
|
450
|
+
// Readline already echoed the command - just process it, no extra output
|
|
451
|
+
await this.handleMetaCommand(trimmed.slice(1));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
// Send regular input to active tool
|
|
455
|
+
await this.sendToTool(trimmed);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
readInput() {
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
// Update prompt in case tool changed
|
|
461
|
+
this.rl?.setPrompt(this.getPrompt());
|
|
462
|
+
this.rl?.prompt();
|
|
463
|
+
const lineHandler = (line) => {
|
|
464
|
+
// Filter out terminal garbage that may have leaked into the input
|
|
465
|
+
let cleaned = line
|
|
466
|
+
.replace(/\x1b\[I/g, '')
|
|
467
|
+
.replace(/\x1b\[O/g, '')
|
|
468
|
+
.replace(/\^\[\[I/g, '')
|
|
469
|
+
.replace(/\^\[\[O/g, '')
|
|
470
|
+
.replace(/\x1b\[[\d;]*[a-zA-Z]/g, '');
|
|
471
|
+
// AGGRESSIVE STRIP: Remove Device Attribute response suffixes
|
|
472
|
+
// e.g. "/cya;1;2;...;52c" -> "/cya"
|
|
473
|
+
// Pattern: semicolon followed by numbers/semicolons ending in c at the end of string
|
|
474
|
+
cleaned = cleaned.replace(/;[\d;]+c$/, '');
|
|
475
|
+
// Also strip if it's just the garbage on its own line
|
|
476
|
+
cleaned = cleaned.replace(/^\d*u?[\d;]+c$/, '');
|
|
477
|
+
cleaned = cleaned.trim();
|
|
478
|
+
// If the line was ONLY garbage (and now empty), ignore it
|
|
479
|
+
if (line.length > 0 && cleaned.length === 0) {
|
|
480
|
+
this.rl?.prompt();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Valid input
|
|
484
|
+
this.rl?.removeListener('line', lineHandler);
|
|
485
|
+
resolve(cleaned);
|
|
486
|
+
};
|
|
487
|
+
this.rl?.on('line', lineHandler);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async handleMetaCommand(cmd) {
|
|
491
|
+
const parts = cmd.split(/\s+/);
|
|
492
|
+
const command = parts[0].toLowerCase();
|
|
493
|
+
switch (command) {
|
|
494
|
+
case 'quit':
|
|
495
|
+
case 'exit':
|
|
496
|
+
case 'cya':
|
|
497
|
+
await this.cleanup();
|
|
498
|
+
console.log(`\n${colors.brightYellow}👋 Goodbye!${colors.reset}\n`);
|
|
499
|
+
this.isRunning = false;
|
|
500
|
+
process.exit(0);
|
|
501
|
+
break;
|
|
502
|
+
case 'claude':
|
|
503
|
+
this.activeTool = 'claude';
|
|
504
|
+
console.log(`${colors.green}●${colors.reset} Switched to ${colors.brightCyan}Claude Code${colors.reset}`);
|
|
505
|
+
break;
|
|
506
|
+
case 'gemini':
|
|
507
|
+
this.activeTool = 'gemini';
|
|
508
|
+
console.log(`${colors.green}●${colors.reset} Switched to ${colors.brightMagenta}Gemini CLI${colors.reset}`);
|
|
509
|
+
break;
|
|
510
|
+
case 'forward':
|
|
511
|
+
case 'fwd':
|
|
512
|
+
await this.handleForward(parts.slice(1).join(' '));
|
|
513
|
+
break;
|
|
514
|
+
case 'interactive':
|
|
515
|
+
case 'shell':
|
|
516
|
+
case 'i':
|
|
517
|
+
await this.enterInteractiveMode();
|
|
518
|
+
break;
|
|
519
|
+
case 'history':
|
|
520
|
+
this.showHistory();
|
|
521
|
+
break;
|
|
522
|
+
case 'status':
|
|
523
|
+
this.showStatus();
|
|
524
|
+
break;
|
|
525
|
+
case 'clear':
|
|
526
|
+
await this.cleanup();
|
|
527
|
+
this.claudeHasSession = false;
|
|
528
|
+
this.geminiHasSession = false;
|
|
529
|
+
this.conversationHistory = [];
|
|
530
|
+
console.log('Sessions and history cleared.');
|
|
531
|
+
break;
|
|
532
|
+
case 'default':
|
|
533
|
+
const toolArg = parts[1];
|
|
534
|
+
if (toolArg) {
|
|
535
|
+
// Set new default
|
|
536
|
+
const result = setDefaultTool(toolArg);
|
|
537
|
+
if (result.success) {
|
|
538
|
+
console.log(`${colors.green}✓${colors.reset} ${result.message}`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
console.log(`${colors.red}✗${colors.reset} ${result.message}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
// Show current default
|
|
546
|
+
const currentDefault = getDefaultTool();
|
|
547
|
+
console.log(`${colors.dim}Current default tool:${colors.reset} ${colors.brightYellow}${currentDefault}${colors.reset}`);
|
|
548
|
+
console.log(`${colors.dim}Usage:${colors.reset} /default <claude|gemini>`);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
case 'help':
|
|
552
|
+
case '?':
|
|
553
|
+
this.showHelp();
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
console.log(`${colors.red}✗${colors.reset} Unknown AIC command: ${colors.brightYellow}/${command}${colors.reset}`);
|
|
557
|
+
console.log(`${colors.dim} Type ${colors.brightYellow}/help${colors.dim} to see available commands.${colors.reset}`);
|
|
558
|
+
console.log(`${colors.dim} To send /${command} to the tool, use ${colors.brightYellow}//${command}${colors.reset}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
showHelp() {
|
|
562
|
+
console.log('');
|
|
563
|
+
console.log(`${colors.brightCyan}A${colors.brightMagenta}I${colors.reset} ${colors.brightYellow}C${colors.white}ode${colors.reset} ${colors.brightYellow}C${colors.white}onnect${colors.reset}² ${colors.dim}- Commands${colors.reset}`);
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log(`${colors.white}Session Commands:${colors.reset}`);
|
|
566
|
+
console.log(` ${rainbowText('/claude')} Switch to Claude Code`);
|
|
567
|
+
console.log(` ${rainbowText('/gemini')} Switch to Gemini CLI`);
|
|
568
|
+
console.log(` ${rainbowText('/i')} Enter interactive mode ${colors.dim}(Ctrl+] or Ctrl+\\ to detach)${colors.reset}`);
|
|
569
|
+
console.log(` ${rainbowText('/forward')} Forward last response ${colors.dim}[tool] [msg]${colors.reset}`);
|
|
570
|
+
console.log(` ${rainbowText('/history')} Show conversation history`);
|
|
571
|
+
console.log(` ${rainbowText('/status')} Show running processes`);
|
|
572
|
+
console.log(` ${rainbowText('/default')} Set default tool ${colors.dim}<claude|gemini>${colors.reset}`);
|
|
573
|
+
console.log(` ${rainbowText('/clear')} Clear sessions and history`);
|
|
574
|
+
console.log(` ${rainbowText('/help')} Show this help`);
|
|
575
|
+
console.log(` ${rainbowText('/quit')} Exit ${colors.dim}(or /cya)${colors.reset}`);
|
|
576
|
+
console.log('');
|
|
577
|
+
console.log(`${colors.white}Tool Commands:${colors.reset}`);
|
|
578
|
+
console.log(` ${colors.brightYellow}//command${colors.reset} Send /command to the active tool`);
|
|
579
|
+
console.log(` ${colors.dim} Opens interactive mode, sends command, Ctrl+] or Ctrl+\\ to return${colors.reset}`);
|
|
580
|
+
console.log('');
|
|
581
|
+
console.log(`${colors.white}Tips:${colors.reset}`);
|
|
582
|
+
console.log(` ${colors.dim}•${colors.reset} ${colors.brightYellow}Tab${colors.reset} Autocomplete commands`);
|
|
583
|
+
console.log(` ${colors.dim}•${colors.reset} ${colors.brightYellow}↑/↓${colors.reset} Navigate history`);
|
|
584
|
+
console.log(` ${colors.dim}•${colors.reset} ${colors.brightYellow}Ctrl+]${colors.reset}, ${colors.brightYellow}Ctrl+\\${colors.reset}, ${colors.brightYellow}Ctrl+^${colors.reset}, or ${colors.brightYellow}Ctrl+_${colors.reset} Detach from interactive mode`);
|
|
585
|
+
console.log(` ${colors.dim}•${colors.reset} ${colors.brightYellow}Esc Esc${colors.reset} Detach (press Escape twice quickly)`);
|
|
586
|
+
console.log('');
|
|
587
|
+
}
|
|
588
|
+
async sendToTool(message) {
|
|
589
|
+
// Record user message
|
|
590
|
+
this.conversationHistory.push({
|
|
591
|
+
tool: this.activeTool,
|
|
592
|
+
role: 'user',
|
|
593
|
+
content: message,
|
|
594
|
+
});
|
|
595
|
+
try {
|
|
596
|
+
let response;
|
|
597
|
+
if (this.activeTool === 'claude') {
|
|
598
|
+
response = await this.sendToClaude(message);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
response = await this.sendToGemini(message);
|
|
602
|
+
}
|
|
603
|
+
// Record assistant response
|
|
604
|
+
this.conversationHistory.push({
|
|
605
|
+
tool: this.activeTool,
|
|
606
|
+
role: 'assistant',
|
|
607
|
+
content: response,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
console.error(`\nError: ${error instanceof Error ? error.message : error}\n`);
|
|
612
|
+
// Remove the user message if failed
|
|
613
|
+
this.conversationHistory.pop();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
sendToClaude(message) {
|
|
617
|
+
return new Promise((resolve, reject) => {
|
|
618
|
+
const args = ['-p']; // Print mode for regular messages
|
|
619
|
+
// Continue session if we have one
|
|
620
|
+
if (this.claudeHasSession) {
|
|
621
|
+
args.push('--continue');
|
|
622
|
+
}
|
|
623
|
+
// Add the message
|
|
624
|
+
args.push(message);
|
|
625
|
+
// Start spinner
|
|
626
|
+
const spinner = new Spinner(`${colors.brightCyan}Claude${colors.reset} is thinking`);
|
|
627
|
+
spinner.start();
|
|
628
|
+
const proc = spawn('claude', args, {
|
|
629
|
+
cwd: this.cwd,
|
|
630
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
631
|
+
env: process.env,
|
|
632
|
+
});
|
|
633
|
+
let stdout = '';
|
|
634
|
+
let stderr = '';
|
|
635
|
+
proc.stdout.on('data', (data) => {
|
|
636
|
+
stdout += data.toString();
|
|
637
|
+
});
|
|
638
|
+
proc.stderr.on('data', (data) => {
|
|
639
|
+
stderr += data.toString();
|
|
640
|
+
});
|
|
641
|
+
proc.on('close', (code) => {
|
|
642
|
+
spinner.stop();
|
|
643
|
+
if (code !== 0) {
|
|
644
|
+
reject(new Error(`Claude exited with code ${code}: ${stderr || stdout}`));
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Render the response with markdown formatting
|
|
648
|
+
console.log('');
|
|
649
|
+
const rendered = marked.parse(stdout.trim());
|
|
650
|
+
process.stdout.write(rendered);
|
|
651
|
+
console.log('');
|
|
652
|
+
this.claudeHasSession = true;
|
|
653
|
+
resolve(stripAnsi(stdout).trim());
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
proc.on('error', (err) => {
|
|
657
|
+
spinner.stop();
|
|
658
|
+
reject(err);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
sendToGemini(message) {
|
|
663
|
+
return new Promise((resolve, reject) => {
|
|
664
|
+
const args = [];
|
|
665
|
+
// Resume session if we have one
|
|
666
|
+
if (this.geminiHasSession) {
|
|
667
|
+
args.push('--resume', 'latest');
|
|
668
|
+
}
|
|
669
|
+
// Add the message
|
|
670
|
+
args.push(message);
|
|
671
|
+
// Start spinner
|
|
672
|
+
const spinner = new Spinner(`${colors.brightMagenta}Gemini${colors.reset} is thinking`);
|
|
673
|
+
spinner.start();
|
|
674
|
+
const proc = spawn('gemini', args, {
|
|
675
|
+
cwd: this.cwd,
|
|
676
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
677
|
+
env: process.env,
|
|
678
|
+
});
|
|
679
|
+
let stdout = '';
|
|
680
|
+
let stderr = '';
|
|
681
|
+
proc.stdout.on('data', (data) => {
|
|
682
|
+
stdout += data.toString();
|
|
683
|
+
});
|
|
684
|
+
proc.stderr.on('data', (data) => {
|
|
685
|
+
stderr += data.toString();
|
|
686
|
+
});
|
|
687
|
+
proc.on('close', (code) => {
|
|
688
|
+
spinner.stop();
|
|
689
|
+
if (code !== 0) {
|
|
690
|
+
reject(new Error(`Gemini exited with code ${code}: ${stderr || stdout}`));
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
// Render the response with markdown formatting
|
|
694
|
+
console.log('');
|
|
695
|
+
const rendered = marked.parse(stdout.trim());
|
|
696
|
+
process.stdout.write(rendered);
|
|
697
|
+
console.log('');
|
|
698
|
+
this.geminiHasSession = true;
|
|
699
|
+
resolve(stripAnsi(stdout).trim());
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
proc.on('error', (err) => {
|
|
703
|
+
spinner.stop();
|
|
704
|
+
reject(err);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Enter full interactive mode with the active tool.
|
|
710
|
+
* - If a process is already running, re-attach to it
|
|
711
|
+
* - If not, spawn a new one
|
|
712
|
+
* - Press Ctrl+] to detach (process keeps running)
|
|
713
|
+
* - Use /exit in the tool to terminate the process
|
|
714
|
+
*/
|
|
715
|
+
async enterInteractiveMode() {
|
|
716
|
+
const toolName = this.activeTool === 'claude' ? 'Claude Code' : 'Gemini CLI';
|
|
717
|
+
const toolColor = this.activeTool === 'claude' ? colors.brightCyan : colors.brightMagenta;
|
|
718
|
+
const command = this.activeTool;
|
|
719
|
+
// Check if we already have a running process
|
|
720
|
+
let ptyProcess = this.runningProcesses.get(this.activeTool);
|
|
721
|
+
const isReattach = ptyProcess !== undefined;
|
|
722
|
+
// Pause readline to prevent interference with raw input
|
|
723
|
+
this.rl?.pause();
|
|
724
|
+
if (isReattach) {
|
|
725
|
+
console.log(`\n${colors.green}↩${colors.reset} Re-attaching to ${toolColor}${toolName}${colors.reset}...`);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
console.log(`\n${colors.green}▶${colors.reset} Starting ${toolColor}${toolName}${colors.reset} interactive mode...`);
|
|
729
|
+
}
|
|
730
|
+
console.log(`${colors.dim}Press ${colors.brightYellow}Ctrl+]${colors.dim} or ${colors.brightYellow}Ctrl+\\${colors.dim} to detach • ${colors.white}/exit${colors.dim} to terminate${colors.reset}\n`);
|
|
731
|
+
// Clear the output buffer for fresh capture
|
|
732
|
+
this.interactiveOutputBuffer.set(this.activeTool, '');
|
|
733
|
+
// Interactive mode takes over stdin
|
|
734
|
+
return new Promise((resolve) => {
|
|
735
|
+
// Spawn new process if needed
|
|
736
|
+
if (!ptyProcess) {
|
|
737
|
+
const args = [];
|
|
738
|
+
// Continue/resume session if we have history from print mode
|
|
739
|
+
if (this.activeTool === 'claude' && this.claudeHasSession) {
|
|
740
|
+
args.push('--continue');
|
|
741
|
+
}
|
|
742
|
+
else if (this.activeTool === 'gemini' && this.geminiHasSession) {
|
|
743
|
+
args.push('--resume', 'latest');
|
|
744
|
+
}
|
|
745
|
+
ptyProcess = pty.spawn(command, args, {
|
|
746
|
+
name: 'xterm-256color',
|
|
747
|
+
cols: process.stdout.columns || 80,
|
|
748
|
+
rows: process.stdout.rows || 24,
|
|
749
|
+
cwd: this.cwd,
|
|
750
|
+
env: process.env,
|
|
751
|
+
});
|
|
752
|
+
// Store the process
|
|
753
|
+
this.runningProcesses.set(this.activeTool, ptyProcess);
|
|
754
|
+
// Handle process exit (user typed /exit in the tool)
|
|
755
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
756
|
+
console.log(`\n${colors.dim}${toolName} exited (code ${exitCode})${colors.reset}`);
|
|
757
|
+
this.runningProcesses.delete(this.activeTool);
|
|
758
|
+
// Mark session as having history
|
|
759
|
+
if (this.activeTool === 'claude') {
|
|
760
|
+
this.claudeHasSession = true;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
this.geminiHasSession = true;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
// Handle resize
|
|
768
|
+
const onResize = () => {
|
|
769
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
770
|
+
};
|
|
771
|
+
process.stdout.on('resize', onResize);
|
|
772
|
+
// Pipe PTY output to terminal AND capture for forwarding
|
|
773
|
+
const outputDisposable = ptyProcess.onData((data) => {
|
|
774
|
+
// Filter out terminal response sequences (DA responses, etc.)
|
|
775
|
+
// These can cause garbage like "0u64;1;2;4;6;..." to appear
|
|
776
|
+
let filteredData = data;
|
|
777
|
+
// Filter focus sequences from output too
|
|
778
|
+
filteredData = filteredData.split(FOCUS_IN_SEQ).join('').split(FOCUS_OUT_SEQ).join('');
|
|
779
|
+
if (filteredData.length > 0) {
|
|
780
|
+
process.stdout.write(filteredData);
|
|
781
|
+
}
|
|
782
|
+
// Capture output for potential forwarding
|
|
783
|
+
const current = this.interactiveOutputBuffer.get(this.activeTool) || '';
|
|
784
|
+
this.interactiveOutputBuffer.set(this.activeTool, current + data);
|
|
785
|
+
});
|
|
786
|
+
// Set up stdin forwarding with detach key detection
|
|
787
|
+
if (process.stdin.isTTY) {
|
|
788
|
+
process.stdin.setRawMode(true);
|
|
789
|
+
}
|
|
790
|
+
process.stdin.resume();
|
|
791
|
+
let detached = false;
|
|
792
|
+
let lastEscapeTime = 0;
|
|
793
|
+
const performDetach = () => {
|
|
794
|
+
if (detached)
|
|
795
|
+
return;
|
|
796
|
+
detached = true;
|
|
797
|
+
cleanup();
|
|
798
|
+
// Save captured output to conversation history for forwarding
|
|
799
|
+
const capturedOutput = this.interactiveOutputBuffer.get(this.activeTool);
|
|
800
|
+
if (capturedOutput) {
|
|
801
|
+
const cleanedOutput = stripAnsi(capturedOutput).trim();
|
|
802
|
+
if (cleanedOutput.length > 50) { // Only save meaningful output
|
|
803
|
+
this.conversationHistory.push({
|
|
804
|
+
tool: this.activeTool,
|
|
805
|
+
role: 'assistant',
|
|
806
|
+
content: cleanedOutput,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
// Clear buffer after saving
|
|
810
|
+
this.interactiveOutputBuffer.set(this.activeTool, '');
|
|
811
|
+
}
|
|
812
|
+
// Clear any pending terminal responses before showing detach message
|
|
813
|
+
process.stdout.write('\x1b[2K\r'); // Clear current line
|
|
814
|
+
console.log(`\n\n${colors.yellow}⏸${colors.reset} Detached from ${toolColor}${toolName}${colors.reset} ${colors.dim}(still running)${colors.reset}`);
|
|
815
|
+
console.log(`${colors.dim}Use ${colors.brightYellow}/i${colors.dim} to re-attach • ${colors.brightGreen}/forward${colors.dim} to send to other tool${colors.reset}`);
|
|
816
|
+
console.log(`${colors.dim}Press ${colors.brightYellow}Enter${colors.dim} to continue${colors.reset}\n`);
|
|
817
|
+
resolve();
|
|
818
|
+
};
|
|
819
|
+
// Debug mode - set AIC_DEBUG=1 to see key codes
|
|
820
|
+
const debugKeys = process.env.AIC_DEBUG === '1';
|
|
821
|
+
const onStdinData = (data) => {
|
|
822
|
+
let str = data.toString();
|
|
823
|
+
// Debug output to see what keys are being received
|
|
824
|
+
if (debugKeys) {
|
|
825
|
+
const hexBytes = Array.from(data).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ');
|
|
826
|
+
console.log(`\n[DEBUG] Received ${data.length} bytes: ${hexBytes}`);
|
|
827
|
+
}
|
|
828
|
+
// Filter out terminal focus reporting sequences (sent when window gains/loses focus)
|
|
829
|
+
// These cause ^[[I and ^[[O to appear in the terminal
|
|
830
|
+
if (str === FOCUS_IN_SEQ || str === FOCUS_OUT_SEQ) {
|
|
831
|
+
if (debugKeys)
|
|
832
|
+
console.log('[DEBUG] Filtered focus event');
|
|
833
|
+
return; // Don't forward to PTY
|
|
834
|
+
}
|
|
835
|
+
// Also filter if focus sequences are embedded in the data
|
|
836
|
+
str = str.split(FOCUS_IN_SEQ).join('').split(FOCUS_OUT_SEQ).join('');
|
|
837
|
+
if (str.length === 0) {
|
|
838
|
+
return; // Nothing left after filtering
|
|
839
|
+
}
|
|
840
|
+
// Check for CSI u sequences (modern keyboard protocol used by iTerm2)
|
|
841
|
+
for (const seq of CSI_U_DETACH_SEQS) {
|
|
842
|
+
if (str === seq || str.includes(seq)) {
|
|
843
|
+
if (debugKeys)
|
|
844
|
+
console.log(`[DEBUG] Detected CSI u detach sequence: ${seq.replace('\x1b', 'ESC')}`);
|
|
845
|
+
performDetach();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Check for detach keys by examining raw bytes (traditional terminals)
|
|
850
|
+
// This is more reliable than string comparison for control characters
|
|
851
|
+
for (let i = 0; i < data.length; i++) {
|
|
852
|
+
const byte = data[i];
|
|
853
|
+
// Ctrl+] (0x1D = 29) - primary detach key
|
|
854
|
+
if (byte === DETACH_KEYS.CTRL_BRACKET) {
|
|
855
|
+
if (debugKeys)
|
|
856
|
+
console.log('[DEBUG] Detected Ctrl+]');
|
|
857
|
+
performDetach();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Ctrl+\ (0x1C = 28) - alternative detach key
|
|
861
|
+
if (byte === DETACH_KEYS.CTRL_BACKSLASH) {
|
|
862
|
+
if (debugKeys)
|
|
863
|
+
console.log('[DEBUG] Detected Ctrl+\\');
|
|
864
|
+
performDetach();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
// Ctrl+^ (0x1E = 30) - another alternative (Ctrl+Shift+6)
|
|
868
|
+
if (byte === DETACH_KEYS.CTRL_CARET) {
|
|
869
|
+
if (debugKeys)
|
|
870
|
+
console.log('[DEBUG] Detected Ctrl+^');
|
|
871
|
+
performDetach();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
// Ctrl+_ (0x1F = 31) - another alternative (Ctrl+Shift+-)
|
|
875
|
+
if (byte === DETACH_KEYS.CTRL_UNDERSCORE) {
|
|
876
|
+
if (debugKeys)
|
|
877
|
+
console.log('[DEBUG] Detected Ctrl+_');
|
|
878
|
+
performDetach();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Handle escape sequences for double-escape detection
|
|
883
|
+
// Check for immediate double escape (0x1B 0x1B) anywhere in buffer
|
|
884
|
+
if (data.length >= 2) {
|
|
885
|
+
for (let i = 0; i < data.length - 1; i++) {
|
|
886
|
+
if (data[i] === DETACH_KEYS.ESCAPE && data[i + 1] === DETACH_KEYS.ESCAPE) {
|
|
887
|
+
if (debugKeys)
|
|
888
|
+
console.log('[DEBUG] Detected double-Escape');
|
|
889
|
+
performDetach();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// Single Escape (0x1B) - track for double-escape detection
|
|
895
|
+
const isSingleEscape = data.length === 1 && data[0] === DETACH_KEYS.ESCAPE;
|
|
896
|
+
if (isSingleEscape) {
|
|
897
|
+
const now = Date.now();
|
|
898
|
+
if (now - lastEscapeTime < 500) {
|
|
899
|
+
// Double escape detected - detach!
|
|
900
|
+
if (debugKeys)
|
|
901
|
+
console.log('[DEBUG] Detected double-Escape (timed)');
|
|
902
|
+
performDetach();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
lastEscapeTime = now;
|
|
906
|
+
// Still forward the escape to the PTY
|
|
907
|
+
ptyProcess.write(str);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
// Reset escape timer if not an escape key
|
|
911
|
+
if (!isSingleEscape) {
|
|
912
|
+
lastEscapeTime = 0;
|
|
913
|
+
}
|
|
914
|
+
// Forward filtered data to PTY
|
|
915
|
+
ptyProcess.write(str);
|
|
916
|
+
};
|
|
917
|
+
process.stdin.on('data', onStdinData);
|
|
918
|
+
// Handle process exit while attached
|
|
919
|
+
const exitHandler = () => {
|
|
920
|
+
if (!detached) {
|
|
921
|
+
cleanup();
|
|
922
|
+
console.log(`\n${colors.dim}Returned to ${colors.brightYellow}aic${colors.reset}\n`);
|
|
923
|
+
resolve();
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
ptyProcess.onExit(exitHandler);
|
|
927
|
+
// Cleanup function
|
|
928
|
+
let cleanedUp = false;
|
|
929
|
+
const cleanup = () => {
|
|
930
|
+
if (cleanedUp)
|
|
931
|
+
return;
|
|
932
|
+
cleanedUp = true;
|
|
933
|
+
process.stdin.removeListener('data', onStdinData);
|
|
934
|
+
process.stdout.removeListener('resize', onResize);
|
|
935
|
+
outputDisposable.dispose();
|
|
936
|
+
// Clear the current line
|
|
937
|
+
process.stdout.write('\x1b[2K\r');
|
|
938
|
+
// CRITICAL FIX: Explicitly disable terminal features that cause garbage
|
|
939
|
+
process.stdout.write('\x1b[?1004l'); // Disable focus reporting (stops ^[[I / ^[[O)
|
|
940
|
+
process.stdout.write('\x1b[?2004l'); // Disable bracketed paste
|
|
941
|
+
process.stdout.write('\x1b[>0u'); // Reset keyboard enhancement to legacy mode (CSI u)
|
|
942
|
+
process.stdout.write('\x1b[?25h'); // Ensure cursor is visible
|
|
943
|
+
if (process.stdin.isTTY) {
|
|
944
|
+
process.stdin.setRawMode(false);
|
|
945
|
+
}
|
|
946
|
+
// Recreate readline to ensure clean state after PTY interaction
|
|
947
|
+
this.rl?.close();
|
|
948
|
+
this.setupReadline();
|
|
949
|
+
// Clear any garbage after a brief delay
|
|
950
|
+
setTimeout(() => {
|
|
951
|
+
process.stdout.write('\x1b[2K\r');
|
|
952
|
+
}, 100);
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Enter interactive mode and automatically send a slash command
|
|
958
|
+
* User stays in interactive mode to see output and interact, then Ctrl+] to return
|
|
959
|
+
*/
|
|
960
|
+
async enterInteractiveModeWithCommand(command) {
|
|
961
|
+
const toolName = this.activeTool === 'claude' ? 'Claude Code' : 'Gemini CLI';
|
|
962
|
+
const toolColor = this.activeTool === 'claude' ? colors.brightCyan : colors.brightMagenta;
|
|
963
|
+
const toolCmd = this.activeTool;
|
|
964
|
+
// Check if we already have a running process
|
|
965
|
+
let ptyProcess = this.runningProcesses.get(this.activeTool);
|
|
966
|
+
const isReattach = ptyProcess !== undefined;
|
|
967
|
+
// Pause readline to prevent interference with raw input
|
|
968
|
+
this.rl?.pause();
|
|
969
|
+
console.log(`${colors.dim}Sending ${colors.brightYellow}${command}${colors.dim}... Press ${colors.brightYellow}Ctrl+]${colors.dim} or ${colors.brightYellow}Ctrl+\\${colors.dim} to return${colors.reset}\n`);
|
|
970
|
+
// Clear the output buffer for fresh capture
|
|
971
|
+
this.interactiveOutputBuffer.set(this.activeTool, '');
|
|
972
|
+
return new Promise((resolve) => {
|
|
973
|
+
// Spawn new process if needed
|
|
974
|
+
if (!ptyProcess) {
|
|
975
|
+
const args = [];
|
|
976
|
+
// Continue/resume session if we have history from print mode
|
|
977
|
+
if (this.activeTool === 'claude' && this.claudeHasSession) {
|
|
978
|
+
args.push('--continue');
|
|
979
|
+
}
|
|
980
|
+
else if (this.activeTool === 'gemini' && this.geminiHasSession) {
|
|
981
|
+
args.push('--resume', 'latest');
|
|
982
|
+
}
|
|
983
|
+
ptyProcess = pty.spawn(toolCmd, args, {
|
|
984
|
+
name: 'xterm-256color',
|
|
985
|
+
cols: process.stdout.columns || 80,
|
|
986
|
+
rows: process.stdout.rows || 24,
|
|
987
|
+
cwd: this.cwd,
|
|
988
|
+
env: process.env,
|
|
989
|
+
});
|
|
990
|
+
// Store the process
|
|
991
|
+
this.runningProcesses.set(this.activeTool, ptyProcess);
|
|
992
|
+
// Handle process exit (user typed /exit in the tool)
|
|
993
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
994
|
+
console.log(`\n${colors.dim}${toolName} exited (code ${exitCode})${colors.reset}`);
|
|
995
|
+
this.runningProcesses.delete(this.activeTool);
|
|
996
|
+
// Mark session as having history
|
|
997
|
+
if (this.activeTool === 'claude') {
|
|
998
|
+
this.claudeHasSession = true;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
this.geminiHasSession = true;
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
// Track if we've sent the command
|
|
1006
|
+
let commandSent = false;
|
|
1007
|
+
// Handle resize
|
|
1008
|
+
const onResize = () => {
|
|
1009
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
1010
|
+
};
|
|
1011
|
+
process.stdout.on('resize', onResize);
|
|
1012
|
+
// Function to send the command
|
|
1013
|
+
const sendCommand = () => {
|
|
1014
|
+
if (commandSent)
|
|
1015
|
+
return;
|
|
1016
|
+
commandSent = true;
|
|
1017
|
+
// Type command character by character for reliability
|
|
1018
|
+
let i = 0;
|
|
1019
|
+
const fullCommand = command + '\r';
|
|
1020
|
+
const typeNextChar = () => {
|
|
1021
|
+
if (i < fullCommand.length) {
|
|
1022
|
+
ptyProcess.write(fullCommand[i]);
|
|
1023
|
+
i++;
|
|
1024
|
+
setTimeout(typeNextChar, 20);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
typeNextChar();
|
|
1028
|
+
};
|
|
1029
|
+
// Pipe PTY output to terminal AND capture for forwarding
|
|
1030
|
+
const outputDisposable = ptyProcess.onData((data) => {
|
|
1031
|
+
// Filter out terminal response sequences (DA responses, etc.)
|
|
1032
|
+
// These can cause garbage like "0u64;1;2;4;6;..." to appear
|
|
1033
|
+
let filteredData = data;
|
|
1034
|
+
// Filter focus sequences from output too
|
|
1035
|
+
filteredData = filteredData.split(FOCUS_IN_SEQ).join('').split(FOCUS_OUT_SEQ).join('');
|
|
1036
|
+
if (filteredData.length > 0) {
|
|
1037
|
+
process.stdout.write(filteredData);
|
|
1038
|
+
}
|
|
1039
|
+
// Capture output for potential forwarding
|
|
1040
|
+
const current = this.interactiveOutputBuffer.get(this.activeTool) || '';
|
|
1041
|
+
this.interactiveOutputBuffer.set(this.activeTool, current + data);
|
|
1042
|
+
});
|
|
1043
|
+
// For reattach, send command quickly. For new process, wait for it to initialize.
|
|
1044
|
+
const sendDelay = isReattach ? 100 : 2500;
|
|
1045
|
+
const fallbackTimer = setTimeout(() => {
|
|
1046
|
+
if (!commandSent) {
|
|
1047
|
+
sendCommand();
|
|
1048
|
+
}
|
|
1049
|
+
}, sendDelay);
|
|
1050
|
+
// Set up stdin forwarding with detach key detection
|
|
1051
|
+
if (process.stdin.isTTY) {
|
|
1052
|
+
process.stdin.setRawMode(true);
|
|
1053
|
+
}
|
|
1054
|
+
process.stdin.resume();
|
|
1055
|
+
let detached = false;
|
|
1056
|
+
let lastEscapeTime = 0;
|
|
1057
|
+
// Debug mode - set AIC_DEBUG=1 to see key codes
|
|
1058
|
+
const debugKeys = process.env.AIC_DEBUG === '1';
|
|
1059
|
+
const performDetach = () => {
|
|
1060
|
+
if (detached)
|
|
1061
|
+
return;
|
|
1062
|
+
detached = true;
|
|
1063
|
+
cleanup();
|
|
1064
|
+
// Save captured output to conversation history for forwarding
|
|
1065
|
+
const capturedOutput = this.interactiveOutputBuffer.get(this.activeTool);
|
|
1066
|
+
if (capturedOutput) {
|
|
1067
|
+
const cleanedOutput = stripAnsi(capturedOutput).trim();
|
|
1068
|
+
if (cleanedOutput.length > 50) { // Only save meaningful output
|
|
1069
|
+
this.conversationHistory.push({
|
|
1070
|
+
tool: this.activeTool,
|
|
1071
|
+
role: 'assistant',
|
|
1072
|
+
content: cleanedOutput,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
// Clear buffer after saving
|
|
1076
|
+
this.interactiveOutputBuffer.set(this.activeTool, '');
|
|
1077
|
+
}
|
|
1078
|
+
// Clear any pending terminal responses before showing detach message
|
|
1079
|
+
process.stdout.write('\x1b[2K\r'); // Clear current line
|
|
1080
|
+
console.log(`\n\n${colors.yellow}⏸${colors.reset} Detached from ${toolColor}${toolName}${colors.reset} ${colors.dim}(still running)${colors.reset}`);
|
|
1081
|
+
console.log(`${colors.dim}Use ${colors.brightYellow}/i${colors.dim} to re-attach • ${colors.brightGreen}/forward${colors.dim} to send to other tool${colors.reset}`);
|
|
1082
|
+
console.log(`${colors.dim}Press ${colors.brightYellow}Enter${colors.dim} to continue${colors.reset}\n`);
|
|
1083
|
+
resolve();
|
|
1084
|
+
};
|
|
1085
|
+
const onStdinData = (data) => {
|
|
1086
|
+
let str = data.toString();
|
|
1087
|
+
// Debug output to see what keys are being received
|
|
1088
|
+
if (debugKeys) {
|
|
1089
|
+
const hexBytes = Array.from(data).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ');
|
|
1090
|
+
console.log(`\n[DEBUG] Received ${data.length} bytes: ${hexBytes}`);
|
|
1091
|
+
}
|
|
1092
|
+
// Filter out terminal focus reporting sequences (sent when window gains/loses focus)
|
|
1093
|
+
// These cause ^[[I and ^[[O to appear in the terminal
|
|
1094
|
+
if (str === FOCUS_IN_SEQ || str === FOCUS_OUT_SEQ) {
|
|
1095
|
+
if (debugKeys)
|
|
1096
|
+
console.log('[DEBUG] Filtered focus event');
|
|
1097
|
+
return; // Don't forward to PTY
|
|
1098
|
+
}
|
|
1099
|
+
// Also filter if focus sequences are embedded in the data
|
|
1100
|
+
str = str.split(FOCUS_IN_SEQ).join('').split(FOCUS_OUT_SEQ).join('');
|
|
1101
|
+
if (str.length === 0) {
|
|
1102
|
+
return; // Nothing left after filtering
|
|
1103
|
+
}
|
|
1104
|
+
// Check for CSI u sequences (modern keyboard protocol used by iTerm2)
|
|
1105
|
+
for (const seq of CSI_U_DETACH_SEQS) {
|
|
1106
|
+
if (str === seq || str.includes(seq)) {
|
|
1107
|
+
if (debugKeys)
|
|
1108
|
+
console.log(`[DEBUG] Detected CSI u detach sequence: ${seq.replace('\x1b', 'ESC')}`);
|
|
1109
|
+
performDetach();
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
// Check for detach keys by examining raw bytes (traditional terminals)
|
|
1114
|
+
// This is more reliable than string comparison for control characters
|
|
1115
|
+
for (let i = 0; i < data.length; i++) {
|
|
1116
|
+
const byte = data[i];
|
|
1117
|
+
// Ctrl+] (0x1D = 29) - primary detach key
|
|
1118
|
+
if (byte === DETACH_KEYS.CTRL_BRACKET) {
|
|
1119
|
+
if (debugKeys)
|
|
1120
|
+
console.log('[DEBUG] Detected Ctrl+]');
|
|
1121
|
+
performDetach();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
// Ctrl+\ (0x1C = 28) - alternative detach key
|
|
1125
|
+
if (byte === DETACH_KEYS.CTRL_BACKSLASH) {
|
|
1126
|
+
if (debugKeys)
|
|
1127
|
+
console.log('[DEBUG] Detected Ctrl+\\');
|
|
1128
|
+
performDetach();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
// Ctrl+^ (0x1E = 30) - another alternative (Ctrl+Shift+6)
|
|
1132
|
+
if (byte === DETACH_KEYS.CTRL_CARET) {
|
|
1133
|
+
if (debugKeys)
|
|
1134
|
+
console.log('[DEBUG] Detected Ctrl+^');
|
|
1135
|
+
performDetach();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
// Ctrl+_ (0x1F = 31) - another alternative (Ctrl+Shift+-)
|
|
1139
|
+
if (byte === DETACH_KEYS.CTRL_UNDERSCORE) {
|
|
1140
|
+
if (debugKeys)
|
|
1141
|
+
console.log('[DEBUG] Detected Ctrl+_');
|
|
1142
|
+
performDetach();
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// Handle escape sequences for double-escape detection
|
|
1147
|
+
// Check for immediate double escape (0x1B 0x1B) anywhere in buffer
|
|
1148
|
+
if (data.length >= 2) {
|
|
1149
|
+
for (let i = 0; i < data.length - 1; i++) {
|
|
1150
|
+
if (data[i] === DETACH_KEYS.ESCAPE && data[i + 1] === DETACH_KEYS.ESCAPE) {
|
|
1151
|
+
if (debugKeys)
|
|
1152
|
+
console.log('[DEBUG] Detected double-Escape');
|
|
1153
|
+
performDetach();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// Single Escape (0x1B) - track for double-escape detection
|
|
1159
|
+
const isSingleEscape = data.length === 1 && data[0] === DETACH_KEYS.ESCAPE;
|
|
1160
|
+
if (isSingleEscape) {
|
|
1161
|
+
const now = Date.now();
|
|
1162
|
+
if (now - lastEscapeTime < 500) {
|
|
1163
|
+
// Double escape detected - detach!
|
|
1164
|
+
if (debugKeys)
|
|
1165
|
+
console.log('[DEBUG] Detected double-Escape (timed)');
|
|
1166
|
+
performDetach();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
lastEscapeTime = now;
|
|
1170
|
+
// Still forward the escape to the PTY
|
|
1171
|
+
ptyProcess.write(str);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
// Reset escape timer if not an escape key
|
|
1175
|
+
if (!isSingleEscape) {
|
|
1176
|
+
lastEscapeTime = 0;
|
|
1177
|
+
}
|
|
1178
|
+
// Forward filtered data to PTY
|
|
1179
|
+
ptyProcess.write(str);
|
|
1180
|
+
};
|
|
1181
|
+
process.stdin.on('data', onStdinData);
|
|
1182
|
+
// Handle process exit while attached
|
|
1183
|
+
const exitHandler = () => {
|
|
1184
|
+
if (!detached) {
|
|
1185
|
+
cleanup();
|
|
1186
|
+
console.log(`\n${colors.dim}Returned to ${colors.brightYellow}aic${colors.reset}\n`);
|
|
1187
|
+
resolve();
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
ptyProcess.onExit(exitHandler);
|
|
1191
|
+
// Cleanup function
|
|
1192
|
+
let cleanedUp = false;
|
|
1193
|
+
const cleanup = () => {
|
|
1194
|
+
if (cleanedUp)
|
|
1195
|
+
return;
|
|
1196
|
+
cleanedUp = true;
|
|
1197
|
+
clearTimeout(fallbackTimer);
|
|
1198
|
+
process.stdin.removeListener('data', onStdinData);
|
|
1199
|
+
process.stdout.removeListener('resize', onResize);
|
|
1200
|
+
outputDisposable.dispose();
|
|
1201
|
+
// Clear the current line
|
|
1202
|
+
process.stdout.write('\x1b[2K\r');
|
|
1203
|
+
// Explicitly disable terminal features that cause garbage
|
|
1204
|
+
process.stdout.write('\x1b[?1004l'); // Disable focus reporting
|
|
1205
|
+
process.stdout.write('\x1b[?2004l'); // Disable bracketed paste
|
|
1206
|
+
process.stdout.write('\x1b[>0u'); // Reset keyboard enhancement to legacy mode (CSI u)
|
|
1207
|
+
process.stdout.write('\x1b[?25h'); // Ensure cursor is visible
|
|
1208
|
+
// Save captured output to conversation history
|
|
1209
|
+
const capturedOutput = this.interactiveOutputBuffer.get(this.activeTool);
|
|
1210
|
+
if (capturedOutput) {
|
|
1211
|
+
const cleanedOutput = stripAnsi(capturedOutput).trim();
|
|
1212
|
+
if (cleanedOutput.length > 50) {
|
|
1213
|
+
this.conversationHistory.push({
|
|
1214
|
+
tool: this.activeTool,
|
|
1215
|
+
role: 'assistant',
|
|
1216
|
+
content: cleanedOutput,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
this.interactiveOutputBuffer.set(this.activeTool, '');
|
|
1220
|
+
}
|
|
1221
|
+
if (process.stdin.isTTY) {
|
|
1222
|
+
process.stdin.setRawMode(false);
|
|
1223
|
+
}
|
|
1224
|
+
// Recreate readline to ensure clean state after PTY interaction
|
|
1225
|
+
this.rl?.close();
|
|
1226
|
+
this.setupReadline();
|
|
1227
|
+
// Clear any garbage after a brief delay
|
|
1228
|
+
setTimeout(() => {
|
|
1229
|
+
process.stdout.write('\x1b[2K\r');
|
|
1230
|
+
}, 100);
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
showStatus() {
|
|
1235
|
+
console.log('');
|
|
1236
|
+
const statusLines = AVAILABLE_TOOLS.map(tool => {
|
|
1237
|
+
const isRunning = this.runningProcesses.has(tool.name);
|
|
1238
|
+
const hasSession = tool.name === 'claude' ? this.claudeHasSession : this.geminiHasSession;
|
|
1239
|
+
const icon = tool.name === 'claude' ? '◆' : '◇';
|
|
1240
|
+
return `${tool.color}${icon} ${tool.displayName.padEnd(12)}${colors.reset} ${isRunning ? `${colors.green}● Running${colors.reset}` : `${colors.dim}○ Stopped${colors.reset}`} ${hasSession ? `${colors.dim}(has history)${colors.reset}` : ''}`;
|
|
1241
|
+
});
|
|
1242
|
+
console.log(drawBox(statusLines, 45));
|
|
1243
|
+
console.log('');
|
|
1244
|
+
}
|
|
1245
|
+
async handleForward(argsString) {
|
|
1246
|
+
// Find the last assistant response
|
|
1247
|
+
const lastResponse = [...this.conversationHistory]
|
|
1248
|
+
.reverse()
|
|
1249
|
+
.find(m => m.role === 'assistant');
|
|
1250
|
+
if (!lastResponse) {
|
|
1251
|
+
console.log('No response to forward yet.');
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const sourceTool = lastResponse.tool;
|
|
1255
|
+
const otherTools = AVAILABLE_TOOLS
|
|
1256
|
+
.map(t => t.name)
|
|
1257
|
+
.filter(t => t !== sourceTool);
|
|
1258
|
+
// Parse args: first word might be a tool name
|
|
1259
|
+
const parts = argsString.trim().split(/\s+/).filter(p => p);
|
|
1260
|
+
let targetTool;
|
|
1261
|
+
let additionalMessage;
|
|
1262
|
+
if (parts.length > 0 && otherTools.includes(parts[0].toLowerCase())) {
|
|
1263
|
+
// First arg is a tool name
|
|
1264
|
+
targetTool = parts[0].toLowerCase();
|
|
1265
|
+
additionalMessage = parts.slice(1).join(' ');
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
// No tool specified - auto-select if only one other tool
|
|
1269
|
+
if (otherTools.length === 1) {
|
|
1270
|
+
targetTool = otherTools[0];
|
|
1271
|
+
additionalMessage = argsString;
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
// Multiple tools available - require explicit selection
|
|
1275
|
+
console.log(`${colors.yellow}Multiple tools available.${colors.reset} Please specify target:`);
|
|
1276
|
+
console.log(` ${colors.brightGreen}/forward${colors.reset} <${otherTools.join('|')}> [message]`);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Validate target tool exists and is not the source
|
|
1281
|
+
if (targetTool === sourceTool) {
|
|
1282
|
+
console.log(`Cannot forward to the same tool (${sourceTool}).`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
// Switch to target tool
|
|
1286
|
+
this.activeTool = targetTool;
|
|
1287
|
+
const sourceDisplayName = getToolDisplayName(sourceTool);
|
|
1288
|
+
const targetDisplayName = getToolDisplayName(targetTool);
|
|
1289
|
+
const sourceColor = getToolColor(sourceTool);
|
|
1290
|
+
const targetColor = getToolColor(targetTool);
|
|
1291
|
+
console.log('');
|
|
1292
|
+
console.log(`${colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
|
|
1293
|
+
console.log(`${colors.green}↗${colors.reset} Forwarding from ${sourceColor}${sourceDisplayName}${colors.reset} → ${targetColor}${targetDisplayName}${colors.reset}`);
|
|
1294
|
+
console.log(`${colors.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
|
|
1295
|
+
console.log(`${targetColor}${targetDisplayName} responds:${colors.reset}`);
|
|
1296
|
+
// Build forward prompt
|
|
1297
|
+
let forwardPrompt = `Another AI assistant (${sourceDisplayName}) provided this response. Please review and share your thoughts:\n\n---\n${lastResponse.content}\n---`;
|
|
1298
|
+
if (additionalMessage.trim()) {
|
|
1299
|
+
forwardPrompt += `\n\nAdditional context: ${additionalMessage.trim()}`;
|
|
1300
|
+
}
|
|
1301
|
+
await this.sendToTool(forwardPrompt);
|
|
1302
|
+
}
|
|
1303
|
+
showHistory() {
|
|
1304
|
+
if (this.conversationHistory.length === 0) {
|
|
1305
|
+
console.log(`\n${colors.dim}No conversation history yet.${colors.reset}\n`);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
console.log(`\n${colors.bold}Conversation History${colors.reset}`);
|
|
1309
|
+
console.log(`${colors.dim}${'─'.repeat(50)}${colors.reset}`);
|
|
1310
|
+
for (let i = 0; i < this.conversationHistory.length; i++) {
|
|
1311
|
+
const msg = this.conversationHistory[i];
|
|
1312
|
+
const isUser = msg.role === 'user';
|
|
1313
|
+
const toolColor = msg.tool === 'claude' ? colors.brightCyan : colors.brightMagenta;
|
|
1314
|
+
let roleDisplay;
|
|
1315
|
+
if (isUser) {
|
|
1316
|
+
roleDisplay = `${colors.yellow}You${colors.reset}`;
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
roleDisplay = `${toolColor}${msg.tool}${colors.reset}`;
|
|
1320
|
+
}
|
|
1321
|
+
const preview = msg.content.length > 80
|
|
1322
|
+
? msg.content.slice(0, 80) + '...'
|
|
1323
|
+
: msg.content;
|
|
1324
|
+
console.log(`${colors.dim}${String(i + 1).padStart(2)}.${colors.reset} ${roleDisplay}: ${colors.white}${preview}${colors.reset}`);
|
|
1325
|
+
}
|
|
1326
|
+
console.log(`${colors.dim}${'─'.repeat(50)}${colors.reset}\n`);
|
|
1327
|
+
}
|
|
1328
|
+
async cleanup() {
|
|
1329
|
+
// Kill any running processes
|
|
1330
|
+
for (const [tool, proc] of this.runningProcesses) {
|
|
1331
|
+
console.log(`Stopping ${tool}...`);
|
|
1332
|
+
proc.kill();
|
|
1333
|
+
}
|
|
1334
|
+
this.runningProcesses.clear();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
export async function startSDKSession() {
|
|
1338
|
+
const session = new SDKSession();
|
|
1339
|
+
await session.start();
|
|
1340
|
+
}
|
|
1341
|
+
//# sourceMappingURL=sdk-session.js.map
|