doer-agent 0.3.1 → 0.3.2

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 +101 -5
  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()) {
@@ -2627,6 +2686,37 @@ async function executeFsRpc(args) {
2627
2686
  mtimeMs: entry.mtimeMs,
2628
2687
  };
2629
2688
  }
2689
+ if (action === "extract_archive") {
2690
+ const archiveEntry = await stat(abs);
2691
+ if (!archiveEntry.isFile()) {
2692
+ throw new Error("path is not a file");
2693
+ }
2694
+ const rawDestinationPath = typeof args.request.destinationPath === "string" ? args.request.destinationPath : "";
2695
+ if (!rawDestinationPath) {
2696
+ throw new Error("destinationPath is required");
2697
+ }
2698
+ const destinationTarget = normalizeFsRpcPath(rawDestinationPath);
2699
+ const archiveBytes = await readFile(abs);
2700
+ const decoded = JSON.parse(gunzipSync(archiveBytes).toString("utf8"));
2701
+ const files = Array.isArray(decoded.files) ? decoded.files : [];
2702
+ await mkdir(destinationTarget.abs, { recursive: true });
2703
+ for (const file of files) {
2704
+ const relPath = typeof file.relPath === "string" ? normalizeArchiveRelativePath(file.relPath) : "";
2705
+ const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
2706
+ if (!relPath || !contentBase64) {
2707
+ throw new Error("archive contains an invalid file entry");
2708
+ }
2709
+ const targetPath = path.join(destinationTarget.abs, relPath);
2710
+ await mkdir(path.dirname(targetPath), { recursive: true });
2711
+ await writeFile(targetPath, Buffer.from(contentBase64, "base64"));
2712
+ }
2713
+ return {
2714
+ ok: true,
2715
+ action,
2716
+ path: formatPath(abs),
2717
+ absolutePath: destinationTarget.formatPath(destinationTarget.abs),
2718
+ };
2719
+ }
2630
2720
  const entry = await stat(abs);
2631
2721
  if (!entry.isFile()) {
2632
2722
  throw new Error("path is not a file");
@@ -3273,6 +3363,7 @@ function stopAllSessionWatchers() {
3273
3363
  }
3274
3364
  async function startSessionWatch(args) {
3275
3365
  const resolvedFile = resolveSessionFilePath(args.filePath);
3366
+ const canonicalFile = await realpath(resolvedFile).catch(() => resolvedFile);
3276
3367
  const watchId = `watch_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
3277
3368
  let watcher = null;
3278
3369
  let active = true;
@@ -3297,9 +3388,9 @@ async function startSessionWatch(args) {
3297
3388
  return;
3298
3389
  }
3299
3390
  active = false;
3391
+ activeSessionWatchers.delete(watchId);
3300
3392
  watcher?.close();
3301
3393
  watcher = null;
3302
- activeSessionWatchers.delete(watchId);
3303
3394
  };
3304
3395
  const notifyFromContent = () => {
3305
3396
  emitEvent({
@@ -3307,8 +3398,13 @@ async function startSessionWatch(args) {
3307
3398
  at: formatLocalTimestamp(),
3308
3399
  });
3309
3400
  };
3310
- watcher = watch(resolvedFile, () => {
3311
- notifyFromContent();
3401
+ watcher = watch(canonicalFile, { persistent: false }, (eventType) => {
3402
+ if (!active) {
3403
+ return;
3404
+ }
3405
+ if (eventType === "change" || eventType === "rename") {
3406
+ notifyFromContent();
3407
+ }
3312
3408
  });
3313
3409
  activeSessionWatchers.set(watchId, cleanup);
3314
3410
  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.2",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",