doer-agent 0.3.1 → 0.3.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.
Files changed (2) hide show
  1. package/dist/agent.js +103 -6
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync, statSync, watch } from "node:fs";
3
- import { chmod, mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { chmod, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { gunzipSync, gzipSync } from "node:zlib";
6
7
  import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, StringCodec } from "nats";
7
8
  const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
8
9
  const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -2427,7 +2428,9 @@ function parseFsRpcAction(value) {
2427
2428
  value === "read_file" ||
2428
2429
  value === "write_file" ||
2429
2430
  value === "download_file" ||
2430
- value === "delete_path") {
2431
+ value === "delete_path" ||
2432
+ value === "archive_dir" ||
2433
+ value === "extract_archive") {
2431
2434
  return value;
2432
2435
  }
2433
2436
  throw new Error("unsupported action");
@@ -2485,6 +2488,34 @@ function inferMimeType(filePath) {
2485
2488
  }
2486
2489
  return "application/octet-stream";
2487
2490
  }
2491
+ function normalizeArchiveRelativePath(value) {
2492
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
2493
+ if (!normalized || normalized.includes("..")) {
2494
+ throw new Error("invalid archive entry path");
2495
+ }
2496
+ return normalized;
2497
+ }
2498
+ async function collectDirectoryFiles(absDir, rootDir = absDir) {
2499
+ const rows = await readdir(absDir, { withFileTypes: true });
2500
+ const files = [];
2501
+ for (const row of rows.sort((a, b) => a.name.localeCompare(b.name))) {
2502
+ const child = path.join(absDir, row.name);
2503
+ if (row.isDirectory()) {
2504
+ files.push(...await collectDirectoryFiles(child, rootDir));
2505
+ continue;
2506
+ }
2507
+ if (!row.isFile()) {
2508
+ continue;
2509
+ }
2510
+ const bytes = await readFile(child);
2511
+ files.push({
2512
+ relPath: normalizeArchiveRelativePath(path.relative(rootDir, child)),
2513
+ contentBase64: Buffer.from(bytes).toString("base64"),
2514
+ sizeBytes: bytes.byteLength,
2515
+ });
2516
+ }
2517
+ return files;
2518
+ }
2488
2519
  async function executeFsRpc(args) {
2489
2520
  const action = parseFsRpcAction(args.request.action);
2490
2521
  const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
@@ -2527,6 +2558,34 @@ async function executeFsRpc(args) {
2527
2558
  total: items.length,
2528
2559
  };
2529
2560
  }
