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