crewly 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/config/roles/architect/prompt.md +7 -5
  2. package/config/roles/backend-developer/prompt.md +8 -4
  3. package/config/roles/content-strategist/prompt.md +12 -4
  4. package/config/roles/designer/prompt.md +8 -4
  5. package/config/roles/developer/prompt.md +7 -4
  6. package/config/roles/frontend-developer/prompt.md +8 -4
  7. package/config/roles/fullstack-dev/prompt.md +8 -4
  8. package/config/roles/generalist/prompt.md +7 -4
  9. package/config/roles/ops/prompt.md +7 -4
  10. package/config/roles/orchestrator/prompt.md +22 -1
  11. package/config/roles/product-manager/prompt.md +8 -4
  12. package/config/roles/qa/prompt.md +8 -4
  13. package/config/roles/qa-engineer/prompt.md +8 -4
  14. package/config/roles/sales/prompt.md +8 -4
  15. package/config/roles/support/prompt.md +8 -4
  16. package/config/roles/tpm/prompt.md +8 -4
  17. package/config/skills/orchestrator/delegate-task/execute.sh +2 -2
  18. package/config/templates/agent-claude-md.md +10 -5
  19. package/dist/backend/backend/src/constants.d.ts +7 -69
  20. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  21. package/dist/backend/backend/src/constants.js +7 -74
  22. package/dist/backend/backend/src/constants.js.map +1 -1
  23. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  24. package/dist/backend/backend/src/controllers/chat/chat.controller.js +39 -0
  25. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  26. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +2 -13
  27. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/agent/agent-registration.service.js +85 -133
  29. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  30. package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts +1 -1
  31. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +8 -8
  32. package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
  33. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts +0 -7
  34. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  35. package/dist/backend/backend/src/services/event-bus/event-bus.service.js +0 -31
  36. package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  37. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +28 -4
  38. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
  39. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +111 -58
  40. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
  41. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +0 -3
  43. package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
  45. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  46. package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/session/session-command-helper.js +16 -4
  48. package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
  49. package/dist/backend/backend/src/utils/terminal-output.utils.d.ts +2 -1
  50. package/dist/backend/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
  51. package/dist/backend/backend/src/utils/terminal-output.utils.js +2 -28
  52. package/dist/backend/backend/src/utils/terminal-output.utils.js.map +1 -1
  53. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts +183 -0
  54. package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
  55. package/dist/backend/backend/src/utils/terminal-string-ops.js +717 -0
  56. package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -0
  57. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
  58. package/dist/backend/backend/src/websocket/terminal.gateway.js +22 -27
  59. package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
  60. package/dist/cli/backend/src/constants.d.ts +7 -69
  61. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  62. package/dist/cli/backend/src/constants.js +7 -74
  63. package/dist/cli/backend/src/constants.js.map +1 -1
  64. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts +2 -1
  65. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
  66. package/dist/cli/backend/src/utils/terminal-output.utils.js +2 -28
  67. package/dist/cli/backend/src/utils/terminal-output.utils.js.map +1 -1
  68. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts +183 -0
  69. package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
  70. package/dist/cli/backend/src/utils/terminal-string-ops.js +717 -0
  71. package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -0
  72. package/package.json +1 -1
