@agimon-ai/po-file-edit 0.0.0-alpha.32 → 0.0.0-alpha.34
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.
|
@@ -47,6 +47,7 @@ ignore = __toESM(ignore, 1);
|
|
|
47
47
|
let node_os = require("node:os");
|
|
48
48
|
node_os = __toESM(node_os, 1);
|
|
49
49
|
let _agimon_ai_po_config = require("@agimon-ai/po-config");
|
|
50
|
+
let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
|
|
50
51
|
let hono = require("hono");
|
|
51
52
|
//#region src/constants/editPresence.ts
|
|
52
53
|
const PRESENCE_NOTE_TITLE = "[Po: other active agents edited files in this result]";
|
|
@@ -1396,17 +1397,44 @@ function describe(error) {
|
|
|
1396
1397
|
return error instanceof Error ? error.message : String(error);
|
|
1397
1398
|
}
|
|
1398
1399
|
/** Reads the file as the source view will show it, or says why it cannot. */
|
|
1399
|
-
async function readWorking(filePath) {
|
|
1400
|
+
async function readWorking(filePath, opened, allowedRoot) {
|
|
1401
|
+
let handle = opened;
|
|
1400
1402
|
let raw;
|
|
1401
1403
|
try {
|
|
1402
|
-
|
|
1404
|
+
handle ??= await (0, _agimon_ai_po_core_platform.openGuardedFile)(filePath, { root: allowedRoot });
|
|
1405
|
+
const stats = await handle.stat();
|
|
1406
|
+
if (!stats.isFile()) return {
|
|
1407
|
+
content: "",
|
|
1408
|
+
hash: "",
|
|
1409
|
+
unavailable: true,
|
|
1410
|
+
reason: "the file is not a regular file"
|
|
1411
|
+
};
|
|
1412
|
+
const named = await node_fs_promises.default.lstat(filePath);
|
|
1413
|
+
if (!named.isFile() || stats.dev !== named.dev || stats.ino !== named.ino || allowedRoot !== void 0 && !await isRealPathInside(allowedRoot, filePath)) return {
|
|
1414
|
+
content: "",
|
|
1415
|
+
hash: "",
|
|
1416
|
+
unavailable: true,
|
|
1417
|
+
reason: "the file path changed or leaves its readable directory"
|
|
1418
|
+
};
|
|
1419
|
+
raw = Buffer.alloc(1048577);
|
|
1420
|
+
if (stats.size <= 1048576) {
|
|
1421
|
+
let offset = 0;
|
|
1422
|
+
while (offset < raw.length) {
|
|
1423
|
+
const { bytesRead } = await handle.read(raw, offset, raw.length - offset, offset);
|
|
1424
|
+
if (bytesRead === 0) break;
|
|
1425
|
+
offset += bytesRead;
|
|
1426
|
+
}
|
|
1427
|
+
raw = raw.subarray(0, offset);
|
|
1428
|
+
}
|
|
1403
1429
|
} catch {
|
|
1404
1430
|
return {
|
|
1405
1431
|
content: "",
|
|
1406
1432
|
hash: "",
|
|
1407
1433
|
unavailable: true,
|
|
1408
|
-
reason: "the file no longer exists"
|
|
1434
|
+
reason: "the file no longer exists or cannot be safely read"
|
|
1409
1435
|
};
|
|
1436
|
+
} finally {
|
|
1437
|
+
if (opened === void 0) await handle?.close();
|
|
1410
1438
|
}
|
|
1411
1439
|
if (raw.byteLength > 1048576) return {
|
|
1412
1440
|
content: "",
|
|
@@ -1579,7 +1607,7 @@ function createFileEditsApi(options = {}) {
|
|
|
1579
1607
|
const versions = await recordedVersions(requested);
|
|
1580
1608
|
if (versions === void 0 || requested === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
1581
1609
|
const filePath = node_path.default.resolve(cwd, requested);
|
|
1582
|
-
const working = await readWorking(filePath);
|
|
1610
|
+
const working = await readWorking(filePath, void 0, cwd !== "" && isInside(node_path.default.resolve(cwd), filePath) ? node_path.default.resolve(cwd) : void 0);
|
|
1583
1611
|
const body = {
|
|
1584
1612
|
path: filePath,
|
|
1585
1613
|
relPath: cwd === "" ? filePath : node_path.default.relative(cwd, filePath),
|
|
@@ -1607,11 +1635,12 @@ function createFileEditsApi(options = {}) {
|
|
|
1607
1635
|
const root = node_path.default.resolve(cwd);
|
|
1608
1636
|
const filePath = node_path.default.resolve(root, requested);
|
|
1609
1637
|
const inSession = await isInsideSession(filePath);
|
|
1610
|
-
|
|
1638
|
+
const allowedRoot = inSession ? root : await readableRootOf(filePath);
|
|
1639
|
+
if (allowedRoot === void 0) return context.json({ error: "The path leaves the session directory." }, 403);
|
|
1611
1640
|
const body = {
|
|
1612
1641
|
path: filePath,
|
|
1613
1642
|
relPath: inSession ? node_path.default.relative(root, filePath) : filePath,
|
|
1614
|
-
working: await readWorking(filePath)
|
|
1643
|
+
working: await readWorking(filePath, void 0, allowedRoot)
|
|
1615
1644
|
};
|
|
1616
1645
|
return context.json(body);
|
|
1617
1646
|
});
|
|
@@ -1622,19 +1651,41 @@ function createFileEditsApi(options = {}) {
|
|
|
1622
1651
|
} catch {
|
|
1623
1652
|
return context.json({ error: "The save body is not JSON." }, 400);
|
|
1624
1653
|
}
|
|
1625
|
-
if (typeof request.path !== "string" || typeof request.content !== "string") return context.json({ error: "A save names a path
|
|
1626
|
-
|
|
1627
|
-
|
|
1654
|
+
if (typeof request !== "object" || request === null || Array.isArray(request) || !("path" in request) || typeof request.path !== "string" || !("content" in request) || typeof request.content !== "string" || !("expectedHash" in request) || typeof request.expectedHash !== "string") return context.json({ error: "A save names a path, the content to write and the hash it was read at." }, 400);
|
|
1655
|
+
const { path: requested, content, expectedHash } = request;
|
|
1656
|
+
if (await recordedVersions(requested) === void 0) return context.json({ error: "This session recorded no changes to that file." }, 404);
|
|
1657
|
+
const filePath = node_path.default.resolve(cwd, requested);
|
|
1628
1658
|
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1659
|
+
if (Buffer.byteLength(content, "utf8") > 1048576) return context.json({ error: `The file is larger than ${MAX_WORKING_BYTES} bytes.` }, 413);
|
|
1660
|
+
try {
|
|
1661
|
+
const canonical = await node_fs_promises.default.realpath(filePath);
|
|
1662
|
+
return await _agimon_ai_po_core_platform.fileMutationRunner.forKey(canonical)(async () => {
|
|
1663
|
+
const handle = await (0, _agimon_ai_po_core_platform.openGuardedFile)(filePath, {
|
|
1664
|
+
write: true,
|
|
1665
|
+
root: node_path.default.resolve(cwd)
|
|
1666
|
+
});
|
|
1667
|
+
try {
|
|
1668
|
+
if (await node_fs_promises.default.realpath(filePath) !== canonical) return context.json({ error: "The file changed before it could be written." }, 409);
|
|
1669
|
+
const current = await readWorking(filePath, handle, node_path.default.resolve(cwd));
|
|
1670
|
+
if (current.unavailable) return context.json({ error: current.reason ?? "The file cannot be read." }, 409);
|
|
1671
|
+
if (current.hash !== expectedHash) return context.json({
|
|
1672
|
+
error: "The file changed since it was opened.",
|
|
1673
|
+
hash: current.hash
|
|
1674
|
+
}, 409);
|
|
1675
|
+
const opened = await handle.stat();
|
|
1676
|
+
const named = await node_fs_promises.default.lstat(filePath);
|
|
1677
|
+
if (!opened.isFile() || opened.nlink !== 1 || !named.isFile() || named.nlink !== 1 || opened.dev !== named.dev || opened.ino !== named.ino || await node_fs_promises.default.realpath(filePath) !== canonical || !await isInsideSession(filePath)) return context.json({ error: "The file changed before it could be written." }, 409);
|
|
1678
|
+
await handle.writeFile(content, "utf8");
|
|
1679
|
+
await handle.truncate(Buffer.byteLength(content, "utf8"));
|
|
1680
|
+
const body = { hash: hashOf(content) };
|
|
1681
|
+
return context.json(body);
|
|
1682
|
+
} finally {
|
|
1683
|
+
await handle.close();
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
} catch {
|
|
1687
|
+
return context.json({ error: "The file cannot be safely written." }, 409);
|
|
1688
|
+
}
|
|
1638
1689
|
});
|
|
1639
1690
|
app.delete(routes.remove.path, async (context) => {
|
|
1640
1691
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
@@ -1642,7 +1693,15 @@ function createFileEditsApi(options = {}) {
|
|
|
1642
1693
|
const filePath = node_path.default.resolve(cwd, requested);
|
|
1643
1694
|
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
1644
1695
|
try {
|
|
1645
|
-
await node_fs_promises.default.
|
|
1696
|
+
const canonical = await node_fs_promises.default.realpath(filePath).catch((error) => {
|
|
1697
|
+
if (hasCode(error, "ENOENT")) return filePath;
|
|
1698
|
+
throw error;
|
|
1699
|
+
});
|
|
1700
|
+
return await _agimon_ai_po_core_platform.fileMutationRunner.forKey(canonical)(async () => {
|
|
1701
|
+
if (!await isInsideSession(filePath)) return context.json({ error: "Files outside the session directory are read-only." }, 403);
|
|
1702
|
+
await node_fs_promises.default.unlink(filePath);
|
|
1703
|
+
return context.body(null, 204);
|
|
1704
|
+
});
|
|
1646
1705
|
} catch (error) {
|
|
1647
1706
|
if (!hasCode(error, "ENOENT")) return context.json({ error: `The file could not be deleted: ${describe(error)}` }, 409);
|
|
1648
1707
|
}
|
|
@@ -1654,14 +1713,17 @@ function createFileEditsApi(options = {}) {
|
|
|
1654
1713
|
const api = {
|
|
1655
1714
|
basePath: API_BASE_PATH,
|
|
1656
1715
|
start(context) {
|
|
1657
|
-
const environment = context.environment ??
|
|
1716
|
+
const environment = context.environment ?? {};
|
|
1658
1717
|
const cwd = context.cwd;
|
|
1659
1718
|
const app = createFileEditsApi({
|
|
1660
|
-
paths: new FileEditPaths(
|
|
1719
|
+
paths: new FileEditPaths({
|
|
1720
|
+
...environment,
|
|
1721
|
+
HOME: context.homeDirectory ?? environment.HOME
|
|
1722
|
+
}),
|
|
1661
1723
|
...context.sessionId === void 0 ? {} : { sessionId: context.sessionId },
|
|
1662
1724
|
...cwd === void 0 ? {} : {
|
|
1663
1725
|
cwd,
|
|
1664
|
-
readableRoots: () => plansRoots(cwd, environment)
|
|
1726
|
+
readableRoots: () => plansRoots(cwd, environment, context.homeDirectory)
|
|
1665
1727
|
}
|
|
1666
1728
|
});
|
|
1667
1729
|
return {
|
|
@@ -1675,8 +1737,8 @@ const api = {
|
|
|
1675
1737
|
* a plan link in the transcript opens in the file view. A config that cannot be
|
|
1676
1738
|
* read admits nothing extra rather than failing the preview.
|
|
1677
1739
|
*/
|
|
1678
|
-
function plansRoots(cwd, environment) {
|
|
1679
|
-
const home = environment.HOME ?? node_os.default.homedir();
|
|
1740
|
+
function plansRoots(cwd, environment, homeDirectory) {
|
|
1741
|
+
const home = homeDirectory ?? environment.HOME ?? node_os.default.homedir();
|
|
1680
1742
|
try {
|
|
1681
1743
|
const configured = (0, _agimon_ai_po_config.loadPoConfig)(cwd, home, environment).modes?.planning?.plansDirectory;
|
|
1682
1744
|
return [(0, _agimon_ai_po_config.resolvePlanningPlansDirectory)(configured, cwd, home)];
|