agent-relay 1.0.7 → 1.0.9
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/README.md +176 -6
- package/dist/bridge/config.d.ts +41 -0
- package/dist/bridge/config.d.ts.map +1 -0
- package/dist/bridge/config.js +143 -0
- package/dist/bridge/config.js.map +1 -0
- package/dist/bridge/index.d.ts +10 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +10 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/multi-project-client.d.ts +99 -0
- package/dist/bridge/multi-project-client.d.ts.map +1 -0
- package/dist/bridge/multi-project-client.js +386 -0
- package/dist/bridge/multi-project-client.js.map +1 -0
- package/dist/bridge/spawner.d.ts +46 -0
- package/dist/bridge/spawner.d.ts.map +1 -0
- package/dist/bridge/spawner.js +223 -0
- package/dist/bridge/spawner.js.map +1 -0
- package/dist/bridge/types.d.ts +55 -0
- package/dist/bridge/types.d.ts.map +1 -0
- package/dist/bridge/types.js +6 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/bridge/utils.d.ts +30 -0
- package/dist/bridge/utils.d.ts.map +1 -0
- package/dist/bridge/utils.js +54 -0
- package/dist/bridge/utils.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +906 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/agent-registry.d.ts +60 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -0
- package/dist/daemon/agent-registry.js +163 -0
- package/dist/daemon/agent-registry.js.map +1 -0
- package/dist/daemon/connection.d.ts +33 -1
- package/dist/daemon/connection.d.ts.map +1 -1
- package/dist/daemon/connection.js +86 -11
- package/dist/daemon/connection.js.map +1 -1
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +2 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/registry.d.ts +9 -0
- package/dist/daemon/registry.d.ts.map +1 -0
- package/dist/daemon/registry.js +9 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/router.d.ts +61 -2
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +219 -4
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +9 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +135 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/dashboard/metrics.d.ts +105 -0
- package/dist/dashboard/metrics.d.ts.map +1 -0
- package/dist/dashboard/metrics.js +192 -0
- package/dist/dashboard/metrics.js.map +1 -0
- package/dist/dashboard/needs-attention.d.ts +24 -0
- package/dist/dashboard/needs-attention.d.ts.map +1 -0
- package/dist/dashboard/needs-attention.js +78 -0
- package/dist/dashboard/needs-attention.js.map +1 -0
- package/dist/dashboard/public/bridge.html +1272 -0
- package/dist/dashboard/public/index.html +2094 -347
- package/dist/dashboard/public/js/app.js +184 -0
- package/dist/dashboard/public/js/app.js.map +7 -0
- package/dist/dashboard/public/metrics.html +999 -0
- package/dist/dashboard/server.d.ts +14 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +689 -16
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/start.js +1 -1
- package/dist/dashboard/start.js.map +1 -1
- package/dist/dashboard-v2/index.d.ts +10 -0
- package/dist/dashboard-v2/index.d.ts.map +1 -0
- package/dist/dashboard-v2/index.js +54 -0
- package/dist/dashboard-v2/index.js.map +1 -0
- package/dist/dashboard-v2/lib/api.d.ts +95 -0
- package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/api.js +270 -0
- package/dist/dashboard-v2/lib/api.js.map +1 -0
- package/dist/dashboard-v2/lib/colors.d.ts +61 -0
- package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/colors.js +198 -0
- package/dist/dashboard-v2/lib/colors.js.map +1 -0
- package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
- package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
- package/dist/dashboard-v2/lib/hierarchy.js +196 -0
- package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
- package/dist/dashboard-v2/types/index.d.ts +154 -0
- package/dist/dashboard-v2/types/index.d.ts.map +1 -0
- package/dist/dashboard-v2/types/index.js +6 -0
- package/dist/dashboard-v2/types/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/protocol/types.d.ts +15 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/storage/adapter.d.ts +74 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +39 -0
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts +92 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +615 -47
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/utils/agent-config.d.ts +45 -0
- package/dist/utils/agent-config.d.ts.map +1 -0
- package/dist/utils/agent-config.js +118 -0
- package/dist/utils/agent-config.js.map +1 -0
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +22 -1
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/client.d.ts +30 -3
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +85 -9
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +127 -4
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +622 -86
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +136 -10
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +599 -79
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/AGENTS.md +132 -27
- package/docs/ARCHITECTURE_DECISIONS.md +175 -0
- package/docs/CHANGELOG.md +1 -1
- package/docs/COMPETITIVE_ANALYSIS.md +897 -0
- package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
- package/docs/DESIGN_V2.md +1079 -0
- package/docs/INTEGRATION-GUIDE.md +926 -0
- package/docs/MONETIZATION.md +1679 -0
- package/docs/PROPOSAL-trajectories.md +1582 -0
- package/docs/PROTOCOL.md +3 -3
- package/docs/SCALING_ANALYSIS.md +280 -0
- package/docs/TMUX_IMPLEMENTATION_NOTES.md +9 -9
- package/docs/TMUX_IMPROVEMENTS.md +968 -0
- package/docs/agent-relay-snippet.md +61 -0
- package/docs/competitive-analysis-mcp-agent-mail.md +389 -0
- package/docs/dashboard-v2-plan.md +179 -0
- package/package.json +10 -3
package/dist/wrapper/parser.js
CHANGED
|
@@ -3,30 +3,101 @@
|
|
|
3
3
|
* Extracts relay commands from agent terminal output.
|
|
4
4
|
*
|
|
5
5
|
* Supports two formats:
|
|
6
|
-
* 1. Inline:
|
|
6
|
+
* 1. Inline: ->relay:<target> <message> (single line, start of line only)
|
|
7
7
|
* 2. Block: [[RELAY]]{ json }[[/RELAY]] (multi-line, structured)
|
|
8
8
|
*
|
|
9
9
|
* Rules:
|
|
10
10
|
* - Inline only matches at start of line (after whitespace)
|
|
11
11
|
* - Ignores content inside code fences
|
|
12
|
-
* - Escape with
|
|
12
|
+
* - Escape with \->relay: to output literal
|
|
13
13
|
* - Block format is preferred for structured data
|
|
14
14
|
*/
|
|
15
15
|
const DEFAULT_OPTIONS = {
|
|
16
16
|
maxBlockBytes: 1024 * 1024, // 1 MiB
|
|
17
17
|
enableInline: true,
|
|
18
18
|
enableBlock: true,
|
|
19
|
+
prefix: '->relay:',
|
|
20
|
+
thinkingPrefix: '->thinking:',
|
|
19
21
|
};
|
|
20
|
-
//
|
|
21
|
-
// Allow common input prefixes: >, $, %, #, →, ➜, bullets (●•◦‣⁃-*⏺◆◇○□■), and their variations
|
|
22
|
-
const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?@relay:(\S+)\s+(.+)$/;
|
|
23
|
-
const INLINE_THINKING = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?@thinking:(\S+)\s+(.+)$/;
|
|
22
|
+
// Static patterns (not prefix-dependent)
|
|
24
23
|
const BLOCK_END = /\[\[\/RELAY\]\]/;
|
|
24
|
+
const BLOCK_METADATA_START = '[[RELAY_METADATA]]';
|
|
25
|
+
const BLOCK_METADATA_END = /\[\[\/RELAY_METADATA\]\]/;
|
|
25
26
|
const CODE_FENCE = /^```/;
|
|
26
|
-
|
|
27
|
+
// Fenced inline patterns: ->relay:Target <<< ... >>>
|
|
28
|
+
// FENCE_END is lenient - matches >>> at start of line regardless of trailing content
|
|
29
|
+
// This prevents stuck fenced mode when >>> has trailing text like ">>> done"
|
|
30
|
+
const FENCE_END = /^(?:\s*)?>>>/;
|
|
31
|
+
// Maximum lines in a fenced block before assuming it's stuck
|
|
32
|
+
const MAX_FENCED_LINES = 200;
|
|
33
|
+
// Continuation helpers
|
|
34
|
+
const BULLET_OR_NUMBERED_LIST = /^[ \t]*([\-*•◦‣⏺◆◇○□■]|[0-9]+[.)])\s+/;
|
|
35
|
+
const PROMPTISH_LINE = /^[\s]*[>$%#➜›»][\s]*$/;
|
|
36
|
+
const RELAY_INJECTION_PREFIX = /^\s*Relay message from /;
|
|
37
|
+
const MAX_INLINE_CONTINUATION_LINES = 30;
|
|
38
|
+
// Claude extended thinking block markers - skip content inside these
|
|
39
|
+
const THINKING_START = /<antml:thinking>/;
|
|
40
|
+
const THINKING_END = /<\/thinking>/;
|
|
41
|
+
/**
|
|
42
|
+
* Escape special regex characters in a string
|
|
43
|
+
*/
|
|
44
|
+
function escapeRegex(str) {
|
|
45
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build inline pattern for a given prefix
|
|
49
|
+
* Allow common input prefixes: >, $, %, #, →, ➜, bullets (●•◦‣⁃-*⏺◆◇○□■), box chars (│┃┆┇┊┋╎╏), and their variations
|
|
50
|
+
*
|
|
51
|
+
* Supports optional thread syntax: ->relay:Target [thread:id] message
|
|
52
|
+
* Thread IDs can contain alphanumeric chars, hyphens, underscores
|
|
53
|
+
*/
|
|
54
|
+
function buildInlinePattern(prefix) {
|
|
55
|
+
const escaped = escapeRegex(prefix);
|
|
56
|
+
// Group 1: target, Group 2: optional thread ID (without brackets), Group 3: message body
|
|
57
|
+
// Includes box drawing characters (│┃┆┇┊┋╎╏) and sparkle (✦) for Gemini CLI output
|
|
58
|
+
return new RegExp(`^(?:\\s*(?:[>$%#→➜›»●•◦‣⁃\\-*⏺◆◇○□■│┃┆┇┊┋╎╏✦]\\s*)*)?${escaped}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s+(.+)$`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build fenced inline pattern for multi-line messages: ->relay:Target <<<
|
|
62
|
+
* This opens a fenced block that continues until >>> is seen on its own line.
|
|
63
|
+
* Group 1: target, Group 2: optional thread ID
|
|
64
|
+
*/
|
|
65
|
+
function buildFencedInlinePattern(prefix) {
|
|
66
|
+
const escaped = escapeRegex(prefix);
|
|
67
|
+
return new RegExp(`^(?:\\s*(?:[>$%#→➜›»●•◦‣⁃\\-*⏺◆◇○□■│┃┆┇┊┋╎╏✦]\\s*)*)?${escaped}(\\S+)(?:\\s+\\[thread:([\\w-]+)\\])?\\s+<<<\\s*$`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build escape pattern for a given prefix (e.g., \->relay: or \->)
|
|
71
|
+
*/
|
|
72
|
+
function buildEscapePattern(prefix, thinkingPrefix) {
|
|
73
|
+
// Extract the first character(s) that would be escaped
|
|
74
|
+
const prefixEscaped = escapeRegex(prefix);
|
|
75
|
+
const thinkingEscaped = escapeRegex(thinkingPrefix);
|
|
76
|
+
return new RegExp(`^(\\s*)\\\\(${prefixEscaped}|${thinkingEscaped})`);
|
|
77
|
+
}
|
|
27
78
|
// ANSI escape sequence pattern for stripping
|
|
28
79
|
// eslint-disable-next-line no-control-regex
|
|
29
80
|
const ANSI_PATTERN = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)|\r/g;
|
|
81
|
+
/**
|
|
82
|
+
* Parse a target string that may contain cross-project syntax.
|
|
83
|
+
* Supports: "agent" (local) or "project:agent" (cross-project)
|
|
84
|
+
*
|
|
85
|
+
* @param target The raw target string from the relay command
|
|
86
|
+
* @returns Object with `to` (agent name) and optional `project`
|
|
87
|
+
*/
|
|
88
|
+
function parseTarget(target) {
|
|
89
|
+
// Check for cross-project syntax: project:agent
|
|
90
|
+
// Only split on FIRST colon to allow agent names with colons
|
|
91
|
+
const colonIndex = target.indexOf(':');
|
|
92
|
+
if (colonIndex > 0 && colonIndex < target.length - 1) {
|
|
93
|
+
// Has a colon with content on both sides
|
|
94
|
+
const project = target.substring(0, colonIndex);
|
|
95
|
+
const agent = target.substring(colonIndex + 1);
|
|
96
|
+
return { to: agent, project };
|
|
97
|
+
}
|
|
98
|
+
// Local target (no colon or malformed)
|
|
99
|
+
return { to: target };
|
|
100
|
+
}
|
|
30
101
|
/**
|
|
31
102
|
* Strip ANSI escape codes from a string for pattern matching.
|
|
32
103
|
*/
|
|
@@ -38,8 +109,38 @@ export class OutputParser {
|
|
|
38
109
|
inCodeFence = false;
|
|
39
110
|
inBlock = false;
|
|
40
111
|
blockBuffer = '';
|
|
112
|
+
blockType = null;
|
|
113
|
+
lastParsedMetadata = null;
|
|
114
|
+
// Claude extended thinking block state - skip content inside <thinking>...</thinking>
|
|
115
|
+
inThinkingBlock = false;
|
|
116
|
+
// Fenced inline state: ->relay:Target <<< ... >>>
|
|
117
|
+
inFencedInline = false;
|
|
118
|
+
fencedInlineBuffer = '';
|
|
119
|
+
fencedInlineTarget = '';
|
|
120
|
+
fencedInlineThread = undefined;
|
|
121
|
+
fencedInlineProject = undefined;
|
|
122
|
+
fencedInlineRaw = [];
|
|
123
|
+
fencedInlineKind = 'message';
|
|
124
|
+
// Dynamic patterns based on prefix configuration
|
|
125
|
+
inlineRelayPattern;
|
|
126
|
+
inlineThinkingPattern;
|
|
127
|
+
fencedRelayPattern;
|
|
128
|
+
fencedThinkingPattern;
|
|
129
|
+
escapePattern;
|
|
41
130
|
constructor(options = {}) {
|
|
42
131
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
132
|
+
// Build patterns based on configured prefixes
|
|
133
|
+
this.inlineRelayPattern = buildInlinePattern(this.options.prefix);
|
|
134
|
+
this.inlineThinkingPattern = buildInlinePattern(this.options.thinkingPrefix);
|
|
135
|
+
this.fencedRelayPattern = buildFencedInlinePattern(this.options.prefix);
|
|
136
|
+
this.fencedThinkingPattern = buildFencedInlinePattern(this.options.thinkingPrefix);
|
|
137
|
+
this.escapePattern = buildEscapePattern(this.options.prefix, this.options.thinkingPrefix);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get the configured relay prefix
|
|
141
|
+
*/
|
|
142
|
+
get prefix() {
|
|
143
|
+
return this.options.prefix;
|
|
43
144
|
}
|
|
44
145
|
/**
|
|
45
146
|
* Push data into the parser and extract commands.
|
|
@@ -51,16 +152,20 @@ export class OutputParser {
|
|
|
51
152
|
parse(data) {
|
|
52
153
|
const commands = [];
|
|
53
154
|
let output = '';
|
|
155
|
+
// If we're inside a fenced inline block, accumulate until we see >>>
|
|
156
|
+
if (this.inFencedInline) {
|
|
157
|
+
return this.parseFencedInlineMode(data, commands);
|
|
158
|
+
}
|
|
54
159
|
// If we're inside a block, accumulate until we see the end
|
|
55
|
-
if (this.inBlock) {
|
|
56
|
-
return this.parseInBlockMode(data, commands);
|
|
160
|
+
if (this.inBlock && this.blockType) {
|
|
161
|
+
return this.parseInBlockMode(data, commands, this.blockType);
|
|
57
162
|
}
|
|
58
|
-
// Find [[RELAY]] that's at the start of a line
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const before = data.substring(0,
|
|
63
|
-
const after = data.substring(
|
|
163
|
+
// Find [[RELAY_METADATA]] or [[RELAY]] that's at the start of a line
|
|
164
|
+
const blockStart = this.findBlockStart(data);
|
|
165
|
+
if (this.options.enableBlock && blockStart.index !== -1 && blockStart.identifier) {
|
|
166
|
+
const blockStartIdentifier = blockStart.identifier;
|
|
167
|
+
const before = data.substring(0, blockStart.index);
|
|
168
|
+
const after = data.substring(blockStart.index + blockStartIdentifier.length);
|
|
64
169
|
// Output everything before the block start
|
|
65
170
|
if (before) {
|
|
66
171
|
const beforeResult = this.parsePassThrough(before, commands);
|
|
@@ -68,17 +173,20 @@ export class OutputParser {
|
|
|
68
173
|
}
|
|
69
174
|
// Enter block mode
|
|
70
175
|
this.inBlock = true;
|
|
176
|
+
this.blockType = blockStartIdentifier === BLOCK_METADATA_START ? 'RELAY_METADATA' : 'RELAY';
|
|
71
177
|
this.blockBuffer = after;
|
|
72
178
|
// Check size limit before processing
|
|
73
179
|
if (this.blockBuffer.length > this.options.maxBlockBytes) {
|
|
74
180
|
console.error('[parser] Block too large, discarding');
|
|
75
181
|
this.inBlock = false;
|
|
76
182
|
this.blockBuffer = '';
|
|
183
|
+
this.blockType = null;
|
|
77
184
|
return { commands, output };
|
|
78
185
|
}
|
|
79
186
|
// Check if block ends in same chunk
|
|
80
|
-
|
|
81
|
-
|
|
187
|
+
const blockEndPattern = this.blockType === 'RELAY_METADATA' ? BLOCK_METADATA_END : BLOCK_END;
|
|
188
|
+
if (blockEndPattern.test(this.blockBuffer)) {
|
|
189
|
+
const blockResult = this.finishBlock(this.blockType);
|
|
82
190
|
if (blockResult.command) {
|
|
83
191
|
commands.push(blockResult.command);
|
|
84
192
|
}
|
|
@@ -96,59 +204,66 @@ export class OutputParser {
|
|
|
96
204
|
return { commands, output };
|
|
97
205
|
}
|
|
98
206
|
/**
|
|
99
|
-
* Find [[RELAY]] that's at the start of a line and not inside a code fence.
|
|
100
|
-
* Returns the index, or -1 if not found.
|
|
207
|
+
* Find [[RELAY_METADATA]] or [[RELAY]] that's at the start of a line and not inside a code fence.
|
|
208
|
+
* Returns the index and identifier, or -1 and null if not found.
|
|
101
209
|
*/
|
|
102
210
|
findBlockStart(data) {
|
|
103
211
|
// Track code fence state through the data
|
|
104
212
|
let inFence = this.inCodeFence;
|
|
105
213
|
let searchStart = 0;
|
|
214
|
+
// Prioritize RELAY_METADATA over RELAY
|
|
215
|
+
const blockIdentifiers = [BLOCK_METADATA_START, '[[RELAY]]'];
|
|
106
216
|
while (searchStart < data.length) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
217
|
+
let earliestBlockIdx = -1;
|
|
218
|
+
let earliestBlockIdentifier = null;
|
|
219
|
+
for (const identifier of blockIdentifiers) {
|
|
220
|
+
const currentBlockIdx = data.indexOf(identifier, searchStart);
|
|
221
|
+
if (currentBlockIdx !== -1 && (earliestBlockIdx === -1 || currentBlockIdx < earliestBlockIdx)) {
|
|
222
|
+
earliestBlockIdx = currentBlockIdx;
|
|
223
|
+
earliestBlockIdentifier = identifier;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// No more blocks found
|
|
227
|
+
if (earliestBlockIdx === -1) {
|
|
112
228
|
// Still update code fence state for remaining data
|
|
229
|
+
let fenceIdx = data.indexOf('```', searchStart);
|
|
113
230
|
while (fenceIdx !== -1) {
|
|
114
|
-
const nextFence = data.indexOf('```', searchStart);
|
|
115
|
-
if (nextFence === -1)
|
|
116
|
-
break;
|
|
117
231
|
inFence = !inFence;
|
|
118
|
-
searchStart =
|
|
232
|
+
searchStart = fenceIdx + 3;
|
|
233
|
+
fenceIdx = data.indexOf('```', searchStart);
|
|
119
234
|
}
|
|
120
|
-
return -1;
|
|
235
|
+
return { index: -1, identifier: null };
|
|
121
236
|
}
|
|
122
|
-
// Process any code fences before this
|
|
237
|
+
// Process any code fences before this block
|
|
123
238
|
let tempIdx = searchStart;
|
|
124
239
|
while (true) {
|
|
125
240
|
const nextFence = data.indexOf('```', tempIdx);
|
|
126
|
-
if (nextFence === -1 || nextFence >=
|
|
241
|
+
if (nextFence === -1 || nextFence >= earliestBlockIdx)
|
|
127
242
|
break;
|
|
128
243
|
inFence = !inFence;
|
|
129
244
|
tempIdx = nextFence + 3;
|
|
130
245
|
}
|
|
131
|
-
// If we're inside a code fence, skip this
|
|
246
|
+
// If we're inside a code fence, skip this block
|
|
132
247
|
if (inFence) {
|
|
133
|
-
searchStart =
|
|
248
|
+
searchStart = earliestBlockIdx + (earliestBlockIdentifier?.length ?? 0); // Skip past the block
|
|
134
249
|
continue;
|
|
135
250
|
}
|
|
136
|
-
// Check if
|
|
137
|
-
if (
|
|
138
|
-
return 0; // At very start
|
|
251
|
+
// Check if block is at start of a line
|
|
252
|
+
if (earliestBlockIdx === 0) {
|
|
253
|
+
return { index: 0, identifier: earliestBlockIdentifier }; // At very start
|
|
139
254
|
}
|
|
140
255
|
// Look backwards for the start of line
|
|
141
|
-
const
|
|
142
|
-
const lastNewline =
|
|
143
|
-
const lineStart =
|
|
144
|
-
// Must be only whitespace before
|
|
256
|
+
const beforeBlock = data.substring(0, earliestBlockIdx);
|
|
257
|
+
const lastNewline = beforeBlock.lastIndexOf('\n');
|
|
258
|
+
const lineStart = beforeBlock.substring(lastNewline + 1);
|
|
259
|
+
// Must be only whitespace before block on this line
|
|
145
260
|
if (/^\s*$/.test(lineStart)) {
|
|
146
|
-
return
|
|
261
|
+
return { index: earliestBlockIdx, identifier: earliestBlockIdentifier };
|
|
147
262
|
}
|
|
148
263
|
// Not at start of line, keep searching
|
|
149
|
-
searchStart =
|
|
264
|
+
searchStart = earliestBlockIdx + (earliestBlockIdentifier?.length ?? 0);
|
|
150
265
|
}
|
|
151
|
-
return -1;
|
|
266
|
+
return { index: -1, identifier: null };
|
|
152
267
|
}
|
|
153
268
|
/**
|
|
154
269
|
* Parse data in pass-through mode - TRUE pass-through for terminal rendering.
|
|
@@ -164,6 +279,79 @@ export class OutputParser {
|
|
|
164
279
|
// Simple approach: split data, check each line (complete or not), rebuild output
|
|
165
280
|
const lines = data.split('\n');
|
|
166
281
|
const hasTrailingNewline = data.endsWith('\n');
|
|
282
|
+
const isInlineStart = (line) => {
|
|
283
|
+
return this.inlineRelayPattern.test(line) || this.inlineThinkingPattern.test(line);
|
|
284
|
+
};
|
|
285
|
+
const isFencedInlineStart = (line) => {
|
|
286
|
+
const stripped = stripAnsi(line);
|
|
287
|
+
const relayMatch = stripped.match(this.fencedRelayPattern);
|
|
288
|
+
if (relayMatch) {
|
|
289
|
+
const [, target, threadId] = relayMatch;
|
|
290
|
+
const { to, project } = parseTarget(target);
|
|
291
|
+
return { target: to, thread: threadId || undefined, project, kind: 'message' };
|
|
292
|
+
}
|
|
293
|
+
const thinkingMatch = stripped.match(this.fencedThinkingPattern);
|
|
294
|
+
if (thinkingMatch) {
|
|
295
|
+
const [, target, threadId] = thinkingMatch;
|
|
296
|
+
const { to, project } = parseTarget(target);
|
|
297
|
+
return { target: to, thread: threadId || undefined, project, kind: 'thinking' };
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
const isBlockMarker = (line) => {
|
|
302
|
+
return CODE_FENCE.test(line) || line.includes('[[RELAY]]') || BLOCK_END.test(line);
|
|
303
|
+
};
|
|
304
|
+
const shouldStopContinuation = (line, continuationCount, lines, currentIndex) => {
|
|
305
|
+
const trimmed = line.trim();
|
|
306
|
+
if (isInlineStart(line))
|
|
307
|
+
return true;
|
|
308
|
+
if (isFencedInlineStart(line))
|
|
309
|
+
return true;
|
|
310
|
+
if (isBlockMarker(line))
|
|
311
|
+
return true;
|
|
312
|
+
if (PROMPTISH_LINE.test(trimmed))
|
|
313
|
+
return true;
|
|
314
|
+
if (RELAY_INJECTION_PREFIX.test(line))
|
|
315
|
+
return true; // Avoid swallowing injected inbound messages
|
|
316
|
+
// Allow blank lines only in structured content like tables or between numbered sections
|
|
317
|
+
if (trimmed === '') {
|
|
318
|
+
// If we haven't started continuation yet, stop on blank
|
|
319
|
+
if (continuationCount === 0)
|
|
320
|
+
return true;
|
|
321
|
+
// Look ahead to see if there's more content that looks like structured markdown
|
|
322
|
+
for (let j = currentIndex + 1; j < lines.length; j++) {
|
|
323
|
+
const nextLine = lines[j].trim();
|
|
324
|
+
if (nextLine === '') {
|
|
325
|
+
// Double blank line always stops
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
// Only continue for table rows or numbered list items after blank
|
|
329
|
+
if (/^\|/.test(nextLine))
|
|
330
|
+
return false; // Table row
|
|
331
|
+
if (/^\d+[.)]\s/.test(nextLine))
|
|
332
|
+
return false; // Numbered list like "1." or "2)"
|
|
333
|
+
// Stop for anything else after a blank line
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
return true; // No more content, stop
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
};
|
|
340
|
+
const isContinuationLine = (original, stripped, prevStripped, continuationCount) => {
|
|
341
|
+
// Note: shouldStopContinuation is already checked in the main loop before calling this
|
|
342
|
+
if (/^[ \t]/.test(original))
|
|
343
|
+
return true; // Indented lines from TUI wrapping
|
|
344
|
+
if (BULLET_OR_NUMBERED_LIST.test(stripped))
|
|
345
|
+
return true; // Bullet/numbered lists after ->relay:
|
|
346
|
+
const prevTrimmed = prevStripped.trimEnd();
|
|
347
|
+
const prevSuggestsContinuation = prevTrimmed !== '' && /[:;,\-–—…]$/.test(prevTrimmed);
|
|
348
|
+
if (prevSuggestsContinuation)
|
|
349
|
+
return true;
|
|
350
|
+
// If we've already continued once, allow subsequent lines until a stop condition
|
|
351
|
+
if (continuationCount > 0)
|
|
352
|
+
return true;
|
|
353
|
+
return false;
|
|
354
|
+
};
|
|
167
355
|
const outputLines = [];
|
|
168
356
|
let strippedCount = 0;
|
|
169
357
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -179,6 +367,62 @@ export class OutputParser {
|
|
|
179
367
|
outputLines.push(line);
|
|
180
368
|
continue;
|
|
181
369
|
}
|
|
370
|
+
// Skip Claude extended thinking blocks - don't parse or output their content
|
|
371
|
+
// Check for thinking end first (to handle end tag on same line as start)
|
|
372
|
+
if (this.inThinkingBlock) {
|
|
373
|
+
if (THINKING_END.test(line)) {
|
|
374
|
+
this.inThinkingBlock = false;
|
|
375
|
+
}
|
|
376
|
+
// Skip this line - don't output thinking content
|
|
377
|
+
strippedCount++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// Check for thinking start
|
|
381
|
+
if (THINKING_START.test(line)) {
|
|
382
|
+
this.inThinkingBlock = true;
|
|
383
|
+
// Also check if it ends on the same line (inline thinking block)
|
|
384
|
+
if (THINKING_END.test(line)) {
|
|
385
|
+
this.inThinkingBlock = false;
|
|
386
|
+
}
|
|
387
|
+
// Skip this line - don't output thinking content
|
|
388
|
+
strippedCount++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Check for fenced inline start: ->relay:Target <<<
|
|
392
|
+
const fencedStart = isFencedInlineStart(line);
|
|
393
|
+
if (fencedStart && this.options.enableInline) {
|
|
394
|
+
// Enter fenced inline mode
|
|
395
|
+
this.inFencedInline = true;
|
|
396
|
+
this.fencedInlineTarget = fencedStart.target;
|
|
397
|
+
this.fencedInlineThread = fencedStart.thread;
|
|
398
|
+
this.fencedInlineProject = fencedStart.project;
|
|
399
|
+
this.fencedInlineKind = fencedStart.kind;
|
|
400
|
+
this.fencedInlineBuffer = '';
|
|
401
|
+
this.fencedInlineRaw = [line];
|
|
402
|
+
// Process remaining lines in fenced mode
|
|
403
|
+
if (i + 1 < lines.length) {
|
|
404
|
+
// Don't double-add trailing newline - the empty string at end of lines array
|
|
405
|
+
// already accounts for it when we join
|
|
406
|
+
const remainingLines = lines.slice(i + 1);
|
|
407
|
+
const remaining = remainingLines.join('\n') + (hasTrailingNewline && remainingLines[remainingLines.length - 1] !== '' ? '\n' : '');
|
|
408
|
+
const result = this.parseFencedInlineMode(remaining, commands);
|
|
409
|
+
strippedCount++;
|
|
410
|
+
// Combine output
|
|
411
|
+
let output = outputLines.join('\n');
|
|
412
|
+
if (hasTrailingNewline && outputLines.length > 0 && !this.inFencedInline) {
|
|
413
|
+
output += '\n';
|
|
414
|
+
}
|
|
415
|
+
output += result.output;
|
|
416
|
+
return output;
|
|
417
|
+
}
|
|
418
|
+
// No more lines - waiting for more data
|
|
419
|
+
strippedCount++;
|
|
420
|
+
let output = outputLines.join('\n');
|
|
421
|
+
if (hasTrailingNewline && outputLines.length > 0) {
|
|
422
|
+
output += '\n';
|
|
423
|
+
}
|
|
424
|
+
return output;
|
|
425
|
+
}
|
|
182
426
|
if (line.length > 0) {
|
|
183
427
|
// Only check complete lines for relay commands.
|
|
184
428
|
const result = this.processLine(line);
|
|
@@ -187,6 +431,7 @@ export class OutputParser {
|
|
|
187
431
|
let body = result.command.body;
|
|
188
432
|
const rawLines = [result.command.raw];
|
|
189
433
|
let consumed = 0;
|
|
434
|
+
let continuationLines = 0;
|
|
190
435
|
while (i + 1 < lines.length) {
|
|
191
436
|
const nextIsLast = i + 1 === lines.length - 1;
|
|
192
437
|
const nextLine = lines[i + 1];
|
|
@@ -195,26 +440,21 @@ export class OutputParser {
|
|
|
195
440
|
break;
|
|
196
441
|
}
|
|
197
442
|
const nextStripped = stripAnsi(nextLine);
|
|
198
|
-
|
|
199
|
-
if
|
|
443
|
+
const prevStripped = stripAnsi(rawLines[rawLines.length - 1] ?? '');
|
|
444
|
+
// Stop if this line clearly marks a new block, prompt, or inline command
|
|
445
|
+
if (shouldStopContinuation(nextStripped, continuationLines, lines, i + 1)) {
|
|
200
446
|
break;
|
|
201
447
|
}
|
|
202
|
-
|
|
203
|
-
if (INLINE_RELAY.test(nextStripped) ||
|
|
204
|
-
INLINE_THINKING.test(nextStripped) ||
|
|
205
|
-
CODE_FENCE.test(nextStripped) ||
|
|
206
|
-
nextStripped.includes('[[RELAY]]') ||
|
|
207
|
-
BLOCK_END.test(nextStripped)) {
|
|
448
|
+
if (continuationLines >= MAX_INLINE_CONTINUATION_LINES) {
|
|
208
449
|
break;
|
|
209
450
|
}
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
// Non-indented lines are regular output, not continuation
|
|
213
|
-
if (!/^[ \t]/.test(nextLine)) {
|
|
451
|
+
// Consume as continuation if it looks like it belongs to the ->relay message
|
|
452
|
+
if (!isContinuationLine(nextLine, nextStripped, prevStripped, continuationLines)) {
|
|
214
453
|
break;
|
|
215
454
|
}
|
|
216
455
|
consumed++;
|
|
217
456
|
i++; // Skip the consumed continuation line
|
|
457
|
+
continuationLines++;
|
|
218
458
|
body += '\n' + nextLine;
|
|
219
459
|
rawLines.push(nextLine);
|
|
220
460
|
}
|
|
@@ -250,18 +490,20 @@ export class OutputParser {
|
|
|
250
490
|
/**
|
|
251
491
|
* Parse while inside a [[RELAY]] block - buffer until we see [[/RELAY]].
|
|
252
492
|
*/
|
|
253
|
-
parseInBlockMode(data, commands) {
|
|
493
|
+
parseInBlockMode(data, commands, blockType) {
|
|
254
494
|
this.blockBuffer += data;
|
|
255
495
|
// Check size limit
|
|
256
496
|
if (this.blockBuffer.length > this.options.maxBlockBytes) {
|
|
257
497
|
console.error('[parser] Block too large, discarding');
|
|
258
498
|
this.inBlock = false;
|
|
259
499
|
this.blockBuffer = '';
|
|
500
|
+
this.blockType = null;
|
|
260
501
|
return { commands, output: '' };
|
|
261
502
|
}
|
|
262
503
|
// Check for block end
|
|
263
|
-
|
|
264
|
-
|
|
504
|
+
const blockEndPattern = blockType === 'RELAY_METADATA' ? BLOCK_METADATA_END : BLOCK_END;
|
|
505
|
+
if (blockEndPattern.test(this.blockBuffer)) {
|
|
506
|
+
const result = this.finishBlock(blockType);
|
|
265
507
|
if (result.command) {
|
|
266
508
|
commands.push(result.command);
|
|
267
509
|
}
|
|
@@ -297,35 +539,41 @@ export class OutputParser {
|
|
|
297
539
|
return { command: null, output: line };
|
|
298
540
|
}
|
|
299
541
|
// Check for escaped inline (on stripped text)
|
|
300
|
-
const escapeMatch = stripped.match(
|
|
542
|
+
const escapeMatch = stripped.match(this.escapePattern);
|
|
301
543
|
if (escapeMatch) {
|
|
302
|
-
// Output with escape removed
|
|
303
|
-
const unescaped = line.replace(
|
|
544
|
+
// Output with escape removed (remove the backslash before the prefix)
|
|
545
|
+
const unescaped = line.replace(/\\/, '');
|
|
304
546
|
return { command: null, output: unescaped };
|
|
305
547
|
}
|
|
306
548
|
// Check for inline relay (on stripped text)
|
|
307
549
|
if (this.options.enableInline) {
|
|
308
|
-
const relayMatch = stripped.match(
|
|
550
|
+
const relayMatch = stripped.match(this.inlineRelayPattern);
|
|
309
551
|
if (relayMatch) {
|
|
310
|
-
const [raw, target, body] = relayMatch;
|
|
552
|
+
const [raw, target, threadId, body] = relayMatch;
|
|
553
|
+
const { to, project } = parseTarget(target);
|
|
311
554
|
return {
|
|
312
555
|
command: {
|
|
313
|
-
to
|
|
556
|
+
to,
|
|
314
557
|
kind: 'message',
|
|
315
558
|
body,
|
|
559
|
+
thread: threadId || undefined, // undefined if no thread specified
|
|
560
|
+
project, // undefined if local, set if cross-project
|
|
316
561
|
raw,
|
|
317
562
|
},
|
|
318
563
|
output: null, // Don't output relay commands
|
|
319
564
|
};
|
|
320
565
|
}
|
|
321
|
-
const thinkingMatch = stripped.match(
|
|
566
|
+
const thinkingMatch = stripped.match(this.inlineThinkingPattern);
|
|
322
567
|
if (thinkingMatch) {
|
|
323
|
-
const [raw, target, body] = thinkingMatch;
|
|
568
|
+
const [raw, target, threadId, body] = thinkingMatch;
|
|
569
|
+
const { to, project } = parseTarget(target);
|
|
324
570
|
return {
|
|
325
571
|
command: {
|
|
326
|
-
to
|
|
572
|
+
to,
|
|
327
573
|
kind: 'thinking',
|
|
328
574
|
body,
|
|
575
|
+
thread: threadId || undefined,
|
|
576
|
+
project,
|
|
329
577
|
raw,
|
|
330
578
|
},
|
|
331
579
|
output: null,
|
|
@@ -339,34 +587,211 @@ export class OutputParser {
|
|
|
339
587
|
* Finish processing a block and extract command.
|
|
340
588
|
* Returns the command (if valid) and any remaining content after [[/RELAY]].
|
|
341
589
|
*/
|
|
342
|
-
finishBlock() {
|
|
343
|
-
const
|
|
590
|
+
finishBlock(blockType) {
|
|
591
|
+
const blockEndIdentifier = blockType === 'RELAY_METADATA' ? BLOCK_METADATA_END.source : BLOCK_END.source;
|
|
592
|
+
const endIdx = this.blockBuffer.indexOf(blockEndIdentifier.replace(/\\/g, '')); // Remove regex escapes for indexOf
|
|
344
593
|
const jsonStr = this.blockBuffer.substring(0, endIdx).trim();
|
|
345
|
-
const remaining = this.blockBuffer.substring(endIdx + '
|
|
594
|
+
const remaining = this.blockBuffer.substring(endIdx + blockEndIdentifier.replace(/\\/g, '').length) || null;
|
|
346
595
|
this.inBlock = false;
|
|
347
596
|
this.blockBuffer = '';
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return { command: null, remaining };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
597
|
+
this.blockType = null;
|
|
598
|
+
if (blockType === 'RELAY_METADATA') {
|
|
599
|
+
try {
|
|
600
|
+
const metadata = JSON.parse(jsonStr);
|
|
601
|
+
this.lastParsedMetadata = metadata;
|
|
602
|
+
return { command: null, remaining, metadata };
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
console.error('[parser] Invalid JSON in RELAY_METADATA block:', err);
|
|
606
|
+
this.lastParsedMetadata = null;
|
|
607
|
+
return { command: null, remaining, metadata: null };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else { // blockType === 'RELAY'
|
|
611
|
+
try {
|
|
612
|
+
const parsed = JSON.parse(jsonStr);
|
|
613
|
+
// Validate required fields
|
|
614
|
+
if (!parsed.to || !parsed.type) {
|
|
615
|
+
console.error('[parser] Block missing required fields (to, type)');
|
|
616
|
+
this.lastParsedMetadata = null; // Clear metadata even if RELAY block is invalid
|
|
617
|
+
return { command: null, remaining, metadata: null };
|
|
618
|
+
}
|
|
619
|
+
// Handle cross-project syntax in block format
|
|
620
|
+
// Supports both explicit "project" field and "project:agent" in "to" field
|
|
621
|
+
let to = parsed.to;
|
|
622
|
+
let project = parsed.project;
|
|
623
|
+
if (!project && typeof to === 'string') {
|
|
624
|
+
// Check if "to" field uses project:agent syntax
|
|
625
|
+
const targetParsed = parseTarget(to);
|
|
626
|
+
to = targetParsed.to;
|
|
627
|
+
project = targetParsed.project;
|
|
628
|
+
}
|
|
629
|
+
const command = {
|
|
630
|
+
to,
|
|
358
631
|
kind: parsed.type,
|
|
359
632
|
body: parsed.body ?? parsed.text ?? '',
|
|
360
633
|
data: parsed.data,
|
|
634
|
+
thread: parsed.thread || undefined,
|
|
635
|
+
project: project || undefined,
|
|
361
636
|
raw: jsonStr,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
637
|
+
meta: this.lastParsedMetadata || undefined, // Attach last parsed metadata
|
|
638
|
+
};
|
|
639
|
+
this.lastParsedMetadata = null; // Clear after use
|
|
640
|
+
return {
|
|
641
|
+
command,
|
|
642
|
+
remaining,
|
|
643
|
+
metadata: null,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
console.error('[parser] Invalid JSON in RELAY block:', err);
|
|
648
|
+
this.lastParsedMetadata = null;
|
|
649
|
+
return { command: null, remaining, metadata: null };
|
|
650
|
+
}
|
|
365
651
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Parse while inside a fenced inline block (->relay:Target <<< ... >>>).
|
|
655
|
+
* Accumulates lines until >>> is seen on its own line.
|
|
656
|
+
*/
|
|
657
|
+
parseFencedInlineMode(data, commands) {
|
|
658
|
+
const lines = data.split('\n');
|
|
659
|
+
const hasTrailingNewline = data.endsWith('\n');
|
|
660
|
+
let output = '';
|
|
661
|
+
let consecutiveBlankLines = 0;
|
|
662
|
+
for (let i = 0; i < lines.length; i++) {
|
|
663
|
+
const line = lines[i];
|
|
664
|
+
const isLastLine = i === lines.length - 1;
|
|
665
|
+
const stripped = stripAnsi(line);
|
|
666
|
+
// Track consecutive blank lines for auto-close
|
|
667
|
+
if (stripped === '') {
|
|
668
|
+
consecutiveBlankLines++;
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
consecutiveBlankLines = 0;
|
|
672
|
+
}
|
|
673
|
+
// Auto-close on double blank line (agent forgot >>>)
|
|
674
|
+
// Only if we have actual content to send
|
|
675
|
+
if (consecutiveBlankLines >= 2 && this.fencedInlineBuffer.trim().length > 0) {
|
|
676
|
+
const body = this.fencedInlineBuffer.trim();
|
|
677
|
+
const command = {
|
|
678
|
+
to: this.fencedInlineTarget,
|
|
679
|
+
kind: this.fencedInlineKind,
|
|
680
|
+
body,
|
|
681
|
+
thread: this.fencedInlineThread,
|
|
682
|
+
project: this.fencedInlineProject,
|
|
683
|
+
raw: this.fencedInlineRaw.join('\n'),
|
|
684
|
+
};
|
|
685
|
+
commands.push(command);
|
|
686
|
+
// Reset fenced inline state
|
|
687
|
+
this.inFencedInline = false;
|
|
688
|
+
this.fencedInlineBuffer = '';
|
|
689
|
+
this.fencedInlineTarget = '';
|
|
690
|
+
this.fencedInlineThread = undefined;
|
|
691
|
+
this.fencedInlineProject = undefined;
|
|
692
|
+
this.fencedInlineRaw = [];
|
|
693
|
+
this.fencedInlineKind = 'message';
|
|
694
|
+
// Process remaining lines in normal mode
|
|
695
|
+
const remainingLines = lines.slice(i);
|
|
696
|
+
const remaining = remainingLines.join('\n') + (hasTrailingNewline ? '\n' : '');
|
|
697
|
+
const result = this.parse(remaining);
|
|
698
|
+
commands.push(...result.commands);
|
|
699
|
+
return { commands, output: result.output };
|
|
700
|
+
}
|
|
701
|
+
// Check if a new relay command started (means previous fenced block was never closed)
|
|
702
|
+
// This prevents a stuck fenced block from swallowing all subsequent messages
|
|
703
|
+
// Note: This is an expected recovery path, not an error - don't log to avoid noise
|
|
704
|
+
if (this.inlineRelayPattern.test(stripped) || this.fencedRelayPattern.test(stripped)) {
|
|
705
|
+
// Discard the incomplete fenced block
|
|
706
|
+
this.inFencedInline = false;
|
|
707
|
+
this.fencedInlineBuffer = '';
|
|
708
|
+
this.fencedInlineTarget = '';
|
|
709
|
+
this.fencedInlineThread = undefined;
|
|
710
|
+
this.fencedInlineProject = undefined;
|
|
711
|
+
this.fencedInlineRaw = [];
|
|
712
|
+
this.fencedInlineKind = 'message';
|
|
713
|
+
// Process remaining lines (including this one) in normal mode
|
|
714
|
+
const remainingLines = lines.slice(i);
|
|
715
|
+
const remaining = remainingLines.join('\n') + (hasTrailingNewline ? '\n' : '');
|
|
716
|
+
const result = this.parse(remaining);
|
|
717
|
+
commands.push(...result.commands);
|
|
718
|
+
return { commands, output: result.output };
|
|
719
|
+
}
|
|
720
|
+
// Check if this line closes the fenced block
|
|
721
|
+
if (FENCE_END.test(stripped)) {
|
|
722
|
+
// Complete the fenced inline command
|
|
723
|
+
const body = this.fencedInlineBuffer.trim();
|
|
724
|
+
this.fencedInlineRaw.push(line);
|
|
725
|
+
const command = {
|
|
726
|
+
to: this.fencedInlineTarget,
|
|
727
|
+
kind: this.fencedInlineKind,
|
|
728
|
+
body,
|
|
729
|
+
thread: this.fencedInlineThread,
|
|
730
|
+
project: this.fencedInlineProject,
|
|
731
|
+
raw: this.fencedInlineRaw.join('\n'),
|
|
732
|
+
};
|
|
733
|
+
commands.push(command);
|
|
734
|
+
// Reset fenced inline state
|
|
735
|
+
this.inFencedInline = false;
|
|
736
|
+
this.fencedInlineBuffer = '';
|
|
737
|
+
this.fencedInlineTarget = '';
|
|
738
|
+
this.fencedInlineThread = undefined;
|
|
739
|
+
this.fencedInlineProject = undefined;
|
|
740
|
+
this.fencedInlineRaw = [];
|
|
741
|
+
this.fencedInlineKind = 'message';
|
|
742
|
+
// Process remaining lines after the fence close
|
|
743
|
+
// Only process if there's actual content after the closing fence
|
|
744
|
+
const remainingLines = lines.slice(i + 1);
|
|
745
|
+
// Filter out trailing empty string from split
|
|
746
|
+
const hasContent = remainingLines.some((l, idx) => l.trim() !== '' || (idx < remainingLines.length - 1));
|
|
747
|
+
if (hasContent) {
|
|
748
|
+
const remaining = remainingLines.join('\n') + (hasTrailingNewline ? '\n' : '');
|
|
749
|
+
const result = this.parse(remaining);
|
|
750
|
+
commands.push(...result.commands);
|
|
751
|
+
output += result.output;
|
|
752
|
+
}
|
|
753
|
+
return { commands, output };
|
|
754
|
+
}
|
|
755
|
+
// Accumulate this line into the buffer (preserving blank lines within content)
|
|
756
|
+
// But skip trailing empty line from split (when input ends with \n)
|
|
757
|
+
const isTrailingEmpty = isLastLine && line === '' && hasTrailingNewline;
|
|
758
|
+
if (!isTrailingEmpty) {
|
|
759
|
+
if (this.fencedInlineBuffer.length > 0) {
|
|
760
|
+
this.fencedInlineBuffer += '\n' + line;
|
|
761
|
+
}
|
|
762
|
+
else if (line.trim() !== '') {
|
|
763
|
+
// Start accumulating from first non-blank line
|
|
764
|
+
this.fencedInlineBuffer = line;
|
|
765
|
+
}
|
|
766
|
+
this.fencedInlineRaw.push(line);
|
|
767
|
+
}
|
|
768
|
+
// Check size limit
|
|
769
|
+
if (this.fencedInlineBuffer.length > this.options.maxBlockBytes) {
|
|
770
|
+
console.error('[parser] Fenced inline block too large, discarding');
|
|
771
|
+
this.inFencedInline = false;
|
|
772
|
+
this.fencedInlineBuffer = '';
|
|
773
|
+
this.fencedInlineTarget = '';
|
|
774
|
+
this.fencedInlineThread = undefined;
|
|
775
|
+
this.fencedInlineProject = undefined;
|
|
776
|
+
this.fencedInlineRaw = [];
|
|
777
|
+
this.fencedInlineKind = 'message';
|
|
778
|
+
return { commands, output: '' };
|
|
779
|
+
}
|
|
780
|
+
// Check line count limit - prevents stuck fenced mode from blocking all messages
|
|
781
|
+
if (this.fencedInlineRaw.length > MAX_FENCED_LINES) {
|
|
782
|
+
console.error('[parser] Fenced inline block exceeded max lines, discarding');
|
|
783
|
+
this.inFencedInline = false;
|
|
784
|
+
this.fencedInlineBuffer = '';
|
|
785
|
+
this.fencedInlineTarget = '';
|
|
786
|
+
this.fencedInlineThread = undefined;
|
|
787
|
+
this.fencedInlineProject = undefined;
|
|
788
|
+
this.fencedInlineRaw = [];
|
|
789
|
+
this.fencedInlineKind = 'message';
|
|
790
|
+
return { commands, output: '' };
|
|
791
|
+
}
|
|
369
792
|
}
|
|
793
|
+
// Still waiting for >>> - return empty output (content is buffered)
|
|
794
|
+
return { commands, output: '' };
|
|
370
795
|
}
|
|
371
796
|
/**
|
|
372
797
|
* Flush any remaining buffer (call on stream end).
|
|
@@ -375,7 +800,17 @@ export class OutputParser {
|
|
|
375
800
|
const result = this.parse('\n');
|
|
376
801
|
this.inBlock = false;
|
|
377
802
|
this.blockBuffer = '';
|
|
803
|
+
this.blockType = null;
|
|
804
|
+
this.lastParsedMetadata = null;
|
|
378
805
|
this.inCodeFence = false;
|
|
806
|
+
this.inThinkingBlock = false;
|
|
807
|
+
this.inFencedInline = false;
|
|
808
|
+
this.fencedInlineBuffer = '';
|
|
809
|
+
this.fencedInlineTarget = '';
|
|
810
|
+
this.fencedInlineThread = undefined;
|
|
811
|
+
this.fencedInlineProject = undefined;
|
|
812
|
+
this.fencedInlineRaw = [];
|
|
813
|
+
this.fencedInlineKind = 'message';
|
|
379
814
|
return result;
|
|
380
815
|
}
|
|
381
816
|
/**
|
|
@@ -384,7 +819,17 @@ export class OutputParser {
|
|
|
384
819
|
reset() {
|
|
385
820
|
this.inBlock = false;
|
|
386
821
|
this.blockBuffer = '';
|
|
822
|
+
this.blockType = null;
|
|
823
|
+
this.lastParsedMetadata = null;
|
|
387
824
|
this.inCodeFence = false;
|
|
825
|
+
this.inThinkingBlock = false;
|
|
826
|
+
this.inFencedInline = false;
|
|
827
|
+
this.fencedInlineBuffer = '';
|
|
828
|
+
this.fencedInlineTarget = '';
|
|
829
|
+
this.fencedInlineThread = undefined;
|
|
830
|
+
this.fencedInlineProject = undefined;
|
|
831
|
+
this.fencedInlineRaw = [];
|
|
832
|
+
this.fencedInlineKind = 'message';
|
|
388
833
|
}
|
|
389
834
|
}
|
|
390
835
|
/**
|
|
@@ -394,4 +839,95 @@ export function formatIncomingMessage(from, body, kind = 'message') {
|
|
|
394
839
|
const prefix = kind === 'thinking' ? '[THINKING]' : '[MSG]';
|
|
395
840
|
return `\n${prefix} from ${from}: ${body}\n`;
|
|
396
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Parse [[RELAY_METADATA]]...[[/RELAY_METADATA]] blocks from agent output.
|
|
844
|
+
* Agents can output metadata to enhance messages.
|
|
845
|
+
*
|
|
846
|
+
* Format:
|
|
847
|
+
* [[RELAY_METADATA]]
|
|
848
|
+
* {
|
|
849
|
+
* "subject": "Task update",
|
|
850
|
+
* "importance": 80,
|
|
851
|
+
* "replyTo": "msg-abc123",
|
|
852
|
+
* "ackRequired": true
|
|
853
|
+
* }
|
|
854
|
+
* [[/RELAY_METADATA]]
|
|
855
|
+
*/
|
|
856
|
+
export function parseRelayMetadataFromOutput(output) {
|
|
857
|
+
const match = output.match(/\[\[RELAY_METADATA\]\]([\s\S]*?)\[\[\/RELAY_METADATA\]\]/);
|
|
858
|
+
if (!match) {
|
|
859
|
+
return { found: false, valid: false, metadata: null, rawContent: null };
|
|
860
|
+
}
|
|
861
|
+
const rawContent = match[1].trim();
|
|
862
|
+
try {
|
|
863
|
+
const metadata = JSON.parse(rawContent);
|
|
864
|
+
return { found: true, valid: true, metadata, rawContent };
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
return { found: true, valid: false, metadata: null, rawContent };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Parse [[SUMMARY]]...[[/SUMMARY]] blocks from agent output.
|
|
872
|
+
* Agents can output summaries to keep a running context of their work.
|
|
873
|
+
*
|
|
874
|
+
* Format:
|
|
875
|
+
* [[SUMMARY]]
|
|
876
|
+
* {
|
|
877
|
+
* "currentTask": "Working on auth module",
|
|
878
|
+
* "context": "Completed login flow, now implementing logout",
|
|
879
|
+
* "files": ["src/auth.ts", "src/session.ts"]
|
|
880
|
+
* }
|
|
881
|
+
* [[/SUMMARY]]
|
|
882
|
+
*/
|
|
883
|
+
export function parseSummaryFromOutput(output) {
|
|
884
|
+
const result = parseSummaryWithDetails(output);
|
|
885
|
+
return result.summary;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Parse SUMMARY block with full details for deduplication.
|
|
889
|
+
* Returns raw content to allow caller to dedupe before logging errors.
|
|
890
|
+
*/
|
|
891
|
+
export function parseSummaryWithDetails(output) {
|
|
892
|
+
const match = output.match(/\[\[SUMMARY\]\]([\s\S]*?)\[\[\/SUMMARY\]\]/);
|
|
893
|
+
if (!match) {
|
|
894
|
+
return { found: false, valid: false, summary: null, rawContent: null };
|
|
895
|
+
}
|
|
896
|
+
const rawContent = match[1].trim();
|
|
897
|
+
try {
|
|
898
|
+
const summary = JSON.parse(rawContent);
|
|
899
|
+
return { found: true, valid: true, summary, rawContent };
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
return { found: true, valid: false, summary: null, rawContent };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Parse [[SESSION_END]]...[[/SESSION_END]] blocks from agent output.
|
|
907
|
+
* Agents output this to explicitly mark their session as complete.
|
|
908
|
+
*
|
|
909
|
+
* Format:
|
|
910
|
+
* [[SESSION_END]]
|
|
911
|
+
* {"summary": "Completed auth module implementation", "completedTasks": ["login", "logout"]}
|
|
912
|
+
* [[/SESSION_END]]
|
|
913
|
+
*
|
|
914
|
+
* Or simply: [[SESSION_END]][[/SESSION_END]] for a clean close without summary.
|
|
915
|
+
*/
|
|
916
|
+
export function parseSessionEndFromOutput(output) {
|
|
917
|
+
const match = output.match(/\[\[SESSION_END\]\]([\s\S]*?)\[\[\/SESSION_END\]\]/);
|
|
918
|
+
if (!match) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
const content = match[1].trim();
|
|
922
|
+
if (!content) {
|
|
923
|
+
return {}; // Empty marker = session ended without summary
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
return JSON.parse(content);
|
|
927
|
+
}
|
|
928
|
+
catch {
|
|
929
|
+
// If not valid JSON, treat the content as a plain summary string
|
|
930
|
+
return { summary: content };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
397
933
|
//# sourceMappingURL=parser.js.map
|