doer-agent 0.8.2 → 0.8.4
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/dist/agent-codex-cli.js
CHANGED
|
@@ -70,6 +70,22 @@ export function buildDaemonMcpConfigArgs(args) {
|
|
|
70
70
|
workspaceRootEnvName: "DOER_DAEMON_WORKSPACE_ROOT",
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
export function buildThreadsMcpConfigArgs(args) {
|
|
74
|
+
return buildWorkspaceMcpConfigArgs({
|
|
75
|
+
agentProjectDir: args.agentProjectDir,
|
|
76
|
+
workspaceRoot: args.workspaceRoot,
|
|
77
|
+
serverName: args.serverName?.trim() || "doer_threads",
|
|
78
|
+
distEntryRelativePath: path.join("dist", "threads-mcp-server.js"),
|
|
79
|
+
srcEntryRelativePath: path.join("src", "threads-mcp-server.ts"),
|
|
80
|
+
workspaceRootEnvName: "DOER_THREADS_WORKSPACE_ROOT",
|
|
81
|
+
env: {
|
|
82
|
+
DOER_THREADS_AGENT_ID: args.agentId,
|
|
83
|
+
DOER_AGENT_TOKEN: args.agentToken,
|
|
84
|
+
DOER_THREADS_SERVER_BASE_URL: args.serverBaseUrl,
|
|
85
|
+
DOER_THREADS_USER_ID: args.userId,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
73
89
|
export function buildMobileMcpConfigArgs(args) {
|
|
74
90
|
return buildWorkspaceMcpConfigArgs({
|
|
75
91
|
agentProjectDir: args.agentProjectDir,
|
|
@@ -88,7 +104,7 @@ export function buildMobileMcpConfigArgs(args) {
|
|
|
88
104
|
}
|
|
89
105
|
export function buildCustomMcpConfigArgs(servers) {
|
|
90
106
|
const configArgs = [];
|
|
91
|
-
const reservedNames = new Set(["doer_daemon", "doer_mobile"]);
|
|
107
|
+
const reservedNames = new Set(["doer_daemon", "doer_mobile", "doer_threads"]);
|
|
92
108
|
const seenNames = new Set();
|
|
93
109
|
for (const server of servers) {
|
|
94
110
|
const serverName = server.name.trim();
|
|
@@ -29,6 +29,7 @@ function buildNotesAiPrompt(request) {
|
|
|
29
29
|
"You are editing a Markdown note inside Doer.",
|
|
30
30
|
"Return only Markdown content. Do not include explanations, preambles, or code fences unless the requested content itself needs them.",
|
|
31
31
|
"If a selection is provided, return only the replacement for that selection. If no selection is provided, return content to insert at the cursor.",
|
|
32
|
+
"When you generate an image, do not write attachment:image links. The generated image will be inserted into the note automatically.",
|
|
32
33
|
"",
|
|
33
34
|
`<instruction>\n${instruction}\n</instruction>`,
|
|
34
35
|
`<document>\n${document}\n</document>`,
|
|
@@ -64,6 +65,18 @@ function agentMessageDeltaFromParams(params) {
|
|
|
64
65
|
}
|
|
65
66
|
return stringValue(record.delta) || stringValue(record.text);
|
|
66
67
|
}
|
|
68
|
+
function generatedImageMarkdownFromParams(params, threadId) {
|
|
69
|
+
const record = recordValue(params);
|
|
70
|
+
const item = recordValue(record?.item);
|
|
71
|
+
if (!record || !item || stringValue(item.type) !== "imageGeneration") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
const imageId = stringValue(item.id);
|
|
75
|
+
if (!threadId || !imageId) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
return `\n\n\n\n`;
|
|
79
|
+
}
|
|
67
80
|
function terminalErrorFromParams(params) {
|
|
68
81
|
const record = recordValue(params);
|
|
69
82
|
const error = recordValue(record?.error);
|
|
@@ -71,7 +84,7 @@ function terminalErrorFromParams(params) {
|
|
|
71
84
|
stringValue(error?.message) ||
|
|
72
85
|
stringValue(record?.reason);
|
|
73
86
|
}
|
|
74
|
-
async function
|
|
87
|
+
async function archiveCompletedThread(args) {
|
|
75
88
|
try {
|
|
76
89
|
await args.manager.request("thread/archive", { threadId: args.threadId }, 30_000);
|
|
77
90
|
}
|
|
@@ -109,7 +122,7 @@ async function runNotesAiSession(args) {
|
|
|
109
122
|
callback();
|
|
110
123
|
};
|
|
111
124
|
args.abortController.signal.addEventListener("abort", () => {
|
|
112
|
-
settleCompleted(() => resolve());
|
|
125
|
+
settleCompleted(() => resolve("aborted"));
|
|
113
126
|
}, { once: true });
|
|
114
127
|
cleanupNotification = args.manager.onNotification((method, params) => {
|
|
115
128
|
const eventThreadId = threadIdFromParams(params);
|
|
@@ -134,6 +147,18 @@ async function runNotesAiSession(args) {
|
|
|
134
147
|
});
|
|
135
148
|
return;
|
|
136
149
|
}
|
|
150
|
+
if (method === "item/completed") {
|
|
151
|
+
const markdown = generatedImageMarkdownFromParams(params, threadId);
|
|
152
|
+
if (!markdown) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
publishEvent({
|
|
156
|
+
nc: args.nc,
|
|
157
|
+
subject: args.eventsSubject,
|
|
158
|
+
payload: { type: "delta", sessionId: args.sessionId, text: markdown },
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
137
162
|
if (!isTerminalTurnMethod(method)) {
|
|
138
163
|
return;
|
|
139
164
|
}
|
|
@@ -144,7 +169,7 @@ async function runNotesAiSession(args) {
|
|
|
144
169
|
subject: args.eventsSubject,
|
|
145
170
|
payload: { type: "done", sessionId: args.sessionId },
|
|
146
171
|
});
|
|
147
|
-
resolve();
|
|
172
|
+
resolve("completed");
|
|
148
173
|
});
|
|
149
174
|
return;
|
|
150
175
|
}
|
|
@@ -177,21 +202,19 @@ async function runNotesAiSession(args) {
|
|
|
177
202
|
if (args.abortController.signal.aborted) {
|
|
178
203
|
return;
|
|
179
204
|
}
|
|
180
|
-
await completed.finally(() => cleanupNotification());
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
settleCompleted(() => { });
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
if (threadId) {
|
|
188
|
-
await archiveThread({
|
|
205
|
+
const completion = await completed.finally(() => cleanupNotification());
|
|
206
|
+
if (completion === "completed") {
|
|
207
|
+
await archiveCompletedThread({
|
|
189
208
|
manager: args.manager,
|
|
190
209
|
threadId,
|
|
191
210
|
onError: args.onError,
|
|
192
211
|
});
|
|
193
212
|
}
|
|
194
213
|
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
settleCompleted(() => { });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
195
218
|
}
|
|
196
219
|
async function handleStart(args) {
|
|
197
220
|
const sessionId = stringValue(args.request.sessionId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildAgentSettingsEnvPatch, readAgentModelInstructions, resolveAgentModelInstructionsFilePath, } from "./agent-settings.js";
|
|
2
|
-
import { buildCustomMcpConfigArgs, buildDaemonMcpConfigArgs, buildMobileMcpConfigArgs } from "./agent-codex-cli.js";
|
|
2
|
+
import { buildCustomMcpConfigArgs, buildDaemonMcpConfigArgs, buildMobileMcpConfigArgs, buildThreadsMcpConfigArgs } from "./agent-codex-cli.js";
|
|
3
3
|
import { CodexAppServerClient } from "./codex-app-server-client.js";
|
|
4
4
|
function toTomlStringLiteral(value) {
|
|
5
5
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
@@ -32,6 +32,14 @@ async function buildCodexAppServerArgs(args) {
|
|
|
32
32
|
agentProjectDir: args.agentProjectDir,
|
|
33
33
|
workspaceRoot: args.workspaceRoot,
|
|
34
34
|
}),
|
|
35
|
+
...buildThreadsMcpConfigArgs({
|
|
36
|
+
agentId: args.agentId,
|
|
37
|
+
agentProjectDir: args.agentProjectDir,
|
|
38
|
+
agentToken: args.agentToken,
|
|
39
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
40
|
+
userId: args.userId,
|
|
41
|
+
workspaceRoot: args.workspaceRoot,
|
|
42
|
+
}),
|
|
35
43
|
...buildMobileMcpConfigArgs({
|
|
36
44
|
agentId: args.agentId,
|
|
37
45
|
agentProjectDir: args.agentProjectDir,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import * as z from "zod/v4";
|
|
5
|
+
function parseWorkspaceRoot(argv) {
|
|
6
|
+
const flagIndex = argv.findIndex((token) => token === "--workspace-root");
|
|
7
|
+
const flagValue = flagIndex >= 0 ? argv[flagIndex + 1] : "";
|
|
8
|
+
const envValue = process.env.DOER_THREADS_WORKSPACE_ROOT?.trim() || process.env.WORKSPACE?.trim() || process.cwd();
|
|
9
|
+
return path.resolve((flagValue || envValue || process.cwd()).trim());
|
|
10
|
+
}
|
|
11
|
+
function formatJson(value) {
|
|
12
|
+
return JSON.stringify(value, null, 2);
|
|
13
|
+
}
|
|
14
|
+
function optionalEnv(name) {
|
|
15
|
+
return process.env[name]?.trim() || "";
|
|
16
|
+
}
|
|
17
|
+
function getThreadApiConfig() {
|
|
18
|
+
const agentId = optionalEnv("DOER_THREADS_AGENT_ID");
|
|
19
|
+
const agentToken = optionalEnv("DOER_AGENT_TOKEN");
|
|
20
|
+
const serverBaseUrl = optionalEnv("DOER_THREADS_SERVER_BASE_URL").replace(/\/$/, "");
|
|
21
|
+
const userId = optionalEnv("DOER_THREADS_USER_ID");
|
|
22
|
+
const missing = [
|
|
23
|
+
agentId ? null : "DOER_THREADS_AGENT_ID",
|
|
24
|
+
agentToken ? null : "DOER_AGENT_TOKEN",
|
|
25
|
+
serverBaseUrl ? null : "DOER_THREADS_SERVER_BASE_URL",
|
|
26
|
+
userId ? null : "DOER_THREADS_USER_ID",
|
|
27
|
+
].filter((item) => Boolean(item));
|
|
28
|
+
if (missing.length > 0) {
|
|
29
|
+
throw new Error(`thread tools are unavailable; missing ${missing.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
return { agentId, agentToken, serverBaseUrl, userId };
|
|
32
|
+
}
|
|
33
|
+
function codexThreadPath(config, method) {
|
|
34
|
+
return `/api/users/${encodeURIComponent(config.userId)}/agents/${encodeURIComponent(config.agentId)}/codex/${method}`;
|
|
35
|
+
}
|
|
36
|
+
async function postDoerJson(pathValue, body, timeoutMs) {
|
|
37
|
+
const config = getThreadApiConfig();
|
|
38
|
+
const response = await fetch(`${config.serverBaseUrl}${pathValue}`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${config.agentToken}`,
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
...(timeoutMs ? { "x-doer-rpc-timeout-ms": String(timeoutMs) } : {}),
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
const data = await response.json().catch(() => ({}));
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(typeof data.error === "string" ? data.error : `Doer server returned ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
function jsonToolResult(result) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: formatJson(result) }],
|
|
57
|
+
structuredContent: result && typeof result === "object" && !Array.isArray(result)
|
|
58
|
+
? result
|
|
59
|
+
: { result },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function threadListParams(args) {
|
|
63
|
+
const limit = Number.isFinite(args.limit) && args.limit
|
|
64
|
+
? Math.min(100, Math.max(1, Math.trunc(args.limit)))
|
|
65
|
+
: 50;
|
|
66
|
+
return {
|
|
67
|
+
cursor: args.cursor?.trim() || null,
|
|
68
|
+
limit,
|
|
69
|
+
sortKey: "updated_at",
|
|
70
|
+
sortDirection: "desc",
|
|
71
|
+
sourceKinds: [
|
|
72
|
+
"cli",
|
|
73
|
+
"vscode",
|
|
74
|
+
"exec",
|
|
75
|
+
"appServer",
|
|
76
|
+
"subAgent",
|
|
77
|
+
"subAgentReview",
|
|
78
|
+
"subAgentCompact",
|
|
79
|
+
"subAgentThreadSpawn",
|
|
80
|
+
"subAgentOther",
|
|
81
|
+
"unknown",
|
|
82
|
+
],
|
|
83
|
+
archived: args.archived ?? false,
|
|
84
|
+
searchTerm: args.searchTerm?.trim() || null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function archiveThread(threadId) {
|
|
88
|
+
const config = getThreadApiConfig();
|
|
89
|
+
return postDoerJson(codexThreadPath(config, "thread/archive"), { threadId: threadId.trim() }, 180_000);
|
|
90
|
+
}
|
|
91
|
+
async function main() {
|
|
92
|
+
parseWorkspaceRoot(process.argv.slice(2));
|
|
93
|
+
const server = new McpServer({
|
|
94
|
+
name: "doer-threads",
|
|
95
|
+
version: "0.1.0",
|
|
96
|
+
}, {
|
|
97
|
+
capabilities: {
|
|
98
|
+
tools: {},
|
|
99
|
+
},
|
|
100
|
+
instructions: "Start, list, read, close, and archive Codex threads for the current Doer agent.",
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("threads_list", {
|
|
103
|
+
description: "List Codex threads for this Doer agent using the same API as the Doer thread list.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
archived: z.boolean().optional().describe("Whether to list archived threads. Defaults to false."),
|
|
106
|
+
cursor: z.string().optional().describe("Optional pagination cursor returned by the thread list API."),
|
|
107
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of threads to return. Defaults to 50."),
|
|
108
|
+
searchTerm: z.string().optional().describe("Optional text search term."),
|
|
109
|
+
},
|
|
110
|
+
}, async ({ archived, cursor, limit, searchTerm }) => {
|
|
111
|
+
const config = getThreadApiConfig();
|
|
112
|
+
const result = await postDoerJson(codexThreadPath(config, "thread/list"), threadListParams({ archived, cursor, limit, searchTerm }), 180_000);
|
|
113
|
+
return jsonToolResult(result);
|
|
114
|
+
});
|
|
115
|
+
server.registerTool("threads_start", {
|
|
116
|
+
description: "Create a new Codex thread for this Doer agent and start its first turn.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
prompt: z.string().min(1).describe("User prompt for the first turn in the new thread."),
|
|
119
|
+
},
|
|
120
|
+
}, async ({ prompt }) => {
|
|
121
|
+
const config = getThreadApiConfig();
|
|
122
|
+
const result = await postDoerJson(codexThreadPath(config, "thread/send"), { prompt }, 180_000);
|
|
123
|
+
return jsonToolResult(result);
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("threads_read", {
|
|
126
|
+
description: "Read a Codex thread and its recent turn contents for this Doer agent.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
threadId: z.string().min(1).describe("Codex thread id to read."),
|
|
129
|
+
cursor: z.string().optional().describe("Optional pagination cursor for turns."),
|
|
130
|
+
limit: z.number().int().min(1).max(100).optional().describe("Maximum number of turns to return. Defaults to 50."),
|
|
131
|
+
},
|
|
132
|
+
}, async ({ threadId, cursor, limit }) => {
|
|
133
|
+
const config = getThreadApiConfig();
|
|
134
|
+
const normalizedThreadId = threadId.trim();
|
|
135
|
+
const turnsLimit = Number.isFinite(limit) && limit ? Math.min(100, Math.max(1, Math.trunc(limit))) : 50;
|
|
136
|
+
const [thread, turns] = await Promise.all([
|
|
137
|
+
postDoerJson(codexThreadPath(config, "thread/read"), { threadId: normalizedThreadId }, 180_000),
|
|
138
|
+
postDoerJson(codexThreadPath(config, "thread/turns/list"), {
|
|
139
|
+
threadId: normalizedThreadId,
|
|
140
|
+
cursor: cursor?.trim() || null,
|
|
141
|
+
limit: turnsLimit,
|
|
142
|
+
sortDirection: "desc",
|
|
143
|
+
}, 180_000),
|
|
144
|
+
]);
|
|
145
|
+
return jsonToolResult({ thread, turns });
|
|
146
|
+
});
|
|
147
|
+
server.registerTool("threads_close", {
|
|
148
|
+
description: "Close a Codex thread for this Doer agent by archiving it with the same API as Doer thread delete.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
threadId: z.string().min(1).describe("Codex thread id to close."),
|
|
151
|
+
},
|
|
152
|
+
}, async ({ threadId }) => {
|
|
153
|
+
return jsonToolResult(await archiveThread(threadId) ?? { ok: true });
|
|
154
|
+
});
|
|
155
|
+
server.registerTool("threads_archive", {
|
|
156
|
+
description: "Archive a Codex thread for this Doer agent using the same API as Doer thread delete.",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
threadId: z.string().min(1).describe("Codex thread id to archive."),
|
|
159
|
+
},
|
|
160
|
+
}, async ({ threadId }) => {
|
|
161
|
+
return jsonToolResult(await archiveThread(threadId) ?? { ok: true });
|
|
162
|
+
});
|
|
163
|
+
const transport = new StdioServerTransport();
|
|
164
|
+
await server.connect(transport);
|
|
165
|
+
}
|
|
166
|
+
main().catch((error) => {
|
|
167
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
168
|
+
process.stderr.write(`${message}\n`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|