@openape/nest 2.0.2 → 2.1.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.
Files changed (2) hide show
  1. package/dist/index.mjs +539 -12
  2. package/package.json +3 -1
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { watch } from "fs";
5
- import process3 from "process";
5
+ import process4 from "process";
6
6
 
7
7
  // src/lib/registry.ts
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -31,7 +31,7 @@ function listAgents() {
31
31
  import { execFile } from "child_process";
32
32
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
33
33
  import { join as join2 } from "path";
34
- import process from "process";
34
+ import process2 from "process";
35
35
  import { promisify } from "util";
36
36
  var execFileAsync = promisify(execFile);
37
37
  var AGENTS_DIR = "/var/openape/agents";
@@ -49,7 +49,7 @@ function ecosystemContents(apesBin, agentName) {
49
49
  module.exports = {
50
50
  apps: [{
51
51
  name: '${pm2AppName(agentName)}',
52
- script: 'openape-chat-bridge',
52
+ script: 'ape-agent',
53
53
  autorestart: true,
54
54
  max_restarts: 10,
55
55
  min_uptime: '30s',
@@ -154,7 +154,7 @@ var Pm2Supervisor = class {
154
154
  return await execFileAsync(
155
155
  this.deps.apesBin,
156
156
  ["run", "--as", agentName, "--wait", "--", ...args],
157
- { maxBuffer: 1024 * 1024, env: process.env, timeout: 6e4, cwd: "/tmp" }
157
+ { maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4, cwd: "/tmp" }
158
158
  );
159
159
  } catch (err) {
160
160
  const e = err;
@@ -167,7 +167,7 @@ var Pm2Supervisor = class {
167
167
 
168
168
  // src/lib/troop-sync.ts
169
169
  import { execFile as execFile2 } from "child_process";
170
- import process2 from "process";
170
+ import process3 from "process";
171
171
  import { promisify as promisify2 } from "util";
172
172
  var execFileAsync2 = promisify2(execFile2);
173
173
  var TICK_MS = 5 * 60 * 1e3;
@@ -206,7 +206,7 @@ var TroopSync = class {
206
206
  await execFileAsync2(
207
207
  this.deps.apesBin,
208
208
  ["run", "--as", name, "--wait", "--", "apes", "agents", "sync"],
209
- { maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4 }
209
+ { maxBuffer: 1024 * 1024, env: process3.env, timeout: 6e4 }
210
210
  );
211
211
  } catch (err) {
212
212
  this.deps.log(`troop-sync: ${name} failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
@@ -214,15 +214,539 @@ var TroopSync = class {
214
214
  }
215
215
  };
216
216
 
217
+ // src/lib/troop-ws.ts
218
+ import { execFile as execFile3, execFileSync } from "child_process";
219
+ import { createHash } from "crypto";
220
+ import { readFileSync as readFileSync3 } from "fs";
221
+ import { hostname, networkInterfaces } from "os";
222
+
223
+ // ../../packages/cli-auth/dist/index.js
224
+ import { ofetch } from "ofetch";
225
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
226
+ import { homedir as homedir2 } from "os";
227
+ import { join as join3 } from "path";
228
+ import { ofetch as ofetch3 } from "ofetch";
229
+ import { Buffer as Buffer2 } from "buffer";
230
+ import { sign } from "crypto";
231
+ import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
232
+ import { homedir as homedir22 } from "os";
233
+ import { join as join22 } from "path";
234
+ import { ofetch as ofetch2 } from "ofetch";
235
+ import { Buffer as Buffer3 } from "buffer";
236
+ import { createPrivateKey } from "crypto";
237
+ import { ofetch as ofetch4 } from "ofetch";
238
+ function getConfigDir() {
239
+ const override = process.env.OPENAPE_CLI_AUTH_HOME;
240
+ if (override) return override;
241
+ return join3(homedir2(), ".config", "apes");
242
+ }
243
+ function getAuthFile() {
244
+ return join3(getConfigDir(), "auth.json");
245
+ }
246
+ function ensureConfigDir() {
247
+ const dir = getConfigDir();
248
+ if (!existsSync2(dir)) {
249
+ mkdirSync3(dir, { recursive: true, mode: 448 });
250
+ }
251
+ }
252
+ function loadIdpAuth() {
253
+ const file = getAuthFile();
254
+ if (!existsSync2(file)) return null;
255
+ try {
256
+ const raw = readFileSync2(file, "utf-8");
257
+ if (!raw.trim()) return null;
258
+ return JSON.parse(raw);
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ function saveIdpAuth(auth) {
264
+ ensureConfigDir();
265
+ const file = getAuthFile();
266
+ let extra = {};
267
+ if (existsSync2(file)) {
268
+ try {
269
+ const raw = readFileSync2(file, "utf-8");
270
+ if (raw.trim()) {
271
+ const prev = JSON.parse(raw);
272
+ for (const key of Object.keys(prev)) {
273
+ if (!(key in auth)) {
274
+ extra[key] = prev[key];
275
+ }
276
+ }
277
+ }
278
+ } catch {
279
+ extra = {};
280
+ }
281
+ }
282
+ const merged = { ...extra, ...auth };
283
+ writeFileSync3(file, JSON.stringify(merged, null, 2), { mode: 384 });
284
+ }
285
+ var AuthError = class extends Error {
286
+ status;
287
+ hint;
288
+ constructor(status, message, hint) {
289
+ super(hint ? `${message}
290
+ ${hint}` : message);
291
+ this.name = "AuthError";
292
+ this.status = status;
293
+ this.hint = hint;
294
+ }
295
+ };
296
+ var NotLoggedInError = class extends AuthError {
297
+ constructor(hint) {
298
+ super(
299
+ 401,
300
+ "Not logged in",
301
+ hint ?? "Run `apes login <email>` once on this device to authenticate against the OpenApe IdP."
302
+ );
303
+ this.name = "NotLoggedInError";
304
+ }
305
+ };
306
+ var OPENSSH_MAGIC = "openssh-key-v1\0";
307
+ function loadEd25519PrivateKey(pem) {
308
+ if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
309
+ return parseOpenSSHEd25519(pem);
310
+ }
311
+ return createPrivateKey(pem);
312
+ }
313
+ function parseOpenSSHEd25519(pem) {
314
+ const b64 = pem.replace(/-----BEGIN OPENSSH PRIVATE KEY-----/, "").replace(/-----END OPENSSH PRIVATE KEY-----/, "").replace(/\s/g, "");
315
+ const buf = Buffer3.from(b64, "base64");
316
+ let offset = 0;
317
+ const magic = buf.subarray(0, OPENSSH_MAGIC.length).toString("ascii");
318
+ if (magic !== OPENSSH_MAGIC) {
319
+ throw new Error("Not an OpenSSH private key");
320
+ }
321
+ offset += OPENSSH_MAGIC.length;
322
+ const cipherLen = buf.readUInt32BE(offset);
323
+ offset += 4;
324
+ const cipher = buf.subarray(offset, offset + cipherLen).toString();
325
+ offset += cipherLen;
326
+ if (cipher !== "none") {
327
+ throw new Error(`Encrypted keys not supported (cipher: ${cipher}). Decrypt first with: ssh-keygen -p -f <key>`);
328
+ }
329
+ const kdfLen = buf.readUInt32BE(offset);
330
+ offset += 4;
331
+ offset += kdfLen;
332
+ const kdfOptsLen = buf.readUInt32BE(offset);
333
+ offset += 4;
334
+ offset += kdfOptsLen;
335
+ const numKeys = buf.readUInt32BE(offset);
336
+ offset += 4;
337
+ if (numKeys !== 1) {
338
+ throw new Error(`Expected 1 key, got ${numKeys}`);
339
+ }
340
+ const pubSectionLen = buf.readUInt32BE(offset);
341
+ offset += 4;
342
+ offset += pubSectionLen;
343
+ const privSectionLen = buf.readUInt32BE(offset);
344
+ offset += 4;
345
+ const privSection = buf.subarray(offset, offset + privSectionLen);
346
+ let pOffset = 0;
347
+ const check1 = privSection.readUInt32BE(pOffset);
348
+ pOffset += 4;
349
+ const check2 = privSection.readUInt32BE(pOffset);
350
+ pOffset += 4;
351
+ if (check1 !== check2) {
352
+ throw new Error("Check integers mismatch \u2014 key may be corrupted or encrypted");
353
+ }
354
+ const keyTypeLen = privSection.readUInt32BE(pOffset);
355
+ pOffset += 4;
356
+ const keyType = privSection.subarray(pOffset, pOffset + keyTypeLen).toString();
357
+ pOffset += keyTypeLen;
358
+ if (keyType !== "ssh-ed25519") {
359
+ throw new Error(`Expected ssh-ed25519, got ${keyType}`);
360
+ }
361
+ const pubKeyLen = privSection.readUInt32BE(pOffset);
362
+ pOffset += 4;
363
+ const pubKey = privSection.subarray(pOffset, pOffset + pubKeyLen);
364
+ pOffset += pubKeyLen;
365
+ const privKeyLen = privSection.readUInt32BE(pOffset);
366
+ pOffset += 4;
367
+ const privKeyData = privSection.subarray(pOffset, pOffset + privKeyLen);
368
+ const seed = privKeyData.subarray(0, 32);
369
+ return createPrivateKey({
370
+ key: { kty: "OKP", crv: "Ed25519", d: seed.toString("base64url"), x: pubKey.toString("base64url") },
371
+ format: "jwk"
372
+ });
373
+ }
374
+ async function getEndpoints(idp) {
375
+ let disco = {};
376
+ try {
377
+ disco = await ofetch2(`${idp}/.well-known/openid-configuration`);
378
+ } catch {
379
+ }
380
+ return {
381
+ challenge: disco.ddisa_agent_challenge_endpoint ?? `${idp}/api/agent/challenge`,
382
+ authenticate: disco.ddisa_agent_authenticate_endpoint ?? `${idp}/api/agent/authenticate`
383
+ };
384
+ }
385
+ function resolveKeyPath(p) {
386
+ if (p.startsWith("~")) return join22(homedir22(), p.slice(1));
387
+ return p;
388
+ }
389
+ function findSigningKey(auth) {
390
+ const candidates = [];
391
+ if (auth.key_path) candidates.push(resolveKeyPath(auth.key_path));
392
+ candidates.push(join22(homedir22(), ".ssh", "id_ed25519"));
393
+ for (const p of candidates) {
394
+ if (existsSync22(p)) {
395
+ try {
396
+ return { keyPath: p, keyContent: readFileSync22(p, "utf-8") };
397
+ } catch {
398
+ }
399
+ }
400
+ }
401
+ return null;
402
+ }
403
+ async function refreshAgentToken(auth, now = Math.floor(Date.now() / 1e3)) {
404
+ const key = findSigningKey(auth);
405
+ if (!key) return null;
406
+ let privateKey;
407
+ try {
408
+ privateKey = loadEd25519PrivateKey(key.keyContent);
409
+ } catch {
410
+ return null;
411
+ }
412
+ let endpoints;
413
+ try {
414
+ endpoints = await getEndpoints(auth.idp);
415
+ } catch {
416
+ return null;
417
+ }
418
+ let challenge;
419
+ try {
420
+ const resp = await ofetch2(endpoints.challenge, {
421
+ method: "POST",
422
+ headers: { "Content-Type": "application/json" },
423
+ body: { agent_id: auth.email }
424
+ });
425
+ challenge = resp.challenge;
426
+ } catch {
427
+ return null;
428
+ }
429
+ let signature;
430
+ try {
431
+ signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
432
+ } catch {
433
+ return null;
434
+ }
435
+ let authResp;
436
+ try {
437
+ authResp = await ofetch2(endpoints.authenticate, {
438
+ method: "POST",
439
+ headers: { "Content-Type": "application/json" },
440
+ body: { agent_id: auth.email, challenge, signature }
441
+ });
442
+ } catch {
443
+ return null;
444
+ }
445
+ return {
446
+ ...auth,
447
+ access_token: authResp.token,
448
+ expires_at: now + (authResp.expires_in || 3600),
449
+ key_path: auth.key_path ?? key.keyPath
450
+ };
451
+ }
452
+ var EXPIRY_SKEW_SECONDS = 30;
453
+ async function getTokenEndpoint(idp) {
454
+ try {
455
+ const disco = await ofetch3(`${idp}/.well-known/openid-configuration`);
456
+ if (disco.token_endpoint) return disco.token_endpoint;
457
+ } catch {
458
+ }
459
+ return `${idp}/token`;
460
+ }
461
+ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
462
+ const auth = loadIdpAuth();
463
+ if (!auth) {
464
+ throw new NotLoggedInError();
465
+ }
466
+ if (auth.expires_at > now + EXPIRY_SKEW_SECONDS) {
467
+ return auth;
468
+ }
469
+ if (!auth.refresh_token) {
470
+ const refreshed = await refreshAgentToken(auth, now);
471
+ if (refreshed) {
472
+ saveIdpAuth(refreshed);
473
+ return refreshed;
474
+ }
475
+ throw new NotLoggedInError(
476
+ `IdP token expired at ${new Date(auth.expires_at * 1e3).toISOString()} and no refresh_token is stored. Run \`apes login\` again.`
477
+ );
478
+ }
479
+ const tokenEndpoint = await getTokenEndpoint(auth.idp);
480
+ const body = new URLSearchParams({
481
+ grant_type: "refresh_token",
482
+ refresh_token: auth.refresh_token
483
+ });
484
+ let response;
485
+ try {
486
+ response = await ofetch3(tokenEndpoint, {
487
+ method: "POST",
488
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
489
+ body: body.toString()
490
+ });
491
+ } catch (err) {
492
+ const status = err.status ?? err.statusCode ?? 0;
493
+ if (status === 400 || status === 401) {
494
+ saveIdpAuth({ ...auth, refresh_token: void 0 });
495
+ throw new NotLoggedInError(
496
+ `Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
497
+ );
498
+ }
499
+ throw new AuthError(
500
+ 0,
501
+ `Network error refreshing IdP token at ${tokenEndpoint}`,
502
+ `Underlying: ${err.message ?? err}`
503
+ );
504
+ }
505
+ if (!response.access_token) {
506
+ throw new AuthError(0, `IdP refresh response missing access_token (endpoint: ${tokenEndpoint})`);
507
+ }
508
+ const next = {
509
+ ...auth,
510
+ access_token: response.access_token,
511
+ refresh_token: response.refresh_token ?? auth.refresh_token,
512
+ expires_at: now + (response.expires_in ?? 3600)
513
+ };
514
+ saveIdpAuth(next);
515
+ return next;
516
+ }
517
+
518
+ // src/lib/troop-ws.ts
519
+ import WebSocket from "ws";
520
+ var HEARTBEAT_INTERVAL_MS = 3e4;
521
+ var RECONNECT_BASE_MS = 1e3;
522
+ var RECONNECT_MAX_MS = 3e4;
523
+ var TroopWs = class {
524
+ constructor(opts) {
525
+ this.opts = opts;
526
+ this.troopUrl = (opts.troopUrl ?? process.env.OPENAPE_TROOP_WS_URL ?? "wss://troop.openape.ai").replace(/\/$/, "");
527
+ this.hostId = readHostId();
528
+ this.hostname = hostname();
529
+ }
530
+ socket = null;
531
+ heartbeatTimer = null;
532
+ reconnectTimer = null;
533
+ reconnectAttempts = 0;
534
+ stopped = false;
535
+ troopUrl;
536
+ hostId;
537
+ hostname;
538
+ start() {
539
+ this.stopped = false;
540
+ void this.connect();
541
+ }
542
+ stop() {
543
+ this.stopped = true;
544
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
545
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
546
+ this.reconnectTimer = null;
547
+ this.heartbeatTimer = null;
548
+ if (this.socket) {
549
+ try {
550
+ this.socket.close();
551
+ } catch {
552
+ }
553
+ this.socket = null;
554
+ }
555
+ }
556
+ async connect() {
557
+ if (this.stopped) return;
558
+ let token;
559
+ try {
560
+ const auth = await ensureFreshIdpAuth();
561
+ token = auth.access_token;
562
+ } catch (err) {
563
+ if (err instanceof NotLoggedInError) {
564
+ this.opts.log("troop-ws: not logged in (apes login) \u2014 skip connect, will retry");
565
+ } else {
566
+ this.opts.log(`troop-ws: auth refresh failed: ${err instanceof Error ? err.message : String(err)}`);
567
+ }
568
+ this.scheduleReconnect();
569
+ return;
570
+ }
571
+ const url = `${this.troopUrl}/api/nest-ws?token=${encodeURIComponent(token)}`;
572
+ const ws = new WebSocket(url);
573
+ this.socket = ws;
574
+ ws.on("open", () => {
575
+ this.reconnectAttempts = 0;
576
+ this.opts.log(`troop-ws: connected to ${this.troopUrl}`);
577
+ ws.send(JSON.stringify({
578
+ type: "hello",
579
+ host_id: this.hostId,
580
+ hostname: this.hostname,
581
+ version: this.opts.version ?? "unknown"
582
+ }));
583
+ this.heartbeatTimer = setInterval(() => {
584
+ try {
585
+ ws.send(JSON.stringify({ type: "heartbeat" }));
586
+ } catch {
587
+ }
588
+ }, HEARTBEAT_INTERVAL_MS);
589
+ });
590
+ ws.on("message", (data) => {
591
+ const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : "";
592
+ if (!text) return;
593
+ let frame;
594
+ try {
595
+ frame = JSON.parse(text);
596
+ } catch {
597
+ return;
598
+ }
599
+ this.handleFrame(frame).catch((err) => {
600
+ this.opts.log(`troop-ws: frame handler error: ${err instanceof Error ? err.message : String(err)}`);
601
+ });
602
+ });
603
+ ws.on("close", (code, reason) => {
604
+ this.opts.log(`troop-ws: disconnected (${code}${reason.length > 0 ? ` ${reason.toString()}` : ""}) \u2014 reconnecting`);
605
+ if (this.heartbeatTimer) {
606
+ clearInterval(this.heartbeatTimer);
607
+ this.heartbeatTimer = null;
608
+ }
609
+ this.socket = null;
610
+ this.scheduleReconnect();
611
+ });
612
+ ws.on("error", (err) => {
613
+ this.opts.log(`troop-ws: socket error: ${err.message}`);
614
+ });
615
+ }
616
+ scheduleReconnect() {
617
+ if (this.stopped) return;
618
+ if (this.reconnectTimer) return;
619
+ const attempt = Math.min(this.reconnectAttempts, 5);
620
+ this.reconnectAttempts++;
621
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS);
622
+ this.reconnectTimer = setTimeout(() => {
623
+ this.reconnectTimer = null;
624
+ void this.connect();
625
+ }, delay);
626
+ }
627
+ async handleFrame(frame) {
628
+ if (frame.type === "welcome") {
629
+ return;
630
+ }
631
+ if (frame.type === "ack") {
632
+ return;
633
+ }
634
+ if (frame.type === "config-update") {
635
+ await this.handleConfigUpdate(frame);
636
+ return;
637
+ }
638
+ if (frame.type === "spawn-intent") {
639
+ await this.handleSpawnIntent(frame);
640
+ return;
641
+ }
642
+ if (frame.type === "reload-bridge") {
643
+ await this.handleReloadBridge(frame);
644
+ }
645
+ }
646
+ async handleConfigUpdate(frame) {
647
+ const local = frame.agent_email.split("+")[0];
648
+ if (!local) return;
649
+ const dash = local.lastIndexOf("-");
650
+ const name = dash > 0 ? local.slice(0, dash) : local;
651
+ this.opts.log(`troop-ws: config-update for ${name} \u2014 running sync`);
652
+ await this.runApes(["run", "--as", name, "--wait", "--", "apes", "agents", "sync"], `config-update sync ${name}`);
653
+ }
654
+ async handleSpawnIntent(frame) {
655
+ this.opts.log(`troop-ws: spawn-intent ${frame.name} (intent ${frame.intent_id})`);
656
+ const args = ["agents", "spawn", frame.name];
657
+ if (frame.bridge?.key) args.push("--bridge-key", frame.bridge.key);
658
+ if (frame.bridge?.base_url) args.push("--bridge-base-url", frame.bridge.base_url);
659
+ if (frame.bridge?.model) args.push("--bridge-model", frame.bridge.model);
660
+ try {
661
+ const { stdout } = await runWithCapture(this.opts.apesBin, args);
662
+ const match = stdout.match(/Registered as\s+(\S+@\S+)/);
663
+ const agentEmail = match?.[1];
664
+ this.send({ type: "spawn-result", intent_id: frame.intent_id, ok: true, agent_email: agentEmail });
665
+ } catch (err) {
666
+ const error = err instanceof Error ? err.message : String(err);
667
+ this.send({ type: "spawn-result", intent_id: frame.intent_id, ok: false, error });
668
+ }
669
+ }
670
+ async handleReloadBridge(frame) {
671
+ this.opts.log(`troop-ws: reload-bridge ${frame.name}`);
672
+ await this.runApes(
673
+ ["run", "--as", frame.name, "--wait", "--", "pm2", "reload", `openape-bridge-${frame.name}`, "--update-env"],
674
+ `reload-bridge ${frame.name}`
675
+ );
676
+ }
677
+ async runApes(args, label) {
678
+ try {
679
+ await runWithCapture(this.opts.apesBin, args);
680
+ } catch (err) {
681
+ this.opts.log(`troop-ws: ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
682
+ }
683
+ }
684
+ send(frame) {
685
+ const ws = this.socket;
686
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
687
+ try {
688
+ ws.send(JSON.stringify(frame));
689
+ } catch (err) {
690
+ this.opts.log(`troop-ws: send failed: ${err instanceof Error ? err.message : String(err)}`);
691
+ }
692
+ }
693
+ };
694
+ function runWithCapture(bin, args) {
695
+ return new Promise((resolve, reject) => {
696
+ execFile3(bin, args, { maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
697
+ if (err) {
698
+ const msg = stderr.toString() || err.message;
699
+ reject(new Error(msg.split("\n").filter(Boolean).slice(-3).join(" / ")));
700
+ return;
701
+ }
702
+ resolve({ stdout: stdout.toString(), stderr: stderr.toString() });
703
+ });
704
+ });
705
+ }
706
+ function readHostId() {
707
+ try {
708
+ if (process.platform === "darwin") {
709
+ const out = execFileSync("/usr/sbin/ioreg", ["-d2", "-c", "IOPlatformExpertDevice"], { encoding: "utf8" });
710
+ const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
711
+ if (match) return match[1];
712
+ }
713
+ } catch {
714
+ }
715
+ return hashBasedHostId();
716
+ }
717
+ function hashBasedHostId() {
718
+ const nics = networkInterfaces();
719
+ const macs = [];
720
+ for (const list of Object.values(nics)) {
721
+ if (!list) continue;
722
+ for (const nic of list) {
723
+ if (!nic.mac || nic.mac === "00:00:00:00:00:00" || nic.internal) continue;
724
+ macs.push(nic.mac);
725
+ }
726
+ }
727
+ const seed = `${hostname()}|${macs.toSorted().join(",")}`;
728
+ return createHash("sha256").update(seed).digest("hex").slice(0, 32);
729
+ }
730
+ function readNestVersion() {
731
+ try {
732
+ const root = new URL("../../package.json", import.meta.url);
733
+ const pkg = JSON.parse(readFileSync3(root, "utf8"));
734
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
735
+ } catch {
736
+ return "unknown";
737
+ }
738
+ }
739
+
217
740
  // src/index.ts
218
- var APES_BIN = process3.env.OPENAPE_APES_BIN ?? "apes";
741
+ var APES_BIN = process4.env.OPENAPE_APES_BIN ?? "apes";
219
742
  var RECONCILE_DEBOUNCE_MS = 1e3;
220
743
  function log(line) {
221
- process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
744
+ process4.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
222
745
  `);
223
746
  }
224
747
  var supervisor = new Pm2Supervisor({ apesBin: APES_BIN, log });
225
748
  var troopSync = new TroopSync({ apesBin: APES_BIN, log });
749
+ var troopWs = new TroopWs({ apesBin: APES_BIN, log, version: readNestVersion() });
226
750
  async function reconcile() {
227
751
  try {
228
752
  await supervisor.reconcile(listAgents());
@@ -233,6 +757,7 @@ async function reconcile() {
233
757
  }
234
758
  void reconcile();
235
759
  troopSync.start();
760
+ troopWs.start();
236
761
  var reconcileTimer;
237
762
  try {
238
763
  watch(REGISTRY_PATH, () => {
@@ -248,15 +773,17 @@ try {
248
773
  void reconcile();
249
774
  }, 5e3).unref();
250
775
  }
251
- process3.on("SIGTERM", () => {
776
+ process4.on("SIGTERM", () => {
252
777
  log("nest: SIGTERM \u2014 stopping");
253
778
  troopSync.stop();
779
+ troopWs.stop();
254
780
  if (reconcileTimer) clearTimeout(reconcileTimer);
255
- process3.exit(0);
781
+ process4.exit(0);
256
782
  });
257
- process3.on("SIGINT", () => {
783
+ process4.on("SIGINT", () => {
258
784
  log("nest: SIGINT \u2014 stopping");
259
785
  troopSync.stop();
786
+ troopWs.stop();
260
787
  if (reconcileTimer) clearTimeout(reconcileTimer);
261
- process3.exit(0);
788
+ process4.exit(0);
262
789
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/nest",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "OpenApe Nest — local control-plane daemon that supervises agent processes on this computer. Talks to troop SP for ownership state, spawns/destroys agents via DDISA always-grants, supervises chat-bridge children (replacing per-agent launchd plists).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,12 +17,14 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "ofetch": "^1.4.1",
20
+ "ws": "^8.18.0",
20
21
  "@openape/cli-auth": "0.4.0",
21
22
  "@openape/core": "0.16.0"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@antfu/eslint-config": "^7.6.1",
25
26
  "@types/node": "^22.19.13",
27
+ "@types/ws": "^8.5.13",
26
28
  "eslint": "^9.35.0",
27
29
  "tsup": "^8.5.1",
28
30
  "typescript": "^5.9.3",