@@ -0,0 +1,717 @@
1
+ /**
2
+ * Terminal string operations — regex-free replacements for terminal processing.
3
+ *
4
+ * Every function in this module uses only string primitives (indexOf, includes,
5
+ * startsWith, charCodeAt, character iteration) — NO regex. This makes the
6
+ * codebase immune to ReDoS by construction.
7
+ *
8
+ * @module terminal-string-ops
9
+ */
10
+ import { RUNTIME_TYPES } from '../constants.js';
11
+ // ─── Character sets ───────────────────────────────────────────────────────────
12
+ /** Braille spinner characters used by Claude Code to indicate processing. */
13
+ const SPINNER_CHARS = new Set([
14
+ 0x280B, // ⠋
15
+ 0x2819, // ⠙
16
+ 0x2839, // ⠹
17
+ 0x2838, // ⠸
18
+ 0x283C, // ⠼
19
+ 0x2834, // ⠴
20
+ 0x2826, // ⠦
21
+ 0x2827, // ⠧
22
+ 0x2807, // ⠇
23
+ 0x280F, // ⠏
24
+ ]);
25
+ /** Filled circle (⏺ U+23FA) — Claude Code working indicator. */
26
+ const WORKING_INDICATOR_CODE = 0x23FA; // ⏺
27
+ /** Box-drawing codepoints (U+2500–U+257F) plus ASCII equivalents. */
28
+ const BOX_DRAWING_MIN = 0x2500;
29
+ const BOX_DRAWING_MAX = 0x257F;
30
+ const EXTRA_BOX_CHARS = new Set([
31
+ 0x7C, // |
32
+ 0x2B, // +
33
+ 0x2D, // -
34
+ 0x2550, // ═
35
+ 0x2551, // ║
36
+ 0x256D, // ╭
37
+ 0x256E, // ╮
38
+ 0x2570, // ╰
39
+ 0x256F, // ╯
40
+ ]);
41
+ /** TUI border characters (vertical lines). */
42
+ const TUI_BORDER_CHARS = new Set([
43
+ 0x2502, // │
44
+ 0x2503, // ┃
45
+ 0x2551, // ║
46
+ 0x7C, // |
47
+ ]);
48
+ /** Processing status keywords (lowercased). */
49
+ const PROCESSING_KEYWORDS = [
50
+ 'thinking', 'processing', 'analyzing', 'running', 'calling', 'frosting',
51
+ ];
52
+ /** Gemini CLI processing keywords (lowercased). */
53
+ const GEMINI_PROCESSING_KEYWORDS = [
54
+ 'reading', 'thinking', 'processing', 'analyzing', 'generating', 'searching',
55
+ ];
56
+ // ─── Helper predicates ────────────────────────────────────────────────────────
57
+ /**
58
+ * Check if a codepoint is a box-drawing character.
59
+ */
60
+ function isBoxDrawing(cp) {
61
+ return (cp >= BOX_DRAWING_MIN && cp <= BOX_DRAWING_MAX) || EXTRA_BOX_CHARS.has(cp);
62
+ }
63
+ /**
64
+ * Check if a codepoint is a TUI border character.
65
+ */
66
+ function isTuiBorder(cp) {
67
+ return TUI_BORDER_CHARS.has(cp);
68
+ }
69
+ /**
70
+ * Check if a codepoint is whitespace (space or tab).
71
+ */
72
+ function isWhitespace(cp) {
73
+ return cp === 0x20 || cp === 0x09; // space or tab
74
+ }
75
+ // ─── stripAnsiCodes ───────────────────────────────────────────────────────────
76
+ /**
77
+ * Strip ANSI escape codes from PTY output using a single-pass state machine.
78
+ *
79
+ * Handles CSI sequences, OSC sequences, single-char escapes, and orphaned
80
+ * CSI fragments from PTY buffer boundary splits. Cursor-forward (C) sequences
81
+ * are replaced with a space to preserve word boundaries.
82
+ *
83
+ * O(n) time, O(n) space. No regex.
84
+ *
85
+ * @param content - Raw PTY output containing ANSI codes
86
+ * @returns Clean text with ANSI codes removed
87
+ */
88
+ export function stripAnsiCodes(content) {
89
+ const len = content.length;
90
+ const out = [];
91
+ let i = 0;
92
+ while (i < len) {
93
+ const ch = content.charCodeAt(i);
94
+ // ESC character (0x1B)
95
+ if (ch === 0x1B) {
96
+ i++;
97
+ if (i >= len)
98
+ break;
99
+ const next = content.charCodeAt(i);
100
+ // CSI sequence: ESC [
101
+ if (next === 0x5B) { // [
102
+ i++;
103
+ // Parse CSI: optional ?, parameter bytes (0x30-0x3F), intermediate (0x20-0x2F), final (0x40-0x7E)
104
+ const hasQuestion = i < len && content.charCodeAt(i) === 0x3F; // ?
105
+ if (hasQuestion)
106
+ i++;
107
+ // Collect parameter + intermediate bytes
108
+ let paramStart = i;
109
+ while (i < len) {
110
+ const c = content.charCodeAt(i);
111
+ if (c >= 0x30 && c <= 0x3F) {
112
+ i++;
113
+ continue;
114
+ } // parameter bytes (digits, ;, <=>?)
115
+ if (c >= 0x20 && c <= 0x2F) {
116
+ i++;
117
+ continue;
118
+ } // intermediate bytes
119
+ break;
120
+ }
121
+ // Final byte
122
+ if (i < len) {
123
+ const final = content.charCodeAt(i);
124
+ if (final >= 0x40 && final <= 0x7E) {
125
+ // Cursor forward (C) → emit space
126
+ if (!hasQuestion && final === 0x43) { // 'C'
127
+ out.push(' ');
128
+ }
129
+ i++;
130
+ continue;
131
+ }
132
+ }
133
+ // Malformed CSI — skip what we consumed
134
+ continue;
135
+ }
136
+ // OSC sequence: ESC ]
137
+ if (next === 0x5D) { // ]
138
+ i++;
139
+ // Consume until BEL (0x07) or ST (ESC \)
140
+ while (i < len) {
141
+ const c = content.charCodeAt(i);
142
+ if (c === 0x07) {
143
+ i++;
144
+ break;
145
+ } // BEL
146
+ if (c === 0x1B && i + 1 < len && content.charCodeAt(i + 1) === 0x5C) {
147
+ i += 2; // ST = ESC backslash
148
+ break;
149
+ }
150
+ i++;
151
+ }
152
+ continue;
153
+ }
154
+ // Other escape sequences: ESC followed by one character
155
+ // Some are two bytes total (ESC + char), skip the next char
156
+ if (next >= 0x20 && next <= 0x7E) {
157
+ i++;
158
+ // Check if there's a trailing parameter byte
159
+ if (i < len) {
160
+ const trailing = content.charCodeAt(i);
161
+ if (trailing >= 0x20 && trailing <= 0x7E && trailing !== 0x5B && trailing !== 0x5D) {
162
+ // Single trailing param (e.g., ESC ( B)
163
+ i++;
164
+ }
165
+ }
166
+ }
167
+ continue;
168
+ }
169
+ // Orphaned CSI fragments: [ followed by digits then a CSI final byte
170
+ // These occur when ESC lands in one PTY chunk and [params... in the next
171
+ if (ch === 0x5B && i + 1 < len) { // [
172
+ const nextCh = content.charCodeAt(i + 1);
173
+ // [? private mode fragment
174
+ if (nextCh === 0x3F) { // ?
175
+ let j = i + 2;
176
+ while (j < len) {
177
+ const c = content.charCodeAt(j);
178
+ if ((c >= 0x30 && c <= 0x39) || c === 0x3B) {
179
+ j++;
180
+ continue;
181
+ } // digits or ;
182
+ break;
183
+ }
184
+ if (j > i + 2 && j < len) {
185
+ const final = content.charCodeAt(j);
186
+ if (final >= 0x41 && final <= 0x7A) { // A-z
187
+ i = j + 1;
188
+ continue;
189
+ }
190
+ }
191
+ }
192
+ // [digits... followed by CSI final byte
193
+ if (nextCh >= 0x30 && nextCh <= 0x39) { // digit
194
+ let j = i + 1;
195
+ let hasDigit = false;
196
+ while (j < len) {
197
+ const c = content.charCodeAt(j);
198
+ if ((c >= 0x30 && c <= 0x39) || c === 0x3B) { // digit or ;
199
+ if (c >= 0x30 && c <= 0x39)
200
+ hasDigit = true;
201
+ j++;
202
+ continue;
203
+ }
204
+ break;
205
+ }
206
+ if (hasDigit && j < len) {
207
+ const final = content.charCodeAt(j);
208
+ // A-B, J, K, H, f, m are common CSI finals
209
+ if ((final >= 0x41 && final <= 0x42) || // A-B
210
+ final === 0x43 || // C (cursor forward)
211
+ final === 0x4A || // J
212
+ final === 0x4B || // K
213
+ final === 0x48 || // H
214
+ final === 0x66 || // f
215
+ final === 0x6D // m
216
+ ) {
217
+ if (final === 0x43) { // C = cursor forward → space
218
+ out.push(' ');
219
+ }
220
+ i = j + 1;
221
+ continue;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ // CR+LF normalization
227
+ if (ch === 0x0D) { // \r
228
+ if (i + 1 < len && content.charCodeAt(i + 1) === 0x0A) { // \r\n
229
+ out.push('\n');
230
+ i += 2;
231
+ }
232
+ else {
233
+ out.push('\n');
234
+ i++;
235
+ }
236
+ continue;
237
+ }
238
+ // Remove control characters except tab (0x09) and newline (0x0A)
239
+ if ((ch >= 0x00 && ch <= 0x08) ||
240
+ ch === 0x0B || ch === 0x0C ||
241
+ (ch >= 0x0E && ch <= 0x1F) ||
242
+ ch === 0x7F) {
243
+ i++;
244
+ continue;
245
+ }
246
+ // Normal character — emit
247
+ out.push(content[i]);
248
+ i++;
249
+ }
250
+ return out.join('');
251
+ }
252
+ // ─── stripBoxDrawing ──────────────────────────────────────────────────────────
253
+ /**
254
+ * Strip box-drawing characters and decorative borders from both ends of a line.
255
+ *
256
+ * @param line - A single terminal line
257
+ * @returns The line with leading/trailing box-drawing chars and whitespace removed
258
+ */
259
+ export function stripBoxDrawing(line) {
260
+ let start = 0;
261
+ let end = line.length;
262
+ // Strip from start
263
+ while (start < end) {
264
+ const cp = line.codePointAt(start);
265
+ if (isBoxDrawing(cp) || isWhitespace(cp)) {
266
+ start += cp > 0xFFFF ? 2 : 1;
267
+ }
268
+ else {
269
+ break;
270
+ }
271
+ }
272
+ // Strip from end
273
+ while (end > start) {
274
+ // Walk back one codepoint
275
+ const prevIdx = end - 1;
276
+ const cp = line.codePointAt(prevIdx);
277
+ // Handle surrogate pairs
278
+ if (prevIdx > start && cp >= 0xDC00 && cp <= 0xDFFF) {
279
+ const highIdx = prevIdx - 1;
280
+ const fullCp = line.codePointAt(highIdx);
281
+ if (isBoxDrawing(fullCp) || isWhitespace(fullCp)) {
282
+ end = highIdx;
283
+ continue;
284
+ }
285
+ break;
286
+ }
287
+ if (isBoxDrawing(cp) || isWhitespace(cp)) {
288
+ end = prevIdx;
289
+ }
290
+ else {
291
+ break;
292
+ }
293
+ }
294
+ return line.slice(start, end);
295
+ }
296
+ // ─── stripTuiLineBorders ──────────────────────────────────────────────────────
297
+ /**
298
+ * Strip TUI line border characters (│ ┃ ║ |) and surrounding whitespace
299
+ * from both ends of a line.
300
+ *
301
+ * @param line - A single terminal line
302
+ * @returns The line with leading/trailing border chars removed
303
+ */
304
+ export function stripTuiLineBorders(line) {
305
+ let start = 0;
306
+ let end = line.length;
307
+ // Strip from start
308
+ while (start < end) {
309
+ const cp = line.charCodeAt(start);
310
+ if (isTuiBorder(cp) || isWhitespace(cp)) {
311
+ start++;
312
+ }
313
+ else {
314
+ // Handle multi-byte border chars
315
+ const fullCp = line.codePointAt(start);
316
+ if (isTuiBorder(fullCp) || isWhitespace(fullCp)) {
317
+ start += fullCp > 0xFFFF ? 2 : 1;
318
+ }
319
+ else {
320
+ break;
321
+ }
322
+ }
323
+ }
324
+ // Strip from end
325
+ while (end > start) {
326
+ const cp = line.charCodeAt(end - 1);
327
+ if (isTuiBorder(cp) || isWhitespace(cp)) {
328
+ end--;
329
+ }
330
+ else {
331
+ break;
332
+ }
333
+ }
334
+ return line.slice(start, end);
335
+ }
336
+ // ─── matchTuiPromptLine ───────────────────────────────────────────────────────
337
+ /**
338
+ * Check if a line matches the pattern of a TUI prompt line: optional border
339
+ * chars, then > followed by content. Returns the content after > prompt or null.
340
+ *
341
+ * Replaces the prompt line regex.
342
+ *
343
+ * @param line - A single terminal line
344
+ * @returns The content after > prompt, or null if not a prompt line
345
+ */
346
+ export function matchTuiPromptLine(line) {
347
+ let i = 0;
348
+ const len = line.length;
349
+ // Skip leading border chars and whitespace
350
+ while (i < len) {
351
+ const cp = line.charCodeAt(i);
352
+ if (isTuiBorder(cp) || isWhitespace(cp)) {
353
+ i++;
354
+ }
355
+ else {
356
+ break;
357
+ }
358
+ }
359
+ // Check for > (greater-than followed by space)
360
+ if (i < len && line.charCodeAt(i) === 0x3E) { // >
361
+ i++;
362
+ if (i < len && isWhitespace(line.charCodeAt(i))) {
363
+ i++;
364
+ // Skip additional whitespace
365
+ while (i < len && isWhitespace(line.charCodeAt(i)))
366
+ i++;
367
+ if (i < len) {
368
+ return line.slice(i);
369
+ }
370
+ }
371
+ }
372
+ return null;
373
+ }
374
+ // ─── Prompt detection ─────────────────────────────────────────────────────────
375
+ /**
376
+ * Check if a single line looks like an agent prompt.
377
+ *
378
+ * Claude Code: ❯, ⏵, $ alone (or with box-drawing), or ❯❯
379
+ * Gemini CLI: > or ! or bordered │ >, or "Type your message" / "YOLO mode"
380
+ * Codex CLI: › or bordered │ ›
381
+ *
382
+ * @param line - A single non-empty terminal line (already stripped of ANSI)
383
+ * @param runtimeType - The agent runtime type
384
+ * @returns true if the line looks like a prompt
385
+ */
386
+ export function isPromptLine(line, runtimeType) {
387
+ const trimmed = line.trim();
388
+ if (trimmed.length === 0)
389
+ return false;
390
+ const stripped = stripBoxDrawing(trimmed);
391
+ const isGemini = runtimeType === RUNTIME_TYPES.GEMINI_CLI;
392
+ const isClaudeCode = runtimeType === RUNTIME_TYPES.CLAUDE_CODE;
393
+ const isCodex = runtimeType === RUNTIME_TYPES.CODEX_CLI;
394
+ // Claude Code prompts
395
+ if (!isGemini && !isCodex) {
396
+ if (trimmed === '❯' || trimmed === '⏵' || trimmed === '$' ||
397
+ stripped === '❯' || stripped === '⏵' || stripped === '$') {
398
+ return true;
399
+ }
400
+ // ❯❯ bypass permissions prompt
401
+ if (trimmed.startsWith('❯❯'))
402
+ return true;
403
+ }
404
+ // Codex CLI prompts
405
+ if (isCodex) {
406
+ if (trimmed === '›' || trimmed.startsWith('› ') ||
407
+ stripped === '›' || stripped.startsWith('› '))
408
+ return true;
409
+ // Bordered > prompt (only in TUI box-drawing context)
410
+ // stripped already has box-drawing chars removed, so if the original
411
+ // had borders and the stripped starts with > , that's a bordered prompt
412
+ if (trimmed !== stripped && (stripped.startsWith('> ') || stripped === '>'))
413
+ return true;
414
+ }
415
+ // Gemini CLI prompts (and fallback for unknown runtime)
416
+ if (!isClaudeCode && !isCodex) {
417
+ if (trimmed === '>' || trimmed === '!' ||
418
+ trimmed.startsWith('> ') || trimmed.startsWith('! ') ||
419
+ stripped === '>' || stripped === '!' ||
420
+ stripped.startsWith('> ') || stripped.startsWith('! ')) {
421
+ return true;
422
+ }
423
+ // Textual prompt placeholders
424
+ const lower = trimmed.toLowerCase();
425
+ if (lower.includes('type your message') || lower.includes('yolo mode')) {
426
+ return true;
427
+ }
428
+ }
429
+ return false;
430
+ }
431
+ /**
432
+ * Check if the agent appears to be at an input prompt by scanning terminal output.
433
+ *
434
+ * Scans the last 10 non-empty lines for prompt patterns, then checks for busy
435
+ * indicators to distinguish idle-at-prompt from actively-processing.
436
+ *
437
+ * @param output - Terminal output text (already stripped of ANSI codes)
438
+ * @param runtimeType - The agent runtime type
439
+ * @returns true if the agent appears idle at a prompt
440
+ */
441
+ export function isAgentAtPrompt(output, runtimeType) {
442
+ if (!output || typeof output !== 'string')
443
+ return false;
444
+ const tailSection = output.slice(-5000);
445
+ const lines = tailSection.split('\n').filter(l => l.trim().length > 0);
446
+ const linesToCheck = lines.slice(-10);
447
+ // Check for prompt indicators
448
+ const hasPrompt = linesToCheck.some(line => isPromptLine(line, runtimeType));
449
+ if (hasPrompt)
450
+ return true;
451
+ // No prompt found — check if agent is still processing
452
+ const recentText = linesToCheck.join('\n');
453
+ // Processing with text check
454
+ if (containsProcessingIndicator(recentText))
455
+ return false;
456
+ // Busy status bar check
457
+ if (containsBusyStatusBar(recentText))
458
+ return false;
459
+ return false;
460
+ }
461
+ // ─── Processing indicator detection ───────────────────────────────────────────
462
+ /**
463
+ * Check if text contains spinner characters or the working indicator (⏺).
464
+ * This is the "spinner-only" check (no keyword matching).
465
+ *
466
+ * Replaces TERMINAL_PATTERNS.PROCESSING regex.
467
+ *
468
+ * @param text - Text to check
469
+ * @returns true if any spinner/working indicator character is found
470
+ */
471
+ export function containsSpinnerOrWorkingIndicator(text) {
472
+ for (let i = 0; i < text.length; i++) {
473
+ const cp = text.codePointAt(i);
474
+ if (SPINNER_CHARS.has(cp) || cp === WORKING_INDICATOR_CODE)
475
+ return true;
476
+ if (cp > 0xFFFF)
477
+ i++; // skip surrogate pair
478
+ }
479
+ return false;
480
+ }
481
+ /**
482
+ * Check if text contains processing indicators including spinner chars,
483
+ * working indicator (⏺), AND status keywords.
484
+ *
485
+ * Replaces TERMINAL_PATTERNS.PROCESSING_WITH_TEXT regex.
486
+ *
487
+ * @param text - Text to check
488
+ * @returns true if any processing indicator is found
489
+ */
490
+ export function containsProcessingIndicator(text) {
491
+ // Check spinner/working indicator chars
492
+ if (containsSpinnerOrWorkingIndicator(text))
493
+ return true;
494
+ // Check keywords (case-insensitive)
495
+ const lower = text.toLowerCase();
496
+ for (const keyword of PROCESSING_KEYWORDS) {
497
+ if (lower.includes(keyword))
498
+ return true;
499
+ }
500
+ return false;
501
+ }
502
+ /**
503
+ * Check if text contains the "esc to interrupt" busy status bar.
504
+ *
505
+ * Replaces TERMINAL_PATTERNS.BUSY_STATUS_BAR regex.
506
+ * Manually matches "esc" then whitespace then "to" then whitespace then "interrupt".
507
+ *
508
+ * @param text - Text to check
509
+ * @returns true if the busy status bar text is found
510
+ */
511
+ export function containsBusyStatusBar(text) {
512
+ const lower = text.toLowerCase();
513
+ let idx = 0;
514
+ while (true) {
515
+ idx = lower.indexOf('esc', idx);
516
+ if (idx === -1)
517
+ return false;
518
+ let pos = idx + 3;
519
+ // Skip whitespace (at least one required)
520
+ if (pos >= lower.length || !isWhitespaceChar(lower.charCodeAt(pos))) {
521
+ idx++;
522
+ continue;
523
+ }
524
+ while (pos < lower.length && isWhitespaceChar(lower.charCodeAt(pos)))
525
+ pos++;
526
+ // Match "to"
527
+ if (pos + 2 > lower.length || lower[pos] !== 't' || lower[pos + 1] !== 'o') {
528
+ idx++;
529
+ continue;
530
+ }
531
+ pos += 2;
532
+ // Skip whitespace (at least one required)
533
+ if (pos >= lower.length || !isWhitespaceChar(lower.charCodeAt(pos))) {
534
+ idx++;
535
+ continue;
536
+ }
537
+ while (pos < lower.length && isWhitespaceChar(lower.charCodeAt(pos)))
538
+ pos++;
539
+ // Match "interrupt"
540
+ const target = 'interrupt';
541
+ if (pos + target.length > lower.length) {
542
+ idx++;
543
+ continue;
544
+ }
545
+ let matched = true;
546
+ for (let k = 0; k < target.length; k++) {
547
+ if (lower[pos + k] !== target[k]) {
548
+ matched = false;
549
+ break;
550
+ }
551
+ }
552
+ if (matched)
553
+ return true;
554
+ idx++;
555
+ }
556
+ }
557
+ /**
558
+ * Check if a character code is whitespace (space, tab, newline, carriage return).
559
+ */
560
+ function isWhitespaceChar(cp) {
561
+ return cp === 0x20 || cp === 0x09 || cp === 0x0A || cp === 0x0D;
562
+ }
563
+ /**
564
+ * Check if text contains Claude Code's Rewind mode.
565
+ *
566
+ * Replaces TERMINAL_PATTERNS.REWIND_MODE regex.
567
+ * Checks for "Rewind" then "Restore the code" appearing after it.
568
+ *
569
+ * @param text - Text to check
570
+ * @returns true if Rewind mode is detected
571
+ */
572
+ export function containsRewindMode(text) {
573
+ const rewindIdx = text.indexOf('Rewind');
574
+ if (rewindIdx === -1)
575
+ return false;
576
+ return text.indexOf('Restore the code', rewindIdx + 6) !== -1;
577
+ }
578
+ /**
579
+ * Check if text contains Gemini CLI processing keywords.
580
+ *
581
+ * Replaces inline Gemini keyword regex in sendMessageWithRetry.
582
+ *
583
+ * @param text - Text to check
584
+ * @returns true if any Gemini processing keyword is found
585
+ */
586
+ export function containsGeminiProcessingKeywords(text) {
587
+ const lower = text.toLowerCase();
588
+ for (const keyword of GEMINI_PROCESSING_KEYWORDS) {
589
+ if (lower.includes(keyword))
590
+ return true;
591
+ }
592
+ return false;
593
+ }
594
+ /**
595
+ * Extract all marker blocks bounded by open/close tag pairs from a buffer.
596
+ *
597
+ * Replaces NOTIFY/SLACK_NOTIFY marker regex patterns.
598
+ *
599
+ * @param buf - The text buffer to search
600
+ * @param openTag - Opening tag string (e.g., "[NOTIFY]")
601
+ * @param closeTag - Closing tag string (e.g., "[/NOTIFY]")
602
+ * @returns Array of extracted marker blocks
603
+ */
604
+ export function extractMarkerBlocks(buf, openTag, closeTag) {
605
+ const results = [];
606
+ let searchStart = 0;
607
+ while (true) {
608
+ const openIdx = buf.indexOf(openTag, searchStart);
609
+ if (openIdx === -1)
610
+ break;
611
+ const contentStart = openIdx + openTag.length;
612
+ const closeIdx = buf.indexOf(closeTag, contentStart);
613
+ if (closeIdx === -1)
614
+ break;
615
+ const content = buf.slice(contentStart, closeIdx).trim();
616
+ const endIndex = closeIdx + closeTag.length;
617
+ results.push({ content, startIndex: openIdx, endIndex });
618
+ searchStart = endIndex;
619
+ }
620
+ return results;
621
+ }
622
+ /**
623
+ * Extract all [CHAT_RESPONSE]...[/CHAT_RESPONSE] blocks from a buffer.
624
+ * Handles the optional :conversationId suffix: [CHAT_RESPONSE:abc123].
625
+ *
626
+ * Replaces the CHAT_RESPONSE regex pattern.
627
+ *
628
+ * @param buf - The text buffer to search
629
+ * @returns Array of extracted chat response blocks
630
+ */
631
+ export function extractChatResponseBlocks(buf) {
632
+ const results = [];
633
+ const openPrefix = '[CHAT_RESPONSE';
634
+ const closeTag = '[/CHAT_RESPONSE]';
635
+ let searchStart = 0;
636
+ while (true) {
637
+ const openIdx = buf.indexOf(openPrefix, searchStart);
638
+ if (openIdx === -1)
639
+ break;
640
+ let pos = openIdx + openPrefix.length;
641
+ // Parse optional :conversationId
642
+ let conversationId = null;
643
+ if (pos < buf.length && buf.charCodeAt(pos) === 0x3A) { // :
644
+ pos++;
645
+ const closeBracket = buf.indexOf(']', pos);
646
+ if (closeBracket === -1)
647
+ break;
648
+ conversationId = buf.slice(pos, closeBracket) || null;
649
+ pos = closeBracket + 1;
650
+ }
651
+ else if (pos < buf.length && buf.charCodeAt(pos) === 0x5D) { // ]
652
+ pos++;
653
+ }
654
+ else {
655
+ // Malformed — skip past this occurrence
656
+ searchStart = openIdx + openPrefix.length;
657
+ continue;
658
+ }
659
+ // Find close tag
660
+ const closeIdx = buf.indexOf(closeTag, pos);
661
+ if (closeIdx === -1)
662
+ break;
663
+ const content = buf.slice(pos, closeIdx).trim();
664
+ const endIndex = closeIdx + closeTag.length;
665
+ results.push({ conversationId, content, startIndex: openIdx, endIndex });
666
+ searchStart = endIndex;
667
+ }
668
+ return results;
669
+ }
670
+ // ─── Conversation ID extraction ───────────────────────────────────────────────
671
+ /**
672
+ * Extract conversation ID from a [CHAT:convId] marker in text.
673
+ *
674
+ * Replaces CHAT_ROUTING_CONSTANTS.CONVERSATION_ID_PATTERN regex.
675
+ *
676
+ * @param text - Text to search
677
+ * @returns The conversation ID or null if not found
678
+ */
679
+ export function extractConversationId(text) {
680
+ const marker = '[CHAT:';
681
+ const idx = text.indexOf(marker);
682
+ if (idx === -1)
683
+ return null;
684
+ const start = idx + marker.length;
685
+ const end = text.indexOf(']', start);
686
+ if (end === -1)
687
+ return null;
688
+ const id = text.slice(start, end);
689
+ return id.length > 0 ? id : null;
690
+ }
691
+ /**
692
+ * Extract and remove the [CHAT:convId] prefix from a message.
693
+ *
694
+ * Replaces the CHAT prefix regex.
695
+ *
696
+ * @param message - Message that may start with [CHAT:...]
697
+ * @returns Object with the prefix length (0 if no prefix) and the content after prefix
698
+ */
699
+ export function extractChatPrefix(message) {
700
+ if (!message.startsWith('[CHAT:')) {
701
+ return { prefixLength: 0, content: message };
702
+ }
703
+ const closeBracket = message.indexOf(']');
704
+ if (closeBracket === -1) {
705
+ return { prefixLength: 0, content: message };
706
+ }
707
+ let afterBracket = closeBracket + 1;
708
+ // Skip whitespace after ]
709
+ while (afterBracket < message.length && isWhitespaceChar(message.charCodeAt(afterBracket))) {
710
+ afterBracket++;
711
+ }
712
+ return {
713
+ prefixLength: afterBracket,
714
+ content: message.slice(afterBracket),
715
+ };
716
+ }
717
+ //# sourceMappingURL=terminal-string-ops.js.map