@nopeek/agent-bridge 0.7.18 → 0.7.20

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/dist/bot.js CHANGED
@@ -613,7 +613,16 @@ export class BotRunner {
613
613
  }
614
614
  };
615
615
  const published = new TurnPublisher({
616
- stream: () => ch.stream(),
616
+ stream: async () => {
617
+ const s = await ch.stream();
618
+ return {
619
+ append: (chunk) => s.append(chunk),
620
+ replace: (full) => s.replace(full),
621
+ done: (final) => s.done(final),
622
+ fail: (text) => s.fail(text),
623
+ cancel: () => ch.recall(s.messageId),
624
+ };
625
+ },
617
626
  send: async (body) => {
618
627
  await ch.send({ text: body });
619
628
  },
@@ -15,6 +15,8 @@ export interface StreamHandle {
15
15
  replace(full: string): void;
16
16
  done(finalText?: string): Promise<unknown>;
17
17
  fail(text: string): Promise<void>;
18
+ /** Drop the placeholder bubble (NoPeek stream() starts as "…"). */
19
+ cancel(): Promise<void>;
18
20
  }
19
21
  export interface TurnSink {
20
22
  stream(): Promise<StreamHandle>;
@@ -35,7 +37,7 @@ export type TurnPublisherOpts = {
35
37
  export type TurnFinishKind = "streamed" | "sent" | "empty";
36
38
  /** Text in `full` that has not already been posted as commentary. */
37
39
  export declare function unpublishedTail(full: string, published: string): string;
38
- /** Unified diffs / `| review` dumps should never land in the chat. */
40
+ /** Unified diffs / `┊ review diff` dumps should never land in the chat. */
39
41
  export declare function isDumpLine(line: string): boolean;
40
42
  /** True when the text has a user-readable recap, not just tools/dumps. */
41
43
  export declare function isHumanBriefing(text: string): boolean;
@@ -78,14 +78,18 @@ export function unpublishedTail(full, published) {
78
78
  return f;
79
79
  }
80
80
  const DEFAULT_MAX_BUBBLE = 480;
81
- /** Unified diffs / `| review` dumps should never land in the chat. */
81
+ /** Hermes TUI activity prefix: review diff / │ read_file / | patch */
82
+ function bareLine(line) {
83
+ return line.replace(/^[┊│|]\s*/, "").trim();
84
+ }
85
+ /** Unified diffs / `┊ review diff` dumps should never land in the chat. */
82
86
  export function isDumpLine(line) {
83
- const l = line.trim();
87
+ const l = bareLine(line);
84
88
  if (!l)
85
89
  return false;
86
- if (/^review diff\b/i.test(l))
90
+ if (/^review\s+diff\b/i.test(l))
87
91
  return true;
88
- if (/^\|\s*(review|read|search|terminal|patch|write)\b/i.test(l))
92
+ if (/^(review|read|search|terminal|patch|write)\b/i.test(l) && line.trim().startsWith("|"))
89
93
  return true;
90
94
  if (/^@@\s+-/.test(l))
91
95
  return true;
@@ -97,6 +101,9 @@ export function isDumpLine(line) {
97
101
  return true;
98
102
  if (/^index [0-9a-f]+\.\.[0-9a-f]+/.test(l))
99
103
  return true;
104
+ // Diff of a markdown list item becomes "-- foo" / "+- foo".
105
+ if (/^[+-]{2}\s+\S/.test(l))
106
+ return true;
100
107
  // Unified-diff / review lines: "+ const x =" not "+1 country".
101
108
  if (/^[+-]\s{2,}\S/.test(l))
102
109
  return true;
@@ -116,6 +123,8 @@ function isToolProgressLine(line) {
116
123
  }
117
124
  /** True when the text has a user-readable recap, not just tools/dumps. */
118
125
  export function isHumanBriefing(text) {
126
+ if (isToolDump(text))
127
+ return false;
119
128
  const prose = text
120
129
  .split("\n")
121
130
  .filter((l) => {
@@ -132,13 +141,19 @@ export function isToolDump(text) {
132
141
  const t = text.trim();
133
142
  if (!t)
134
143
  return false;
144
+ if (/review\s+diff/i.test(t))
145
+ return true;
135
146
  if (isDumpLine(t))
136
147
  return true;
137
148
  if (/^diff --git /m.test(t) && /^@@ /m.test(t))
138
149
  return true;
150
+ if (/^--\s+\S/m.test(t) && /^\+-\s+\S/m.test(t))
151
+ return true;
152
+ if (/^\s*#{1,3}\s+Voice\b/m.test(t) && /^\s*#{1,3}\s+Memory\b/m.test(t))
153
+ return true;
139
154
  const lines = t.split("\n");
140
155
  if (lines.length >= 6) {
141
- const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|\| )/.test(l)).length;
156
+ const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|[┊│|]\s)/.test(l)).length;
142
157
  if (dumpy / lines.length >= 0.4)
143
158
  return true;
144
159
  }
@@ -162,9 +177,15 @@ export function splitBubbles(text, max = DEFAULT_MAX_BUBBLE) {
162
177
  return [];
163
178
  const paras = cleaned.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
164
179
  const out = [];
180
+ let afterDump = false;
165
181
  for (const p of paras) {
166
- if (isToolDump(p))
182
+ if (isToolDump(p) || /review\s+diff/i.test(p)) {
183
+ afterDump = true;
167
184
  continue;
185
+ }
186
+ if (afterDump && !looksLikeCloseout(p))
187
+ continue;
188
+ afterDump = false;
168
189
  if (p.length <= max) {
169
190
  out.push(p);
170
191
  continue;
@@ -194,6 +215,18 @@ export function splitBubbles(text, max = DEFAULT_MAX_BUBBLE) {
194
215
  }
195
216
  return out;
196
217
  }
218
+ /** After a dump hunk, only keep an actual close-out — not leftover SOUL/skill text. */
219
+ function looksLikeCloseout(p) {
220
+ if (/what was wrong|what i did|current state|^\*\*What /im.test(p))
221
+ return true;
222
+ if (/^Done\b/i.test(p.trim()))
223
+ return true;
224
+ if (/^\s*#{1,3}\s/m.test(p))
225
+ return false;
226
+ if (isToolDump(p))
227
+ return false;
228
+ return isHumanBriefing(p);
229
+ }
197
230
  function hardWrap(s, max) {
198
231
  const out = [];
199
232
  let rest = s.trim();
@@ -412,7 +445,13 @@ export class TurnPublisher {
412
445
  s.append(extra);
413
446
  const parts = splitBubbles(final, this.maxBubbleChars);
414
447
  if (!parts.length) {
415
- await s.done("…").catch(() => { });
448
+ const keep = stripDumps(final).trim();
449
+ if (keep && isHumanBriefing(keep)) {
450
+ await s.done(keep).catch(() => { });
451
+ this.published += keep;
452
+ return;
453
+ }
454
+ await s.cancel().catch(() => { });
416
455
  return;
417
456
  }
418
457
  const first = parts[0];
@@ -438,8 +477,9 @@ export class TurnPublisher {
438
477
  }
439
478
  /** Final recap stays one Telegram-style bubble. Dumps still dropped. */
440
479
  async sendBriefing(text) {
441
- const cleaned = stripDumps(text.replace(/\r\n/g, "\n")).trim();
442
- if (!cleaned || isToolDump(cleaned))
480
+ const parts = splitBubbles(text.replace(/\r\n/g, "\n"), 4000);
481
+ const cleaned = parts.join("\n\n").trim();
482
+ if (!cleaned || isToolDump(cleaned) || !isHumanBriefing(cleaned))
443
483
  return false;
444
484
  await this.sink.send(cleaned);
445
485
  this.published += this.published ? `\n\n${cleaned}` : cleaned;
@@ -489,7 +529,11 @@ export class TurnPublisher {
489
529
  this.progress = null;
490
530
  const text = this.progressLines.join("\n");
491
531
  this.progressLines = [];
492
- if (s && text)
532
+ if (!s)
533
+ return;
534
+ if (text)
493
535
  await s.done(text).catch(() => { });
536
+ else
537
+ await s.cancel().catch(() => { });
494
538
  }
495
539
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nopeek/agent-bridge",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
4
  "description": "Run your own agents as E2EE NoPeek bots. Pairs with one-time codes (multiple accounts per computer), runs every bot each account owns, and pipes messages to any command or webhook.",
5
5
  "type": "module",
6
6
  "license": "MIT",