@nopeek/agent-bridge 0.7.10 → 0.7.11
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/README.md +3 -0
- package/dist/backends.js +16 -7
- package/dist/bot.js +18 -7
- package/dist/brain.d.ts +8 -1
- package/dist/brain.js +9 -1
- package/dist/cli.js +0 -0
- package/dist/hermes-http.d.ts +1 -1
- package/dist/hermes-http.js +76 -20
- package/dist/tool-progress.d.ts +29 -0
- package/dist/tool-progress.js +105 -0
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -41,6 +41,9 @@ The `npr_…` code comes from Settings → Connect your computer (bots) in the a
|
|
|
41
41
|
|
|
42
42
|
Hermes brains prefer the local agent API (`HERMES_API_URL`, default
|
|
43
43
|
`http://127.0.0.1:8642`) when `/health` is up, and fall back to `hermes chat`.
|
|
44
|
+
While Hermes is working, the bot posts Telegram-style tool lines a few at a
|
|
45
|
+
time (read file, patch, skill, memory). A quiet gap starts a new message.
|
|
46
|
+
The final reply is the answer plus a short recap when tools were used.
|
|
44
47
|
Idle kill waits for **no output and no CPU** (default 15 min). Authenticated
|
|
45
48
|
`GET /status` includes `bridge`, `bots[]` (each with `lastTurn`), `hermesApi`,
|
|
46
49
|
and `lastTurn`. See `docs/HERMES-BRAIN-PLAN.md`.
|
package/dist/backends.js
CHANGED
|
@@ -16,7 +16,7 @@ import { createHash } from "node:crypto";
|
|
|
16
16
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { basename, join } from "node:path";
|
|
19
|
-
import { stripAnsi } from "./brain.js";
|
|
19
|
+
import { asHooks, stripAnsi } from "./brain.js";
|
|
20
20
|
import { hermesHttpBrain, probeHermesApi } from "./hermes-http.js";
|
|
21
21
|
const BRAIN_UNREACHABLE = "⚠️ I couldn't reach my brain just now — please try again in a moment.";
|
|
22
22
|
// ------------------------------------------------------------------ souls ----
|
|
@@ -224,7 +224,8 @@ function runClaudeOnce(bin, args, text, cwd, timeoutMs, tag, onDelta) {
|
|
|
224
224
|
* host powers from wherever the bridge happens to run.
|
|
225
225
|
*/
|
|
226
226
|
export function claudeBrain(cfg) {
|
|
227
|
-
return async (text, ctx,
|
|
227
|
+
return async (text, ctx, hooks) => {
|
|
228
|
+
const { onChunk } = asHooks(hooks);
|
|
228
229
|
const bin = resolveBin("claude", "CLAUDE_BIN");
|
|
229
230
|
if (!bin) {
|
|
230
231
|
console.error(`[brain:claude:@${ctx.botHandle}] claude not found on PATH`);
|
|
@@ -302,8 +303,15 @@ you reply. Your replies are chat messages — every word you print is sent verba
|
|
|
302
303
|
## Guardrails
|
|
303
304
|
- Never expose secrets, keys, or tokens.
|
|
304
305
|
- Confirm before destructive or outward-facing actions.
|
|
305
|
-
- Report honestly
|
|
306
|
+
- Report honestly. If something failed, say so.
|
|
306
307
|
- If a request is far outside your purpose, say so briefly and offer what you can do.
|
|
308
|
+
|
|
309
|
+
## After tool work
|
|
310
|
+
When you used tools this turn (read or wrote files, ran commands, changed skills, saved memory), end with a short recap:
|
|
311
|
+
- What was going on (one line).
|
|
312
|
+
- What you did (one or two lines).
|
|
313
|
+
- Result (one line).
|
|
314
|
+
Skip the recap for simple conversation with no tools.
|
|
307
315
|
`;
|
|
308
316
|
/**
|
|
309
317
|
* Create (or reuse) the bot's isolated Hermes profile: its own SOUL.md and
|
|
@@ -527,7 +535,8 @@ function hermesCliBrain(cfg) {
|
|
|
527
535
|
finish();
|
|
528
536
|
});
|
|
529
537
|
});
|
|
530
|
-
return async (text, ctx,
|
|
538
|
+
return async (text, ctx, hooks) => {
|
|
539
|
+
const { onChunk } = asHooks(hooks);
|
|
531
540
|
const handle = ctx.botHandle.replace(/^@/, "");
|
|
532
541
|
const tag = `[brain:hermes:@${handle}]`;
|
|
533
542
|
if (!resolveBin("hermes", "HERMES_BIN")) {
|
|
@@ -606,15 +615,15 @@ function hermesCliBrain(cfg) {
|
|
|
606
615
|
export function hermesBrain(cfg) {
|
|
607
616
|
const cli = hermesCliBrain(cfg);
|
|
608
617
|
const http = hermesHttpBrain(cfg);
|
|
609
|
-
return async (text, ctx,
|
|
618
|
+
return async (text, ctx, hooks) => {
|
|
610
619
|
const health = await probeHermesApi(cfg);
|
|
611
620
|
if (health.ok) {
|
|
612
|
-
const reply = await http(text, ctx,
|
|
621
|
+
const reply = await http(text, ctx, hooks);
|
|
613
622
|
if (reply)
|
|
614
623
|
return reply;
|
|
615
624
|
console.error(`[brain:hermes:@${ctx.botHandle.replace(/^@/, "")}] API returned empty — falling back to CLI`);
|
|
616
625
|
}
|
|
617
|
-
return cli(text, ctx,
|
|
626
|
+
return cli(text, ctx, hooks);
|
|
618
627
|
};
|
|
619
628
|
}
|
|
620
629
|
export function hermesHttpOnlyBrain(cfg) {
|
package/dist/bot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { isChannelMode } from "./control.js";
|
|
|
7
7
|
import { FALLBACK_REPLY, resolveBrain } from "./brain.js";
|
|
8
8
|
import { beginTurn, finishTurn, lastTurnFor } from "./last-turn.js";
|
|
9
9
|
import { FileStore } from "./storage.js";
|
|
10
|
+
import { ToolProgressFlusher } from "./tool-progress.js";
|
|
10
11
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
12
|
const MAX_BACKOFF_MS = 60_000;
|
|
12
13
|
// Owner-membership answers are cached per channel for a short window; a
|
|
@@ -551,16 +552,20 @@ export class BotRunner {
|
|
|
551
552
|
// append onto the open promise keeps them ordered and loses none.
|
|
552
553
|
// (Ref object rather than a `let`: TS can't see closure assignments.)
|
|
553
554
|
const streamRef = { p: null };
|
|
555
|
+
const stopTyping = () => {
|
|
556
|
+
try {
|
|
557
|
+
ch.typing(false);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
/* best-effort */
|
|
561
|
+
}
|
|
562
|
+
};
|
|
554
563
|
const onChunk = (delta) => {
|
|
555
564
|
if (!delta)
|
|
556
565
|
return;
|
|
566
|
+
void progress.flush();
|
|
557
567
|
if (!streamRef.p) {
|
|
558
|
-
|
|
559
|
-
ch.typing(false); // the live bubble replaces the typing indicator
|
|
560
|
-
}
|
|
561
|
-
catch {
|
|
562
|
-
/* best-effort */
|
|
563
|
-
}
|
|
568
|
+
stopTyping();
|
|
564
569
|
streamRef.p = ch.stream();
|
|
565
570
|
streamRef.p.catch((err) => {
|
|
566
571
|
this.notePostFailure(m.channelId, err);
|
|
@@ -569,6 +574,10 @@ export class BotRunner {
|
|
|
569
574
|
}
|
|
570
575
|
streamRef.p.then((s) => s.append(delta)).catch(() => { });
|
|
571
576
|
};
|
|
577
|
+
const progress = new ToolProgressFlusher(async (body) => {
|
|
578
|
+
stopTyping();
|
|
579
|
+
await ch.send({ text: body });
|
|
580
|
+
});
|
|
572
581
|
let reply = "";
|
|
573
582
|
const turn = beginTurn(this.info.handle, m.channelId, resolved.kind);
|
|
574
583
|
try {
|
|
@@ -577,7 +586,7 @@ export class BotRunner {
|
|
|
577
586
|
botUserId: this.info.userId,
|
|
578
587
|
channelId: m.channelId,
|
|
579
588
|
senderUserId: m.senderUserId,
|
|
580
|
-
}, onChunk);
|
|
589
|
+
}, { onChunk, onTool: (ev) => progress.push(ev) });
|
|
581
590
|
const trimmed = reply.trim();
|
|
582
591
|
const timedOut = /went quiet for over \d+s/i.test(trimmed);
|
|
583
592
|
finishTurn(turn, {
|
|
@@ -591,6 +600,7 @@ export class BotRunner {
|
|
|
591
600
|
finishTurn(turn, { ok: false, error: err.message, timedOut: false });
|
|
592
601
|
// Brain blew up mid-stream: finalize the partial bubble with an honest
|
|
593
602
|
// error line instead of leaving a forever-blinking cursor.
|
|
603
|
+
await progress.flush();
|
|
594
604
|
const stream = streamRef.p ? await streamRef.p.catch(() => null) : null;
|
|
595
605
|
if (stream)
|
|
596
606
|
await stream.fail(FALLBACK_REPLY).catch(() => { });
|
|
@@ -604,6 +614,7 @@ export class BotRunner {
|
|
|
604
614
|
/* best-effort */
|
|
605
615
|
}
|
|
606
616
|
}
|
|
617
|
+
await progress.flush();
|
|
607
618
|
const stream = streamRef.p ? await streamRef.p.catch(() => null) : null;
|
|
608
619
|
if (stream) {
|
|
609
620
|
await stream.done(reply.trim() || undefined);
|
package/dist/brain.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeConfig } from "./config.js";
|
|
2
|
+
import type { ToolProgressEvent } from "./tool-progress.js";
|
|
2
3
|
export interface BrainContext {
|
|
3
4
|
botHandle: string;
|
|
4
5
|
botUserId: string;
|
|
@@ -9,8 +10,14 @@ export interface BrainContext {
|
|
|
9
10
|
* A brain answers one message. If it can stream, it calls `onChunk(delta)` as
|
|
10
11
|
* text arrives (plain text, ANSI-stripped) and STILL returns the full reply —
|
|
11
12
|
* the returned string is authoritative for the final message body.
|
|
13
|
+
* `onTool` is optional: Hermes HTTP uses it for live tool-progress lines.
|
|
12
14
|
*/
|
|
13
|
-
export type
|
|
15
|
+
export type BrainHooks = {
|
|
16
|
+
onChunk?: (delta: string) => void;
|
|
17
|
+
onTool?: (ev: ToolProgressEvent) => void;
|
|
18
|
+
};
|
|
19
|
+
export type Brain = (text: string, ctx: BrainContext, hooks?: BrainHooks | ((delta: string) => void)) => Promise<string>;
|
|
20
|
+
export declare function asHooks(hooks?: BrainHooks | ((delta: string) => void)): BrainHooks;
|
|
14
21
|
export declare const FALLBACK_REPLY = "Sorry \u2014 I hit an error processing that. Please try again.";
|
|
15
22
|
export declare function stripAnsi(s: string): string;
|
|
16
23
|
export interface ResolvedBrain {
|
package/dist/brain.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
// own service): the bridge never knows or cares what's on the other side.
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { claudeBrain, hermesBrain, hermesHttpOnlyBrain } from "./backends.js";
|
|
10
|
+
export function asHooks(hooks) {
|
|
11
|
+
if (!hooks)
|
|
12
|
+
return {};
|
|
13
|
+
if (typeof hooks === "function")
|
|
14
|
+
return { onChunk: hooks };
|
|
15
|
+
return hooks;
|
|
16
|
+
}
|
|
10
17
|
export const FALLBACK_REPLY = "Sorry — I hit an error processing that. Please try again.";
|
|
11
18
|
// ANSI escape sequences (CSI, OSC, and lone ESC controls). Agent runtimes like
|
|
12
19
|
// Hermes color their stdout; the chat must receive plain text.
|
|
@@ -23,7 +30,8 @@ export function stripAnsi(s) {
|
|
|
23
30
|
* NOPEEK_BOT_HANDLE, NOPEEK_BOT_USER_ID, NOPEEK_CHANNEL_ID, NOPEEK_SENDER_USER_ID.
|
|
24
31
|
*/
|
|
25
32
|
function cmdBrain(cmd, timeoutMs) {
|
|
26
|
-
return (text, ctx,
|
|
33
|
+
return (text, ctx, hooks) => new Promise((resolvePromise) => {
|
|
34
|
+
const { onChunk } = asHooks(hooks);
|
|
27
35
|
const child = spawn("bash", ["-c", cmd], {
|
|
28
36
|
stdio: ["pipe", "pipe", "pipe"],
|
|
29
37
|
env: {
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/hermes-http.d.ts
CHANGED
package/dist/hermes-http.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { asHooks } from "./brain.js";
|
|
2
|
+
import { RECAP_HINT } from "./tool-progress.js";
|
|
1
3
|
let lastHealth = null;
|
|
2
4
|
const HEALTH_TTL_MS = 15_000;
|
|
3
5
|
export function lastHermesApiHealth() {
|
|
@@ -35,16 +37,20 @@ export async function probeHermesApi(cfg, force = false) {
|
|
|
35
37
|
* Session continuity: X-Hermes-Session-Key = nopeek-<channelId> (stable per chat).
|
|
36
38
|
*/
|
|
37
39
|
export function hermesHttpBrain(cfg) {
|
|
38
|
-
return async (text, ctx,
|
|
40
|
+
return async (text, ctx, hooks) => {
|
|
41
|
+
const { onChunk, onTool } = asHooks(hooks);
|
|
39
42
|
const handle = ctx.botHandle.replace(/^@/, "");
|
|
40
43
|
const tag = `[brain:hermes-http:@${handle}]`;
|
|
41
44
|
const url = cfg.hermesApiUrl.replace(/\/+$/, "");
|
|
42
45
|
const headers = {
|
|
43
46
|
"content-type": "application/json",
|
|
44
|
-
"X-Hermes-Session-Key": `nopeek-${ctx.channelId}`,
|
|
45
47
|
};
|
|
46
|
-
|
|
48
|
+
// Hermes rejects X-Hermes-Session-Key with 403 unless API_SERVER_KEY is set.
|
|
49
|
+
// Without a key, skip the header so the turn still runs (no session memory).
|
|
50
|
+
if (cfg.hermesApiKey) {
|
|
47
51
|
headers.authorization = `Bearer ${cfg.hermesApiKey}`;
|
|
52
|
+
headers["X-Hermes-Session-Key"] = `nopeek-${ctx.channelId}`;
|
|
53
|
+
}
|
|
48
54
|
// Idle abort — same rule as the CLI path. A wall-clock timeout on the
|
|
49
55
|
// whole POST would kill a healthy 20-minute agentic turn. Any SSE byte
|
|
50
56
|
// (token, keepalive, tool-progress) resets the idle timer.
|
|
@@ -66,7 +72,10 @@ export function hermesHttpBrain(cfg) {
|
|
|
66
72
|
body: JSON.stringify({
|
|
67
73
|
model: "hermes-agent",
|
|
68
74
|
stream: true,
|
|
69
|
-
messages: [
|
|
75
|
+
messages: [
|
|
76
|
+
{ role: "system", content: RECAP_HINT },
|
|
77
|
+
{ role: "user", content: text },
|
|
78
|
+
],
|
|
70
79
|
}),
|
|
71
80
|
signal: controller.signal,
|
|
72
81
|
});
|
|
@@ -98,29 +107,40 @@ export function hermesHttpBrain(cfg) {
|
|
|
98
107
|
break;
|
|
99
108
|
armIdle();
|
|
100
109
|
buf += decoder.decode(value, { stream: true });
|
|
101
|
-
const
|
|
102
|
-
buf =
|
|
103
|
-
for (const
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
106
|
-
continue;
|
|
107
|
-
const payload = trimmed.slice(5).trim();
|
|
108
|
-
if (!payload || payload === "[DONE]")
|
|
110
|
+
const frames = buf.split("\n\n");
|
|
111
|
+
buf = frames.pop() ?? "";
|
|
112
|
+
for (const frame of frames) {
|
|
113
|
+
const ev = parseSseFrame(frame);
|
|
114
|
+
if (!ev)
|
|
109
115
|
continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
catch {
|
|
116
|
+
if (ev.event === "hermes.tool.progress") {
|
|
117
|
+
const tool = asToolProgress(ev.data);
|
|
118
|
+
if (tool)
|
|
119
|
+
onTool?.(tool);
|
|
115
120
|
continue;
|
|
116
121
|
}
|
|
117
|
-
const delta = extractDelta(
|
|
122
|
+
const delta = extractDelta(ev.data);
|
|
118
123
|
if (!delta)
|
|
119
124
|
continue;
|
|
120
125
|
reply += delta;
|
|
121
126
|
onChunk?.(delta);
|
|
122
127
|
}
|
|
123
128
|
}
|
|
129
|
+
if (buf.trim()) {
|
|
130
|
+
const ev = parseSseFrame(buf);
|
|
131
|
+
if (ev?.event === "hermes.tool.progress") {
|
|
132
|
+
const tool = asToolProgress(ev.data);
|
|
133
|
+
if (tool)
|
|
134
|
+
onTool?.(tool);
|
|
135
|
+
}
|
|
136
|
+
else if (ev) {
|
|
137
|
+
const delta = extractDelta(ev.data);
|
|
138
|
+
if (delta) {
|
|
139
|
+
reply += delta;
|
|
140
|
+
onChunk?.(delta);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
124
144
|
}
|
|
125
145
|
catch (err) {
|
|
126
146
|
const msg = err.message || "";
|
|
@@ -135,6 +155,44 @@ export function hermesHttpBrain(cfg) {
|
|
|
135
155
|
return reply.trim();
|
|
136
156
|
};
|
|
137
157
|
}
|
|
158
|
+
function parseSseFrame(frame) {
|
|
159
|
+
let event = "message";
|
|
160
|
+
const dataLines = [];
|
|
161
|
+
for (const raw of frame.split("\n")) {
|
|
162
|
+
const line = raw.replace(/\r$/, "");
|
|
163
|
+
if (!line || line.startsWith(":"))
|
|
164
|
+
continue;
|
|
165
|
+
if (line.startsWith("event:"))
|
|
166
|
+
event = line.slice(6).trim();
|
|
167
|
+
else if (line.startsWith("data:"))
|
|
168
|
+
dataLines.push(line.slice(5).trimStart());
|
|
169
|
+
}
|
|
170
|
+
if (dataLines.length === 0)
|
|
171
|
+
return null;
|
|
172
|
+
const payload = dataLines.join("\n");
|
|
173
|
+
if (!payload || payload === "[DONE]")
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
return { event, data: JSON.parse(payload) };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return { event, data: payload };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function asToolProgress(data) {
|
|
183
|
+
if (!data || typeof data !== "object")
|
|
184
|
+
return null;
|
|
185
|
+
const obj = data;
|
|
186
|
+
const tool = typeof obj.tool === "string" ? obj.tool : typeof obj.name === "string" ? obj.name : "";
|
|
187
|
+
if (!tool)
|
|
188
|
+
return null;
|
|
189
|
+
return {
|
|
190
|
+
tool,
|
|
191
|
+
emoji: typeof obj.emoji === "string" ? obj.emoji : undefined,
|
|
192
|
+
label: typeof obj.label === "string" ? obj.label : undefined,
|
|
193
|
+
status: typeof obj.status === "string" ? obj.status : "running",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
138
196
|
function extractDelta(parsed) {
|
|
139
197
|
if (!parsed || typeof parsed !== "object")
|
|
140
198
|
return "";
|
|
@@ -143,8 +201,6 @@ function extractDelta(parsed) {
|
|
|
143
201
|
const fromChoice = choice?.delta?.content ?? choice?.message?.content;
|
|
144
202
|
if (typeof fromChoice === "string")
|
|
145
203
|
return fromChoice;
|
|
146
|
-
// Hermes also emits custom SSE tool-progress events; ignore those for the
|
|
147
|
-
// chat body (they keep the HTTP connection alive, which is the point).
|
|
148
204
|
if (typeof obj.data?.content === "string")
|
|
149
205
|
return obj.data.content;
|
|
150
206
|
if (typeof obj.data?.text === "string")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type ToolProgressEvent = {
|
|
2
|
+
tool: string;
|
|
3
|
+
emoji?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
};
|
|
7
|
+
/** One Telegram-style line. Running events only; completed is silent. */
|
|
8
|
+
export declare function formatToolLine(ev: ToolProgressEvent): string | null;
|
|
9
|
+
export type ProgressSender = (text: string) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Collect tool lines and flush them as short chat messages.
|
|
12
|
+
* Flush when we have `maxLines` tools, or after `gapMs` of quiet.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ToolProgressFlusher {
|
|
15
|
+
private readonly send;
|
|
16
|
+
private readonly gapMs;
|
|
17
|
+
private readonly maxLines;
|
|
18
|
+
private lines;
|
|
19
|
+
private timer;
|
|
20
|
+
private chain;
|
|
21
|
+
private seen;
|
|
22
|
+
constructor(send: ProgressSender, gapMs?: number, maxLines?: number);
|
|
23
|
+
get pending(): number;
|
|
24
|
+
push(ev: ToolProgressEvent): void;
|
|
25
|
+
flush(): Promise<void>;
|
|
26
|
+
private arm;
|
|
27
|
+
}
|
|
28
|
+
/** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
|
|
29
|
+
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.";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Telegram-style tool progress for NoPeek bots.
|
|
2
|
+
//
|
|
3
|
+
// Hermes emits `hermes.tool.progress` SSE events while it works. Telegram
|
|
4
|
+
// already shows those as short status messages (a few tools, then a new
|
|
5
|
+
// message after a quiet gap). We do the same in the encrypted chat so the
|
|
6
|
+
// human can see the bot is actually taking action.
|
|
7
|
+
const FALLBACK_EMOJI = {
|
|
8
|
+
read_file: "📖",
|
|
9
|
+
write_file: "✍️",
|
|
10
|
+
patch: "🔧",
|
|
11
|
+
search_files: "🔎",
|
|
12
|
+
terminal: "💻",
|
|
13
|
+
web_search: "🔍",
|
|
14
|
+
web_extract: "📄",
|
|
15
|
+
web_crawl: "🕸️",
|
|
16
|
+
memory: "🧠",
|
|
17
|
+
skill_view: "📘",
|
|
18
|
+
skill_manage: "🧩",
|
|
19
|
+
skills_list: "📚",
|
|
20
|
+
todo: "✅",
|
|
21
|
+
execute_code: "🐍",
|
|
22
|
+
delegate_task: "👥",
|
|
23
|
+
cronjob: "⏰",
|
|
24
|
+
process: "⚙️",
|
|
25
|
+
};
|
|
26
|
+
/** One Telegram-style line. Running events only; completed is silent. */
|
|
27
|
+
export function formatToolLine(ev) {
|
|
28
|
+
if (ev.status && ev.status !== "running")
|
|
29
|
+
return null;
|
|
30
|
+
const tool = (ev.tool || "").trim();
|
|
31
|
+
if (!tool || tool.startsWith("_"))
|
|
32
|
+
return null;
|
|
33
|
+
const emoji = (ev.emoji || FALLBACK_EMOJI[tool] || "⚡").trim() || "⚡";
|
|
34
|
+
const label = tidyLabel(ev.label);
|
|
35
|
+
if (label)
|
|
36
|
+
return `${emoji} ${tool}: "${label}"`;
|
|
37
|
+
return `${emoji} ${tool}...`;
|
|
38
|
+
}
|
|
39
|
+
function tidyLabel(raw) {
|
|
40
|
+
if (!raw)
|
|
41
|
+
return "";
|
|
42
|
+
const s = raw.replace(/\s+/g, " ").trim();
|
|
43
|
+
if (!s)
|
|
44
|
+
return "";
|
|
45
|
+
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Collect tool lines and flush them as short chat messages.
|
|
49
|
+
* Flush when we have `maxLines` tools, or after `gapMs` of quiet.
|
|
50
|
+
*/
|
|
51
|
+
export class ToolProgressFlusher {
|
|
52
|
+
send;
|
|
53
|
+
gapMs;
|
|
54
|
+
maxLines;
|
|
55
|
+
lines = [];
|
|
56
|
+
timer = null;
|
|
57
|
+
chain = Promise.resolve();
|
|
58
|
+
seen = new Set();
|
|
59
|
+
constructor(send, gapMs = 2200, maxLines = 4) {
|
|
60
|
+
this.send = send;
|
|
61
|
+
this.gapMs = gapMs;
|
|
62
|
+
this.maxLines = maxLines;
|
|
63
|
+
}
|
|
64
|
+
get pending() {
|
|
65
|
+
return this.lines.length;
|
|
66
|
+
}
|
|
67
|
+
push(ev) {
|
|
68
|
+
const line = formatToolLine(ev);
|
|
69
|
+
if (!line)
|
|
70
|
+
return;
|
|
71
|
+
if (this.lines[this.lines.length - 1] === line)
|
|
72
|
+
return;
|
|
73
|
+
if (this.seen.has(line) && this.lines.includes(line))
|
|
74
|
+
return;
|
|
75
|
+
this.seen.add(line);
|
|
76
|
+
this.lines.push(line);
|
|
77
|
+
if (this.lines.length >= this.maxLines) {
|
|
78
|
+
void this.flush();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.arm();
|
|
82
|
+
}
|
|
83
|
+
async flush() {
|
|
84
|
+
if (this.timer) {
|
|
85
|
+
clearTimeout(this.timer);
|
|
86
|
+
this.timer = null;
|
|
87
|
+
}
|
|
88
|
+
if (this.lines.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
const text = this.lines.join("\n");
|
|
91
|
+
this.lines = [];
|
|
92
|
+
this.chain = this.chain.then(() => this.send(text).catch(() => { }));
|
|
93
|
+
await this.chain;
|
|
94
|
+
}
|
|
95
|
+
arm() {
|
|
96
|
+
if (this.timer)
|
|
97
|
+
clearTimeout(this.timer);
|
|
98
|
+
this.timer = setTimeout(() => {
|
|
99
|
+
this.timer = null;
|
|
100
|
+
void this.flush();
|
|
101
|
+
}, this.gapMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
|
|
105
|
+
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.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
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",
|
|
@@ -22,8 +22,15 @@
|
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=22"
|
|
24
24
|
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"prepack": "tsc -p tsconfig.json",
|
|
28
|
+
"start": "node dist/cli.js",
|
|
29
|
+
"dev": "tsx src/cli.ts",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
31
|
+
},
|
|
25
32
|
"dependencies": {
|
|
26
|
-
"@nopeek/chat": "
|
|
33
|
+
"@nopeek/chat": "workspace:^0.2.4"
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
29
36
|
"@types/node": "^22.10.0",
|
|
@@ -40,11 +47,5 @@
|
|
|
40
47
|
],
|
|
41
48
|
"publishConfig": {
|
|
42
49
|
"access": "public"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"build": "tsc -p tsconfig.json",
|
|
46
|
-
"start": "node dist/cli.js",
|
|
47
|
-
"dev": "tsx src/cli.ts",
|
|
48
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
49
50
|
}
|
|
50
|
-
}
|
|
51
|
+
}
|