@openape/nest 1.1.2 → 2.0.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 +45 -237
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,17 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { watch } from "fs";
4
5
  import process3 from "process";
5
6
 
6
- // src/lib/intent-channel.ts
7
- import { readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
8
- import { homedir as homedir2 } from "os";
9
- import { join as join2 } from "path";
10
-
11
- // src/api/agents.ts
12
- import { execFile } from "child_process";
13
- import { promisify } from "util";
14
-
15
7
  // src/lib/registry.ts
16
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
17
9
  import { homedir } from "os";
@@ -31,220 +23,23 @@ function readRegistry() {
31
23
  return emptyRegistry();
32
24
  }
33
25
  }
34
- function writeRegistry(reg) {
35
- mkdirSync(REGISTRY_DIR, { recursive: true });
36
- writeFileSync(REGISTRY_PATH, `${JSON.stringify(reg, null, 2)}
37
- `, { mode: 384 });
38
- }
39
26
  function listAgents() {
40
27
  return readRegistry().agents;
41
28
  }
42
- function findAgent(name) {
43
- return readRegistry().agents.find((a) => a.name === name);
44
- }
45
- function upsertAgent(entry) {
46
- const reg = readRegistry();
47
- const existing = reg.agents.findIndex((a) => a.name === entry.name);
48
- if (existing >= 0) reg.agents[existing] = entry;
49
- else reg.agents.push(entry);
50
- writeRegistry(reg);
51
- }
52
- function removeAgent(name) {
53
- const reg = readRegistry();
54
- const before = reg.agents.length;
55
- reg.agents = reg.agents.filter((a) => a.name !== name);
56
- if (reg.agents.length === before) return false;
57
- writeRegistry(reg);
58
- return true;
59
- }
60
-
61
- // src/api/agents.ts
62
- var execFileAsync = promisify(execFile);
63
- var NAME_REGEX = /^[a-z][a-z0-9-]{0,23}$/;
64
- async function handleAgentSpawn(ctx) {
65
- const body = ctx.body;
66
- const name = typeof body?.name === "string" ? body.name : "";
67
- if (!NAME_REGEX.test(name)) {
68
- throw new Error(`name must match ${NAME_REGEX} (got "${name}")`);
69
- }
70
- if (findAgent(name)) {
71
- throw new Error(`agent "${name}" is already registered with this nest`);
72
- }
73
- const args = ["run", "--as", "root", "--wait", "--", "apes", "agents", "spawn", name];
74
- const includeBridge = body?.bridge !== false;
75
- if (includeBridge) {
76
- args.push("--bridge");
77
- if (typeof body?.bridgeKey === "string") args.push("--bridge-key", body.bridgeKey);
78
- if (typeof body?.bridgeBaseUrl === "string") args.push("--bridge-base-url", body.bridgeBaseUrl);
79
- if (typeof body?.bridgeModel === "string") args.push("--bridge-model", body.bridgeModel);
80
- }
81
- ctx.log(`nest: spawning agent "${name}" via apes...`);
82
- const { stdout: _stdout } = await execFileAsync(ctx.apesBin, args, { maxBuffer: 4 * 1024 * 1024 });
83
- const uid = await readUidFromDscl(name);
84
- const entry = {
85
- name,
86
- uid,
87
- home: `/Users/${name}`,
88
- email: "",
89
- // filled in on first sync — we don't know it locally
90
- registeredAt: Math.floor(Date.now() / 1e3),
91
- bridge: includeBridge ? {
92
- baseUrl: typeof body?.bridgeBaseUrl === "string" ? body.bridgeBaseUrl : void 0,
93
- apiKey: typeof body?.bridgeKey === "string" ? body.bridgeKey : void 0,
94
- model: typeof body?.bridgeModel === "string" ? body.bridgeModel : void 0
95
- } : void 0
96
- };
97
- upsertAgent(entry);
98
- await ctx.supervisor.reconcile(listAgents());
99
- return { name, email: entry.email, uid, home: entry.home };
100
- }
101
- async function handleAgentDestroy(ctx, name) {
102
- if (!NAME_REGEX.test(name)) throw new Error(`invalid agent name "${name}"`);
103
- const entry = findAgent(name);
104
- if (!entry) throw new Error(`agent "${name}" not registered with this nest`);
105
- ctx.log(`nest: destroying agent "${name}"...`);
106
- const args = ["run", "--as", "root", "--", "apes", "agents", "destroy", name, "--force"];
107
- await execFileAsync(ctx.apesBin, args, { maxBuffer: 4 * 1024 * 1024 });
108
- removeAgent(name);
109
- await ctx.supervisor.reconcile(listAgents());
110
- return { name, removed: true };
111
- }
112
- async function readUidFromDscl(name) {
113
- try {
114
- const { stdout } = await execFileAsync("/usr/bin/dscl", [".", "-read", `/Users/${name}`, "UniqueID"]);
115
- const match = stdout.match(/UniqueID:\s*(\d+)/);
116
- if (match) return Number(match[1]);
117
- } catch {
118
- }
119
- return -1;
120
- }
121
-
122
- // src/lib/intent-channel.ts
123
- var POLL_MS = 1e3;
124
- var INTENTS_DIR = join2(homedir2(), "intents");
125
- var IntentChannel = class {
126
- constructor(deps) {
127
- this.deps = deps;
128
- mkdirSync2(INTENTS_DIR, { recursive: true });
129
- chmodSync(INTENTS_DIR, 504);
130
- }
131
- timer;
132
- inflight = /* @__PURE__ */ new Set();
133
- start() {
134
- if (this.timer) return;
135
- this.timer = setInterval(() => void this.tick(), POLL_MS);
136
- this.deps.log(`intent-channel: polling ${INTENTS_DIR}`);
137
- }
138
- stop() {
139
- if (this.timer) clearInterval(this.timer);
140
- this.timer = void 0;
141
- }
142
- async tick() {
143
- let entries;
144
- try {
145
- entries = readdirSync(INTENTS_DIR);
146
- } catch {
147
- return;
148
- }
149
- for (const f of entries) {
150
- if (!f.endsWith(".json")) continue;
151
- if (this.inflight.has(f)) continue;
152
- this.inflight.add(f);
153
- void this.process(f).finally(() => this.inflight.delete(f));
154
- }
155
- }
156
- async process(filename) {
157
- const path = join2(INTENTS_DIR, filename);
158
- let intent;
159
- try {
160
- const raw = readFileSync2(path, "utf8");
161
- intent = JSON.parse(raw);
162
- } catch (err) {
163
- this.deps.log(`intent-channel: failed to read ${filename}: ${err instanceof Error ? err.message : String(err)}`);
164
- try {
165
- unlinkSync(path);
166
- } catch {
167
- }
168
- return;
169
- }
170
- this.deps.log(`intent-channel: processing ${intent.action} (id=${intent.id})`);
171
- let response;
172
- try {
173
- const ctx = {
174
- url: new URL("intent:/"),
175
- body: intent,
176
- log: this.deps.log,
177
- apesBin: this.deps.apesBin,
178
- caller: "<intent-channel>",
179
- grantId: intent.id,
180
- supervisor: this.deps.supervisor
181
- };
182
- let result;
183
- switch (intent.action) {
184
- case "spawn":
185
- result = await handleAgentSpawn(ctx);
186
- break;
187
- case "destroy":
188
- result = await handleAgentDestroy(ctx, intent.name);
189
- break;
190
- case "list":
191
- result = { agents: listAgents() };
192
- break;
193
- default:
194
- throw new Error(`unknown action: ${intent.action ?? "<undefined>"}`);
195
- }
196
- response = { ok: true, result };
197
- } catch (err) {
198
- const msg = err instanceof Error ? err.message : String(err);
199
- this.deps.log(`intent-channel: ${intent.action} failed: ${msg}`);
200
- response = { ok: false, error: msg };
201
- }
202
- const respTmp = `${path.replace(/\.json$/, "")}.response.tmp`;
203
- const respFinal = `${path.replace(/\.json$/, "")}.response`;
204
- writeFileSync2(respTmp, `${JSON.stringify(response)}
205
- `, { mode: 432 });
206
- renameSync(respTmp, respFinal);
207
- try {
208
- unlinkSync(path);
209
- } catch {
210
- }
211
- }
212
- };
213
- function reapStaleResponses(log2) {
214
- let entries;
215
- try {
216
- entries = readdirSync(INTENTS_DIR);
217
- } catch {
218
- return;
219
- }
220
- const now = Date.now();
221
- for (const f of entries) {
222
- if (!f.endsWith(".response")) continue;
223
- const path = join2(INTENTS_DIR, f);
224
- try {
225
- const st = statSync(path);
226
- if (now - st.mtimeMs > 60 * 60 * 1e3) {
227
- unlinkSync(path);
228
- log2(`intent-channel: reaped stale ${f}`);
229
- }
230
- } catch {
231
- }
232
- }
233
- }
234
29
 
