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