@nopeek/agent-bridge 0.7.17 → 0.7.19

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/backends.js CHANGED
@@ -25,8 +25,7 @@ const SOUL_TEMPLATE = `You are {{NAME}} (handle @{{HANDLE}}), a personal AI agen
25
25
  NoPeek end-to-end-encrypted messenger. A human messages you in a chat; you help;
26
26
  you reply. Everything you output is sent verbatim as a chat message.
27
27
 
28
- Voice: friendly, sharp, concise. This is a chat, not a report short paragraphs
29
- or brief lists, no markdown headers. Answer the question directly.
28
+ Voice: friendly, sharp, concise. This is a chat, not a report. Telegram markdown is ok (**bold**, lists, inline code, fenced copy blocks). Answer the question directly.
30
29
 
31
30
  You have your own memory of past conversations in this chat (each chat is a
32
31
  separate ongoing thread). Remember what the humans tell you.
@@ -293,7 +292,7 @@ you reply. Your replies are chat messages — every word you print is sent verba
293
292
 
294
293
  ## Voice
295
294
  - Friendly, sharp, practical. Concise by default — this is a chat, not a report.
296
- - Plain language. No markdown headers in replies; short paragraphs or brief lists only.
295
+ - Plain language. Telegram markdown is ok: **bold** labels, lists, inline code, and fenced copy blocks. No # headers.
297
296
 
298
297
  ## Memory
299
298
  - This profile is yours alone: your soul (this file) and your memories
@@ -7,7 +7,7 @@ export type ToolProgressEvent = {
7
7
  /** One Telegram-style line. Running events only; completed is silent. */
8
8
  export declare function formatToolLine(ev: ToolProgressEvent): string | null;
9
9
  /** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
10
- export declare const RECAP_HINT = "When this turn used tools, your LAST message must be a short phone briefing: (1) what was wrong, (2) what you did, (3) the solution / current state. Never end on a tool list, a diff, code, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
10
+ export declare const RECAP_HINT = "When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
11
11
  /** Posted when tools ran but the model never wrote a human recap. */
12
12
  export declare const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
13
13
  export interface StreamHandle {
@@ -35,7 +35,7 @@ export type TurnPublisherOpts = {
35
35
  export type TurnFinishKind = "streamed" | "sent" | "empty";
36
36
  /** Text in `full` that has not already been posted as commentary. */
37
37
  export declare function unpublishedTail(full: string, published: string): string;
38
- /** Unified diffs / `| review` dumps should never land in the chat. */
38
+ /** Unified diffs / `┊ review diff` dumps should never land in the chat. */
39
39
  export declare function isDumpLine(line: string): boolean;
40
40
  /** True when the text has a user-readable recap, not just tools/dumps. */
41
41
  export declare function isHumanBriefing(text: string): boolean;
@@ -78,10 +78,12 @@ export declare class TurnPublisher {
78
78
  /** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
79
79
  private armGap;
80
80
  private enqueue;
81
- /** Open or append the current commentary/answer stream with this chunk only. */
81
+ /** After tools start, hold prose until finish() so the briefing is one bubble. */
82
82
  private appendCommentary;
83
83
  /** Finalize the current commentary so later tools land below it. */
84
84
  private commitText;
85
+ /** Final recap stays one Telegram-style bubble. Dumps still dropped. */
86
+ private sendBriefing;
85
87
  private sendBubbles;
86
88
  private addProgressLine;
87
89
  private closeProgress;
@@ -49,7 +49,7 @@ function tidyLabel(raw) {
49
49
  return s.length > 80 ? `${s.slice(0, 77)}...` : s;
50
50
  }
51
51
  /** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
52
- export const RECAP_HINT = "When this turn used tools, your LAST message must be a short phone briefing: (1) what was wrong, (2) what you did, (3) the solution / current state. Never end on a tool list, a diff, code, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
52
+ export const RECAP_HINT = "When this turn used tools, your LAST message must be ONE phone briefing (not split). Use **bold** labels and blank lines like Telegram: **What was wrong**, **What I did**, **Current state**. Put copyable commands in ``` fences. Never end on a tool list, a diff, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
53
53
  /** Posted when tools ran but the model never wrote a human recap. */
54
54
  export const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
55
55
  /** Text in `full` that has not already been posted as commentary. */
@@ -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,9 +101,14 @@ 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;
110
+ if (/^[+-][A-Za-z_]/.test(l))
111
+ return true;
103
112
  if (/^[+-]\s*(import |from |export |const |let |function |class |def |if cmd\b|return )/.test(l))
104
113
  return true;
