@nopeek/agent-bridge 0.7.6 → 0.7.8
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 +103 -20
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +6 -1
- package/package.json +1 -1
package/dist/backends.js
CHANGED
|
@@ -15,7 +15,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
15
15
|
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
|
-
import { join } from "node:path";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
19
|
import { stripAnsi } from "./brain.js";
|
|
20
20
|
const BRAIN_UNREACHABLE = "⚠️ I couldn't reach my brain just now — please try again in a moment.";
|
|
21
21
|
// ------------------------------------------------------------------ souls ----
|
|
@@ -139,19 +139,31 @@ function runClaudeOnce(bin, args, text, cwd, timeoutMs, tag, onDelta) {
|
|
|
139
139
|
console.error(`${tag} stderr: ${stderr.slice(0, 1000)}`);
|
|
140
140
|
resolvePromise({ reply, exitCode });
|
|
141
141
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
// IDLE timeout (see the identical fix in the hermes runOnce() above): reset
|
|
143
|
+
// on every byte of output so a genuinely agentic, tool-using turn can run as
|
|
144
|
+
// long as it keeps producing output, and only true silence kills it.
|
|
145
|
+
let timer;
|
|
146
|
+
const armIdleTimer = () => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
timer = setTimeout(() => {
|
|
149
|
+
console.error(`${tag} idle ${timeoutMs / 1000}s (no output), killing`);
|
|
150
|
+
child.kill("SIGKILL");
|
|
151
|
+
finish(null);
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
};
|
|
154
|
+
armIdleTimer();
|
|
147
155
|
child.stdout.on("data", (d) => {
|
|
156
|
+
armIdleTimer();
|
|
148
157
|
lineBuf += d.toString();
|
|
149
158
|
const lines = lineBuf.split("\n");
|
|
150
159
|
lineBuf = lines.pop() ?? "";
|
|
151
160
|
for (const line of lines)
|
|
152
161
|
handleLine(line);
|
|
153
162
|
});
|
|
154
|
-
child.stderr.on("data", (d) =>
|
|
163
|
+
child.stderr.on("data", (d) => {
|
|
164
|
+
armIdleTimer();
|
|
165
|
+
stderr += d.toString();
|
|
166
|
+
});
|
|
155
167
|
child.on("error", (err) => {
|
|
156
168
|
clearTimeout(timer);
|
|
157
169
|
console.error(`${tag} spawn error: ${err.message}`);
|
|
@@ -307,17 +319,54 @@ const HERMES_NO_SESSION = /^No session found matching/i;
|
|
|
307
319
|
/** Session footer Hermes prints on exit; we filter it as chrome but capture
|
|
308
320
|
* the id so a fresh session can be renamed for future --continue. */
|
|
309
321
|
const HERMES_SESSION_ID = /^session(_| )?id:\s*(\S+)/i;
|
|
322
|
+
/**
|
|
323
|
+
* How this Hermes CLI selects a profile. Hermes ≥ v0.14 DROPPED the global
|
|
324
|
+
* `--profile <path>` flag (profiles are now named, selected via the
|
|
325
|
+
* HERMES_PROFILE env var); passing the old flag makes it exit 2 with an
|
|
326
|
+
* argparse usage error and NO stdout — which surfaced to users as a bot that
|
|
327
|
+
* "stopped responding" (the empty reply fell through to BRAIN_UNREACHABLE).
|
|
328
|
+
* Detected once per process from `hermes --help`, then cached.
|
|
329
|
+
* "env" → HERMES_PROFILE=<name> (v0.14+)
|
|
330
|
+
* "flag" → --profile <path> (legacy)
|
|
331
|
+
*/
|
|
332
|
+
let hermesProfileMode = null;
|
|
333
|
+
function detectHermesProfileMode(bin) {
|
|
334
|
+
if (hermesProfileMode)
|
|
335
|
+
return hermesProfileMode;
|
|
336
|
+
try {
|
|
337
|
+
const r = spawnSync(bin, ["--help"], {
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
timeout: 20_000,
|
|
340
|
+
env: { ...process.env, HOME: hermesHome() },
|
|
341
|
+
});
|
|
342
|
+
const help = `${r.stdout || ""}${r.stderr || ""}`;
|
|
343
|
+
// Only the legacy CLI advertises a global --profile option.
|
|
344
|
+
hermesProfileMode = /^\s*--profile\b|\[--profile\b/m.test(help) ? "flag" : "env";
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
hermesProfileMode = "env"; // current CLI is the safer default
|
|
348
|
+
}
|
|
349
|
+
console.log(`[hermes] profile selection mode: ${hermesProfileMode}`);
|
|
350
|
+
return hermesProfileMode;
|
|
351
|
+
}
|
|
310
352
|
export function hermesBrain(cfg) {
|
|
311
353
|
const runOnce = (profile, text, sessionName, tag, onChunk) => new Promise((resolvePromise) => {
|
|
312
354
|
const bin = resolveBin("hermes", "HERMES_BIN");
|
|
313
|
-
const
|
|
355
|
+
const mode = detectHermesProfileMode(bin);
|
|
356
|
+
// v0.14+ takes the profile NAME via env; legacy took the PATH via flag.
|
|
357
|
+
const profileName = basename(profile);
|
|
358
|
+
const args = mode === "flag" ? ["--profile", profile, "chat", "-Q"] : ["chat", "-Q"];
|
|
314
359
|
if (sessionName)
|
|
315
360
|
args.push("--continue", sessionName);
|
|
316
361
|
args.push("-q", text);
|
|
317
362
|
const child = spawn(bin, args, {
|
|
318
363
|
stdio: ["ignore", "pipe", "pipe"],
|
|
319
364
|
// Hermes keys everything off $HOME; point it at the Hermes install.
|
|
320
|
-
env: {
|
|
365
|
+
env: {
|
|
366
|
+
...process.env,
|
|
367
|
+
HOME: hermesHome(),
|
|
368
|
+
...(mode === "env" ? { HERMES_PROFILE: profileName } : {}),
|
|
369
|
+
},
|
|
321
370
|
});
|
|
322
371
|
let out = "";
|
|
323
372
|
let lineBuf = "";
|
|
@@ -384,20 +433,35 @@ export function hermesBrain(cfg) {
|
|
|
384
433
|
noSession = true;
|
|
385
434
|
resolvePromise({ reply: out.trim(), noSession, sessionId, stderr, timedOut });
|
|
386
435
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
436
|
+
// IDLE timeout, not a flat wall-clock one: reset on every byte of output
|
|
437
|
+
// (stdout OR stderr — either is a sign the process is alive and working,
|
|
438
|
+
// not hung). A genuinely agentic turn (tool use, indexing a fresh repo on
|
|
439
|
+
// a brand-new profile) can legitimately run for minutes while still
|
|
440
|
+
// producing periodic status output — a flat timer kills that productive
|
|
441
|
+
// work indiscriminately. Only true SILENCE this long means hung.
|
|
442
|
+
let timer;
|
|
443
|
+
const armIdleTimer = () => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
timer = setTimeout(() => {
|
|
446
|
+
console.error(`${tag} idle ${cfg.brainTimeoutMs / 1000}s (no output), killing`);
|
|
447
|
+
timedOut = true;
|
|
448
|
+
child.kill("SIGKILL");
|
|
449
|
+
finish();
|
|
450
|
+
}, cfg.brainTimeoutMs);
|
|
451
|
+
};
|
|
452
|
+
armIdleTimer();
|
|
393
453
|
child.stdout.on("data", (d) => {
|
|
454
|
+
armIdleTimer();
|
|
394
455
|
lineBuf += d.toString();
|
|
395
456
|
const lines = lineBuf.split("\n");
|
|
396
457
|
lineBuf = lines.pop() ?? "";
|
|
397
458
|
for (const line of lines)
|
|
398
459
|
handleLine(line);
|
|
399
460
|
});
|
|
400
|
-
child.stderr.on("data", (d) =>
|
|
461
|
+
child.stderr.on("data", (d) => {
|
|
462
|
+
armIdleTimer();
|
|
463
|
+
stderr += d.toString();
|
|
464
|
+
});
|
|
401
465
|
child.on("error", (err) => {
|
|
402
466
|
clearTimeout(timer);
|
|
403
467
|
console.error(`${tag} spawn error: ${err.message}`);
|
|
@@ -435,9 +499,16 @@ export function hermesBrain(cfg) {
|
|
|
435
499
|
run = await runOnce(profile, text, null, tag, onChunk);
|
|
436
500
|
if (run.sessionId) {
|
|
437
501
|
const bin = resolveBin("hermes", "HERMES_BIN");
|
|
438
|
-
const
|
|
502
|
+
const mode = detectHermesProfileMode(bin);
|
|
503
|
+
const rn = spawn(bin, mode === "flag"
|
|
504
|
+
? ["--profile", profile, "sessions", "rename", run.sessionId, sessionName]
|
|
505
|
+
: ["sessions", "rename", run.sessionId, sessionName], {
|
|
439
506
|
stdio: "ignore",
|
|
440
|
-
env: {
|
|
507
|
+
env: {
|
|
508
|
+
...process.env,
|
|
509
|
+
HOME: hermesHome(),
|
|
510
|
+
...(mode === "env" ? { HERMES_PROFILE: basename(profile) } : {}),
|
|
511
|
+
},
|
|
441
512
|
});
|
|
442
513
|
rn.on("error", () => {
|
|
443
514
|
/* best-effort — worst case the next message starts fresh again */
|
|
@@ -456,9 +527,21 @@ export function hermesBrain(cfg) {
|
|
|
456
527
|
// start), which misdirects the user to run a command that isn't the fix.
|
|
457
528
|
if (run.timedOut) {
|
|
458
529
|
const secs = Math.round(cfg.brainTimeoutMs / 1000);
|
|
459
|
-
return `⚠️ My brain
|
|
530
|
+
return `⚠️ My brain went quiet for over ${secs}s with no output and I had to give up on that turn — a long tool-use step or a very slow provider response can trigger this. Try messaging me again.`;
|
|
531
|
+
}
|
|
532
|
+
// Surface the REAL reason. Blaming "no authenticated provider" for every
|
|
533
|
+
// empty reply hid a CLI-flag incompatibility (Hermes v0.14 dropped
|
|
534
|
+
// --profile) for a full day: the bot kept answering "run hermes model",
|
|
535
|
+
// which was not the fix and sent debugging down the wrong path.
|
|
536
|
+
const err = run.stderr.trim();
|
|
537
|
+
if (/usage: hermes|invalid choice|unrecognized arguments/i.test(err)) {
|
|
538
|
+
return "⚠️ My brain couldn't start — the Hermes CLI on the host rejected the command (its options changed). The bridge needs updating: run `npm i -g @nopeek/agent-bridge@latest` on that computer.";
|
|
539
|
+
}
|
|
540
|
+
if (err) {
|
|
541
|
+
const first = err.split("\n").find((l) => l.trim()) ?? "";
|
|
542
|
+
return `⚠️ My brain returned no answer. Host reported: ${first.slice(0, 180)}`;
|
|
460
543
|
}
|
|
461
|
-
return "⚠️ My brain isn't reachable right now — Hermes
|
|
544
|
+
return "⚠️ My brain isn't reachable right now — Hermes returned nothing. Check `hermes status` / `hermes model` on the host, then message me again.";
|
|
462
545
|
}
|
|
463
546
|
return run.reply;
|
|
464
547
|
};
|
package/dist/bridge.d.ts
CHANGED
package/dist/bridge.js
CHANGED
|
@@ -17,7 +17,7 @@ import { resolveBrain } from "./brain.js";
|
|
|
17
17
|
import { provisionSoul, provisionHermesProfile } from "./backends.js";
|
|
18
18
|
import { reportCapabilities } from "./capabilities.js";
|
|
19
19
|
import { isBrainBackend } from "./config.js";
|
|
20
|
-
export const VERSION = "0.7.
|
|
20
|
+
export const VERSION = "0.7.8";
|
|
21
21
|
/** How often to re-probe + report brain availability to the server. */
|
|
22
22
|
const CAPABILITIES_INTERVAL_MS = 5 * 60_000;
|
|
23
23
|
// ---- Reconnect watchdog -----------------------------------------------------
|
package/dist/config.d.ts
CHANGED
|
@@ -82,9 +82,9 @@ export interface BridgeConfig {
|
|
|
82
82
|
export declare const DEFAULT_API_URL = "https://d3qweh72vesa98.cloudfront.net";
|
|
83
83
|
export declare const DEFAULT_APP_URL = "https://d32w3to73s0pu5.cloudfront.net/messenger/";
|
|
84
84
|
export declare const DEFAULT_PORT = 8790;
|
|
85
|
-
export declare const DEFAULT_BRAIN_TIMEOUT_MS =
|
|
85
|
+
export declare const DEFAULT_BRAIN_TIMEOUT_MS = 300000;
|
|
86
86
|
export declare function defaultHomeDir(): string;
|
|
87
|
-
export declare const HELP = "nopeek-agent-bridge \u2014 run your agents as E2EE NoPeek bots\n\nUsage:\n nopeek-agent-bridge install Install as a background service (launchd/systemd),\n then finish setup from the NoPeek app:\n Bots -> Connect this computer.\n nopeek-agent-bridge uninstall Remove the background service (keeps data/settings).\n nopeek-agent-bridge status Show the running bridge's status.\n nopeek-agent-bridge [run] Run in the foreground. Unpaired bridges wait to be\n paired from the NoPeek app; --pair still works:\n npx @nopeek/agent-bridge --pair npr_\u2026 --app-id app_\u2026\n\nOptions:\n --pair <code> Pairing code from the NoPeek app (env NOPEEK_PAIRING_CODE)\n --app-id <id> NoPeek app id (env NOPEEK_APP_ID)\n --api-url <url> API base, default https://d3qweh72vesa98.cloudfront.net (env NOPEEK_API_URL)\n --brain-cmd <cmd> Shell brain: message on stdin -> reply on stdout (env BRAIN_CMD)\n --brain-url <url> Webhook brain: POST {text,...} -> {text|reply} (env BRAIN_URL)\n --brain-map <json> Per-bot overrides {\"<handle>\":{\"cmd\":\"\u2026\"}|{\"url\":\"\u2026\"}|\n {\"backend\":\"claude\"|\"hermes\"|\"echo\"}} (env BRAIN_MAP)\n --brain-provision-cmd <cmd> Run once per new bot; stdout becomes its brain command\n (env BRAIN_PROVISION_CMD \u2014 e.g. a script that creates a\n fresh Hermes profile with its own soul + memory)\n --app-url <url> App opened after install (env NOPEEK_APP_URL)\n --no-open install: don't open the app in the browser\n --brain-timeout-ms <ms> Brain timeout, default
|
|
87
|
+
export declare const HELP = "nopeek-agent-bridge \u2014 run your agents as E2EE NoPeek bots\n\nUsage:\n nopeek-agent-bridge install Install as a background service (launchd/systemd),\n then finish setup from the NoPeek app:\n Bots -> Connect this computer.\n nopeek-agent-bridge uninstall Remove the background service (keeps data/settings).\n nopeek-agent-bridge status Show the running bridge's status.\n nopeek-agent-bridge [run] Run in the foreground. Unpaired bridges wait to be\n paired from the NoPeek app; --pair still works:\n npx @nopeek/agent-bridge --pair npr_\u2026 --app-id app_\u2026\n\nOptions:\n --pair <code> Pairing code from the NoPeek app (env NOPEEK_PAIRING_CODE)\n --app-id <id> NoPeek app id (env NOPEEK_APP_ID)\n --api-url <url> API base, default https://d3qweh72vesa98.cloudfront.net (env NOPEEK_API_URL)\n --brain-cmd <cmd> Shell brain: message on stdin -> reply on stdout (env BRAIN_CMD)\n --brain-url <url> Webhook brain: POST {text,...} -> {text|reply} (env BRAIN_URL)\n --brain-map <json> Per-bot overrides {\"<handle>\":{\"cmd\":\"\u2026\"}|{\"url\":\"\u2026\"}|\n {\"backend\":\"claude\"|\"hermes\"|\"echo\"}} (env BRAIN_MAP)\n --brain-provision-cmd <cmd> Run once per new bot; stdout becomes its brain command\n (env BRAIN_PROVISION_CMD \u2014 e.g. a script that creates a\n fresh Hermes profile with its own soul + memory)\n --app-url <url> App opened after install (env NOPEEK_APP_URL)\n --no-open install: don't open the app in the browser\n --brain-timeout-ms <ms> Brain timeout, default 300000 (env BRAIN_TIMEOUT_MS)\n --port <port> Local control API port, default 8790 (env NOPEEK_BRIDGE_PORT)\n --data-dir <dir> Device-key store dir (env NOPEEK_BRIDGE_DATA_DIR)\n --home <dir> Bridge home, default ~/.nopeek-bridge (env NOPEEK_BRIDGE_HOME)\n --config <path> Config file, default ./nopeek-bridge.config.json\n -h, --help Show this help\n\nWith no brain configured, bots run in echo mode (\"You said: \u2026\") \u2014 a zero-config smoke test.\nPairing and brains can be managed entirely from the NoPeek app once the service is running.";
|
|
88
88
|
/** Load config from argv + env + cwd config file + home settings. Never
|
|
89
89
|
* requires pairing — an unpaired bridge waits for the app to pair it. */
|
|
90
90
|
export declare function loadConfig(argv?: string[]): BridgeConfig;
|
package/dist/config.js
CHANGED
|
@@ -15,7 +15,12 @@ export function isBrainBackend(v) {
|
|
|
15
15
|
export const DEFAULT_API_URL = "https://d3qweh72vesa98.cloudfront.net";
|
|
16
16
|
export const DEFAULT_APP_URL = "https://d32w3to73s0pu5.cloudfront.net/messenger/";
|
|
17
17
|
export const DEFAULT_PORT = 8790;
|
|
18
|
-
|
|
18
|
+
// Idle timeout (resets on any child-process output, not a flat wall-clock cap
|
|
19
|
+
// — see backends.ts). 300s of true SILENCE covers a long tool-use step (repo
|
|
20
|
+
// indexing, slow provider call) on a genuinely agentic brain without needing
|
|
21
|
+
// per-deployment tuning, while still killing a truly hung process reasonably
|
|
22
|
+
// fast.
|
|
23
|
+
export const DEFAULT_BRAIN_TIMEOUT_MS = 300_000;
|
|
19
24
|
export function defaultHomeDir() {
|
|
20
25
|
return process.env.NOPEEK_BRIDGE_HOME || join(homedir(), ".nopeek-bridge");
|
|
21
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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",
|