@co0ontty/wand 0.2.0 → 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.
@@ -0,0 +1,649 @@
1
+ /**
2
+ * ClaudePtyBridge - PTY output parsing and event bridge
3
+ *
4
+ * Transforms raw PTY output into structured events for WebSocket broadcast.
5
+ * Maintains two parallel output streams:
6
+ * 1. Raw output for terminal view (passthrough)
7
+ * 2. Structured messages for chat view (parsed)
8
+ */
9
+ import { EventEmitter } from "node:events";
10
+ // ── Constants ──
11
+ const OUTPUT_MAX_SIZE = 120000;
12
+ const SESSION_ID_WINDOW_SIZE = 4096;
13
+ const PERMISSION_WINDOW_SIZE = 800;
14
+ const CLAUDE_SESSION_ID_PATTERN = /"session_id"\s*:\s*"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/i;
15
+ // Patterns for permission detection
16
+ const PERMISSION_PATTERNS = [
17
+ /(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
18
+ /\[(?:y|yes)\s*\/\s*(?:n|no)\]/i,
19
+ /\((?:y|yes)\s*\/\s*(?:n|no)\)/i,
20
+ /\((?:y|yes)\s*\/\s*(?:n|no)\s*\/\s*always\)/i,
21
+ /\bcontinue\?\s*(?:\((?:y|yes)\s*\/\s*(?:n|no)\))?/i,
22
+ /\bare you sure\??/i,
23
+ /\bdo you want to continue\??/i,
24
+ /\bdo you want to (?:create|write|delete|modify|execute)/i,
25
+ /\bconfirm(?:\s+execution|\s+changes|\s+action)?\??/i,
26
+ /\bproceed\??/i,
27
+ /\benter to confirm\b/i,
28
+ /\bwould you like to\b/i,
29
+ /\bshall i\b/i,
30
+ /\bcan i\b/i,
31
+ /\bpermission\b/i,
32
+ /\bgrant\b.*\bpermission\b/i
33
+ ];
34
+ // ── Helper Functions ──
35
+ /** Append text to a windowed buffer, trimming from start if over max size. */
36
+ function appendWindow(buffer, chunk, maxSize) {
37
+ const next = buffer + chunk;
38
+ return next.length > maxSize ? next.slice(-maxSize) : next;
39
+ }
40
+ /** Normalize prompt text for permission detection */
41
+ function normalizePromptText(value) {
42
+ return value
43
+ .replace(/\u001b\[(\d+)C/g, (_match, count) => " ".repeat(Number(count) || 1))
44
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "")
45
+ .replace(/\r/g, "\n")
46
+ .replace(/[ \t]+/g, " ")
47
+ .replace(/\n+/g, "\n")
48
+ .trim();
49
+ }
50
+ /** Normalize PTY output (fix line endings) */
51
+ function normalizePtyOutput(value) {
52
+ return value.replace(/\r\r\n/g, "\r\n");
53
+ }
54
+ /**
55
+ * ClaudePtyBridge transforms raw PTY output into structured events.
56
+ *
57
+ * Events emitted:
58
+ * - "output.raw" - Raw PTY output for terminal view
59
+ * - "output.chat" - Structured chat content update
60
+ * - "chat.turn" - Conversation turn completed
61
+ * - "permission.prompt" - Permission request detected
62
+ * - "permission.resolved" - Permission resolved
63
+ * - "session.id" - Claude session ID captured
64
+ * - "task" - Task info update
65
+ * - "ended" - Session ended
66
+ */
67
+ export class ClaudePtyBridge extends EventEmitter {
68
+ sessionId;
69
+ // Output state
70
+ rawOutput;
71
+ messages;
72
+ // Chat parsing state
73
+ chatState;
74
+ // Permission detection state
75
+ permissionState;
76
+ // Session ID capture
77
+ sessionIdWindow;
78
+ claudeSessionId = null;
79
+ // Task tracking
80
+ currentTask = null;
81
+ taskDebounceTimer = null;
82
+ lastEmittedTask = null;
83
+ // Options
84
+ isClaudeCommand;
85
+ autoApprove;
86
+ approvalPolicy;
87
+ ptyWrite;
88
+ /** Set to true once onExit() has been called; guards against post-exit method calls */
89
+ _exited = false;
90
+ // Permission memory for "approve_turn" policy
91
+ rememberedScopes = new Set();
92
+ rememberedTargets = new Set();
93
+ constructor(options) {
94
+ super();
95
+ this.sessionId = options.sessionId;
96
+ this.messages = options.initialMessages ?? [];
97
+ this.rawOutput = options.initialOutput ?? "";
98
+ this.isClaudeCommand = options.isClaudeCommand ?? false;
99
+ this.autoApprove = options.autoApprove ?? false;
100
+ this.approvalPolicy = options.approvalPolicy ?? "ask-every-time";
101
+ this.ptyWrite = options.ptyWrite ?? null;
102
+ this.chatState = {
103
+ phase: "idle",
104
+ buffer: "",
105
+ lastUserInput: "",
106
+ echoSkipped: false,
107
+ assistantIndex: null,
108
+ };
109
+ this.permissionState = {
110
+ window: "",
111
+ isBlocked: false,
112
+ lastPrompt: null,
113
+ lastScope: null,
114
+ lastTarget: null,
115
+ lastAutoConfirmAt: 0,
116
+ };
117
+ this.sessionIdWindow = "";
118
+ }
119
+ // ── Core API ──
120
+ /**
121
+ * Process a raw PTY chunk.
122
+ * Emits events via EventEmitter.
123
+ */
124
+ processChunk(chunk) {
125
+ // Guard against post-exit calls (e.g., late PTY drain data after onExit)
126
+ if (this._exited)
127
+ return;
128
+ // 1. Append to raw output
129
+ this.rawOutput = appendWindow(this.rawOutput, normalizePtyOutput(chunk), OUTPUT_MAX_SIZE);
130
+ // 2. Emit raw output event (for terminal view)
131
+ this.emitEvent({
132
+ type: "output.raw",
133
+ sessionId: this.sessionId,
134
+ timestamp: Date.now(),
135
+ data: { chunk, output: this.rawOutput },
136
+ });
137
+ // 3. Session ID capture
138
+ this.captureSessionId(chunk);
139
+ // 4. Permission detection
140
+ this.detectPermission(chunk);
141
+ // 5. Chat parsing (if responding)
142
+ if (this.chatState.phase === "responding") {
143
+ this.chatState.buffer += chunk;
144
+ this.parseChatResponse();
145
+ }
146
+ }
147
+ /**
148
+ * Called when user sends input.
149
+ * Starts tracking a new assistant response.
150
+ */
151
+ onUserInput(input) {
152
+ // Guard against post-exit calls
153
+ if (this._exited)
154
+ return;
155
+ // Filter out non-chat input (control chars, etc.)
156
+ if (!this.isRealChatInput(input)) {
157
+ process.stderr.write(`[Bridge] Input filtered as non-chat: ${input.substring(0, 50)}\n`);
158
+ return;
159
+ }
160
+ const cleanInput = input.replace(/[\r\n]+$/, "").trim();
161
+ process.stderr.write(`[Bridge] Starting chat tracking for: ${cleanInput.substring(0, 50)}\n`);
162
+ // Add user message
163
+ this.messages.push({
164
+ role: "user",
165
+ content: [{ type: "text", text: cleanInput }],
166
+ });
167
+ // Add assistant placeholder for streaming
168
+ this.messages.push({
169
+ role: "assistant",
170
+ content: [],
171
+ });
172
+ // Initialize chat state
173
+ this.chatState = {
174
+ phase: "responding",
175
+ buffer: "",
176
+ lastUserInput: cleanInput,
177
+ echoSkipped: false,
178
+ assistantIndex: this.messages.length - 1,
179
+ };
180
+ // Emit chat state update
181
+ this.emitEvent({
182
+ type: "output.chat",
183
+ sessionId: this.sessionId,
184
+ timestamp: Date.now(),
185
+ data: {
186
+ messages: this.messages,
187
+ streamingIndex: this.chatState.assistantIndex,
188
+ isResponding: true,
189
+ },
190
+ });
191
+ }
192
+ /**
193
+ * Called when PTY process exits.
194
+ * Finalizes any pending response.
195
+ */
196
+ onExit(exitCode) {
197
+ // Mark as exited FIRST — prevents any concurrent or subsequent method calls
198
+ this._exited = true;
199
+ if (this.chatState.phase === "responding") {
200
+ this.chatState.echoSkipped = true; // Force skip echo on exit
201
+ this.finalizeResponse();
202
+ }
203
+ // Clear task debounce timer
204
+ if (this.taskDebounceTimer) {
205
+ clearTimeout(this.taskDebounceTimer);
206
+ this.taskDebounceTimer = null;
207
+ }
208
+ // Clear permission state — prevents stale blocked state after exit
209
+ this.permissionState.isBlocked = false;
210
+ this.permissionState.lastPrompt = null;
211
+ this.permissionState.lastScope = null;
212
+ this.permissionState.lastTarget = null;
213
+ this.emitEvent({
214
+ type: "ended",
215
+ sessionId: this.sessionId,
216
+ timestamp: Date.now(),
217
+ data: { exitCode },
218
+ });
219
+ }
220
+ // ── State Accessors ──
221
+ getMessages() {
222
+ return this.messages;
223
+ }
224
+ getRawOutput() {
225
+ return this.rawOutput;
226
+ }
227
+ getClaudeSessionId() {
228
+ return this.claudeSessionId;
229
+ }
230
+ isPermissionBlocked() {
231
+ return this.permissionState.isBlocked;
232
+ }
233
+ getPermissionState() {
234
+ return this.permissionState;
235
+ }
236
+ getCurrentTask() {
237
+ return this.currentTask;
238
+ }
239
+ /**
240
+ * Set the PTY write function for sending approval input.
241
+ */
242
+ setPtyWrite(fn) {
243
+ this.ptyWrite = fn;
244
+ }
245
+ // ── Permission Resolution ──
246
+ /**
247
+ * Resolve the current permission prompt.
248
+ * @param resolution - How to resolve the permission
249
+ */
250
+ resolvePermission(resolution) {
251
+ // Guard against post-exit calls — ptyWrite may be stale after exit
252
+ if (this._exited)
253
+ return;
254
+ if (!this.permissionState.isBlocked)
255
+ return;
256
+ // Handle "approve_turn" - remember this scope/target for the rest of the turn
257
+ if (resolution === "approve_turn") {
258
+ if (this.permissionState.lastScope) {
259
+ this.rememberedScopes.add(this.permissionState.lastScope);
260
+ }
261
+ if (this.permissionState.lastTarget) {
262
+ this.rememberedTargets.add(this.permissionState.lastTarget);
263
+ }
264
+ }
265
+ // Send approval/denial to PTY
266
+ if (this.ptyWrite) {
267
+ if (resolution === "deny") {
268
+ this.ptyWrite("n\r");
269
+ }
270
+ else {
271
+ this.ptyWrite("\r");
272
+ }
273
+ }
274
+ // Clear state
275
+ this.permissionState.isBlocked = false;
276
+ this.permissionState.lastPrompt = null;
277
+ this.permissionState.lastScope = null;
278
+ this.permissionState.lastTarget = null;
279
+ this.emitEvent({
280
+ type: "permission.resolved",
281
+ sessionId: this.sessionId,
282
+ timestamp: Date.now(),
283
+ data: { resolution },
284
+ });
285
+ }
286
+ /**
287
+ * Check if a permission scope/target should be auto-approved based on remembered decisions.
288
+ */
289
+ shouldAutoApprove(scope, target) {
290
+ if (this.rememberedScopes.has(scope))
291
+ return true;
292
+ if (target && this.rememberedTargets.has(target))
293
+ return true;
294
+ return false;
295
+ }
296
+ /**
297
+ * Clear remembered permissions (call at the start of a new turn).
298
+ */
299
+ clearRememberedPermissions() {
300
+ this.rememberedScopes.clear();
301
+ this.rememberedTargets.clear();
302
+ }
303
+ /**
304
+ * Clear permission blocked state (called when permission is resolved externally).
305
+ */
306
+ clearPermissionBlocked() {
307
+ this.permissionState.isBlocked = false;
308
+ this.permissionState.lastPrompt = null;
309
+ this.permissionState.lastScope = null;
310
+ this.permissionState.lastTarget = null;
311
+ }
312
+ // ── Private Implementation ──
313
+ emitEvent(event) {
314
+ this.emit("event", event);
315
+ }
316
+ isRealChatInput(input) {
317
+ const trimmed = input.replace(/[\r\n]+$/, "").trim();
318
+ // Empty or whitespace-only
319
+ if (!trimmed)
320
+ return false;
321
+ // Single control character (Ctrl+C, Ctrl+D, etc.)
322
+ if (trimmed.length === 1 && trimmed.charCodeAt(0) < 32)
323
+ return false;
324
+ // ANSI escape sequences (arrow keys, etc.)
325
+ if (trimmed.startsWith("\x1b"))
326
+ return false;
327
+ // Single "y" or "n" — likely auto-confirm response
328
+ if (/^[yn]$/i.test(trimmed))
329
+ return false;
330
+ // Just Enter/CR
331
+ if (trimmed === "\r" || trimmed === "\n")
332
+ return false;
333
+ return true;
334
+ }
335
+ captureSessionId(chunk) {
336
+ if (this.claudeSessionId)
337
+ return;
338
+ this.sessionIdWindow = appendWindow(this.sessionIdWindow, chunk, SESSION_ID_WINDOW_SIZE);
339
+ const match = CLAUDE_SESSION_ID_PATTERN.exec(this.sessionIdWindow);
340
+ if (match?.[1]) {
341
+ this.claudeSessionId = match[1];
342
+ this.emitEvent({
343
+ type: "session.id",
344
+ sessionId: this.sessionId,
345
+ timestamp: Date.now(),
346
+ data: { claudeSessionId: match[1] },
347
+ });
348
+ }
349
+ }
350
+ detectPermission(chunk) {
351
+ if (!this.isClaudeCommand)
352
+ return;
353
+ this.permissionState.window = appendWindow(this.permissionState.window, chunk, PERMISSION_WINDOW_SIZE);
354
+ const normalized = normalizePromptText(this.permissionState.window);
355
+ const blocked = this.isPermissionPromptDetected(normalized);
356
+ if (this.permissionState.isBlocked === blocked)
357
+ return;
358
+ this.permissionState.isBlocked = blocked;
359
+ if (blocked) {
360
+ const prompt = this.extractPromptText(normalized);
361
+ const target = this.extractPermissionTarget(normalized);
362
+ const scope = this.inferScope(normalized, target);
363
+ this.permissionState.lastPrompt = prompt;
364
+ this.permissionState.lastScope = scope;
365
+ this.permissionState.lastTarget = target ?? null;
366
+ // Check if we should auto-approve
367
+ const shouldAutoApprove = this.autoApprove || this.shouldAutoApprove(scope, target);
368
+ if (shouldAutoApprove) {
369
+ // Debounce auto-confirm to avoid rapid repeats
370
+ const now = Date.now();
371
+ if (now - this.permissionState.lastAutoConfirmAt < 500)
372
+ return;
373
+ this.permissionState.lastAutoConfirmAt = now;
374
+ process.stderr.write(`[wand] Auto-confirming permission for ${scope}${target ? `: ${target}` : ""}\n`);
375
+ // Send approval to PTY
376
+ if (this.ptyWrite) {
377
+ this.ptyWrite("\r");
378
+ }
379
+ // Clear blocked state immediately
380
+ this.permissionState.isBlocked = false;
381
+ this.permissionState.lastPrompt = null;
382
+ this.permissionState.lastScope = null;
383
+ this.permissionState.lastTarget = null;
384
+ this.emitEvent({
385
+ type: "permission.resolved",
386
+ sessionId: this.sessionId,
387
+ timestamp: Date.now(),
388
+ data: { resolution: "approve_once", autoApproved: true },
389
+ });
390
+ }
391
+ else {
392
+ // Emit permission prompt event for UI to handle
393
+ this.emitEvent({
394
+ type: "permission.prompt",
395
+ sessionId: this.sessionId,
396
+ timestamp: Date.now(),
397
+ data: { prompt, scope, target },
398
+ });
399
+ }
400
+ }
401
+ else {
402
+ this.permissionState.lastPrompt = null;
403
+ this.permissionState.lastScope = null;
404
+ this.permissionState.lastTarget = null;
405
+ this.emitEvent({
406
+ type: "permission.resolved",
407
+ sessionId: this.sessionId,
408
+ timestamp: Date.now(),
409
+ data: {},
410
+ });
411
+ }
412
+ }
413
+ isPermissionPromptDetected(normalized) {
414
+ return (/\bdo you want to\b/i.test(normalized) ||
415
+ /\bgrant\b.*\bpermission\b/i.test(normalized) ||
416
+ /\bhaven't granted\b/i.test(normalized) ||
417
+ /\bpermission\b/i.test(normalized) ||
418
+ /\benter to confirm\b/i.test(normalized));
419
+ }
420
+ extractPromptText(normalized) {
421
+ // Return a snippet around the permission prompt
422
+ const match = normalized.match(/.{0,100}(?:do you want to|permission|grant|enter to confirm).{0,100}/i);
423
+ return match?.[0] ?? normalized.slice(-100);
424
+ }
425
+ extractPermissionTarget(normalized) {
426
+ const match = normalized.match(/write to\s+([^,.\n]+)/i)
427
+ || normalized.match(/modify\s+([^,.\n]+)/i)
428
+ || normalized.match(/delete\s+([^,.\n]+)/i)
429
+ || normalized.match(/execute\s+([^,.\n]+)/i);
430
+ return match?.[1]?.trim();
431
+ }
432
+ inferScope(normalized, target) {
433
+ const lower = normalized.toLowerCase();
434
+ if (lower.includes("write") || lower.includes("redirection") || lower.includes("output redirection")) {
435
+ return "write_file";
436
+ }
437
+ if (lower.includes("network") || lower.includes("web") || lower.includes("fetch") || lower.includes("url")) {
438
+ return "network";
439
+ }
440
+ if (lower.includes("command") || lower.includes("bash") || lower.includes("execute")) {
441
+ return "run_command";
442
+ }
443
+ return "unknown";
444
+ }
445
+ parseChatResponse() {
446
+ const clean = this.stripAnsi(this.chatState.buffer);
447
+ if (!this.chatState.echoSkipped) {
448
+ const echoEndIndex = this.findEchoEndIndex(clean, this.chatState.lastUserInput);
449
+ if (echoEndIndex <= 0) {
450
+ return;
451
+ }
452
+ this.chatState.echoSkipped = true;
453
+ this.chatState.buffer = clean.slice(echoEndIndex);
454
+ process.stderr.write(`[Bridge] Echo skipped, remaining length: ${this.chatState.buffer.length}\n`);
455
+ }
456
+ if (this.detectCompletion(this.chatState.buffer)) {
457
+ process.stderr.write("[Bridge] Completion detected, finalizing\n");
458
+ this.finalizeResponse();
459
+ return;
460
+ }
461
+ this.updateAssistantContent();
462
+ }
463
+ detectCompletion(clean) {
464
+ const lines = clean.split("\n").map((line) => line.trimEnd());
465
+ for (let i = lines.length - 1; i >= 0; i--) {
466
+ const trimmed = lines[i].trim();
467
+ if (!trimmed) {
468
+ continue;
469
+ }
470
+ if (this.isStatusLine(trimmed)) {
471
+ continue;
472
+ }
473
+ if (trimmed.startsWith("❯")) {
474
+ const afterPrompt = trimmed.slice(1).trim();
475
+ return afterPrompt.length === 0 || afterPrompt.startsWith("Try");
476
+ }
477
+ return false;
478
+ }
479
+ return false;
480
+ }
481
+ updateAssistantContent() {
482
+ const idx = this.chatState.assistantIndex;
483
+ if (idx === null)
484
+ return;
485
+ const text = this.cleanForChat(this.chatState.buffer);
486
+ if (text) {
487
+ this.messages[idx].content = [{ type: "text", text }];
488
+ }
489
+ this.emitEvent({
490
+ type: "output.chat",
491
+ sessionId: this.sessionId,
492
+ timestamp: Date.now(),
493
+ data: {
494
+ messages: this.messages,
495
+ streamingIndex: idx,
496
+ isResponding: true,
497
+ },
498
+ });
499
+ }
500
+ finalizeResponse() {
501
+ const idx = this.chatState.assistantIndex;
502
+ if (idx !== null) {
503
+ const text = this.cleanForChat(this.chatState.buffer);
504
+ if (text) {
505
+ this.messages[idx].content = [{ type: "text", text }];
506
+ }
507
+ else if (this.messages[idx].content.length === 0) {
508
+ this.messages.splice(idx, 1);
509
+ }
510
+ }
511
+ // Emit turn completed
512
+ const lastTurn = this.messages[this.messages.length - 1];
513
+ if (lastTurn?.role === "assistant" && lastTurn.content.length > 0) {
514
+ this.emitEvent({
515
+ type: "chat.turn",
516
+ sessionId: this.sessionId,
517
+ timestamp: Date.now(),
518
+ data: { turn: lastTurn, messages: this.messages },
519
+ });
520
+ }
521
+ // Reset state
522
+ this.chatState = {
523
+ phase: "idle",
524
+ buffer: "",
525
+ lastUserInput: "",
526
+ echoSkipped: false,
527
+ assistantIndex: null,
528
+ };
529
+ // Emit idle state
530
+ this.emitEvent({
531
+ type: "output.chat",
532
+ sessionId: this.sessionId,
533
+ timestamp: Date.now(),
534
+ data: { messages: this.messages, isResponding: false },
535
+ });
536
+ }
537
+ // ── Text Processing Utilities ──
538
+ stripAnsi(text) {
539
+ // eslint-disable-next-line no-control-regex
540
+ return text
541
+ .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "") // CSI sequences
542
+ .replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "") // OSC sequences
543
+ .replace(/\x1b[><=ePX^_]/g, "") // Single-char escapes
544
+ // eslint-disable-next-line no-control-regex
545
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "") // Control chars (keep \t \n \r)
546
+ .replace(/\r\n?/g, "\n");
547
+ }
548
+ /**
549
+ * Find the end index of the echoed user input in the PTY buffer.
550
+ * The echo may contain ANSI codes between characters.
551
+ * Returns the index after the last character of the echo.
552
+ */
553
+ findEchoEndIndex(buffer, userInput) {
554
+ // Keep alphanumeric and common symbols for matching
555
+ const inputChars = userInput.replace(/[^a-zA-Z0-9+=?!\-]/g, "");
556
+ if (inputChars.length === 0)
557
+ return 0;
558
+ let matchedChars = 0;
559
+ let endIndex = 0;
560
+ for (let i = 0; i < buffer.length && matchedChars < inputChars.length; i++) {
561
+ const ch = buffer[i];
562
+ // Check if this printable char matches the next expected char
563
+ if (/[a-zA-Z0-9+=?!\-]/.test(ch) && ch.toLowerCase() === inputChars[matchedChars].toLowerCase()) {
564
+ matchedChars++;
565
+ endIndex = i + 1;
566
+ }
567
+ // Skip ANSI codes and other non-matching characters
568
+ }
569
+ // Look for a newline or prompt marker after the echo
570
+ for (let i = endIndex; i < buffer.length && i < endIndex + 50; i++) {
571
+ if (buffer[i] === "\n" || buffer[i] === "\r") {
572
+ endIndex = i + 1;
573
+ break;
574
+ }
575
+ }
576
+ return matchedChars === inputChars.length ? endIndex : 0;
577
+ }
578
+ isStatusLine(line) {
579
+ if (!line)
580
+ return false;
581
+ if (line.startsWith("────"))
582
+ return true;
583
+ if (line === "❯")
584
+ return true;
585
+ if (line.includes("esc to interrupt"))
586
+ return true;
587
+ if (line.includes("Claude Code v"))
588
+ return true;
589
+ if (line.includes("Failed to install Anthropic"))
590
+ return true;
591
+ if (line.includes("Claude Code has switched"))
592
+ return true;
593
+ if (line.includes("[wand]"))
594
+ return true;
595
+ if (line.includes("Captured Claude session ID"))
596
+ return true;
597
+ if (line.includes("ctrl+g"))
598
+ return true;
599
+ if (line.includes("/effort"))
600
+ return true;
601
+ if (line.includes("? for shortcuts"))
602
+ return true;
603
+ if (line.includes("auto mode is unavailable"))
604
+ return true;
605
+ if (/MCP server.*failed/i.test(line))
606
+ return true;
607
+ if (line.includes("Germinating") || line.includes("Doodling") || line.includes("Brewing"))
608
+ return true;
609
+ if (line.includes("Permissions") && line.includes("mode"))
610
+ return true;
611
+ if (line.startsWith("●") && line.includes("·"))
612
+ return true;
613
+ if (line.startsWith("[>") || line.startsWith("[<"))
614
+ return true;
615
+ return false;
616
+ }
617
+ cleanForChat(raw) {
618
+ const text = this.stripAnsi(raw);
619
+ const lines = text.split("\n");
620
+ const cleanLines = [];
621
+ for (const rawLine of lines) {
622
+ const trimmed = rawLine.trim();
623
+ if (!trimmed) {
624
+ if (cleanLines.length > 0 && cleanLines[cleanLines.length - 1] !== "") {
625
+ cleanLines.push("");
626
+ }
627
+ continue;
628
+ }
629
+ if (trimmed === this.chatState.lastUserInput.trim()) {
630
+ continue;
631
+ }
632
+ if (this.isStatusLine(trimmed)) {
633
+ continue;
634
+ }
635
+ if (trimmed.startsWith("❯")) {
636
+ continue;
637
+ }
638
+ let normalized = trimmed;
639
+ if (normalized.startsWith("●") || normalized.startsWith("⏺")) {
640
+ normalized = normalized.slice(1).trimStart();
641
+ }
642
+ cleanLines.push(normalized);
643
+ }
644
+ while (cleanLines.length > 0 && cleanLines[cleanLines.length - 1] === "") {
645
+ cleanLines.pop();
646
+ }
647
+ return cleanLines.join("\n");
648
+ }
649
+ }
@@ -0,0 +1,35 @@
1
+ import type { ContentBlock } from "./types.js";
2
+ export interface ClaudeStreamAdapterState {
3
+ blocks: ContentBlock[];
4
+ usage?: {
5
+ inputTokens?: number;
6
+ outputTokens?: number;
7
+ cacheReadInputTokens?: number;
8
+ cacheCreationInputTokens?: number;
9
+ totalCostUsd?: number;
10
+ };
11
+ sessionId: string | null;
12
+ }
13
+ type ClaudeStreamEvent = {
14
+ type?: string;
15
+ session_id?: string;
16
+ message?: {
17
+ role?: string;
18
+ content?: unknown[];
19
+ };
20
+ content_block?: {
21
+ type?: string;
22
+ [key: string]: unknown;
23
+ };
24
+ delta?: {
25
+ type?: string;
26
+ text?: string;
27
+ thinking?: string;
28
+ partial_json?: string;
29
+ };
30
+ result?: unknown;
31
+ usage?: Record<string, unknown>;
32
+ total_cost_usd?: number;
33
+ };
34
+ export declare function updateClaudeStreamState(state: ClaudeStreamAdapterState, event: ClaudeStreamEvent): ClaudeStreamAdapterState;
35
+ export {};