@meandmyagents/agent-runner 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +19 -11
  2. package/package.json +1 -1
  3. package/src/runner.js +342 -13
package/README.md CHANGED
@@ -3,30 +3,38 @@
3
3
  Local visible-session runner for MeAndMyAgents.
4
4
 
5
5
  ```bash
6
- npm exec --yes --registry=https://registry.npmjs.org/ --package=@meandmyagents/agent-runner@latest -- meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex
6
+ npm exec --yes --registry=https://registry.npmjs.org/ --package=@meandmyagents/agent-runner@latest -- meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex-app
7
7
  ```
8
8
 
9
9
  The runner polls the cheap `/api/agent/events` endpoint with an agent-bound key.
10
- When work exists, it claims the wake event and launches a visible Codex or Claude
11
- CLI session in the task project path. It injects that same key into the child
12
- process as `MAA_API_KEY`, so one Mac can run several agent profiles without
13
- sharing identity. The launched agent uses MCP to start, complete, and drain the
14
- remaining actionable cards.
10
+ When work exists, it claims the wake event and launches a visible Codex Desktop
11
+ thread or Claude CLI session in the task project path. Codex app threads get the
12
+ agent key in their per-thread MCP config, so one Mac can run several agent
13
+ profiles without sharing identity. The launched agent uses MCP to start,
14
+ complete, and drain the remaining actionable cards.
15
15
 
16
- Codex is launched with `--ask-for-approval never --sandbox danger-full-access`.
16
+ Use `--launcher codex-app` for Codex Desktop. It opens a Codex app-server
17
+ controlled thread with approval policy `never` and sandbox
18
+ `danger-full-access`, then waits for that turn to finish before polling again.
17
19
  Claude is launched with `--permission-mode bypassPermissions`. The runner prompt
18
20
  also includes the effective workflow instructions saved in MeAndMyAgents, tells
19
- the agent not to wait for human answers, and tells it to verify, commit, and push
20
- code changes before calling `complete_task`.
21
+ the agent not to wait for human answers, and tells it to verify, commit, and
22
+ push code changes before calling `complete_task`.
21
23
 
22
- On macOS, `--launcher codex` will also look for the bundled Codex app executable
24
+ On macOS, `--launcher codex-app` will look for the bundled Codex app executable
23
25
  at `/Applications/Codex.app/Contents/Resources/codex` when `codex` is not on
24
26
  `PATH`. If your launcher lives somewhere else, pass it explicitly:
25
27
 
26
28
  ```bash
27
- meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex --command /Applications/Codex.app/Contents/Resources/codex
29
+ meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex-app --command /Applications/Codex.app/Contents/Resources/codex
28
30
  ```
29
31
 
30
32
  ```bash
31
33
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher claude --once
32
34
  ```