235
30
  // src/lib/pm2-supervisor.ts
236
- import { execFile as execFile2 } from "child_process";
237
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
238
- import { join as join3 } from "path";
31
+ import { execFile } from "child_process";
32
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
33
+ import { join as join2 } from "path";
239
34
  import process from "process";
240
- import { promisify as promisify2 } from "util";
241
- var execFileAsync2 = promisify2(execFile2);
35
+ import { promisify } from "util";
36
+ var execFileAsync = promisify(execFile);
242
37
  var AGENTS_DIR = "/var/openape/agents";
243
38
  function pm2AppName(agentName) {
244
39
  return `openape-bridge-${agentName}`;
245
40
  }
246
41
  function ecosystemPath(agentName) {
247
- return join3(AGENTS_DIR, agentName, "ecosystem.config.js");
42
+ return join2(AGENTS_DIR, agentName, "ecosystem.config.js");
248
43
  }
249
44
  function ecosystemContents(apesBin, agentName) {
250
45
  void apesBin;
@@ -265,7 +60,7 @@ module.exports = {
265
60
  `;
266
61
  }
267
62
  function startScriptPath(agentName) {
268
- return join3(AGENTS_DIR, agentName, "start.sh");
63
+ return join2(AGENTS_DIR, agentName, "start.sh");
269
64
  }
270
65
  function startScriptContents(agentName) {
271
66
  const ecosystem = ecosystemPath(agentName);
@@ -315,13 +110,13 @@ var Pm2Supervisor = class {
315
110
  async stopAll() {
316
111
  }
317
112
  async startOrReload(agentName) {
318
- mkdirSync3(AGENTS_DIR, { recursive: true, mode: 493 });
319
- const dir = join3(AGENTS_DIR, agentName);
320
- mkdirSync3(dir, { recursive: true, mode: 493 });
113
+ mkdirSync2(AGENTS_DIR, { recursive: true, mode: 493 });
114
+ const dir = join2(AGENTS_DIR, agentName);
115
+ mkdirSync2(dir, { recursive: true, mode: 493 });
321
116
  const path = ecosystemPath(agentName);
322
- writeFileSync3(path, ecosystemContents(this.deps.apesBin, agentName), { mode: 420 });
117
+ writeFileSync2(path, ecosystemContents(this.deps.apesBin, agentName), { mode: 420 });
323
118
  const startPath = startScriptPath(agentName);
324
- writeFileSync3(startPath, startScriptContents(agentName), { mode: 493 });
119
+ writeFileSync2(startPath, startScriptContents(agentName), { mode: 493 });
325
120
  void path;
326
121
  try {
327
122
  await this.runAsAgent(agentName, ["bash", startPath]);
@@ -355,7 +150,7 @@ var Pm2Supervisor = class {
355
150
  */
356
151
  async runAsAgent(agentName, args) {
357
152
  try {
358
- return await execFileAsync2(
153
+ return await execFileAsync(
359
154
  this.deps.apesBin,
360
155
  ["run", "--as", agentName, "--wait", "--", ...args],
361
156
  { maxBuffer: 1024 * 1024, env: process.env, timeout: 6e4, cwd: "/tmp" }
@@ -370,10 +165,10 @@ var Pm2Supervisor = class {
370
165
  };
371
166
 
372
167
  // src/lib/troop-sync.ts
373
- import { execFile as execFile3 } from "child_process";
168
+ import { execFile as execFile2 } from "child_process";
374
169
  import process2 from "process";
375
- import { promisify as promisify3 } from "util";
376
- var execFileAsync3 = promisify3(execFile3);
170
+ import { promisify as promisify2 } from "util";
171
+ var execFileAsync2 = promisify2(execFile2);
377
172
  var TICK_MS = 5 * 60 * 1e3;
378
173
  var TroopSync = class {
379
174
  constructor(deps) {
@@ -407,7 +202,7 @@ var TroopSync = class {
407
202
  }
408
203
  async syncOne(name) {
409
204
  try {
410
- await execFileAsync3(
205
+ await execFileAsync2(
411
206
  this.deps.apesBin,
412
207
  ["run", "--as", name, "--wait", "--", "apes", "agents", "sync"],
413
208
  { maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4 }
@@ -420,34 +215,47 @@ var TroopSync = class {
420
215
 
421
216
  // src/index.ts
422
217
  var APES_BIN = process3.env.OPENAPE_APES_BIN ?? "apes";
218
+ var RECONCILE_DEBOUNCE_MS = 1e3;
423
219
  function log(line) {
424
220
  process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
425
221
  `);
426
222
  }