105
114
  if (/^[+-]\s*["'`{]/.test(l))
@@ -114,6 +123,8 @@ function isToolProgressLine(line) {
114
123
  }
115
124
  /** True when the text has a user-readable recap, not just tools/dumps. */
116
125
  export function isHumanBriefing(text) {
126
+ if (isToolDump(text))
127
+ return false;
117
128
  const prose = text
118
129
  .split("\n")
119
130
  .filter((l) => {
@@ -130,13 +141,19 @@ export function isToolDump(text) {
130
141
  const t = text.trim();
131
142
  if (!t)
132
143
  return false;
144
+ if (/review\s+diff/i.test(t))
145
+ return true;
133
146
  if (isDumpLine(t))
134
147
  return true;
135
148
  if (/^diff --git /m.test(t) && /^@@ /m.test(t))
136
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;
137
154
  const lines = t.split("\n");
138
155
  if (lines.length >= 6) {
139
- const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|\| )/.test(l)).length;
156
+ const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|[┊│|]\s)/.test(l)).length;
140
157
  if (dumpy / lines.length >= 0.4)
141
158
  return true;
142
159
  }
@@ -148,8 +165,6 @@ export function stripDumps(text) {
148
165
  return text;
149
166
  if (!text.includes("\n"))
150
167
  return isDumpLine(text) ? "" : text;
151
- if (isToolDump(text))
152
- return "";
153
168
  return text
154
169
  .split("\n")
155
170
  .filter((l) => !isDumpLine(l))
@@ -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;
184
+ continue;
185
+ }
186
+ if (afterDump && !looksLikeCloseout(p))
167
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();
@@ -275,13 +308,16 @@ export class TurnPublisher {
275
308
  if (this.seen.has(line) && this.progressLines.includes(line))
276
309
  return;
277
310
  this.seen.add(line);
311
+ const firstTool = !this.toolsUsed;
278
312
  this.toolsUsed = true;
279
313
  this.cancelStart();
280
314
  this.cancelGap();
281
315
  this.enqueue(async () => {
282
- const pending = this.held;
283
- this.held = "";
284
- await this.commitText(pending);
316
+ if (firstTool) {
317
+ const pending = this.held;
318
+ this.held = "";
319
+ await this.commitText(pending);
320
+ }
285
321
  await this.addProgressLine(line);
286
322
  });
287
323
  }
@@ -290,16 +326,20 @@ export class TurnPublisher {
290
326
  this.cancelGap();
291
327
  await this.write;
292
328
  await this.closeProgress();
293
- await this.commitText(this.held);
329
+ const pending = this.held;
294
330
  this.held = "";
331
+ if (!this.toolsUsed) {
332
+ await this.commitText(pending);
333
+ }
295
334
  const rest = unpublishedTail(reply.trim(), this.published);
296
- if (rest) {
335
+ const briefing = rest || (this.toolsUsed ? pending.trim() : "");
336
+ if (briefing) {
297
337
  this.opts.stopTyping();
298
- await this.sendBubbles(rest);
338
+ await this.sendBriefing(briefing);
299
339
  }
300
340
  if (this.toolsUsed && reply.trim() && !isHumanBriefing(this.published)) {
301
341
  this.opts.stopTyping();
302
- await this.sendBubbles(MISSING_BRIEFING);
342
+ await this.sendBriefing(MISSING_BRIEFING);
303
343
  }
304
344
  if (!this.published.trim())
305
345
  return "empty";
@@ -354,26 +394,13 @@ export class TurnPublisher {
354
394
  console.error(`[turn] ${err.message}`);
355
395
  });
356
396
  }
357
- /** Open or append the current commentary/answer stream with this chunk only. */
397
+ /** After tools start, hold prose until finish() so the briefing is one bubble. */
358
398
  async appendCommentary(chunk) {
359
399
  if (!chunk)
360
400
  return;
361
401
  if (this.toolsUsed) {
362
402
  await this.closeProgress();
363
403
  this.held += chunk;
364
- const cut = this.held.lastIndexOf("\n\n");
365
- if (cut >= 0) {
366
- const ready = this.held.slice(0, cut);
367
- this.held = this.held.slice(cut + 2);
368
- this.opts.stopTyping();
369
- await this.sendBubbles(ready);
370
- }
371
- else if (this.held.length >= this.maxBubbleChars) {
372
- const ready = this.held;
373
- this.held = "";
374
- this.opts.stopTyping();
375
- await this.sendBubbles(ready);
376
- }
377
404
  return;
378
405
  }
379
406
  this.opts.stopTyping();
@@ -442,6 +469,16 @@ export class TurnPublisher {
442
469
  this.opts.stopTyping();
443
470
  await this.sendBubbles(pending);
444
471
  }
472
+ /** Final recap stays one Telegram-style bubble. Dumps still dropped. */
473
+ async sendBriefing(text) {
474
+ const parts = splitBubbles(text.replace(/\r\n/g, "\n"), 4000);
475
+ const cleaned = parts.join("\n\n").trim();
476
+ if (!cleaned || isToolDump(cleaned) || !isHumanBriefing(cleaned))
477
+ return false;
478
+ await this.sink.send(cleaned);
479
+ this.published += this.published ? `\n\n${cleaned}` : cleaned;
480
+ return true;
481
+ }
445
482
  async sendBubbles(text) {
446
483
  const parts = splitBubbles(text, this.maxBubbleChars);
447
484
  if (!parts.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nopeek/agent-bridge",
3
- "version": "0.7.17",
3
+ "version": "0.7.19",
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",