@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.
@@ -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
- };