@link-assistant/hive-mind 0.39.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/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- package/src/youtrack/youtrack.lib.mjs +425 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Interactive Mode Library
|
|
4
|
+
*
|
|
5
|
+
* [EXPERIMENTAL] This module provides real-time PR comment updates during Claude execution.
|
|
6
|
+
* It parses Claude CLI's NDJSON output and posts relevant events as GitHub PR comments.
|
|
7
|
+
*
|
|
8
|
+
* Supported JSON event types:
|
|
9
|
+
* - system.init: Session initialization
|
|
10
|
+
* - assistant (text): AI text responses
|
|
11
|
+
* - assistant (tool_use): Tool invocations
|
|
12
|
+
* - user (tool_result): Tool execution results
|
|
13
|
+
* - result: Session completion
|
|
14
|
+
* - unrecognized: Any unknown event types
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Full GitHub markdown support with collapsible sections
|
|
18
|
+
* - Smart content truncation (keeps start and end, removes middle)
|
|
19
|
+
* - Collapsed raw JSON in each comment for debugging
|
|
20
|
+
* - Rate limiting and comment queue management
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* const { createInteractiveHandler } = await import('./interactive-mode.lib.mjs');
|
|
24
|
+
* const handler = createInteractiveHandler({ owner, repo, prNumber, $ });
|
|
25
|
+
* await handler.processEvent(jsonObject);
|
|
26
|
+
*
|
|
27
|
+
* @module interactive-mode.lib.mjs
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Configuration constants
|
|
32
|
+
const CONFIG = {
|
|
33
|
+
// Minimum time between comments to avoid rate limiting (in ms)
|
|
34
|
+
MIN_COMMENT_INTERVAL: 5000,
|
|
35
|
+
// Maximum lines to show before truncation kicks in
|
|
36
|
+
MAX_LINES_BEFORE_TRUNCATION: 50,
|
|
37
|
+
// Lines to keep at start when truncating
|
|
38
|
+
LINES_TO_KEEP_START: 20,
|
|
39
|
+
// Lines to keep at end when truncating
|
|
40
|
+
LINES_TO_KEEP_END: 20,
|
|
41
|
+
// Maximum JSON depth for raw JSON display
|
|
42
|
+
MAX_JSON_DEPTH: 10
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Truncate content in the middle, keeping start and end
|
|
47
|
+
* This helps show context while reducing size for large outputs
|
|
48
|
+
*
|
|
49
|
+
* @param {string} content - Content to potentially truncate
|
|
50
|
+
* @param {Object} options - Truncation options
|
|
51
|
+
* @param {number} [options.maxLines=50] - Maximum lines before truncation
|
|
52
|
+
* @param {number} [options.keepStart=20] - Lines to keep at start
|
|
53
|
+
* @param {number} [options.keepEnd=20] - Lines to keep at end
|
|
54
|
+
* @returns {string} Truncated content with ellipsis indicator
|
|
55
|
+
*/
|
|
56
|
+
const truncateMiddle = (content, options = {}) => {
|
|
57
|
+
const {
|
|
58
|
+
maxLines = CONFIG.MAX_LINES_BEFORE_TRUNCATION,
|
|
59
|
+
keepStart = CONFIG.LINES_TO_KEEP_START,
|
|
60
|
+
keepEnd = CONFIG.LINES_TO_KEEP_END
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
if (!content || typeof content !== 'string') {
|
|
64
|
+
return content || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = content.split('\n');
|
|
68
|
+
if (lines.length <= maxLines) {
|
|
69
|
+
return content;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startLines = lines.slice(0, keepStart);
|
|
73
|
+
const endLines = lines.slice(-keepEnd);
|
|
74
|
+
const removedCount = lines.length - keepStart - keepEnd;
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
...startLines,
|
|
78
|
+
'',
|
|
79
|
+
`... [${removedCount} lines truncated] ...`,
|
|
80
|
+
'',
|
|
81
|
+
...endLines
|
|
82
|
+
].join('\n');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Safely stringify JSON with depth limit and circular reference handling
|
|
87
|
+
*
|
|
88
|
+
* @param {any} obj - Object to stringify
|
|
89
|
+
* @param {number} [indent=2] - Indentation spaces
|
|
90
|
+
* @returns {string} Formatted JSON string
|
|
91
|
+
*/
|
|
92
|
+
const safeJsonStringify = (obj, indent = 2) => {
|
|
93
|
+
const seen = new WeakSet();
|
|
94
|
+
return JSON.stringify(obj, (key, value) => {
|
|
95
|
+
if (typeof value === 'object' && value !== null) {
|
|
96
|
+
if (seen.has(value)) {
|
|
97
|
+
return '[Circular]';
|
|
98
|
+
}
|
|
99
|
+
seen.add(value);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}, indent);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a collapsible section in GitHub markdown
|
|
107
|
+
*
|
|
108
|
+
* @param {string} summary - Summary text shown when collapsed
|
|
109
|
+
* @param {string} content - Content shown when expanded
|
|
110
|
+
* @param {boolean} [startOpen=false] - Whether to start expanded
|
|
111
|
+
* @returns {string} GitHub markdown details block
|
|
112
|
+
*/
|
|
113
|
+
const createCollapsible = (summary, content, startOpen = false) => {
|
|
114
|
+
const openAttr = startOpen ? ' open' : '';
|
|
115
|
+
return `<details${openAttr}>
|
|
116
|
+
<summary>${summary}</summary>
|
|
117
|
+
|
|
118
|
+
${content}
|
|
119
|
+
|
|
120
|
+
</details>`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a collapsible raw JSON section
|
|
125
|
+
* Always wraps data in an array for consistent merging
|
|
126
|
+
*
|
|
127
|
+
* @param {Object|Array} data - JSON data to display (will be wrapped in array if not already)
|
|
128
|
+
* @returns {string} Collapsible JSON block
|
|
129
|
+
*/
|
|
130
|
+
const createRawJsonSection = (data) => {
|
|
131
|
+
// Ensure data is always an array at root level for easier merging
|
|
132
|
+
const dataArray = Array.isArray(data) ? data : [data];
|
|
133
|
+
const jsonContent = truncateMiddle(safeJsonStringify(dataArray, 2), {
|
|
134
|
+
maxLines: 100,
|
|
135
|
+
keepStart: 40,
|
|
136
|
+
keepEnd: 40
|
|
137
|
+
});
|
|
138
|
+
return createCollapsible(
|
|
139
|
+
'📄 Raw JSON',
|
|
140
|
+
'```json\n' + jsonContent + '\n```'
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format duration from milliseconds to human-readable string
|
|
146
|
+
*
|
|
147
|
+
* @param {number} ms - Duration in milliseconds
|
|
148
|
+
* @returns {string} Formatted duration (e.g., "12m 7s")
|
|
149
|
+
*/
|
|
150
|
+
const formatDuration = (ms) => {
|
|
151
|
+
if (!ms || ms < 0) return 'unknown';
|
|
152
|
+
|
|
153
|
+
const seconds = Math.floor(ms / 1000);
|
|
154
|
+
const minutes = Math.floor(seconds / 60);
|
|
155
|
+
const hours = Math.floor(minutes / 60);
|
|
156
|
+
|
|
157
|
+
if (hours > 0) {
|
|
158
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
159
|
+
} else if (minutes > 0) {
|
|
160
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
161
|
+
} else {
|
|
162
|
+
return `${seconds}s`;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Format cost to USD string
|
|
168
|
+
*
|
|
169
|
+
* @param {number} cost - Cost in USD
|
|
170
|
+
* @returns {string} Formatted cost (e.g., "$1.60")
|
|
171
|
+
*/
|
|
172
|
+
const formatCost = (cost) => {
|
|
173
|
+
if (typeof cost !== 'number' || isNaN(cost)) return 'unknown';
|
|
174
|
+
return `$${cost.toFixed(2)}`;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Escape special markdown characters in text
|
|
179
|
+
*
|
|
180
|
+
* @param {string} text - Text to escape
|
|
181
|
+
* @returns {string} Escaped text
|
|
182
|
+
*/
|
|
183
|
+
const escapeMarkdown = (text) => {
|
|
184
|
+
if (!text || typeof text !== 'string') return '';
|
|
185
|
+
// Escape backticks that would break code blocks
|
|
186
|
+
return text.replace(/```/g, '\\`\\`\\`');
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get tool icon based on tool name
|
|
191
|
+
*
|
|
192
|
+
* @param {string} toolName - Name of the tool
|
|
193
|
+
* @returns {string} Emoji icon
|
|
194
|
+
*/
|
|
195
|
+
const getToolIcon = (toolName) => {
|
|
196
|
+
const icons = {
|
|
197
|
+
'Bash': '💻',
|
|
198
|
+
'Read': '📖',
|
|
199
|
+
'Write': '✏️',
|
|
200
|
+
'Edit': '📝',
|
|
201
|
+
'Glob': '🔍',
|
|
202
|
+
'Grep': '🔎',
|
|
203
|
+
'WebFetch': '🌐',
|
|
204
|
+
'WebSearch': '🔍',
|
|
205
|
+
'TodoWrite': '📋',
|
|
206
|
+
'Task': '🎯',
|
|
207
|
+
'NotebookEdit': '📓',
|
|
208
|
+
'default': '🔧'
|
|
209
|
+
};
|
|
210
|
+
return icons[toolName] || icons.default;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Creates an interactive mode handler for processing Claude CLI events
|
|
215
|
+
*
|
|
216
|
+
* @param {Object} options - Handler configuration
|
|
217
|
+
* @param {string} options.owner - Repository owner
|
|
218
|
+
* @param {string} options.repo - Repository name
|
|
219
|
+
* @param {number} options.prNumber - Pull request number
|
|
220
|
+
* @param {Function} options.$ - command-stream $ function
|
|
221
|
+
* @param {Function} options.log - Logging function
|
|
222
|
+
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
223
|
+
* @returns {Object} Handler object with event processing methods
|
|
224
|
+
*/
|
|
225
|
+
export const createInteractiveHandler = (options) => {
|
|
226
|
+
const { owner, repo, prNumber, $, log, verbose = false } = options;
|
|
227
|
+
|
|
228
|
+
// State tracking for the handler
|
|
229
|
+
const state = {
|
|
230
|
+
sessionId: null,
|
|
231
|
+
messageCount: 0,
|
|
232
|
+
toolUseCount: 0,
|
|
233
|
+
toolResultCount: 0,
|
|
234
|
+
lastCommentTime: 0,
|
|
235
|
+
// Queue stores objects with body and optional toolId for tracking
|
|
236
|
+
// { body: string, toolId?: string }
|
|
237
|
+
commentQueue: [],
|
|
238
|
+
isProcessing: false,
|
|
239
|
+
startTime: Date.now(),
|
|
240
|
+
// Track pending tool calls for merging with results
|
|
241
|
+
// Map of tool_use_id -> { commentId, toolData, inputDisplay, toolName, toolIcon, commentIdPromise, resolveCommentId }
|
|
242
|
+
// commentId may be null initially if comment is queued; commentIdPromise resolves when comment is posted
|
|
243
|
+
pendingToolCalls: new Map(),
|
|
244
|
+
// Simple map of tool_use_id -> { toolName, toolIcon } for standalone tool results
|
|
245
|
+
// This is preserved even after pendingToolCalls entry is deleted
|
|
246
|
+
toolUseRegistry: new Map()
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Post a comment to the PR (with rate limiting)
|
|
251
|
+
* @param {string} body - Comment body
|
|
252
|
+
* @param {string} [toolId] - Optional tool ID for tracking pending tool calls
|
|
253
|
+
* @returns {Promise<string|null>} Comment ID if successful, null if queued or failed
|
|
254
|
+
* @private
|
|
255
|
+
*/
|
|
256
|
+
const postComment = async (body, toolId = null) => {
|
|
257
|
+
if (!prNumber || !owner || !repo) {
|
|
258
|
+
if (verbose) {
|
|
259
|
+
await log('⚠️ Interactive mode: Cannot post comment - missing PR info', { verbose: true });
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const timeSinceLastComment = now - state.lastCommentTime;
|
|
266
|
+
|
|
267
|
+
if (timeSinceLastComment < CONFIG.MIN_COMMENT_INTERVAL) {
|
|
268
|
+
// Queue the comment for later with toolId for tracking
|
|
269
|
+
state.commentQueue.push({ body, toolId });
|
|
270
|
+
if (verbose) {
|
|
271
|
+
await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}`, { verbose: true });
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Post comment and capture the output to get the comment URL/ID
|
|
278
|
+
const result = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${body}`;
|
|
279
|
+
state.lastCommentTime = Date.now();
|
|
280
|
+
|
|
281
|
+
// Extract comment ID from the result (gh outputs the comment URL)
|
|
282
|
+
// Format: https://github.com/owner/repo/pull/123#issuecomment-1234567890
|
|
283
|
+
// Note: command-stream returns stdout as a Buffer, so we need to call .toString()
|
|
284
|
+
const output = result.stdout?.toString() || result.toString() || '';
|
|
285
|
+
const match = output.match(/issuecomment-(\d+)/);
|
|
286
|
+
const commentId = match ? match[1] : null;
|
|
287
|
+
|
|
288
|
+
if (verbose) {
|
|
289
|
+
await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''}`, { verbose: true });
|
|
290
|
+
}
|
|
291
|
+
return commentId;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (verbose) {
|
|
294
|
+
await log(`⚠️ Interactive mode: Failed to post comment: ${error.message}`, { verbose: true });
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Edit an existing comment on the PR
|
|
302
|
+
* @param {string} commentId - Comment ID to edit
|
|
303
|
+
* @param {string} body - New comment body
|
|
304
|
+
* @returns {Promise<boolean>} True if successful
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
const editComment = async (commentId, body) => {
|
|
308
|
+
if (!prNumber || !owner || !repo || !commentId) {
|
|
309
|
+
if (verbose) {
|
|
310
|
+
await log('⚠️ Interactive mode: Cannot edit comment - missing info', { verbose: true });
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
await $`gh api repos/${owner}/${repo}/issues/comments/${commentId} -X PATCH -f body=${body}`;
|
|
317
|
+
if (verbose) {
|
|
318
|
+
await log(`✅ Interactive mode: Comment ${commentId} updated`, { verbose: true });
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (verbose) {
|
|
323
|
+
await log(`⚠️ Interactive mode: Failed to edit comment: ${error.message}`, { verbose: true });
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Process queued comments
|
|
331
|
+
* When a queued comment is posted, if it has an associated toolId,
|
|
332
|
+
* update the pending tool call with the new comment ID
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
const processQueue = async () => {
|
|
336
|
+
if (state.isProcessing || state.commentQueue.length === 0) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
state.isProcessing = true;
|
|
341
|
+
|
|
342
|
+
while (state.commentQueue.length > 0) {
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
const timeSinceLastComment = now - state.lastCommentTime;
|
|
345
|
+
|
|
346
|
+
if (timeSinceLastComment < CONFIG.MIN_COMMENT_INTERVAL) {
|
|
347
|
+
// Wait until we can post
|
|
348
|
+
await new Promise(resolve => setTimeout(resolve, CONFIG.MIN_COMMENT_INTERVAL - timeSinceLastComment));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const queueItem = state.commentQueue.shift();
|
|
352
|
+
if (queueItem) {
|
|
353
|
+
const { body, toolId } = queueItem;
|
|
354
|
+
// Post the comment (don't pass toolId to avoid re-queueing)
|
|
355
|
+
const commentId = await postComment(body);
|
|
356
|
+
|
|
357
|
+
// If this was a tool use comment, update the pending call with the comment ID
|
|
358
|
+
if (toolId && commentId) {
|
|
359
|
+
const pendingCall = state.pendingToolCalls.get(toolId);
|
|
360
|
+
if (pendingCall) {
|
|
361
|
+
pendingCall.commentId = commentId;
|
|
362
|
+
// Resolve the promise so tool_result handler can proceed
|
|
363
|
+
if (pendingCall.resolveCommentId) {
|
|
364
|
+
pendingCall.resolveCommentId(commentId);
|
|
365
|
+
}
|
|
366
|
+
if (verbose) {
|
|
367
|
+
await log(`📋 Interactive mode: Updated pending tool call ${toolId} with comment ID ${commentId}`, { verbose: true });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
state.isProcessing = false;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Handle system.init event
|
|
379
|
+
* @param {Object} data - Event data
|
|
380
|
+
*/
|
|
381
|
+
const handleSystemInit = async (data) => {
|
|
382
|
+
state.sessionId = data.session_id;
|
|
383
|
+
state.startTime = Date.now();
|
|
384
|
+
|
|
385
|
+
const tools = data.tools || [];
|
|
386
|
+
const toolsList = tools.length > 0
|
|
387
|
+
? tools.map(t => `\`${t}\``).join(', ')
|
|
388
|
+
: '_No tools available_';
|
|
389
|
+
|
|
390
|
+
// Format MCP servers
|
|
391
|
+
const mcpServers = data.mcp_servers || [];
|
|
392
|
+
const mcpServersList = mcpServers.length > 0
|
|
393
|
+
? mcpServers.map(s => `\`${s.name}\` (${s.status || 'unknown'})`).join(', ')
|
|
394
|
+
: '_None_';
|
|
395
|
+
|
|
396
|
+
// Format slash commands
|
|
397
|
+
const slashCommands = data.slash_commands || [];
|
|
398
|
+
const slashCommandsList = slashCommands.length > 0
|
|
399
|
+
? slashCommands.map(c => `\`/${c}\``).join(', ')
|
|
400
|
+
: '_None_';
|
|
401
|
+
|
|
402
|
+
// Format agents
|
|
403
|
+
const agents = data.agents || [];
|
|
404
|
+
const agentsList = agents.length > 0
|
|
405
|
+
? agents.map(a => `\`${a}\``).join(', ')
|
|
406
|
+
: '_None_';
|
|
407
|
+
|
|
408
|
+
const comment = `## 🚀 Interactive session started
|
|
409
|
+
|
|
410
|
+
| Property | Value |
|
|
411
|
+
|----------|-------|
|
|
412
|
+
| **Session ID** | \`${data.session_id || 'unknown'}\` |
|
|
413
|
+
| **Model** | \`${data.model || 'unknown'}\` |
|
|
414
|
+
| **Claude Code Version** | \`${data.claude_code_version || 'unknown'}\` |
|
|
415
|
+
| **Permission Mode** | \`${data.permissionMode || 'unknown'}\` |
|
|
416
|
+
| **Working Directory** | \`${data.cwd || 'unknown'}\` |
|
|
417
|
+
| **Available Tools** | ${toolsList} |
|
|
418
|
+
| **MCP Servers** | ${mcpServersList} |
|
|
419
|
+
| **Slash Commands** | ${slashCommandsList} |
|
|
420
|
+
| **Agents** | ${agentsList} |
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
${createRawJsonSection(data)}`;
|
|
425
|
+
|
|
426
|
+
await postComment(comment);
|
|
427
|
+
|
|
428
|
+
if (verbose) {
|
|
429
|
+
await log(`🔌 Interactive mode: Session initialized (${state.sessionId})`, { verbose: true });
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Handle assistant text event
|
|
435
|
+
* @param {Object} data - Event data
|
|
436
|
+
* @param {string} text - The text content
|
|
437
|
+
*/
|
|
438
|
+
const handleAssistantText = async (data, text) => {
|
|
439
|
+
state.messageCount++;
|
|
440
|
+
|
|
441
|
+
// Truncate very long text responses
|
|
442
|
+
const displayText = truncateMiddle(text, {
|
|
443
|
+
maxLines: 80,
|
|
444
|
+
keepStart: 35,
|
|
445
|
+
keepEnd: 35
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Simple format: just the message and collapsed Raw JSON
|
|
449
|
+
const comment = `${displayText}
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
${createRawJsonSection(data)}`;
|
|
454
|
+
|
|
455
|
+
await postComment(comment);
|
|
456
|
+
|
|
457
|
+
if (verbose) {
|
|
458
|
+
await log(`💬 Interactive mode: Assistant text (${text.length} chars)`, { verbose: true });
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Handle assistant tool_use event
|
|
464
|
+
* @param {Object} data - Event data
|
|
465
|
+
* @param {Object} toolUse - Tool use details
|
|
466
|
+
*/
|
|
467
|
+
const handleToolUse = async (data, toolUse) => {
|
|
468
|
+
state.toolUseCount++;
|
|
469
|
+
|
|
470
|
+
const toolName = toolUse.name || 'Unknown';
|
|
471
|
+
const toolIcon = getToolIcon(toolName);
|
|
472
|
+
const toolId = toolUse.id || 'unknown';
|
|
473
|
+
|
|
474
|
+
// Register this tool use for potential standalone result rendering
|
|
475
|
+
state.toolUseRegistry.set(toolId, { toolName, toolIcon });
|
|
476
|
+
|
|
477
|
+
// Format tool input based on tool type
|
|
478
|
+
let inputDisplay = '';
|
|
479
|
+
const input = toolUse.input || {};
|
|
480
|
+
|
|
481
|
+
if (toolName === 'Bash' && input.command) {
|
|
482
|
+
const truncatedCommand = truncateMiddle(input.command, {
|
|
483
|
+
maxLines: 30,
|
|
484
|
+
keepStart: 12,
|
|
485
|
+
keepEnd: 12
|
|
486
|
+
});
|
|
487
|
+
inputDisplay = createCollapsible(
|
|
488
|
+
'📋 Executed command',
|
|
489
|
+
'```bash\n' + escapeMarkdown(truncatedCommand) + '\n```',
|
|
490
|
+
true
|
|
491
|
+
);
|
|
492
|
+
} else if (toolName === 'Read' && input.file_path) {
|
|
493
|
+
inputDisplay = `**File:** \`${input.file_path}\``;
|
|
494
|
+
if (input.offset || input.limit) {
|
|
495
|
+
inputDisplay += `\n**Range:** offset=${input.offset || 0}, limit=${input.limit || 'all'}`;
|
|
496
|
+
}
|
|
497
|
+
} else if (toolName === 'Write' && input.file_path) {
|
|
498
|
+
inputDisplay = `**File:** \`${input.file_path}\``;
|
|
499
|
+
if (input.content) {
|
|
500
|
+
const truncatedContent = truncateMiddle(input.content, {
|
|
501
|
+
maxLines: 30,
|
|
502
|
+
keepStart: 12,
|
|
503
|
+
keepEnd: 12
|
|
504
|
+
});
|
|
505
|
+
// Format content as diff with + prefix for added lines
|
|
506
|
+
const diffContent = truncatedContent.split('\n').map(line => `+ ${line}`).join('\n');
|
|
507
|
+
inputDisplay += '\n\n' + createCollapsible(
|
|
508
|
+
'📄 Content',
|
|
509
|
+
'```diff\n' + escapeMarkdown(diffContent) + '\n```'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
} else if (toolName === 'Edit' && input.file_path) {
|
|
513
|
+
inputDisplay = `**File:** \`${input.file_path}\``;
|
|
514
|
+
if (input.old_string && input.new_string) {
|
|
515
|
+
const truncatedOld = truncateMiddle(input.old_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
|
|
516
|
+
const truncatedNew = truncateMiddle(input.new_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
|
|
517
|
+
// Format as unified diff with - for removed lines and + for added lines
|
|
518
|
+
const diffOld = truncatedOld.split('\n').map(line => `- ${line}`).join('\n');
|
|
519
|
+
const diffNew = truncatedNew.split('\n').map(line => `+ ${line}`).join('\n');
|
|
520
|
+
inputDisplay += '\n\n' + createCollapsible(
|
|
521
|
+
'🔄 Change',
|
|
522
|
+
'```diff\n' + escapeMarkdown(diffOld + '\n' + diffNew) + '\n```',
|
|
523
|
+
true
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
} else if ((toolName === 'Glob' || toolName === 'Grep') && input.pattern) {
|
|
527
|
+
inputDisplay = `**Pattern:** \`${input.pattern}\``;
|
|
528
|
+
if (input.path) inputDisplay += `\n**Path:** \`${input.path}\``;
|
|
529
|
+
} else if (toolName === 'WebFetch' && input.url) {
|
|
530
|
+
inputDisplay = `**URL:** ${input.url}`;
|
|
531
|
+
if (input.prompt) inputDisplay += `\n**Prompt:** ${input.prompt}`;
|
|
532
|
+
} else if (toolName === 'WebSearch' && input.query) {
|
|
533
|
+
inputDisplay = `**Query:** ${input.query}`;
|
|
534
|
+
} else if (toolName === 'TodoWrite' && input.todos) {
|
|
535
|
+
// Show up to 30 todos, skip items in the middle if more
|
|
536
|
+
const MAX_TODOS_DISPLAY = 30;
|
|
537
|
+
const todos = input.todos;
|
|
538
|
+
let todosPreview;
|
|
539
|
+
|
|
540
|
+
if (todos.length <= MAX_TODOS_DISPLAY) {
|
|
541
|
+
// Show all todos if 30 or fewer
|
|
542
|
+
todosPreview = todos.map(t => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`).join('\n');
|
|
543
|
+
} else {
|
|
544
|
+
// Show first 15, "...and N more" in middle, then last 15
|
|
545
|
+
const KEEP_START = 15;
|
|
546
|
+
const KEEP_END = 15;
|
|
547
|
+
const skipped = todos.length - KEEP_START - KEEP_END;
|
|
548
|
+
|
|
549
|
+
const startTodos = todos.slice(0, KEEP_START).map(t => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`);
|
|
550
|
+
const endTodos = todos.slice(-KEEP_END).map(t => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`);
|
|
551
|
+
|
|
552
|
+
todosPreview = [
|
|
553
|
+
...startTodos,
|
|
554
|
+
`- _...and ${skipped} more_`,
|
|
555
|
+
...endTodos
|
|
556
|
+
].join('\n');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
inputDisplay = createCollapsible(
|
|
560
|
+
`📋 Todos (${todos.length} items)`,
|
|
561
|
+
todosPreview,
|
|
562
|
+
true
|
|
563
|
+
);
|
|
564
|
+
} else if (toolName === 'Task') {
|
|
565
|
+
inputDisplay = `**Description:** ${input.description || 'N/A'}`;
|
|
566
|
+
if (input.prompt) {
|
|
567
|
+
const truncatedPrompt = truncateMiddle(input.prompt, { maxLines: 20, keepStart: 8, keepEnd: 8 });
|
|
568
|
+
inputDisplay += '\n\n' + createCollapsible('📝 Prompt', truncatedPrompt);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
// Generic input display
|
|
572
|
+
const inputJson = truncateMiddle(safeJsonStringify(input, 2), {
|
|
573
|
+
maxLines: 30,
|
|
574
|
+
keepStart: 12,
|
|
575
|
+
keepEnd: 12
|
|
576
|
+
});
|
|
577
|
+
inputDisplay = createCollapsible(
|
|
578
|
+
'📥 Input',
|
|
579
|
+
'```json\n' + inputJson + '\n```'
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Post the tool use comment and store info for merging with result later
|
|
584
|
+
const comment = `## ${toolIcon} ${toolName} tool use
|
|
585
|
+
|
|
586
|
+
${inputDisplay}
|
|
587
|
+
|
|
588
|
+
_⏳ Waiting for result..._
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
${createRawJsonSection(data)}`;
|
|
593
|
+
|
|
594
|
+
// Create a promise that will resolve with the comment ID
|
|
595
|
+
// This handles both immediate posting and queued posting
|
|
596
|
+
let resolveCommentId;
|
|
597
|
+
const commentIdPromise = new Promise((resolve) => {
|
|
598
|
+
resolveCommentId = resolve;
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Store pending tool call BEFORE posting to ensure it's tracked
|
|
602
|
+
// even if the comment gets queued
|
|
603
|
+
state.pendingToolCalls.set(toolId, {
|
|
604
|
+
commentId: null, // Will be set when comment is actually posted
|
|
605
|
+
commentIdPromise,
|
|
606
|
+
resolveCommentId,
|
|
607
|
+
toolData: data,
|
|
608
|
+
inputDisplay,
|
|
609
|
+
toolName,
|
|
610
|
+
toolIcon
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Post the comment, passing toolId for queue tracking
|
|
614
|
+
const commentId = await postComment(comment, toolId);
|
|
615
|
+
|
|
616
|
+
// If posted immediately (not queued), update the pending call and resolve the promise
|
|
617
|
+
if (commentId) {
|
|
618
|
+
const pendingCall = state.pendingToolCalls.get(toolId);
|
|
619
|
+
if (pendingCall) {
|
|
620
|
+
pendingCall.commentId = commentId;
|
|
621
|
+
resolveCommentId(commentId);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// If queued (commentId is null), processQueue will update it later
|
|
625
|
+
|
|
626
|
+
if (verbose) {
|
|
627
|
+
await log(`🔧 Interactive mode: Tool use - ${toolName}${commentId ? ` (comment: ${commentId})` : ' (queued)'}`, { verbose: true });
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Handle user tool_result event
|
|
633
|
+
* @param {Object} data - Event data
|
|
634
|
+
* @param {Object} toolResult - Tool result details
|
|
635
|
+
*/
|
|
636
|
+
const handleToolResult = async (data, toolResult) => {
|
|
637
|
+
state.toolResultCount++;
|
|
638
|
+
|
|
639
|
+
const toolUseId = toolResult.tool_use_id || 'unknown';
|
|
640
|
+
const isError = toolResult.is_error || false;
|
|
641
|
+
const statusIcon = isError ? '❌' : '✅';
|
|
642
|
+
const statusText = isError ? 'Error' : 'Success';
|
|
643
|
+
|
|
644
|
+
// Get content - can be string or array
|
|
645
|
+
let content = '';
|
|
646
|
+
if (typeof toolResult.content === 'string') {
|
|
647
|
+
content = toolResult.content;
|
|
648
|
+
} else if (Array.isArray(toolResult.content)) {
|
|
649
|
+
content = toolResult.content.map(c => {
|
|
650
|
+
if (typeof c === 'string') return c;
|
|
651
|
+
if (c.type === 'text') return c.text || '';
|
|
652
|
+
return safeJsonStringify(c);
|
|
653
|
+
}).join('\n');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Truncate large outputs
|
|
657
|
+
const truncatedContent = truncateMiddle(content, {
|
|
658
|
+
maxLines: 60,
|
|
659
|
+
keepStart: 25,
|
|
660
|
+
keepEnd: 25
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Check if we have a pending tool call to merge with
|
|
664
|
+
const pendingCall = state.pendingToolCalls.get(toolUseId);
|
|
665
|
+
|
|
666
|
+
if (pendingCall) {
|
|
667
|
+
const { toolData, inputDisplay, toolName, toolIcon, commentIdPromise } = pendingCall;
|
|
668
|
+
let { commentId } = pendingCall;
|
|
669
|
+
|
|
670
|
+
// If comment ID is not yet available (comment was queued), wait for it
|
|
671
|
+
// But use a timeout to avoid blocking forever
|
|
672
|
+
if (!commentId && commentIdPromise) {
|
|
673
|
+
if (verbose) {
|
|
674
|
+
await log(`⏳ Interactive mode: Waiting for tool use comment to be posted (tool: ${toolUseId})`, { verbose: true });
|
|
675
|
+
}
|
|
676
|
+
// Wait for the comment to be posted (with 30 second timeout)
|
|
677
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 30000));
|
|
678
|
+
commentId = await Promise.race([commentIdPromise, timeoutPromise]);
|
|
679
|
+
|
|
680
|
+
if (!commentId) {
|
|
681
|
+
if (verbose) {
|
|
682
|
+
await log('⚠️ Interactive mode: Timeout waiting for tool use comment, posting result separately', { verbose: true });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (commentId) {
|
|
688
|
+
// Create merged comment with both call and result
|
|
689
|
+
const mergedComment = `## ${toolIcon} ${toolName} tool use
|
|
690
|
+
|
|
691
|
+
${inputDisplay}
|
|
692
|
+
|
|
693
|
+
${createCollapsible(
|
|
694
|
+
`📤 Output (${statusIcon} ${statusText.toLowerCase()})`,
|
|
695
|
+
'```\n' + escapeMarkdown(truncatedContent) + '\n```',
|
|
696
|
+
true
|
|
697
|
+
)}
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
${createRawJsonSection([toolData, data])}`;
|
|
702
|
+
|
|
703
|
+
// Edit the existing comment
|
|
704
|
+
const editSuccess = await editComment(commentId, mergedComment);
|
|
705
|
+
|
|
706
|
+
if (editSuccess) {
|
|
707
|
+
state.pendingToolCalls.delete(toolUseId);
|
|
708
|
+
if (verbose) {
|
|
709
|
+
await log(`📋 Interactive mode: Tool result merged into comment ${commentId} (${content.length} chars)`, { verbose: true });
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
// If edit failed, fall through to posting new comment
|
|
714
|
+
if (verbose) {
|
|
715
|
+
await log(`⚠️ Interactive mode: Failed to edit comment ${commentId}, posting result separately`, { verbose: true });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Clean up the pending call since we're posting separately
|
|
720
|
+
state.pendingToolCalls.delete(toolUseId);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Post as new comment if no pending call or edit failed
|
|
724
|
+
// Look up tool name from registry for better header
|
|
725
|
+
const registryEntry = state.toolUseRegistry.get(toolUseId);
|
|
726
|
+
const standaloneToolName = registryEntry?.toolName;
|
|
727
|
+
const standaloneToolIcon = registryEntry?.toolIcon || '🔧';
|
|
728
|
+
const standaloneHeader = standaloneToolName
|
|
729
|
+
? `${standaloneToolIcon} ${standaloneToolName} tool result`
|
|
730
|
+
: 'Tool result';
|
|
731
|
+
|
|
732
|
+
const comment = `## ${standaloneHeader}
|
|
733
|
+
|
|
734
|
+
${createCollapsible(
|
|
735
|
+
`📤 Output (${statusIcon} ${statusText.toLowerCase()})`,
|
|
736
|
+
'```\n' + escapeMarkdown(truncatedContent) + '\n```',
|
|
737
|
+
true
|
|
738
|
+
)}
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
${createRawJsonSection(data)}`;
|
|
743
|
+
|
|
744
|
+
await postComment(comment);
|
|
745
|
+
|
|
746
|
+
if (verbose) {
|
|
747
|
+
const contentLength = content.length;
|
|
748
|
+
await log(`📋 Interactive mode: Tool result posted as separate comment (${contentLength} chars)`, { verbose: true });
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Handle result event (session complete)
|
|
754
|
+
* @param {Object} data - Event data
|
|
755
|
+
*/
|
|
756
|
+
const handleResult = async (data) => {
|
|
757
|
+
const isError = data.is_error || false;
|
|
758
|
+
const statusIcon = isError ? '❌' : '✅';
|
|
759
|
+
const statusText = isError ? 'Session Failed' : 'Session Complete';
|
|
760
|
+
|
|
761
|
+
// Format result text
|
|
762
|
+
const resultText = data.result || '_No result message_';
|
|
763
|
+
const truncatedResult = truncateMiddle(resultText, {
|
|
764
|
+
maxLines: 50,
|
|
765
|
+
keepStart: 20,
|
|
766
|
+
keepEnd: 20
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Build stats table
|
|
770
|
+
let statsTable = '| Metric | Value |\n|--------|-------|\n';
|
|
771
|
+
statsTable += `| **Status** | ${statusText} |\n`;
|
|
772
|
+
statsTable += `| **Session ID** | \`${data.session_id || 'unknown'}\` |\n`;
|
|
773
|
+
|
|
774
|
+
if (data.duration_ms) {
|
|
775
|
+
statsTable += `| **Duration** | ${formatDuration(data.duration_ms)} |\n`;
|
|
776
|
+
}
|
|
777
|
+
if (data.duration_api_ms) {
|
|
778
|
+
statsTable += `| **API Time** | ${formatDuration(data.duration_api_ms)} |\n`;
|
|
779
|
+
}
|
|
780
|
+
if (data.num_turns) {
|
|
781
|
+
statsTable += `| **Turns** | ${data.num_turns} |\n`;
|
|
782
|
+
}
|
|
783
|
+
if (typeof data.total_cost_usd === 'number') {
|
|
784
|
+
statsTable += `| **Cost** | ${formatCost(data.total_cost_usd)} |\n`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Usage breakdown if available
|
|
788
|
+
let usageSection = '';
|
|
789
|
+
if (data.usage) {
|
|
790
|
+
const u = data.usage;
|
|
791
|
+
usageSection = '\n### 📊 Token Usage\n\n| Type | Count |\n|------|-------|\n';
|
|
792
|
+
if (u.input_tokens) usageSection += `| Input | ${u.input_tokens.toLocaleString()} |\n`;
|
|
793
|
+
if (u.output_tokens) usageSection += `| Output | ${u.output_tokens.toLocaleString()} |\n`;
|
|
794
|
+
if (u.cache_creation_input_tokens) usageSection += `| Cache Creation | ${u.cache_creation_input_tokens.toLocaleString()} |\n`;
|
|
795
|
+
if (u.cache_read_input_tokens) usageSection += `| Cache Read | ${u.cache_read_input_tokens.toLocaleString()} |\n`;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const comment = `## ${statusIcon} ${statusText}
|
|
799
|
+
|
|
800
|
+
${statsTable}
|
|
801
|
+
${usageSection}
|
|
802
|
+
|
|
803
|
+
### 📝 Result
|
|
804
|
+
|
|
805
|
+
${createCollapsible('View Result', truncatedResult, !isError)}
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
${createRawJsonSection(data)}`;
|
|
810
|
+
|
|
811
|
+
await postComment(comment);
|
|
812
|
+
|
|
813
|
+
if (verbose) {
|
|
814
|
+
await log(`🏁 Interactive mode: Session ${statusText.toLowerCase()}`, { verbose: true });
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Handle unrecognized event types
|
|
820
|
+
* @param {Object} data - Event data
|
|
821
|
+
*/
|
|
822
|
+
const handleUnrecognized = async (data) => {
|
|
823
|
+
const eventType = data.type || 'unknown';
|
|
824
|
+
const subtype = data.subtype ? `.${data.subtype}` : '';
|
|
825
|
+
|
|
826
|
+
const comment = `## ❓ Unrecognized Event: \`${eventType}${subtype}\`
|
|
827
|
+
|
|
828
|
+
This event type is not yet supported by interactive mode.
|
|
829
|
+
|
|
830
|
+
${createRawJsonSection(data)}`;
|
|
831
|
+
|
|
832
|
+
await postComment(comment);
|
|
833
|
+
|
|
834
|
+
if (verbose) {
|
|
835
|
+
await log(`❓ Interactive mode: Unrecognized event type: ${eventType}${subtype}`, { verbose: true });
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Process a single JSON event from Claude CLI
|
|
841
|
+
*
|
|
842
|
+
* @param {Object} data - Parsed JSON object from Claude CLI output
|
|
843
|
+
* @returns {Promise<void>}
|
|
844
|
+
*/
|
|
845
|
+
const processEvent = async (data) => {
|
|
846
|
+
if (!data || typeof data !== 'object') {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Handle events without type as unrecognized
|
|
851
|
+
if (!data.type) {
|
|
852
|
+
await handleUnrecognized(data);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
switch (data.type) {
|
|
857
|
+
case 'system':
|
|
858
|
+
if (data.subtype === 'init') {
|
|
859
|
+
await handleSystemInit(data);
|
|
860
|
+
} else {
|
|
861
|
+
// Unknown system subtype
|
|
862
|
+
await handleUnrecognized(data);
|
|
863
|
+
}
|
|
864
|
+
break;
|
|
865
|
+
|
|
866
|
+
case 'assistant':
|
|
867
|
+
if (data.message && data.message.content) {
|
|
868
|
+
const content = Array.isArray(data.message.content)
|
|
869
|
+
? data.message.content
|
|
870
|
+
: [data.message.content];
|
|
871
|
+
|
|
872
|
+
for (const item of content) {
|
|
873
|
+
if (item.type === 'text' && item.text) {
|
|
874
|
+
await handleAssistantText(data, item.text);
|
|
875
|
+
} else if (item.type === 'tool_use') {
|
|
876
|
+
await handleToolUse(data, item);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
|
|
882
|
+
case 'user':
|
|
883
|
+
if (data.message && data.message.content) {
|
|
884
|
+
const content = Array.isArray(data.message.content)
|
|
885
|
+
? data.message.content
|
|
886
|
+
: [data.message.content];
|
|
887
|
+
|
|
888
|
+
for (const item of content) {
|
|
889
|
+
if (item.type === 'tool_result') {
|
|
890
|
+
await handleToolResult(data, item);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
break;
|
|
895
|
+
|
|
896
|
+
case 'result':
|
|
897
|
+
await handleResult(data);
|
|
898
|
+
break;
|
|
899
|
+
|
|
900
|
+
default:
|
|
901
|
+
await handleUnrecognized(data);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Process any queued comments
|
|
905
|
+
await processQueue();
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Flush any remaining queued comments
|
|
910
|
+
* Should be called at the end of a session
|
|
911
|
+
*
|
|
912
|
+
* @returns {Promise<void>}
|
|
913
|
+
*/
|
|
914
|
+
const flush = async () => {
|
|
915
|
+
await processQueue();
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Get current handler state (for debugging)
|
|
920
|
+
*
|
|
921
|
+
* @returns {Object} Current state
|
|
922
|
+
*/
|
|
923
|
+
const getState = () => ({ ...state });
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
processEvent,
|
|
927
|
+
flush,
|
|
928
|
+
getState,
|
|
929
|
+
// Expose individual handlers for testing
|
|
930
|
+
_handlers: {
|
|
931
|
+
handleSystemInit,
|
|
932
|
+
handleAssistantText,
|
|
933
|
+
handleToolUse,
|
|
934
|
+
handleToolResult,
|
|
935
|
+
handleResult,
|
|
936
|
+
handleUnrecognized
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Check if interactive mode is supported for the given tool
|
|
943
|
+
*
|
|
944
|
+
* @param {string} tool - Tool name (claude, opencode, codex)
|
|
945
|
+
* @returns {boolean} Whether interactive mode is supported
|
|
946
|
+
*/
|
|
947
|
+
export const isInteractiveModeSupported = (tool) => {
|
|
948
|
+
// Currently only supported for Claude
|
|
949
|
+
return tool === 'claude';
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Validate interactive mode configuration
|
|
954
|
+
*
|
|
955
|
+
* @param {Object} argv - Parsed command line arguments
|
|
956
|
+
* @param {Function} log - Logging function
|
|
957
|
+
* @returns {Promise<boolean>} Whether configuration is valid
|
|
958
|
+
*/
|
|
959
|
+
export const validateInteractiveModeConfig = async (argv, log) => {
|
|
960
|
+
if (!argv.interactiveMode) {
|
|
961
|
+
return true; // Not enabled, nothing to validate
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Check tool support
|
|
965
|
+
if (!isInteractiveModeSupported(argv.tool)) {
|
|
966
|
+
await log(`⚠️ --interactive-mode is only supported for --tool claude (current: ${argv.tool})`, { level: 'warning' });
|
|
967
|
+
await log(' Interactive mode will be disabled for this session.', { level: 'warning' });
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Check PR requirement
|
|
972
|
+
// Note: This should be called after PR is created/determined
|
|
973
|
+
// The actual PR number check happens during execution
|
|
974
|
+
|
|
975
|
+
await log('🔌 Interactive mode: ENABLED (experimental)', { level: 'info' });
|
|
976
|
+
await log(' Claude output will be posted as PR comments in real-time.', { level: 'info' });
|
|
977
|
+
|
|
978
|
+
return true;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// Export utilities for testing
|
|
982
|
+
export const utils = {
|
|
983
|
+
truncateMiddle,
|
|
984
|
+
safeJsonStringify,
|
|
985
|
+
createCollapsible,
|
|
986
|
+
createRawJsonSection,
|
|
987
|
+
formatDuration,
|
|
988
|
+
formatCost,
|
|
989
|
+
escapeMarkdown,
|
|
990
|
+
getToolIcon,
|
|
991
|
+
CONFIG
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// Export all functions
|
|
995
|
+
export default {
|
|
996
|
+
createInteractiveHandler,
|
|
997
|
+
isInteractiveModeSupported,
|
|
998
|
+
validateInteractiveModeConfig,
|
|
999
|
+
utils
|
|
1000
|
+
};
|