doer-agent 0.4.6 → 0.4.8
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 +1 -1
- package/dist/agent-codex-cli.js +26 -0
- package/dist/agent-daemon-rpc.js +475 -0
- package/dist/agent-fs-rpc.js +5 -0
- package/dist/agent-run-state.js +61 -3
- package/dist/agent-runtime-utils.js +91 -14
- package/dist/agent-runtime-utils.test.js +38 -0
- package/dist/agent-task-execution.js +1 -215
- package/dist/agent.js +29 -29
- package/dist/daemon-log-runner.js +176 -0
- package/dist/daemon-mcp-server.js +166 -0
- package/package.json +1 -1
- package/runtime/bin/apply_patch +0 -5
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
function readRequiredEnv(name) {
|
|
4
|
+
const value = process.env[name]?.trim() || "";
|
|
5
|
+
if (!value) {
|
|
6
|
+
throw new Error(`${name} is required`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
async function readState(statePath) {
|
|
11
|
+
const raw = await readFile(statePath, "utf8");
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
async function writeState(statePath, state) {
|
|
15
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
16
|
+
}
|
|
17
|
+
async function appendEvent(eventsPath, event) {
|
|
18
|
+
const row = {
|
|
19
|
+
ts: new Date().toISOString(),
|
|
20
|
+
type: event.type,
|
|
21
|
+
text: event.text ?? null,
|
|
22
|
+
pid: typeof event.pid === "number" && event.pid > 0 ? event.pid : null,
|
|
23
|
+
code: typeof event.code === "number" ? event.code : null,
|
|
24
|
+
signal: event.signal?.trim() || null,
|
|
25
|
+
};
|
|
26
|
+
await appendFile(eventsPath, `${JSON.stringify(row)}\n`, "utf8");
|
|
27
|
+
}
|
|
28
|
+
function attachLineLogger(stream, type, eventsPath, pid) {
|
|
29
|
+
if (!stream) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
stream.setEncoding("utf8");
|
|
33
|
+
let pending = "";
|
|
34
|
+
stream.on("data", (chunk) => {
|
|
35
|
+
pending += chunk;
|
|
36
|
+
const lines = pending.split(/\r\n|\n|\r/);
|
|
37
|
+
pending = lines.pop() ?? "";
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
void appendEvent(eventsPath, {
|
|
40
|
+
type,
|
|
41
|
+
text: line,
|
|
42
|
+
pid,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
stream.on("end", () => {
|
|
47
|
+
if (!pending) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
void appendEvent(eventsPath, {
|
|
51
|
+
type,
|
|
52
|
+
text: pending,
|
|
53
|
+
pid,
|
|
54
|
+
});
|
|
55
|
+
pending = "";
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
const statePath = readRequiredEnv("DOER_DAEMON_STATE_PATH");
|
|
60
|
+
const eventsPath = readRequiredEnv("DOER_DAEMON_EVENTS_PATH");
|
|
61
|
+
const command = readRequiredEnv("DOER_DAEMON_COMMAND");
|
|
62
|
+
const cwd = readRequiredEnv("DOER_DAEMON_CWD");
|
|
63
|
+
const shellPath = readRequiredEnv("DOER_DAEMON_SHELL_PATH");
|
|
64
|
+
const childEnv = { ...process.env };
|
|
65
|
+
delete childEnv.DOER_DAEMON_STATE_PATH;
|
|
66
|
+
delete childEnv.DOER_DAEMON_EVENTS_PATH;
|
|
67
|
+
delete childEnv.DOER_DAEMON_COMMAND;
|
|
68
|
+
delete childEnv.DOER_DAEMON_CWD;
|
|
69
|
+
delete childEnv.DOER_DAEMON_SHELL_PATH;
|
|
70
|
+
let state = await readState(statePath);
|
|
71
|
+
state = {
|
|
72
|
+
...state,
|
|
73
|
+
runnerPid: process.pid,
|
|
74
|
+
};
|
|
75
|
+
await writeState(statePath, state);
|
|
76
|
+
const child = spawn(command, {
|
|
77
|
+
cwd,
|
|
78
|
+
env: childEnv,
|
|
79
|
+
shell: shellPath,
|
|
80
|
+
detached: false,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
});
|
|
83
|
+
if (typeof child.pid !== "number" || child.pid <= 0) {
|
|
84
|
+
throw new Error("failed to spawn daemon child");
|
|
85
|
+
}
|
|
86
|
+
state = {
|
|
87
|
+
...state,
|
|
88
|
+
pid: child.pid,
|
|
89
|
+
stoppedAt: null,
|
|
90
|
+
lastExitCode: null,
|
|
91
|
+
};
|
|
92
|
+
await writeState(statePath, state);
|
|
93
|
+
await appendEvent(eventsPath, {
|
|
94
|
+
type: "start",
|
|
95
|
+
pid: child.pid,
|
|
96
|
+
});
|
|
97
|
+
attachLineLogger(child.stdout, "stdout", eventsPath, child.pid);
|
|
98
|
+
attachLineLogger(child.stderr, "stderr", eventsPath, child.pid);
|
|
99
|
+
const forwardSignal = (signal) => {
|
|
100
|
+
if (child.exitCode !== null || child.killed) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
child.kill(signal);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore forwarding failures
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
111
|
+
for (const signal of signals) {
|
|
112
|
+
process.on(signal, () => {
|
|
113
|
+
void appendEvent(eventsPath, {
|
|
114
|
+
type: "signal",
|
|
115
|
+
pid: child.pid,
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
forwardSignal(signal);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
child.once("error", reject);
|
|
123
|
+
child.once("exit", async (code, signal) => {
|
|
124
|
+
try {
|
|
125
|
+
const latest = await readState(statePath);
|
|
126
|
+
await writeState(statePath, {
|
|
127
|
+
...latest,
|
|
128
|
+
pid: null,
|
|
129
|
+
runnerPid: null,
|
|
130
|
+
stoppedAt: new Date().toISOString(),
|
|
131
|
+
lastExitCode: typeof code === "number" ? code : null,
|
|
132
|
+
});
|
|
133
|
+
await appendEvent(eventsPath, {
|
|
134
|
+
type: code === 0 ? "exit" : "error",
|
|
135
|
+
pid: child.pid,
|
|
136
|
+
code,
|
|
137
|
+
signal,
|
|
138
|
+
text: signal ? `process exited due to ${signal}` : code === 0 ? "process exited cleanly" : `process exited with code ${code}`,
|
|
139
|
+
});
|
|
140
|
+
resolve();
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
reject(error);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
main().catch(async (error) => {
|
|
149
|
+
const eventsPath = process.env.DOER_DAEMON_EVENTS_PATH?.trim();
|
|
150
|
+
const statePath = process.env.DOER_DAEMON_STATE_PATH?.trim();
|
|
151
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
152
|
+
if (eventsPath) {
|
|
153
|
+
await appendEvent(eventsPath, {
|
|
154
|
+
type: "error",
|
|
155
|
+
pid: process.pid,
|
|
156
|
+
text: message,
|
|
157
|
+
}).catch(() => undefined);
|
|
158
|
+
}
|
|
159
|
+
if (statePath) {
|
|
160
|
+
try {
|
|
161
|
+
const state = await readState(statePath);
|
|
162
|
+
await writeState(statePath, {
|
|
163
|
+
...state,
|
|
164
|
+
pid: null,
|
|
165
|
+
runnerPid: null,
|
|
166
|
+
stoppedAt: new Date().toISOString(),
|
|
167
|
+
lastExitCode: state.lastExitCode ?? 1,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// ignore
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
process.stderr.write(`${message}\n`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import * as z from "zod/v4";
|
|
6
|
+
import { deleteAgentDaemonLocal, listAgentDaemonsLocal, readAgentDaemonLogsLocal, restartAgentDaemonLocal, startAgentDaemonLocal, stopAgentDaemonLocal, } from "./agent-daemon-rpc.js";
|
|
7
|
+
import { readAgentSettingsConfig } from "./agent-settings.js";
|
|
8
|
+
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
9
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const AGENT_PROJECT_DIR = path.join(MODULE_DIR, "..");
|
|
11
|
+
function parseWorkspaceRoot(argv) {
|
|
12
|
+
const flagIndex = argv.findIndex((token) => token === "--workspace-root");
|
|
13
|
+
const flagValue = flagIndex >= 0 ? argv[flagIndex + 1] : "";
|
|
14
|
+
const envValue = process.env.DOER_DAEMON_WORKSPACE_ROOT?.trim() || process.env.WORKSPACE?.trim() || process.cwd();
|
|
15
|
+
return path.resolve((flagValue || envValue || process.cwd()).trim());
|
|
16
|
+
}
|
|
17
|
+
function formatJson(value) {
|
|
18
|
+
return JSON.stringify(value, null, 2);
|
|
19
|
+
}
|
|
20
|
+
async function main() {
|
|
21
|
+
const workspaceRoot = parseWorkspaceRoot(process.argv.slice(2));
|
|
22
|
+
const runtimeEnvHelpers = createRuntimeEnvHelpers({
|
|
23
|
+
resolveWorkspaceRoot: () => workspaceRoot,
|
|
24
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
25
|
+
});
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: "doer-daemon",
|
|
28
|
+
version: "0.1.0",
|
|
29
|
+
}, {
|
|
30
|
+
capabilities: {
|
|
31
|
+
tools: {},
|
|
32
|
+
},
|
|
33
|
+
instructions: "Manage long-lived workspace daemons. Use these tools to list, start, stop, and inspect daemon logs.",
|
|
34
|
+
});
|
|
35
|
+
server.registerTool("daemon_list", {
|
|
36
|
+
description: "List daemons managed for the current workspace.",
|
|
37
|
+
inputSchema: {},
|
|
38
|
+
}, async () => {
|
|
39
|
+
const daemons = await listAgentDaemonsLocal(workspaceRoot);
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: formatJson({ daemons }),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
structuredContent: { daemons },
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
server.registerTool("daemon_start", {
|
|
51
|
+
description: "Start a new long-lived daemon process for the current workspace.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
command: z.string().min(1).describe("Shell command to run, such as `npm run dev`."),
|
|
54
|
+
cwd: z.string().optional().describe("Optional working directory relative to the workspace root."),
|
|
55
|
+
label: z.string().optional().describe("Optional UI label for the daemon."),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ command, cwd, label }) => {
|
|
58
|
+
const daemon = await startAgentDaemonLocal({
|
|
59
|
+
workspaceRoot,
|
|
60
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
61
|
+
request: {
|
|
62
|
+
command,
|
|
63
|
+
cwd: cwd ?? ".",
|
|
64
|
+
label,
|
|
65
|
+
},
|
|
66
|
+
resolveShellPath: runtimeEnvHelpers.resolveShellPath,
|
|
67
|
+
resolveTaskWorkspace: runtimeEnvHelpers.resolveTaskWorkspace,
|
|
68
|
+
readAgentSettingsConfig,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: formatJson({ daemon }),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
structuredContent: { daemon },
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
server.registerTool("daemon_stop", {
|
|
81
|
+
description: "Stop a running daemon by id.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
id: z.string().min(1).describe("Daemon id returned by daemon_list or daemon_start."),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ id }) => {
|
|
86
|
+
const daemon = await stopAgentDaemonLocal(workspaceRoot, id);
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: formatJson({ daemon }),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
structuredContent: { daemon },
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
server.registerTool("daemon_restart", {
|
|
98
|
+
description: "Restart a daemon by id.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
id: z.string().min(1).describe("Daemon id returned by daemon_list or daemon_start."),
|
|
101
|
+
},
|
|
102
|
+
}, async ({ id }) => {
|
|
103
|
+
const daemon = await restartAgentDaemonLocal({
|
|
104
|
+
workspaceRoot,
|
|
105
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
106
|
+
daemonId: id,
|
|
107
|
+
resolveShellPath: runtimeEnvHelpers.resolveShellPath,
|
|
108
|
+
readAgentSettingsConfig,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: formatJson({ daemon }),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
structuredContent: { daemon },
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
server.registerTool("daemon_delete", {
|
|
121
|
+
description: "Delete a daemon by id, stopping it first if needed.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
id: z.string().min(1).describe("Daemon id returned by daemon_list or daemon_start."),
|
|
124
|
+
},
|
|
125
|
+
}, async ({ id }) => {
|
|
126
|
+
await deleteAgentDaemonLocal(workspaceRoot, id);
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: formatJson({ deleted: true, daemonId: id }),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
structuredContent: { deleted: true, daemonId: id },
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
server.registerTool("daemon_logs", {
|
|
138
|
+
description: "Read recent tail log events for a daemon.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
id: z.string().min(1).describe("Daemon id returned by daemon_list or daemon_start."),
|
|
141
|
+
limit: z.number().int().min(1).max(1000).optional().describe("Maximum number of recent log lines to read."),
|
|
142
|
+
},
|
|
143
|
+
}, async ({ id, limit }) => {
|
|
144
|
+
const logs = await readAgentDaemonLogsLocal({
|
|
145
|
+
workspaceRoot,
|
|
146
|
+
daemonId: id,
|
|
147
|
+
limit,
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: formatJson(logs),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
structuredContent: logs,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
const transport = new StdioServerTransport();
|
|
160
|
+
await server.connect(transport);
|
|
161
|
+
}
|
|
162
|
+
main().catch((error) => {
|
|
163
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
164
|
+
process.stderr.write(`${message}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
package/package.json
CHANGED