crewly 1.2.0 → 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.
- package/config/constants.ts +5 -0
- package/config/roles/architect/prompt.md +7 -5
- package/config/roles/backend-developer/prompt.md +8 -4
- package/config/roles/content-strategist/prompt.md +12 -4
- package/config/roles/designer/prompt.md +8 -4
- package/config/roles/developer/prompt.md +7 -4
- package/config/roles/frontend-developer/prompt.md +8 -4
- package/config/roles/fullstack-dev/prompt.md +8 -4
- package/config/roles/generalist/prompt.md +7 -4
- package/config/roles/ops/prompt.md +7 -4
- package/config/roles/orchestrator/prompt.md +22 -1
- package/config/roles/product-manager/prompt.md +8 -4
- package/config/roles/qa/prompt.md +8 -4
- package/config/roles/qa-engineer/prompt.md +8 -4
- package/config/roles/sales/prompt.md +8 -4
- package/config/roles/support/prompt.md +8 -4
- package/config/roles/tpm/prompt.md +8 -4
- package/config/skills/orchestrator/delegate-task/execute.sh +7 -5
- package/config/skills/orchestrator/delegate-task/instructions.md +22 -17
- package/config/templates/agent-claude-md.md +10 -5
- package/dist/backend/backend/src/constants.d.ts +18 -63
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +17 -68
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +39 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +1 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +2 -13
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +105 -130
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +8 -8
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts +30 -27
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js +128 -53
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +47 -10
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +35 -3
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +123 -25
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts +6 -0
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +25 -3
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +29 -2
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.d.ts +2 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/terminal-output.utils.js +2 -28
- package/dist/backend/backend/src/utils/terminal-output.utils.js.map +1 -1
- package/dist/backend/backend/src/utils/terminal-string-ops.d.ts +183 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.js +717 -0
- package/dist/backend/backend/src/utils/terminal-string-ops.js.map +1 -0
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js +22 -27
- package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
- package/dist/backend/config/constants.d.ts +5 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +5 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +18 -63
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +17 -68
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.d.ts +2 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.d.ts.map +1 -1
- package/dist/cli/backend/src/utils/terminal-output.utils.js +2 -28
- package/dist/cli/backend/src/utils/terminal-output.utils.js.map +1 -1
- package/dist/cli/backend/src/utils/terminal-string-ops.d.ts +183 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.js +717 -0
- package/dist/cli/backend/src/utils/terminal-string-ops.js.map +1 -0
- package/dist/cli/config/constants.d.ts +5 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +5 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/package.json +2 -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
|