35
+
36
+ For legacy terminal Codex behavior, `--launcher codex` is still available.
37
+
38
+ If the runner reports that Codex Desktop remote control is not ready, open Codex
39
+ Desktop once and make sure the standalone Codex install is available. The runner
40
+ calls `codex remote-control` for you before opening the app-server thread.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meandmyagents/agent-runner",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local visible-session task runner for Me And My Agents.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/runner.js CHANGED
@@ -3,7 +3,10 @@ import { existsSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
 
5
5
  export const defaultApiUrl = "https://meandmyagents.com/api";
6
- const supportedLaunchers = new Set(["codex", "claude"]);
6
+ const codexAppLauncher = "codex-app";
7
+ const supportedLaunchers = new Set(["codex", codexAppLauncher, "claude"]);
8
+ const codexAppClientVersion = "0.1.3";
9
+ const codexAppRequestTimeoutMs = 30_000;
7
10
 
8
11
  export function parseRunnerArgs(argv = process.argv.slice(2)) {
9
12
  const options = {
@@ -126,8 +129,22 @@ export function buildLaunchCommand({
126
129
  throw new Error(`Unsupported launcher: ${launcher}`);
127
130
  }
128
131
 
132
+ if (launcher === codexAppLauncher) {
133
+ return {
134
+ type: codexAppLauncher,
135
+ command: launcherCommand || launcherExecutableName(launcher),
136
+ args: [],
137
+ cwd,
138
+ prompt,
139
+ env: {
140
+ MAA_API_KEY: apiKey,
141
+ MAA_API_URL: mcpUrlFromApiUrl(apiUrl)
142
+ }
143
+ };
144
+ }
145
+
129
146
  return {
130
- command: launcherCommand || launcher,
147
+ command: launcherCommand || launcherExecutableName(launcher),
131
148
  args: buildLauncherArgs({ launcher, prompt, cwd }),
132
149
  cwd,
133
150
  env: {
@@ -137,6 +154,40 @@ export function buildLaunchCommand({
137
154
  };
138
155
  }
139
156
 
157
+ export function buildCodexAppThreadStartParams({ cwd, apiKey, apiUrl }) {
158
+ return {
159
+ approvalPolicy: "never",
160
+ config: apiKey
161
+ ? {
162
+ mcp_servers: {
163
+ me_and_my_agents: {
164
+ enabled: true,
165
+ http_headers: { Authorization: `Bearer ${apiKey}` },
166
+ url: mcpUrlFromApiUrl(apiUrl)
167
+ }
168
+ }
169
+ }
170
+ : null,
171
+ cwd,
172
+ developerInstructions:
173
+ "This thread was opened by the MeAndMyAgents local runner. Treat the user prompt as an autonomous task wake-up and use the MeAndMyAgents MCP server for task state.",
174
+ sandbox: "danger-full-access",
175
+ serviceName: "MeAndMyAgents",
176
+ sessionStartSource: "startup",
177
+ threadSource: "user"
178
+ };
179
+ }
180
+
181
+ export function buildCodexAppTurnStartParams({ threadId, cwd, prompt }) {
182
+ return {
183
+ approvalPolicy: "never",
184
+ cwd,
185
+ input: [{ type: "text", text: prompt }],
186
+ sandboxPolicy: { type: "dangerFullAccess" },
187
+ threadId
188
+ };
189
+ }
190
+
140
191
  function buildLauncherArgs({ launcher, prompt, cwd }) {
141
192
  if (launcher === "codex") {
142
193
  return [
@@ -248,6 +299,14 @@ async function launchEvent(options, event) {
248
299
  `Launching ${launch.command} for ${event.card?.title ?? event.id} in ${launch.cwd}\n`
249
300
  );
250
301
 
302
+ if (options.launcher === codexAppLauncher) {
303
+ await launchCodexAppSession(launch, {
304
+ apiKey: options.key,
305
+ apiUrl: options.apiUrl
306
+ });
307
+ return;
308
+ }
309
+
251
310
  await new Promise((resolve, reject) => {
252
311
  const child = spawn(launch.command, launch.args, {
253
312
  cwd: launch.cwd,
@@ -265,6 +324,269 @@ async function launchEvent(options, event) {
265
324
  });
266
325
  }
267
326
 
327
+ async function launchCodexAppSession(launch, { apiKey, apiUrl }) {
328
+ await ensureCodexAppRemoteControl(launch);
329
+
330
+ const proxy = spawn(launch.command, ["app-server", "proxy"], {
331
+ cwd: launch.cwd,
332
+ env: { ...process.env, ...launch.env },
333
+ stdio: ["pipe", "pipe", "pipe"]
334
+ });
335
+ const connection = createCodexAppConnection(proxy);
336
+
337
+ try {
338
+ await connection.request("initialize", {
339
+ clientInfo: {
340
+ name: "meandmyagents-runner",
341
+ title: "MeAndMyAgents runner",
342
+ version: codexAppClientVersion
343
+ },
344
+ capabilities: { experimentalApi: true }
345
+ });
346
+
347
+ const threadResponse = await connection.request(
348
+ "thread/start",
349
+ buildCodexAppThreadStartParams({ cwd: launch.cwd, apiKey, apiUrl })
350
+ );
351
+ const threadId = threadResponse?.thread?.id;
352
+ if (!threadId) {
353
+ throw new Error("Codex app-server did not return a thread id.");
354
+ }
355
+
356
+ const turnResponse = await connection.request(
357
+ "turn/start",
358
+ buildCodexAppTurnStartParams({
359
+ threadId,
360
+ cwd: launch.cwd,
361
+ prompt: launch.prompt
362
+ })
363
+ );
364
+ const turnId = turnResponse?.turn?.id;
365
+ process.stdout.write(
366
+ `Codex app thread ${threadId}${turnId ? ` turn ${turnId}` : ""} started.\n`
367
+ );
368
+
369
+ const completion = await connection.waitForNotification((message) => {
370
+ if (message.method !== "turn/completed") return false;
371
+ if (message.params?.threadId !== threadId) return false;
372
+ return !turnId || message.params?.turn?.id === turnId;
373
+ });
374
+ const completedTurn = completion.params?.turn;
375
+ if (completedTurn?.status === "failed") {
376
+ const message = completedTurn.error?.message ?? "Codex app turn failed.";
377
+ throw new Error(message);
378
+ }
379
+ } finally {
380
+ connection.close();
381
+ }
382
+ }
383
+
384
+ async function ensureCodexAppRemoteControl(launch) {
385
+ try {
386
+ await runProcess({
387
+ command: launch.command,
388
+ args: ["remote-control"],
389
+ cwd: launch.cwd,
390
+ env: launch.env,
391
+ timeoutMs: codexAppRequestTimeoutMs
392
+ });
393
+ } catch (error) {
394
+ throw new Error(
395
+ [
396
+ "Codex Desktop remote control is not ready.",
397
+ String(error?.message ?? error),
398
+ "Open Codex Desktop, make sure the standalone Codex install is available, then run the runner again."
399
+ ].join("\n")
400
+ );
401
+ }
402
+ }
403
+
404
+ function createCodexAppConnection(child) {
405
+ const pending = new Map();
406
+ const notificationWaiters = new Set();
407
+ const notificationBacklog = [];
408
+ let stdoutBuffer = "";
409
+ let stderr = "";
410
+ let closed = false;
411
+
412
+ child.stdout.on("data", (chunk) => {
413
+ stdoutBuffer += chunk.toString("utf8");
414
+ stdoutBuffer = consumeCodexAppMessages(stdoutBuffer, (message) => {
415
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
416
+ const waiter = pending.get(message.id);
417
+ if (!waiter) return;
418
+ pending.delete(message.id);
419
+ clearTimeout(waiter.timer);
420
+ if (message.error) {
421
+ waiter.reject(new Error(message.error.message ?? "Codex app-server request failed."));
422
+ } else {
423
+ waiter.resolve(message.result);
424
+ }
425
+ return;
426
+ }
427
+
428
+ let handled = false;
429
+ for (const waiter of notificationWaiters) {
430
+ if (waiter.predicate(message)) {
431
+ notificationWaiters.delete(waiter);
432
+ clearTimeout(waiter.timer);
433
+ waiter.resolve(message);
434
+ handled = true;
435
+ }
436
+ }
437
+ if (!handled) {
438
+ notificationBacklog.push(message);
439
+ if (notificationBacklog.length > 100) notificationBacklog.shift();
440
+ }
441
+ });
442
+ });
443
+
444
+ child.stderr.on("data", (chunk) => {
445
+ stderr += chunk.toString("utf8");
446
+ });
447
+
448
+ child.once("error", (error) => {
449
+ rejectAll(error);
450
+ });
451
+
452
+ child.once("exit", (code) => {
453
+ closed = true;
454
+ if (code && code !== 0) {
455
+ rejectAll(new Error(`Codex app-server proxy exited with code ${code}: ${stderr}`));
456
+ }
457
+ });
458
+
459
+ let nextId = 1;
460
+
461
+ return {
462
+ request(method, params) {
463
+ if (closed) {
464
+ return Promise.reject(new Error("Codex app-server proxy is closed."));
465
+ }
466
+
467
+ const id = nextId;
468
+ nextId += 1;
469
+ const message = { id, method, params };
470
+
471
+ return new Promise((resolve, reject) => {
472
+ const timer = setTimeout(() => {
473
+ pending.delete(id);
474
+ reject(new Error(`Codex app-server request timed out: ${method}`));
475
+ }, codexAppRequestTimeoutMs);
476
+ pending.set(id, { resolve, reject, timer });
477
+ child.stdin.write(`${JSON.stringify(message)}\n`);
478
+ });
479
+ },
480
+
481
+ waitForNotification(predicate) {
482
+ if (closed) {
483
+ return Promise.reject(new Error("Codex app-server proxy is closed."));
484
+ }
485
+
486
+ const existingIndex = notificationBacklog.findIndex(predicate);
487
+ if (existingIndex >= 0) {
488
+ const [message] = notificationBacklog.splice(existingIndex, 1);
489
+ return Promise.resolve(message);
490
+ }
491
+
492
+ return new Promise((resolve, reject) => {
493
+ const timer = setTimeout(() => {
494
+ notificationWaiters.delete(waiter);
495
+ reject(new Error("Timed out waiting for Codex app turn completion."));
496
+ }, Number(process.env.MAA_CODEX_APP_TURN_TIMEOUT_MS ?? 21_600_000));
497
+ const waiter = { predicate, resolve, reject, timer };
498
+ notificationWaiters.add(waiter);
499
+ });
500
+ },
501
+
502
+ close() {
503
+ closed = true;
504
+ child.kill("SIGTERM");
505
+ }
506
+ };
507
+
508
+ function rejectAll(error) {
509
+ for (const waiter of pending.values()) {
510
+ clearTimeout(waiter.timer);
511
+ waiter.reject(error);
512
+ }
513
+ pending.clear();
514
+
515
+ for (const waiter of notificationWaiters) {
516
+ clearTimeout(waiter.timer);
517
+ waiter.reject(error);
518
+ }
519
+ notificationWaiters.clear();
520
+ }
521
+ }
522
+
523
+ function consumeCodexAppMessages(buffer, onMessage) {
524
+ let nextBuffer = buffer;
525
+
526
+ while (nextBuffer.length > 0) {
527
+ const contentLengthMatch = nextBuffer.match(/^Content-Length: (\d+)\r?\n\r?\n/i);
528
+ if (contentLengthMatch) {
529
+ const headerLength = contentLengthMatch[0].length;
530
+ const contentLength = Number(contentLengthMatch[1]);
531
+ if (nextBuffer.length < headerLength + contentLength) break;
532
+ emitJson(nextBuffer.slice(headerLength, headerLength + contentLength), onMessage);
533
+ nextBuffer = nextBuffer.slice(headerLength + contentLength);
534
+ continue;
535
+ }
536
+
537
+ const newlineIndex = nextBuffer.indexOf("\n");
538
+ if (newlineIndex === -1) break;
539
+ const line = nextBuffer.slice(0, newlineIndex).trim();
540
+ nextBuffer = nextBuffer.slice(newlineIndex + 1);
541
+ if (line) emitJson(line, onMessage);
542
+ }
543
+
544
+ return nextBuffer;
545
+ }
546
+
547
+ function emitJson(value, onMessage) {
548
+ try {
549
+ onMessage(JSON.parse(value));
550
+ } catch {
551
+ // Ignore non-JSON process output; request timeouts surface protocol failures.
552
+ }
553
+ }
554
+
555
+ async function runProcess({ command, args, cwd, env, timeoutMs }) {
556
+ return await new Promise((resolve, reject) => {
557
+ const child = spawn(command, args, {
558
+ cwd,
559
+ env: { ...process.env, ...env },
560
+ stdio: ["ignore", "pipe", "pipe"]
561
+ });
562
+ let stdout = "";
563
+ let stderr = "";
564
+ const timer = setTimeout(() => {
565
+ child.kill("SIGTERM");
566
+ reject(new Error(`${command} ${args.join(" ")} timed out.`));
567
+ }, timeoutMs);
568
+
569
+ child.stdout.on("data", (chunk) => {
570
+ stdout += chunk.toString("utf8");
571
+ });
572
+ child.stderr.on("data", (chunk) => {
573
+ stderr += chunk.toString("utf8");
574
+ });
575
+ child.once("error", (error) => {
576
+ clearTimeout(timer);
577
+ reject(error);
578
+ });
579
+ child.once("exit", (code) => {
580
+ clearTimeout(timer);
581
+ if (code && code !== 0) {
582
+ reject(new Error(stderr || stdout || `${command} exited with code ${code}`));
583
+ return;
584
+ }
585
+ resolve({ stdout, stderr });
586
+ });
587
+ });
588
+ }
589
+
268
590
  export function resolveLauncherCommand({
269
591
  launcher,
270
592
  launcherCommand = "",
@@ -278,12 +600,18 @@ export function resolveLauncherCommand({
278
600
  throw new Error(`Unsupported launcher: ${launcher}`);
279
601
  }
280
602
 
281
- if (isCommandOnPath(launcher, { pathEnv, platform, exists })) return launcher;
603
+ const executableName = launcherExecutableName(launcher);
282
604
 
283
- const knownPath = knownLauncherPaths(launcher, platform, homeDir).find((path) =>
284
- exists(path)
285
- );
286
- return knownPath ?? launcher;
605
+ if (isCommandOnPath(executableName, { pathEnv, platform, exists })) {
606
+ return executableName;
607
+ }
608
+
609
+ const knownPath = knownLauncherPaths(launcher, platform, homeDir).find((path) => exists(path));
610
+ return knownPath ?? executableName;
611
+ }
612
+
613
+ function launcherExecutableName(launcher) {
614
+ return launcher === codexAppLauncher ? "codex" : launcher;
287
615
  }
288
616
 
289
617
  function isCommandOnPath(command, { pathEnv, platform, exists }) {
@@ -301,12 +629,12 @@ function isCommandOnPath(command, { pathEnv, platform, exists }) {
301
629
  function knownLauncherPaths(launcher, platform, homeDir) {
302
630
  if (platform === "darwin") {
303
631
  const commonBinPaths = [
304
- homeDir ? join(homeDir, ".local/bin", launcher) : "",
305
- join("/opt/homebrew/bin", launcher),
306
- join("/usr/local/bin", launcher)
632
+ homeDir ? join(homeDir, ".local/bin", launcherExecutableName(launcher)) : "",
633
+ join("/opt/homebrew/bin", launcherExecutableName(launcher)),
634
+ join("/usr/local/bin", launcherExecutableName(launcher))
307
635
  ].filter(Boolean);
308
636
 
309
- if (launcher === "codex") {
637
+ if (launcher === "codex" || launcher === codexAppLauncher) {
310
638
  return [
311
639
  ...commonBinPaths,
312
640
  "/Applications/Codex.app/Contents/Resources/codex"
@@ -316,7 +644,7 @@ function knownLauncherPaths(launcher, platform, homeDir) {
316
644
  return commonBinPaths;
317
645
  }
318
646
 
319
- return homeDir ? [join(homeDir, ".local/bin", launcher)] : [];
647
+ return homeDir ? [join(homeDir, ".local/bin", launcherExecutableName(launcher))] : [];
320
648
  }
321
649
 
322
650
  function normalizeApiUrl(value) {
@@ -342,13 +670,14 @@ function helpText() {
342
670
  return `MeAndMyAgents agent runner
343
671
 
344
672
  Usage:
673
+ meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex-app
345
674
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex
346
675
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher claude --once
347
676
 
348
677
  Options:
349
678
  --key, -k Required agent-bound API key.
350
679
  --api-url, -u API base URL. Defaults to ${defaultApiUrl}.
351
- --launcher, -l Visible CLI launcher: codex or claude.
680
+ --launcher, -l Visible launcher: codex-app, codex, or claude.
352
681
  --command, -c Optional explicit executable path, e.g. /Applications/Codex.app/Contents/Resources/codex.
353
682
  --interval Poll interval in seconds. Defaults to 10.
354
683
  --once Check once, launch at most one visible session, then exit.