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.
@@ -1,7 +1,8 @@
1
1
  import path from "node:path";
2
- import { gunzipSync, gzipSync } from "node:zlib";
3
- import { mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
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 === "read_file" ||
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 normalizeArchiveRelativePath(value) {
92
- const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
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 collectDirectoryFiles(absDir, rootDir = absDir) {
99
- const rows = await readdir(absDir, { withFileTypes: true });
100
- const files = [];
101
- for (const row of rows.sort((a, b) => a.name.localeCompare(b.name))) {
102
- const child = path.join(absDir, row.name);
103
- if (row.isDirectory()) {
104
- files.push(...await collectDirectoryFiles(child, rootDir));
105
- continue;
106
- }
107
- if (!row.isFile()) {
108
- continue;
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
- const files = await collectDirectoryFiles(abs);
172
- if (!files.some((file) => file.relPath === "SKILL.md")) {
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 writeFile(archiveTarget.abs, payload);
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 === "write_file") {
260
- const contentBase64 = typeof args.request.contentBase64 === "string" ? args.request.contentBase64 : "";
261
- if (!contentBase64) {
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
- const bytes = Buffer.from(contentBase64, "base64");
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 decoded = JSON.parse(gunzipSync(archiveBytes).toString("utf8"));
334
- const files = Array.isArray(decoded.files) ? decoded.files : [];
335
- await mkdir(destinationTarget.abs, { recursive: true });
336
- for (const file of files) {
337
- const relPath = typeof file.relPath === "string" ? normalizeArchiveRelativePath(file.relPath) : "";
338
- const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
339
- if (!relPath || !contentBase64) {
340
- throw new Error("archive contains an invalid file entry");
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
- const targetPath = path.join(destinationTarget.abs, relPath);
343
- await mkdir(path.dirname(targetPath), { recursive: true });
344
- await writeFile(targetPath, Buffer.from(contentBase64, "base64"));
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.5",
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",