doer-agent 0.4.5 → 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 +69 -67
- 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() : ".";
|
|
@@ -25,8 +26,7 @@ function parseFsRpcAction(value) {
|
|
|
25
26
|
value === "stat" ||
|
|
26
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,
|
|
@@ -256,15 +248,12 @@ async function executeFsRpc(args) {
|
|
|
256
248
|
upload,
|
|
257
249
|
};
|
|
258
250
|
}
|
|
259
|
-
if (action === "
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
throw new Error("contentBase64 is required");
|
|
263
|
-
}
|
|
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";
|
|
264
254
|
const parentDir = path.dirname(abs);
|
|
265
255
|
await mkdir(parentDir, { recursive: true });
|
|
266
|
-
|
|
267
|
-
await writeFile(abs, bytes);
|
|
256
|
+
await writeFile(abs, text, { encoding: encoding });
|
|
268
257
|
const entry = await stat(abs);
|
|
269
258
|
return {
|
|
270
259
|
ok: true,
|
|
@@ -274,6 +263,7 @@ async function executeFsRpc(args) {
|
|
|
274
263
|
size: entry.size,
|
|
275
264
|
mimeType: inferMimeType(abs),
|
|
276
265
|
mtimeMs: entry.mtimeMs,
|
|
266
|
+
encoding,
|
|
277
267
|
};
|
|
278
268
|
}
|
|
279
269
|
if (action === "delete_path") {
|
|
@@ -308,6 +298,9 @@ async function executeFsRpc(args) {
|
|
|
308
298
|
const parentDir = path.dirname(abs);
|
|
309
299
|
await mkdir(parentDir, { recursive: true });
|
|
310
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
|
+
}
|
|
311
304
|
const entry = await stat(abs);
|
|
312
305
|
return {
|
|
313
306
|
ok: true,
|
|
@@ -330,18 +323,42 @@ async function executeFsRpc(args) {
|
|
|
330
323
|
}
|
|
331
324
|
const destinationTarget = normalizeFsRpcPath(args.workspaceRoot, rawDestinationPath);
|
|
332
325
|
const archiveBytes = await readFile(abs);
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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;
|
|
341
347
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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})`);
|
|
345
362
|
}
|
|
346
363
|
return {
|
|
347
364
|
ok: true,
|
|
@@ -354,21 +371,6 @@ async function executeFsRpc(args) {
|
|
|
354
371
|
if (!entry.isFile()) {
|
|
355
372
|
throw new Error("path is not a file");
|
|
356
373
|
}
|
|
357
|
-
if (action === "read_file") {
|
|
358
|
-
const maxBytes = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.maxBytes, 2_000_000), 5_000_000));
|
|
359
|
-
const data = await readFile(abs);
|
|
360
|
-
const truncated = data.byteLength > maxBytes;
|
|
361
|
-
const bytes = truncated ? data.subarray(0, maxBytes) : data;
|
|
362
|
-
return {
|
|
363
|
-
ok: true,
|
|
364
|
-
action,
|
|
365
|
-
path: formatPath(abs),
|
|
366
|
-
mimeType: inferMimeType(abs),
|
|
367
|
-
size: entry.size,
|
|
368
|
-
truncated,
|
|
369
|
-
contentBase64: bytes.toString("base64"),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
374
|
const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
|
|
373
375
|
const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
|
|
374
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",
|