gemkit-cli 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +141 -7
  2. package/dist/commands/agent/index.d.ts +9 -0
  3. package/dist/commands/agent/index.js +1329 -0
  4. package/dist/commands/cache/index.d.ts +5 -0
  5. package/dist/commands/cache/index.js +43 -0
  6. package/dist/commands/catalog/index.d.ts +2 -0
  7. package/dist/commands/catalog/index.js +57 -0
  8. package/dist/commands/config/index.d.ts +7 -0
  9. package/dist/commands/config/index.js +122 -0
  10. package/dist/commands/convert/index.d.ts +8 -0
  11. package/dist/commands/convert/index.js +391 -0
  12. package/dist/commands/doctor/index.d.ts +2 -0
  13. package/dist/commands/doctor/index.js +243 -0
  14. package/dist/commands/extension/index.d.ts +5 -0
  15. package/dist/commands/extension/index.js +52 -0
  16. package/dist/commands/index.d.ts +5 -0
  17. package/dist/commands/index.js +37 -0
  18. package/dist/commands/init/index.d.ts +6 -0
  19. package/dist/commands/init/index.js +345 -0
  20. package/dist/commands/new/index.d.ts +5 -0
  21. package/dist/commands/new/index.js +49 -0
  22. package/dist/commands/office/index.d.ts +5 -0
  23. package/dist/commands/office/index.js +283 -0
  24. package/dist/commands/paste/index.d.ts +10 -0
  25. package/dist/commands/paste/index.js +533 -0
  26. package/dist/commands/plan/index.d.ts +8 -0
  27. package/dist/commands/plan/index.js +247 -0
  28. package/dist/commands/session/index.d.ts +8 -0
  29. package/dist/commands/session/index.js +289 -0
  30. package/dist/commands/tokens/index.d.ts +6 -0
  31. package/dist/commands/tokens/index.js +148 -0
  32. package/dist/commands/update/index.d.ts +26 -0
  33. package/dist/commands/update/index.js +199 -0
  34. package/dist/commands/versions/index.d.ts +5 -0
  35. package/dist/commands/versions/index.js +39 -0
  36. package/dist/domains/agent/index.d.ts +8 -0
  37. package/dist/domains/agent/index.js +8 -0
  38. package/dist/domains/agent/mappings.d.ts +32 -0
  39. package/dist/domains/agent/mappings.js +164 -0
  40. package/dist/domains/agent/profile.d.ts +26 -0
  41. package/dist/domains/agent/profile.js +225 -0
  42. package/dist/domains/agent/pty-context.d.ts +11 -0
  43. package/dist/domains/agent/pty-context.js +83 -0
  44. package/dist/domains/agent/pty-providers.d.ts +18 -0
  45. package/dist/domains/agent/pty-providers.js +66 -0
  46. package/dist/domains/agent/pty-session.d.ts +33 -0
  47. package/dist/domains/agent/pty-session.js +82 -0
  48. package/dist/domains/agent/pty-types.d.ts +127 -0
  49. package/dist/domains/agent/pty-types.js +4 -0
  50. package/dist/domains/agent/search.d.ts +45 -0
  51. package/dist/domains/agent/search.js +614 -0
  52. package/dist/domains/agent/types.d.ts +78 -0
  53. package/dist/domains/agent/types.js +5 -0
  54. package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
  55. package/dist/domains/agent-office/documents-scanner.js +143 -0
  56. package/dist/domains/agent-office/event-emitter.d.ts +43 -0
  57. package/dist/domains/agent-office/event-emitter.js +86 -0
  58. package/dist/domains/agent-office/file-watcher.d.ts +40 -0
  59. package/dist/domains/agent-office/file-watcher.js +173 -0
  60. package/dist/domains/agent-office/icons.d.ts +11 -0
  61. package/dist/domains/agent-office/icons.js +36 -0
  62. package/dist/domains/agent-office/index.d.ts +12 -0
  63. package/dist/domains/agent-office/index.js +20 -0
  64. package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
  65. package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
  66. package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
  67. package/dist/domains/agent-office/renderer/web/server.js +228 -0
  68. package/dist/domains/agent-office/renderer/web.d.ts +30 -0
  69. package/dist/domains/agent-office/renderer/web.js +111 -0
  70. package/dist/domains/agent-office/session-bridge.d.ts +23 -0
  71. package/dist/domains/agent-office/session-bridge.js +171 -0
  72. package/dist/domains/agent-office/state-machine.d.ts +5 -0
  73. package/dist/domains/agent-office/state-machine.js +82 -0
  74. package/dist/domains/agent-office/types.d.ts +91 -0
  75. package/dist/domains/agent-office/types.js +4 -0
  76. package/dist/domains/cache/index.d.ts +1 -0
  77. package/dist/domains/cache/index.js +1 -0
  78. package/dist/domains/cache/manager.d.ts +22 -0
  79. package/dist/domains/cache/manager.js +84 -0
  80. package/dist/domains/config/index.d.ts +5 -0
  81. package/dist/domains/config/index.js +5 -0
  82. package/dist/domains/config/manager.d.ts +24 -0
  83. package/dist/domains/config/manager.js +85 -0
  84. package/dist/domains/config/schema.d.ts +17 -0
  85. package/dist/domains/config/schema.js +96 -0
  86. package/dist/domains/convert/converter.d.ts +78 -0
  87. package/dist/domains/convert/converter.js +471 -0
  88. package/dist/domains/convert/index.d.ts +5 -0
  89. package/dist/domains/convert/index.js +5 -0
  90. package/dist/domains/convert/types.d.ts +88 -0
  91. package/dist/domains/convert/types.js +18 -0
  92. package/dist/domains/github/download.d.ts +12 -0
  93. package/dist/domains/github/download.js +51 -0
  94. package/dist/domains/github/index.d.ts +2 -0
  95. package/dist/domains/github/index.js +2 -0
  96. package/dist/domains/github/releases.d.ts +16 -0
  97. package/dist/domains/github/releases.js +68 -0
  98. package/dist/domains/installation/conflict.d.ts +13 -0
  99. package/dist/domains/installation/conflict.js +38 -0
  100. package/dist/domains/installation/file-sync.d.ts +16 -0
  101. package/dist/domains/installation/file-sync.js +77 -0
  102. package/dist/domains/installation/index.d.ts +3 -0
  103. package/dist/domains/installation/index.js +3 -0
  104. package/dist/domains/installation/metadata.d.ts +20 -0
  105. package/dist/domains/installation/metadata.js +52 -0
  106. package/dist/domains/plan/index.d.ts +2 -0
  107. package/dist/domains/plan/index.js +2 -0
  108. package/dist/domains/plan/resolver.d.ts +24 -0
  109. package/dist/domains/plan/resolver.js +164 -0
  110. package/dist/domains/plan/types.d.ts +13 -0
  111. package/dist/domains/plan/types.js +4 -0
  112. package/dist/domains/session/env.d.ts +51 -0
  113. package/dist/domains/session/env.js +118 -0
  114. package/dist/domains/session/index.d.ts +8 -0
  115. package/dist/domains/session/index.js +8 -0
  116. package/dist/domains/session/manager.d.ts +56 -0
  117. package/dist/domains/session/manager.js +205 -0
  118. package/dist/domains/session/paths.d.ts +6 -0
  119. package/dist/domains/session/paths.js +6 -0
  120. package/dist/domains/session/types.d.ts +121 -0
  121. package/dist/domains/session/types.js +5 -0
  122. package/dist/domains/session/writer.d.ts +82 -0
  123. package/dist/domains/session/writer.js +431 -0
  124. package/dist/domains/tokens/index.d.ts +5 -0
  125. package/dist/domains/tokens/index.js +5 -0
  126. package/dist/domains/tokens/pricing.d.ts +38 -0
  127. package/dist/domains/tokens/pricing.js +129 -0
  128. package/dist/domains/tokens/scanner.d.ts +42 -0
  129. package/dist/domains/tokens/scanner.js +168 -0
  130. package/dist/index.d.ts +5 -0
  131. package/dist/index.js +87 -58
  132. package/dist/services/aipty.d.ts +76 -0
  133. package/dist/services/aipty.js +276 -0
  134. package/dist/services/archive.d.ts +22 -0
  135. package/dist/services/archive.js +53 -0
  136. package/dist/services/auto-update.d.ts +26 -0
  137. package/dist/services/auto-update.js +117 -0
  138. package/dist/services/hash.d.ts +36 -0
  139. package/dist/services/hash.js +63 -0
  140. package/dist/services/logger.d.ts +28 -0
  141. package/dist/services/logger.js +102 -0
  142. package/dist/services/music.d.ts +67 -0
  143. package/dist/services/music.js +290 -0
  144. package/dist/services/npm.d.ts +22 -0
  145. package/dist/services/npm.js +65 -0
  146. package/dist/services/pty-client.d.ts +66 -0
  147. package/dist/services/pty-client.js +154 -0
  148. package/dist/services/pty-server.d.ts +102 -0
  149. package/dist/services/pty-server.js +613 -0
  150. package/dist/types/index.d.ts +155 -0
  151. package/dist/types/index.js +4 -0
  152. package/dist/utils/colors.d.ts +43 -0
  153. package/dist/utils/colors.js +98 -0
  154. package/dist/utils/errors.d.ts +24 -0
  155. package/dist/utils/errors.js +56 -0
  156. package/dist/utils/paths.d.ts +46 -0
  157. package/dist/utils/paths.js +89 -0
  158. package/dist/utils/platform.d.ts +11 -0
  159. package/dist/utils/platform.js +31 -0
  160. 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
+ }