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.
- package/dist/agent.js +101 -5
- 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(
|
|
3311
|
-
|
|
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() });
|