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.
@@ -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![generated image](.codex/generated_images/${threadId}/${imageId}.png)\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 archiveThread(args) {
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
- catch (error) {
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",