2561
+ if (action === "archive_dir") {
2562
+ const entry = await stat(abs);
2563
+ if (!entry.isDirectory()) {
2564
+ throw new Error("path is not a directory");
2565
+ }
2566
+ const rawArchivePath = typeof args.request.archivePath === "string" ? args.request.archivePath : "";
2567
+ if (!rawArchivePath) {
2568
+ throw new Error("archivePath is required");
2569
+ }
2570
+ const archiveTarget = normalizeFsRpcPath(rawArchivePath);
2571
+ const files = await collectDirectoryFiles(abs);
2572
+ if (!files.some((file) => file.relPath === "SKILL.md")) {
2573
+ throw new Error("Selected skill directory must contain SKILL.md");
2574
+ }
2575
+ const payload = gzipSync(Buffer.from(JSON.stringify({
2576
+ files,
2577
+ }), "utf8"));
2578
+ await mkdir(path.dirname(archiveTarget.abs), { recursive: true });
2579
+ await writeFile(archiveTarget.abs, payload);
2580
+ const archiveStat = await stat(archiveTarget.abs);
2581
+ return {
2582
+ ok: true,
2583
+ action,
2584
+ path: formatPath(abs),
2585
+ archivePath: archiveTarget.formatPath(archiveTarget.abs),
2586
+ size: archiveStat.size,
2587
+ };
2588
+ }
2530
2589
  if (action === "fetch_file") {
2531
2590
  const entry = await stat(abs);
2532
2591
  if (!entry.isFile()) {
@@ -2537,12 +2596,13 @@ async function executeFsRpc(args) {
2537
2596
  if (!uploadUrl || !agentId) {
2538
2597
  throw new Error("missing upload parameters");
2539
2598
  }
2599
+ const resolvedUploadUrl = new URL(uploadUrl, `${args.serverBaseUrl}/`).toString();
2540
2600
  const data = await readFile(abs);
2541
2601
  const fileName = path.basename(abs) || "file";
2542
2602
  const form = new FormData();
2543
2603
  form.append("file", new File([data], fileName));
2544
2604
  form.append("agentId", agentId);
2545
- const response = await fetch(uploadUrl, {
2605
+ const response = await fetch(resolvedUploadUrl, {
2546
2606
  method: "POST",
2547
2607
  headers: { Authorization: `Bearer ${args.agentToken}` },
2548
2608
  body: form,
@@ -2627,6 +2687,37 @@ async function executeFsRpc(args) {
2627
2687
  mtimeMs: entry.mtimeMs,
2628
2688
  };
2629
2689
  }
2690
+ if (action === "extract_archive") {
2691
+ const archiveEntry = await stat(abs);
2692
+ if (!archiveEntry.isFile()) {
2693
+ throw new Error("path is not a file");
2694
+ }
2695
+ const rawDestinationPath = typeof args.request.destinationPath === "string" ? args.request.destinationPath : "";
2696
+ if (!rawDestinationPath) {
2697
+ throw new Error("destinationPath is required");
2698
+ }
2699
+ const destinationTarget = normalizeFsRpcPath(rawDestinationPath);
2700
+ const archiveBytes = await readFile(abs);
2701
+ const decoded = JSON.parse(gunzipSync(archiveBytes).toString("utf8"));
2702
+ const files = Array.isArray(decoded.files) ? decoded.files : [];
2703
+ await mkdir(destinationTarget.abs, { recursive: true });
2704
+ for (const file of files) {
2705
+ const relPath = typeof file.relPath === "string" ? normalizeArchiveRelativePath(file.relPath) : "";
2706
+ const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
2707
+ if (!relPath || !contentBase64) {
2708
+ throw new Error("archive contains an invalid file entry");
2709
+ }
2710
+ const targetPath = path.join(destinationTarget.abs, relPath);
2711
+ await mkdir(path.dirname(targetPath), { recursive: true });
2712
+ await writeFile(targetPath, Buffer.from(contentBase64, "base64"));
2713
+ }
2714
+ return {
2715
+ ok: true,
2716
+ action,
2717
+ path: formatPath(abs),
2718
+ absolutePath: destinationTarget.formatPath(destinationTarget.abs),
2719
+ };
2720
+ }
2630
2721
  const entry = await stat(abs);
2631
2722
  if (!entry.isFile()) {
2632
2723
  throw new Error("path is not a file");
@@ -3273,6 +3364,7 @@ function stopAllSessionWatchers() {
3273
3364
  }
3274
3365
  async function startSessionWatch(args) {
3275
3366
  const resolvedFile = resolveSessionFilePath(args.filePath);
3367
+ const canonicalFile = await realpath(resolvedFile).catch(() => resolvedFile);
3276
3368
  const watchId = `watch_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
3277
3369
  let watcher = null;
3278
3370
  let active = true;
@@ -3297,9 +3389,9 @@ async function startSessionWatch(args) {
3297
3389
  return;
3298
3390
  }
3299
3391
  active = false;
3392
+ activeSessionWatchers.delete(watchId);
3300
3393
  watcher?.close();
3301
3394
  watcher = null;
3302
- activeSessionWatchers.delete(watchId);
3303
3395
  };
3304
3396
  const notifyFromContent = () => {
3305
3397
  emitEvent({
@@ -3307,8 +3399,13 @@ async function startSessionWatch(args) {
3307
3399
  at: formatLocalTimestamp(),
3308
3400
  });
3309
3401
  };
3310
- watcher = watch(resolvedFile, () => {
3311
- notifyFromContent();
3402
+ watcher = watch(canonicalFile, { persistent: false }, (eventType) => {
3403
+ if (!active) {
3404
+ return;
3405
+ }
3406
+ if (eventType === "change" || eventType === "rename") {
3407
+ notifyFromContent();
3408
+ }
3312
3409
  });
3313
3410
  activeSessionWatchers.set(watchId, cleanup);
3314
3411
  emitEvent({ type: "stream.started", watchId, at: formatLocalTimestamp() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",