@nopeek/agent-bridge 0.7.16 → 0.7.18

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,9 @@ 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 (read/write files, commands, skills, memory), end your reply with a short recap: what was going on, what you did, and the result. Skip the recap 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
+ /** Posted when tools ran but the model never wrote a human recap. */
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.";
11
13
  export interface StreamHandle {
12
14
  append(chunk: string): void;
13
15
  replace(full: string): void;
@@ -35,6 +37,8 @@ export type TurnFinishKind = "streamed" | "sent" | "empty";
35
37
  export declare function unpublishedTail(full: string, published: string): string;
36
38
  /** Unified diffs / `| review` dumps should never land in the chat. */
37
39
  export declare function isDumpLine(line: string): boolean;
40
+ /** True when the text has a user-readable recap, not just tools/dumps. */
41
+ export declare function isHumanBriefing(text: string): boolean;
38
42
  export declare function isToolDump(text: string): boolean;
39
43
  /** Drop dump lines from a live chunk without swallowing normal punctuation. */
40
44
  export declare function stripDumps(text: string): string;
@@ -74,10 +78,12 @@ export declare class TurnPublisher {
74
78
  /** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
75
79
  private armGap;
76
80
  private enqueue;
77
- /** 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. */
78
82
  private appendCommentary;
79
83
  /** Finalize the current commentary so later tools land below it. */
80
84
  private commitText;
85
+ /** Final recap stays one Telegram-style bubble. Dumps still dropped. */
86
+ private sendBriefing;
81
87
  private sendBubbles;
82
88
  private addProgressLine;
83
89
  private closeProgress;
@@ -49,7 +49,9 @@ 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 (read/write files, commands, skills, memory), end your reply with a short recap: what was going on, what you did, and the result. Skip the recap 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
+ /** Posted when tools ran but the model never wrote a human recap. */
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.";
53
55
  /** Text in `full` that has not already been posted as commentary. */
54
56
  export function unpublishedTail(full, published) {
55
57
  const f = full.trim();
@@ -95,8 +97,37 @@ export function isDumpLine(line) {
95
97
  return true;
96
98
  if (/^index [0-9a-f]+\.\.[0-9a-f]+/.test(l))
97
99
  return true;
100
+ // Unified-diff / review lines: "+ const x =" not "+1 country".
101
+ if (/^[+-]\s{2,}\S/.test(l))
102
+ return true;
103
+ if (/^[+-][A-Za-z_]/.test(l))
104
+ return true;
105
+ if (/^[+-]\s*(import |from |export |const |let |function |class |def |if cmd\b|return )/.test(l))
106
+ return true;
107
+ if (/^[+-]\s*["'`{]/.test(l))
108
+ return true;
98
109
  return false;
99
110
  }
111
+ function isToolProgressLine(line) {
112
+ const l = line.trim();
113
+ if (!l)
114
+ return false;
115
+ return /^.+ \S+: ".+"$/.test(l) || /^.+ \S+\.\.\.$/.test(l);
116
+ }
117
+ /** True when the text has a user-readable recap, not just tools/dumps. */
118
+ export function isHumanBriefing(text) {
119
+ const prose = text
120
+ .split("\n")
121
+ .filter((l) => {
122
+ const t = l.trim();
123
+ return Boolean(t) && !isDumpLine(t) && !isToolProgressLine(t);
124
+ })
125
+ .join("\n")
126
+ .trim();
127
+ if (!prose || isToolDump(prose))
128
+ return false;
129
+ return true;
130
+ }
100
131
  export function isToolDump(text) {
101
132
  const t = text.trim();
102
133
  if (!t)
@@ -119,8 +150,6 @@ export function stripDumps(text) {
119
150
  return text;
120
151
  if (!text.includes("\n"))
121
152
  return isDumpLine(text) ? "" : text;
122
- if (isToolDump(text))
123
- return "";
124
153
  return text
125
154
  .split("\n")
126
155
  .filter((l) => !isDumpLine(l))
@@ -246,13 +275,16 @@ export class TurnPublisher {
246
275
  if (this.seen.has(line) && this.progressLines.includes(line))
247
276
  return;
248
277
  this.seen.add(line);
278
+ const firstTool = !this.toolsUsed;
249
279
  this.toolsUsed = true;
250
280
  this.cancelStart();
251
281
  this.cancelGap();
252
282
  this.enqueue(async () => {
253
- const pending = this.held;
254
- this.held = "";
255
- await this.commitText(pending);
283
+ if (firstTool) {
284
+ const pending = this.held;
285
+ this.held = "";
286
+ await this.commitText(pending);
287
+ }
256
288
  await this.addProgressLine(line);
257
289
  });
258
290
  }
@@ -261,14 +293,24 @@ export class TurnPublisher {
261
293
  this.cancelGap();
262
294
  await this.write;
263
295
  await this.closeProgress();
264
- await this.commitText(this.held);
296
+ const pending = this.held;
265
297
  this.held = "";
298
+ if (!this.toolsUsed) {
299
+ await this.commitText(pending);
300
+ }
266
301
  const rest = unpublishedTail(reply.trim(), this.published);
267
- if (!rest)
268
- return this.published.trim() ? "streamed" : "empty";
269
- this.opts.stopTyping();
270
- const sent = await this.sendBubbles(rest);
271
- return sent ? "sent" : this.published.trim() ? "streamed" : "empty";
302
+ const briefing = rest || (this.toolsUsed ? pending.trim() : "");
303
+ if (briefing) {
304
+ this.opts.stopTyping();
305
+ await this.sendBriefing(briefing);
306
+ }
307
+ if (this.toolsUsed && reply.trim() && !isHumanBriefing(this.published)) {
308
+ this.opts.stopTyping();
309
+ await this.sendBriefing(MISSING_BRIEFING);
310
+ }
311
+ if (!this.published.trim())
312
+ return "empty";
313
+ return rest || this.toolsUsed ? "sent" : "streamed";
272
314
  }
273
315
  async fail(text) {
274
316
  this.cancelStart();
@@ -319,26 +361,13 @@ export class TurnPublisher {
319
361
  console.error(`[turn] ${err.message}`);
320
362
  });
321
363
  }
322
- /** Open or append the current commentary/answer stream with this chunk only. */
364
+ /** After tools start, hold prose until finish() so the briefing is one bubble. */
323
365
  async appendCommentary(chunk) {
324
366
  if (!chunk)
325
367
  return;
326
368
  if (this.toolsUsed) {
327
369
  await this.closeProgress();
328
370
  this.held += chunk;
329
- const cut = this.held.lastIndexOf("\n\n");
330
- if (cut >= 0) {
331
- const ready = this.held.slice(0, cut);
332
- this.held = this.held.slice(cut + 2);
333
- this.opts.stopTyping();
334
- await this.sendBubbles(ready);
335
- }
336
- else if (this.held.length >= this.maxBubbleChars) {
337
- const ready = this.held;
338
- this.held = "";
339
- this.opts.stopTyping();
340
- await this.sendBubbles(ready);
341
- }
342
371
  return;
343
372
  }
344
373
  this.opts.stopTyping();
@@ -382,7 +411,11 @@ export class TurnPublisher {
382
411
  if (extra)
383
412
  s.append(extra);
384
413
  const parts = splitBubbles(final, this.maxBubbleChars);
385
- const first = parts[0] || final;
414
+ if (!parts.length) {
415
+ await s.done("…").catch(() => { });
416
+ return;
417
+ }
418
+ const first = parts[0];
386
419
  await s.done(first).catch(() => { });
387
420
  this.published += first;
388
421
  for (const more of parts.slice(1)) {
@@ -403,6 +436,15 @@ export class TurnPublisher {
403
436
  this.opts.stopTyping();
404
437
  await this.sendBubbles(pending);
405
438
  }
439
+ /** Final recap stays one Telegram-style bubble. Dumps still dropped. */
440
+ async sendBriefing(text) {
441
+ const cleaned = stripDumps(text.replace(/\r\n/g, "\n")).trim();
442
+ if (!cleaned || isToolDump(cleaned))
443
+ return false;
444
+ await this.sink.send(cleaned);
445
+ this.published += this.published ? `\n\n${cleaned}` : cleaned;
446
+ return true;
447
+ }
406
448
  async sendBubbles(text) {
407
449
  const parts = splitBubbles(text, this.maxBubbleChars);
408
450
  if (!parts.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nopeek/agent-bridge",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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",