427
223
  var supervisor = new Pm2Supervisor({ apesBin: APES_BIN, log });
428
224
  var troopSync = new TroopSync({ apesBin: APES_BIN, log });
429
- var intentChannel = new IntentChannel({ apesBin: APES_BIN, supervisor, log });
430
- void supervisor.reconcile(listAgents()).then(
431
- () => log("nest: pm2-supervisor reconciled with registry")
432
- ).catch((err) => {
433
- log(`nest: pm2-supervisor reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
434
- });
225
+ async function reconcile() {
226
+ try {
227
+ await supervisor.reconcile(listAgents());
228
+ log("nest: pm2-supervisor reconciled with registry");
229
+ } catch (err) {
230
+ log(`nest: reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
231
+ }
232
+ }
233
+ void reconcile();
435
234
  troopSync.start();
436
- intentChannel.start();
437
- var reaperTimer = setInterval(reapStaleResponses, 60 * 60 * 1e3, log);
235
+ var reconcileTimer;
236
+ try {
237
+ watch(REGISTRY_PATH, () => {
238
+ if (reconcileTimer) clearTimeout(reconcileTimer);
239
+ reconcileTimer = setTimeout(() => {
240
+ void reconcile();
241
+ }, RECONCILE_DEBOUNCE_MS);
242
+ });
243
+ log(`nest: watching ${REGISTRY_PATH} for registry changes`);
244
+ } catch (err) {
245
+ log(`nest: registry watch failed (${err instanceof Error ? err.message : String(err)}) \u2014 falling back to 5s poll`);
246
+ setInterval(() => {
247
+ void reconcile();
248
+ }, 5e3).unref();
249
+ }
438
250
  process3.on("SIGTERM", () => {
439
251
  log("nest: SIGTERM \u2014 stopping");
440
- void supervisor.stopAll();
441
252
  troopSync.stop();
442
- intentChannel.stop();
443
- clearInterval(reaperTimer);
253
+ if (reconcileTimer) clearTimeout(reconcileTimer);
444
254
  process3.exit(0);
445
255
  });
446
256
  process3.on("SIGINT", () => {
447
257
  log("nest: SIGINT \u2014 stopping");
448
- void supervisor.stopAll();
449
258
  troopSync.stop();
450
- intentChannel.stop();
451
- clearInterval(reaperTimer);
259
+ if (reconcileTimer) clearTimeout(reconcileTimer);
452
260
  process3.exit(0);
453
261
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/nest",
3
- "version": "1.1.2",
3
+ "version": "2.0.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",