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.
Files changed (140) hide show
  1. package/README.md +176 -6
  2. package/dist/bridge/config.d.ts +41 -0
  3. package/dist/bridge/config.d.ts.map +1 -0
  4. package/dist/bridge/config.js +143 -0
  5. package/dist/bridge/config.js.map +1 -0
  6. package/dist/bridge/index.d.ts +10 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +10 -0
  9. package/dist/bridge/index.js.map +1 -0
  10. package/dist/bridge/multi-project-client.d.ts +99 -0
  11. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  12. package/dist/bridge/multi-project-client.js +386 -0
  13. package/dist/bridge/multi-project-client.js.map +1 -0
  14. package/dist/bridge/spawner.d.ts +46 -0
  15. package/dist/bridge/spawner.d.ts.map +1 -0
  16. package/dist/bridge/spawner.js +223 -0
  17. package/dist/bridge/spawner.js.map +1 -0
  18. package/dist/bridge/types.d.ts +55 -0
  19. package/dist/bridge/types.d.ts.map +1 -0
  20. package/dist/bridge/types.js +6 -0
  21. package/dist/bridge/types.js.map +1 -0
  22. package/dist/bridge/utils.d.ts +30 -0
  23. package/dist/bridge/utils.d.ts.map +1 -0
  24. package/dist/bridge/utils.js +54 -0
  25. package/dist/bridge/utils.js.map +1 -0
  26. package/dist/cli/index.d.ts +2 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +906 -6
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/daemon/agent-registry.d.ts +60 -0
  31. package/dist/daemon/agent-registry.d.ts.map +1 -0
  32. package/dist/daemon/agent-registry.js +163 -0
  33. package/dist/daemon/agent-registry.js.map +1 -0
  34. package/dist/daemon/connection.d.ts +33 -1
  35. package/dist/daemon/connection.d.ts.map +1 -1
  36. package/dist/daemon/connection.js +86 -11
  37. package/dist/daemon/connection.js.map +1 -1
  38. package/dist/daemon/index.d.ts +2 -0
  39. package/dist/daemon/index.d.ts.map +1 -1
  40. package/dist/daemon/index.js +2 -0
  41. package/dist/daemon/index.js.map +1 -1
  42. package/dist/daemon/registry.d.ts +9 -0
  43. package/dist/daemon/registry.d.ts.map +1 -0
  44. package/dist/daemon/registry.js +9 -0
  45. package/dist/daemon/registry.js.map +1 -0
  46. package/dist/daemon/router.d.ts +61 -2
  47. package/dist/daemon/router.d.ts.map +1 -1
  48. package/dist/daemon/router.js +219 -4
  49. package/dist/daemon/router.js.map +1 -1
  50. package/dist/daemon/server.d.ts +9 -0
  51. package/dist/daemon/server.d.ts.map +1 -1
  52. package/dist/daemon/server.js +135 -16
  53. package/dist/daemon/server.js.map +1 -1
  54. package/dist/dashboard/metrics.d.ts +105 -0
  55. package/dist/dashboard/metrics.d.ts.map +1 -0
  56. package/dist/dashboard/metrics.js +192 -0
  57. package/dist/dashboard/metrics.js.map +1 -0
  58. package/dist/dashboard/needs-attention.d.ts +24 -0
  59. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  60. package/dist/dashboard/needs-attention.js +78 -0
  61. package/dist/dashboard/needs-attention.js.map +1 -0
  62. package/dist/dashboard/public/bridge.html +1272 -0
  63. package/dist/dashboard/public/index.html +2094 -347
  64. package/dist/dashboard/public/js/app.js +184 -0
  65. package/dist/dashboard/public/js/app.js.map +7 -0
  66. package/dist/dashboard/public/metrics.html +999 -0
  67. package/dist/dashboard/server.d.ts +14 -1
  68. package/dist/dashboard/server.d.ts.map +1 -1
  69. package/dist/dashboard/server.js +689 -16
  70. package/dist/dashboard/server.js.map +1 -1
  71. package/dist/dashboard/start.js +1 -1
  72. package/dist/dashboard/start.js.map +1 -1
  73. package/dist/dashboard-v2/index.d.ts +10 -0
  74. package/dist/dashboard-v2/index.d.ts.map +1 -0
  75. package/dist/dashboard-v2/index.js +54 -0
  76. package/dist/dashboard-v2/index.js.map +1 -0
  77. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  78. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  79. package/dist/dashboard-v2/lib/api.js +270 -0
  80. package/dist/dashboard-v2/lib/api.js.map +1 -0
  81. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  82. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  83. package/dist/dashboard-v2/lib/colors.js +198 -0
  84. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  85. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  86. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  87. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  88. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  89. package/dist/dashboard-v2/types/index.d.ts +154 -0
  90. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  91. package/dist/dashboard-v2/types/index.js +6 -0
  92. package/dist/dashboard-v2/types/index.js.map +1 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/protocol/types.d.ts +15 -1
  96. package/dist/protocol/types.d.ts.map +1 -1
  97. package/dist/storage/adapter.d.ts +74 -1
  98. package/dist/storage/adapter.d.ts.map +1 -1
  99. package/dist/storage/adapter.js +39 -0
  100. package/dist/storage/adapter.js.map +1 -1
  101. package/dist/storage/sqlite-adapter.d.ts +92 -1
  102. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  103. package/dist/storage/sqlite-adapter.js +615 -47
  104. package/dist/storage/sqlite-adapter.js.map +1 -1
  105. package/dist/utils/agent-config.d.ts +45 -0
  106. package/dist/utils/agent-config.d.ts.map +1 -0
  107. package/dist/utils/agent-config.js +118 -0
  108. package/dist/utils/agent-config.js.map +1 -0
  109. package/dist/utils/project-namespace.d.ts.map +1 -1
  110. package/dist/utils/project-namespace.js +22 -1
  111. package/dist/utils/project-namespace.js.map +1 -1
  112. package/dist/wrapper/client.d.ts +30 -3
  113. package/dist/wrapper/client.d.ts.map +1 -1
  114. package/dist/wrapper/client.js +85 -9
  115. package/dist/wrapper/client.js.map +1 -1
  116. package/dist/wrapper/parser.d.ts +127 -4
  117. package/dist/wrapper/parser.d.ts.map +1 -1
  118. package/dist/wrapper/parser.js +622 -86
  119. package/dist/wrapper/parser.js.map +1 -1
  120. package/dist/wrapper/tmux-wrapper.d.ts +136 -10
  121. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  122. package/dist/wrapper/tmux-wrapper.js +599 -79
  123. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  124. package/docs/AGENTS.md +132 -27
  125. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  126. package/docs/CHANGELOG.md +1 -1
  127. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  128. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  129. package/docs/DESIGN_V2.md +1079 -0
  130. package/docs/INTEGRATION-GUIDE.md +926 -0
  131. package/docs/MONETIZATION.md +1679 -0
  132. package/docs/PROPOSAL-trajectories.md +1582 -0
  133. package/docs/PROTOCOL.md +3 -3
  134. package/docs/SCALING_ANALYSIS.md +280 -0
  135. package/docs/TMUX_IMPLEMENTATION_NOTES.md +9 -9
  136. package/docs/TMUX_IMPROVEMENTS.md +968 -0
  137. package/docs/agent-relay-snippet.md +61 -0
  138. package/docs/competitive-analysis-mcp-agent-mail.md +389 -0
  139. package/docs/dashboard-v2-plan.md +179 -0
  140. package/package.json +10 -3
