@jungjaehoon/mama-core 1.0.2 → 1.1.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/package.json +33 -21
- package/src/config-loader.js +0 -218
- package/src/db-adapter/README.md +0 -110
- package/src/db-adapter/base-adapter.js +0 -91
- package/src/db-adapter/index.js +0 -31
- package/src/db-adapter/sqlite-adapter.js +0 -364
- package/src/db-adapter/statement.js +0 -127
- package/src/db-manager.js +0 -671
- package/src/debug-logger.js +0 -86
- package/src/decision-formatter.js +0 -1276
- package/src/decision-tracker.js +0 -621
- package/src/embedding-cache.js +0 -222
- package/src/embedding-client.js +0 -141
- package/src/embedding-server/index.js +0 -424
- package/src/embedding-server/mobile/auth.js +0 -160
- package/src/embedding-server/mobile/daemon.js +0 -313
- package/src/embedding-server/mobile/output-parser.js +0 -281
- package/src/embedding-server/mobile/session-api.js +0 -279
- package/src/embedding-server/mobile/session-manager.js +0 -377
- package/src/embedding-server/mobile/websocket-handler.js +0 -443
- package/src/embeddings.js +0 -305
- package/src/errors.js +0 -326
- package/src/index.js +0 -41
- package/src/mama-api.js +0 -2614
- package/src/memory-inject.js +0 -174
- package/src/memory-store.js +0 -89
- package/src/notification-manager.js +0 -3
- package/src/ollama-client.js +0 -391
- package/src/outcome-tracker.js +0 -351
- package/src/progress-indicator.js +0 -110
- package/src/query-intent.js +0 -237
- package/src/relevance-scorer.js +0 -286
- package/src/tier-validator.js +0 -269
- package/src/time-formatter.js +0 -98
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Claude Code Daemon - stream-json based subprocess manager
|
|
3
|
-
* @module mobile/daemon
|
|
4
|
-
* @version 1.5.2
|
|
5
|
-
*
|
|
6
|
-
* Manages Claude Code as a subprocess using stream-json mode for
|
|
7
|
-
* bidirectional JSON communication.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const { ClaudeDaemon } = require('./daemon');
|
|
11
|
-
* const daemon = new ClaudeDaemon('/path/to/project', 'session_123');
|
|
12
|
-
* await daemon.spawn();
|
|
13
|
-
* daemon.send('Hello Claude!');
|
|
14
|
-
* daemon.on('output', (data) => console.log(data));
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { EventEmitter } = require('events');
|
|
18
|
-
const { spawn } = require('child_process');
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* ANSI escape code regex pattern
|
|
22
|
-
* @type {RegExp}
|
|
23
|
-
*/
|
|
24
|
-
// eslint-disable-next-line no-control-regex
|
|
25
|
-
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* ClaudeDaemon class - manages Claude Code subprocess via stream-json
|
|
29
|
-
* @extends EventEmitter
|
|
30
|
-
*
|
|
31
|
-
* @fires ClaudeDaemon#output - When stdout data is received
|
|
32
|
-
* @fires ClaudeDaemon#error - When an error occurs
|
|
33
|
-
* @fires ClaudeDaemon#exit - When the process exits
|
|
34
|
-
*/
|
|
35
|
-
class ClaudeDaemon extends EventEmitter {
|
|
36
|
-
/**
|
|
37
|
-
* Create a new ClaudeDaemon instance
|
|
38
|
-
* @param {string} projectDir - Working directory for Claude Code
|
|
39
|
-
* @param {string} sessionId - Unique session identifier
|
|
40
|
-
*/
|
|
41
|
-
constructor(projectDir, sessionId) {
|
|
42
|
-
super();
|
|
43
|
-
this.projectDir = projectDir;
|
|
44
|
-
this.sessionId = sessionId;
|
|
45
|
-
this.process = null;
|
|
46
|
-
this.pid = null;
|
|
47
|
-
this.isRunning = false;
|
|
48
|
-
this.buffer = '';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Spawn the Claude Code subprocess with stream-json mode
|
|
53
|
-
* @returns {Promise<void>}
|
|
54
|
-
* @throws {Error} If spawn fails or process is already running
|
|
55
|
-
*/
|
|
56
|
-
async spawn() {
|
|
57
|
-
if (this.isRunning) {
|
|
58
|
-
throw new Error('Daemon is already running');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
try {
|
|
63
|
-
// Use stream-json mode for bidirectional JSON communication
|
|
64
|
-
const args = [
|
|
65
|
-
'--dangerously-skip-permissions',
|
|
66
|
-
'--input-format',
|
|
67
|
-
'stream-json',
|
|
68
|
-
'--output-format',
|
|
69
|
-
'stream-json',
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
// Spawn Claude Code CLI
|
|
73
|
-
this.process = spawn('claude', args, {
|
|
74
|
-
cwd: this.projectDir,
|
|
75
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
-
env: {
|
|
77
|
-
...process.env,
|
|
78
|
-
CLAUDE_CODE_ENTRY_POINT: 'cli',
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Store PID
|
|
83
|
-
this.pid = this.process.pid;
|
|
84
|
-
this.isRunning = true;
|
|
85
|
-
|
|
86
|
-
console.error(
|
|
87
|
-
`[MobileDaemon] Session ${this.sessionId} spawned with PID ${this.pid} (stream-json mode)`
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Handle stdout data (JSON stream)
|
|
91
|
-
this.process.stdout.on('data', (chunk) => {
|
|
92
|
-
this.buffer += chunk.toString();
|
|
93
|
-
|
|
94
|
-
// Try to parse complete JSON objects from buffer
|
|
95
|
-
let newlineIndex;
|
|
96
|
-
while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) {
|
|
97
|
-
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
98
|
-
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
99
|
-
|
|
100
|
-
if (line) {
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(line);
|
|
103
|
-
console.error(
|
|
104
|
-
`[MobileDaemon] Parsed JSON: ${JSON.stringify(parsed).substring(0, 100)}...`
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// Detect tool usage (content_block_start with tool_use type)
|
|
108
|
-
if (
|
|
109
|
-
parsed.type === 'content_block_start' &&
|
|
110
|
-
parsed.content_block?.type === 'tool_use'
|
|
111
|
-
) {
|
|
112
|
-
const toolBlock = parsed.content_block;
|
|
113
|
-
console.error(`[MobileDaemon] Tool use detected: ${toolBlock.name}`);
|
|
114
|
-
this.emit('tool_use', {
|
|
115
|
-
tool: toolBlock.name,
|
|
116
|
-
toolId: toolBlock.id,
|
|
117
|
-
input: toolBlock.input,
|
|
118
|
-
sessionId: this.sessionId,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Detect tool completion (content_block_stop after tool_use)
|
|
123
|
-
if (parsed.type === 'content_block_stop' && parsed.index !== undefined) {
|
|
124
|
-
console.error(`[MobileDaemon] Tool complete for block ${parsed.index}`);
|
|
125
|
-
this.emit('tool_complete', {
|
|
126
|
-
index: parsed.index,
|
|
127
|
-
sessionId: this.sessionId,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Extract text content from various message types
|
|
132
|
-
let textContent = '';
|
|
133
|
-
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
134
|
-
// Assistant message with content blocks
|
|
135
|
-
for (const block of parsed.message.content) {
|
|
136
|
-
if (block.type === 'text') {
|
|
137
|
-
textContent += block.text;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} else if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
141
|
-
// Streaming delta
|
|
142
|
-
textContent = parsed.delta.text;
|
|
143
|
-
} else if (parsed.result?.content) {
|
|
144
|
-
// Final result
|
|
145
|
-
textContent = parsed.result.content;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (textContent) {
|
|
149
|
-
this.emit('output', {
|
|
150
|
-
type: 'stdout',
|
|
151
|
-
text: textContent,
|
|
152
|
-
raw: line,
|
|
153
|
-
parsed,
|
|
154
|
-
sessionId: this.sessionId,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Detect response completion (result message)
|
|
159
|
-
if (parsed.type === 'result') {
|
|
160
|
-
console.error(`[MobileDaemon] Response complete for session ${this.sessionId}`);
|
|
161
|
-
this.emit('response_complete', {
|
|
162
|
-
sessionId: this.sessionId,
|
|
163
|
-
result: parsed.result,
|
|
164
|
-
duration_ms: parsed.duration_ms,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
} catch (e) {
|
|
168
|
-
// Not valid JSON, emit as raw text
|
|
169
|
-
console.error(`[MobileDaemon] Raw output: ${line.substring(0, 100)}...`);
|
|
170
|
-
this.emit('output', {
|
|
171
|
-
type: 'stdout',
|
|
172
|
-
text: line,
|
|
173
|
-
raw: line,
|
|
174
|
-
sessionId: this.sessionId,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Handle stderr data
|
|
182
|
-
this.process.stderr.on('data', (chunk) => {
|
|
183
|
-
const text = chunk.toString();
|
|
184
|
-
console.error(`[MobileDaemon] stderr: ${text.substring(0, 200)}`);
|
|
185
|
-
this.emit('output', {
|
|
186
|
-
type: 'stderr',
|
|
187
|
-
text: text.replace(ANSI_REGEX, ''),
|
|
188
|
-
raw: text,
|
|
189
|
-
sessionId: this.sessionId,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Handle process errors
|
|
194
|
-
this.process.on('error', (err) => {
|
|
195
|
-
console.error(`[MobileDaemon] Error in session ${this.sessionId}:`, err.message);
|
|
196
|
-
this.isRunning = false;
|
|
197
|
-
this.emit('error', {
|
|
198
|
-
error: err,
|
|
199
|
-
sessionId: this.sessionId,
|
|
200
|
-
});
|
|
201
|
-
reject(err);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Handle process exit
|
|
205
|
-
this.process.on('exit', (code, signal) => {
|
|
206
|
-
console.error(
|
|
207
|
-
`[MobileDaemon] Session ${this.sessionId} exited with code ${code}, signal ${signal}`
|
|
208
|
-
);
|
|
209
|
-
this.isRunning = false;
|
|
210
|
-
this.pid = null;
|
|
211
|
-
this.emit('exit', {
|
|
212
|
-
code,
|
|
213
|
-
signal,
|
|
214
|
-
sessionId: this.sessionId,
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Process should be ready immediately in stream-json mode
|
|
219
|
-
setTimeout(() => {
|
|
220
|
-
if (this.isRunning) {
|
|
221
|
-
resolve();
|
|
222
|
-
}
|
|
223
|
-
}, 100);
|
|
224
|
-
} catch (err) {
|
|
225
|
-
console.error(`[MobileDaemon] Failed to spawn session ${this.sessionId}:`, err.message);
|
|
226
|
-
this.isRunning = false;
|
|
227
|
-
this.emit('error', {
|
|
228
|
-
error: err,
|
|
229
|
-
sessionId: this.sessionId,
|
|
230
|
-
});
|
|
231
|
-
reject(err);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Send a message to Claude via stdin (stream-json format)
|
|
238
|
-
* @param {string} message - Message to send
|
|
239
|
-
* @throws {Error} If process is not running
|
|
240
|
-
*/
|
|
241
|
-
send(message) {
|
|
242
|
-
if (!this.isRunning || !this.process || !this.process.stdin) {
|
|
243
|
-
throw new Error('Daemon is not running');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Send as JSON object with type "user" and nested message object
|
|
247
|
-
const jsonMessage = JSON.stringify({
|
|
248
|
-
type: 'user',
|
|
249
|
-
message: {
|
|
250
|
-
role: 'user',
|
|
251
|
-
content: message,
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
this.process.stdin.write(jsonMessage + '\n');
|
|
256
|
-
|
|
257
|
-
console.error(
|
|
258
|
-
`[MobileDaemon] Sent JSON to session ${this.sessionId}: ${jsonMessage.substring(0, 100)}...`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Kill the Claude Code subprocess
|
|
264
|
-
* @param {string} [signal='SIGTERM'] - Signal to send to process
|
|
265
|
-
*/
|
|
266
|
-
kill(signal = 'SIGTERM') {
|
|
267
|
-
if (!this.process) {
|
|
268
|
-
console.error(`[MobileDaemon] No process to kill for session ${this.sessionId}`);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
console.error(`[MobileDaemon] Killing session ${this.sessionId} with signal ${signal}`);
|
|
274
|
-
this.process.kill(signal);
|
|
275
|
-
this.isRunning = false;
|
|
276
|
-
|
|
277
|
-
// Emit exit event
|
|
278
|
-
this.emit('exit', {
|
|
279
|
-
code: null,
|
|
280
|
-
signal,
|
|
281
|
-
sessionId: this.sessionId,
|
|
282
|
-
manual: true,
|
|
283
|
-
});
|
|
284
|
-
} catch (err) {
|
|
285
|
-
console.error(`[MobileDaemon] Error killing session ${this.sessionId}:`, err.message);
|
|
286
|
-
this.emit('error', {
|
|
287
|
-
error: err,
|
|
288
|
-
sessionId: this.sessionId,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Check if the daemon is currently running
|
|
295
|
-
* @returns {boolean}
|
|
296
|
-
*/
|
|
297
|
-
isActive() {
|
|
298
|
-
return this.isRunning && this.process !== null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Get the process ID
|
|
303
|
-
* @returns {number|null}
|
|
304
|
-
*/
|
|
305
|
-
getPid() {
|
|
306
|
-
return this.pid;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
module.exports = {
|
|
311
|
-
ClaudeDaemon,
|
|
312
|
-
ANSI_REGEX,
|
|
313
|
-
};
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Output Parser - parses Claude Code stdout into structured events
|
|
3
|
-
* @module mobile/output-parser
|
|
4
|
-
* @version 1.5.0
|
|
5
|
-
*
|
|
6
|
-
* Parses raw stdout from Claude Code CLI, identifying message types
|
|
7
|
-
* such as stream content, tool usage, completion, and errors.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const { OutputParser } = require('./output-parser');
|
|
11
|
-
* const parser = new OutputParser();
|
|
12
|
-
* const events = parser.parse(chunk);
|
|
13
|
-
* // events: [{ type: 'stream', text: '...' }, { type: 'tool_use', tool: 'Read' }]
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Message types returned by the parser
|
|
18
|
-
* @typedef {'stream'|'tool_use'|'tool_complete'|'complete'|'error'} MessageType
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parsed output event
|
|
23
|
-
* @typedef {Object} ParsedEvent
|
|
24
|
-
* @property {MessageType} type - Event type
|
|
25
|
-
* @property {string} [text] - Text content (for stream type)
|
|
26
|
-
* @property {string} [tool] - Tool name (for tool_use type)
|
|
27
|
-
* @property {string} [status] - Tool status (started/completed)
|
|
28
|
-
* @property {string} [message] - Error message (for error type)
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Tool detection patterns
|
|
33
|
-
* @type {RegExp[]}
|
|
34
|
-
*/
|
|
35
|
-
const TOOL_PATTERNS = [
|
|
36
|
-
/Tool:\s*(\w+)/i,
|
|
37
|
-
/Using\s+(\w+)\s+tool/i,
|
|
38
|
-
/Calling\s+(\w+)/i,
|
|
39
|
-
/⏺\s*(\w+)/, // Claude Code uses this indicator
|
|
40
|
-
/\[(\w+)\]\s*Reading/i,
|
|
41
|
-
/\[(\w+)\]\s*Writing/i,
|
|
42
|
-
/\[(\w+)\]\s*Editing/i,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Tool completion patterns
|
|
47
|
-
* @type {RegExp[]}
|
|
48
|
-
*/
|
|
49
|
-
const TOOL_COMPLETE_PATTERNS = [/Tool\s+(\w+)\s+completed/i, /(\w+)\s+finished/i, /✓\s*(\w+)/];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Error patterns
|
|
53
|
-
* @type {RegExp[]}
|
|
54
|
-
*/
|
|
55
|
-
const ERROR_PATTERNS = [
|
|
56
|
-
/Error:\s*(.+)/i,
|
|
57
|
-
/ERROR:\s*(.+)/i,
|
|
58
|
-
/Failed:\s*(.+)/i,
|
|
59
|
-
/Exception:\s*(.+)/i,
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Completion/prompt patterns
|
|
64
|
-
* @type {RegExp[]}
|
|
65
|
-
*/
|
|
66
|
-
const COMPLETE_PATTERNS = [
|
|
67
|
-
/^>\s*$/m, // Simple prompt
|
|
68
|
-
/^\$\s*$/m, // Shell prompt
|
|
69
|
-
/^claude>\s*$/im, // Claude prompt
|
|
70
|
-
/\n\n\n$/, // Multiple empty lines
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* ANSI escape code regex pattern
|
|
75
|
-
* @type {RegExp}
|
|
76
|
-
*/
|
|
77
|
-
// eslint-disable-next-line no-control-regex
|
|
78
|
-
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* OutputParser class - parses Claude Code stdout
|
|
82
|
-
*/
|
|
83
|
-
class OutputParser {
|
|
84
|
-
/**
|
|
85
|
-
* Create a new OutputParser instance
|
|
86
|
-
*/
|
|
87
|
-
constructor() {
|
|
88
|
-
this.buffer = '';
|
|
89
|
-
this.lastToolDetected = null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Parse a stdout chunk into events
|
|
94
|
-
* @param {string|Buffer} chunk - Raw stdout data
|
|
95
|
-
* @returns {ParsedEvent[]} Array of parsed events
|
|
96
|
-
*/
|
|
97
|
-
parse(chunk) {
|
|
98
|
-
const text = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
99
|
-
const cleanText = this.stripAnsi(text);
|
|
100
|
-
|
|
101
|
-
// Add to buffer for incomplete line handling
|
|
102
|
-
this.buffer += cleanText;
|
|
103
|
-
|
|
104
|
-
const events = [];
|
|
105
|
-
|
|
106
|
-
// Check for errors first (highest priority)
|
|
107
|
-
const errorResult = this.detectError(this.buffer);
|
|
108
|
-
if (errorResult) {
|
|
109
|
-
events.push({
|
|
110
|
-
type: 'error',
|
|
111
|
-
message: errorResult,
|
|
112
|
-
});
|
|
113
|
-
// Don't clear buffer entirely, might have more content
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Check for tool usage
|
|
117
|
-
const toolResult = this.detectToolUse(this.buffer);
|
|
118
|
-
if (toolResult) {
|
|
119
|
-
events.push({
|
|
120
|
-
type: 'tool_use',
|
|
121
|
-
tool: toolResult.tool,
|
|
122
|
-
status: 'started',
|
|
123
|
-
args: toolResult.args || null,
|
|
124
|
-
});
|
|
125
|
-
this.lastToolDetected = toolResult.tool;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check for tool completion
|
|
129
|
-
const toolComplete = this.detectToolComplete(this.buffer);
|
|
130
|
-
if (toolComplete) {
|
|
131
|
-
events.push({
|
|
132
|
-
type: 'tool_complete',
|
|
133
|
-
tool: toolComplete,
|
|
134
|
-
status: 'completed',
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Check for completion
|
|
139
|
-
if (this.detectComplete(this.buffer)) {
|
|
140
|
-
events.push({
|
|
141
|
-
type: 'complete',
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Always emit stream event with the text
|
|
146
|
-
if (cleanText.trim()) {
|
|
147
|
-
events.push({
|
|
148
|
-
type: 'stream',
|
|
149
|
-
text: cleanText,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Clear buffer if we have complete lines (ends with newline)
|
|
154
|
-
if (this.buffer.endsWith('\n')) {
|
|
155
|
-
this.buffer = '';
|
|
156
|
-
} else {
|
|
157
|
-
// Keep only the incomplete last line
|
|
158
|
-
const lastNewline = this.buffer.lastIndexOf('\n');
|
|
159
|
-
if (lastNewline !== -1) {
|
|
160
|
-
this.buffer = this.buffer.substring(lastNewline + 1);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return events;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Remove ANSI escape codes from text
|
|
169
|
-
* @param {string} text - Text with potential ANSI codes
|
|
170
|
-
* @returns {string} Clean text
|
|
171
|
-
*/
|
|
172
|
-
stripAnsi(text) {
|
|
173
|
-
return text.replace(ANSI_REGEX, '');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Detect tool usage pattern in text
|
|
178
|
-
* @param {string} text - Text to analyze
|
|
179
|
-
* @returns {Object|null} Tool info or null
|
|
180
|
-
*/
|
|
181
|
-
detectToolUse(text) {
|
|
182
|
-
for (const pattern of TOOL_PATTERNS) {
|
|
183
|
-
const match = text.match(pattern);
|
|
184
|
-
if (match) {
|
|
185
|
-
const tool = match[1];
|
|
186
|
-
// Try to extract args if present
|
|
187
|
-
const argsMatch = text.match(new RegExp(`${tool}[:\\s]+(.+?)(?:\\n|$)`, 'i'));
|
|
188
|
-
return {
|
|
189
|
-
tool,
|
|
190
|
-
args: argsMatch ? argsMatch[1].trim() : null,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Detect tool completion pattern in text
|
|
199
|
-
* @param {string} text - Text to analyze
|
|
200
|
-
* @returns {string|null} Tool name or null
|
|
201
|
-
*/
|
|
202
|
-
detectToolComplete(text) {
|
|
203
|
-
for (const pattern of TOOL_COMPLETE_PATTERNS) {
|
|
204
|
-
const match = text.match(pattern);
|
|
205
|
-
if (match) {
|
|
206
|
-
return match[1];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Detect completion pattern in text
|
|
214
|
-
* @param {string} text - Text to analyze
|
|
215
|
-
* @returns {boolean} True if completion detected
|
|
216
|
-
*/
|
|
217
|
-
detectComplete(text) {
|
|
218
|
-
for (const pattern of COMPLETE_PATTERNS) {
|
|
219
|
-
if (pattern.test(text)) {
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Detect error pattern in text
|
|
228
|
-
* @param {string} text - Text to analyze
|
|
229
|
-
* @returns {string|null} Error message or null
|
|
230
|
-
*/
|
|
231
|
-
detectError(text) {
|
|
232
|
-
for (const pattern of ERROR_PATTERNS) {
|
|
233
|
-
const match = text.match(pattern);
|
|
234
|
-
if (match) {
|
|
235
|
-
return match[1].trim();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Flush any remaining buffer content
|
|
243
|
-
* @returns {ParsedEvent[]} Remaining events
|
|
244
|
-
*/
|
|
245
|
-
flush() {
|
|
246
|
-
const events = [];
|
|
247
|
-
if (this.buffer.trim()) {
|
|
248
|
-
events.push({
|
|
249
|
-
type: 'stream',
|
|
250
|
-
text: this.buffer,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
this.buffer = '';
|
|
254
|
-
this.lastToolDetected = null;
|
|
255
|
-
return events;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Reset the parser state
|
|
260
|
-
*/
|
|
261
|
-
reset() {
|
|
262
|
-
this.buffer = '';
|
|
263
|
-
this.lastToolDetected = null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get current buffer content
|
|
268
|
-
* @returns {string}
|
|
269
|
-
*/
|
|
270
|
-
getBuffer() {
|
|
271
|
-
return this.buffer;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
module.exports = {
|
|
276
|
-
OutputParser,
|
|
277
|
-
TOOL_PATTERNS,
|
|
278
|
-
ERROR_PATTERNS,
|
|
279
|
-
COMPLETE_PATTERNS,
|
|
280
|
-
ANSI_REGEX,
|
|
281
|
-
};
|