@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.
- package/README.md +22 -8
- package/package.json +1 -1
- 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
|
|
11
|
-
CLI session in the task project path.
|
|
12
|
-
|
|
13
|
-
sharing identity. The launched agent uses MCP to start,
|
|
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
|
-
|
|
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
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
603
|
+
const executableName = launcherExecutableName(launcher);
|
|
239
604
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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.
|