@nopeek/agent-bridge 0.2.1 → 0.3.0

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/bridge.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BridgeConfig, BrainSpec } from "./config.js";
2
- export declare const VERSION = "0.2.1";
2
+ export declare const VERSION = "0.3.0";
3
3
  export interface PairRequest {
4
4
  pairingSecret: string;
5
5
  appId: string;
@@ -10,6 +10,8 @@ export interface BrainsPatch {
10
10
  brainCmd?: string | null;
11
11
  /** Global webhook brain; null clears it. */
12
12
  brainUrl?: string | null;
13
+ /** Auto-provisioner command for newly adopted bots; null clears it. */
14
+ brainProvisionCmd?: string | null;
13
15
  /** Per-handle entries; a null value removes that bot's override. */
14
16
  map?: Record<string, BrainSpec | null>;
15
17
  }
@@ -43,6 +45,14 @@ export declare class BridgeApp {
43
45
  statusMinimal(): Record<string, unknown>;
44
46
  statusFull(): Record<string, unknown>;
45
47
  private startBot;
48
+ private provisioning;
49
+ /**
50
+ * Auto-provision a brain for a newly adopted bot: run BRAIN_PROVISION_CMD
51
+ * (e.g. "create a Hermes profile with its own soul + memory for this handle")
52
+ * and store its stdout as the bot's brain command. Best-effort — on any
53
+ * failure the bot simply keeps the default brain.
54
+ */
55
+ private provisionBrain;
46
56
  /** Fetch the authoritative bot list and start anything we're missing. */
47
57
  private syncBots;
48
58
  private startCore;
package/dist/bridge.js CHANGED
@@ -6,11 +6,12 @@
6
6
  // Pairing, unpairing and brain config all happen at runtime (from the app) and
7
7
  // persist to <home>/settings.json — no restart, no terminal.
8
8
  import { hostname } from "node:os";
9
+ import { spawn } from "node:child_process";
9
10
  import { saveSettings } from "./config.js";
10
11
  import { BotRunner } from "./bot.js";
11
12
  import { ControlSocket } from "./control.js";
12
13
  import { resolveBrain } from "./brain.js";
13
- export const VERSION = "0.2.1";
14
+ export const VERSION = "0.3.0";
14
15
  export class PairError extends Error {
15
16
  code;
16
17
  constructor(code, message) {
@@ -99,6 +100,8 @@ export class BridgeApp {
99
100
  this.cfg.brainCmd = patch.brainCmd || null;
100
101
  if (patch.brainUrl !== undefined)
101
102
  this.cfg.brainUrl = patch.brainUrl || null;
103
+ if (patch.brainProvisionCmd !== undefined)
104
+ this.cfg.brainProvisionCmd = patch.brainProvisionCmd || null;
102
105
  if (patch.map) {
103
106
  for (const [rawHandle, spec] of Object.entries(patch.map)) {
104
107
  const handle = rawHandle.replace(/^@/, "");
@@ -146,6 +149,7 @@ export class BridgeApp {
146
149
  ? { url: this.cfg.brainUrl }
147
150
  : { echo: true },
148
151
  map: this.cfg.brainMap,
152
+ provisionCmd: this.cfg.brainProvisionCmd,
149
153
  },
150
154
  bots: [...this.bots.values()].map((b) => ({
151
155
  handle: b.info.handle,
@@ -163,6 +167,59 @@ export class BridgeApp {
163
167
  const runner = new BotRunner(info, this.cfg);
164
168
  this.bots.set(info.userId, runner);
165
169
  runner.start(); // background; failures are isolated inside the runner
170
+ void this.provisionBrain(info); // background; bot echoes until it lands
171
+ }
172
+ // A handle is provisioned at most once per process; the persisted BRAIN_MAP
173
+ // entry prevents re-provisioning across restarts.
174
+ provisioning = new Set();
175
+ /**
176
+ * Auto-provision a brain for a newly adopted bot: run BRAIN_PROVISION_CMD
177
+ * (e.g. "create a Hermes profile with its own soul + memory for this handle")
178
+ * and store its stdout as the bot's brain command. Best-effort — on any
179
+ * failure the bot simply keeps the default brain.
180
+ */
181
+ async provisionBrain(info) {
182
+ const cmd = this.cfg.brainProvisionCmd;
183
+ const handle = info.handle.replace(/^@/, "");
184
+ if (!cmd || this.cfg.brainMap[handle] || this.provisioning.has(handle))
185
+ return;
186
+ this.provisioning.add(handle);
187
+ console.log(`[provision:@${handle}] running brain provisioner`);
188
+ const out = await new Promise((resolvePromise) => {
189
+ const child = spawn("bash", ["-c", cmd], {
190
+ stdio: ["ignore", "pipe", "pipe"],
191
+ env: { ...process.env, NOPEEK_BOT_HANDLE: handle, NOPEEK_BOT_USER_ID: info.userId },
192
+ });
193
+ let stdout = "";
194
+ let stderr = "";
195
+ const timer = setTimeout(() => {
196
+ child.kill("SIGKILL");
197
+ resolvePromise(null);
198
+ }, 120_000);
199
+ child.stdout.on("data", (d) => (stdout += d.toString()));
200
+ child.stderr.on("data", (d) => (stderr += d.toString()));
201
+ child.on("error", () => {
202
+ clearTimeout(timer);
203
+ resolvePromise(null);
204
+ });
205
+ child.on("close", (code) => {
206
+ clearTimeout(timer);
207
+ if (code !== 0) {
208
+ console.error(`[provision:@${handle}] exit ${code}. stderr: ${stderr.slice(0, 1000)}`);
209
+ resolvePromise(null);
210
+ return;
211
+ }
212
+ resolvePromise(stdout.trim());
213
+ });
214
+ });
215
+ if (!out) {
216
+ console.error(`[provision:@${handle}] provisioner produced no brain command — bot keeps the default brain`);
217
+ return;
218
+ }
219
+ // Use the LAST non-empty stdout line: provisioners may log progress above it.
220
+ const brainCmd = out.split("\n").map((l) => l.trim()).filter(Boolean).pop();
221
+ this.setBrains({ map: { [handle]: { cmd: brainCmd } } });
222
+ console.log(`[provision:@${handle}] brain provisioned`);
166
223
  }
167
224
  /** Fetch the authoritative bot list and start anything we're missing. */
168
225
  async syncBots() {
package/dist/cli.js CHANGED
@@ -51,7 +51,7 @@ if (sub === "status") {
51
51
  }
52
52
  if (sub === "install") {
53
53
  try {
54
- await installService(cfg);
54
+ await installService(cfg, rest.includes("--no-open"));
55
55
  }
56
56
  catch (err) {
57
57
  console.error(`[install] ${err.message}`);
package/dist/config.d.ts CHANGED
@@ -18,7 +18,16 @@ export interface BridgeConfig {
18
18
  brainUrl: string | null;
19
19
  /** Per-handle overrides: { "<handle>": {"cmd": "…"} | {"url": "…"} }. */
20
20
  brainMap: Record<string, BrainSpec>;
21
+ /**
22
+ * Auto-provisioner: run ONCE for each adopted bot that has no BRAIN_MAP
23
+ * entry (env: NOPEEK_BOT_HANDLE, NOPEEK_BOT_USER_ID). Its trimmed stdout
24
+ * becomes that bot's brain command — e.g. a script that creates a fresh
25
+ * Hermes profile (own soul + memory) per bot and prints how to invoke it.
26
+ */
27
+ brainProvisionCmd: string | null;
21
28
  brainTimeoutMs: number;
29
+ /** Where `install` sends the user to finish setup (opened in the browser). */
30
+ appUrl: string;
22
31
  /** Local control API port (status + pairing + brain config, loopback only). */
23
32
  port: number;
24
33
  /** Where per-bot device identity/key stores live. */
@@ -30,10 +39,11 @@ export interface BridgeConfig {
30
39
  declineMessage: string | null;
31
40
  }
32
41
  export declare const DEFAULT_API_URL = "https://d3qweh72vesa98.cloudfront.net";
42
+ export declare const DEFAULT_APP_URL = "https://d32w3to73s0pu5.cloudfront.net/messenger/";
33
43
  export declare const DEFAULT_PORT = 8790;
34
44
  export declare const DEFAULT_BRAIN_TIMEOUT_MS = 180000;
35
45
  export declare function defaultHomeDir(): string;
36
- 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\"}} (env BRAIN_MAP)\n --brain-timeout-ms <ms> Brain timeout, default 180000 (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.";
46
+ 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\"}} (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 180000 (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.";
37
47
  /** Load config from argv + env + cwd config file + home settings. Never
38
48
  * requires pairing — an unpaired bridge waits for the app to pair it. */
39
49
  export declare function loadConfig(argv?: string[]): BridgeConfig;
package/dist/config.js CHANGED
@@ -10,6 +10,7 @@ import { homedir } from "node:os";
10
10
  import { join, resolve } from "node:path";
11
11
  import { parseArgs } from "node:util";
12
12
  export const DEFAULT_API_URL = "https://d3qweh72vesa98.cloudfront.net";
13
+ export const DEFAULT_APP_URL = "https://d32w3to73s0pu5.cloudfront.net/messenger/";
13
14
  export const DEFAULT_PORT = 8790;
14
15
  export const DEFAULT_BRAIN_TIMEOUT_MS = 180_000;
15
16
  export function defaultHomeDir() {
@@ -22,8 +23,11 @@ const CLI_OPTIONS = {
22
23
  "brain-cmd": { type: "string" },
23
24
  "brain-url": { type: "string" },
24
25
  "brain-map": { type: "string" },
26
+ "brain-provision-cmd": { type: "string" },
25
27
  "brain-timeout-ms": { type: "string" },
26
28
  "decline-message": { type: "string" },
29
+ "app-url": { type: "string" },
30
+ "no-open": { type: "boolean" },
27
31
  port: { type: "string" },
28
32
  "data-dir": { type: "string" },
29
33
  home: { type: "string" },
@@ -38,8 +42,10 @@ const FLAG_TO_KEY = {
38
42
  "brain-cmd": "BRAIN_CMD",
39
43
  "brain-url": "BRAIN_URL",
40
44
  "brain-map": "BRAIN_MAP",
45
+ "brain-provision-cmd": "BRAIN_PROVISION_CMD",
41
46
  "brain-timeout-ms": "BRAIN_TIMEOUT_MS",
42
47
  "decline-message": "NOPEEK_DECLINE_MESSAGE",
48
+ "app-url": "NOPEEK_APP_URL",
43
49
  port: "NOPEEK_BRIDGE_PORT",
44
50
  "data-dir": "NOPEEK_BRIDGE_DATA_DIR",
45
51
  home: "NOPEEK_BRIDGE_HOME",
@@ -63,6 +69,11 @@ Options:
63
69
  --brain-cmd <cmd> Shell brain: message on stdin -> reply on stdout (env BRAIN_CMD)
64
70
  --brain-url <url> Webhook brain: POST {text,...} -> {text|reply} (env BRAIN_URL)
65
71
  --brain-map <json> Per-bot overrides {"<handle>":{"cmd":"…"}|{"url":"…"}} (env BRAIN_MAP)
72
+ --brain-provision-cmd <cmd> Run once per new bot; stdout becomes its brain command
73
+ (env BRAIN_PROVISION_CMD — e.g. a script that creates a
74
+ fresh Hermes profile with its own soul + memory)
75
+ --app-url <url> App opened after install (env NOPEEK_APP_URL)
76
+ --no-open install: don't open the app in the browser
66
77
  --brain-timeout-ms <ms> Brain timeout, default ${DEFAULT_BRAIN_TIMEOUT_MS} (env BRAIN_TIMEOUT_MS)
67
78
  --port <port> Local control API port, default ${DEFAULT_PORT} (env NOPEEK_BRIDGE_PORT)
68
79
  --data-dir <dir> Device-key store dir (env NOPEEK_BRIDGE_DATA_DIR)
@@ -179,11 +190,13 @@ export function loadConfig(argv = process.argv.slice(2)) {
179
190
  brainCmd: get("brain-cmd") ?? null,
180
191
  brainUrl: get("brain-url") ?? null,
181
192
  brainMap: parseBrainMap(get("brain-map")),
193
+ brainProvisionCmd: get("brain-provision-cmd") ?? null,
182
194
  brainTimeoutMs,
183
195
  port,
184
196
  dataDir,
185
197
  homeDir,
186
198
  declineMessage: get("decline-message") ?? null,
199
+ appUrl: get("app-url") ?? DEFAULT_APP_URL,
187
200
  };
188
201
  }
189
202
  /**
@@ -200,6 +213,7 @@ export function saveSettings(cfg) {
200
213
  ...(cfg.brainCmd ? { BRAIN_CMD: cfg.brainCmd } : {}),
201
214
  ...(cfg.brainUrl ? { BRAIN_URL: cfg.brainUrl } : {}),
202
215
  ...(Object.keys(cfg.brainMap).length ? { BRAIN_MAP: JSON.stringify(cfg.brainMap) } : {}),
216
+ ...(cfg.brainProvisionCmd ? { BRAIN_PROVISION_CMD: cfg.brainProvisionCmd } : {}),
203
217
  ...(cfg.declineMessage ? { NOPEEK_DECLINE_MESSAGE: cfg.declineMessage } : {}),
204
218
  };
205
219
  const path = settingsPath(cfg.homeDir);
package/dist/service.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { BridgeConfig } from "./config.js";
2
- export declare function installService(cfg: BridgeConfig): Promise<void>;
2
+ export declare function installService(cfg: BridgeConfig, noOpen?: boolean): Promise<void>;
3
3
  export declare function uninstallService(): void;
4
4
  export declare function printStatus(port: number): Promise<void>;
package/dist/service.js CHANGED
@@ -41,7 +41,7 @@ function launchctl(args, ignoreFailure = false) {
41
41
  throw err;
42
42
  }
43
43
  }
44
- export async function installService(cfg) {
44
+ export async function installService(cfg, noOpen = false) {
45
45
  if (process.platform !== "darwin" && process.platform !== "linux") {
46
46
  throw new Error(`automatic service install supports macOS and Linux. On this platform, run the bridge with any process manager:\n nopeek-agent-bridge run`);
47
47
  }
@@ -122,9 +122,21 @@ WantedBy=default.target
122
122
  }
123
123
  console.log(`
124
124
  Done. Next step — in the NoPeek app on THIS computer:
125
- Contacts -> My Bots -> Connect this computer
125
+ Contacts -> My Bots -> "Run your bots on this computer" -> Connect
126
126
  Pairing, choosing your agent (Hermes, …) and everything else happens in the app.
127
127
  Logs: ${logFile}`);
128
+ // Take the user straight to the app: sign in once, tap Connect, done.
129
+ // (Sessions persist, so next time this is automatic.)
130
+ if (!noOpen) {
131
+ const opener = process.platform === "darwin" ? "open" : "xdg-open";
132
+ try {
133
+ execFileSync(opener, [cfg.appUrl], { stdio: "ignore" });
134
+ console.log(`[install] opened ${cfg.appUrl}`);
135
+ }
136
+ catch {
137
+ console.log(`[install] open ${cfg.appUrl} in your browser to finish setup`);
138
+ }
139
+ }
128
140
  }
129
141
  export function uninstallService() {
130
142
  if (process.platform === "darwin") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nopeek/agent-bridge",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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",