doer-agent 0.4.4 → 0.4.6
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-fs-rpc.js +113 -80
- package/package.json +3 -2
package/dist/agent-fs-rpc.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
|
+
import { create as createTar, extract as extractTar } from "tar";
|
|
5
6
|
const fsRpcCodec = StringCodec();
|
|
6
7
|
function normalizeFsRpcPath(workspaceRoot, rawPath) {
|
|
7
8
|
const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
@@ -23,10 +24,9 @@ function normalizeFsRpcPath(workspaceRoot, rawPath) {
|
|
|
23
24
|
function parseFsRpcAction(value) {
|
|
24
25
|
if (value === "list" ||
|
|
25
26
|
value === "stat" ||
|
|
26
|
-
value === "
|
|
27
|
+
value === "upload_file" ||
|
|
27
28
|
value === "read_text" ||
|
|
28
|
-
value === "
|
|
29
|
-
value === "write_file" ||
|
|
29
|
+
value === "write_text" ||
|
|
30
30
|
value === "download_file" ||
|
|
31
31
|
value === "delete_path" ||
|
|
32
32
|
value === "archive_dir" ||
|
|
@@ -88,33 +88,20 @@ function inferMimeType(filePath) {
|
|
|
88
88
|
}
|
|
89
89
|
return "application/octet-stream";
|
|
90
90
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
if (!normalized || normalized.includes("..")) {
|
|
94
|
-
throw new Error("invalid archive entry path");
|
|
95
|
-
}
|
|
96
|
-
return normalized;
|
|
91
|
+
function sha256Hex(bytes) {
|
|
92
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
97
93
|
}
|
|
98
|
-
async function
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
const bytes = await readFile(child);
|
|
111
|
-
files.push({
|
|
112
|
-
relPath: normalizeArchiveRelativePath(path.relative(rootDir, child)),
|
|
113
|
-
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
114
|
-
sizeBytes: bytes.byteLength,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return files;
|
|
94
|
+
async function createTarGzipBuffer(cwd, entries) {
|
|
95
|
+
const stream = createTar({
|
|
96
|
+
cwd,
|
|
97
|
+
gzip: true,
|
|
98
|
+
portable: true,
|
|
99
|
+
}, entries);
|
|
100
|
+
const chunks = [];
|
|
101
|
+
for await (const chunk of stream) {
|
|
102
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(chunks);
|
|
118
105
|
}
|
|
119
106
|
async function executeFsRpc(args) {
|
|
120
107
|
const action = parseFsRpcAction(args.request.action);
|
|
@@ -168,13 +155,18 @@ async function executeFsRpc(args) {
|
|
|
168
155
|
throw new Error("archivePath is required");
|
|
169
156
|
}
|
|
170
157
|
const archiveTarget = normalizeFsRpcPath(args.workspaceRoot, rawArchivePath);
|
|
171
|
-
|
|
172
|
-
|
|
158
|
+
try {
|
|
159
|
+
const manifestEntry = await stat(path.join(abs, "SKILL.md"));
|
|
160
|
+
if (!manifestEntry.isFile()) {
|
|
161
|
+
throw new Error("Selected skill directory must contain SKILL.md");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
173
165
|
throw new Error("Selected skill directory must contain SKILL.md");
|
|
174
166
|
}
|
|
175
|
-
const payload = gzipSync(Buffer.from(JSON.stringify({ files }), "utf8"));
|
|
176
167
|
await mkdir(path.dirname(archiveTarget.abs), { recursive: true });
|
|
177
|
-
await
|
|
168
|
+
const archiveBytes = await createTarGzipBuffer(abs, ["."]);
|
|
169
|
+
await writeFile(archiveTarget.abs, archiveBytes);
|
|
178
170
|
const archiveStat = await stat(archiveTarget.abs);
|
|
179
171
|
return {
|
|
180
172
|
ok: true,
|
|
@@ -184,27 +176,58 @@ async function executeFsRpc(args) {
|
|
|
184
176
|
size: archiveStat.size,
|
|
185
177
|
};
|
|
186
178
|
}
|
|
187
|
-
if (action === "
|
|
179
|
+
if (action === "upload_file") {
|
|
188
180
|
const entry = await stat(abs);
|
|
189
181
|
if (!entry.isFile()) {
|
|
190
182
|
throw new Error("path is not a file");
|
|
191
183
|
}
|
|
192
184
|
const uploadUrl = typeof args.request.uploadUrl === "string" ? args.request.uploadUrl : "";
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
const uploadMode = args.request.uploadMode === "multipart" ? "multipart" : "raw";
|
|
186
|
+
const uploadMethod = args.request.uploadMethod === "POST" ? "POST" : "PUT";
|
|
187
|
+
const uploadContentType = typeof args.request.uploadContentType === "string" && args.request.uploadContentType.trim()
|
|
188
|
+
? args.request.uploadContentType.trim()
|
|
189
|
+
: inferMimeType(abs);
|
|
190
|
+
const uploadFieldName = typeof args.request.uploadFieldName === "string" && args.request.uploadFieldName.trim()
|
|
191
|
+
? args.request.uploadFieldName.trim()
|
|
192
|
+
: "file";
|
|
193
|
+
if (!uploadUrl) {
|
|
194
|
+
throw new Error("uploadUrl is required");
|
|
196
195
|
}
|
|
197
196
|
const resolvedUploadUrl = new URL(uploadUrl, `${args.serverBaseUrl}/`).toString();
|
|
198
197
|
const data = await readFile(abs);
|
|
199
198
|
const fileName = path.basename(abs) || "file";
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
const serverOrigin = new URL(args.serverBaseUrl).origin;
|
|
200
|
+
const targetOrigin = new URL(resolvedUploadUrl).origin;
|
|
201
|
+
const headers = {};
|
|
202
|
+
if (targetOrigin === serverOrigin) {
|
|
203
|
+
headers.Authorization = `Bearer ${args.agentToken}`;
|
|
204
|
+
}
|
|
205
|
+
let response;
|
|
206
|
+
if (uploadMode === "multipart") {
|
|
207
|
+
const form = new FormData();
|
|
208
|
+
const formFields = args.request.formFields && typeof args.request.formFields === "object" && !Array.isArray(args.request.formFields)
|
|
209
|
+
? args.request.formFields
|
|
210
|
+
: {};
|
|
211
|
+
for (const [key, value] of Object.entries(formFields)) {
|
|
212
|
+
if (typeof value === "string") {
|
|
213
|
+
form.append(key, value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
form.append(uploadFieldName, new File([data], fileName, { type: uploadContentType }));
|
|
217
|
+
response = await fetch(resolvedUploadUrl, {
|
|
218
|
+
method: uploadMethod,
|
|
219
|
+
headers,
|
|
220
|
+
body: form,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
headers["Content-Type"] = uploadContentType;
|
|
225
|
+
response = await fetch(resolvedUploadUrl, {
|
|
226
|
+
method: uploadMethod,
|
|
227
|
+
headers,
|
|
228
|
+
body: data,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
208
231
|
const text = await response.text();
|
|
209
232
|
let upload = {};
|
|
210
233
|
try {
|
|
@@ -225,15 +248,12 @@ async function executeFsRpc(args) {
|
|
|
225
248
|
upload,
|
|
226
249
|
};
|
|
227
250
|
}
|
|
228
|
-
if (action === "
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
throw new Error("contentBase64 is required");
|
|
232
|
-
}
|
|
251
|
+
if (action === "write_text") {
|
|
252
|
+
const text = typeof args.request.text === "string" ? args.request.text : "";
|
|
253
|
+
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
|
233
254
|
const parentDir = path.dirname(abs);
|
|
234
255
|
await mkdir(parentDir, { recursive: true });
|
|
235
|
-
|
|
236
|
-
await writeFile(abs, bytes);
|
|
256
|
+
await writeFile(abs, text, { encoding: encoding });
|
|
237
257
|
const entry = await stat(abs);
|
|
238
258
|
return {
|
|
239
259
|
ok: true,
|
|
@@ -243,6 +263,7 @@ async function executeFsRpc(args) {
|
|
|
243
263
|
size: entry.size,
|
|
244
264
|
mimeType: inferMimeType(abs),
|
|
245
265
|
mtimeMs: entry.mtimeMs,
|
|
266
|
+
encoding,
|
|
246
267
|
};
|
|
247
268
|
}
|
|
248
269
|
if (action === "delete_path") {
|
|
@@ -277,6 +298,9 @@ async function executeFsRpc(args) {
|
|
|
277
298
|
const parentDir = path.dirname(abs);
|
|
278
299
|
await mkdir(parentDir, { recursive: true });
|
|
279
300
|
await writeFile(abs, bytes);
|
|
301
|
+
if (abs.endsWith(".skillpkg")) {
|
|
302
|
+
console.log(`[doer-agent] skillpkg downloaded path=${formatPath(abs)} size=${bytes.byteLength} sha256=${sha256Hex(bytes)}`);
|
|
303
|
+
}
|
|
280
304
|
const entry = await stat(abs);
|
|
281
305
|
return {
|
|
282
306
|
ok: true,
|
|
@@ -299,18 +323,42 @@ async function executeFsRpc(args) {
|
|
|
299
323
|
}
|
|
300
324
|
const destinationTarget = normalizeFsRpcPath(args.workspaceRoot, rawDestinationPath);
|
|
301
325
|
const archiveBytes = await readFile(abs);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
326
|
+
const magic = archiveBytes.subarray(0, 8).toString("hex");
|
|
327
|
+
const digest = sha256Hex(archiveBytes);
|
|
328
|
+
const destinationParent = path.dirname(destinationTarget.abs);
|
|
329
|
+
const tempDestinationAbs = path.join(destinationParent, `.tmp-extract-${path.basename(destinationTarget.abs)}-${crypto.randomBytes(6).toString("hex")}`);
|
|
330
|
+
await mkdir(destinationParent, { recursive: true });
|
|
331
|
+
try {
|
|
332
|
+
const existing = await stat(destinationTarget.abs);
|
|
333
|
+
if (existing.isDirectory()) {
|
|
334
|
+
const entries = await readdir(destinationTarget.abs);
|
|
335
|
+
if (entries.length > 0) {
|
|
336
|
+
throw new Error("destinationPath already exists");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new Error("destinationPath already exists");
|
|
341
|
+
}
|
|
342
|
+
await rm(destinationTarget.abs, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
if (!(error instanceof Error) || !error.message.includes("ENOENT")) {
|
|
346
|
+
throw error;
|
|
310
347
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
348
|
+
}
|
|
349
|
+
await mkdir(tempDestinationAbs, { recursive: true });
|
|
350
|
+
try {
|
|
351
|
+
await extractTar({
|
|
352
|
+
cwd: tempDestinationAbs,
|
|
353
|
+
file: abs,
|
|
354
|
+
gzip: true,
|
|
355
|
+
});
|
|
356
|
+
await rename(tempDestinationAbs, destinationTarget.abs);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
await rm(tempDestinationAbs, { recursive: true, force: true }).catch(() => undefined);
|
|
360
|
+
const message = error instanceof Error ? error.message : "extract failed";
|
|
361
|
+
throw new Error(`${message} (magic=${magic} size=${archiveBytes.byteLength} sha256=${digest})`);
|
|
314
362
|
}
|
|
315
363
|
return {
|
|
316
364
|
ok: true,
|
|
@@ -323,21 +371,6 @@ async function executeFsRpc(args) {
|
|
|
323
371
|
if (!entry.isFile()) {
|
|
324
372
|
throw new Error("path is not a file");
|
|
325
373
|
}
|
|
326
|
-
if (action === "read_file") {
|
|
327
|
-
const maxBytes = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.maxBytes, 2_000_000), 5_000_000));
|
|
328
|
-
const data = await readFile(abs);
|
|
329
|
-
const truncated = data.byteLength > maxBytes;
|
|
330
|
-
const bytes = truncated ? data.subarray(0, maxBytes) : data;
|
|
331
|
-
return {
|
|
332
|
-
ok: true,
|
|
333
|
-
action,
|
|
334
|
-
path: formatPath(abs),
|
|
335
|
-
mimeType: inferMimeType(abs),
|
|
336
|
-
size: entry.size,
|
|
337
|
-
truncated,
|
|
338
|
-
contentBase64: bytes.toString("base64"),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
374
|
const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
|
|
342
375
|
const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
|
|
343
376
|
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Reverse-polling agent runtime for doer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/agent.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
28
28
|
"@openai/codex-sdk": "^0.115.0",
|
|
29
|
-
"nats": "^2.29.3"
|
|
29
|
+
"nats": "^2.29.3",
|
|
30
|
+
"tar": "^7.5.13"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^20",
|