@nopeek/agent-bridge 0.7.15 → 0.7.16
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/tool-progress.d.ts +10 -0
- package/dist/tool-progress.js +82 -37
- package/package.json +1 -1
package/dist/tool-progress.d.ts
CHANGED
|
@@ -27,12 +27,17 @@ export type TurnPublisherOpts = {
|
|
|
27
27
|
maxProgressLines?: number;
|
|
28
28
|
/** Split commentary/recap into a new bubble after this many chars. */
|
|
29
29
|
maxBubbleChars?: number;
|
|
30
|
+
/** If nothing arrives for this long, the next chunk is a new message. 0 = off. */
|
|
31
|
+
bubbleGapMs?: number;
|
|
30
32
|
};
|
|
31
33
|
export type TurnFinishKind = "streamed" | "sent" | "empty";
|
|
32
34
|
/** Text in `full` that has not already been posted as commentary. */
|
|
33
35
|
export declare function unpublishedTail(full: string, published: string): string;
|
|
34
36
|
/** Unified diffs / `| review` dumps should never land in the chat. */
|
|
37
|
+
export declare function isDumpLine(line: string): boolean;
|
|
35
38
|
export declare function isToolDump(text: string): boolean;
|
|
39
|
+
/** Drop dump lines from a live chunk without swallowing normal punctuation. */
|
|
40
|
+
export declare function stripDumps(text: string): string;
|
|
36
41
|
/** Split a reply into short chat bubbles. Drops tool-dump paragraphs. */
|
|
37
42
|
export declare function splitBubbles(text: string, max?: number): string[];
|
|
38
43
|
/**
|
|
@@ -53,16 +58,21 @@ export declare class TurnPublisher {
|
|
|
53
58
|
private published;
|
|
54
59
|
private toolsUsed;
|
|
55
60
|
private startTimer;
|
|
61
|
+
private gapTimer;
|
|
56
62
|
private seen;
|
|
57
63
|
private readonly startDelayMs;
|
|
58
64
|
private readonly maxProgressLines;
|
|
59
65
|
private readonly maxBubbleChars;
|
|
66
|
+
private readonly bubbleGapMs;
|
|
60
67
|
constructor(sink: TurnSink, opts: TurnPublisherOpts);
|
|
61
68
|
onChunk(delta: string): void;
|
|
62
69
|
onTool(ev: ToolProgressEvent): void;
|
|
63
70
|
finish(reply: string): Promise<TurnFinishKind>;
|
|
64
71
|
fail(text: string): Promise<void>;
|
|
65
72
|
private cancelStart;
|
|
73
|
+
private cancelGap;
|
|
74
|
+
/** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
|
|
75
|
+
private armGap;
|
|
66
76
|
private enqueue;
|
|
67
77
|
/** Open or append the current commentary/answer stream with this chunk only. */
|
|
68
78
|
private appendCommentary;
|
package/dist/tool-progress.js
CHANGED
|
@@ -77,26 +77,55 @@ export function unpublishedTail(full, published) {
|
|
|
77
77
|
}
|
|
78
78
|
const DEFAULT_MAX_BUBBLE = 480;
|
|
79
79
|
/** Unified diffs / `| review` dumps should never land in the chat. */
|
|
80
|
+
export function isDumpLine(line) {
|
|
81
|
+
const l = line.trim();
|
|
82
|
+
if (!l)
|
|
83
|
+
return false;
|
|
84
|
+
if (/^review diff\b/i.test(l))
|
|
85
|
+
return true;
|
|
86
|
+
if (/^\|\s*(review|read|search|terminal|patch|write)\b/i.test(l))
|
|
87
|
+
return true;
|
|
88
|
+
if (/^@@\s+-/.test(l))
|
|
89
|
+
return true;
|
|
90
|
+
if (/^diff --git /.test(l))
|
|
91
|
+
return true;
|
|
92
|
+
if (/^\*\*\*\s+(Begin|Update|Add|Delete|End)\b/.test(l))
|
|
93
|
+
return true;
|
|
94
|
+
if (/^[ab]:?\/{1,2}Volumes\//.test(l))
|
|
95
|
+
return true;
|
|
96
|
+
if (/^index [0-9a-f]+\.\.[0-9a-f]+/.test(l))
|
|
97
|
+
return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
80
100
|
export function isToolDump(text) {
|
|
81
101
|
const t = text.trim();
|
|
82
102
|
if (!t)
|
|
83
103
|
return false;
|
|
84
|
-
if (
|
|
85
|
-
return true;
|
|
86
|
-
if (/^@@\s+-\d+/.test(t))
|
|
87
|
-
return true;
|
|
88
|
-
if (/^\*\*\*\s+(Begin|Update|Add|Delete) Patch/m.test(t))
|
|
104
|
+
if (isDumpLine(t))
|
|
89
105
|
return true;
|
|
90
106
|
if (/^diff --git /m.test(t) && /^@@ /m.test(t))
|
|
91
107
|
return true;
|
|
92
108
|
const lines = t.split("\n");
|
|
93
109
|
if (lines.length >= 6) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
110
|
+
const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|\| )/.test(l)).length;
|
|
111
|
+
if (dumpy / lines.length >= 0.4)
|
|
96
112
|
return true;
|
|
97
113
|
}
|
|
98
114
|
return false;
|
|
99
115
|
}
|
|
116
|
+
/** Drop dump lines from a live chunk without swallowing normal punctuation. */
|
|
117
|
+
export function stripDumps(text) {
|
|
118
|
+
if (!text)
|
|
119
|
+
return text;
|
|
120
|
+
if (!text.includes("\n"))
|
|
121
|
+
return isDumpLine(text) ? "" : text;
|
|
122
|
+
if (isToolDump(text))
|
|
123
|
+
return "";
|
|
124
|
+
return text
|
|
125
|
+
.split("\n")
|
|
126
|
+
.filter((l) => !isDumpLine(l))
|
|
127
|
+
.join("\n");
|
|
128
|
+
}
|
|
100
129
|
/** Split a reply into short chat bubbles. Drops tool-dump paragraphs. */
|
|
101
130
|
export function splitBubbles(text, max = DEFAULT_MAX_BUBBLE) {
|
|
102
131
|
const cleaned = text.replace(/\r\n/g, "\n").trim();
|
|
@@ -168,39 +197,44 @@ export class TurnPublisher {
|
|
|
168
197
|
published = "";
|
|
169
198
|
toolsUsed = false;
|
|
170
199
|
startTimer = null;
|
|
200
|
+
gapTimer = null;
|
|
171
201
|
seen = new Set();
|
|
172
202
|
startDelayMs;
|
|
173
203
|
maxProgressLines;
|
|
174
204
|
maxBubbleChars;
|
|
205
|
+
bubbleGapMs;
|
|
175
206
|
constructor(sink, opts) {
|
|
176
207
|
this.sink = sink;
|
|
177
208
|
this.opts = opts;
|
|
178
209
|
this.startDelayMs = opts.startDelayMs ?? 400;
|
|
179
210
|
this.maxProgressLines = opts.maxProgressLines ?? 8;
|
|
180
211
|
this.maxBubbleChars = opts.maxBubbleChars ?? DEFAULT_MAX_BUBBLE;
|
|
212
|
+
this.bubbleGapMs = opts.bubbleGapMs ?? 2000;
|
|
181
213
|
}
|
|
182
214
|
onChunk(delta) {
|
|
183
|
-
|
|
215
|
+
const chunk = stripDumps(delta);
|
|
216
|
+
if (!chunk)
|
|
184
217
|
return;
|
|
218
|
+
this.armGap();
|
|
185
219
|
if (this.toolsUsed) {
|
|
186
220
|
this.cancelStart();
|
|
187
|
-
this.enqueue(() => this.appendCommentary(
|
|
221
|
+
this.enqueue(() => this.appendCommentary(chunk));
|
|
188
222
|
return;
|
|
189
223
|
}
|
|
190
224
|
if (this.textP) {
|
|
191
|
-
this.enqueue(() => this.appendCommentary(
|
|
225
|
+
this.enqueue(() => this.appendCommentary(chunk));
|
|
192
226
|
return;
|
|
193
227
|
}
|
|
194
|
-
this.held +=
|
|
228
|
+
this.held += chunk;
|
|
195
229
|
if (this.startTimer)
|
|
196
230
|
return;
|
|
197
231
|
this.startTimer = setTimeout(() => {
|
|
198
232
|
this.startTimer = null;
|
|
199
233
|
if (this.toolsUsed || this.textP || !this.held)
|
|
200
234
|
return;
|
|
201
|
-
const
|
|
235
|
+
const ready = this.held;
|
|
202
236
|
this.held = "";
|
|
203
|
-
this.enqueue(() => this.appendCommentary(
|
|
237
|
+
this.enqueue(() => this.appendCommentary(ready));
|
|
204
238
|
}, this.startDelayMs);
|
|
205
239
|
}
|
|
206
240
|
onTool(ev) {
|
|
@@ -213,10 +247,8 @@ export class TurnPublisher {
|
|
|
213
247
|
return;
|
|
214
248
|
this.seen.add(line);
|
|
215
249
|
this.toolsUsed = true;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.startTimer = null;
|
|
219
|
-
}
|
|
250
|
+
this.cancelStart();
|
|
251
|
+
this.cancelGap();
|
|
220
252
|
this.enqueue(async () => {
|
|
221
253
|
const pending = this.held;
|
|
222
254
|
this.held = "";
|
|
@@ -226,27 +258,12 @@ export class TurnPublisher {
|
|
|
226
258
|
}
|
|
227
259
|
async finish(reply) {
|
|
228
260
|
this.cancelStart();
|
|
261
|
+
this.cancelGap();
|
|
229
262
|
await this.write;
|
|
230
263
|
await this.closeProgress();
|
|
231
|
-
const trimmed = reply.trim();
|
|
232
|
-
if (this.textP && !this.toolsUsed) {
|
|
233
|
-
const s = await this.textP.catch(() => null);
|
|
234
|
-
this.textP = null;
|
|
235
|
-
if (s) {
|
|
236
|
-
const parts = splitBubbles(trimmed || this.textBuf, this.maxBubbleChars);
|
|
237
|
-
const first = parts[0] || trimmed;
|
|
238
|
-
await s.done(first || undefined);
|
|
239
|
-
this.published += first || "";
|
|
240
|
-
for (const extra of parts.slice(1)) {
|
|
241
|
-
this.opts.stopTyping();
|
|
242
|
-
await this.sink.send(extra);
|
|
243
|
-
this.published += `\n\n${extra}`;
|
|
244
|
-
}
|
|
245
|
-
return first ? "streamed" : "empty";
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
264
|
await this.commitText(this.held);
|
|
249
|
-
|
|
265
|
+
this.held = "";
|
|
266
|
+
const rest = unpublishedTail(reply.trim(), this.published);
|
|
250
267
|
if (!rest)
|
|
251
268
|
return this.published.trim() ? "streamed" : "empty";
|
|
252
269
|
this.opts.stopTyping();
|
|
@@ -255,6 +272,7 @@ export class TurnPublisher {
|
|
|
255
272
|
}
|
|
256
273
|
async fail(text) {
|
|
257
274
|
this.cancelStart();
|
|
275
|
+
this.cancelGap();
|
|
258
276
|
await this.write;
|
|
259
277
|
await this.closeProgress();
|
|
260
278
|
if (this.textP) {
|
|
@@ -276,6 +294,26 @@ export class TurnPublisher {
|
|
|
276
294
|
clearTimeout(this.startTimer);
|
|
277
295
|
this.startTimer = null;
|
|
278
296
|
}
|
|
297
|
+
cancelGap() {
|
|
298
|
+
if (!this.gapTimer)
|
|
299
|
+
return;
|
|
300
|
+
clearTimeout(this.gapTimer);
|
|
301
|
+
this.gapTimer = null;
|
|
302
|
+
}
|
|
303
|
+
/** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
|
|
304
|
+
armGap() {
|
|
305
|
+
this.cancelGap();
|
|
306
|
+
if (this.bubbleGapMs <= 0)
|
|
307
|
+
return;
|
|
308
|
+
this.gapTimer = setTimeout(() => {
|
|
309
|
+
this.gapTimer = null;
|
|
310
|
+
this.enqueue(async () => {
|
|
311
|
+
const pending = this.held;
|
|
312
|
+
this.held = "";
|
|
313
|
+
await this.commitText(pending);
|
|
314
|
+
});
|
|
315
|
+
}, this.bubbleGapMs);
|
|
316
|
+
}
|
|
279
317
|
enqueue(fn) {
|
|
280
318
|
this.write = this.write.then(fn).catch((err) => {
|
|
281
319
|
console.error(`[turn] ${err.message}`);
|
|
@@ -343,8 +381,15 @@ export class TurnPublisher {
|
|
|
343
381
|
if (s && final) {
|
|
344
382
|
if (extra)
|
|
345
383
|
s.append(extra);
|
|
346
|
-
|
|
347
|
-
|
|
384
|
+
const parts = splitBubbles(final, this.maxBubbleChars);
|
|
385
|
+
const first = parts[0] || final;
|
|
386
|
+
await s.done(first).catch(() => { });
|
|
387
|
+
this.published += first;
|
|
388
|
+
for (const more of parts.slice(1)) {
|
|
389
|
+
this.opts.stopTyping();
|
|
390
|
+
await this.sink.send(more);
|
|
391
|
+
this.published += `\n\n${more}`;
|
|
392
|
+
}
|
|
348
393
|
return;
|
|
349
394
|
}
|
|
350
395
|
if (final.trim()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.16",
|
|
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",
|