gemkit-cli 0.2.3 → 0.3.1
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 +141 -7
- package/dist/commands/agent/index.d.ts +9 -0
- package/dist/commands/agent/index.js +1329 -0
- package/dist/commands/cache/index.d.ts +5 -0
- package/dist/commands/cache/index.js +43 -0
- package/dist/commands/catalog/index.d.ts +2 -0
- package/dist/commands/catalog/index.js +57 -0
- package/dist/commands/config/index.d.ts +7 -0
- package/dist/commands/config/index.js +122 -0
- package/dist/commands/convert/index.d.ts +8 -0
- package/dist/commands/convert/index.js +391 -0
- package/dist/commands/doctor/index.d.ts +2 -0
- package/dist/commands/doctor/index.js +243 -0
- package/dist/commands/extension/index.d.ts +5 -0
- package/dist/commands/extension/index.js +52 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +37 -0
- package/dist/commands/init/index.d.ts +6 -0
- package/dist/commands/init/index.js +345 -0
- package/dist/commands/new/index.d.ts +5 -0
- package/dist/commands/new/index.js +49 -0
- package/dist/commands/office/index.d.ts +5 -0
- package/dist/commands/office/index.js +283 -0
- package/dist/commands/paste/index.d.ts +10 -0
- package/dist/commands/paste/index.js +533 -0
- package/dist/commands/plan/index.d.ts +8 -0
- package/dist/commands/plan/index.js +247 -0
- package/dist/commands/session/index.d.ts +8 -0
- package/dist/commands/session/index.js +289 -0
- package/dist/commands/tokens/index.d.ts +6 -0
- package/dist/commands/tokens/index.js +148 -0
- package/dist/commands/update/index.d.ts +26 -0
- package/dist/commands/update/index.js +199 -0
- package/dist/commands/versions/index.d.ts +5 -0
- package/dist/commands/versions/index.js +39 -0
- package/dist/domains/agent/index.d.ts +8 -0
- package/dist/domains/agent/index.js +8 -0
- package/dist/domains/agent/mappings.d.ts +32 -0
- package/dist/domains/agent/mappings.js +164 -0
- package/dist/domains/agent/profile.d.ts +26 -0
- package/dist/domains/agent/profile.js +225 -0
- package/dist/domains/agent/pty-context.d.ts +11 -0
- package/dist/domains/agent/pty-context.js +83 -0
- package/dist/domains/agent/pty-providers.d.ts +18 -0
- package/dist/domains/agent/pty-providers.js +66 -0
- package/dist/domains/agent/pty-session.d.ts +33 -0
- package/dist/domains/agent/pty-session.js +82 -0
- package/dist/domains/agent/pty-types.d.ts +127 -0
- package/dist/domains/agent/pty-types.js +4 -0
- package/dist/domains/agent/search.d.ts +45 -0
- package/dist/domains/agent/search.js +614 -0
- package/dist/domains/agent/types.d.ts +78 -0
- package/dist/domains/agent/types.js +5 -0
- package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
- package/dist/domains/agent-office/documents-scanner.js +143 -0
- package/dist/domains/agent-office/event-emitter.d.ts +43 -0
- package/dist/domains/agent-office/event-emitter.js +86 -0
- package/dist/domains/agent-office/file-watcher.d.ts +40 -0
- package/dist/domains/agent-office/file-watcher.js +173 -0
- package/dist/domains/agent-office/icons.d.ts +11 -0
- package/dist/domains/agent-office/icons.js +36 -0
- package/dist/domains/agent-office/index.d.ts +12 -0
- package/dist/domains/agent-office/index.js +20 -0
- package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
- package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
- package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
- package/dist/domains/agent-office/renderer/web/server.js +228 -0
- package/dist/domains/agent-office/renderer/web.d.ts +30 -0
- package/dist/domains/agent-office/renderer/web.js +111 -0
- package/dist/domains/agent-office/session-bridge.d.ts +23 -0
- package/dist/domains/agent-office/session-bridge.js +171 -0
- package/dist/domains/agent-office/state-machine.d.ts +5 -0
- package/dist/domains/agent-office/state-machine.js +82 -0
- package/dist/domains/agent-office/types.d.ts +91 -0
- package/dist/domains/agent-office/types.js +4 -0
- package/dist/domains/cache/index.d.ts +1 -0
- package/dist/domains/cache/index.js +1 -0
- package/dist/domains/cache/manager.d.ts +22 -0
- package/dist/domains/cache/manager.js +84 -0
- package/dist/domains/config/index.d.ts +5 -0
- package/dist/domains/config/index.js +5 -0
- package/dist/domains/config/manager.d.ts +24 -0
- package/dist/domains/config/manager.js +85 -0
- package/dist/domains/config/schema.d.ts +17 -0
- package/dist/domains/config/schema.js +96 -0
- package/dist/domains/convert/converter.d.ts +78 -0
- package/dist/domains/convert/converter.js +471 -0
- package/dist/domains/convert/index.d.ts +5 -0
- package/dist/domains/convert/index.js +5 -0
- package/dist/domains/convert/types.d.ts +88 -0
- package/dist/domains/convert/types.js +18 -0
- package/dist/domains/github/download.d.ts +12 -0
- package/dist/domains/github/download.js +51 -0
- package/dist/domains/github/index.d.ts +2 -0
- package/dist/domains/github/index.js +2 -0
- package/dist/domains/github/releases.d.ts +16 -0
- package/dist/domains/github/releases.js +68 -0
- package/dist/domains/installation/conflict.d.ts +13 -0
- package/dist/domains/installation/conflict.js +38 -0
- package/dist/domains/installation/file-sync.d.ts +16 -0
- package/dist/domains/installation/file-sync.js +77 -0
- package/dist/domains/installation/index.d.ts +3 -0
- package/dist/domains/installation/index.js +3 -0
- package/dist/domains/installation/metadata.d.ts +20 -0
- package/dist/domains/installation/metadata.js +52 -0
- package/dist/domains/plan/index.d.ts +2 -0
- package/dist/domains/plan/index.js +2 -0
- package/dist/domains/plan/resolver.d.ts +24 -0
- package/dist/domains/plan/resolver.js +164 -0
- package/dist/domains/plan/types.d.ts +13 -0
- package/dist/domains/plan/types.js +4 -0
- package/dist/domains/session/env.d.ts +51 -0
- package/dist/domains/session/env.js +118 -0
- package/dist/domains/session/index.d.ts +8 -0
- package/dist/domains/session/index.js +8 -0
- package/dist/domains/session/manager.d.ts +56 -0
- package/dist/domains/session/manager.js +205 -0
- package/dist/domains/session/paths.d.ts +6 -0
- package/dist/domains/session/paths.js +6 -0
- package/dist/domains/session/types.d.ts +121 -0
- package/dist/domains/session/types.js +5 -0
- package/dist/domains/session/writer.d.ts +82 -0
- package/dist/domains/session/writer.js +431 -0
- package/dist/domains/tokens/index.d.ts +5 -0
- package/dist/domains/tokens/index.js +5 -0
- package/dist/domains/tokens/pricing.d.ts +38 -0
- package/dist/domains/tokens/pricing.js +129 -0
- package/dist/domains/tokens/scanner.d.ts +42 -0
- package/dist/domains/tokens/scanner.js +168 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +90 -59
- package/dist/services/aipty.d.ts +76 -0
- package/dist/services/aipty.js +276 -0
- package/dist/services/archive.d.ts +22 -0
- package/dist/services/archive.js +53 -0
- package/dist/services/auto-update.d.ts +26 -0
- package/dist/services/auto-update.js +117 -0
- package/dist/services/hash.d.ts +36 -0
- package/dist/services/hash.js +63 -0
- package/dist/services/logger.d.ts +28 -0
- package/dist/services/logger.js +102 -0
- package/dist/services/music.d.ts +67 -0
- package/dist/services/music.js +290 -0
- package/dist/services/npm.d.ts +22 -0
- package/dist/services/npm.js +65 -0
- package/dist/services/pty-client.d.ts +66 -0
- package/dist/services/pty-client.js +154 -0
- package/dist/services/pty-server.d.ts +102 -0
- package/dist/services/pty-server.js +613 -0
- package/dist/types/index.d.ts +155 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/colors.d.ts +43 -0
- package/dist/utils/colors.js +98 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.js +56 -0
- package/dist/utils/paths.d.ts +46 -0
- package/dist/utils/paths.js +89 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +31 -0
- package/package.json +55 -54
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Server - Maintains AI session and accepts commands via TCP
|
|
3
|
+
* Ported from claude-pty-wrapper/server.js
|
|
4
|
+
*/
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { AIPTY } from './aipty.js';
|
|
8
|
+
const DEFAULT_PORT = 3377;
|
|
9
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
+
const STABILITY_THRESHOLD_MS = 2000;
|
|
11
|
+
export class PtyServer {
|
|
12
|
+
options;
|
|
13
|
+
ai = null;
|
|
14
|
+
server = null;
|
|
15
|
+
outputHistory = '';
|
|
16
|
+
lastPromptIndex = 0;
|
|
17
|
+
lastSentPrompt = '';
|
|
18
|
+
lastDataReceivedTime = 0;
|
|
19
|
+
currentExchangeId = null;
|
|
20
|
+
exchangeHistory = [];
|
|
21
|
+
streamClients = [];
|
|
22
|
+
port;
|
|
23
|
+
debug;
|
|
24
|
+
pid = 0;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.port = options.port || DEFAULT_PORT;
|
|
28
|
+
this.debug = options.debug || false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start the server
|
|
32
|
+
*/
|
|
33
|
+
async start() {
|
|
34
|
+
const { provider, model, tools, sessionState } = this.options;
|
|
35
|
+
if (this.debug) {
|
|
36
|
+
console.log(`[Server] Starting ${provider} with model: ${model}`);
|
|
37
|
+
}
|
|
38
|
+
this.ai = new AIPTY({
|
|
39
|
+
provider,
|
|
40
|
+
model,
|
|
41
|
+
tools,
|
|
42
|
+
debug: this.debug
|
|
43
|
+
});
|
|
44
|
+
this.ai.on('data', (data) => {
|
|
45
|
+
this.outputHistory += data;
|
|
46
|
+
this.lastDataReceivedTime = Date.now();
|
|
47
|
+
// Emit events to stream clients
|
|
48
|
+
if (this.streamClients.length > 0) {
|
|
49
|
+
const events = this.parseOutputForEvents(data);
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
this.emitStreamEvent(event);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.ai.on('ready', () => {
|
|
56
|
+
if (this.debug) {
|
|
57
|
+
console.log(`[Server] ${provider} is ready`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.ai.on('exit', (code) => {
|
|
61
|
+
if (this.debug) {
|
|
62
|
+
console.log(`[Server] ${provider} exited with code: ${code}`);
|
|
63
|
+
}
|
|
64
|
+
this.ai = null;
|
|
65
|
+
});
|
|
66
|
+
await this.ai.start();
|
|
67
|
+
// Start TCP server
|
|
68
|
+
await this.startTcpServer();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Start TCP server for client commands
|
|
72
|
+
*/
|
|
73
|
+
startTcpServer() {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
this.server = net.createServer((socket) => {
|
|
76
|
+
let isStreamMode = false;
|
|
77
|
+
socket.on('data', (data) => {
|
|
78
|
+
const message = data.toString().trim();
|
|
79
|
+
const [cmd, ...argParts] = message.split(' ');
|
|
80
|
+
const args = argParts.join(' ');
|
|
81
|
+
const response = this.handleCommand(cmd, args);
|
|
82
|
+
// Handle stream mode
|
|
83
|
+
if (response === '__STREAM_MODE__') {
|
|
84
|
+
isStreamMode = true;
|
|
85
|
+
this.streamClients.push(socket);
|
|
86
|
+
socket.write(JSON.stringify({ type: 'stream_started' }) + '\n');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
socket.write(response + '\n');
|
|
90
|
+
});
|
|
91
|
+
socket.on('error', () => {
|
|
92
|
+
// Client disconnected, ignore
|
|
93
|
+
});
|
|
94
|
+
socket.on('close', () => {
|
|
95
|
+
if (isStreamMode) {
|
|
96
|
+
this.streamClients = this.streamClients.filter(c => c !== socket);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
this.server.listen(this.port, DEFAULT_HOST, () => {
|
|
101
|
+
if (this.debug) {
|
|
102
|
+
console.log(`[Server] Listening on ${DEFAULT_HOST}:${this.port}`);
|
|
103
|
+
}
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
this.server.on('error', (err) => {
|
|
107
|
+
if (err.code === 'EADDRINUSE') {
|
|
108
|
+
reject(new Error(`Port ${this.port} is already in use`));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reject(err);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Handle client command
|
|
118
|
+
*/
|
|
119
|
+
handleCommand(cmd, args) {
|
|
120
|
+
switch (cmd) {
|
|
121
|
+
case 'status':
|
|
122
|
+
return JSON.stringify(this.getStatus());
|
|
123
|
+
case 'send':
|
|
124
|
+
return this.handleSend(args);
|
|
125
|
+
case 'read':
|
|
126
|
+
const lines = parseInt(args) || 200;
|
|
127
|
+
const allLines = this.outputHistory.split('\n');
|
|
128
|
+
return allLines.slice(-lines).join('\n');
|
|
129
|
+
case 'extract':
|
|
130
|
+
return this.extractAnswer();
|
|
131
|
+
case 'exchange':
|
|
132
|
+
return JSON.stringify(this.getStructuredExchange());
|
|
133
|
+
case 'pending':
|
|
134
|
+
return JSON.stringify(this.detectPendingTools());
|
|
135
|
+
case 'complete':
|
|
136
|
+
return JSON.stringify(this.isComplete());
|
|
137
|
+
case 'history':
|
|
138
|
+
if (args === 'clear') {
|
|
139
|
+
this.exchangeHistory = [];
|
|
140
|
+
return JSON.stringify({ ok: true, message: 'History cleared' });
|
|
141
|
+
}
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
provider: this.options.provider,
|
|
144
|
+
count: this.exchangeHistory.length,
|
|
145
|
+
exchanges: this.exchangeHistory
|
|
146
|
+
});
|
|
147
|
+
case 'stop':
|
|
148
|
+
if (this.ai) {
|
|
149
|
+
this.ai.stop().then(() => {
|
|
150
|
+
process.exit(0);
|
|
151
|
+
});
|
|
152
|
+
return JSON.stringify({ ok: true, message: 'Stopping...' });
|
|
153
|
+
}
|
|
154
|
+
return JSON.stringify({ error: 'Not running' });
|
|
155
|
+
case 'stream':
|
|
156
|
+
return '__STREAM_MODE__';
|
|
157
|
+
default:
|
|
158
|
+
return JSON.stringify({ error: 'Unknown command: ' + cmd });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Handle send command
|
|
163
|
+
*/
|
|
164
|
+
handleSend(prompt) {
|
|
165
|
+
if (!this.ai || !this.ai.getIsReady()) {
|
|
166
|
+
return JSON.stringify({ error: 'Not ready' });
|
|
167
|
+
}
|
|
168
|
+
this.lastPromptIndex = this.outputHistory.length;
|
|
169
|
+
// Only update lastSentPrompt for actual prompts, not tool confirmations
|
|
170
|
+
const isToolConfirmation = /^[123yn]$/i.test(prompt.trim());
|
|
171
|
+
if (!isToolConfirmation) {
|
|
172
|
+
this.lastSentPrompt = prompt;
|
|
173
|
+
this.currentExchangeId = crypto.randomUUID();
|
|
174
|
+
}
|
|
175
|
+
this.lastDataReceivedTime = Date.now();
|
|
176
|
+
this.ai.write(prompt);
|
|
177
|
+
setTimeout(() => this.ai?.write('\r'), 300);
|
|
178
|
+
return JSON.stringify({ ok: true, exchangeId: this.currentExchangeId });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get server status
|
|
182
|
+
*/
|
|
183
|
+
getStatus() {
|
|
184
|
+
return {
|
|
185
|
+
running: this.ai !== null,
|
|
186
|
+
ready: this.ai?.getIsReady() || false,
|
|
187
|
+
provider: this.options.provider,
|
|
188
|
+
outputLength: this.outputHistory.length
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Clean ANSI codes from output
|
|
193
|
+
*/
|
|
194
|
+
cleanAnsi(output) {
|
|
195
|
+
return output
|
|
196
|
+
.replace(/\x1b\[\?[0-9;]*[a-z]/gi, '')
|
|
197
|
+
.replace(/\x1b\[1C/g, ' ')
|
|
198
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
199
|
+
.replace(/\x1b\][^\x07]*\x07/g, '')
|
|
200
|
+
.replace(/\r/g, '');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Extract answer based on provider
|
|
204
|
+
*/
|
|
205
|
+
extractAnswer() {
|
|
206
|
+
if (this.options.provider === 'gemini') {
|
|
207
|
+
return this.extractAnswerGemini();
|
|
208
|
+
}
|
|
209
|
+
return this.extractAnswerClaude();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Extract answer from Claude output
|
|
213
|
+
*/
|
|
214
|
+
extractAnswerClaude() {
|
|
215
|
+
const cleanOutput = this.cleanAnsi(this.outputHistory);
|
|
216
|
+
const matches = cleanOutput.match(/●\s*([\s\S]*?)(?=❯|$)/g);
|
|
217
|
+
if (matches && matches.length > 0) {
|
|
218
|
+
let lastMatch = matches[matches.length - 1];
|
|
219
|
+
lastMatch = lastMatch
|
|
220
|
+
.replace(/^●\s*/, '')
|
|
221
|
+
.replace(/[─╭╰│┌┐└┘├┤┬┴┼⎿⎾]+/g, '')
|
|
222
|
+
.replace(/\? for shortcuts/g, '')
|
|
223
|
+
.replace(/esc to interrupt/g, '')
|
|
224
|
+
.replace(/\w+ing…/g, '')
|
|
225
|
+
.replace(/\(thinking\)/g, '')
|
|
226
|
+
.replace(/·\s*/g, '')
|
|
227
|
+
.replace(/[✢✶✻✽*]/g, '')
|
|
228
|
+
.replace(/Tip:\s*Use\s+\/\w+[^.]*\./gi, '')
|
|
229
|
+
.trim();
|
|
230
|
+
const lines = lastMatch.split('\n')
|
|
231
|
+
.map(l => l.trim())
|
|
232
|
+
.filter(l => {
|
|
233
|
+
if (l.length === 0)
|
|
234
|
+
return false;
|
|
235
|
+
if (l.match(/^[\s─╭╰│┌┐└┘├┤┬┴┼⎿⎾✢✶✻✽·*]+$/))
|
|
236
|
+
return false;
|
|
237
|
+
if (l.match(/thinking|thought for/i))
|
|
238
|
+
return false;
|
|
239
|
+
if (l.match(/^\d+s\)$/))
|
|
240
|
+
return false;
|
|
241
|
+
if (l.match(/^IDE disconnected$/))
|
|
242
|
+
return false;
|
|
243
|
+
if (l.match(/^Tip:\s*Use\s+\/\w+/i))
|
|
244
|
+
return false;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
return lines.join('\n').trim();
|
|
248
|
+
}
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Extract answer from Gemini output
|
|
253
|
+
*/
|
|
254
|
+
extractAnswerGemini() {
|
|
255
|
+
let clean = this.cleanAnsi(this.outputHistory);
|
|
256
|
+
// Find the LAST occurrence of the user's prompt
|
|
257
|
+
const promptPattern = `> ${this.lastSentPrompt}`;
|
|
258
|
+
const promptIdx = clean.lastIndexOf(promptPattern);
|
|
259
|
+
if (promptIdx === -1) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
// Get content after the prompt
|
|
263
|
+
let afterPrompt = clean.slice(promptIdx + promptPattern.length);
|
|
264
|
+
// Find the next user prompt indicator to stop
|
|
265
|
+
const nextPromptMatch = afterPrompt.match(/\n\s*>\s+(?!Type your message)[^\n]{10,}/);
|
|
266
|
+
if (nextPromptMatch) {
|
|
267
|
+
afterPrompt = afterPrompt.slice(0, nextPromptMatch.index);
|
|
268
|
+
}
|
|
269
|
+
// Extract content after ✦ markers (AI responses)
|
|
270
|
+
const answerParts = [];
|
|
271
|
+
const aiMatches = afterPrompt.split('✦');
|
|
272
|
+
for (let i = 1; i < aiMatches.length; i++) {
|
|
273
|
+
let part = aiMatches[i];
|
|
274
|
+
// Stop at tool markers
|
|
275
|
+
const toolMatch = part.match(/[✓⊶⊷x?]\s*(?:Shell|WriteFile|ReadFile|ReadFolder|GoogleSearch|Activate Skill)/i);
|
|
276
|
+
if (toolMatch) {
|
|
277
|
+
part = part.slice(0, toolMatch.index);
|
|
278
|
+
}
|
|
279
|
+
// Stop at spinner/status messages
|
|
280
|
+
const spinnerMatch = part.match(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]|\(esc to cancel/);
|
|
281
|
+
if (spinnerMatch) {
|
|
282
|
+
part = part.slice(0, spinnerMatch.index);
|
|
283
|
+
}
|
|
284
|
+
// Stop at file preview box
|
|
285
|
+
const fileBoxMatch = part.match(/╭─|│\s*\d+\s*│|│\s*\d+\s+[*#\-\w]/);
|
|
286
|
+
if (fileBoxMatch) {
|
|
287
|
+
part = part.slice(0, fileBoxMatch.index);
|
|
288
|
+
}
|
|
289
|
+
if (part.trim()) {
|
|
290
|
+
answerParts.push(part);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let answer = answerParts.join('\n\n').trim();
|
|
294
|
+
// Remove UI artifacts
|
|
295
|
+
answer = answer
|
|
296
|
+
.replace(/[█▀▄░▓▒│┃╭╮╯╰┌┐└┘├┤┬┴┼─━╱╲]+/g, '')
|
|
297
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '')
|
|
298
|
+
.replace(/\d+s\)/g, '')
|
|
299
|
+
.replace(/\(esc to cancel[^)]*\)/g, '')
|
|
300
|
+
.replace(/Action Required[^\n]*/gi, '')
|
|
301
|
+
.replace(/Waiting for user confirmation[^\n]*/gi, '')
|
|
302
|
+
.replace(/\d+\.\s*Allow[^\n]*/gi, '')
|
|
303
|
+
.replace(/>\s+[^\n]+/g, '')
|
|
304
|
+
.replace(/Type your message[^\n]*/gi, '');
|
|
305
|
+
// Remove duplicate lines
|
|
306
|
+
const lines = answer.split('\n');
|
|
307
|
+
const uniqueLines = [];
|
|
308
|
+
const seenLines = new Set();
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
const normalized = line.trim();
|
|
311
|
+
if (normalized.length === 0) {
|
|
312
|
+
if (uniqueLines.length === 0 || uniqueLines[uniqueLines.length - 1].trim() !== '') {
|
|
313
|
+
uniqueLines.push(line);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (!seenLines.has(normalized)) {
|
|
317
|
+
seenLines.add(normalized);
|
|
318
|
+
uniqueLines.push(line);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return uniqueLines.join('\n').trim();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Detect pending tool confirmations
|
|
325
|
+
*/
|
|
326
|
+
detectPendingTools() {
|
|
327
|
+
const cleanOutput = this.cleanAnsi(this.outputHistory);
|
|
328
|
+
const recentOutput = cleanOutput.slice(-8000);
|
|
329
|
+
const pending = [];
|
|
330
|
+
// Check for waiting confirmation indicators
|
|
331
|
+
const isWaiting = recentOutput.includes('Waiting for user confirmation') ||
|
|
332
|
+
recentOutput.includes('Action Required') ||
|
|
333
|
+
recentOutput.includes('Allow once') ||
|
|
334
|
+
recentOutput.includes('Allow for this session') ||
|
|
335
|
+
recentOutput.match(/●\s*1\.\s*Allow/) ||
|
|
336
|
+
recentOutput.match(/Apply this change\?/i);
|
|
337
|
+
if (this.options.provider === 'gemini') {
|
|
338
|
+
const pendingPatterns = [
|
|
339
|
+
{ regex: /\?\s*Shell\s+([^\n│\[]+)/gi, type: 'shell', field: 'command' },
|
|
340
|
+
{ regex: /\?\s*WriteFile\s+(?:Writing to\s+)?([^\n│]+)/gi, type: 'write_file', field: 'path' },
|
|
341
|
+
{ regex: /\?\s*ReadFile\s+(?:Reading\s+)?([^\n│]+)/gi, type: 'read_file', field: 'path' },
|
|
342
|
+
{ regex: /\?\s*ReadFolder\s+([^\n│]+)/gi, type: 'read_folder', field: 'path' },
|
|
343
|
+
];
|
|
344
|
+
const seen = new Set();
|
|
345
|
+
for (const { regex, type, field } of pendingPatterns) {
|
|
346
|
+
const matches = recentOutput.matchAll(regex);
|
|
347
|
+
for (const match of matches) {
|
|
348
|
+
const detail = this.cleanToolDetail(match[1]);
|
|
349
|
+
const key = `${type}:${detail.slice(0, 30)}`;
|
|
350
|
+
if (detail && !seen.has(key) && isWaiting) {
|
|
351
|
+
seen.add(key);
|
|
352
|
+
const tool = {
|
|
353
|
+
type,
|
|
354
|
+
[field]: detail,
|
|
355
|
+
waiting: true,
|
|
356
|
+
options: ['1. Allow once', '2. Allow for this session', '3. No, suggest changes']
|
|
357
|
+
};
|
|
358
|
+
pending.push(tool);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Check for "Apply this change?" pattern
|
|
363
|
+
if (recentOutput.match(/Apply this change\?/i) && isWaiting) {
|
|
364
|
+
pending.push({
|
|
365
|
+
type: 'apply_change',
|
|
366
|
+
waiting: true,
|
|
367
|
+
options: ['1. Allow once', '2. Allow for this session', '3. No, suggest changes']
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// Action Required indicator
|
|
371
|
+
const actionMatch = recentOutput.match(/Action Required\s*(\d+)\s*of\s*(\d+)/i);
|
|
372
|
+
if (actionMatch) {
|
|
373
|
+
pending.forEach(p => {
|
|
374
|
+
p.actionRequired = { current: parseInt(actionMatch[1]), total: parseInt(actionMatch[2]) };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Claude tool confirmation patterns
|
|
379
|
+
if (this.options.provider === 'claude') {
|
|
380
|
+
const runMatch = recentOutput.match(/Run\s+(.+?)\s*\?/i);
|
|
381
|
+
if (runMatch && isWaiting) {
|
|
382
|
+
pending.push({ type: 'shell', command: runMatch[1].trim(), waiting: true });
|
|
383
|
+
}
|
|
384
|
+
const writeMatch = recentOutput.match(/Write to\s+(.+?)\s*\?/i);
|
|
385
|
+
if (writeMatch && isWaiting) {
|
|
386
|
+
pending.push({ type: 'write_file', path: writeMatch[1].trim(), waiting: true });
|
|
387
|
+
}
|
|
388
|
+
const editMatch = recentOutput.match(/Edit\s+(.+?)\s*\?/i);
|
|
389
|
+
if (editMatch && isWaiting) {
|
|
390
|
+
pending.push({ type: 'edit_file', path: editMatch[1].trim(), waiting: true });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
hasPending: pending.length > 0 && !!isWaiting,
|
|
395
|
+
tools: pending,
|
|
396
|
+
hint: isWaiting ? 'Send "1" to allow once, "2" for session, "3" to suggest changes' : undefined
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Clean tool detail string
|
|
401
|
+
*/
|
|
402
|
+
cleanToolDetail(detail) {
|
|
403
|
+
return detail
|
|
404
|
+
.replace(/[│┃╭╮╯╰┌┐└┘├┤┬┴┼█▀▄░▓▒]+/g, '')
|
|
405
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '')
|
|
406
|
+
.replace(/\(esc to cancel[^)]*\)/g, '')
|
|
407
|
+
.replace(/\d+s\)/g, '')
|
|
408
|
+
.replace(/…\s*$/, '')
|
|
409
|
+
.replace(/\s+/g, ' ')
|
|
410
|
+
.trim();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Extract tool results from output
|
|
414
|
+
*/
|
|
415
|
+
extractToolResults() {
|
|
416
|
+
const results = [];
|
|
417
|
+
const seen = new Set();
|
|
418
|
+
if (this.options.provider === 'gemini') {
|
|
419
|
+
const clean = this.cleanAnsi(this.outputHistory);
|
|
420
|
+
const promptPattern = `> ${this.lastSentPrompt}`;
|
|
421
|
+
const promptIdx = clean.lastIndexOf(promptPattern);
|
|
422
|
+
const searchArea = promptIdx !== -1 ? clean.slice(promptIdx) : clean.slice(-30000);
|
|
423
|
+
const completedPatterns = [
|
|
424
|
+
{ regex: /✓\s*Shell\s+([^\n│]+)/gi, type: 'shell', status: 'completed' },
|
|
425
|
+
{ regex: /✓\s*WriteFile\s+(?:Writing to\s+)?([^\n│]+)/gi, type: 'write_file', status: 'completed' },
|
|
426
|
+
{ regex: /✓\s*ReadFile\s+(?:Reading\s+)?([^\n│]+)/gi, type: 'read_file', status: 'completed' },
|
|
427
|
+
{ regex: /✓\s*ReadFolder\s+([^\n│]+)/gi, type: 'read_folder', status: 'completed' },
|
|
428
|
+
{ regex: /✓\s*GoogleSearch\s+([^\n│]+)/gi, type: 'web_search', status: 'completed' },
|
|
429
|
+
{ regex: /x\s*Shell\s+([^\n│]+)/gi, type: 'shell', status: 'failed' },
|
|
430
|
+
{ regex: /x\s*WriteFile\s+([^\n│]+)/gi, type: 'write_file', status: 'failed' },
|
|
431
|
+
];
|
|
432
|
+
for (const { regex, type, status } of completedPatterns) {
|
|
433
|
+
const matches = searchArea.matchAll(regex);
|
|
434
|
+
for (const match of matches) {
|
|
435
|
+
const detail = this.cleanToolDetail(match[1]);
|
|
436
|
+
const key = `${type}:${detail.slice(0, 50)}`;
|
|
437
|
+
if (detail && !seen.has(key)) {
|
|
438
|
+
seen.add(key);
|
|
439
|
+
results.push({ type, detail, status });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (this.options.provider === 'claude') {
|
|
445
|
+
const cleanOutput = this.cleanAnsi(this.outputHistory);
|
|
446
|
+
const toolMatches = cleanOutput.matchAll(/Ran\s+(\w+):\s*([^\n]+)/g);
|
|
447
|
+
for (const match of toolMatches) {
|
|
448
|
+
const key = `${match[1]}:${match[2].slice(0, 50)}`;
|
|
449
|
+
if (!seen.has(key)) {
|
|
450
|
+
seen.add(key);
|
|
451
|
+
results.push({ type: match[1], detail: match[2].trim(), status: 'completed' });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return results;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get structured exchange data
|
|
459
|
+
*/
|
|
460
|
+
getStructuredExchange() {
|
|
461
|
+
const answer = this.extractAnswer();
|
|
462
|
+
const pending = this.detectPendingTools();
|
|
463
|
+
const toolResults = this.extractToolResults();
|
|
464
|
+
if (!this.currentExchangeId) {
|
|
465
|
+
this.currentExchangeId = crypto.randomUUID();
|
|
466
|
+
}
|
|
467
|
+
const exchange = {
|
|
468
|
+
id: this.currentExchangeId,
|
|
469
|
+
timestamp: new Date().toISOString(),
|
|
470
|
+
prompt: this.lastSentPrompt,
|
|
471
|
+
answer,
|
|
472
|
+
pending: pending.hasPending ? pending.tools : [],
|
|
473
|
+
toolResults,
|
|
474
|
+
status: pending.hasPending ? 'waiting_confirmation' : 'complete'
|
|
475
|
+
};
|
|
476
|
+
// Save to history when complete
|
|
477
|
+
if (exchange.status === 'complete' && answer.trim()) {
|
|
478
|
+
const existingIdx = this.exchangeHistory.findIndex(e => e.id === this.currentExchangeId);
|
|
479
|
+
if (existingIdx >= 0) {
|
|
480
|
+
this.exchangeHistory[existingIdx] = exchange;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
this.exchangeHistory.push(exchange);
|
|
484
|
+
}
|
|
485
|
+
this.currentExchangeId = null;
|
|
486
|
+
}
|
|
487
|
+
return exchange;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Check if response is complete
|
|
491
|
+
*/
|
|
492
|
+
isComplete() {
|
|
493
|
+
const recentOutput = this.outputHistory.slice(this.lastPromptIndex);
|
|
494
|
+
const cleanOutput = this.cleanAnsi(recentOutput);
|
|
495
|
+
if (this.options.provider === 'claude') {
|
|
496
|
+
const hasResponse = cleanOutput.includes('●');
|
|
497
|
+
const lastResponseIdx = cleanOutput.lastIndexOf('●');
|
|
498
|
+
const lastPromptIdx = cleanOutput.lastIndexOf('❯');
|
|
499
|
+
if (hasResponse && lastPromptIdx > lastResponseIdx) {
|
|
500
|
+
return { complete: true };
|
|
501
|
+
}
|
|
502
|
+
return { complete: false, reason: 'streaming' };
|
|
503
|
+
}
|
|
504
|
+
else if (this.options.provider === 'gemini') {
|
|
505
|
+
const hasResponse = cleanOutput.includes('✦');
|
|
506
|
+
if (!hasResponse) {
|
|
507
|
+
return { complete: false, reason: 'no_response' };
|
|
508
|
+
}
|
|
509
|
+
// Check for pending tool confirmations
|
|
510
|
+
const tail = cleanOutput.slice(-3000);
|
|
511
|
+
const hasPendingText = tail.includes('Waiting for user confirmation') ||
|
|
512
|
+
tail.includes('Action Required') ||
|
|
513
|
+
(tail.includes('Allow once') && tail.includes('Allow for this session'));
|
|
514
|
+
const hasPendingMarker = /\?\s*(Shell|WriteFile|ReadFile|ReadFolder)/i.test(tail);
|
|
515
|
+
if (hasPendingText || hasPendingMarker) {
|
|
516
|
+
return {
|
|
517
|
+
complete: false,
|
|
518
|
+
reason: 'pending_tool',
|
|
519
|
+
hint: 'Tool confirmation required. Use "gk agent send 1" to approve.'
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// Check stability
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
const timeSinceLastData = now - this.lastDataReceivedTime;
|
|
525
|
+
if (timeSinceLastData < STABILITY_THRESHOLD_MS) {
|
|
526
|
+
return { complete: false, reason: 'streaming' };
|
|
527
|
+
}
|
|
528
|
+
const lastStarIdx = cleanOutput.lastIndexOf('✦');
|
|
529
|
+
const contentAfterStar = cleanOutput.slice(lastStarIdx + 1);
|
|
530
|
+
const hasContent = contentAfterStar.trim().length > 50;
|
|
531
|
+
if (hasContent) {
|
|
532
|
+
return { complete: true };
|
|
533
|
+
}
|
|
534
|
+
return { complete: false, reason: 'waiting_content' };
|
|
535
|
+
}
|
|
536
|
+
return { complete: false, reason: 'unknown' };
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Parse output for stream events
|
|
540
|
+
*/
|
|
541
|
+
parseOutputForEvents(rawData) {
|
|
542
|
+
const cleanData = this.cleanAnsi(rawData);
|
|
543
|
+
const events = [];
|
|
544
|
+
// Tool confirmation request
|
|
545
|
+
if (cleanData.match(/\?\s*(Shell|WriteFile|ReadFile|ReadFolder)/i)) {
|
|
546
|
+
const match = cleanData.match(/\?\s*(Shell|WriteFile|ReadFile|ReadFolder)\s+([^\n\[]+)/i);
|
|
547
|
+
events.push({
|
|
548
|
+
type: 'tool_confirmation_required',
|
|
549
|
+
tool: match ? match[1] : 'unknown',
|
|
550
|
+
detail: match ? match[2].trim() : null
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// Tool completion
|
|
554
|
+
const completionMatch = cleanData.match(/✓\s*(Shell|WriteFile|ReadFile|ReadFolder|GoogleSearch)\s+([^\n]+)/i);
|
|
555
|
+
if (completionMatch) {
|
|
556
|
+
events.push({
|
|
557
|
+
type: 'tool_completed',
|
|
558
|
+
tool: completionMatch[1],
|
|
559
|
+
detail: completionMatch[2].trim()
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
// Response marker
|
|
563
|
+
if (cleanData.includes('✦') || cleanData.includes('●')) {
|
|
564
|
+
events.push({ type: 'response_chunk' });
|
|
565
|
+
}
|
|
566
|
+
// Prompt ready
|
|
567
|
+
if (cleanData.match(/[❯>]\s*$/) || cleanData.includes('Type your message')) {
|
|
568
|
+
events.push({ type: 'prompt_ready' });
|
|
569
|
+
}
|
|
570
|
+
return events;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Emit event to stream clients
|
|
574
|
+
*/
|
|
575
|
+
emitStreamEvent(event) {
|
|
576
|
+
const data = JSON.stringify(event) + '\n';
|
|
577
|
+
for (const client of this.streamClients) {
|
|
578
|
+
try {
|
|
579
|
+
client.write(data);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// Remove dead clients
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
this.streamClients = this.streamClients.filter(c => !c.destroyed);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Stop the server
|
|
589
|
+
*/
|
|
590
|
+
async stop() {
|
|
591
|
+
if (this.server) {
|
|
592
|
+
this.server.close();
|
|
593
|
+
}
|
|
594
|
+
if (this.ai) {
|
|
595
|
+
await this.ai.stop();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Start server as standalone process
|
|
601
|
+
*/
|
|
602
|
+
export async function startPtyServerProcess(options) {
|
|
603
|
+
const server = new PtyServer(options);
|
|
604
|
+
await server.start();
|
|
605
|
+
process.on('SIGINT', async () => {
|
|
606
|
+
await server.stop();
|
|
607
|
+
process.exit(0);
|
|
608
|
+
});
|
|
609
|
+
process.on('SIGTERM', async () => {
|
|
610
|
+
await server.stop();
|
|
611
|
+
process.exit(0);
|
|
612
|
+
});
|
|
613
|
+
}
|