@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 +2 -3
- package/dist/tool-progress.d.ts +5 -3
- package/dist/tool-progress.js +67 -30
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
package/dist/tool-progress.d.ts
CHANGED
|
@@ -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
|
|
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 /
|
|
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
|
-
/**
|
|
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;
|
package/dist/tool-progress.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
87
|
+
const l = bareLine(line);
|
|
84
88
|
if (!l)
|
|
85
89
|
return false;
|
|
86
|
-
if (/^review
|
|
90
|
+
if (/^review\s+diff\b/i.test(l))
|
|
87
91
|
return true;
|
|
88
|
-
if (
|
|
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) || /^(@@ |[+-](?![+-])
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
const briefing = rest || (this.toolsUsed ? pending.trim() : "");
|
|
336
|
+
if (briefing) {
|
|
297
337
|
this.opts.stopTyping();
|
|
298
|
-
await this.
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
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",
|