doer-agent 0.7.6 → 0.7.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/dist/agent-bundled-skills.js +3 -6
- package/dist/agent-http-proxy-rpc.js +168 -24
- package/dist/agent-notes-local.js +244 -0
- package/dist/agent-notes-rpc.js +106 -0
- package/dist/agent-runtime-utils.js +3 -0
- package/dist/agent.js +34 -1
- package/dist/daemon-mcp-server.js +125 -0
- package/package.json +2 -1
- package/runtime/skills/doer-agent-notes/SKILL.md +73 -0
|
@@ -46,8 +46,7 @@ async function removeLegacyManagedSkill(args) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
export async function ensureBundledDoerSkills(args) {
|
|
49
|
-
|
|
50
|
-
if (!sourceRootExists) {
|
|
49
|
+
if (!(await pathExists(args.bundledSkillsRoot))) {
|
|
51
50
|
return;
|
|
52
51
|
}
|
|
53
52
|
const entries = await readdir(args.bundledSkillsRoot, { withFileTypes: true });
|
|
@@ -55,8 +54,7 @@ export async function ensureBundledDoerSkills(args) {
|
|
|
55
54
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
56
55
|
continue;
|
|
57
56
|
}
|
|
58
|
-
const
|
|
59
|
-
const sourceSkillFile = path.join(sourceSkillDir, "SKILL.md");
|
|
57
|
+
const sourceSkillFile = path.join(args.bundledSkillsRoot, entry.name, "SKILL.md");
|
|
60
58
|
const targetSkillDir = path.join(args.codexHome, "skills", entry.name);
|
|
61
59
|
const targetSkillFile = path.join(targetSkillDir, "SKILL.md");
|
|
62
60
|
try {
|
|
@@ -65,9 +63,8 @@ export async function ensureBundledDoerSkills(args) {
|
|
|
65
63
|
args.onInfo?.(`bundled skill skipped name=${entry.name} reason=user-managed-target`);
|
|
66
64
|
continue;
|
|
67
65
|
}
|
|
68
|
-
const managedSource = withManagedMarker(source);
|
|
69
66
|
await mkdir(targetSkillDir, { recursive: true });
|
|
70
|
-
await writeFile(targetSkillFile,
|
|
67
|
+
await writeFile(targetSkillFile, withManagedMarker(source), "utf8");
|
|
71
68
|
await removeLegacyManagedSkill({ codexHome: args.codexHome, skillName: entry.name, onInfo: args.onInfo });
|
|
72
69
|
args.onInfo?.(`bundled skill synced name=${entry.name} path=.codex/skills/${entry.name}/SKILL.md`);
|
|
73
70
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
1
|
+
import { appendFile, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { StringCodec } from "nats";
|
|
4
4
|
const proxyRpcCodec = StringCodec();
|
|
5
5
|
const PROXY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
|
|
6
6
|
const MAX_PROXY_BODY_BYTES = 5 * 1024 * 1024;
|
|
7
|
+
const MAX_PROXY_LOGS = 100;
|
|
7
8
|
function getProxyRegistryPath(workspaceRoot) {
|
|
8
9
|
return path.join(workspaceRoot, ".doer-agent", "http-proxies.json");
|
|
9
10
|
}
|
|
11
|
+
function getProxyLogsPath(workspaceRoot, proxyId) {
|
|
12
|
+
return path.join(workspaceRoot, ".doer-agent", "http-proxy-logs", `${proxyId}.jsonl`);
|
|
13
|
+
}
|
|
10
14
|
function slugify(value) {
|
|
11
15
|
const slug = value
|
|
12
16
|
.trim()
|
|
@@ -91,10 +95,39 @@ function normalizeProxyRecord(value) {
|
|
|
91
95
|
name: normalizeName(row.name),
|
|
92
96
|
host,
|
|
93
97
|
port,
|
|
98
|
+
enabled: row.enabled !== false,
|
|
94
99
|
createdAt,
|
|
95
100
|
updatedAt,
|
|
96
101
|
};
|
|
97
102
|
}
|
|
103
|
+
function normalizeProxyLogRecord(value) {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const row = value;
|
|
108
|
+
const id = typeof row.id === "string" && row.id.trim() ? row.id.trim() : "";
|
|
109
|
+
const at = typeof row.at === "string" && row.at.trim() ? row.at.trim() : "";
|
|
110
|
+
const method = typeof row.method === "string" && row.method.trim() ? row.method.trim().toUpperCase() : "";
|
|
111
|
+
const requestPath = typeof row.path === "string" && row.path.trim() ? row.path.trim() : "/";
|
|
112
|
+
const status = typeof row.status === "number" && Number.isInteger(row.status) ? row.status : null;
|
|
113
|
+
const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) ? Math.max(0, Math.round(row.durationMs)) : 0;
|
|
114
|
+
const requestBytes = typeof row.requestBytes === "number" && Number.isFinite(row.requestBytes) ? Math.max(0, Math.round(row.requestBytes)) : 0;
|
|
115
|
+
const responseBytes = typeof row.responseBytes === "number" && Number.isFinite(row.responseBytes) ? Math.max(0, Math.round(row.responseBytes)) : 0;
|
|
116
|
+
if (!id || !at || !method) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
at,
|
|
122
|
+
method,
|
|
123
|
+
path: requestPath,
|
|
124
|
+
status,
|
|
125
|
+
durationMs,
|
|
126
|
+
requestBytes,
|
|
127
|
+
responseBytes,
|
|
128
|
+
error: typeof row.error === "string" && row.error.trim() ? row.error.trim().slice(0, 500) : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
98
131
|
async function readProxyRegistry(workspaceRoot) {
|
|
99
132
|
const raw = await readFile(getProxyRegistryPath(workspaceRoot), "utf8").catch(() => "");
|
|
100
133
|
if (!raw) {
|
|
@@ -118,6 +151,33 @@ async function writeProxyRegistry(workspaceRoot, proxies) {
|
|
|
118
151
|
await mkdir(path.dirname(registryPath), { recursive: true });
|
|
119
152
|
await writeFile(registryPath, `${JSON.stringify({ proxies }, null, 2)}\n`, "utf8");
|
|
120
153
|
}
|
|
154
|
+
function normalizeLimit(value, fallback) {
|
|
155
|
+
const numeric = Number(value);
|
|
156
|
+
if (!Number.isFinite(numeric)) {
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
return Math.max(1, Math.min(Math.floor(numeric), 1000));
|
|
160
|
+
}
|
|
161
|
+
async function readProxyLogs(workspaceRoot, proxyId, limit) {
|
|
162
|
+
const raw = await readFile(getProxyLogsPath(workspaceRoot, proxyId), "utf8").catch(() => "");
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return raw
|
|
167
|
+
.split("\n")
|
|
168
|
+
.map((line) => line.trim())
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.slice(-limit)
|
|
171
|
+
.map((line) => {
|
|
172
|
+
try {
|
|
173
|
+
return normalizeProxyLogRecord(JSON.parse(line));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.filter((log) => Boolean(log));
|
|
180
|
+
}
|
|
121
181
|
async function createProxy(workspaceRoot, request) {
|
|
122
182
|
const proxies = await readProxyRegistry(workspaceRoot);
|
|
123
183
|
const name = normalizeName(request.name);
|
|
@@ -136,54 +196,132 @@ async function createProxy(workspaceRoot, request) {
|
|
|
136
196
|
name,
|
|
137
197
|
host,
|
|
138
198
|
port,
|
|
199
|
+
enabled: request.enabled !== false,
|
|
139
200
|
createdAt: now,
|
|
140
201
|
updatedAt: now,
|
|
141
202
|
};
|
|
142
203
|
await writeProxyRegistry(workspaceRoot, [...proxies, proxy].sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
|
|
143
204
|
return proxy;
|
|
144
205
|
}
|
|
206
|
+
async function updateProxy(workspaceRoot, request) {
|
|
207
|
+
const proxyId = normalizeProxyId(request.proxyId);
|
|
208
|
+
const proxies = await readProxyRegistry(workspaceRoot);
|
|
209
|
+
const proxy = proxies.find((item) => item.id === proxyId);
|
|
210
|
+
if (!proxy) {
|
|
211
|
+
throw new Error("proxy not found");
|
|
212
|
+
}
|
|
213
|
+
const updated = {
|
|
214
|
+
...proxy,
|
|
215
|
+
name: request.name === undefined ? proxy.name : normalizeName(request.name),
|
|
216
|
+
host: request.host === undefined ? proxy.host : normalizeHost(request.host),
|
|
217
|
+
port: request.port === undefined ? proxy.port : normalizePort(request.port),
|
|
218
|
+
enabled: request.enabled === undefined ? proxy.enabled : request.enabled !== false,
|
|
219
|
+
updatedAt: new Date().toISOString(),
|
|
220
|
+
};
|
|
221
|
+
await writeProxyRegistry(workspaceRoot, proxies.map((item) => (item.id === proxyId ? updated : item)));
|
|
222
|
+
return updated;
|
|
223
|
+
}
|
|
145
224
|
async function deleteProxy(workspaceRoot, proxyId) {
|
|
146
225
|
const proxies = await readProxyRegistry(workspaceRoot);
|
|
147
226
|
await writeProxyRegistry(workspaceRoot, proxies.filter((proxy) => proxy.id !== proxyId));
|
|
227
|
+
await unlink(getProxyLogsPath(workspaceRoot, proxyId)).catch(() => undefined);
|
|
148
228
|
}
|
|
149
|
-
async function
|
|
229
|
+
async function appendProxyLog(workspaceRoot, proxyId, log) {
|
|
230
|
+
const entry = {
|
|
231
|
+
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
232
|
+
at: new Date().toISOString(),
|
|
233
|
+
...log,
|
|
234
|
+
};
|
|
235
|
+
const logsPath = getProxyLogsPath(workspaceRoot, proxyId);
|
|
236
|
+
await mkdir(path.dirname(logsPath), { recursive: true });
|
|
237
|
+
await appendFile(logsPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
238
|
+
}
|
|
239
|
+
async function readProxyLogsForRequest(workspaceRoot, request) {
|
|
150
240
|
const proxyId = normalizeProxyId(request.proxyId);
|
|
151
241
|
const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
|
|
152
242
|
if (!proxy) {
|
|
153
243
|
throw new Error("proxy not found");
|
|
154
244
|
}
|
|
245
|
+
return {
|
|
246
|
+
proxy,
|
|
247
|
+
events: await readProxyLogs(workspaceRoot, proxyId, normalizeLimit(request.limit, MAX_PROXY_LOGS)),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function handleProxyFetch(workspaceRoot, request) {
|
|
251
|
+
const proxyId = normalizeProxyId(request.proxyId);
|
|
252
|
+
const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
|
|
253
|
+
const startedAt = Date.now();
|
|
155
254
|
const method = normalizeMethod(request.method);
|
|
156
255
|
const requestPath = normalizePath(request.path);
|
|
157
|
-
const headers = normalizeHeaders(request.headers);
|
|
158
256
|
const bodyBase64 = typeof request.bodyBase64 === "string" ? request.bodyBase64 : "";
|
|
159
257
|
const body = bodyBase64 ? Buffer.from(bodyBase64, "base64") : undefined;
|
|
258
|
+
const requestBytes = body?.byteLength ?? 0;
|
|
259
|
+
let status = null;
|
|
260
|
+
let responseBytes = 0;
|
|
261
|
+
let errorMessage = null;
|
|
262
|
+
const finishLog = async () => {
|
|
263
|
+
await appendProxyLog(workspaceRoot, proxyId, {
|
|
264
|
+
method,
|
|
265
|
+
path: requestPath,
|
|
266
|
+
status,
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
requestBytes,
|
|
269
|
+
responseBytes,
|
|
270
|
+
error: errorMessage,
|
|
271
|
+
}).catch(() => undefined);
|
|
272
|
+
};
|
|
273
|
+
if (!proxy) {
|
|
274
|
+
throw new Error("proxy not found");
|
|
275
|
+
}
|
|
276
|
+
if (!proxy.enabled) {
|
|
277
|
+
errorMessage = "proxy disabled";
|
|
278
|
+
await finishLog();
|
|
279
|
+
throw new Error("proxy disabled");
|
|
280
|
+
}
|
|
281
|
+
const headers = normalizeHeaders(request.headers);
|
|
160
282
|
if ((body?.byteLength ?? 0) > MAX_PROXY_BODY_BYTES) {
|
|
283
|
+
errorMessage = "proxy request body too large";
|
|
284
|
+
await finishLog();
|
|
161
285
|
throw new Error("proxy request body too large");
|
|
162
286
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
287
|
+
try {
|
|
288
|
+
const url = new URL(requestPath, `http://${proxy.host}:${proxy.port}`);
|
|
289
|
+
const response = await fetch(url, {
|
|
290
|
+
method,
|
|
291
|
+
headers,
|
|
292
|
+
body: method === "GET" || method === "HEAD" ? undefined : body,
|
|
293
|
+
redirect: "manual",
|
|
294
|
+
});
|
|
295
|
+
status = response.status;
|
|
296
|
+
const responseBuffer = Buffer.from(await response.arrayBuffer());
|
|
297
|
+
responseBytes = responseBuffer.byteLength;
|
|
298
|
+
if (responseBuffer.byteLength > MAX_PROXY_BODY_BYTES) {
|
|
299
|
+
errorMessage = "proxy response body too large";
|
|
300
|
+
await finishLog();
|
|
301
|
+
throw new Error("proxy response body too large");
|
|
302
|
+
}
|
|
303
|
+
const responseHeaders = {};
|
|
304
|
+
response.headers.forEach((value, key) => {
|
|
305
|
+
responseHeaders[key] = value;
|
|
306
|
+
});
|
|
307
|
+
await finishLog();
|
|
308
|
+
return {
|
|
309
|
+
status: response.status,
|
|
310
|
+
statusText: response.statusText,
|
|
311
|
+
headers: responseHeaders,
|
|
312
|
+
bodyBase64: responseBuffer.toString("base64"),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (!errorMessage) {
|
|
317
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
318
|
+
await finishLog();
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
173
321
|
}
|
|
174
|
-
const responseHeaders = {};
|
|
175
|
-
response.headers.forEach((value, key) => {
|
|
176
|
-
responseHeaders[key] = value;
|
|
177
|
-
});
|
|
178
|
-
return {
|
|
179
|
-
status: response.status,
|
|
180
|
-
statusText: response.statusText,
|
|
181
|
-
headers: responseHeaders,
|
|
182
|
-
bodyBase64: responseBuffer.toString("base64"),
|
|
183
|
-
};
|
|
184
322
|
}
|
|
185
323
|
async function executeProxyRpc(args) {
|
|
186
|
-
const action = args.request.action === "create" || args.request.action === "delete" || args.request.action === "handle"
|
|
324
|
+
const action = args.request.action === "create" || args.request.action === "update" || args.request.action === "delete" || args.request.action === "logs" || args.request.action === "handle"
|
|
187
325
|
? args.request.action
|
|
188
326
|
: "list";
|
|
189
327
|
if (action === "list") {
|
|
@@ -192,10 +330,16 @@ async function executeProxyRpc(args) {
|
|
|
192
330
|
if (action === "create") {
|
|
193
331
|
return { ok: true, action, proxy: await createProxy(args.workspaceRoot, args.request) };
|
|
194
332
|
}
|
|
333
|
+
if (action === "update") {
|
|
334
|
+
return { ok: true, action, proxy: await updateProxy(args.workspaceRoot, args.request) };
|
|
335
|
+
}
|
|
195
336
|
if (action === "delete") {
|
|
196
337
|
await deleteProxy(args.workspaceRoot, normalizeProxyId(args.request.proxyId));
|
|
197
338
|
return { ok: true, action };
|
|
198
339
|
}
|
|
340
|
+
if (action === "logs") {
|
|
341
|
+
return { ok: true, action, ...await readProxyLogsForRequest(args.workspaceRoot, args.request) };
|
|
342
|
+
}
|
|
199
343
|
return { ok: true, action, response: await handleProxyFetch(args.workspaceRoot, args.request) };
|
|
200
344
|
}
|
|
201
345
|
export async function handleHttpProxyRpcMessage(args) {
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { formatPatch, structuredPatch } from "diff";
|
|
5
|
+
const AGENT_NOTES_ROOT = ".doer-agent/notes";
|
|
6
|
+
const PATCHES_ROOT = `${AGENT_NOTES_ROOT}/patches`;
|
|
7
|
+
function nowIso() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function createTimeBasedPatchId(createdAt) {
|
|
11
|
+
const stamp = createdAt.replace(/[-:.]/g, "").replace("T", "t").replace("Z", "z");
|
|
12
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
13
|
+
return `${stamp}-${random}`;
|
|
14
|
+
}
|
|
15
|
+
function patchRelPath(id, createdAt) {
|
|
16
|
+
const date = new Date(createdAt);
|
|
17
|
+
const year = String(date.getUTCFullYear());
|
|
18
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
19
|
+
return path.posix.join(PATCHES_ROOT, year, month, `${id}.patch`);
|
|
20
|
+
}
|
|
21
|
+
export function sanitizeNoteId(value) {
|
|
22
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
23
|
+
const baseName = path.posix.basename(trimmed);
|
|
24
|
+
const withoutSlashes = baseName.replace(/[\\/]/g, "");
|
|
25
|
+
if (!withoutSlashes || withoutSlashes === "." || withoutSlashes === "..") {
|
|
26
|
+
throw new Error("note filename is required");
|
|
27
|
+
}
|
|
28
|
+
return withoutSlashes.endsWith(".md") ? withoutSlashes : `${withoutSlashes}.md`;
|
|
29
|
+
}
|
|
30
|
+
function workspacePath(workspaceRoot, relPath) {
|
|
31
|
+
const root = path.resolve(workspaceRoot);
|
|
32
|
+
const abs = path.resolve(root, relPath.replace(/^\/+/, ""));
|
|
33
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
34
|
+
throw new Error("path escapes workspace root");
|
|
35
|
+
}
|
|
36
|
+
return abs;
|
|
37
|
+
}
|
|
38
|
+
function noteRelPath(noteId) {
|
|
39
|
+
return path.posix.join(AGENT_NOTES_ROOT, sanitizeNoteId(noteId));
|
|
40
|
+
}
|
|
41
|
+
function sha256(text) {
|
|
42
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
async function readTextIfExists(abs) {
|
|
45
|
+
try {
|
|
46
|
+
return await readFile(abs, "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function metadata(args) {
|
|
56
|
+
const rows = [
|
|
57
|
+
`# doer-note-patch-id: ${args.patchId}`,
|
|
58
|
+
`# created-at: ${args.createdAt}`,
|
|
59
|
+
`# operation: ${args.operation}`,
|
|
60
|
+
`# note-id: ${args.noteId}`,
|
|
61
|
+
`# note-path: ${noteRelPath(args.noteId)}`,
|
|
62
|
+
];
|
|
63
|
+
if (args.nextNoteId) {
|
|
64
|
+
rows.push(`# next-note-id: ${args.nextNoteId}`);
|
|
65
|
+
rows.push(`# next-note-path: ${noteRelPath(args.nextNoteId)}`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof args.oldText === "string" && typeof args.nextText === "string") {
|
|
68
|
+
rows.push(`# base-sha256: ${sha256(args.oldText)}`);
|
|
69
|
+
rows.push(`# next-sha256: ${sha256(args.nextText)}`);
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
function changedPatch(args) {
|
|
74
|
+
const patch = structuredPatch(`a/${args.noteId}`, `b/${args.noteId}`, args.oldText, args.nextText, undefined, undefined, { context: 3 });
|
|
75
|
+
patch.isGit = true;
|
|
76
|
+
return `${metadata(args).join("\n")}\n${formatPatch(patch)}`;
|
|
77
|
+
}
|
|
78
|
+
function renamePatch(args) {
|
|
79
|
+
const patch = {
|
|
80
|
+
oldFileName: `a/${args.oldNoteId}`,
|
|
81
|
+
newFileName: `b/${args.nextNoteId}`,
|
|
82
|
+
oldHeader: undefined,
|
|
83
|
+
newHeader: undefined,
|
|
84
|
+
hunks: [],
|
|
85
|
+
isGit: true,
|
|
86
|
+
isRename: true,
|
|
87
|
+
};
|
|
88
|
+
return `${metadata({
|
|
89
|
+
patchId: args.patchId,
|
|
90
|
+
createdAt: args.createdAt,
|
|
91
|
+
operation: "rename",
|
|
92
|
+
noteId: args.oldNoteId,
|
|
93
|
+
nextNoteId: args.nextNoteId,
|
|
94
|
+
}).join("\n")}\n${formatPatch(patch)}`;
|
|
95
|
+
}
|
|
96
|
+
async function writePatch(workspaceRoot, patchId, createdAt, text) {
|
|
97
|
+
const relPath = patchRelPath(patchId, createdAt);
|
|
98
|
+
const abs = workspacePath(workspaceRoot, relPath);
|
|
99
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
100
|
+
await writeFile(abs, text, "utf8");
|
|
101
|
+
return relPath;
|
|
102
|
+
}
|
|
103
|
+
export async function listAgentNotesLocal(workspaceRoot) {
|
|
104
|
+
const rootAbs = workspacePath(workspaceRoot, AGENT_NOTES_ROOT);
|
|
105
|
+
const rows = await readdir(rootAbs, { withFileTypes: true }).catch(() => []);
|
|
106
|
+
const notes = await Promise.all(rows
|
|
107
|
+
.filter((row) => row.isFile() && row.name.endsWith(".md"))
|
|
108
|
+
.map(async (row) => {
|
|
109
|
+
const abs = path.join(rootAbs, row.name);
|
|
110
|
+
const entry = await stat(abs);
|
|
111
|
+
const id = sanitizeNoteId(row.name);
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
path: noteRelPath(id),
|
|
115
|
+
name: row.name,
|
|
116
|
+
size: entry.size,
|
|
117
|
+
mtimeMs: entry.mtimeMs,
|
|
118
|
+
};
|
|
119
|
+
}));
|
|
120
|
+
notes.sort((a, b) => {
|
|
121
|
+
return b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name);
|
|
122
|
+
});
|
|
123
|
+
return notes;
|
|
124
|
+
}
|
|
125
|
+
export async function getAgentNoteLocal(workspaceRoot, noteId) {
|
|
126
|
+
const id = sanitizeNoteId(noteId);
|
|
127
|
+
const content = await readTextIfExists(workspacePath(workspaceRoot, noteRelPath(id)));
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
path: noteRelPath(id),
|
|
131
|
+
name: id,
|
|
132
|
+
content,
|
|
133
|
+
totalSize: Buffer.byteLength(content, "utf8"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export async function saveAgentNoteLocal(args) {
|
|
137
|
+
const noteId = sanitizeNoteId(args.noteId);
|
|
138
|
+
const noteAbs = workspacePath(args.workspaceRoot, noteRelPath(noteId));
|
|
139
|
+
const oldText = await readTextIfExists(noteAbs);
|
|
140
|
+
const nextText = args.content.replace(/\r\n/g, "\n");
|
|
141
|
+
if (oldText === nextText) {
|
|
142
|
+
return {
|
|
143
|
+
note: await getAgentNoteLocal(args.workspaceRoot, noteId),
|
|
144
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
145
|
+
patchPath: null,
|
|
146
|
+
patchId: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const createdAt = nowIso();
|
|
150
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
151
|
+
const patchText = changedPatch({ noteId, oldText, nextText, patchId, createdAt, operation: "content" });
|
|
152
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, patchText);
|
|
153
|
+
await mkdir(path.dirname(noteAbs), { recursive: true });
|
|
154
|
+
await writeFile(noteAbs, nextText, "utf8");
|
|
155
|
+
return {
|
|
156
|
+
note: await getAgentNoteLocal(args.workspaceRoot, noteId),
|
|
157
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
158
|
+
patchPath,
|
|
159
|
+
patchId,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function createAgentNoteLocal(workspaceRoot, name) {
|
|
163
|
+
const createdAt = nowIso();
|
|
164
|
+
const noteId = sanitizeNoteId(name);
|
|
165
|
+
const noteAbs = workspacePath(workspaceRoot, noteRelPath(noteId));
|
|
166
|
+
try {
|
|
167
|
+
await stat(noteAbs);
|
|
168
|
+
throw new Error("A note with that filename already exists");
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
176
|
+
const patchText = changedPatch({ noteId, oldText: "", nextText: "", patchId, createdAt, operation: "create" });
|
|
177
|
+
const patchPath = await writePatch(workspaceRoot, patchId, createdAt, patchText);
|
|
178
|
+
await mkdir(path.dirname(noteAbs), { recursive: true });
|
|
179
|
+
await writeFile(noteAbs, "", "utf8");
|
|
180
|
+
return {
|
|
181
|
+
note: await getAgentNoteLocal(workspaceRoot, noteId),
|
|
182
|
+
notes: await listAgentNotesLocal(workspaceRoot),
|
|
183
|
+
patchPath,
|
|
184
|
+
patchId,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export async function renameAgentNoteLocal(args) {
|
|
188
|
+
const currentId = sanitizeNoteId(args.noteId);
|
|
189
|
+
const nextId = sanitizeNoteId(args.name);
|
|
190
|
+
if (currentId === nextId) {
|
|
191
|
+
return {
|
|
192
|
+
note: await getAgentNoteLocal(args.workspaceRoot, currentId),
|
|
193
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
194
|
+
patchPath: null,
|
|
195
|
+
patchId: null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const nextAbs = workspacePath(args.workspaceRoot, noteRelPath(nextId));
|
|
199
|
+
try {
|
|
200
|
+
await stat(nextAbs);
|
|
201
|
+
throw new Error("A note with that filename already exists");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const currentAbs = workspacePath(args.workspaceRoot, noteRelPath(currentId));
|
|
209
|
+
const createdAt = nowIso();
|
|
210
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
211
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, renamePatch({ oldNoteId: currentId, nextNoteId: nextId, patchId, createdAt }));
|
|
212
|
+
await mkdir(path.dirname(nextAbs), { recursive: true });
|
|
213
|
+
await rename(currentAbs, nextAbs);
|
|
214
|
+
return {
|
|
215
|
+
note: await getAgentNoteLocal(args.workspaceRoot, nextId),
|
|
216
|
+
notes: await listAgentNotesLocal(args.workspaceRoot),
|
|
217
|
+
patchPath,
|
|
218
|
+
patchId,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export async function deleteAgentNoteLocal(args) {
|
|
222
|
+
const noteId = sanitizeNoteId(args.noteId);
|
|
223
|
+
const noteAbs = workspacePath(args.workspaceRoot, noteRelPath(noteId));
|
|
224
|
+
const oldText = await readTextIfExists(noteAbs);
|
|
225
|
+
const createdAt = nowIso();
|
|
226
|
+
const patchId = createTimeBasedPatchId(createdAt);
|
|
227
|
+
const patchPath = await writePatch(args.workspaceRoot, patchId, createdAt, changedPatch({ noteId, oldText, nextText: "", patchId, createdAt, operation: "delete" }));
|
|
228
|
+
await rm(noteAbs, { force: true });
|
|
229
|
+
const notes = await listAgentNotesLocal(args.workspaceRoot);
|
|
230
|
+
return {
|
|
231
|
+
note: notes[0] ? await getAgentNoteLocal(args.workspaceRoot, notes[0].id) : null,
|
|
232
|
+
notes,
|
|
233
|
+
patchPath,
|
|
234
|
+
patchId,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export function agentNotesCapabilitiesLocal() {
|
|
238
|
+
return {
|
|
239
|
+
storage: "agent-workspace",
|
|
240
|
+
notesRoot: AGENT_NOTES_ROOT,
|
|
241
|
+
patchFormat: "git-diff",
|
|
242
|
+
patchesRoot: PATCHES_ROOT,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { agentNotesCapabilitiesLocal, createAgentNoteLocal, deleteAgentNoteLocal, getAgentNoteLocal, listAgentNotesLocal, renameAgentNoteLocal, saveAgentNoteLocal, } from "./agent-notes-local.js";
|
|
3
|
+
const notesRpcCodec = StringCodec();
|
|
4
|
+
function parseAction(value) {
|
|
5
|
+
if (value === "capabilities" ||
|
|
6
|
+
value === "list" ||
|
|
7
|
+
value === "get" ||
|
|
8
|
+
value === "create" ||
|
|
9
|
+
value === "save" ||
|
|
10
|
+
value === "rename" ||
|
|
11
|
+
value === "delete") {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
throw new Error("unsupported notes action");
|
|
15
|
+
}
|
|
16
|
+
function stringValue(value) {
|
|
17
|
+
return typeof value === "string" ? value : "";
|
|
18
|
+
}
|
|
19
|
+
async function executeNotesRpc(workspaceRoot, request) {
|
|
20
|
+
const action = parseAction(request.action);
|
|
21
|
+
if (action === "capabilities") {
|
|
22
|
+
return { ok: true, action, capabilities: agentNotesCapabilitiesLocal() };
|
|
23
|
+
}
|
|
24
|
+
if (action === "list") {
|
|
25
|
+
return { ok: true, action, notes: await listAgentNotesLocal(workspaceRoot) };
|
|
26
|
+
}
|
|
27
|
+
if (action === "get") {
|
|
28
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
29
|
+
const noteId = stringValue(request.noteId) || notes[0]?.id || "";
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
action,
|
|
33
|
+
note: noteId ? await getAgentNoteLocal(workspaceRoot, noteId) : null,
|
|
34
|
+
notes,
|
|
35
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (action === "create") {
|
|
39
|
+
const name = stringValue(request.name).trim();
|
|
40
|
+
if (!name) {
|
|
41
|
+
throw new Error("name is required");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
action,
|
|
46
|
+
...(await createAgentNoteLocal(workspaceRoot, name)),
|
|
47
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (action === "save") {
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
action,
|
|
54
|
+
...(await saveAgentNoteLocal({
|
|
55
|
+
workspaceRoot,
|
|
56
|
+
noteId: stringValue(request.noteId),
|
|
57
|
+
content: stringValue(request.content),
|
|
58
|
+
})),
|
|
59
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (action === "rename") {
|
|
63
|
+
const name = stringValue(request.name).trim();
|
|
64
|
+
if (!name) {
|
|
65
|
+
throw new Error("name is required");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
action,
|
|
70
|
+
...(await renameAgentNoteLocal({
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
noteId: stringValue(request.noteId),
|
|
73
|
+
name,
|
|
74
|
+
})),
|
|
75
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
action,
|
|
81
|
+
...(await deleteAgentNoteLocal({
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
noteId: stringValue(request.noteId),
|
|
84
|
+
})),
|
|
85
|
+
capabilities: agentNotesCapabilitiesLocal(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function handleNotesRpcMessage(args) {
|
|
89
|
+
let payload = {};
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(notesRpcCodec.decode(args.msg.data));
|
|
92
|
+
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
93
|
+
throw new Error("agent id mismatch");
|
|
94
|
+
}
|
|
95
|
+
args.msg.respond(notesRpcCodec.encode(JSON.stringify(await executeNotesRpc(args.workspaceRoot, payload))));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
99
|
+
args.msg.respond(notesRpcCodec.encode(JSON.stringify({
|
|
100
|
+
ok: false,
|
|
101
|
+
action: typeof payload.action === "string" ? payload.action : "",
|
|
102
|
+
error: message,
|
|
103
|
+
})));
|
|
104
|
+
args.onError(`notes rpc failed action=${typeof payload.action === "string" ? payload.action : "unknown"} error=${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -22,6 +22,9 @@ export function buildAgentSkillRpcSubject(userId, agentId) {
|
|
|
22
22
|
export function buildAgentFsRpcSubject(userId, agentId) {
|
|
23
23
|
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
24
24
|
}
|
|
25
|
+
export function buildAgentNotesRpcSubject(userId, agentId) {
|
|
26
|
+
return `doer.agent.notes.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
27
|
+
}
|
|
25
28
|
export function buildAgentDaemonRpcSubject(userId, agentId) {
|
|
26
29
|
return `doer.agent.daemon.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
27
30
|
}
|
package/dist/agent.js
CHANGED
|
@@ -5,6 +5,8 @@ import { StringCodec } from "nats";
|
|
|
5
5
|
import { buildAgentSettingsEnvPatch, readAgentSettingsConfig, } from "./agent-settings.js";
|
|
6
6
|
import { handleFsRpcMessage } from "./agent-fs-rpc.js";
|
|
7
7
|
import { handleGitRpcMessage } from "./agent-git-rpc.js";
|
|
8
|
+
import { handleNotesRpcMessage } from "./agent-notes-rpc.js";
|
|
9
|
+
import { ensureBundledDoerSkills } from "./agent-bundled-skills.js";
|
|
8
10
|
import { subscribeToCodexAppRpc } from "./agent-codex-app-rpc.js";
|
|
9
11
|
import { createCodexAppServerManager } from "./codex-app-server-manager.js";
|
|
10
12
|
import { subscribeToDaemonRpc } from "./agent-daemon-rpc.js";
|
|
@@ -15,7 +17,7 @@ import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
|
|
|
15
17
|
import { subscribeToMaintenanceRpc } from "./agent-maintenance-rpc.js";
|
|
16
18
|
import { subscribeToHttpProxyRpc } from "./agent-http-proxy-rpc.js";
|
|
17
19
|
import { sendSignalToTaskProcess } from "./agent-task-execution.js";
|
|
18
|
-
import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
|
|
20
|
+
import { buildAgentCodexAppEventsSubject, buildAgentCodexAppRpcSubject, buildAgentDaemonRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentHttpProxyRpcSubject, buildAgentMaintenanceRpcSubject, buildAgentNotesRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, } from "./agent-runtime-utils.js";
|
|
19
21
|
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
20
22
|
import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
|
|
21
23
|
import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
|
|
@@ -23,6 +25,7 @@ const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
|
|
|
23
25
|
const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
|
|
25
27
|
const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
|
|
28
|
+
const BUNDLED_SKILLS_ROOT = path.join(AGENT_PROJECT_DIR, "runtime", "skills");
|
|
26
29
|
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
27
30
|
const HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
28
31
|
const codexAppEventCodec = StringCodec();
|
|
@@ -108,6 +111,25 @@ function subscribeToFsRpc(args) {
|
|
|
108
111
|
});
|
|
109
112
|
writeAgentInfo(`fs rpc subscribed subject=${subject}`);
|
|
110
113
|
}
|
|
114
|
+
function subscribeToNotesRpc(args) {
|
|
115
|
+
const subject = buildAgentNotesRpcSubject(args.userId, args.agentId);
|
|
116
|
+
args.jetstream.nc.subscribe(subject, {
|
|
117
|
+
callback: (error, msg) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
writeAgentError(`notes rpc subscription error: ${message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
void handleNotesRpcMessage({
|
|
124
|
+
msg,
|
|
125
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
126
|
+
agentId: args.agentId,
|
|
127
|
+
onError: writeAgentError,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
writeAgentInfo(`notes rpc subscribed subject=${subject}`);
|
|
132
|
+
}
|
|
111
133
|
function formatCodexAppNotificationParams(params) {
|
|
112
134
|
if (params === undefined) {
|
|
113
135
|
return "undefined";
|
|
@@ -249,6 +271,12 @@ async function main() {
|
|
|
249
271
|
jetstream.nc.publish(buildAgentCodexAppEventsSubject(userId, initialAgentId), codexAppEventCodec.encode(JSON.stringify(event)));
|
|
250
272
|
},
|
|
251
273
|
});
|
|
274
|
+
void ensureBundledDoerSkills({
|
|
275
|
+
bundledSkillsRoot: BUNDLED_SKILLS_ROOT,
|
|
276
|
+
codexHome: runtimeEnvHelpers.resolveCodexHomePath(),
|
|
277
|
+
onInfo: writeAgentInfo,
|
|
278
|
+
onError: writeAgentError,
|
|
279
|
+
});
|
|
252
280
|
subscribeToFsRpc({
|
|
253
281
|
jetstream,
|
|
254
282
|
serverBaseUrl,
|
|
@@ -256,6 +284,11 @@ async function main() {
|
|
|
256
284
|
agentId: initialAgentId,
|
|
257
285
|
agentToken,
|
|
258
286
|
});
|
|
287
|
+
subscribeToNotesRpc({
|
|
288
|
+
jetstream,
|
|
289
|
+
userId,
|
|
290
|
+
agentId: initialAgentId,
|
|
291
|
+
});
|
|
259
292
|
subscribeToDaemonRpc({
|
|
260
293
|
nc: jetstream.nc,
|
|
261
294
|
subject: buildAgentDaemonRpcSubject(userId, initialAgentId),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6,6 +7,7 @@ import * as z from "zod/v4";
|
|
|
6
7
|
import { deleteAgentDaemonLocal, listAgentDaemonsLocal, readAgentDaemonLogsLocal, restartAgentDaemonLocal, startAgentDaemonLocal, stopAgentDaemonLocal, } from "./agent-daemon-rpc.js";
|
|
7
8
|
import { readAgentSettingsConfig } from "./agent-settings.js";
|
|
8
9
|
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
10
|
+
import { agentNotesCapabilitiesLocal, createAgentNoteLocal, deleteAgentNoteLocal, getAgentNoteLocal, listAgentNotesLocal, renameAgentNoteLocal, saveAgentNoteLocal, } from "./agent-notes-local.js";
|
|
9
11
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const AGENT_PROJECT_DIR = path.join(MODULE_DIR, "..");
|
|
11
13
|
function parseWorkspaceRoot(argv) {
|
|
@@ -17,6 +19,23 @@ function parseWorkspaceRoot(argv) {
|
|
|
17
19
|
function formatJson(value) {
|
|
18
20
|
return JSON.stringify(value, null, 2);
|
|
19
21
|
}
|
|
22
|
+
function runSearch(command, args, cwd) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
child.stdout.setEncoding("utf8");
|
|
28
|
+
child.stderr.setEncoding("utf8");
|
|
29
|
+
child.stdout.on("data", (chunk) => {
|
|
30
|
+
stdout += chunk;
|
|
31
|
+
});
|
|
32
|
+
child.stderr.on("data", (chunk) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
child.once("error", reject);
|
|
36
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
20
39
|
async function main() {
|
|
21
40
|
const workspaceRoot = parseWorkspaceRoot(process.argv.slice(2));
|
|
22
41
|
const runtimeEnvHelpers = createRuntimeEnvHelpers({
|
|
@@ -156,6 +175,112 @@ async function main() {
|
|
|
156
175
|
structuredContent: logs,
|
|
157
176
|
};
|
|
158
177
|
});
|
|
178
|
+
server.registerTool("notes_list", {
|
|
179
|
+
description: "List Doer note files in .doer-agent/notes.",
|
|
180
|
+
inputSchema: {},
|
|
181
|
+
}, async () => {
|
|
182
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: formatJson({ notes, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
185
|
+
structuredContent: { notes, capabilities: agentNotesCapabilitiesLocal() },
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
server.registerTool("notes_read", {
|
|
189
|
+
description: "Read a Doer note file from .doer-agent/notes.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
noteId: z.string().optional().describe("Note filename. Defaults to the first note when omitted."),
|
|
192
|
+
},
|
|
193
|
+
}, async ({ noteId }) => {
|
|
194
|
+
const notes = await listAgentNotesLocal(workspaceRoot);
|
|
195
|
+
const targetNoteId = noteId?.trim() || notes[0]?.id || "";
|
|
196
|
+
const note = targetNoteId ? await getAgentNoteLocal(workspaceRoot, targetNoteId) : null;
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: formatJson({ note, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
199
|
+
structuredContent: { note, capabilities: agentNotesCapabilitiesLocal() },
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
server.registerTool("notes_create", {
|
|
203
|
+
description: "Create an empty Doer note and record a patch entry.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
name: z.string().min(1).describe("Note filename. .md is appended if omitted."),
|
|
206
|
+
},
|
|
207
|
+
}, async ({ name }) => {
|
|
208
|
+
const result = await createAgentNoteLocal(workspaceRoot, name);
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
211
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
server.registerTool("notes_save", {
|
|
215
|
+
description: "Save a Doer note and record a git-diff patch entry for the content change.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
noteId: z.string().min(1).describe("Note filename."),
|
|
218
|
+
content: z.string().describe("Full note content to save."),
|
|
219
|
+
},
|
|
220
|
+
}, async ({ noteId, content }) => {
|
|
221
|
+
const result = await saveAgentNoteLocal({ workspaceRoot, noteId, content });
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
224
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
server.registerTool("notes_rename", {
|
|
228
|
+
description: "Rename a Doer note and record a git-diff rename patch entry.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
noteId: z.string().describe("Current note filename."),
|
|
231
|
+
name: z.string().min(1).describe("New filename. .md is appended if omitted."),
|
|
232
|
+
},
|
|
233
|
+
}, async ({ noteId, name }) => {
|
|
234
|
+
const result = await renameAgentNoteLocal({ workspaceRoot, noteId, name });
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
237
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
server.registerTool("notes_delete", {
|
|
241
|
+
description: "Delete a Doer note and record a git-diff deletion patch entry.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
noteId: z.string().describe("Note filename to delete."),
|
|
244
|
+
},
|
|
245
|
+
}, async ({ noteId }) => {
|
|
246
|
+
const result = await deleteAgentNoteLocal({ workspaceRoot, noteId });
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: formatJson({ ...result, capabilities: agentNotesCapabilitiesLocal() }) }],
|
|
249
|
+
structuredContent: { ...result, capabilities: agentNotesCapabilitiesLocal() },
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
server.registerTool("notes_search", {
|
|
253
|
+
description: "Search Doer notes with ripgrep under .doer-agent/notes. Patches are excluded by default.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
query: z.string().min(1).describe("Text or regex pattern to search for."),
|
|
256
|
+
includePatches: z.boolean().optional().describe("Search patch history too."),
|
|
257
|
+
fixedStrings: z.boolean().optional().describe("Treat query as a literal string."),
|
|
258
|
+
limit: z.number().int().min(1).max(200).optional().describe("Maximum matching lines to return."),
|
|
259
|
+
},
|
|
260
|
+
}, async ({ query, includePatches, fixedStrings, limit }) => {
|
|
261
|
+
const args = [
|
|
262
|
+
"--line-number",
|
|
263
|
+
"--column",
|
|
264
|
+
"--no-heading",
|
|
265
|
+
"--color=never",
|
|
266
|
+
fixedStrings === false ? null : "--fixed-strings",
|
|
267
|
+
"--glob",
|
|
268
|
+
"*.md",
|
|
269
|
+
includePatches ? "--glob" : null,
|
|
270
|
+
includePatches ? "*.patch" : null,
|
|
271
|
+
query,
|
|
272
|
+
".doer-agent/notes",
|
|
273
|
+
].filter((item) => Boolean(item));
|
|
274
|
+
const result = await runSearch("rg", args, workspaceRoot);
|
|
275
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
276
|
+
throw new Error(result.stderr || "notes search failed");
|
|
277
|
+
}
|
|
278
|
+
const lines = result.stdout.split("\n").filter(Boolean).slice(0, limit ?? 50);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: formatJson({ matches: lines, truncated: result.stdout.split("\n").filter(Boolean).length > lines.length }) }],
|
|
281
|
+
structuredContent: { matches: lines, truncated: result.stdout.split("\n").filter(Boolean).length > lines.length },
|
|
282
|
+
};
|
|
283
|
+
});
|
|
159
284
|
const transport = new StdioServerTransport();
|
|
160
285
|
await server.connect(transport);
|
|
161
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Reverse-polling agent runtime for doer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/agent.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"@openai/codex": "^0.133.0",
|
|
29
29
|
"@openai/codex-sdk": "^0.133.0",
|
|
30
|
+
"diff": "^9.0.0",
|
|
30
31
|
"nats": "^2.29.3",
|
|
31
32
|
"tar": "^7.5.15"
|
|
32
33
|
},
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doer-agent-notes
|
|
3
|
+
description: Use when working with Doer agent text notes, memo files, note patch history, or note CRUD/search tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Doer Agent Notes
|
|
7
|
+
|
|
8
|
+
Use the Doer notes tools for note CRUD whenever they are available. Do not edit `.doer-agent/notes/*.md` directly when a notes tool can perform the same operation, because note tools also record patch history.
|
|
9
|
+
|
|
10
|
+
## Storage
|
|
11
|
+
|
|
12
|
+
Current note files live directly under `.doer-agent/notes` as Markdown files.
|
|
13
|
+
|
|
14
|
+
- Notes: `.doer-agent/notes/{note-id}.md`
|
|
15
|
+
- There is no built-in default note filename. Users choose the filename when creating a note.
|
|
16
|
+
- `note-id` is the filename. If the extension is omitted, normalize it back to `.md`.
|
|
17
|
+
- Note content is free-form Markdown text.
|
|
18
|
+
- Titles, tags, and frontmatter are optional; do not require them for storage.
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
Prefer these tools when present:
|
|
23
|
+
|
|
24
|
+
- `notes_list`
|
|
25
|
+
- `notes_read`
|
|
26
|
+
- `notes_create`
|
|
27
|
+
- `notes_save`
|
|
28
|
+
- `notes_rename`
|
|
29
|
+
- `notes_delete`
|
|
30
|
+
- `notes_search`
|
|
31
|
+
|
|
32
|
+
## Patch History
|
|
33
|
+
|
|
34
|
+
Note change history is append-only and stored under `.doer-agent/notes/patches/YYYY/MM/*.patch`.
|
|
35
|
+
|
|
36
|
+
Patch files use git diff format with Doer metadata comment lines before the diff body.
|
|
37
|
+
|
|
38
|
+
For content changes, include:
|
|
39
|
+
|
|
40
|
+
- `# doer-note-patch-id`
|
|
41
|
+
- `# created-at`
|
|
42
|
+
- `# operation: content`
|
|
43
|
+
- `# note-id`
|
|
44
|
+
- `# note-path`
|
|
45
|
+
- `# base-sha256`
|
|
46
|
+
- `# next-sha256`
|
|
47
|
+
|
|
48
|
+
For filename changes, include:
|
|
49
|
+
|
|
50
|
+
- `# doer-note-patch-id`
|
|
51
|
+
- `# created-at`
|
|
52
|
+
- `# operation: rename`
|
|
53
|
+
- `# note-id`
|
|
54
|
+
- `# note-path`
|
|
55
|
+
- `# next-note-id`
|
|
56
|
+
- `# next-note-path`
|
|
57
|
+
|
|
58
|
+
For deletions, include:
|
|
59
|
+
|
|
60
|
+
- `# doer-note-patch-id`
|
|
61
|
+
- `# created-at`
|
|
62
|
+
- `# operation: delete`
|
|
63
|
+
- `# note-id`
|
|
64
|
+
- `# note-path`
|
|
65
|
+
- `# base-sha256`
|
|
66
|
+
- `# next-sha256`
|
|
67
|
+
|
|
68
|
+
## Principles
|
|
69
|
+
|
|
70
|
+
- The note source of truth is human-readable `.md` files.
|
|
71
|
+
- Change history is append-only `.patch` files.
|
|
72
|
+
- Search indexes, vector databases, and other derived stores are not source of truth.
|
|
73
|
+
- Derived data must be reproducible from `.md` and `.patch` files.
|