@nopeek/agent-bridge 0.5.4 → 0.5.5
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.d.ts +0 -5
- package/dist/backends.js +98 -27
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +1 -1
- package/package.json +1 -1
package/dist/backends.d.ts
CHANGED
|
@@ -33,9 +33,4 @@ export declare function hermesHome(): string;
|
|
|
33
33
|
* stays authenticated from one login). Idempotent.
|
|
34
34
|
*/
|
|
35
35
|
export declare function provisionHermesProfile(handle: string): string;
|
|
36
|
-
/**
|
|
37
|
-
* Hermes backend: per-bot profile (own soul + memories), one persistent
|
|
38
|
-
* session per channel (`--continue nopeek-<channel>`), streaming cleaned
|
|
39
|
-
* stdout lines to onChunk.
|
|
40
|
-
*/
|
|
41
36
|
export declare function hermesBrain(cfg: BridgeConfig): Brain;
|
package/dist/backends.js
CHANGED
|
@@ -292,7 +292,7 @@ export function provisionHermesProfile(handle) {
|
|
|
292
292
|
}
|
|
293
293
|
// Chat chrome Hermes prints even in -Q mode: ruler lines, the echoed prompt
|
|
294
294
|
// bubble (● …), the goodbye line, and session-info footers.
|
|
295
|
-
const HERMES_CHROME = [/^[─━—-]{4,}\s*─*$/, /^●/, /^Goodbye!/, /^session( id
|
|
295
|
+
const HERMES_CHROME = [/^[─━—-]{4,}\s*─*$/, /^●/, /^Goodbye!/, /^session(_| )?id:/i, /^↻/];
|
|
296
296
|
function isHermesChrome(line) {
|
|
297
297
|
return HERMES_CHROME.some((re) => re.test(line));
|
|
298
298
|
}
|
|
@@ -301,26 +301,20 @@ function isHermesChrome(line) {
|
|
|
301
301
|
* session per channel (`--continue nopeek-<channel>`), streaming cleaned
|
|
302
302
|
* stdout lines to onChunk.
|
|
303
303
|
*/
|
|
304
|
+
/** `hermes chat --continue <name>` FAILS (does not create) when the named
|
|
305
|
+
* session doesn't exist yet — this is what its error looks like. */
|
|
306
|
+
const HERMES_NO_SESSION = /^No session found matching/i;
|
|
307
|
+
/** Session footer Hermes prints on exit; we filter it as chrome but capture
|
|
308
|
+
* the id so a fresh session can be renamed for future --continue. */
|
|
309
|
+
const HERMES_SESSION_ID = /^session(_| )?id:\s*(\S+)/i;
|
|
304
310
|
export function hermesBrain(cfg) {
|
|
305
|
-
|
|
306
|
-
const handle = ctx.botHandle.replace(/^@/, "");
|
|
307
|
-
const tag = `[brain:hermes:@${handle}]`;
|
|
311
|
+
const runOnce = (profile, text, sessionName, tag, onChunk) => new Promise((resolvePromise) => {
|
|
308
312
|
const bin = resolveBin("hermes", "HERMES_BIN");
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let profile;
|
|
315
|
-
try {
|
|
316
|
-
profile = provisionHermesProfile(handle);
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
console.error(`${tag} profile provisioning failed: ${err.message}`);
|
|
320
|
-
resolvePromise(BRAIN_UNREACHABLE);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const child = spawn(bin, ["--profile", profile, "chat", "-Q", "--continue", `nopeek-${ctx.channelId}`, "-q", text], {
|
|
313
|
+
const args = ["--profile", profile, "chat", "-Q"];
|
|
314
|
+
if (sessionName)
|
|
315
|
+
args.push("--continue", sessionName);
|
|
316
|
+
args.push("-q", text);
|
|
317
|
+
const child = spawn(bin, args, {
|
|
324
318
|
stdio: ["ignore", "pipe", "pipe"],
|
|
325
319
|
// Hermes keys everything off $HOME; point it at the Hermes install.
|
|
326
320
|
env: { ...process.env, HOME: hermesHome() },
|
|
@@ -330,12 +324,42 @@ export function hermesBrain(cfg) {
|
|
|
330
324
|
let sawContent = false; // suppress leading blank lines
|
|
331
325
|
let stderr = "";
|
|
332
326
|
let settled = false;
|
|
327
|
+
let noSession = false;
|
|
328
|
+
let sessionId = null;
|
|
329
|
+
let bannerCont = false; // inside a wrapped "↻ Resumed session …" banner
|
|
333
330
|
const handleLine = (raw) => {
|
|
334
331
|
const line = stripAnsi(raw);
|
|
332
|
+
const sid = HERMES_SESSION_ID.exec(line);
|
|
333
|
+
if (sid)
|
|
334
|
+
sessionId = sid[2];
|
|
335
|
+
// The resume banner ("↻ Resumed session … (1 message, 2 total messages)")
|
|
336
|
+
// can wrap across lines; swallow continuations until the closing paren.
|
|
337
|
+
if (bannerCont) {
|
|
338
|
+
if (/\)\s*$/.test(line) || !line.trim())
|
|
339
|
+
bannerCont = false;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (/^↻/.test(line.trim())) {
|
|
343
|
+
if (!/\)\s*$/.test(line.trim()))
|
|
344
|
+
bannerCont = true;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
335
347
|
if (isHermesChrome(line))
|
|
336
348
|
return;
|
|
337
349
|
if (!sawContent && !line.trim())
|
|
338
350
|
return;
|
|
351
|
+
// Leading host warnings ("⚠ tirith security scanner…") are chrome,
|
|
352
|
+
// not reply. Only before real content — a ⚠ mid-reply is kept.
|
|
353
|
+
if (!sawContent && /^[⚠✗]/.test(line.trim()))
|
|
354
|
+
return;
|
|
355
|
+
// First content line saying "No session found" = the --continue miss,
|
|
356
|
+
// not a reply. Don't stream it to the chat; the caller retries fresh.
|
|
357
|
+
if (!sawContent && HERMES_NO_SESSION.test(line.trim())) {
|
|
358
|
+
noSession = true;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (noSession)
|
|
362
|
+
return; // swallow the "Use 'hermes sessions list'…" tail too
|
|
339
363
|
sawContent = true;
|
|
340
364
|
out += `${line}\n`;
|
|
341
365
|
onChunk?.(`${line}\n`);
|
|
@@ -346,14 +370,18 @@ export function hermesBrain(cfg) {
|
|
|
346
370
|
settled = true;
|
|
347
371
|
if (lineBuf)
|
|
348
372
|
handleLine(lineBuf);
|
|
349
|
-
|
|
350
|
-
if (!
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
// The session footer lands on STDERR in -Q mode — scan there too.
|
|
374
|
+
if (!sessionId) {
|
|
375
|
+
for (const line of stderr.split("\n")) {
|
|
376
|
+
const sid = HERMES_SESSION_ID.exec(stripAnsi(line).trim());
|
|
377
|
+
if (sid)
|
|
378
|
+
sessionId = sid[2];
|
|
379
|
+
}
|
|
355
380
|
}
|
|
356
|
-
|
|
381
|
+
// "No session found" can land on stderr in some hermes builds.
|
|
382
|
+
if (!out.trim() && HERMES_NO_SESSION.test(stderr.trim()))
|
|
383
|
+
noSession = true;
|
|
384
|
+
resolvePromise({ reply: out.trim(), noSession, sessionId, stderr });
|
|
357
385
|
};
|
|
358
386
|
const timer = setTimeout(() => {
|
|
359
387
|
console.error(`${tag} timed out after ${cfg.brainTimeoutMs / 1000}s, killing`);
|
|
@@ -373,7 +401,7 @@ export function hermesBrain(cfg) {
|
|
|
373
401
|
console.error(`${tag} spawn error: ${err.message}`);
|
|
374
402
|
if (!settled) {
|
|
375
403
|
settled = true;
|
|
376
|
-
resolvePromise(
|
|
404
|
+
resolvePromise({ reply: "", noSession: false, sessionId: null, stderr: err.message });
|
|
377
405
|
}
|
|
378
406
|
});
|
|
379
407
|
child.on("close", () => {
|
|
@@ -381,4 +409,47 @@ export function hermesBrain(cfg) {
|
|
|
381
409
|
finish();
|
|
382
410
|
});
|
|
383
411
|
});
|
|
412
|
+
return async (text, ctx, onChunk) => {
|
|
413
|
+
const handle = ctx.botHandle.replace(/^@/, "");
|
|
414
|
+
const tag = `[brain:hermes:@${handle}]`;
|
|
415
|
+
if (!resolveBin("hermes", "HERMES_BIN")) {
|
|
416
|
+
console.error(`${tag} hermes not found on PATH`);
|
|
417
|
+
return BRAIN_UNREACHABLE;
|
|
418
|
+
}
|
|
419
|
+
let profile;
|
|
420
|
+
try {
|
|
421
|
+
profile = provisionHermesProfile(handle);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
console.error(`${tag} profile provisioning failed: ${err.message}`);
|
|
425
|
+
return BRAIN_UNREACHABLE;
|
|
426
|
+
}
|
|
427
|
+
const sessionName = `nopeek-${ctx.channelId}`;
|
|
428
|
+
let run = await runOnce(profile, text, sessionName, tag, onChunk);
|
|
429
|
+
if (run.noSession && !run.reply) {
|
|
430
|
+
// First message in this channel: the named session doesn't exist yet.
|
|
431
|
+
// Start fresh, then name the new session so the NEXT message continues it.
|
|
432
|
+
console.log(`${tag} no session ${sessionName} yet — starting fresh`);
|
|
433
|
+
run = await runOnce(profile, text, null, tag, onChunk);
|
|
434
|
+
if (run.sessionId) {
|
|
435
|
+
const bin = resolveBin("hermes", "HERMES_BIN");
|
|
436
|
+
const rn = spawn(bin, ["--profile", profile, "sessions", "rename", run.sessionId, sessionName], {
|
|
437
|
+
stdio: "ignore",
|
|
438
|
+
env: { ...process.env, HOME: hermesHome() },
|
|
439
|
+
});
|
|
440
|
+
rn.on("error", () => {
|
|
441
|
+
/* best-effort — worst case the next message starts fresh again */
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
console.error(`${tag} fresh session id not captured — next message will start fresh again`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!run.reply) {
|
|
449
|
+
if (run.stderr.trim())
|
|
450
|
+
console.error(`${tag} stderr: ${run.stderr.slice(0, 1000)}`);
|
|
451
|
+
return "⚠️ My brain isn't reachable right now — Hermes has no authenticated provider on the host. Run 'hermes model' there, then message me again.";
|
|
452
|
+
}
|
|
453
|
+
return run.reply;
|
|
454
|
+
};
|
|
384
455
|
}
|
package/dist/bridge.d.ts
CHANGED
package/dist/bridge.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveBrain } from "./brain.js";
|
|
|
14
14
|
import { provisionSoul, provisionHermesProfile } from "./backends.js";
|
|
15
15
|
import { reportCapabilities } from "./capabilities.js";
|
|
16
16
|
import { isBrainBackend } from "./config.js";
|
|
17
|
-
export const VERSION = "0.5.
|
|
17
|
+
export const VERSION = "0.5.5";
|
|
18
18
|
/** How often to re-probe + report brain availability to the server. */
|
|
19
19
|
const CAPABILITIES_INTERVAL_MS = 5 * 60_000;
|
|
20
20
|
export class PairError extends Error {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Run your own agents as E2EE NoPeek bots. Pairs with a one-time code, runs every bot you own, and pipes messages to any command or webhook.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|