@meandmyagents/agent-runner 0.1.1 → 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 +22 -8
  2. package/package.json +1 -1
  3. package/src/runner.js +389 -17
package/README.md CHANGED
@@ -3,24 +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
- On macOS, `--launcher codex` will also look for the bundled Codex app executable
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.
19
+ Claude is launched with `--permission-mode bypassPermissions`. The runner prompt
20
+ also includes the effective workflow instructions saved in MeAndMyAgents, tells
21
+ the agent not to wait for human answers, and tells it to verify, commit, and
22
+ push code changes before calling `complete_task`.
23
+
24
+ On macOS, `--launcher codex-app` will look for the bundled Codex app executable
17
25
  at `/Applications/Codex.app/Contents/Resources/codex` when `codex` is not on
18
26
  `PATH`. If your launcher lives somewhere else, pass it explicitly:
19
27
 
20
28
  ```bash
21
- 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
22
30
  ```
23
31
 
24
32
  ```bash
25
33
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher claude --once
26
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.1",
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 = {
@@ -77,17 +80,38 @@ export function buildAgentPrompt({
77
80
  agentName,
78
81
  cardId,
79
82
  eventId,
80
- projectName
83
+ project,
84
+ projectName,
85
+ workflow
81
86
  }) {
87
+ const workflowInstructions = Array.isArray(workflow?.instructions)
88
+ ? workflow.instructions
89
+ : [];
90
+ const defaultBranch = project?.defaultBranch || "the default branch";
91
+ const codebaseLines = project?.repositoryUrl
92
+ ? [
93
+ `Repository: ${project.repositoryUrl}`,
94
+ `Default branch: ${defaultBranch}`,
95
+ project.localPath ? `Local checkout path: ${project.localPath}` : null,
96
+ `If you modify code, run verification, then commit and push to ${defaultBranch} before complete_task.`
97
+ ].filter(Boolean)
98
+ : ["This project has no repository URL, so treat it as non-code work unless list_my_tasks says otherwise."];
99
+
82
100
  return [
83
101
  `You are ${agentName}. MeAndMyAgents has assigned work in ${projectName}.`,
84
102
  `Wake event: ${eventId}. First card that woke this session: ${cardId}.`,
103
+ "This is an autonomous runner wake-up. Do not wait for a human answer.",
104
+ "If you are blocked, add a card comment explaining the blocker and stop; do not sit idle asking a question.",
85
105
  "Use the MeAndMyAgents MCP server now.",
86
106
  "Call whoami to confirm your identity and project access.",
107
+ "Follow these workflow rules from MeAndMyAgents:",
108
+ ...workflowInstructions.map((instruction) => `- ${instruction}`),
109
+ "Codebase context:",
110
+ ...codebaseLines.map((line) => `- ${line}`),
87
111
  "Call list_my_tasks, then start_task on the next actionable card before editing files.",
88
112
  "Work only in the task codebase.localPath and only when codebase.canModifyCode is true.",
89
113
  "Add comments with useful progress and completion summaries.",
90
- "Call complete_task when a task is ready for human testing.",
114
+ "Call complete_task with a useful summary when a task is ready for human testing.",
91
115
  "Call list_my_tasks again after each completion.",
92
116
  "Keep taking the next task until no actionable tasks remain, then stop."
93
117
  ].join("\n");
@@ -105,9 +129,23 @@ export function buildLaunchCommand({
105
129
  throw new Error(`Unsupported launcher: ${launcher}`);
106
130
  }
107
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
+
108
146
  return {
109
- command: launcherCommand || launcher,
110
- args: [prompt],
147
+ command: launcherCommand || launcherExecutableName(launcher),
148
+ args: buildLauncherArgs({ launcher, prompt, cwd }),
111
149
  cwd,
112
150
  env: {
113
151
  MAA_API_KEY: apiKey,
@@ -116,6 +154,60 @@ export function buildLaunchCommand({
116
154
  };
117
155
  }
118
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
+
191
+ function buildLauncherArgs({ launcher, prompt, cwd }) {
192
+ if (launcher === "codex") {
193
+ return [
194
+ "--ask-for-approval",
195
+ "never",
196
+ "--sandbox",
197
+ "danger-full-access",
198
+ "--cd",
199
+ cwd,
200
+ prompt
201
+ ];
202
+ }
203
+
204
+ if (launcher === "claude") {
205
+ return ["--permission-mode", "bypassPermissions", prompt];
206
+ }
207
+
208
+ return [prompt];
209
+ }
210
+
119
211
  export async function runCli(argv = process.argv.slice(2)) {
120
212
  const options = parseRunnerArgs(argv);
121
213
 
@@ -187,7 +279,9 @@ async function launchEvent(options, event) {
187
279
  agentName: event.agent?.name ?? options.launcher,
188
280
  cardId: event.card?.id ?? "unknown-card",
189
281
  eventId: event.id,
190
- projectName: event.project?.name ?? "this project"
282
+ project: event.project,
283
+ projectName: event.project?.name ?? "this project",
284
+ workflow: event.workflow
191
285
  });
192
286
  const launch = buildLaunchCommand({
193
287
  launcher: options.launcher,
@@ -205,6 +299,14 @@ async function launchEvent(options, event) {
205
299
  `Launching ${launch.command} for ${event.card?.title ?? event.id} in ${launch.cwd}\n`
206
300
  );
207
301
 
302
+ if (options.launcher === codexAppLauncher) {
303
+ await launchCodexAppSession(launch, {
304
+ apiKey: options.key,
305
+ apiUrl: options.apiUrl
306
+ });
307
+ return;
308
+ }
309
+
208
310
  await new Promise((resolve, reject) => {
209
311
  const child = spawn(launch.command, launch.args, {
210
312
  cwd: launch.cwd,
@@ -222,6 +324,269 @@ async function launchEvent(options, event) {
222
324
  });
223
325
  }
224
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
+
225
590
  export function resolveLauncherCommand({
226
591
  launcher,
227
592
  launcherCommand = "",
@@ -235,12 +600,18 @@ export function resolveLauncherCommand({
235
600
  throw new Error(`Unsupported launcher: ${launcher}`);
236
601
  }
237
602
 
238
- if (isCommandOnPath(launcher, { pathEnv, platform, exists })) return launcher;
603
+ const executableName = launcherExecutableName(launcher);
239
604
 
240
- const knownPath = knownLauncherPaths(launcher, platform, homeDir).find((path) =>
241
- exists(path)
242
- );
243
- 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;
244
615
  }
245
616
 
246
617
  function isCommandOnPath(command, { pathEnv, platform, exists }) {
@@ -258,12 +629,12 @@ function isCommandOnPath(command, { pathEnv, platform, exists }) {
258
629
  function knownLauncherPaths(launcher, platform, homeDir) {
259
630
  if (platform === "darwin") {
260
631
  const commonBinPaths = [
261
- homeDir ? join(homeDir, ".local/bin", launcher) : "",
262
- join("/opt/homebrew/bin", launcher),
263
- 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))
264
635
  ].filter(Boolean);
265
636
 
266
- if (launcher === "codex") {
637
+ if (launcher === "codex" || launcher === codexAppLauncher) {
267
638
  return [
268
639
  ...commonBinPaths,
269
640
  "/Applications/Codex.app/Contents/Resources/codex"
@@ -273,7 +644,7 @@ function knownLauncherPaths(launcher, platform, homeDir) {
273
644
  return commonBinPaths;
274
645
  }
275
646
 
276
- return homeDir ? [join(homeDir, ".local/bin", launcher)] : [];
647
+ return homeDir ? [join(homeDir, ".local/bin", launcherExecutableName(launcher))] : [];
277
648
  }
278
649
 
279
650
  function normalizeApiUrl(value) {
@@ -299,13 +670,14 @@ function helpText() {
299
670
  return `MeAndMyAgents agent runner
300
671
 
301
672
  Usage:
673
+ meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex-app
302
674
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher codex
303
675
  meandmyagents-runner watch --key maa_live_YOUR_AGENT_KEY --launcher claude --once
304
676
 
305
677
  Options:
306
678
  --key, -k Required agent-bound API key.
307
679
  --api-url, -u API base URL. Defaults to ${defaultApiUrl}.
308
- --launcher, -l Visible CLI launcher: codex or claude.
680
+ --launcher, -l Visible launcher: codex-app, codex, or claude.
309
681
  --command, -c Optional explicit executable path, e.g. /Applications/Codex.app/Contents/Resources/codex.
310
682
  --interval Poll interval in seconds. Defaults to 10.
311
683
  --once Check once, launch at most one visible session, then exit.