@@ -3,30 +3,101 @@
3
3
  * Extracts relay commands from agent terminal output.
4
4
  *
5
5
  * Supports two formats:
6
- * 1. Inline: @relay:<target> <message> (single line, start of line only)
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 \@relay: to output literal
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
- // Patterns
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
- const ESCAPE_PREFIX = /^(\s*)\\@(relay|thinking):/;
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 (or start of input)
59
- // and NOT inside a code fence
60
- const blockStartIdx = this.findBlockStart(data);
61
- if (this.options.enableBlock && blockStartIdx !== -1) {
62
- const before = data.substring(0, blockStartIdx);
63
- const after = data.substring(blockStartIdx + '[[RELAY]]'.length);
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
- if (BLOCK_END.test(this.blockBuffer)) {
81
- const blockResult = this.finishBlock();
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
- // Look for next [[RELAY]] or code fence
108
- const relayIdx = data.indexOf('[[RELAY]]', searchStart);
109
- const fenceIdx = data.indexOf('```', searchStart);
110
- // No more [[RELAY]] found
111
- if (relayIdx === -1) {
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 = nextFence + 3;
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 [[RELAY]]
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 >= relayIdx)
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 [[RELAY]]
246
+ // If we're inside a code fence, skip this block
132
247
  if (inFence) {
133
- searchStart = relayIdx + 9; // Skip past [[RELAY]]
248
+ searchStart = earliestBlockIdx + (earliestBlockIdentifier?.length ?? 0); // Skip past the block
134
249
  continue;
135
250
  }
136
- // Check if [[RELAY]] is at start of a line
137
- if (relayIdx === 0) {
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 beforeRelay = data.substring(0, relayIdx);
142
- const lastNewline = beforeRelay.lastIndexOf('\n');
143
- const lineStart = beforeRelay.substring(lastNewline + 1);
144
- // Must be only whitespace before [[RELAY]] on this line
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 relayIdx;
261
+ return { index: earliestBlockIdx, identifier: earliestBlockIdentifier };
147
262
  }
148
263
  // Not at start of line, keep searching
149
- searchStart = relayIdx + 9;
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
- // Stop at empty lines - they end the continuation
199
- if (nextStripped.trim() === '') {
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
- // Stop if the next line starts another inline command, code fence, or block marker
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
- // Only consume as continuation if the line is INDENTED (starts with whitespace)
211
- // This handles TUI wrapping where continuation lines are indented
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
- if (BLOCK_END.test(this.blockBuffer)) {
264
- const result = this.finishBlock();
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(ESCAPE_PREFIX);
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(INLINE_RELAY);
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: target,
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(INLINE_THINKING);
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: target,
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 endIdx = this.blockBuffer.indexOf('[[/RELAY]]');
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 + '[[/RELAY]]'.length) || null;
594
+ const remaining = this.blockBuffer.substring(endIdx + blockEndIdentifier.replace(/\\/g, '').length) || null;
346
595
  this.inBlock = false;
347
596
  this.blockBuffer = '';
348
- try {
349
- const parsed = JSON.parse(jsonStr);
350
- // Validate required fields
351
- if (!parsed.to || !parsed.type) {
352
- console.error('[parser] Block missing required fields (to, type)');
353
- return { command: null, remaining };
354
- }
355
- return {
356
- command: {
357
- to: parsed.to,
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
- remaining,
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
- catch (err) {
367
- console.error('[parser] Invalid JSON in block:', err);
368
- return { command: null, remaining };
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