@hachej/boring-agent 0.1.13 → 0.1.14
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/README.md +242 -42
- package/dist/DebugDrawer-MCYZ4AWZ.js +6 -0
- package/dist/chunk-F3CE5CNW.js +38 -0
- package/dist/chunk-MMJA3QON.js +295 -0
- package/dist/front/index.d.ts +48 -76
- package/dist/front/index.js +2396 -1795
- package/dist/front/styles.css +2242 -1267
- package/dist/{sandbox-handle-store-DCNEJJ6b.d.ts → sandbox-handle-store-hK76cTjn.d.ts} +2 -0
- package/dist/server/index.d.ts +22 -2
- package/dist/server/index.js +2015 -1541
- package/dist/shared/index.d.ts +11 -7
- package/dist/shared/index.js +10 -1
- package/dist/{harness-DT3ZzdAN.d.ts → tool-ui-DSmWuqGe.d.ts} +33 -4
- package/package.json +4 -4
package/dist/server/index.js
CHANGED
|
@@ -1489,286 +1489,827 @@ function createNodeWorkspace(root) {
|
|
|
1489
1489
|
};
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
|
-
// src/server/
|
|
1493
|
-
import {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
var
|
|
1501
|
-
var
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
maxBuffer: 1024 * 1024 * 20,
|
|
1518
|
-
timeout: 5 * 60 * 1e3
|
|
1519
|
-
});
|
|
1492
|
+
// src/server/http/routes/file.ts
|
|
1493
|
+
import { dirname as dirname4, extname, relative as relative4 } from "path/posix";
|
|
1494
|
+
|
|
1495
|
+
// src/server/http/middleware.ts
|
|
1496
|
+
var ERROR_CODE_AUTH_REQUIRED = "auth_required";
|
|
1497
|
+
var ERROR_CODE_AUTH_INVALID = "auth_invalid";
|
|
1498
|
+
var ERROR_CODE_INVALID_PATH = "invalid_path";
|
|
1499
|
+
var ERROR_CODE_VALIDATION_ERROR = "validation_error";
|
|
1500
|
+
var ERROR_CODE_PATH_REJECTED = "path_rejected";
|
|
1501
|
+
var ERROR_CODE_NOT_FOUND = "not_found";
|
|
1502
|
+
var ERROR_CODE_ALREADY_EXISTS = "already_exists";
|
|
1503
|
+
var ERROR_CODE_CONFLICT = "conflict";
|
|
1504
|
+
var ERROR_CODE_RANGE_NOT_SATISFIABLE = "range_not_satisfiable";
|
|
1505
|
+
var ERROR_CODE_INTERNAL = "internal";
|
|
1506
|
+
var ERROR_CODE_NOT_IMPLEMENTED = "not_implemented";
|
|
1507
|
+
var DEV_MODE_WARNING = "No auth token set \u2014 running in dev mode";
|
|
1508
|
+
var DEFAULT_WORKSPACE_ID = "default";
|
|
1509
|
+
function errorPayload(code, message, field) {
|
|
1510
|
+
return {
|
|
1511
|
+
error: {
|
|
1512
|
+
code,
|
|
1513
|
+
message,
|
|
1514
|
+
...field ? { field } : {}
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1520
1517
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
return false;
|
|
1527
|
-
}
|
|
1518
|
+
function ensureWorkspaceContext(request, workspaceId, authenticated) {
|
|
1519
|
+
request.workspaceContext = {
|
|
1520
|
+
workspaceId,
|
|
1521
|
+
authenticated
|
|
1522
|
+
};
|
|
1528
1523
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1524
|
+
function createAuthMiddleware(opts = {}) {
|
|
1525
|
+
let warnedDevMode = false;
|
|
1526
|
+
return async function authMiddleware(request, reply) {
|
|
1527
|
+
const workspaceId = opts.workspaceId ?? DEFAULT_WORKSPACE_ID;
|
|
1528
|
+
const authToken = opts.authToken?.trim() || void 0;
|
|
1529
|
+
ensureWorkspaceContext(request, workspaceId, false);
|
|
1530
|
+
if (opts.publicPaths?.includes(request.url.split("?")[0])) {
|
|
1531
|
+
return;
|
|
1537
1532
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
async function fingerprint(contributions) {
|
|
1546
|
-
const hash = createHash3("sha256");
|
|
1547
|
-
hash.update(JSON.stringify({ v: PROVISIONING_VERSION }));
|
|
1548
|
-
for (const { id, provisioning } of contributions) {
|
|
1549
|
-
hash.update(`plugin:${id}
|
|
1550
|
-
`);
|
|
1551
|
-
for (const template of provisioning.templateDirs ?? []) {
|
|
1552
|
-
const templatePath = toPath(template.path);
|
|
1553
|
-
hash.update(`template:${template.id}:${template.target ?? ""}:${templatePath}
|
|
1554
|
-
`);
|
|
1555
|
-
if (await exists(templatePath)) await hashPath(templatePath, hash);
|
|
1533
|
+
if (!authToken) {
|
|
1534
|
+
if (!warnedDevMode) {
|
|
1535
|
+
warnedDevMode = true;
|
|
1536
|
+
request.log.warn(DEV_MODE_WARNING);
|
|
1537
|
+
opts.onDevModeWarning?.(DEV_MODE_WARNING);
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1556
1540
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1541
|
+
const header = request.headers.authorization;
|
|
1542
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
1543
|
+
reply.code(401).send(
|
|
1544
|
+
errorPayload(
|
|
1545
|
+
ERROR_CODE_AUTH_REQUIRED,
|
|
1546
|
+
"Missing Bearer token"
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
return;
|
|
1564
1550
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const env = {};
|
|
1570
|
-
for (const { provisioning } of contributions) {
|
|
1571
|
-
for (const spec of provisioning.python ?? []) {
|
|
1572
|
-
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
1573
|
-
env[key] = toPath(value);
|
|
1574
|
-
}
|
|
1551
|
+
const candidateToken = header.slice("Bearer ".length);
|
|
1552
|
+
if (candidateToken !== authToken) {
|
|
1553
|
+
reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
|
|
1554
|
+
return;
|
|
1575
1555
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1556
|
+
ensureWorkspaceContext(request, workspaceId, true);
|
|
1557
|
+
};
|
|
1578
1558
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1559
|
+
function createBodyValidator(schema) {
|
|
1560
|
+
return async function validateBody(request, reply) {
|
|
1561
|
+
const parsed = schema.safeParse(request.body);
|
|
1562
|
+
if (!parsed.success) {
|
|
1563
|
+
const firstIssue = parsed.error.issues[0];
|
|
1564
|
+
const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
|
|
1565
|
+
reply.code(400).send(
|
|
1566
|
+
errorPayload(
|
|
1567
|
+
ERROR_CODE_VALIDATION_ERROR,
|
|
1568
|
+
firstIssue?.message ?? "Invalid request body",
|
|
1569
|
+
fieldName || void 0
|
|
1570
|
+
)
|
|
1571
|
+
);
|
|
1572
|
+
return;
|
|
1587
1573
|
}
|
|
1574
|
+
request.body = parsed.data;
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/server/logging.ts
|
|
1579
|
+
var SENSITIVE_KEYS = new Set([
|
|
1580
|
+
"apiKey",
|
|
1581
|
+
"api_key",
|
|
1582
|
+
"token",
|
|
1583
|
+
"secret",
|
|
1584
|
+
"password",
|
|
1585
|
+
"authorization",
|
|
1586
|
+
"cookie",
|
|
1587
|
+
"oidcToken",
|
|
1588
|
+
"accessToken",
|
|
1589
|
+
"refreshToken",
|
|
1590
|
+
"ANTHROPIC_API_KEY",
|
|
1591
|
+
"OPENAI_API_KEY",
|
|
1592
|
+
"VERCEL_OIDC_TOKEN",
|
|
1593
|
+
"VERCEL_TEAM_ID"
|
|
1594
|
+
].map((key) => key.toLowerCase()));
|
|
1595
|
+
function isSensitiveKey(key) {
|
|
1596
|
+
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
1597
|
+
}
|
|
1598
|
+
function redactValue(key, value, seen) {
|
|
1599
|
+
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
1600
|
+
if (value == null || typeof value !== "object") return value;
|
|
1601
|
+
if (value instanceof Date) return value;
|
|
1602
|
+
if (seen.has(value)) return "[Circular]";
|
|
1603
|
+
seen.add(value);
|
|
1604
|
+
if (Array.isArray(value)) {
|
|
1605
|
+
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
1606
|
+
seen.delete(value);
|
|
1607
|
+
return out2;
|
|
1608
|
+
}
|
|
1609
|
+
const out = {};
|
|
1610
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
1611
|
+
out[childKey] = redactValue(childKey, childValue, seen);
|
|
1588
1612
|
}
|
|
1613
|
+
seen.delete(value);
|
|
1614
|
+
return out;
|
|
1589
1615
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
if (uv) await run("uv", ["venv", ".venv"], workspaceRoot);
|
|
1596
|
-
else await run("/usr/bin/python3", ["-m", "venv", ".venv"], workspaceRoot);
|
|
1616
|
+
function redact(fields) {
|
|
1617
|
+
const out = {};
|
|
1618
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1619
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1620
|
+
out[k] = redactValue(k, v, seen);
|
|
1597
1621
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1624
|
+
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
1625
|
+
function createLogger(prefix) {
|
|
1626
|
+
function emit(level, msg, fields) {
|
|
1627
|
+
const entry = {
|
|
1628
|
+
level,
|
|
1629
|
+
prefix,
|
|
1630
|
+
msg,
|
|
1631
|
+
...fields ? redact(fields) : {},
|
|
1632
|
+
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
1633
|
+
};
|
|
1634
|
+
if (level === "error") {
|
|
1635
|
+
console.error(JSON.stringify(entry));
|
|
1636
|
+
} else if (level === "warn") {
|
|
1637
|
+
console.warn(JSON.stringify(entry));
|
|
1605
1638
|
} else {
|
|
1606
|
-
|
|
1607
|
-
if (spec.extraLibs?.length) await run(venvPython, ["-m", "pip", "install", ...spec.extraLibs], workspaceRoot);
|
|
1639
|
+
console.log(JSON.stringify(entry));
|
|
1608
1640
|
}
|
|
1609
1641
|
}
|
|
1642
|
+
return {
|
|
1643
|
+
debug(msg, fields) {
|
|
1644
|
+
if (verbose) emit("debug", msg, fields);
|
|
1645
|
+
},
|
|
1646
|
+
info(msg, fields) {
|
|
1647
|
+
emit("info", msg, fields);
|
|
1648
|
+
},
|
|
1649
|
+
warn(msg, fields) {
|
|
1650
|
+
emit("warn", msg, fields);
|
|
1651
|
+
},
|
|
1652
|
+
error(msg, fields) {
|
|
1653
|
+
emit("error", msg, fields);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1610
1656
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1620
|
-
throw new Error(`Invalid provisioning env key: ${key}`);
|
|
1621
|
-
}
|
|
1657
|
+
|
|
1658
|
+
// src/server/http/routes/file.ts
|
|
1659
|
+
var log = createLogger("boring/workspace-settings");
|
|
1660
|
+
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
1661
|
+
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
1662
|
+
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
1663
|
+
function defaultWorkspaceSettings() {
|
|
1664
|
+
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
1622
1665
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (hasPython && !await exists(join3(workspaceRoot, ".venv", "bin", "python"))) return false;
|
|
1626
|
-
return true;
|
|
1666
|
+
function isPathValidationError(err) {
|
|
1667
|
+
return err instanceof Error && typeof err.reason === "string";
|
|
1627
1668
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
assertEnvKey(key);
|
|
1634
|
-
return `export ${key}=${bashSingleQuote(value)}`;
|
|
1635
|
-
}).join("\n");
|
|
1636
|
-
const base = `#!/usr/bin/env bash
|
|
1637
|
-
set -euo pipefail
|
|
1638
|
-
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
1639
|
-
WORKSPACE_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
1640
|
-
export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
|
|
1641
|
-
${exports}
|
|
1642
|
-
VENV_BIN="$WORKSPACE_ROOT/.venv/bin"
|
|
1643
|
-
`;
|
|
1644
|
-
await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
|
|
1645
|
-
`);
|
|
1646
|
-
await writeExecutable(join3(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
|
|
1647
|
-
`);
|
|
1648
|
-
await writeExecutable(join3(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
1649
|
-
`);
|
|
1650
|
-
await writeExecutable(join3(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
1651
|
-
`);
|
|
1652
|
-
if (await exists(venvBin)) {
|
|
1653
|
-
for (const entry of await readdir2(venvBin)) {
|
|
1654
|
-
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
1655
|
-
const full = join3(venvBin, entry);
|
|
1656
|
-
const info = await stat3(full).catch(() => null);
|
|
1657
|
-
if (!info?.isFile()) continue;
|
|
1658
|
-
await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
1659
|
-
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
1660
|
-
case "$SHEBANG" in
|
|
1661
|
-
*python*) exec "$VENV_BIN/python" "$TARGET" "$@" ;;
|
|
1662
|
-
esac
|
|
1663
|
-
exec "$TARGET" "$@"
|
|
1664
|
-
`);
|
|
1665
|
-
}
|
|
1669
|
+
function classifyError(err, reply, subject) {
|
|
1670
|
+
if (isPathValidationError(err)) {
|
|
1671
|
+
return reply.code(403).send({
|
|
1672
|
+
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
1673
|
+
});
|
|
1666
1674
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}) {
|
|
1674
|
-
const active = [];
|
|
1675
|
-
for (const contribution of contributions ?? []) {
|
|
1676
|
-
if (contribution.provisioning) active.push({ id: contribution.id, provisioning: contribution.provisioning });
|
|
1675
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
1676
|
+
const code = err?.code;
|
|
1677
|
+
if (code === "EPERM" || message.toLowerCase().includes("traversal") || message.includes("EPERM")) {
|
|
1678
|
+
return reply.code(403).send({
|
|
1679
|
+
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
1680
|
+
});
|
|
1677
1681
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
const binDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
1683
|
-
if (!force && await exists(markerPath)) {
|
|
1684
|
-
try {
|
|
1685
|
-
const marker = JSON.parse(await readFile4(markerPath, "utf8"));
|
|
1686
|
-
if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
|
|
1687
|
-
await writeShims(workspaceRoot, env);
|
|
1688
|
-
return { fingerprint: hash, changed: false, env, binDir };
|
|
1689
|
-
}
|
|
1690
|
-
} catch {
|
|
1691
|
-
}
|
|
1682
|
+
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
1683
|
+
return reply.code(404).send({
|
|
1684
|
+
error: { code: ERROR_CODE_NOT_FOUND, message: `${subject} not found` }
|
|
1685
|
+
});
|
|
1692
1686
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
return {
|
|
1687
|
+
if (code === "EEXIST" || message.includes("EEXIST")) {
|
|
1688
|
+
return reply.code(409).send({
|
|
1689
|
+
error: { code: ERROR_CODE_ALREADY_EXISTS, message: `${subject} already exists` }
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
return reply.code(500).send({
|
|
1693
|
+
error: { code: ERROR_CODE_INTERNAL, message }
|
|
1694
|
+
});
|
|
1699
1695
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1696
|
+
function requireStringParam(value, field, reply) {
|
|
1697
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1698
|
+
reply.code(400).send({
|
|
1699
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `${field} is required`, field }
|
|
1700
|
+
});
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
if (value.includes("\0")) {
|
|
1704
|
+
reply.code(400).send({
|
|
1705
|
+
error: { code: ERROR_CODE_INVALID_PATH, message: "null bytes not allowed", field }
|
|
1706
|
+
});
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
return value;
|
|
1710
1710
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
return void 0;
|
|
1721
|
-
}
|
|
1722
|
-
entries.delete(key);
|
|
1723
|
-
entries.set(key, entry);
|
|
1724
|
-
return entry.value;
|
|
1725
|
-
},
|
|
1726
|
-
set(key, value) {
|
|
1727
|
-
entries.delete(key);
|
|
1728
|
-
entries.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
1729
|
-
while (entries.size > maxEntries) {
|
|
1730
|
-
const oldest = entries.keys().next().value;
|
|
1731
|
-
if (!oldest) return;
|
|
1732
|
-
entries.delete(oldest);
|
|
1711
|
+
function parseWorkspaceSettings(raw) {
|
|
1712
|
+
try {
|
|
1713
|
+
const parsed = JSON.parse(raw);
|
|
1714
|
+
const dir = parsed?.markdown?.imageUploadDir;
|
|
1715
|
+
return {
|
|
1716
|
+
...parsed,
|
|
1717
|
+
markdown: {
|
|
1718
|
+
...parsed.markdown ?? {},
|
|
1719
|
+
imageUploadDir: typeof dir === "string" && dir.trim() ? dir.trim() : DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR
|
|
1733
1720
|
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1721
|
+
};
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
log.warn("failed to parse .boring/settings \u2014 falling back to defaults", {
|
|
1724
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1725
|
+
});
|
|
1726
|
+
return defaultWorkspaceSettings();
|
|
1727
|
+
}
|
|
1739
1728
|
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1729
|
+
async function readWorkspaceSettings(workspace) {
|
|
1730
|
+
try {
|
|
1731
|
+
return parseWorkspaceSettings(await workspace.readFile(BORING_SETTINGS_PATH));
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
const code = error?.code;
|
|
1734
|
+
if (code === "ENOENT") return defaultWorkspaceSettings();
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1746
1737
|
}
|
|
1747
|
-
function
|
|
1748
|
-
|
|
1738
|
+
function normalizeUploadDir(value) {
|
|
1739
|
+
if (typeof value !== "string") return null;
|
|
1740
|
+
const dir = value.trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
1741
|
+
if (!dir || dir.includes("\0") || dir.startsWith("/") || dir.split("/").includes("..")) return null;
|
|
1742
|
+
return dir.replace(/\/+$/, "");
|
|
1749
1743
|
}
|
|
1750
|
-
function
|
|
1751
|
-
|
|
1744
|
+
function extForUpload(filename, contentType) {
|
|
1745
|
+
const fromName = extname(filename).toLowerCase().replace(/^\./, "");
|
|
1746
|
+
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
1747
|
+
if (contentType === "image/jpeg") return "jpg";
|
|
1748
|
+
if (contentType === "image/png") return "png";
|
|
1749
|
+
if (contentType === "image/gif") return "gif";
|
|
1750
|
+
if (contentType === "image/webp") return "webp";
|
|
1751
|
+
if (contentType === "image/svg+xml") return "svg";
|
|
1752
|
+
return "bin";
|
|
1752
1753
|
}
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
]);
|
|
1759
|
-
if ((result.exitCode ?? 1) !== 0) {
|
|
1760
|
-
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
1761
|
-
}
|
|
1762
|
-
return JSON.parse(out);
|
|
1754
|
+
function basenameForUpload(filename) {
|
|
1755
|
+
const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
|
|
1756
|
+
const withoutExt = base.replace(/\.[^.]*$/, "");
|
|
1757
|
+
const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1758
|
+
return safe || "image";
|
|
1763
1759
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1760
|
+
function markdownUrlFor(sourcePath, assetPath) {
|
|
1761
|
+
if (!sourcePath) return assetPath;
|
|
1762
|
+
const fromDir = dirname4(sourcePath.replace(/\\/g, "/"));
|
|
1763
|
+
const rel = relative4(fromDir === "." ? "" : fromDir, assetPath);
|
|
1764
|
+
return rel && !rel.startsWith(".") ? rel : rel || assetPath;
|
|
1765
|
+
}
|
|
1766
|
+
function contentTypeForPath(path4) {
|
|
1767
|
+
switch (extname(path4).toLowerCase()) {
|
|
1768
|
+
case ".avif":
|
|
1769
|
+
return "image/avif";
|
|
1770
|
+
case ".gif":
|
|
1771
|
+
return "image/gif";
|
|
1772
|
+
case ".jpg":
|
|
1773
|
+
case ".jpeg":
|
|
1774
|
+
return "image/jpeg";
|
|
1775
|
+
case ".png":
|
|
1776
|
+
return "image/png";
|
|
1777
|
+
case ".svg":
|
|
1778
|
+
return "image/svg+xml";
|
|
1779
|
+
case ".webp":
|
|
1780
|
+
return "image/webp";
|
|
1781
|
+
case ".css":
|
|
1782
|
+
return "text/css; charset=utf-8";
|
|
1783
|
+
case ".html":
|
|
1784
|
+
case ".htm":
|
|
1785
|
+
return "text/html; charset=utf-8";
|
|
1786
|
+
case ".js":
|
|
1787
|
+
return "text/javascript; charset=utf-8";
|
|
1788
|
+
case ".mjs":
|
|
1789
|
+
return "text/javascript; charset=utf-8";
|
|
1790
|
+
case ".pdf":
|
|
1791
|
+
return "application/pdf";
|
|
1792
|
+
default:
|
|
1793
|
+
return "application/octet-stream";
|
|
1769
1794
|
}
|
|
1770
1795
|
}
|
|
1771
|
-
function
|
|
1796
|
+
function fileRoutes(app, opts, done) {
|
|
1797
|
+
async function resolveWorkspace(request) {
|
|
1798
|
+
if (opts.getWorkspace) return await opts.getWorkspace(request);
|
|
1799
|
+
if (opts.workspace) return opts.workspace;
|
|
1800
|
+
throw new Error("file route requires workspace or getWorkspace");
|
|
1801
|
+
}
|
|
1802
|
+
app.get("/api/v1/files/raw", async (request, reply) => {
|
|
1803
|
+
const query = request.query;
|
|
1804
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1805
|
+
if (path4 === null) return;
|
|
1806
|
+
try {
|
|
1807
|
+
const workspace = await resolveWorkspace(request);
|
|
1808
|
+
if (!workspace.readBinaryFile) {
|
|
1809
|
+
return reply.code(501).send({
|
|
1810
|
+
error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
const stat7 = await workspace.stat(path4);
|
|
1814
|
+
if (stat7.kind !== "file") {
|
|
1815
|
+
return reply.code(400).send({
|
|
1816
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const bytes = await workspace.readBinaryFile(path4);
|
|
1820
|
+
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
return classifyError(err, reply, "file");
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
app.get("/api/v1/files", async (request, reply) => {
|
|
1826
|
+
const query = request.query;
|
|
1827
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1828
|
+
if (path4 === null) return;
|
|
1829
|
+
try {
|
|
1830
|
+
const workspace = await resolveWorkspace(request);
|
|
1831
|
+
if (workspace.readFileWithStat) {
|
|
1832
|
+
const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
|
|
1833
|
+
return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
|
|
1834
|
+
}
|
|
1835
|
+
const content = await workspace.readFile(path4);
|
|
1836
|
+
const stat7 = await workspace.stat(path4);
|
|
1837
|
+
return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
return classifyError(err, reply, "file");
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
app.post("/api/v1/files", async (request, reply) => {
|
|
1843
|
+
const body = request.body;
|
|
1844
|
+
const path4 = requireStringParam(body?.path, "path", reply);
|
|
1845
|
+
if (path4 === null) return;
|
|
1846
|
+
if (typeof body.content !== "string") {
|
|
1847
|
+
return reply.code(400).send({
|
|
1848
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "content is required", field: "content" }
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
const expectedMtimeMs = typeof body.expectedMtimeMs === "number" ? body.expectedMtimeMs : null;
|
|
1852
|
+
try {
|
|
1853
|
+
const workspace = await resolveWorkspace(request);
|
|
1854
|
+
if (expectedMtimeMs !== null) {
|
|
1855
|
+
try {
|
|
1856
|
+
const current = await workspace.stat(path4);
|
|
1857
|
+
if (current.kind === "file" && current.mtimeMs !== expectedMtimeMs) {
|
|
1858
|
+
return reply.code(409).send({
|
|
1859
|
+
error: {
|
|
1860
|
+
code: ERROR_CODE_CONFLICT,
|
|
1861
|
+
message: "file has been modified since last read",
|
|
1862
|
+
currentMtimeMs: current.mtimeMs,
|
|
1863
|
+
expectedMtimeMs
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
} catch (statErr) {
|
|
1868
|
+
const code = statErr?.code;
|
|
1869
|
+
if (code === "ENOENT") {
|
|
1870
|
+
return reply.code(409).send({
|
|
1871
|
+
error: {
|
|
1872
|
+
code: ERROR_CODE_CONFLICT,
|
|
1873
|
+
message: "file no longer exists",
|
|
1874
|
+
expectedMtimeMs
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
throw statErr;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (body.createDirs) {
|
|
1882
|
+
const dir = path4.includes("/") ? path4.slice(0, path4.lastIndexOf("/")) : void 0;
|
|
1883
|
+
if (dir) await workspace.mkdir(dir, { recursive: true });
|
|
1884
|
+
}
|
|
1885
|
+
const content = body.content;
|
|
1886
|
+
const stat7 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
|
|
1887
|
+
await workspace.writeFile(path4, content);
|
|
1888
|
+
return await workspace.stat(path4);
|
|
1889
|
+
})();
|
|
1890
|
+
return { ok: true, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
return classifyError(err, reply, "file");
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
app.post("/api/v1/files/upload", async (request, reply) => {
|
|
1896
|
+
const body = request.body;
|
|
1897
|
+
const filename = requireStringParam(body?.filename, "filename", reply);
|
|
1898
|
+
if (filename === null) return;
|
|
1899
|
+
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
1900
|
+
if (contentBase64 === null) return;
|
|
1901
|
+
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
1902
|
+
if (!contentType.startsWith("image/")) {
|
|
1903
|
+
return reply.code(400).send({
|
|
1904
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
const workspace = await resolveWorkspace(request);
|
|
1909
|
+
const settings = await readWorkspaceSettings(workspace);
|
|
1910
|
+
const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
|
|
1911
|
+
const ext = extForUpload(filename, contentType);
|
|
1912
|
+
const base = basenameForUpload(filename);
|
|
1913
|
+
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1914
|
+
const path4 = `${dir}/${base}-${unique}.${ext}`;
|
|
1915
|
+
const estimatedBytes = Math.ceil(contentBase64.length * 0.75);
|
|
1916
|
+
if (estimatedBytes === 0 || estimatedBytes > MAX_UPLOAD_BYTES) {
|
|
1917
|
+
return reply.code(400).send({
|
|
1918
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `upload must be 1 byte to ${MAX_UPLOAD_BYTES} bytes`, field: "contentBase64" }
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
1922
|
+
await workspace.mkdir(dir, { recursive: true });
|
|
1923
|
+
const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
1924
|
+
if (!workspace.writeBinaryFile) {
|
|
1925
|
+
throw new Error("workspace does not support binary uploads");
|
|
1926
|
+
}
|
|
1927
|
+
await workspace.writeBinaryFile(path4, bytes);
|
|
1928
|
+
return await workspace.stat(path4);
|
|
1929
|
+
})();
|
|
1930
|
+
const MAX_SOURCE_PATH = 1024;
|
|
1931
|
+
const rawSourcePath = body.sourcePath;
|
|
1932
|
+
const sourcePath = typeof rawSourcePath === "string" && rawSourcePath.length > 0 && rawSourcePath.length <= MAX_SOURCE_PATH && !rawSourcePath.includes("\0") ? rawSourcePath : null;
|
|
1933
|
+
return {
|
|
1934
|
+
ok: true,
|
|
1935
|
+
path: path4,
|
|
1936
|
+
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
1937
|
+
mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
|
|
1938
|
+
};
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
return classifyError(err, reply, "upload");
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
app.get("/api/v1/workspace-settings", async (request, reply) => {
|
|
1944
|
+
try {
|
|
1945
|
+
const workspace = await resolveWorkspace(request);
|
|
1946
|
+
return { settings: await readWorkspaceSettings(workspace) };
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
return classifyError(err, reply, "settings");
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
app.put("/api/v1/workspace-settings", async (request, reply) => {
|
|
1952
|
+
const body = request.body;
|
|
1953
|
+
const incoming = body?.settings ?? body;
|
|
1954
|
+
const markdown = incoming.markdown ?? {};
|
|
1955
|
+
const imageUploadDir = normalizeUploadDir(markdown.imageUploadDir);
|
|
1956
|
+
if (!imageUploadDir) {
|
|
1957
|
+
return reply.code(400).send({
|
|
1958
|
+
error: { code: ERROR_CODE_INVALID_PATH, message: "markdown.imageUploadDir must be a relative workspace path", field: "markdown.imageUploadDir" }
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const workspace = await resolveWorkspace(request);
|
|
1963
|
+
const current = await readWorkspaceSettings(workspace);
|
|
1964
|
+
const next = {
|
|
1965
|
+
...current,
|
|
1966
|
+
markdown: {
|
|
1967
|
+
...current.markdown ?? {},
|
|
1968
|
+
imageUploadDir
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
await workspace.mkdir(".boring", { recursive: true });
|
|
1972
|
+
await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
|
|
1973
|
+
`);
|
|
1974
|
+
return { settings: next };
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
return classifyError(err, reply, "settings");
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
app.delete("/api/v1/files", async (request, reply) => {
|
|
1980
|
+
const query = request.query;
|
|
1981
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1982
|
+
if (path4 === null) return;
|
|
1983
|
+
try {
|
|
1984
|
+
const workspace = await resolveWorkspace(request);
|
|
1985
|
+
await workspace.unlink(path4);
|
|
1986
|
+
return { ok: true };
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
return classifyError(err, reply, "file");
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
app.post("/api/v1/files/move", async (request, reply) => {
|
|
1992
|
+
const body = request.body;
|
|
1993
|
+
const from = requireStringParam(body?.from, "from", reply);
|
|
1994
|
+
if (from === null) return;
|
|
1995
|
+
const to = requireStringParam(body?.to, "to", reply);
|
|
1996
|
+
if (to === null) return;
|
|
1997
|
+
try {
|
|
1998
|
+
const workspace = await resolveWorkspace(request);
|
|
1999
|
+
await workspace.rename(from, to);
|
|
2000
|
+
return { ok: true };
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
return classifyError(err, reply, "file");
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
app.post("/api/v1/dirs", async (request, reply) => {
|
|
2006
|
+
const body = request.body;
|
|
2007
|
+
const path4 = requireStringParam(body?.path, "path", reply);
|
|
2008
|
+
if (path4 === null) return;
|
|
2009
|
+
const recursive = body.recursive === true;
|
|
2010
|
+
try {
|
|
2011
|
+
const workspace = await resolveWorkspace(request);
|
|
2012
|
+
await workspace.mkdir(path4, { recursive });
|
|
2013
|
+
return { ok: true };
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
return classifyError(err, reply, "directory");
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
app.get("/api/v1/stat", async (request, reply) => {
|
|
2019
|
+
const query = request.query;
|
|
2020
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
2021
|
+
if (path4 === null) return;
|
|
2022
|
+
try {
|
|
2023
|
+
const workspace = await resolveWorkspace(request);
|
|
2024
|
+
const stat7 = await workspace.stat(path4);
|
|
2025
|
+
return stat7;
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
return classifyError(err, reply, "path");
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
done();
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/server/workspace/provisionRuntime.ts
|
|
2034
|
+
import { createHash as createHash3 } from "crypto";
|
|
2035
|
+
import { constants as constants2 } from "fs";
|
|
2036
|
+
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
|
|
2037
|
+
import { dirname as dirname5, join as join3, resolve as resolve3 } from "path";
|
|
2038
|
+
import { fileURLToPath } from "url";
|
|
2039
|
+
import { execFile } from "child_process";
|
|
2040
|
+
import { promisify } from "util";
|
|
2041
|
+
var execFileAsync = promisify(execFile);
|
|
2042
|
+
var PROVISIONING_VERSION = 1;
|
|
2043
|
+
function toPath(value) {
|
|
2044
|
+
return value instanceof URL ? fileURLToPath(value) : value;
|
|
2045
|
+
}
|
|
2046
|
+
async function exists(path4) {
|
|
2047
|
+
try {
|
|
2048
|
+
await access2(path4, constants2.F_OK);
|
|
2049
|
+
return true;
|
|
2050
|
+
} catch {
|
|
2051
|
+
return false;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
async function run(cmd, args, cwd) {
|
|
2055
|
+
await execFileAsync(cmd, args, {
|
|
2056
|
+
cwd,
|
|
2057
|
+
env: process.env,
|
|
2058
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
2059
|
+
timeout: 5 * 60 * 1e3
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
async function commandExists(cmd) {
|
|
2063
|
+
try {
|
|
2064
|
+
await execFileAsync(cmd, ["--version"], { maxBuffer: 1024 * 1024 });
|
|
2065
|
+
return true;
|
|
2066
|
+
} catch {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function hashPath(path4, hash) {
|
|
2071
|
+
const info = await stat3(path4);
|
|
2072
|
+
if (info.isDirectory()) {
|
|
2073
|
+
const entries = (await readdir2(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
|
|
2074
|
+
for (const entry of entries) {
|
|
2075
|
+
hash.update(`dir:${entry}
|
|
2076
|
+
`);
|
|
2077
|
+
await hashPath(join3(path4, entry), hash);
|
|
2078
|
+
}
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (!info.isFile()) return;
|
|
2082
|
+
hash.update(`file:${path4}
|
|
2083
|
+
`);
|
|
2084
|
+
hash.update(await readFile4(path4));
|
|
2085
|
+
}
|
|
2086
|
+
async function fingerprint(contributions) {
|
|
2087
|
+
const hash = createHash3("sha256");
|
|
2088
|
+
hash.update(JSON.stringify({ v: PROVISIONING_VERSION }));
|
|
2089
|
+
for (const { id, provisioning } of contributions) {
|
|
2090
|
+
hash.update(`plugin:${id}
|
|
2091
|
+
`);
|
|
2092
|
+
for (const template of provisioning.templateDirs ?? []) {
|
|
2093
|
+
const templatePath = toPath(template.path);
|
|
2094
|
+
hash.update(`template:${template.id}:${template.target ?? ""}:${templatePath}
|
|
2095
|
+
`);
|
|
2096
|
+
if (await exists(templatePath)) await hashPath(templatePath, hash);
|
|
2097
|
+
}
|
|
2098
|
+
for (const spec of provisioning.python ?? []) {
|
|
2099
|
+
const projectFile = toPath(spec.projectFile);
|
|
2100
|
+
hash.update(`python:${spec.id}:${projectFile}
|
|
2101
|
+
`);
|
|
2102
|
+
hash.update(JSON.stringify(spec.extraLibs ?? []));
|
|
2103
|
+
hash.update(JSON.stringify(Object.fromEntries(Object.entries(spec.env ?? {}).map(([k, v]) => [k, String(v)]))));
|
|
2104
|
+
if (await exists(projectFile)) await hashPath(dirname5(projectFile), hash);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2108
|
+
}
|
|
2109
|
+
function collectEnv(contributions) {
|
|
2110
|
+
const env = {};
|
|
2111
|
+
for (const { provisioning } of contributions) {
|
|
2112
|
+
for (const spec of provisioning.python ?? []) {
|
|
2113
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
2114
|
+
env[key] = toPath(value);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return env;
|
|
2119
|
+
}
|
|
2120
|
+
async function seedTemplates(workspaceRoot, contributions) {
|
|
2121
|
+
for (const { provisioning } of contributions) {
|
|
2122
|
+
for (const template of provisioning.templateDirs ?? []) {
|
|
2123
|
+
await cp(toPath(template.path), resolve3(workspaceRoot, template.target ?? "."), {
|
|
2124
|
+
recursive: true,
|
|
2125
|
+
force: false,
|
|
2126
|
+
errorOnExist: false
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
async function ensurePython(workspaceRoot, specs) {
|
|
2132
|
+
if (specs.length === 0) return;
|
|
2133
|
+
const venvPython = join3(workspaceRoot, ".venv", "bin", "python");
|
|
2134
|
+
const uv = await commandExists("uv");
|
|
2135
|
+
if (!await exists(venvPython)) {
|
|
2136
|
+
if (uv) await run("uv", ["venv", ".venv"], workspaceRoot);
|
|
2137
|
+
else await run("/usr/bin/python3", ["-m", "venv", ".venv"], workspaceRoot);
|
|
2138
|
+
}
|
|
2139
|
+
for (const spec of specs) {
|
|
2140
|
+
const projectDir = dirname5(toPath(spec.projectFile));
|
|
2141
|
+
if (uv) {
|
|
2142
|
+
await run("uv", ["pip", "install", "--python", venvPython, projectDir], workspaceRoot);
|
|
2143
|
+
if (spec.extraLibs?.length) {
|
|
2144
|
+
await run("uv", ["pip", "install", "--python", venvPython, ...spec.extraLibs], workspaceRoot);
|
|
2145
|
+
}
|
|
2146
|
+
} else {
|
|
2147
|
+
await run(venvPython, ["-m", "pip", "install", projectDir], workspaceRoot);
|
|
2148
|
+
if (spec.extraLibs?.length) await run(venvPython, ["-m", "pip", "install", ...spec.extraLibs], workspaceRoot);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
async function writeExecutable(path4, body) {
|
|
2153
|
+
await writeFile4(path4, body, "utf8");
|
|
2154
|
+
await chmod3(path4, 493);
|
|
2155
|
+
}
|
|
2156
|
+
function bashSingleQuote(value) {
|
|
2157
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
2158
|
+
}
|
|
2159
|
+
function assertEnvKey(key) {
|
|
2160
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
2161
|
+
throw new Error(`Invalid provisioning env key: ${key}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
async function isRuntimeMaterialized(workspaceRoot, contributions) {
|
|
2165
|
+
const hasPython = contributions.some(({ provisioning }) => (provisioning.python ?? []).length > 0);
|
|
2166
|
+
if (hasPython && !await exists(join3(workspaceRoot, ".venv", "bin", "python"))) return false;
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
async function writeShims(workspaceRoot, env) {
|
|
2170
|
+
const shimDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
2171
|
+
const venvBin = join3(workspaceRoot, ".venv", "bin");
|
|
2172
|
+
await mkdir4(shimDir, { recursive: true });
|
|
2173
|
+
const exports = Object.entries(env).map(([key, value]) => {
|
|
2174
|
+
assertEnvKey(key);
|
|
2175
|
+
return `export ${key}=${bashSingleQuote(value)}`;
|
|
2176
|
+
}).join("\n");
|
|
2177
|
+
const base = `#!/usr/bin/env bash
|
|
2178
|
+
set -euo pipefail
|
|
2179
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
2180
|
+
WORKSPACE_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
2181
|
+
export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
|
|
2182
|
+
${exports}
|
|
2183
|
+
VENV_BIN="$WORKSPACE_ROOT/.venv/bin"
|
|
2184
|
+
`;
|
|
2185
|
+
await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
|
|
2186
|
+
`);
|
|
2187
|
+
await writeExecutable(join3(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
|
|
2188
|
+
`);
|
|
2189
|
+
await writeExecutable(join3(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
2190
|
+
`);
|
|
2191
|
+
await writeExecutable(join3(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
2192
|
+
`);
|
|
2193
|
+
if (await exists(venvBin)) {
|
|
2194
|
+
for (const entry of await readdir2(venvBin)) {
|
|
2195
|
+
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
2196
|
+
const full = join3(venvBin, entry);
|
|
2197
|
+
const info = await stat3(full).catch(() => null);
|
|
2198
|
+
if (!info?.isFile()) continue;
|
|
2199
|
+
await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
2200
|
+
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
2201
|
+
case "$SHEBANG" in
|
|
2202
|
+
*python*) exec "$VENV_BIN/python" "$TARGET" "$@" ;;
|
|
2203
|
+
esac
|
|
2204
|
+
exec "$TARGET" "$@"
|
|
2205
|
+
`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return shimDir;
|
|
2209
|
+
}
|
|
2210
|
+
async function provisionRuntimeWorkspace({
|
|
2211
|
+
workspaceRoot,
|
|
2212
|
+
contributions,
|
|
2213
|
+
force = false
|
|
2214
|
+
}) {
|
|
2215
|
+
const active = [];
|
|
2216
|
+
for (const contribution of contributions ?? []) {
|
|
2217
|
+
if (contribution.provisioning) active.push({ id: contribution.id, provisioning: contribution.provisioning });
|
|
2218
|
+
}
|
|
2219
|
+
await mkdir4(workspaceRoot, { recursive: true });
|
|
2220
|
+
const env = collectEnv(active);
|
|
2221
|
+
const hash = await fingerprint(active);
|
|
2222
|
+
const markerPath = join3(workspaceRoot, ".boring-agent", "provisioning.json");
|
|
2223
|
+
const binDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
2224
|
+
if (!force && await exists(markerPath)) {
|
|
2225
|
+
try {
|
|
2226
|
+
const marker = JSON.parse(await readFile4(markerPath, "utf8"));
|
|
2227
|
+
if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
|
|
2228
|
+
await writeShims(workspaceRoot, env);
|
|
2229
|
+
return { fingerprint: hash, changed: false, env, binDir };
|
|
2230
|
+
}
|
|
2231
|
+
} catch {
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
await seedTemplates(workspaceRoot, active);
|
|
2235
|
+
await ensurePython(workspaceRoot, active.flatMap(({ provisioning }) => provisioning.python ?? []));
|
|
2236
|
+
const actualBinDir = await writeShims(workspaceRoot, env);
|
|
2237
|
+
await mkdir4(dirname5(markerPath), { recursive: true });
|
|
2238
|
+
await writeFile4(markerPath, JSON.stringify({ v: PROVISIONING_VERSION, fingerprint: hash }, null, 2), "utf8");
|
|
2239
|
+
return { fingerprint: hash, changed: true, env, binDir: actualBinDir };
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/server/workspace/createVercelSandboxWorkspace.ts
|
|
2243
|
+
var VERCEL_SANDBOX_REMOTE_ROOT = "/vercel/sandbox";
|
|
2244
|
+
var VERCEL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
2245
|
+
var CACHE_TTL_MS = 15e3;
|
|
2246
|
+
var CACHE_MAX_ENTRIES = 512;
|
|
2247
|
+
var MAX_INLINE_WRITE_BYTES = 128 * 1024;
|
|
2248
|
+
var metadataInvalidators = /* @__PURE__ */ new WeakMap();
|
|
2249
|
+
function toSandboxPath(relPath) {
|
|
2250
|
+
return validatePath(VERCEL_SANDBOX_REMOTE_ROOT, relPath);
|
|
2251
|
+
}
|
|
2252
|
+
function createTimedLruCache(ttlMs, maxEntries) {
|
|
2253
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2254
|
+
return {
|
|
2255
|
+
get(key) {
|
|
2256
|
+
const now = Date.now();
|
|
2257
|
+
const entry = entries.get(key);
|
|
2258
|
+
if (!entry) return void 0;
|
|
2259
|
+
if (entry.expiresAt <= now) {
|
|
2260
|
+
entries.delete(key);
|
|
2261
|
+
return void 0;
|
|
2262
|
+
}
|
|
2263
|
+
entries.delete(key);
|
|
2264
|
+
entries.set(key, entry);
|
|
2265
|
+
return entry.value;
|
|
2266
|
+
},
|
|
2267
|
+
set(key, value) {
|
|
2268
|
+
entries.delete(key);
|
|
2269
|
+
entries.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
2270
|
+
while (entries.size > maxEntries) {
|
|
2271
|
+
const oldest = entries.keys().next().value;
|
|
2272
|
+
if (!oldest) return;
|
|
2273
|
+
entries.delete(oldest);
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
clear() {
|
|
2277
|
+
entries.clear();
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function cloneStat(stat7) {
|
|
2282
|
+
return {
|
|
2283
|
+
size: stat7.size,
|
|
2284
|
+
mtimeMs: stat7.mtimeMs,
|
|
2285
|
+
kind: stat7.kind
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function cloneEntries(entries) {
|
|
2289
|
+
return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
|
|
2290
|
+
}
|
|
2291
|
+
function shellQuote2(value) {
|
|
2292
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2293
|
+
}
|
|
2294
|
+
async function runJson(sandbox, script) {
|
|
2295
|
+
const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
|
|
2296
|
+
const [out, err] = await Promise.all([
|
|
2297
|
+
result.stdout?.() ?? Promise.resolve(""),
|
|
2298
|
+
result.stderr?.() ?? Promise.resolve("")
|
|
2299
|
+
]);
|
|
2300
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
2301
|
+
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
2302
|
+
}
|
|
2303
|
+
return JSON.parse(out);
|
|
2304
|
+
}
|
|
2305
|
+
async function runShell(sandbox, script) {
|
|
2306
|
+
const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
|
|
2307
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
2308
|
+
const err = await (result.stderr?.() ?? Promise.resolve(""));
|
|
2309
|
+
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function registerMetadataInvalidator(sandbox, invalidate) {
|
|
1772
2313
|
const existing = metadataInvalidators.get(sandbox);
|
|
1773
2314
|
if (existing) {
|
|
1774
2315
|
existing.add(invalidate);
|
|
@@ -2128,7 +2669,7 @@ function createServerFileSearch(workspace, sandbox) {
|
|
|
2128
2669
|
|
|
2129
2670
|
// src/server/workspace/provision.ts
|
|
2130
2671
|
import { cp as cp2, mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
2131
|
-
import { dirname as
|
|
2672
|
+
import { dirname as dirname6, join as join4 } from "path";
|
|
2132
2673
|
var PROVISION_MARKER_REL_PATH = ".boring-agent/provisioned";
|
|
2133
2674
|
async function fileExists(targetPath) {
|
|
2134
2675
|
try {
|
|
@@ -2157,7 +2698,7 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
2157
2698
|
{ cause: error }
|
|
2158
2699
|
);
|
|
2159
2700
|
}
|
|
2160
|
-
await mkdir5(
|
|
2701
|
+
await mkdir5(dirname6(markerPath), { recursive: true });
|
|
2161
2702
|
await writeFile5(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
|
|
2162
2703
|
}
|
|
2163
2704
|
|
|
@@ -2364,7 +2905,7 @@ import { createGzip } from "zlib";
|
|
|
2364
2905
|
import { put } from "@vercel/blob";
|
|
2365
2906
|
var urlCache = /* @__PURE__ */ new Map();
|
|
2366
2907
|
var LOG_PREFIX = "[template-tarball]";
|
|
2367
|
-
function
|
|
2908
|
+
function log2(msg, meta = {}) {
|
|
2368
2909
|
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
|
|
2369
2910
|
process.stderr.write(`${LOG_PREFIX} ${msg}${metaStr}
|
|
2370
2911
|
`);
|
|
@@ -2447,11 +2988,11 @@ async function packageTemplate(templatePath, opts = {}) {
|
|
|
2447
2988
|
const hash = computeTemplateHash(files);
|
|
2448
2989
|
const cached = urlCache.get(hash);
|
|
2449
2990
|
if (cached) {
|
|
2450
|
-
|
|
2991
|
+
log2("cache hit", { hash, fileCount: files.length });
|
|
2451
2992
|
return { url: cached, hash };
|
|
2452
2993
|
}
|
|
2453
2994
|
const tarball = await buildTarGz(files);
|
|
2454
|
-
|
|
2995
|
+
log2("tarball built", {
|
|
2455
2996
|
hash,
|
|
2456
2997
|
fileCount: files.length,
|
|
2457
2998
|
sizeBytes: tarball.length,
|
|
@@ -2460,7 +3001,7 @@ async function packageTemplate(templatePath, opts = {}) {
|
|
|
2460
3001
|
const upload = opts.uploadFn ?? defaultUpload;
|
|
2461
3002
|
const url = await upload(hash, tarball);
|
|
2462
3003
|
urlCache.set(hash, url);
|
|
2463
|
-
|
|
3004
|
+
log2("uploaded", { hash, url, totalMs: Date.now() - startMs });
|
|
2464
3005
|
return { url, hash };
|
|
2465
3006
|
}
|
|
2466
3007
|
|
|
@@ -2736,101 +3277,21 @@ import Fastify from "fastify";
|
|
|
2736
3277
|
import { basename as basename2 } from "path";
|
|
2737
3278
|
|
|
2738
3279
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
3280
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2739
3281
|
import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
|
|
2740
|
-
import { extname, join as join6 } from "path";
|
|
3282
|
+
import { extname as extname2, join as join6 } from "path";
|
|
2741
3283
|
import {
|
|
2742
|
-
createAgentSession,
|
|
2743
|
-
SessionManager,
|
|
2744
|
-
AuthStorage,
|
|
2745
|
-
ModelRegistry,
|
|
2746
|
-
DefaultResourceLoader,
|
|
2747
|
-
SettingsManager,
|
|
2748
|
-
getAgentDir
|
|
2749
|
-
|
|
2750
|
-
} from "@mariozechner/pi-coding-agent";
|
|
2751
|
-
|
|
2752
|
-
// src/server/logging.ts
|
|
2753
|
-
var SENSITIVE_KEYS = new Set([
|
|
2754
|
-
"apiKey",
|
|
2755
|
-
"api_key",
|
|
2756
|
-
"token",
|
|
2757
|
-
"secret",
|
|
2758
|
-
"password",
|
|
2759
|
-
"authorization",
|
|
2760
|
-
"cookie",
|
|
2761
|
-
"oidcToken",
|
|
2762
|
-
"accessToken",
|
|
2763
|
-
"refreshToken",
|
|
2764
|
-
"ANTHROPIC_API_KEY",
|
|
2765
|
-
"OPENAI_API_KEY",
|
|
2766
|
-
"VERCEL_OIDC_TOKEN",
|
|
2767
|
-
"VERCEL_TEAM_ID"
|
|
2768
|
-
].map((key) => key.toLowerCase()));
|
|
2769
|
-
function isSensitiveKey(key) {
|
|
2770
|
-
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
2771
|
-
}
|
|
2772
|
-
function redactValue(key, value, seen) {
|
|
2773
|
-
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
2774
|
-
if (value == null || typeof value !== "object") return value;
|
|
2775
|
-
if (value instanceof Date) return value;
|
|
2776
|
-
if (seen.has(value)) return "[Circular]";
|
|
2777
|
-
seen.add(value);
|
|
2778
|
-
if (Array.isArray(value)) {
|
|
2779
|
-
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
2780
|
-
seen.delete(value);
|
|
2781
|
-
return out2;
|
|
2782
|
-
}
|
|
2783
|
-
const out = {};
|
|
2784
|
-
for (const [childKey, childValue] of Object.entries(value)) {
|
|
2785
|
-
out[childKey] = redactValue(childKey, childValue, seen);
|
|
2786
|
-
}
|
|
2787
|
-
seen.delete(value);
|
|
2788
|
-
return out;
|
|
2789
|
-
}
|
|
2790
|
-
function redact(fields) {
|
|
2791
|
-
const out = {};
|
|
2792
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
2793
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
2794
|
-
out[k] = redactValue(k, v, seen);
|
|
2795
|
-
}
|
|
2796
|
-
return out;
|
|
2797
|
-
}
|
|
2798
|
-
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
2799
|
-
function createLogger(prefix) {
|
|
2800
|
-
function emit(level, msg, fields) {
|
|
2801
|
-
const entry = {
|
|
2802
|
-
level,
|
|
2803
|
-
prefix,
|
|
2804
|
-
msg,
|
|
2805
|
-
...fields ? redact(fields) : {},
|
|
2806
|
-
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
2807
|
-
};
|
|
2808
|
-
if (level === "error") {
|
|
2809
|
-
console.error(JSON.stringify(entry));
|
|
2810
|
-
} else if (level === "warn") {
|
|
2811
|
-
console.warn(JSON.stringify(entry));
|
|
2812
|
-
} else {
|
|
2813
|
-
console.log(JSON.stringify(entry));
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
return {
|
|
2817
|
-
debug(msg, fields) {
|
|
2818
|
-
if (verbose) emit("debug", msg, fields);
|
|
2819
|
-
},
|
|
2820
|
-
info(msg, fields) {
|
|
2821
|
-
emit("info", msg, fields);
|
|
2822
|
-
},
|
|
2823
|
-
warn(msg, fields) {
|
|
2824
|
-
emit("warn", msg, fields);
|
|
2825
|
-
},
|
|
2826
|
-
error(msg, fields) {
|
|
2827
|
-
emit("error", msg, fields);
|
|
2828
|
-
}
|
|
2829
|
-
};
|
|
2830
|
-
}
|
|
3284
|
+
createAgentSession,
|
|
3285
|
+
SessionManager,
|
|
3286
|
+
AuthStorage,
|
|
3287
|
+
ModelRegistry,
|
|
3288
|
+
DefaultResourceLoader,
|
|
3289
|
+
SettingsManager as SettingsManager2,
|
|
3290
|
+
getAgentDir as getAgentDir2
|
|
3291
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2831
3292
|
|
|
2832
3293
|
// src/server/harness/pi-coding-agent/tool-adapter.ts
|
|
2833
|
-
function adaptToolForPi(tool) {
|
|
3294
|
+
function adaptToolForPi(tool, sessionId) {
|
|
2834
3295
|
return {
|
|
2835
3296
|
name: tool.name,
|
|
2836
3297
|
label: tool.name,
|
|
@@ -2841,7 +3302,8 @@ function adaptToolForPi(tool) {
|
|
|
2841
3302
|
const result = await tool.execute(params, {
|
|
2842
3303
|
toolCallId,
|
|
2843
3304
|
abortSignal: signal ?? new AbortController().signal,
|
|
2844
|
-
onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0
|
|
3305
|
+
onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
|
|
3306
|
+
sessionId
|
|
2845
3307
|
});
|
|
2846
3308
|
if (result.isError) {
|
|
2847
3309
|
throw new Error(result.content.map((c) => c.text).join("\n"));
|
|
@@ -2853,8 +3315,8 @@ function adaptToolForPi(tool) {
|
|
|
2853
3315
|
}
|
|
2854
3316
|
};
|
|
2855
3317
|
}
|
|
2856
|
-
function adaptToolsForPi(tools) {
|
|
2857
|
-
return tools.map(adaptToolForPi);
|
|
3318
|
+
function adaptToolsForPi(tools, sessionId) {
|
|
3319
|
+
return tools.map((tool) => adaptToolForPi(tool, sessionId));
|
|
2858
3320
|
}
|
|
2859
3321
|
|
|
2860
3322
|
// src/server/harness/pi-coding-agent/stream-adapter.ts
|
|
@@ -3064,12 +3526,24 @@ function defaultSessionDir(cwd) {
|
|
|
3064
3526
|
return join5(homedir3(), ".pi", "agent", "sessions", safePath);
|
|
3065
3527
|
}
|
|
3066
3528
|
var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
3529
|
+
var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
|
|
3530
|
+
function sessionDirForNamespace(namespace) {
|
|
3531
|
+
const safeNamespace = namespace.trim();
|
|
3532
|
+
if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
|
|
3533
|
+
throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
|
|
3534
|
+
}
|
|
3535
|
+
return join5(homedir3(), ".pi", "agent", "sessions", safeNamespace);
|
|
3536
|
+
}
|
|
3067
3537
|
var PiSessionStore = class {
|
|
3068
3538
|
cwd;
|
|
3069
3539
|
sessionDir;
|
|
3070
|
-
constructor(cwd,
|
|
3540
|
+
constructor(cwd, options) {
|
|
3071
3541
|
this.cwd = cwd;
|
|
3072
|
-
|
|
3542
|
+
if (typeof options === "string") {
|
|
3543
|
+
this.sessionDir = options;
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace) : defaultSessionDir(cwd));
|
|
3073
3547
|
}
|
|
3074
3548
|
async list(ctx) {
|
|
3075
3549
|
const files = await readdir4(this.sessionDir).catch(() => []);
|
|
@@ -3505,7 +3979,7 @@ function createSessionTitleScheduler(opts) {
|
|
|
3505
3979
|
if (!detail || detail.turnCount !== 1) return;
|
|
3506
3980
|
if (hasCustomTitle(detail.title)) return;
|
|
3507
3981
|
const fallbackTitle = formatFallbackTitle(getNow());
|
|
3508
|
-
const apiKey = opts.getApiKey?.()?.trim() ||
|
|
3982
|
+
const apiKey = opts.getApiKey?.()?.trim() || "";
|
|
3509
3983
|
if (!apiKey || !fetchImpl) {
|
|
3510
3984
|
opts.writeTitle(input.sessionId, fallbackTitle);
|
|
3511
3985
|
return;
|
|
@@ -3533,6 +4007,7 @@ function createSessionTitleScheduler(opts) {
|
|
|
3533
4007
|
}
|
|
3534
4008
|
|
|
3535
4009
|
// src/server/models/modelConfig.ts
|
|
4010
|
+
import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
3536
4011
|
var INFOMANIAK_PROVIDER = "infomaniak";
|
|
3537
4012
|
var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
|
|
3538
4013
|
var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
|
|
@@ -3661,6 +4136,16 @@ function registerConfiguredModelProviders(registry) {
|
|
|
3661
4136
|
}
|
|
3662
4137
|
return registered;
|
|
3663
4138
|
}
|
|
4139
|
+
function readPiSettingsDefaultModel() {
|
|
4140
|
+
try {
|
|
4141
|
+
const settings = SettingsManager.create(process.cwd(), getAgentDir());
|
|
4142
|
+
const provider = clean(settings.getDefaultProvider());
|
|
4143
|
+
const id = clean(settings.getDefaultModel());
|
|
4144
|
+
return provider && id ? { provider, id } : void 0;
|
|
4145
|
+
} catch {
|
|
4146
|
+
return void 0;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
3664
4149
|
function readConfiguredDefaultModel() {
|
|
3665
4150
|
const encoded = clean(getEnv("BORING_AGENT_DEFAULT_MODEL"));
|
|
3666
4151
|
if (encoded) {
|
|
@@ -3674,7 +4159,7 @@ function readConfiguredDefaultModel() {
|
|
|
3674
4159
|
if (explicitProvider && explicitId) {
|
|
3675
4160
|
return { provider: explicitProvider, id: explicitId };
|
|
3676
4161
|
}
|
|
3677
|
-
return readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
|
|
4162
|
+
return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
|
|
3678
4163
|
}
|
|
3679
4164
|
|
|
3680
4165
|
// src/server/piPackages.ts
|
|
@@ -3720,6 +4205,17 @@ function mergePiPackageSources(base = [], additional = []) {
|
|
|
3720
4205
|
}
|
|
3721
4206
|
|
|
3722
4207
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
4208
|
+
var WORKSPACE_PATHS_GUIDELINE = [
|
|
4209
|
+
"## Workspace paths",
|
|
4210
|
+
"",
|
|
4211
|
+
'- The "Current working directory" above is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
|
|
4212
|
+
"- Never pass an absolute path or a path that walks outside the workspace (no leading `/`, no `..` that escapes the root). The sandbox will reject it and the call is wasted.",
|
|
4213
|
+
"- For `find`/`grep`/`ls`: omit the `path` argument to search from the workspace root. Pass `path` only when you need to restrict to a subdirectory, and only as a workspace-relative path.",
|
|
4214
|
+
"- For `read`/`edit`/`write`: pass workspace-relative paths only."
|
|
4215
|
+
].join("\n");
|
|
4216
|
+
function composeSystemPromptAppend(hostAppend) {
|
|
4217
|
+
return [WORKSPACE_PATHS_GUIDELINE, hostAppend?.trim()].filter(Boolean).join("\n\n");
|
|
4218
|
+
}
|
|
3723
4219
|
function extractUserMessageText(message) {
|
|
3724
4220
|
const record = message;
|
|
3725
4221
|
if (record?.role !== "user") return "";
|
|
@@ -3748,6 +4244,16 @@ function findLastAssistantMessage(messages) {
|
|
|
3748
4244
|
}
|
|
3749
4245
|
return void 0;
|
|
3750
4246
|
}
|
|
4247
|
+
function extractAssistantReasoningTexts(message) {
|
|
4248
|
+
const content = message?.content;
|
|
4249
|
+
if (!Array.isArray(content)) return [];
|
|
4250
|
+
return content.flatMap((part) => {
|
|
4251
|
+
const item = part;
|
|
4252
|
+
if (item.type !== "thinking" && item.type !== "reasoning") return [];
|
|
4253
|
+
const text = item.thinking ?? item.text ?? item.content;
|
|
4254
|
+
return typeof text === "string" && text.length > 0 ? [text] : [];
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
3751
4257
|
function resolveRequestedModel(modelRegistry, input) {
|
|
3752
4258
|
const requestedId = input.model?.id;
|
|
3753
4259
|
if (!input.model || !requestedId) return void 0;
|
|
@@ -3767,27 +4273,37 @@ function resolveDefaultModel(modelRegistry) {
|
|
|
3767
4273
|
}
|
|
3768
4274
|
return void 0;
|
|
3769
4275
|
}
|
|
3770
|
-
function
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
packages: mergePiPackageSources(projectSettings.packages ?? [], piPackages)
|
|
4276
|
+
function readSettingsFileIfPresent(path4) {
|
|
4277
|
+
return existsSync(path4) ? readFileSync2(path4, "utf-8") : void 0;
|
|
4278
|
+
}
|
|
4279
|
+
function mergeInjectedProjectPackages(settingsJson, piPackages) {
|
|
4280
|
+
const settings = settingsJson ? JSON.parse(settingsJson) : {};
|
|
4281
|
+
const configuredPackages = Array.isArray(settings.packages) ? settings.packages : [];
|
|
4282
|
+
return JSON.stringify({
|
|
4283
|
+
...settings,
|
|
4284
|
+
packages: mergePiPackageSources(configuredPackages, piPackages)
|
|
3780
4285
|
});
|
|
4286
|
+
}
|
|
4287
|
+
function createResourceSettingsManager(cwd, agentDir, piPackages) {
|
|
4288
|
+
if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
|
|
4289
|
+
const globalSettingsPath = join6(agentDir, "settings.json");
|
|
4290
|
+
const projectSettingsPath = join6(cwd, ".pi", "settings.json");
|
|
4291
|
+
let globalSettingsOverrideJson;
|
|
4292
|
+
let projectSettingsOverrideJson;
|
|
3781
4293
|
const storage = {
|
|
3782
4294
|
withLock(scope, fn) {
|
|
3783
4295
|
if (scope === "global") {
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
4296
|
+
const current2 = globalSettingsOverrideJson ?? readSettingsFileIfPresent(globalSettingsPath);
|
|
4297
|
+
const next2 = fn(current2);
|
|
4298
|
+
if (next2 !== void 0) globalSettingsOverrideJson = next2;
|
|
4299
|
+
return;
|
|
3787
4300
|
}
|
|
4301
|
+
const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages);
|
|
4302
|
+
const next = fn(current);
|
|
4303
|
+
if (next !== void 0) projectSettingsOverrideJson = next;
|
|
3788
4304
|
}
|
|
3789
4305
|
};
|
|
3790
|
-
return
|
|
4306
|
+
return SettingsManager2.fromStorage(storage);
|
|
3791
4307
|
}
|
|
3792
4308
|
async function applyRequestedSessionOptions(handle, input) {
|
|
3793
4309
|
const requestedModel = resolveRequestedModel(handle.modelRegistry, input);
|
|
@@ -3801,10 +4317,10 @@ async function applyRequestedSessionOptions(handle, input) {
|
|
|
3801
4317
|
handle.piSession.setThinkingLevel(input.thinkingLevel);
|
|
3802
4318
|
}
|
|
3803
4319
|
}
|
|
3804
|
-
var
|
|
4320
|
+
var log3 = createLogger("pi-harness");
|
|
3805
4321
|
var DEFAULT_ATTACHMENT_DIR = "assets/images";
|
|
3806
4322
|
function extForAttachment(filename, contentType) {
|
|
3807
|
-
const fromName =
|
|
4323
|
+
const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
|
|
3808
4324
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
3809
4325
|
if (contentType === "image/jpeg") return "jpg";
|
|
3810
4326
|
if (contentType === "image/png") return "png";
|
|
@@ -3820,7 +4336,10 @@ function basenameForAttachment(filename) {
|
|
|
3820
4336
|
return safe || "image";
|
|
3821
4337
|
}
|
|
3822
4338
|
function createPiCodingAgentHarness(opts) {
|
|
3823
|
-
const sessionStore = new PiSessionStore(opts.cwd
|
|
4339
|
+
const sessionStore = new PiSessionStore(opts.cwd, {
|
|
4340
|
+
sessionNamespace: opts.sessionNamespace,
|
|
4341
|
+
sessionDir: opts.sessionDir
|
|
4342
|
+
});
|
|
3824
4343
|
const piSessions = /* @__PURE__ */ new Map();
|
|
3825
4344
|
const scheduleSessionTitle = createSessionTitleScheduler({
|
|
3826
4345
|
loadSession: (sessionId) => sessionStore.load({ workspaceId: "default" }, sessionId),
|
|
@@ -3854,38 +4373,29 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3854
4373
|
}
|
|
3855
4374
|
const resolvedModel = resolveRequestedModel(modelRegistry, input);
|
|
3856
4375
|
const model = resolvedModel ?? resolveDefaultModel(modelRegistry);
|
|
3857
|
-
const
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
skillsOverride: () => loadSkills({
|
|
3876
|
-
cwd: ctx.workdir,
|
|
3877
|
-
agentDir,
|
|
3878
|
-
skillPaths: additionalSkillPaths,
|
|
3879
|
-
includeDefaults: false
|
|
3880
|
-
})
|
|
3881
|
-
} : {}
|
|
3882
|
-
});
|
|
3883
|
-
})() : void 0;
|
|
4376
|
+
const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
|
|
4377
|
+
const agentDir = getAgentDir2();
|
|
4378
|
+
const additionalSkillPaths = opts.resourceLoaderOptions?.additionalSkillPaths ?? [];
|
|
4379
|
+
const piPackages = opts.resourceLoaderOptions?.piPackages ?? [];
|
|
4380
|
+
const settingsManager = createResourceSettingsManager(
|
|
4381
|
+
ctx.workdir,
|
|
4382
|
+
agentDir,
|
|
4383
|
+
piPackages
|
|
4384
|
+
);
|
|
4385
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
4386
|
+
cwd: ctx.workdir,
|
|
4387
|
+
agentDir,
|
|
4388
|
+
settingsManager,
|
|
4389
|
+
appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
|
|
4390
|
+
...opts.resourceLoaderOptions?.noContextFiles ? { noContextFiles: true } : {},
|
|
4391
|
+
...opts.resourceLoaderOptions?.noSkills ? { noSkills: true } : {},
|
|
4392
|
+
...additionalSkillPaths.length ? { additionalSkillPaths } : {}
|
|
4393
|
+
});
|
|
3884
4394
|
await resourceLoader?.reload();
|
|
3885
4395
|
const { session: piSession } = await createAgentSession({
|
|
3886
4396
|
cwd: ctx.workdir,
|
|
3887
4397
|
tools: [],
|
|
3888
|
-
customTools: adaptToolsForPi(opts.tools),
|
|
4398
|
+
customTools: adaptToolsForPi(opts.tools, input.sessionId),
|
|
3889
4399
|
model,
|
|
3890
4400
|
thinkingLevel: input.thinkingLevel ?? "off",
|
|
3891
4401
|
sessionManager,
|
|
@@ -3909,6 +4419,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3909
4419
|
if (!handle) return;
|
|
3910
4420
|
handle.piSession.dispose();
|
|
3911
4421
|
piSessions.delete(sessionId);
|
|
4422
|
+
clearNativeFollowUpWork(sessionId);
|
|
3912
4423
|
}
|
|
3913
4424
|
const originalDelete = sessionStore.delete.bind(sessionStore);
|
|
3914
4425
|
sessionStore.delete = async (ctx, sessionId) => {
|
|
@@ -3916,17 +4427,95 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3916
4427
|
disposePiSession(sessionId);
|
|
3917
4428
|
};
|
|
3918
4429
|
const nativeFollowUpPending = /* @__PURE__ */ new Set();
|
|
4430
|
+
const nativeFollowUpQueues = /* @__PURE__ */ new Map();
|
|
4431
|
+
function clearNativeFollowUpWork(sessionId) {
|
|
4432
|
+
nativeFollowUpPending.delete(sessionId);
|
|
4433
|
+
nativeFollowUpQueues.delete(sessionId);
|
|
4434
|
+
}
|
|
4435
|
+
function userMessageText(message) {
|
|
4436
|
+
const content = message.content;
|
|
4437
|
+
if (!Array.isArray(content)) return "";
|
|
4438
|
+
return content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
|
|
4439
|
+
}
|
|
4440
|
+
function removeFirstMatchingOrdinal(items, matches, ordinal) {
|
|
4441
|
+
let seen = 0;
|
|
4442
|
+
const index = items.findIndex((item) => {
|
|
4443
|
+
if (!matches(item)) return false;
|
|
4444
|
+
if (seen++ !== ordinal) return false;
|
|
4445
|
+
return true;
|
|
4446
|
+
});
|
|
4447
|
+
if (index >= 0) items.splice(index, 1);
|
|
4448
|
+
}
|
|
4449
|
+
function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
|
|
4450
|
+
const session = piSession;
|
|
4451
|
+
if (!text) {
|
|
4452
|
+
session.agent?.clearFollowUpQueue?.();
|
|
4453
|
+
if (Array.isArray(session._followUpMessages)) session._followUpMessages = [];
|
|
4454
|
+
session._emitQueueUpdate?.();
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
const queuedMessages = session.agent?.followUpQueue?.messages;
|
|
4458
|
+
if (Array.isArray(queuedMessages)) {
|
|
4459
|
+
removeFirstMatchingOrdinal(queuedMessages, (message) => userMessageText(message) === text, textOrdinal);
|
|
4460
|
+
}
|
|
4461
|
+
if (Array.isArray(session._followUpMessages)) {
|
|
4462
|
+
removeFirstMatchingOrdinal(session._followUpMessages, (message) => message === text, textOrdinal);
|
|
4463
|
+
}
|
|
4464
|
+
session._emitQueueUpdate?.();
|
|
4465
|
+
}
|
|
4466
|
+
function hasFollowUpSelector(options) {
|
|
4467
|
+
return Boolean(options?.clientNonce) || options?.clientSeq !== void 0;
|
|
4468
|
+
}
|
|
4469
|
+
function matchesFollowUpSelector(item, options) {
|
|
4470
|
+
if (!hasFollowUpSelector(options)) return true;
|
|
4471
|
+
return Boolean(options?.clientNonce && item.clientNonce === options.clientNonce) || options?.clientSeq !== void 0 && item.clientSeq === options.clientSeq;
|
|
4472
|
+
}
|
|
4473
|
+
function removeNativeFollowUp(sessionId, options) {
|
|
4474
|
+
const queue = nativeFollowUpQueues.get(sessionId);
|
|
4475
|
+
if (!queue?.length) {
|
|
4476
|
+
if (!hasFollowUpSelector(options)) clearNativeFollowUpWork(sessionId);
|
|
4477
|
+
return [];
|
|
4478
|
+
}
|
|
4479
|
+
const removed = [];
|
|
4480
|
+
const next = [];
|
|
4481
|
+
const textCounts = /* @__PURE__ */ new Map();
|
|
4482
|
+
for (const request of queue) {
|
|
4483
|
+
const textOrdinal = textCounts.get(request.text) ?? 0;
|
|
4484
|
+
textCounts.set(request.text, textOrdinal + 1);
|
|
4485
|
+
if (matchesFollowUpSelector(request, options)) removed.push({ request, textOrdinal });
|
|
4486
|
+
else next.push(request);
|
|
4487
|
+
}
|
|
4488
|
+
if (next.length > 0) nativeFollowUpQueues.set(sessionId, next);
|
|
4489
|
+
else clearNativeFollowUpWork(sessionId);
|
|
4490
|
+
return removed;
|
|
4491
|
+
}
|
|
3919
4492
|
return {
|
|
3920
4493
|
id: "pi-coding-agent",
|
|
3921
4494
|
placement: "server",
|
|
3922
4495
|
sessions: sessionStore,
|
|
3923
|
-
async followUp(sessionId, text, _attachments,
|
|
4496
|
+
async followUp(sessionId, text, _attachments, displayText = text, options) {
|
|
3924
4497
|
const handle = piSessions.get(sessionId);
|
|
3925
|
-
if (!handle)
|
|
4498
|
+
if (!handle) throw new Error("followup_session_not_ready");
|
|
4499
|
+
const queue = nativeFollowUpQueues.get(sessionId) ?? [];
|
|
4500
|
+
queue.push({
|
|
4501
|
+
text,
|
|
4502
|
+
displayText,
|
|
4503
|
+
clientNonce: options?.clientNonce,
|
|
4504
|
+
clientSeq: options?.clientSeq
|
|
4505
|
+
});
|
|
4506
|
+
nativeFollowUpQueues.set(sessionId, queue);
|
|
3926
4507
|
nativeFollowUpPending.add(sessionId);
|
|
3927
4508
|
await handle.piSession.followUp(text);
|
|
3928
4509
|
},
|
|
3929
|
-
clearFollowUp(
|
|
4510
|
+
clearFollowUp(sessionId, options) {
|
|
4511
|
+
const handle = piSessions.get(sessionId);
|
|
4512
|
+
const removed = removeNativeFollowUp(sessionId, options);
|
|
4513
|
+
if (!handle) return;
|
|
4514
|
+
if (!options?.clientNonce && options?.clientSeq === void 0) {
|
|
4515
|
+
removePiQueuedFollowUp(handle.piSession);
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4518
|
+
for (const item of removed) removePiQueuedFollowUp(handle.piSession, item.request.text, item.textOrdinal);
|
|
3930
4519
|
},
|
|
3931
4520
|
/**
|
|
3932
4521
|
* Pi exposes the resolved system prompt as a getter on AgentSession.
|
|
@@ -3992,6 +4581,27 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3992
4581
|
}
|
|
3993
4582
|
let sawTextChunk = false;
|
|
3994
4583
|
let inlineTurnIndex = 0;
|
|
4584
|
+
let currentPiAssistantMessageId = null;
|
|
4585
|
+
const messageIdsWithStreamedReasoning = /* @__PURE__ */ new Set();
|
|
4586
|
+
let piSeq = 0;
|
|
4587
|
+
const nextPiSeq = () => ++piSeq;
|
|
4588
|
+
const STANDARD_VISIBLE_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
4589
|
+
"text-start",
|
|
4590
|
+
"text-delta",
|
|
4591
|
+
"text-end",
|
|
4592
|
+
"reasoning-start",
|
|
4593
|
+
"reasoning-delta",
|
|
4594
|
+
"reasoning-end",
|
|
4595
|
+
"tool-input-available",
|
|
4596
|
+
"tool-input-error",
|
|
4597
|
+
"tool-output-available",
|
|
4598
|
+
"tool-output-error"
|
|
4599
|
+
]);
|
|
4600
|
+
const standardToolInputsSeen = /* @__PURE__ */ new Set();
|
|
4601
|
+
function isStandardVisibleChunk(chunk2) {
|
|
4602
|
+
const type = chunk2.type;
|
|
4603
|
+
return typeof type === "string" && STANDARD_VISIBLE_CHUNK_TYPES.has(type);
|
|
4604
|
+
}
|
|
3995
4605
|
function namespaceInlinePartIds(input2) {
|
|
3996
4606
|
if (inlineTurnIndex === 0) return input2;
|
|
3997
4607
|
return input2.map((chunk2) => {
|
|
@@ -4002,6 +4612,23 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4002
4612
|
return chunk2;
|
|
4003
4613
|
});
|
|
4004
4614
|
}
|
|
4615
|
+
function filterSdkChunksForCurrentSegment(input2) {
|
|
4616
|
+
const out = [];
|
|
4617
|
+
for (const chunk2 of input2) {
|
|
4618
|
+
const rec = chunk2;
|
|
4619
|
+
if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk2)) continue;
|
|
4620
|
+
if (rec.type === "tool-input-available" && rec.toolCallId) {
|
|
4621
|
+
standardToolInputsSeen.add(rec.toolCallId);
|
|
4622
|
+
out.push(chunk2);
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
if ((rec.type === "tool-output-available" || rec.type === "tool-output-error" || rec.type === "tool-output-denied") && rec.toolCallId && !standardToolInputsSeen.has(rec.toolCallId)) {
|
|
4626
|
+
continue;
|
|
4627
|
+
}
|
|
4628
|
+
out.push(chunk2);
|
|
4629
|
+
}
|
|
4630
|
+
return out;
|
|
4631
|
+
}
|
|
4005
4632
|
let promptSettled = false;
|
|
4006
4633
|
let promptPromise = Promise.resolve();
|
|
4007
4634
|
const unsubscribe = piSession.subscribe((event) => {
|
|
@@ -4024,56 +4651,138 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4024
4651
|
if (activeTools.size === 0) stopHeartbeat();
|
|
4025
4652
|
}
|
|
4026
4653
|
let converted;
|
|
4654
|
+
const piHistoryChunks = [];
|
|
4655
|
+
const eventMessage = event.message;
|
|
4656
|
+
if (event.type === "message_start" && (eventMessage?.role === "user" || eventMessage?.role === "assistant")) {
|
|
4657
|
+
const messageId = typeof eventMessage.id === "string" ? eventMessage.id : `${eventMessage.role}-${Date.now()}`;
|
|
4658
|
+
const role = eventMessage.role;
|
|
4659
|
+
if (role === "assistant") currentPiAssistantMessageId = messageId;
|
|
4660
|
+
const text = role === "user" ? extractUserMessageText(eventMessage) : void 0;
|
|
4661
|
+
if (role === "user" && text || role === "assistant") {
|
|
4662
|
+
piHistoryChunks.push({
|
|
4663
|
+
type: "data-pi-message-start",
|
|
4664
|
+
data: { seq: nextPiSeq(), messageId, role, ...text ? { text } : {} }
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
if (event.type === "message_update") {
|
|
4669
|
+
const ame = event.assistantMessageEvent;
|
|
4670
|
+
const messageId = typeof event.messageId === "string" ? event.messageId : typeof event.message?.id === "string" ? event.message.id : currentPiAssistantMessageId ?? "assistant-streaming";
|
|
4671
|
+
if (ame.type === "text_start") {
|
|
4672
|
+
piHistoryChunks.push({
|
|
4673
|
+
type: "data-pi-text-start",
|
|
4674
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4675
|
+
});
|
|
4676
|
+
} else if (ame.type === "text_delta" && ame.delta) {
|
|
4677
|
+
const seq = nextPiSeq();
|
|
4678
|
+
piHistoryChunks.push(
|
|
4679
|
+
{
|
|
4680
|
+
type: "data-pi-text-delta",
|
|
4681
|
+
data: { seq, messageId, partId: String(ame.contentIndex), delta: ame.delta }
|
|
4682
|
+
},
|
|
4683
|
+
// Back-compat for the current client projection. Remove once
|
|
4684
|
+
// ChatPanel consumes the stable data-pi-text-* DTOs directly.
|
|
4685
|
+
{
|
|
4686
|
+
type: "data-pi-message-delta",
|
|
4687
|
+
data: { seq, messageId, role: "assistant", delta: ame.delta }
|
|
4688
|
+
}
|
|
4689
|
+
);
|
|
4690
|
+
} else if (ame.type === "text_end") {
|
|
4691
|
+
piHistoryChunks.push({
|
|
4692
|
+
type: "data-pi-text-end",
|
|
4693
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex), ...typeof ame.content === "string" ? { text: ame.content } : {} }
|
|
4694
|
+
});
|
|
4695
|
+
} else if (ame.type === "thinking_start") {
|
|
4696
|
+
messageIdsWithStreamedReasoning.add(messageId);
|
|
4697
|
+
piHistoryChunks.push({
|
|
4698
|
+
type: "data-pi-reasoning-start",
|
|
4699
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4700
|
+
});
|
|
4701
|
+
} else if (ame.type === "thinking_delta") {
|
|
4702
|
+
messageIdsWithStreamedReasoning.add(messageId);
|
|
4703
|
+
piHistoryChunks.push({
|
|
4704
|
+
type: "data-pi-reasoning-delta",
|
|
4705
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex), delta: ame.delta }
|
|
4706
|
+
});
|
|
4707
|
+
} else if (ame.type === "thinking_end") {
|
|
4708
|
+
piHistoryChunks.push({
|
|
4709
|
+
type: "data-pi-reasoning-end",
|
|
4710
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4711
|
+
});
|
|
4712
|
+
} else if (ame.type === "toolcall_end") {
|
|
4713
|
+
piHistoryChunks.push({
|
|
4714
|
+
type: "data-pi-tool-call-end",
|
|
4715
|
+
data: { seq: nextPiSeq(), messageId, toolCallId: ame.toolCall.id, toolName: ame.toolCall.name, input: ame.toolCall.arguments }
|
|
4716
|
+
});
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (event.type === "message_end" && (eventMessage?.role === "user" || eventMessage?.role === "assistant")) {
|
|
4720
|
+
const role = eventMessage.role;
|
|
4721
|
+
const messageId = typeof eventMessage.id === "string" ? eventMessage.id : role === "assistant" && currentPiAssistantMessageId ? currentPiAssistantMessageId : `${role}-${Date.now()}`;
|
|
4722
|
+
const text = role === "user" ? extractUserMessageText(eventMessage) : extractAssistantMessageText(eventMessage).text;
|
|
4723
|
+
if (role === "assistant" && !messageIdsWithStreamedReasoning.has(messageId)) {
|
|
4724
|
+
for (const reasoningText of extractAssistantReasoningTexts(eventMessage)) {
|
|
4725
|
+
const partId = `reasoning-${nextPiSeq()}`;
|
|
4726
|
+
piHistoryChunks.push(
|
|
4727
|
+
{ type: "data-pi-reasoning-start", data: { seq: nextPiSeq(), messageId, partId } },
|
|
4728
|
+
{ type: "data-pi-reasoning-delta", data: { seq: nextPiSeq(), messageId, partId, delta: reasoningText } },
|
|
4729
|
+
{ type: "data-pi-reasoning-end", data: { seq: nextPiSeq(), messageId, partId } }
|
|
4730
|
+
);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
piHistoryChunks.push({
|
|
4734
|
+
type: "data-pi-message-end",
|
|
4735
|
+
data: { seq: nextPiSeq(), messageId, role, ...text ? { text } : {} }
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
if (event.type === "tool_execution_start" && currentPiAssistantMessageId) {
|
|
4739
|
+
piHistoryChunks.push({
|
|
4740
|
+
type: "data-pi-tool-call-end",
|
|
4741
|
+
data: { seq: nextPiSeq(), messageId: currentPiAssistantMessageId, toolCallId: event.toolCallId, toolName: event.toolName, input: event.args }
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
if (event.type === "tool_execution_end" && currentPiAssistantMessageId) {
|
|
4745
|
+
piHistoryChunks.push({
|
|
4746
|
+
type: "data-pi-tool-result",
|
|
4747
|
+
data: { seq: nextPiSeq(), messageId: currentPiAssistantMessageId, toolCallId: event.toolCallId, output: event.result, isError: event.isError }
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4027
4750
|
if (event.type === "message_start" && event.message?.role === "user") {
|
|
4028
4751
|
const text = extractUserMessageText(event.message);
|
|
4029
|
-
converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }] :
|
|
4752
|
+
converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }, ...piHistoryChunks] : piHistoryChunks;
|
|
4030
4753
|
if (text && text !== input.message) {
|
|
4031
4754
|
inlineTurnIndex += 1;
|
|
4032
|
-
|
|
4755
|
+
sawTextChunk = false;
|
|
4756
|
+
currentPiAssistantMessageId = null;
|
|
4757
|
+
const queue = nativeFollowUpQueues.get(input.sessionId);
|
|
4758
|
+
if (queue?.length) {
|
|
4759
|
+
const index = queue.findIndex((item) => item.text === text || item.displayText === text);
|
|
4760
|
+
if (index >= 0) queue.splice(index, 1);
|
|
4761
|
+
else queue.shift();
|
|
4762
|
+
if (queue.length > 0) nativeFollowUpQueues.set(input.sessionId, queue);
|
|
4763
|
+
else clearNativeFollowUpWork(input.sessionId);
|
|
4764
|
+
} else {
|
|
4765
|
+
nativeFollowUpPending.delete(input.sessionId);
|
|
4766
|
+
}
|
|
4033
4767
|
}
|
|
4034
4768
|
} else if (event.type === "message_end" && event.message?.role === "user") {
|
|
4035
|
-
converted =
|
|
4769
|
+
converted = piHistoryChunks;
|
|
4036
4770
|
} else {
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
const ame = event.assistantMessageEvent;
|
|
4041
|
-
if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
|
|
4042
|
-
const id = inlineTurnIndex === 0 ? String(ame.contentIndex) : `turn-${inlineTurnIndex}:${ame.contentIndex}`;
|
|
4043
|
-
converted = [
|
|
4044
|
-
...textStartSeen.has(ame.contentIndex) ? [] : [{ type: "text-start", id }],
|
|
4045
|
-
{ type: "text-delta", id, delta: ame.content },
|
|
4046
|
-
...converted
|
|
4047
|
-
];
|
|
4048
|
-
textDeltaSeen.add(ame.contentIndex);
|
|
4049
|
-
assistantText += ame.content;
|
|
4050
|
-
}
|
|
4771
|
+
const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
4772
|
+
const sdkChunksForTurn = filterSdkChunksForCurrentSegment(sdkChunks);
|
|
4773
|
+
converted = [...piHistoryChunks, ...sdkChunksForTurn];
|
|
4051
4774
|
}
|
|
4052
4775
|
for (const chunk2 of converted) {
|
|
4053
4776
|
const t = chunk2.type;
|
|
4054
|
-
if (t === "text-delta") {
|
|
4777
|
+
if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
|
|
4055
4778
|
sawTextChunk = true;
|
|
4056
4779
|
}
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
if (event.type === "message_end" && !sawTextChunk) {
|
|
4060
|
-
const { role, text, errorText } = extractAssistantMessageText(
|
|
4061
|
-
event.message
|
|
4062
|
-
);
|
|
4063
|
-
if (role === "assistant" && errorText.length > 0) {
|
|
4064
|
-
chunks.push({ type: "error", errorText });
|
|
4065
|
-
sawTextChunk = true;
|
|
4066
|
-
} else if (role === "assistant" && text.length > 0) {
|
|
4067
|
-
const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
|
|
4068
|
-
chunks.push(
|
|
4069
|
-
{ type: "text-start", id },
|
|
4070
|
-
{ type: "text-delta", id, delta: text },
|
|
4071
|
-
{ type: "text-end", id }
|
|
4072
|
-
);
|
|
4780
|
+
const piEndData = chunk2.data;
|
|
4781
|
+
if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
|
|
4073
4782
|
sawTextChunk = true;
|
|
4074
|
-
assistantText += text;
|
|
4075
4783
|
}
|
|
4076
4784
|
}
|
|
4785
|
+
chunks.push(...converted);
|
|
4077
4786
|
if (event.type === "agent_end") {
|
|
4078
4787
|
if (!sawTextChunk) {
|
|
4079
4788
|
const { role, text, errorText } = extractAssistantMessageText(
|
|
@@ -4085,11 +4794,10 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4085
4794
|
chunks.push({ type: "error", errorText });
|
|
4086
4795
|
sawTextChunk = true;
|
|
4087
4796
|
} else if (role === "assistant" && text.length > 0) {
|
|
4088
|
-
const
|
|
4797
|
+
const messageId = currentPiAssistantMessageId ?? "assistant-streaming";
|
|
4089
4798
|
chunks.push(
|
|
4090
|
-
{ type: "
|
|
4091
|
-
{ type: "
|
|
4092
|
-
{ type: "text-end", id }
|
|
4799
|
+
{ type: "data-pi-message-start", data: { seq: nextPiSeq(), messageId, role } },
|
|
4800
|
+
{ type: "data-pi-message-end", data: { seq: nextPiSeq(), messageId, role, text } }
|
|
4093
4801
|
);
|
|
4094
4802
|
sawTextChunk = true;
|
|
4095
4803
|
assistantText += text;
|
|
@@ -4117,6 +4825,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4117
4825
|
continue;
|
|
4118
4826
|
}
|
|
4119
4827
|
try {
|
|
4828
|
+
const estimatedSize = Math.ceil(b64.length * 0.75);
|
|
4829
|
+
if (estimatedSize > 10 * 1024 * 1024) {
|
|
4830
|
+
writeErrors.push(`${a.filename ?? "image"}: attachment exceeds 10 MB limit`);
|
|
4831
|
+
continue;
|
|
4832
|
+
}
|
|
4120
4833
|
const bytes = Buffer.from(b64, "base64");
|
|
4121
4834
|
const ext = extForAttachment(a.filename ?? "image", contentType);
|
|
4122
4835
|
const base = basenameForAttachment(a.filename ?? "image");
|
|
@@ -4127,7 +4840,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4127
4840
|
savedPaths.push(relPath);
|
|
4128
4841
|
} catch (err) {
|
|
4129
4842
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4130
|
-
|
|
4843
|
+
log3.error("attachment write failed", { workdir: ctx.workdir, error: msg });
|
|
4131
4844
|
writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
|
|
4132
4845
|
}
|
|
4133
4846
|
}
|
|
@@ -4151,6 +4864,12 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
4151
4864
|
const prepared = await prepareTurn(message, attachments);
|
|
4152
4865
|
return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
|
|
4153
4866
|
promptSettled = true;
|
|
4867
|
+
if (!done && nativeFollowUpPending.has(input.sessionId)) {
|
|
4868
|
+
clearNativeFollowUpWork(input.sessionId);
|
|
4869
|
+
done = true;
|
|
4870
|
+
stopHeartbeat();
|
|
4871
|
+
if (wake) wake();
|
|
4872
|
+
}
|
|
4154
4873
|
}).catch((err) => {
|
|
4155
4874
|
promptSettled = true;
|
|
4156
4875
|
streamError = err;
|
|
@@ -4207,7 +4926,7 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
4207
4926
|
|
|
4208
4927
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
4209
4928
|
import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
|
|
4210
|
-
import { join as join7, extname as
|
|
4929
|
+
import { join as join7, extname as extname3, resolve as resolve4, sep as sep3, relative as relative5 } from "path";
|
|
4211
4930
|
import { homedir as homedir4 } from "os";
|
|
4212
4931
|
import { pathToFileURL } from "url";
|
|
4213
4932
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
@@ -4233,7 +4952,7 @@ async function fileExists2(path4) {
|
|
|
4233
4952
|
async function discoverFromDir(dir, source) {
|
|
4234
4953
|
if (!await dirExists(dir)) return [];
|
|
4235
4954
|
const entries = await readdir5(dir);
|
|
4236
|
-
return entries.filter((e) => VALID_EXTENSIONS.has(
|
|
4955
|
+
return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join7(dir, e), source }));
|
|
4237
4956
|
}
|
|
4238
4957
|
function extractTools(mod) {
|
|
4239
4958
|
const tools = [];
|
|
@@ -4282,6 +5001,10 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
4282
5001
|
if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep3)) {
|
|
4283
5002
|
return [];
|
|
4284
5003
|
}
|
|
5004
|
+
const relPath = relative5(resolvedPkgDir, resolvedMain);
|
|
5005
|
+
if (relPath.startsWith("..")) {
|
|
5006
|
+
return [];
|
|
5007
|
+
}
|
|
4285
5008
|
return loadModule(resolvedMain, importFn);
|
|
4286
5009
|
}
|
|
4287
5010
|
async function loadExtensionsJson(cwd) {
|
|
@@ -4363,7 +5086,7 @@ import {
|
|
|
4363
5086
|
// src/server/tools/operations/bound.ts
|
|
4364
5087
|
import { constants as constants3 } from "fs";
|
|
4365
5088
|
import { access as access3, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, readdir as readdir6, readlink, realpath as realpath2, stat as stat6, writeFile as writeFile8 } from "fs/promises";
|
|
4366
|
-
import { dirname as
|
|
5089
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve5 } from "path";
|
|
4367
5090
|
function toPosixPath(value) {
|
|
4368
5091
|
return value.split("\\").join("/");
|
|
4369
5092
|
}
|
|
@@ -4420,7 +5143,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
4420
5143
|
for (const entry of entries) {
|
|
4421
5144
|
if (out.length >= limit) return;
|
|
4422
5145
|
const absolutePath = resolve5(current, entry.name);
|
|
4423
|
-
const relativePath = toPosixPath(
|
|
5146
|
+
const relativePath = toPosixPath(relative6(root, absolutePath));
|
|
4424
5147
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
4425
5148
|
if (matchesGlob(relativePath, pattern)) {
|
|
4426
5149
|
out.push(absolutePath);
|
|
@@ -4437,7 +5160,7 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
4437
5160
|
await stat6(current);
|
|
4438
5161
|
return current;
|
|
4439
5162
|
} catch {
|
|
4440
|
-
const parent =
|
|
5163
|
+
const parent = dirname7(current);
|
|
4441
5164
|
if (parent === current) return current;
|
|
4442
5165
|
current = parent;
|
|
4443
5166
|
}
|
|
@@ -4449,10 +5172,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
4449
5172
|
const s = await lstat3(absPath);
|
|
4450
5173
|
if (s.isSymbolicLink()) {
|
|
4451
5174
|
const target = await readlink(absPath);
|
|
4452
|
-
const resolvedTarget = resolve5(
|
|
5175
|
+
const resolvedTarget = resolve5(dirname7(absPath), target);
|
|
4453
5176
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
4454
5177
|
const realAncestor = await realpath2(nearestAncestor);
|
|
4455
|
-
const rel2 =
|
|
5178
|
+
const rel2 = relative6(realRoot, realAncestor);
|
|
4456
5179
|
if (rel2.startsWith("..") || isAbsolute4(rel2)) {
|
|
4457
5180
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4458
5181
|
}
|
|
@@ -4462,1156 +5185,704 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
4462
5185
|
if (err.message?.includes("is outside workspace")) {
|
|
4463
5186
|
throw err;
|
|
4464
5187
|
}
|
|
4465
|
-
const code = err.code;
|
|
4466
|
-
if (code !== "ENOENT") throw err;
|
|
4467
|
-
}
|
|
4468
|
-
let realCandidate;
|
|
4469
|
-
try {
|
|
4470
|
-
realCandidate = await realpath2(absPath);
|
|
4471
|
-
} catch (err) {
|
|
4472
|
-
const code = err.code;
|
|
4473
|
-
if (code === "ENOENT") {
|
|
4474
|
-
const nearestAncestor = await findNearestExistingAncestor(
|
|
4475
|
-
const realAncestor = await realpath2(nearestAncestor);
|
|
4476
|
-
const rel2 =
|
|
4477
|
-
if (rel2.startsWith("..") || isAbsolute4(rel2)) {
|
|
4478
|
-
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4479
|
-
}
|
|
4480
|
-
return;
|
|
4481
|
-
}
|
|
4482
|
-
throw err;
|
|
4483
|
-
}
|
|
4484
|
-
const rel =
|
|
4485
|
-
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
|
4486
|
-
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
function boundFs(workspaceRoot) {
|
|
4490
|
-
const read = {
|
|
4491
|
-
async readFile(absolutePath) {
|
|
4492
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4493
|
-
return await readFile8(absolutePath);
|
|
4494
|
-
},
|
|
4495
|
-
async access(absolutePath) {
|
|
4496
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4497
|
-
await access3(absolutePath, constants3.R_OK);
|
|
4498
|
-
}
|
|
4499
|
-
};
|
|
4500
|
-
const write = {
|
|
4501
|
-
async writeFile(absolutePath, content) {
|
|
4502
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4503
|
-
await mkdir10(dirname6(absolutePath), { recursive: true });
|
|
4504
|
-
await writeFile8(absolutePath, content);
|
|
4505
|
-
},
|
|
4506
|
-
async mkdir(dir) {
|
|
4507
|
-
await assertWithinWorkspace(workspaceRoot, dir);
|
|
4508
|
-
await mkdir10(dir, { recursive: true });
|
|
4509
|
-
}
|
|
4510
|
-
};
|
|
4511
|
-
const edit = {
|
|
4512
|
-
async readFile(absolutePath) {
|
|
4513
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4514
|
-
return await readFile8(absolutePath);
|
|
4515
|
-
},
|
|
4516
|
-
async writeFile(absolutePath, content) {
|
|
4517
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4518
|
-
await writeFile8(absolutePath, content);
|
|
4519
|
-
},
|
|
4520
|
-
async access(absolutePath) {
|
|
4521
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4522
|
-
await access3(absolutePath, constants3.R_OK | constants3.W_OK);
|
|
4523
|
-
}
|
|
4524
|
-
};
|
|
4525
|
-
const find = {
|
|
4526
|
-
async exists(absolutePath) {
|
|
4527
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4528
|
-
try {
|
|
4529
|
-
await stat6(absolutePath);
|
|
4530
|
-
return true;
|
|
4531
|
-
} catch (err) {
|
|
4532
|
-
if (err.code === "ENOENT") return false;
|
|
4533
|
-
throw err;
|
|
4534
|
-
}
|
|
4535
|
-
},
|
|
4536
|
-
async glob(pattern, cwd, options) {
|
|
4537
|
-
await assertWithinWorkspace(workspaceRoot, cwd);
|
|
4538
|
-
const matches = [];
|
|
4539
|
-
await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
|
|
4540
|
-
return matches;
|
|
4541
|
-
}
|
|
4542
|
-
};
|
|
4543
|
-
const grep = {
|
|
4544
|
-
async isDirectory(absolutePath) {
|
|
4545
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4546
|
-
return (await stat6(absolutePath)).isDirectory();
|
|
4547
|
-
},
|
|
4548
|
-
async readFile(absolutePath) {
|
|
4549
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4550
|
-
return await readFile8(absolutePath, "utf8");
|
|
4551
|
-
}
|
|
4552
|
-
};
|
|
4553
|
-
const ls = {
|
|
4554
|
-
async exists(absolutePath) {
|
|
4555
|
-
try {
|
|
4556
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4557
|
-
await stat6(absolutePath);
|
|
4558
|
-
return true;
|
|
4559
|
-
} catch {
|
|
4560
|
-
return false;
|
|
4561
|
-
}
|
|
4562
|
-
},
|
|
4563
|
-
async stat(absolutePath) {
|
|
4564
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4565
|
-
return await stat6(absolutePath);
|
|
4566
|
-
},
|
|
4567
|
-
async readdir(absolutePath) {
|
|
4568
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4569
|
-
return await readdir6(absolutePath);
|
|
4570
|
-
}
|
|
4571
|
-
};
|
|
4572
|
-
return { read, write, edit, find, grep, ls };
|
|
4573
|
-
}
|
|
4574
|
-
|
|
4575
|
-
// src/server/tools/operations/vercel.ts
|
|
4576
|
-
import { isAbsolute as isAbsolute5, relative as relative5 } from "path";
|
|
4577
|
-
function rootAliases(workspace) {
|
|
4578
|
-
const aliases = [workspace.root];
|
|
4579
|
-
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
|
|
4580
|
-
if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
4581
|
-
return aliases;
|
|
4582
|
-
}
|
|
4583
|
-
function isOutsideWorkspaceRel(rel) {
|
|
4584
|
-
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute5(rel);
|
|
4585
|
-
}
|
|
4586
|
-
function toRelPath(workspace, absolutePath) {
|
|
4587
|
-
for (const root of rootAliases(workspace)) {
|
|
4588
|
-
const rel = relative5(root, absolutePath);
|
|
4589
|
-
if (!isOutsideWorkspaceRel(rel)) return rel;
|
|
4590
|
-
}
|
|
4591
|
-
const skillMarker = "/.agents/skills/";
|
|
4592
|
-
const skillIndex = absolutePath.indexOf(skillMarker);
|
|
4593
|
-
if (skillIndex >= 0) {
|
|
4594
|
-
const skillPath = absolutePath.slice(skillIndex + skillMarker.length);
|
|
4595
|
-
if (skillPath.includes("\0") || skillPath.split(/[\\/]+/).includes("..")) {
|
|
4596
|
-
throw new Error(`path "${absolutePath}" escapes the workspace skills directory`);
|
|
4597
|
-
}
|
|
4598
|
-
return `.agents/skills/${skillPath}`;
|
|
4599
|
-
}
|
|
4600
|
-
throw new Error(
|
|
4601
|
-
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
|
|
4602
|
-
);
|
|
4603
|
-
}
|
|
4604
|
-
function vercelBashOps(sandbox) {
|
|
4605
|
-
return {
|
|
4606
|
-
exec(command, cwd, { onData, signal, timeout, env }) {
|
|
4607
|
-
const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
|
|
4608
|
-
return sandbox.exec(command, {
|
|
4609
|
-
cwd,
|
|
4610
|
-
env: filteredEnv,
|
|
4611
|
-
signal,
|
|
4612
|
-
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
4613
|
-
onStdout: (chunk2) => onData(Buffer.from(chunk2)),
|
|
4614
|
-
onStderr: (chunk2) => onData(Buffer.from(chunk2))
|
|
4615
|
-
}).then((result) => ({ exitCode: result.exitCode }));
|
|
4616
|
-
}
|
|
4617
|
-
};
|
|
5188
|
+
const code = err.code;
|
|
5189
|
+
if (code !== "ENOENT") throw err;
|
|
5190
|
+
}
|
|
5191
|
+
let realCandidate;
|
|
5192
|
+
try {
|
|
5193
|
+
realCandidate = await realpath2(absPath);
|
|
5194
|
+
} catch (err) {
|
|
5195
|
+
const code = err.code;
|
|
5196
|
+
if (code === "ENOENT") {
|
|
5197
|
+
const nearestAncestor = await findNearestExistingAncestor(dirname7(absPath));
|
|
5198
|
+
const realAncestor = await realpath2(nearestAncestor);
|
|
5199
|
+
const rel2 = relative6(realRoot, realAncestor);
|
|
5200
|
+
if (rel2.startsWith("..") || isAbsolute4(rel2)) {
|
|
5201
|
+
throw new Error(`path "${absPath}" is outside workspace`);
|
|
5202
|
+
}
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
throw err;
|
|
5206
|
+
}
|
|
5207
|
+
const rel = relative6(realRoot, realCandidate);
|
|
5208
|
+
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
|
5209
|
+
throw new Error(`path "${absPath}" is outside workspace`);
|
|
5210
|
+
}
|
|
4618
5211
|
}
|
|
4619
|
-
function
|
|
4620
|
-
|
|
5212
|
+
function boundFs(workspaceRoot) {
|
|
5213
|
+
const read = {
|
|
4621
5214
|
async readFile(absolutePath) {
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
return Buffer.from(content, "utf-8");
|
|
5215
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5216
|
+
return await readFile8(absolutePath);
|
|
4625
5217
|
},
|
|
4626
5218
|
async access(absolutePath) {
|
|
4627
|
-
|
|
4628
|
-
await
|
|
5219
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5220
|
+
await access3(absolutePath, constants3.R_OK);
|
|
4629
5221
|
}
|
|
4630
5222
|
};
|
|
4631
|
-
|
|
4632
|
-
function vercelWriteOps(workspace) {
|
|
4633
|
-
return {
|
|
5223
|
+
const write = {
|
|
4634
5224
|
async writeFile(absolutePath, content) {
|
|
4635
|
-
|
|
4636
|
-
await
|
|
5225
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5226
|
+
await mkdir10(dirname7(absolutePath), { recursive: true });
|
|
5227
|
+
await writeFile8(absolutePath, content);
|
|
4637
5228
|
},
|
|
4638
5229
|
async mkdir(dir) {
|
|
4639
|
-
|
|
4640
|
-
await
|
|
5230
|
+
await assertWithinWorkspace(workspaceRoot, dir);
|
|
5231
|
+
await mkdir10(dir, { recursive: true });
|
|
4641
5232
|
}
|
|
4642
5233
|
};
|
|
4643
|
-
|
|
4644
|
-
function vercelEditOps(workspace) {
|
|
4645
|
-
return {
|
|
5234
|
+
const edit = {
|
|
4646
5235
|
async readFile(absolutePath) {
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
return Buffer.from(content, "utf-8");
|
|
5236
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5237
|
+
return await readFile8(absolutePath);
|
|
4650
5238
|
},
|
|
4651
5239
|
async writeFile(absolutePath, content) {
|
|
4652
|
-
|
|
4653
|
-
await
|
|
5240
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5241
|
+
await writeFile8(absolutePath, content);
|
|
4654
5242
|
},
|
|
4655
5243
|
async access(absolutePath) {
|
|
4656
|
-
|
|
4657
|
-
await
|
|
5244
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5245
|
+
await access3(absolutePath, constants3.R_OK | constants3.W_OK);
|
|
4658
5246
|
}
|
|
4659
5247
|
};
|
|
4660
|
-
|
|
4661
|
-
function toRemotePath2(value) {
|
|
4662
|
-
if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
4663
|
-
if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
4664
|
-
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
|
|
4665
|
-
}
|
|
4666
|
-
return value;
|
|
4667
|
-
}
|
|
4668
|
-
function findPredicate(pattern) {
|
|
4669
|
-
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
4670
|
-
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
4671
|
-
let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
|
|
4672
|
-
if (!translated.startsWith("*")) translated = `*${translated}`;
|
|
4673
|
-
return `-path ${shellEscape(translated)}`;
|
|
4674
|
-
}
|
|
4675
|
-
function findIgnoreArgs(cwd, ignore) {
|
|
4676
|
-
return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
|
|
4677
|
-
}
|
|
4678
|
-
function fallbackFindCommand(pattern, cwd, options) {
|
|
4679
|
-
return [
|
|
4680
|
-
"find",
|
|
4681
|
-
shellEscape(cwd),
|
|
4682
|
-
"-maxdepth 20",
|
|
4683
|
-
"-type f",
|
|
4684
|
-
findIgnoreArgs(cwd, options.ignore),
|
|
4685
|
-
findPredicate(pattern),
|
|
4686
|
-
`| head -n ${Math.max(1, Math.trunc(options.limit))}`
|
|
4687
|
-
].filter(Boolean).join(" ");
|
|
4688
|
-
}
|
|
4689
|
-
function isFdMissing(result) {
|
|
4690
|
-
if (result.exitCode !== 127) return false;
|
|
4691
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8");
|
|
4692
|
-
return /\bfd: (?:not found|command not found)\b/i.test(stderr);
|
|
4693
|
-
}
|
|
4694
|
-
function vercelFindOps(sandbox, workspace) {
|
|
4695
|
-
return {
|
|
5248
|
+
const find = {
|
|
4696
5249
|
async exists(absolutePath) {
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
}
|
|
5250
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5251
|
+
try {
|
|
5252
|
+
await stat6(absolutePath);
|
|
5253
|
+
return true;
|
|
5254
|
+
} catch (err) {
|
|
5255
|
+
if (err.code === "ENOENT") return false;
|
|
5256
|
+
throw err;
|
|
4705
5257
|
}
|
|
4706
|
-
const result = await sandbox.exec(`test -e ${shellEscape(absolutePath)}`, {
|
|
4707
|
-
timeoutMs: 5e3
|
|
4708
|
-
});
|
|
4709
|
-
return result.exitCode === 0;
|
|
4710
5258
|
},
|
|
4711
5259
|
async glob(pattern, cwd, options) {
|
|
4712
|
-
|
|
4713
|
-
const
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
}
|
|
4717
|
-
args.push(pattern, remoteCwd);
|
|
4718
|
-
let result = await sandbox.exec(args.map(shellEscape).join(" "), {
|
|
4719
|
-
timeoutMs: 3e4,
|
|
4720
|
-
maxOutputBytes: 1048576
|
|
4721
|
-
});
|
|
4722
|
-
if (isFdMissing(result)) {
|
|
4723
|
-
result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
|
|
4724
|
-
timeoutMs: 3e4,
|
|
4725
|
-
maxOutputBytes: 1048576
|
|
4726
|
-
});
|
|
4727
|
-
}
|
|
4728
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
4729
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
4730
|
-
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
4731
|
-
}
|
|
4732
|
-
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
4733
|
-
return stdout.split("\n").filter(Boolean);
|
|
5260
|
+
await assertWithinWorkspace(workspaceRoot, cwd);
|
|
5261
|
+
const matches = [];
|
|
5262
|
+
await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
|
|
5263
|
+
return matches;
|
|
4734
5264
|
}
|
|
4735
5265
|
};
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
try {
|
|
4741
|
-
const rel = toRelPath(workspace, absolutePath);
|
|
4742
|
-
await workspace.stat(rel);
|
|
4743
|
-
return true;
|
|
4744
|
-
} catch {
|
|
4745
|
-
return false;
|
|
4746
|
-
}
|
|
4747
|
-
},
|
|
4748
|
-
async stat(absolutePath) {
|
|
4749
|
-
const rel = toRelPath(workspace, absolutePath);
|
|
4750
|
-
const s = await workspace.stat(rel);
|
|
4751
|
-
return { isDirectory: () => s.kind === "dir" };
|
|
5266
|
+
const grep = {
|
|
5267
|
+
async isDirectory(absolutePath) {
|
|
5268
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5269
|
+
return (await stat6(absolutePath)).isDirectory();
|
|
4752
5270
|
},
|
|
4753
|
-
async
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
return entries.map((e) => e.name);
|
|
5271
|
+
async readFile(absolutePath) {
|
|
5272
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5273
|
+
return await readFile8(absolutePath, "utf8");
|
|
4757
5274
|
}
|
|
4758
5275
|
};
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
formatSize,
|
|
4768
|
-
truncateHead,
|
|
4769
|
-
truncateLine
|
|
4770
|
-
} from "@mariozechner/pi-coding-agent";
|
|
4771
|
-
import { resolve as resolve6, relative as relative6 } from "path";
|
|
4772
|
-
|
|
4773
|
-
// src/server/catalog/tools/_shared.ts
|
|
4774
|
-
function makeError(message) {
|
|
4775
|
-
return {
|
|
4776
|
-
content: [{ type: "text", text: message }],
|
|
4777
|
-
isError: true
|
|
4778
|
-
};
|
|
4779
|
-
}
|
|
4780
|
-
function bytesWritten(content) {
|
|
4781
|
-
return new TextEncoder().encode(content).byteLength;
|
|
4782
|
-
}
|
|
4783
|
-
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
4784
|
-
function decode(bytes) {
|
|
4785
|
-
return decoder.decode(bytes);
|
|
4786
|
-
}
|
|
4787
|
-
|
|
4788
|
-
// src/server/tools/vercelGrepTool.ts
|
|
4789
|
-
var PI_GREP_TOOL = createGrepToolDefinition("/");
|
|
4790
|
-
var DEFAULT_LIMIT2 = 100;
|
|
4791
|
-
var GREP_MAX_LINE_LENGTH = 500;
|
|
4792
|
-
var GREP_TIMEOUT_MS = 3e4;
|
|
4793
|
-
var GREP_MAX_OUTPUT_BYTES = 2097152;
|
|
4794
|
-
function quoteShell(value) {
|
|
4795
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4796
|
-
}
|
|
4797
|
-
function numberParam(value, fallback, min = 1) {
|
|
4798
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
4799
|
-
return Math.max(min, Math.floor(value));
|
|
4800
|
-
}
|
|
4801
|
-
function optionalStringParam(value) {
|
|
4802
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4803
|
-
}
|
|
4804
|
-
function buildRgCommand(params) {
|
|
4805
|
-
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
4806
|
-
if (params.ignoreCase === true) args.push("--ignore-case");
|
|
4807
|
-
if (params.literal === true) args.push("--fixed-strings");
|
|
4808
|
-
const glob = optionalStringParam(params.glob);
|
|
4809
|
-
if (glob) args.push("--glob", quoteShell(glob));
|
|
4810
|
-
const context = numberParam(params.context, 0, 0);
|
|
4811
|
-
if (context > 0) args.push("--context", String(context));
|
|
4812
|
-
const pattern = params.pattern;
|
|
4813
|
-
const searchPath = optionalStringParam(params.path) ?? ".";
|
|
4814
|
-
args.push("--", quoteShell(pattern), quoteShell(searchPath));
|
|
4815
|
-
return `rg ${args.join(" ")}`;
|
|
4816
|
-
}
|
|
4817
|
-
function parseRgJson(stdout, limit) {
|
|
4818
|
-
const outputLines = [];
|
|
4819
|
-
let matchCount = 0;
|
|
4820
|
-
let matchLimitReached = false;
|
|
4821
|
-
let linesTruncated = false;
|
|
4822
|
-
for (const rawLine of stdout.split("\n")) {
|
|
4823
|
-
if (rawLine.trim().length === 0) continue;
|
|
4824
|
-
let event;
|
|
4825
|
-
try {
|
|
4826
|
-
event = JSON.parse(rawLine);
|
|
4827
|
-
} catch {
|
|
4828
|
-
continue;
|
|
4829
|
-
}
|
|
4830
|
-
const filePath = event.data?.path?.text;
|
|
4831
|
-
const lineNumber = event.data?.line_number;
|
|
4832
|
-
const lineText = event.data?.lines?.text;
|
|
4833
|
-
if (!filePath || typeof lineNumber !== "number" || typeof lineText !== "string") {
|
|
4834
|
-
continue;
|
|
4835
|
-
}
|
|
4836
|
-
const isMatch = event.type === "match";
|
|
4837
|
-
if (isMatch) {
|
|
4838
|
-
matchCount += 1;
|
|
4839
|
-
if (matchCount > limit) {
|
|
4840
|
-
matchLimitReached = true;
|
|
4841
|
-
break;
|
|
5276
|
+
const ls = {
|
|
5277
|
+
async exists(absolutePath) {
|
|
5278
|
+
try {
|
|
5279
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5280
|
+
await stat6(absolutePath);
|
|
5281
|
+
return true;
|
|
5282
|
+
} catch {
|
|
5283
|
+
return false;
|
|
4842
5284
|
}
|
|
4843
|
-
}
|
|
4844
|
-
|
|
5285
|
+
},
|
|
5286
|
+
async stat(absolutePath) {
|
|
5287
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5288
|
+
return await stat6(absolutePath);
|
|
5289
|
+
},
|
|
5290
|
+
async readdir(absolutePath) {
|
|
5291
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5292
|
+
return await readdir6(absolutePath);
|
|
4845
5293
|
}
|
|
4846
|
-
const sanitized = lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
4847
|
-
const { text, wasTruncated } = truncateLine(sanitized);
|
|
4848
|
-
if (wasTruncated) linesTruncated = true;
|
|
4849
|
-
const separator = isMatch ? ":" : "-";
|
|
4850
|
-
outputLines.push(`${filePath}${separator}${lineNumber}${separator} ${text}`);
|
|
4851
|
-
}
|
|
4852
|
-
return { outputLines, matchCount, matchLimitReached, linesTruncated };
|
|
4853
|
-
}
|
|
4854
|
-
function syntheticExecTruncation(output) {
|
|
4855
|
-
const outputLines = output.length === 0 ? 0 : output.split("\n").length;
|
|
4856
|
-
const outputBytes = bytesWritten(output);
|
|
4857
|
-
return {
|
|
4858
|
-
content: output,
|
|
4859
|
-
truncated: true,
|
|
4860
|
-
truncatedBy: "bytes",
|
|
4861
|
-
totalLines: outputLines,
|
|
4862
|
-
totalBytes: outputBytes,
|
|
4863
|
-
outputLines,
|
|
4864
|
-
outputBytes,
|
|
4865
|
-
lastLinePartial: false,
|
|
4866
|
-
firstLineExceedsLimit: false,
|
|
4867
|
-
maxLines: Number.MAX_SAFE_INTEGER,
|
|
4868
|
-
maxBytes: GREP_MAX_OUTPUT_BYTES
|
|
4869
5294
|
};
|
|
5295
|
+
return { read, write, edit, find, grep, ls };
|
|
4870
5296
|
}
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
);
|
|
4887
|
-
details.matchLimitReached = effectiveLimit;
|
|
4888
|
-
}
|
|
4889
|
-
if (truncation.truncated || sandboxTruncated) {
|
|
4890
|
-
const effectiveTruncation = truncation.truncated ? truncation : syntheticExecTruncation(output);
|
|
4891
|
-
notices.push(`${formatSize(effectiveTruncation.maxBytes)} limit reached`);
|
|
4892
|
-
details.truncation = effectiveTruncation;
|
|
5297
|
+
|
|
5298
|
+
// src/server/tools/operations/vercel.ts
|
|
5299
|
+
import { isAbsolute as isAbsolute5, relative as relative7 } from "path";
|
|
5300
|
+
function rootAliases(workspace) {
|
|
5301
|
+
const aliases = [workspace.root];
|
|
5302
|
+
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
|
|
5303
|
+
if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
5304
|
+
return aliases;
|
|
5305
|
+
}
|
|
5306
|
+
function isOutsideWorkspaceRel(rel) {
|
|
5307
|
+
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute5(rel);
|
|
5308
|
+
}
|
|
5309
|
+
function toRelPath(workspace, absolutePath) {
|
|
5310
|
+
for (const root of rootAliases(workspace)) {
|
|
5311
|
+
const rel = relative7(root, absolutePath);
|
|
5312
|
+
if (!isOutsideWorkspaceRel(rel)) return rel;
|
|
4893
5313
|
}
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
);
|
|
4898
|
-
|
|
5314
|
+
const skillMarker = "/.agents/skills/";
|
|
5315
|
+
const skillIndex = absolutePath.indexOf(skillMarker);
|
|
5316
|
+
if (skillIndex >= 0) {
|
|
5317
|
+
const skillPath = absolutePath.slice(skillIndex + skillMarker.length);
|
|
5318
|
+
if (skillPath.includes("\0") || skillPath.split(/[\\/]+/).includes("..")) {
|
|
5319
|
+
throw new Error(`path "${absolutePath}" escapes the workspace skills directory`);
|
|
5320
|
+
}
|
|
5321
|
+
return `.agents/skills/${skillPath}`;
|
|
4899
5322
|
}
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
5323
|
+
throw new Error(
|
|
5324
|
+
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
|
|
5325
|
+
);
|
|
5326
|
+
}
|
|
5327
|
+
function vercelBashOps(sandbox) {
|
|
4903
5328
|
return {
|
|
4904
|
-
|
|
4905
|
-
|
|
5329
|
+
exec(command, cwd, { onData, signal, timeout, env }) {
|
|
5330
|
+
const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
|
|
5331
|
+
return sandbox.exec(command, {
|
|
5332
|
+
cwd,
|
|
5333
|
+
env: filteredEnv,
|
|
5334
|
+
signal,
|
|
5335
|
+
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
5336
|
+
onStdout: (chunk2) => onData(Buffer.from(chunk2)),
|
|
5337
|
+
onStderr: (chunk2) => onData(Buffer.from(chunk2))
|
|
5338
|
+
}).then((result) => ({ exitCode: result.exitCode }));
|
|
5339
|
+
}
|
|
4906
5340
|
};
|
|
4907
5341
|
}
|
|
4908
|
-
function
|
|
5342
|
+
function vercelReadOps(workspace) {
|
|
4909
5343
|
return {
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
}
|
|
4919
|
-
if (typeof params.pattern !== "string" || params.pattern.length === 0) {
|
|
4920
|
-
return makeError("pattern is required");
|
|
4921
|
-
}
|
|
4922
|
-
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
4923
|
-
const resolved = resolve6(workspaceRoot, params.path);
|
|
4924
|
-
const rel = relative6(workspaceRoot, resolved);
|
|
4925
|
-
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
4926
|
-
return makeError(`path "${params.path}" is outside workspace`);
|
|
4927
|
-
}
|
|
4928
|
-
}
|
|
4929
|
-
try {
|
|
4930
|
-
const result = await sandbox.exec(buildRgCommand(params), {
|
|
4931
|
-
signal: ctx.abortSignal,
|
|
4932
|
-
timeoutMs: GREP_TIMEOUT_MS,
|
|
4933
|
-
maxOutputBytes: GREP_MAX_OUTPUT_BYTES
|
|
4934
|
-
});
|
|
4935
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
4936
|
-
const stderr = decode(result.stderr).trim();
|
|
4937
|
-
const message = stderr || `ripgrep exited with code ${result.exitCode}`;
|
|
4938
|
-
return makeError(`grep failed: ${message}`);
|
|
4939
|
-
}
|
|
4940
|
-
const limit = numberParam(params.limit, DEFAULT_LIMIT2);
|
|
4941
|
-
const parsed = parseRgJson(decode(result.stdout), limit);
|
|
4942
|
-
return buildSuccessResult(parsed, limit, result.truncated);
|
|
4943
|
-
} catch (error) {
|
|
4944
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
4945
|
-
return makeError(`grep failed: ${message}`);
|
|
4946
|
-
}
|
|
5344
|
+
async readFile(absolutePath) {
|
|
5345
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5346
|
+
const content = await workspace.readFile(rel);
|
|
5347
|
+
return Buffer.from(content, "utf-8");
|
|
5348
|
+
},
|
|
5349
|
+
async access(absolutePath) {
|
|
5350
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5351
|
+
await workspace.stat(rel);
|
|
4947
5352
|
}
|
|
4948
5353
|
};
|
|
4949
5354
|
}
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
5355
|
+
function vercelWriteOps(workspace) {
|
|
5356
|
+
return {
|
|
5357
|
+
async writeFile(absolutePath, content) {
|
|
5358
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5359
|
+
await workspace.writeFile(rel, content);
|
|
5360
|
+
},
|
|
5361
|
+
async mkdir(dir) {
|
|
5362
|
+
const rel = toRelPath(workspace, dir);
|
|
5363
|
+
await workspace.mkdir(rel, { recursive: true });
|
|
5364
|
+
}
|
|
5365
|
+
};
|
|
4954
5366
|
}
|
|
4955
|
-
function
|
|
5367
|
+
function vercelEditOps(workspace) {
|
|
4956
5368
|
return {
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
} : void 0,
|
|
4970
|
-
{}
|
|
4971
|
-
);
|
|
4972
|
-
const textContent = (result.content ?? []).filter(isTextContent).map((c) => ({ type: "text", text: c.text }));
|
|
4973
|
-
return {
|
|
4974
|
-
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
4975
|
-
isError: false,
|
|
4976
|
-
details: result.details
|
|
4977
|
-
};
|
|
5369
|
+
async readFile(absolutePath) {
|
|
5370
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5371
|
+
const content = await workspace.readFile(rel);
|
|
5372
|
+
return Buffer.from(content, "utf-8");
|
|
5373
|
+
},
|
|
5374
|
+
async writeFile(absolutePath, content) {
|
|
5375
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5376
|
+
await workspace.writeFile(rel, content);
|
|
5377
|
+
},
|
|
5378
|
+
async access(absolutePath) {
|
|
5379
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5380
|
+
await workspace.stat(rel);
|
|
4978
5381
|
}
|
|
4979
5382
|
};
|
|
4980
5383
|
}
|
|
4981
|
-
function
|
|
4982
|
-
|
|
4983
|
-
if (
|
|
4984
|
-
return
|
|
4985
|
-
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
4986
|
-
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
4987
|
-
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
4988
|
-
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
4989
|
-
vercelGrepTool(bundle.sandbox, cwd),
|
|
4990
|
-
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
4991
|
-
];
|
|
5384
|
+
function toRemotePath2(value) {
|
|
5385
|
+
if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
5386
|
+
if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
5387
|
+
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
|
|
4992
5388
|
}
|
|
4993
|
-
|
|
4994
|
-
return [
|
|
4995
|
-
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
4996
|
-
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
4997
|
-
adaptPiTool(createEditToolDefinition(cwd, { operations: ops.edit })),
|
|
4998
|
-
adaptPiTool(createFindToolDefinition(cwd, { operations: ops.find })),
|
|
4999
|
-
adaptPiTool(createGrepToolDefinition2(cwd, { operations: ops.grep })),
|
|
5000
|
-
adaptPiTool(createLsToolDefinition(cwd, { operations: ops.ls }))
|
|
5001
|
-
];
|
|
5002
|
-
}
|
|
5003
|
-
|
|
5004
|
-
// src/server/tools/harness/index.ts
|
|
5005
|
-
import {
|
|
5006
|
-
createBashToolDefinition,
|
|
5007
|
-
createLocalBashOperations
|
|
5008
|
-
} from "@mariozechner/pi-coding-agent";
|
|
5009
|
-
function shellEscape2(s) {
|
|
5010
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5389
|
+
return value;
|
|
5011
5390
|
}
|
|
5012
|
-
function
|
|
5013
|
-
const
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
env: withWorkspacePythonEnv({
|
|
5019
|
-
workspaceRoot,
|
|
5020
|
-
env: context.env,
|
|
5021
|
-
sandboxRoot: "/workspace"
|
|
5022
|
-
})
|
|
5023
|
-
});
|
|
5391
|
+
function findPredicate(pattern) {
|
|
5392
|
+
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
5393
|
+
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
5394
|
+
let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
|
|
5395
|
+
if (!translated.startsWith("*")) translated = `*${translated}`;
|
|
5396
|
+
return `-path ${shellEscape(translated)}`;
|
|
5024
5397
|
}
|
|
5025
|
-
function
|
|
5026
|
-
return (
|
|
5027
|
-
...context,
|
|
5028
|
-
env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
|
|
5029
|
-
});
|
|
5398
|
+
function findIgnoreArgs(cwd, ignore) {
|
|
5399
|
+
return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
|
|
5030
5400
|
}
|
|
5031
|
-
function
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
return {
|
|
5042
|
-
operations: createLocalBashOperations(),
|
|
5043
|
-
spawnHook: directSpawnHook(bundle.workspace.root)
|
|
5044
|
-
};
|
|
5045
|
-
}
|
|
5401
|
+
function fallbackFindCommand(pattern, cwd, options) {
|
|
5402
|
+
return [
|
|
5403
|
+
"find",
|
|
5404
|
+
shellEscape(cwd),
|
|
5405
|
+
"-maxdepth 20",
|
|
5406
|
+
"-type f",
|
|
5407
|
+
findIgnoreArgs(cwd, options.ignore),
|
|
5408
|
+
findPredicate(pattern),
|
|
5409
|
+
`| head -n ${Math.max(1, Math.trunc(options.limit))}`
|
|
5410
|
+
].filter(Boolean).join(" ");
|
|
5046
5411
|
}
|
|
5047
|
-
function
|
|
5048
|
-
return
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
promptSnippet: piTool.promptSnippet,
|
|
5052
|
-
parameters: piTool.parameters,
|
|
5053
|
-
async execute(params, ctx) {
|
|
5054
|
-
const result = await piTool.execute(
|
|
5055
|
-
ctx.toolCallId,
|
|
5056
|
-
params,
|
|
5057
|
-
ctx.abortSignal,
|
|
5058
|
-
ctx.onUpdate ? (update) => {
|
|
5059
|
-
const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
5060
|
-
ctx.onUpdate(text);
|
|
5061
|
-
} : void 0,
|
|
5062
|
-
{}
|
|
5063
|
-
);
|
|
5064
|
-
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
5065
|
-
return {
|
|
5066
|
-
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5067
|
-
isError: false,
|
|
5068
|
-
details: result.details
|
|
5069
|
-
};
|
|
5070
|
-
}
|
|
5071
|
-
};
|
|
5412
|
+
function isFdMissing(result) {
|
|
5413
|
+
if (result.exitCode !== 127) return false;
|
|
5414
|
+
const stderr = Buffer.from(result.stderr).toString("utf-8");
|
|
5415
|
+
return /\bfd: (?:not found|command not found)\b/i.test(stderr);
|
|
5072
5416
|
}
|
|
5073
|
-
function
|
|
5417
|
+
function vercelFindOps(sandbox, workspace) {
|
|
5074
5418
|
return {
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5419
|
+
async exists(absolutePath) {
|
|
5420
|
+
if (workspace) {
|
|
5421
|
+
try {
|
|
5422
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5423
|
+
await workspace.stat(rel);
|
|
5424
|
+
return true;
|
|
5425
|
+
} catch {
|
|
5426
|
+
return false;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
const result = await sandbox.exec(`test -e ${shellEscape(absolutePath)}`, {
|
|
5430
|
+
timeoutMs: 5e3
|
|
5431
|
+
});
|
|
5432
|
+
return result.exitCode === 0;
|
|
5089
5433
|
},
|
|
5090
|
-
async
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
};
|
|
5434
|
+
async glob(pattern, cwd, options) {
|
|
5435
|
+
const remoteCwd = toRemotePath2(cwd);
|
|
5436
|
+
const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
|
|
5437
|
+
for (const ig of options.ignore) {
|
|
5438
|
+
args.push("--exclude", ig);
|
|
5096
5439
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5440
|
+
args.push(pattern, remoteCwd);
|
|
5441
|
+
let result = await sandbox.exec(args.map(shellEscape).join(" "), {
|
|
5442
|
+
timeoutMs: 3e4,
|
|
5443
|
+
maxOutputBytes: 1048576
|
|
5444
|
+
});
|
|
5445
|
+
if (isFdMissing(result)) {
|
|
5446
|
+
result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
|
|
5447
|
+
timeoutMs: 3e4,
|
|
5448
|
+
maxOutputBytes: 1048576
|
|
5449
|
+
});
|
|
5101
5450
|
}
|
|
5102
|
-
if (
|
|
5103
|
-
|
|
5451
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
5452
|
+
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
5453
|
+
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
5104
5454
|
}
|
|
5455
|
+
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
5456
|
+
return stdout.split("\n").filter(Boolean);
|
|
5457
|
+
}
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
function vercelLsOps(workspace) {
|
|
5461
|
+
return {
|
|
5462
|
+
async exists(absolutePath) {
|
|
5105
5463
|
try {
|
|
5106
|
-
const
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : void 0,
|
|
5112
|
-
vmSize: input.vmSize === "xxs" || input.vmSize === "xs" || input.vmSize === "s" || input.vmSize === "m" || input.vmSize === "l" ? input.vmSize : void 0
|
|
5113
|
-
});
|
|
5114
|
-
return {
|
|
5115
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
5116
|
-
isError: result.exitCode !== 0,
|
|
5117
|
-
details: result
|
|
5118
|
-
};
|
|
5119
|
-
} catch (error) {
|
|
5120
|
-
return {
|
|
5121
|
-
content: [{ type: "text", text: error instanceof Error ? error.message : "execute_isolated_code failed" }],
|
|
5122
|
-
isError: true
|
|
5123
|
-
};
|
|
5464
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5465
|
+
await workspace.stat(rel);
|
|
5466
|
+
return true;
|
|
5467
|
+
} catch {
|
|
5468
|
+
return false;
|
|
5124
5469
|
}
|
|
5470
|
+
},
|
|
5471
|
+
async stat(absolutePath) {
|
|
5472
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5473
|
+
const s = await workspace.stat(rel);
|
|
5474
|
+
return { isDirectory: () => s.kind === "dir" };
|
|
5475
|
+
},
|
|
5476
|
+
async readdir(absolutePath) {
|
|
5477
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5478
|
+
const entries = await workspace.readdir(rel);
|
|
5479
|
+
return entries.map((e) => e.name);
|
|
5125
5480
|
}
|
|
5126
5481
|
};
|
|
5127
5482
|
}
|
|
5128
|
-
function
|
|
5129
|
-
|
|
5130
|
-
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
|
|
5131
|
-
];
|
|
5132
|
-
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
5133
|
-
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
5134
|
-
}
|
|
5135
|
-
return tools;
|
|
5483
|
+
function shellEscape(arg) {
|
|
5484
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
5136
5485
|
}
|
|
5137
5486
|
|
|
5138
|
-
// src/server/
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
var ERROR_CODE_NOT_IMPLEMENTED = "not_implemented";
|
|
5150
|
-
var DEV_MODE_WARNING = "No auth token set \u2014 running in dev mode";
|
|
5151
|
-
var DEFAULT_WORKSPACE_ID = "default";
|
|
5152
|
-
function errorPayload(code, message, field) {
|
|
5487
|
+
// src/server/tools/vercelGrepTool.ts
|
|
5488
|
+
import {
|
|
5489
|
+
createGrepToolDefinition,
|
|
5490
|
+
formatSize,
|
|
5491
|
+
truncateHead,
|
|
5492
|
+
truncateLine
|
|
5493
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5494
|
+
import { resolve as resolve6, relative as relative8 } from "path";
|
|
5495
|
+
|
|
5496
|
+
// src/server/catalog/tools/_shared.ts
|
|
5497
|
+
function makeError(message) {
|
|
5153
5498
|
return {
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5499
|
+
content: [{ type: "text", text: message }],
|
|
5500
|
+
isError: true
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
function bytesWritten(content) {
|
|
5504
|
+
return new TextEncoder().encode(content).byteLength;
|
|
5505
|
+
}
|
|
5506
|
+
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
5507
|
+
function decode(bytes) {
|
|
5508
|
+
return decoder.decode(bytes);
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
// src/server/tools/vercelGrepTool.ts
|
|
5512
|
+
var PI_GREP_TOOL = createGrepToolDefinition("/");
|
|
5513
|
+
var DEFAULT_LIMIT2 = 100;
|
|
5514
|
+
var GREP_MAX_LINE_LENGTH = 500;
|
|
5515
|
+
var GREP_TIMEOUT_MS = 3e4;
|
|
5516
|
+
var GREP_MAX_OUTPUT_BYTES = 2097152;
|
|
5517
|
+
function quoteShell(value) {
|
|
5518
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5519
|
+
}
|
|
5520
|
+
function numberParam(value, fallback, min = 1) {
|
|
5521
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
5522
|
+
return Math.max(min, Math.floor(value));
|
|
5523
|
+
}
|
|
5524
|
+
function optionalStringParam(value) {
|
|
5525
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5526
|
+
}
|
|
5527
|
+
function buildRgCommand(params) {
|
|
5528
|
+
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
5529
|
+
if (params.ignoreCase === true) args.push("--ignore-case");
|
|
5530
|
+
if (params.literal === true) args.push("--fixed-strings");
|
|
5531
|
+
const glob = optionalStringParam(params.glob);
|
|
5532
|
+
if (glob) args.push("--glob", quoteShell(glob));
|
|
5533
|
+
const context = numberParam(params.context, 0, 0);
|
|
5534
|
+
if (context > 0) args.push("--context", String(context));
|
|
5535
|
+
const pattern = params.pattern;
|
|
5536
|
+
const searchPath = optionalStringParam(params.path) ?? ".";
|
|
5537
|
+
args.push("--", quoteShell(pattern), quoteShell(searchPath));
|
|
5538
|
+
return `rg ${args.join(" ")}`;
|
|
5539
|
+
}
|
|
5540
|
+
function parseRgJson(stdout, limit) {
|
|
5541
|
+
const outputLines = [];
|
|
5542
|
+
let matchCount = 0;
|
|
5543
|
+
let matchLimitReached = false;
|
|
5544
|
+
let linesTruncated = false;
|
|
5545
|
+
for (const rawLine of stdout.split("\n")) {
|
|
5546
|
+
if (rawLine.trim().length === 0) continue;
|
|
5547
|
+
let event;
|
|
5548
|
+
try {
|
|
5549
|
+
event = JSON.parse(rawLine);
|
|
5550
|
+
} catch {
|
|
5551
|
+
continue;
|
|
5158
5552
|
}
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
authenticated
|
|
5165
|
-
};
|
|
5166
|
-
}
|
|
5167
|
-
function createAuthMiddleware(opts = {}) {
|
|
5168
|
-
let warnedDevMode = false;
|
|
5169
|
-
return async function authMiddleware(request, reply) {
|
|
5170
|
-
const workspaceId = opts.workspaceId ?? DEFAULT_WORKSPACE_ID;
|
|
5171
|
-
const authToken = opts.authToken?.trim() || void 0;
|
|
5172
|
-
ensureWorkspaceContext(request, workspaceId, false);
|
|
5173
|
-
if (opts.publicPaths?.includes(request.url.split("?")[0])) {
|
|
5174
|
-
return;
|
|
5553
|
+
const filePath = event.data?.path?.text;
|
|
5554
|
+
const lineNumber = event.data?.line_number;
|
|
5555
|
+
const lineText = event.data?.lines?.text;
|
|
5556
|
+
if (!filePath || typeof lineNumber !== "number" || typeof lineText !== "string") {
|
|
5557
|
+
continue;
|
|
5175
5558
|
}
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5559
|
+
const isMatch = event.type === "match";
|
|
5560
|
+
if (isMatch) {
|
|
5561
|
+
matchCount += 1;
|
|
5562
|
+
if (matchCount > limit) {
|
|
5563
|
+
matchLimitReached = true;
|
|
5564
|
+
break;
|
|
5181
5565
|
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
const header = request.headers.authorization;
|
|
5185
|
-
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
5186
|
-
reply.code(401).send(
|
|
5187
|
-
errorPayload(
|
|
5188
|
-
ERROR_CODE_AUTH_REQUIRED,
|
|
5189
|
-
"Missing Bearer token"
|
|
5190
|
-
)
|
|
5191
|
-
);
|
|
5192
|
-
return;
|
|
5193
|
-
}
|
|
5194
|
-
const candidateToken = header.slice("Bearer ".length);
|
|
5195
|
-
if (candidateToken !== authToken) {
|
|
5196
|
-
reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
|
|
5197
|
-
return;
|
|
5566
|
+
} else if (event.type !== "context") {
|
|
5567
|
+
continue;
|
|
5198
5568
|
}
|
|
5199
|
-
|
|
5200
|
-
|
|
5569
|
+
const sanitized = lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
5570
|
+
const { text, wasTruncated } = truncateLine(sanitized);
|
|
5571
|
+
if (wasTruncated) linesTruncated = true;
|
|
5572
|
+
const separator = isMatch ? ":" : "-";
|
|
5573
|
+
outputLines.push(`${filePath}${separator}${lineNumber}${separator} ${text}`);
|
|
5574
|
+
}
|
|
5575
|
+
return { outputLines, matchCount, matchLimitReached, linesTruncated };
|
|
5201
5576
|
}
|
|
5202
|
-
function
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
request.body = parsed.data;
|
|
5577
|
+
function syntheticExecTruncation(output) {
|
|
5578
|
+
const outputLines = output.length === 0 ? 0 : output.split("\n").length;
|
|
5579
|
+
const outputBytes = bytesWritten(output);
|
|
5580
|
+
return {
|
|
5581
|
+
content: output,
|
|
5582
|
+
truncated: true,
|
|
5583
|
+
truncatedBy: "bytes",
|
|
5584
|
+
totalLines: outputLines,
|
|
5585
|
+
totalBytes: outputBytes,
|
|
5586
|
+
outputLines,
|
|
5587
|
+
outputBytes,
|
|
5588
|
+
lastLinePartial: false,
|
|
5589
|
+
firstLineExceedsLimit: false,
|
|
5590
|
+
maxLines: Number.MAX_SAFE_INTEGER,
|
|
5591
|
+
maxBytes: GREP_MAX_OUTPUT_BYTES
|
|
5218
5592
|
};
|
|
5219
5593
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
function healthRoutes(app, opts, done) {
|
|
5223
|
-
const startTime = Date.now();
|
|
5224
|
-
app.get("/health", async () => {
|
|
5594
|
+
function buildSuccessResult(parsed, effectiveLimit, sandboxTruncated) {
|
|
5595
|
+
if (parsed.matchCount === 0) {
|
|
5225
5596
|
return {
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
5597
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
5598
|
+
details: void 0
|
|
5229
5599
|
};
|
|
5230
|
-
});
|
|
5231
|
-
app.get("/ready", async (_request, reply) => {
|
|
5232
|
-
const state = opts.getReadiness();
|
|
5233
|
-
if (state.degradedReason) {
|
|
5234
|
-
return reply.code(503).send({
|
|
5235
|
-
status: "degraded",
|
|
5236
|
-
reason: state.degradedReason
|
|
5237
|
-
});
|
|
5238
|
-
}
|
|
5239
|
-
if (!state.sandboxReady || !state.harnessReady) {
|
|
5240
|
-
return reply.code(503).send({
|
|
5241
|
-
status: "provisioning",
|
|
5242
|
-
retryAfter: 2
|
|
5243
|
-
});
|
|
5244
|
-
}
|
|
5245
|
-
return { status: "ready" };
|
|
5246
|
-
});
|
|
5247
|
-
done();
|
|
5248
|
-
}
|
|
5249
|
-
|
|
5250
|
-
// src/server/http/routes/file.ts
|
|
5251
|
-
import { dirname as dirname7, extname as extname3, relative as relative7 } from "path/posix";
|
|
5252
|
-
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
5253
|
-
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
5254
|
-
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
5255
|
-
function defaultWorkspaceSettings() {
|
|
5256
|
-
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
5257
|
-
}
|
|
5258
|
-
function isPathValidationError(err) {
|
|
5259
|
-
return err instanceof Error && typeof err.reason === "string";
|
|
5260
|
-
}
|
|
5261
|
-
function classifyError(err, reply, subject) {
|
|
5262
|
-
if (isPathValidationError(err)) {
|
|
5263
|
-
return reply.code(403).send({
|
|
5264
|
-
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
5265
|
-
});
|
|
5266
|
-
}
|
|
5267
|
-
const message = err instanceof Error ? err.message : "unknown error";
|
|
5268
|
-
const code = err?.code;
|
|
5269
|
-
if (code === "EPERM" || message.includes("traversal") || message.includes("EPERM")) {
|
|
5270
|
-
return reply.code(403).send({
|
|
5271
|
-
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
5272
|
-
});
|
|
5273
|
-
}
|
|
5274
|
-
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
5275
|
-
return reply.code(404).send({
|
|
5276
|
-
error: { code: ERROR_CODE_NOT_FOUND, message: `${subject} not found` }
|
|
5277
|
-
});
|
|
5278
5600
|
}
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5601
|
+
const rawOutput = parsed.outputLines.join("\n");
|
|
5602
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
5603
|
+
let output = truncation.content;
|
|
5604
|
+
const details = {};
|
|
5605
|
+
const notices = [];
|
|
5606
|
+
if (parsed.matchLimitReached) {
|
|
5607
|
+
notices.push(
|
|
5608
|
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
|
|
5609
|
+
);
|
|
5610
|
+
details.matchLimitReached = effectiveLimit;
|
|
5283
5611
|
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
function requireStringParam(value, field, reply) {
|
|
5289
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
5290
|
-
reply.code(400).send({
|
|
5291
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `${field} is required`, field }
|
|
5292
|
-
});
|
|
5293
|
-
return null;
|
|
5612
|
+
if (truncation.truncated || sandboxTruncated) {
|
|
5613
|
+
const effectiveTruncation = truncation.truncated ? truncation : syntheticExecTruncation(output);
|
|
5614
|
+
notices.push(`${formatSize(effectiveTruncation.maxBytes)} limit reached`);
|
|
5615
|
+
details.truncation = effectiveTruncation;
|
|
5294
5616
|
}
|
|
5295
|
-
if (
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5617
|
+
if (parsed.linesTruncated) {
|
|
5618
|
+
notices.push(
|
|
5619
|
+
`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`
|
|
5620
|
+
);
|
|
5621
|
+
details.linesTruncated = true;
|
|
5300
5622
|
}
|
|
5301
|
-
|
|
5623
|
+
if (notices.length > 0) output += `
|
|
5624
|
+
|
|
5625
|
+
[${notices.join(". ")}]`;
|
|
5626
|
+
return {
|
|
5627
|
+
content: [{ type: "text", text: output }],
|
|
5628
|
+
details: Object.keys(details).length > 0 ? details : void 0
|
|
5629
|
+
};
|
|
5302
5630
|
}
|
|
5303
|
-
function
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5631
|
+
function vercelGrepTool(sandbox, workspaceRoot) {
|
|
5632
|
+
return {
|
|
5633
|
+
name: PI_GREP_TOOL.name,
|
|
5634
|
+
description: PI_GREP_TOOL.description,
|
|
5635
|
+
promptSnippet: PI_GREP_TOOL.promptSnippet,
|
|
5636
|
+
parameters: PI_GREP_TOOL.parameters,
|
|
5637
|
+
async execute(input, ctx) {
|
|
5638
|
+
const params = input;
|
|
5639
|
+
if (ctx.abortSignal.aborted) {
|
|
5640
|
+
return makeError("grep aborted");
|
|
5312
5641
|
}
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5642
|
+
if (typeof params.pattern !== "string" || params.pattern.length === 0) {
|
|
5643
|
+
return makeError("pattern is required");
|
|
5644
|
+
}
|
|
5645
|
+
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
5646
|
+
const resolved = resolve6(workspaceRoot, params.path);
|
|
5647
|
+
const rel = relative8(workspaceRoot, resolved);
|
|
5648
|
+
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
5649
|
+
return makeError(`path "${params.path}" is outside workspace`);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
try {
|
|
5653
|
+
const result = await sandbox.exec(buildRgCommand(params), {
|
|
5654
|
+
signal: ctx.abortSignal,
|
|
5655
|
+
timeoutMs: GREP_TIMEOUT_MS,
|
|
5656
|
+
maxOutputBytes: GREP_MAX_OUTPUT_BYTES
|
|
5657
|
+
});
|
|
5658
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
5659
|
+
const stderr = decode(result.stderr).trim();
|
|
5660
|
+
const message = stderr || `ripgrep exited with code ${result.exitCode}`;
|
|
5661
|
+
return makeError(`grep failed: ${message}`);
|
|
5662
|
+
}
|
|
5663
|
+
const limit = numberParam(params.limit, DEFAULT_LIMIT2);
|
|
5664
|
+
const parsed = parseRgJson(decode(result.stdout), limit);
|
|
5665
|
+
return buildSuccessResult(parsed, limit, result.truncated);
|
|
5666
|
+
} catch (error) {
|
|
5667
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
5668
|
+
return makeError(`grep failed: ${message}`);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
};
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
// src/server/tools/filesystem/index.ts
|
|
5675
|
+
function isTextContent(content) {
|
|
5676
|
+
return content.type === "text" && typeof content.text === "string";
|
|
5677
|
+
}
|
|
5678
|
+
function adaptPiTool(piTool) {
|
|
5679
|
+
return {
|
|
5680
|
+
name: piTool.name,
|
|
5681
|
+
description: piTool.description,
|
|
5682
|
+
promptSnippet: piTool.promptSnippet,
|
|
5683
|
+
parameters: piTool.parameters,
|
|
5684
|
+
async execute(params, ctx) {
|
|
5685
|
+
const result = await piTool.execute(
|
|
5686
|
+
ctx.toolCallId,
|
|
5687
|
+
params,
|
|
5688
|
+
ctx.abortSignal,
|
|
5689
|
+
ctx.onUpdate ? (update) => {
|
|
5690
|
+
const text = update.content?.filter(isTextContent).map((c) => c.text).join("");
|
|
5691
|
+
if (text) ctx.onUpdate?.(text);
|
|
5692
|
+
} : void 0,
|
|
5693
|
+
{}
|
|
5694
|
+
);
|
|
5695
|
+
const textContent = (result.content ?? []).filter(isTextContent).map((c) => ({ type: "text", text: c.text }));
|
|
5696
|
+
return {
|
|
5697
|
+
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5698
|
+
isError: false,
|
|
5699
|
+
details: result.details
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5317
5703
|
}
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5704
|
+
function buildFilesystemAgentTools(bundle) {
|
|
5705
|
+
const cwd = bundle.workspace.root;
|
|
5706
|
+
if (bundle.sandbox.provider === "vercel-sandbox") {
|
|
5707
|
+
return [
|
|
5708
|
+
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
5709
|
+
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
5710
|
+
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
5711
|
+
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
5712
|
+
vercelGrepTool(bundle.sandbox, cwd),
|
|
5713
|
+
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
5714
|
+
];
|
|
5325
5715
|
}
|
|
5716
|
+
const ops = boundFs(cwd);
|
|
5717
|
+
return [
|
|
5718
|
+
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
5719
|
+
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
5720
|
+
adaptPiTool(createEditToolDefinition(cwd, { operations: ops.edit })),
|
|
5721
|
+
adaptPiTool(createFindToolDefinition(cwd, { operations: ops.find })),
|
|
5722
|
+
adaptPiTool(createGrepToolDefinition2(cwd, { operations: ops.grep })),
|
|
5723
|
+
adaptPiTool(createLsToolDefinition(cwd, { operations: ops.ls }))
|
|
5724
|
+
];
|
|
5326
5725
|
}
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
}
|
|
5333
|
-
function
|
|
5334
|
-
|
|
5335
|
-
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
5336
|
-
if (contentType === "image/jpeg") return "jpg";
|
|
5337
|
-
if (contentType === "image/png") return "png";
|
|
5338
|
-
if (contentType === "image/gif") return "gif";
|
|
5339
|
-
if (contentType === "image/webp") return "webp";
|
|
5340
|
-
if (contentType === "image/svg+xml") return "svg";
|
|
5341
|
-
return "bin";
|
|
5726
|
+
|
|
5727
|
+
// src/server/tools/harness/index.ts
|
|
5728
|
+
import {
|
|
5729
|
+
createBashToolDefinition,
|
|
5730
|
+
createLocalBashOperations
|
|
5731
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5732
|
+
function shellEscape2(s) {
|
|
5733
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5342
5734
|
}
|
|
5343
|
-
function
|
|
5344
|
-
const
|
|
5345
|
-
const
|
|
5346
|
-
|
|
5347
|
-
|
|
5735
|
+
function bwrapSpawnHook(workspaceRoot) {
|
|
5736
|
+
const args = buildBwrapArgs(workspaceRoot);
|
|
5737
|
+
const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
|
|
5738
|
+
return (context) => ({
|
|
5739
|
+
...context,
|
|
5740
|
+
command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
|
|
5741
|
+
env: withWorkspacePythonEnv({
|
|
5742
|
+
workspaceRoot,
|
|
5743
|
+
env: context.env,
|
|
5744
|
+
sandboxRoot: "/workspace"
|
|
5745
|
+
})
|
|
5746
|
+
});
|
|
5348
5747
|
}
|
|
5349
|
-
function
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5748
|
+
function directSpawnHook(workspaceRoot) {
|
|
5749
|
+
return (context) => ({
|
|
5750
|
+
...context,
|
|
5751
|
+
env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
|
|
5752
|
+
});
|
|
5354
5753
|
}
|
|
5355
|
-
function
|
|
5356
|
-
switch (
|
|
5357
|
-
case "
|
|
5358
|
-
return
|
|
5359
|
-
case "
|
|
5360
|
-
return
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
case ".png":
|
|
5365
|
-
return "image/png";
|
|
5366
|
-
case ".svg":
|
|
5367
|
-
return "image/svg+xml";
|
|
5368
|
-
case ".webp":
|
|
5369
|
-
return "image/webp";
|
|
5370
|
-
case ".css":
|
|
5371
|
-
return "text/css; charset=utf-8";
|
|
5372
|
-
case ".html":
|
|
5373
|
-
case ".htm":
|
|
5374
|
-
return "text/html; charset=utf-8";
|
|
5375
|
-
case ".js":
|
|
5376
|
-
return "text/javascript; charset=utf-8";
|
|
5377
|
-
case ".mjs":
|
|
5378
|
-
return "text/javascript; charset=utf-8";
|
|
5379
|
-
case ".pdf":
|
|
5380
|
-
return "application/pdf";
|
|
5754
|
+
function bashOptionsForMode(bundle) {
|
|
5755
|
+
switch (bundle.sandbox.provider) {
|
|
5756
|
+
case "vercel-sandbox":
|
|
5757
|
+
return { operations: vercelBashOps(bundle.sandbox) };
|
|
5758
|
+
case "bwrap":
|
|
5759
|
+
return {
|
|
5760
|
+
operations: createLocalBashOperations(),
|
|
5761
|
+
spawnHook: bwrapSpawnHook(bundle.workspace.root)
|
|
5762
|
+
};
|
|
5381
5763
|
default:
|
|
5382
|
-
return
|
|
5764
|
+
return {
|
|
5765
|
+
operations: createLocalBashOperations(),
|
|
5766
|
+
spawnHook: directSpawnHook(bundle.workspace.root)
|
|
5767
|
+
};
|
|
5383
5768
|
}
|
|
5384
5769
|
}
|
|
5385
|
-
function
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
}
|
|
5401
|
-
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
}
|
|
5408
|
-
const bytes = await workspace.readBinaryFile(path4);
|
|
5409
|
-
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
5410
|
-
} catch (err) {
|
|
5411
|
-
return classifyError(err, reply, "file");
|
|
5412
|
-
}
|
|
5413
|
-
});
|
|
5414
|
-
app.get("/api/v1/files", async (request, reply) => {
|
|
5415
|
-
const query = request.query;
|
|
5416
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5417
|
-
if (path4 === null) return;
|
|
5418
|
-
try {
|
|
5419
|
-
const workspace = await resolveWorkspace(request);
|
|
5420
|
-
if (workspace.readFileWithStat) {
|
|
5421
|
-
const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
|
|
5422
|
-
return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
|
|
5423
|
-
}
|
|
5424
|
-
const content = await workspace.readFile(path4);
|
|
5425
|
-
const stat7 = await workspace.stat(path4);
|
|
5426
|
-
return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
5427
|
-
} catch (err) {
|
|
5428
|
-
return classifyError(err, reply, "file");
|
|
5429
|
-
}
|
|
5430
|
-
});
|
|
5431
|
-
app.post("/api/v1/files", async (request, reply) => {
|
|
5432
|
-
const body = request.body;
|
|
5433
|
-
const path4 = requireStringParam(body?.path, "path", reply);
|
|
5434
|
-
if (path4 === null) return;
|
|
5435
|
-
if (typeof body.content !== "string") {
|
|
5436
|
-
return reply.code(400).send({
|
|
5437
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "content is required", field: "content" }
|
|
5438
|
-
});
|
|
5770
|
+
function adaptPiTool2(piTool) {
|
|
5771
|
+
return {
|
|
5772
|
+
name: piTool.name,
|
|
5773
|
+
description: piTool.description,
|
|
5774
|
+
promptSnippet: piTool.promptSnippet,
|
|
5775
|
+
parameters: piTool.parameters,
|
|
5776
|
+
async execute(params, ctx) {
|
|
5777
|
+
const result = await piTool.execute(
|
|
5778
|
+
ctx.toolCallId,
|
|
5779
|
+
params,
|
|
5780
|
+
ctx.abortSignal,
|
|
5781
|
+
ctx.onUpdate ? (update) => {
|
|
5782
|
+
const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
5783
|
+
ctx.onUpdate(text);
|
|
5784
|
+
} : void 0,
|
|
5785
|
+
{}
|
|
5786
|
+
);
|
|
5787
|
+
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
5788
|
+
return {
|
|
5789
|
+
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5790
|
+
isError: false,
|
|
5791
|
+
details: result.details
|
|
5792
|
+
};
|
|
5439
5793
|
}
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
});
|
|
5466
|
-
}
|
|
5467
|
-
throw statErr;
|
|
5468
|
-
}
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
function createExecuteIsolatedCodeTool(sandbox) {
|
|
5797
|
+
return {
|
|
5798
|
+
name: "execute_isolated_code",
|
|
5799
|
+
description: "Execute code in an isolated sandbox environment.",
|
|
5800
|
+
parameters: {
|
|
5801
|
+
type: "object",
|
|
5802
|
+
properties: {
|
|
5803
|
+
code: { type: "string" },
|
|
5804
|
+
language: { type: "string", enum: ["python", "shell"] },
|
|
5805
|
+
image: { type: "string" },
|
|
5806
|
+
packages: { type: "array", items: { type: "string" } },
|
|
5807
|
+
sandboxId: { type: "string" },
|
|
5808
|
+
vmSize: { type: "string", enum: ["xxs", "xs", "s", "m", "l"] }
|
|
5809
|
+
},
|
|
5810
|
+
required: ["code", "language"],
|
|
5811
|
+
additionalProperties: false
|
|
5812
|
+
},
|
|
5813
|
+
async execute(input) {
|
|
5814
|
+
if (!sandbox.executeIsolatedCode) {
|
|
5815
|
+
return {
|
|
5816
|
+
content: [{ type: "text", text: "isolated-code capability is not available" }],
|
|
5817
|
+
isError: true
|
|
5818
|
+
};
|
|
5469
5819
|
}
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5820
|
+
const code = input.code;
|
|
5821
|
+
const language = input.language;
|
|
5822
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
5823
|
+
return { content: [{ type: "text", text: "code is required" }], isError: true };
|
|
5473
5824
|
}
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
const body = request.body;
|
|
5486
|
-
const filename = requireStringParam(body?.filename, "filename", reply);
|
|
5487
|
-
if (filename === null) return;
|
|
5488
|
-
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
5489
|
-
if (contentBase64 === null) return;
|
|
5490
|
-
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
5491
|
-
if (!contentType.startsWith("image/")) {
|
|
5492
|
-
return reply.code(400).send({
|
|
5493
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
5494
|
-
});
|
|
5495
|
-
}
|
|
5496
|
-
try {
|
|
5497
|
-
const workspace = await resolveWorkspace(request);
|
|
5498
|
-
const settings = await readWorkspaceSettings(workspace);
|
|
5499
|
-
const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
|
|
5500
|
-
const ext = extForUpload(filename, contentType);
|
|
5501
|
-
const base = basenameForUpload(filename);
|
|
5502
|
-
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5503
|
-
const path4 = `${dir}/${base}-${unique}.${ext}`;
|
|
5504
|
-
const bytes = Buffer.from(contentBase64, "base64");
|
|
5505
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
5506
|
-
return reply.code(400).send({
|
|
5507
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `upload must be 1 byte to ${MAX_UPLOAD_BYTES} bytes`, field: "contentBase64" }
|
|
5825
|
+
if (language !== "python" && language !== "shell") {
|
|
5826
|
+
return { content: [{ type: "text", text: "language must be python or shell" }], isError: true };
|
|
5827
|
+
}
|
|
5828
|
+
try {
|
|
5829
|
+
const result = await sandbox.executeIsolatedCode({
|
|
5830
|
+
code,
|
|
5831
|
+
language,
|
|
5832
|
+
image: typeof input.image === "string" ? input.image : void 0,
|
|
5833
|
+
packages: Array.isArray(input.packages) ? input.packages.filter((v) => typeof v === "string") : void 0,
|
|
5834
|
+
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : void 0,
|
|
5835
|
+
vmSize: input.vmSize === "xxs" || input.vmSize === "xs" || input.vmSize === "s" || input.vmSize === "m" || input.vmSize === "l" ? input.vmSize : void 0
|
|
5508
5836
|
});
|
|
5837
|
+
return {
|
|
5838
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
5839
|
+
isError: result.exitCode !== 0,
|
|
5840
|
+
details: result
|
|
5841
|
+
};
|
|
5842
|
+
} catch (error) {
|
|
5843
|
+
return {
|
|
5844
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : "execute_isolated_code failed" }],
|
|
5845
|
+
isError: true
|
|
5846
|
+
};
|
|
5509
5847
|
}
|
|
5510
|
-
await workspace.mkdir(dir, { recursive: true });
|
|
5511
|
-
const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
5512
|
-
if (!workspace.writeBinaryFile) {
|
|
5513
|
-
throw new Error("workspace does not support binary uploads");
|
|
5514
|
-
}
|
|
5515
|
-
await workspace.writeBinaryFile(path4, bytes);
|
|
5516
|
-
return await workspace.stat(path4);
|
|
5517
|
-
})();
|
|
5518
|
-
const sourcePath = typeof body.sourcePath === "string" && !body.sourcePath.includes("\0") ? body.sourcePath : null;
|
|
5519
|
-
return {
|
|
5520
|
-
ok: true,
|
|
5521
|
-
path: path4,
|
|
5522
|
-
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
5523
|
-
mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
|
|
5524
|
-
};
|
|
5525
|
-
} catch (err) {
|
|
5526
|
-
return classifyError(err, reply, "upload");
|
|
5527
|
-
}
|
|
5528
|
-
});
|
|
5529
|
-
app.get("/api/v1/workspace-settings", async (request, reply) => {
|
|
5530
|
-
try {
|
|
5531
|
-
const workspace = await resolveWorkspace(request);
|
|
5532
|
-
return { settings: await readWorkspaceSettings(workspace) };
|
|
5533
|
-
} catch (err) {
|
|
5534
|
-
return classifyError(err, reply, "settings");
|
|
5535
5848
|
}
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
function buildHarnessAgentTools(bundle) {
|
|
5852
|
+
const tools = [
|
|
5853
|
+
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
|
|
5854
|
+
];
|
|
5855
|
+
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
5856
|
+
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
5857
|
+
}
|
|
5858
|
+
return tools;
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
// src/server/http/routes/health.ts
|
|
5862
|
+
function healthRoutes(app, opts, done) {
|
|
5863
|
+
const startTime = Date.now();
|
|
5864
|
+
app.get("/health", async () => {
|
|
5865
|
+
return {
|
|
5866
|
+
status: "ok",
|
|
5867
|
+
version: opts.version,
|
|
5868
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
5869
|
+
};
|
|
5536
5870
|
});
|
|
5537
|
-
app.
|
|
5538
|
-
const
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
return reply.code(400).send({
|
|
5544
|
-
error: { code: ERROR_CODE_INVALID_PATH, message: "markdown.imageUploadDir must be a relative workspace path", field: "markdown.imageUploadDir" }
|
|
5871
|
+
app.get("/ready", async (_request, reply) => {
|
|
5872
|
+
const state = opts.getReadiness();
|
|
5873
|
+
if (state.degradedReason) {
|
|
5874
|
+
return reply.code(503).send({
|
|
5875
|
+
status: "degraded",
|
|
5876
|
+
reason: state.degradedReason
|
|
5545
5877
|
});
|
|
5546
5878
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
markdown: {
|
|
5553
|
-
...current.markdown ?? {},
|
|
5554
|
-
imageUploadDir
|
|
5555
|
-
}
|
|
5556
|
-
};
|
|
5557
|
-
await workspace.mkdir(".boring", { recursive: true });
|
|
5558
|
-
await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
|
|
5559
|
-
`);
|
|
5560
|
-
return { settings: next };
|
|
5561
|
-
} catch (err) {
|
|
5562
|
-
return classifyError(err, reply, "settings");
|
|
5563
|
-
}
|
|
5564
|
-
});
|
|
5565
|
-
app.delete("/api/v1/files", async (request, reply) => {
|
|
5566
|
-
const query = request.query;
|
|
5567
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5568
|
-
if (path4 === null) return;
|
|
5569
|
-
try {
|
|
5570
|
-
const workspace = await resolveWorkspace(request);
|
|
5571
|
-
await workspace.unlink(path4);
|
|
5572
|
-
return { ok: true };
|
|
5573
|
-
} catch (err) {
|
|
5574
|
-
return classifyError(err, reply, "file");
|
|
5575
|
-
}
|
|
5576
|
-
});
|
|
5577
|
-
app.post("/api/v1/files/move", async (request, reply) => {
|
|
5578
|
-
const body = request.body;
|
|
5579
|
-
const from = requireStringParam(body?.from, "from", reply);
|
|
5580
|
-
if (from === null) return;
|
|
5581
|
-
const to = requireStringParam(body?.to, "to", reply);
|
|
5582
|
-
if (to === null) return;
|
|
5583
|
-
try {
|
|
5584
|
-
const workspace = await resolveWorkspace(request);
|
|
5585
|
-
await workspace.rename(from, to);
|
|
5586
|
-
return { ok: true };
|
|
5587
|
-
} catch (err) {
|
|
5588
|
-
return classifyError(err, reply, "file");
|
|
5589
|
-
}
|
|
5590
|
-
});
|
|
5591
|
-
app.post("/api/v1/dirs", async (request, reply) => {
|
|
5592
|
-
const body = request.body;
|
|
5593
|
-
const path4 = requireStringParam(body?.path, "path", reply);
|
|
5594
|
-
if (path4 === null) return;
|
|
5595
|
-
const recursive = body.recursive === true;
|
|
5596
|
-
try {
|
|
5597
|
-
const workspace = await resolveWorkspace(request);
|
|
5598
|
-
await workspace.mkdir(path4, { recursive });
|
|
5599
|
-
return { ok: true };
|
|
5600
|
-
} catch (err) {
|
|
5601
|
-
return classifyError(err, reply, "directory");
|
|
5602
|
-
}
|
|
5603
|
-
});
|
|
5604
|
-
app.get("/api/v1/stat", async (request, reply) => {
|
|
5605
|
-
const query = request.query;
|
|
5606
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5607
|
-
if (path4 === null) return;
|
|
5608
|
-
try {
|
|
5609
|
-
const workspace = await resolveWorkspace(request);
|
|
5610
|
-
const stat7 = await workspace.stat(path4);
|
|
5611
|
-
return stat7;
|
|
5612
|
-
} catch (err) {
|
|
5613
|
-
return classifyError(err, reply, "path");
|
|
5879
|
+
if (!state.sandboxReady || !state.harnessReady) {
|
|
5880
|
+
return reply.code(503).send({
|
|
5881
|
+
status: "provisioning",
|
|
5882
|
+
retryAfter: 2
|
|
5883
|
+
});
|
|
5614
5884
|
}
|
|
5885
|
+
return { status: "ready" };
|
|
5615
5886
|
});
|
|
5616
5887
|
done();
|
|
5617
5888
|
}
|
|
@@ -6053,6 +6324,60 @@ function parseFileChangeChunk(chunk2) {
|
|
|
6053
6324
|
return change;
|
|
6054
6325
|
}
|
|
6055
6326
|
|
|
6327
|
+
// src/server/harness/pi-coding-agent/projectPiDataMessages.ts
|
|
6328
|
+
function projectPiDataMessages(messages) {
|
|
6329
|
+
const dataParts = messages.flatMap((message) => message.parts ?? []).filter((part) => {
|
|
6330
|
+
const type = part.type;
|
|
6331
|
+
return typeof type === "string" && type.startsWith("data-pi-");
|
|
6332
|
+
});
|
|
6333
|
+
if (dataParts.length === 0) return messages;
|
|
6334
|
+
const projected = [];
|
|
6335
|
+
const ensureMessage = (id, role, text = "") => {
|
|
6336
|
+
let msg = projected.find((item) => item.id === id);
|
|
6337
|
+
if (!msg) {
|
|
6338
|
+
msg = { id, role, parts: text ? [{ type: "text", text }] : [] };
|
|
6339
|
+
projected.push(msg);
|
|
6340
|
+
} else if (text && !(msg.parts ?? []).some((part) => part.type === "text" && part.text)) {
|
|
6341
|
+
msg.parts = [...msg.parts ?? [], { type: "text", text }];
|
|
6342
|
+
}
|
|
6343
|
+
return msg;
|
|
6344
|
+
};
|
|
6345
|
+
for (const part of dataParts) {
|
|
6346
|
+
const data = part.data ?? {};
|
|
6347
|
+
const messageId = typeof data.messageId === "string" ? data.messageId : void 0;
|
|
6348
|
+
if (!messageId) continue;
|
|
6349
|
+
if (part.type === "data-pi-message-start" && (data.role === "user" || data.role === "assistant")) {
|
|
6350
|
+
ensureMessage(messageId, data.role, typeof data.text === "string" ? data.text : "");
|
|
6351
|
+
} else if (part.type === "data-pi-text-start") {
|
|
6352
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6353
|
+
if (!msg.parts?.some((p) => p.type === "text")) msg.parts = [...msg.parts ?? [], { type: "text", text: "" }];
|
|
6354
|
+
} else if (part.type === "data-pi-text-delta") {
|
|
6355
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6356
|
+
const delta = typeof data.delta === "string" ? data.delta : "";
|
|
6357
|
+
const index = (msg.parts ?? []).findIndex((p) => p.type === "text");
|
|
6358
|
+
if (index >= 0) {
|
|
6359
|
+
msg.parts = (msg.parts ?? []).map((p, i) => i === index && p.type === "text" ? { ...p, text: `${p.text}${delta}` } : p);
|
|
6360
|
+
} else {
|
|
6361
|
+
msg.parts = [...msg.parts ?? [], { type: "text", text: delta }];
|
|
6362
|
+
}
|
|
6363
|
+
} else if (part.type === "data-pi-text-end" || part.type === "data-pi-message-end" && data.role === "assistant") {
|
|
6364
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6365
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
6366
|
+
if (text && !(msg.parts ?? []).some((p) => p.type === "text" && p.text)) msg.parts = [...msg.parts ?? [], { type: "text", text }];
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
if (projected.length === 0) return messages;
|
|
6370
|
+
const projectedIds = new Set(projected.map((message) => message.id));
|
|
6371
|
+
const preserved = messages.filter((message) => {
|
|
6372
|
+
if (projectedIds.has(message.id)) return false;
|
|
6373
|
+
return !(message.parts ?? []).some((part) => {
|
|
6374
|
+
const type = part.type;
|
|
6375
|
+
return typeof type === "string" && type.startsWith("data-pi-");
|
|
6376
|
+
});
|
|
6377
|
+
});
|
|
6378
|
+
return [...preserved, ...projected];
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6056
6381
|
// src/server/http/routes/chat.ts
|
|
6057
6382
|
var chatBodySchema = z.object({
|
|
6058
6383
|
sessionId: z.string().min(1).max(128),
|
|
@@ -6078,6 +6403,35 @@ function chatRoutes(app, opts, done) {
|
|
|
6078
6403
|
const { sessionChangesTracker } = opts;
|
|
6079
6404
|
const validateBody = createBodyValidator(chatBodySchema);
|
|
6080
6405
|
const buffers = new StreamBufferStore();
|
|
6406
|
+
const lastFollowUpBySession = /* @__PURE__ */ new Map();
|
|
6407
|
+
const cancelledFollowUpsBySession = /* @__PURE__ */ new Map();
|
|
6408
|
+
const MAX_FOLLOWUP_CACHE = 5e3;
|
|
6409
|
+
function followUpCancelKeys(clientNonce, clientSeq) {
|
|
6410
|
+
return [
|
|
6411
|
+
clientNonce ? `nonce:${clientNonce}` : void 0,
|
|
6412
|
+
clientSeq !== void 0 ? `seq:${clientSeq}` : void 0
|
|
6413
|
+
].filter((key) => Boolean(key));
|
|
6414
|
+
}
|
|
6415
|
+
function isFollowUpCancelled(sessionId, clientNonce, clientSeq) {
|
|
6416
|
+
const cancelled = cancelledFollowUpsBySession.get(sessionId);
|
|
6417
|
+
if (!cancelled) return false;
|
|
6418
|
+
return followUpCancelKeys(clientNonce, clientSeq).some((key) => cancelled.has(key));
|
|
6419
|
+
}
|
|
6420
|
+
function markFollowUpCancelled(sessionId, clientNonce, clientSeq) {
|
|
6421
|
+
const keys = followUpCancelKeys(clientNonce, clientSeq);
|
|
6422
|
+
if (keys.length === 0) return;
|
|
6423
|
+
const cancelled = cancelledFollowUpsBySession.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
6424
|
+
for (const key of keys) cancelled.add(key);
|
|
6425
|
+
cancelledFollowUpsBySession.set(sessionId, cancelled);
|
|
6426
|
+
}
|
|
6427
|
+
function evictFollowupCache() {
|
|
6428
|
+
const keys = Array.from(/* @__PURE__ */ new Set([...lastFollowUpBySession.keys(), ...cancelledFollowUpsBySession.keys()]));
|
|
6429
|
+
if (keys.length <= MAX_FOLLOWUP_CACHE) return;
|
|
6430
|
+
for (let i = 0; i < keys.length - MAX_FOLLOWUP_CACHE; i++) {
|
|
6431
|
+
lastFollowUpBySession.delete(keys[i]);
|
|
6432
|
+
cancelledFollowUpsBySession.delete(keys[i]);
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6081
6435
|
async function resolveRuntime(request) {
|
|
6082
6436
|
if (opts.getRuntime) return await opts.getRuntime(request);
|
|
6083
6437
|
if (opts.harness && opts.workdir) {
|
|
@@ -6256,13 +6610,49 @@ function chatRoutes(app, opts, done) {
|
|
|
6256
6610
|
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "attachments is invalid" }
|
|
6257
6611
|
});
|
|
6258
6612
|
}
|
|
6613
|
+
const clientSeq = typeof body.clientSeq === "number" && Number.isFinite(body.clientSeq) ? body.clientSeq : void 0;
|
|
6614
|
+
const clientNonce = typeof body.clientNonce === "string" && body.clientNonce.length > 0 ? body.clientNonce : void 0;
|
|
6615
|
+
if (isFollowUpCancelled(sessionId, clientNonce, clientSeq)) {
|
|
6616
|
+
return reply.code(202).send({ queued: false, deleted: true });
|
|
6617
|
+
}
|
|
6618
|
+
if (clientSeq !== void 0) {
|
|
6619
|
+
const last = lastFollowUpBySession.get(sessionId);
|
|
6620
|
+
if (last !== void 0 && clientSeq <= last.seq) {
|
|
6621
|
+
if (clientSeq === last.seq && clientNonce && clientNonce === last.nonce) {
|
|
6622
|
+
return reply.code(202).send({ queued: true, duplicate: true });
|
|
6623
|
+
}
|
|
6624
|
+
return reply.code(409).send({
|
|
6625
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_out_of_order" }
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6259
6629
|
const runtime = await resolveRuntime(request);
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6630
|
+
if (!runtime.harness.followUp) {
|
|
6631
|
+
return reply.code(409).send({
|
|
6632
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_unsupported" }
|
|
6633
|
+
});
|
|
6634
|
+
}
|
|
6635
|
+
try {
|
|
6636
|
+
await runtime.harness.followUp(
|
|
6637
|
+
sessionId,
|
|
6638
|
+
body.message,
|
|
6639
|
+
parsedAttachments.data,
|
|
6640
|
+
typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message,
|
|
6641
|
+
{ clientNonce, clientSeq }
|
|
6642
|
+
);
|
|
6643
|
+
} catch (err) {
|
|
6644
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6645
|
+
if (message === "followup_session_not_ready") {
|
|
6646
|
+
return reply.code(409).send({
|
|
6647
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_session_not_ready" }
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6650
|
+
throw err;
|
|
6651
|
+
}
|
|
6652
|
+
if (clientSeq !== void 0) {
|
|
6653
|
+
lastFollowUpBySession.set(sessionId, { seq: clientSeq, nonce: clientNonce });
|
|
6654
|
+
evictFollowupCache();
|
|
6655
|
+
}
|
|
6266
6656
|
return reply.code(202).send({ queued: true });
|
|
6267
6657
|
}
|
|
6268
6658
|
);
|
|
@@ -6270,8 +6660,14 @@ function chatRoutes(app, opts, done) {
|
|
|
6270
6660
|
"/api/v1/agent/chat/:sessionId/followup",
|
|
6271
6661
|
async (request, reply) => {
|
|
6272
6662
|
const { sessionId } = request.params;
|
|
6663
|
+
const query = request.query;
|
|
6664
|
+
const clientNonce = typeof query.clientNonce === "string" && query.clientNonce.length > 0 ? query.clientNonce : void 0;
|
|
6665
|
+
const rawClientSeq = typeof query.clientSeq === "string" ? Number(query.clientSeq) : query.clientSeq;
|
|
6666
|
+
const clientSeq = typeof rawClientSeq === "number" && Number.isFinite(rawClientSeq) ? rawClientSeq : void 0;
|
|
6273
6667
|
const runtime = await resolveRuntime(request);
|
|
6274
|
-
|
|
6668
|
+
markFollowUpCancelled(sessionId, clientNonce, clientSeq);
|
|
6669
|
+
evictFollowupCache();
|
|
6670
|
+
runtime.harness.clearFollowUp?.(sessionId, { clientNonce, clientSeq });
|
|
6275
6671
|
return reply.code(204).send();
|
|
6276
6672
|
}
|
|
6277
6673
|
);
|
|
@@ -6289,7 +6685,7 @@ function chatRoutes(app, opts, done) {
|
|
|
6289
6685
|
try {
|
|
6290
6686
|
const runtime = await resolveRuntime(request);
|
|
6291
6687
|
if (runtime.harness.sessions.saveMessages) {
|
|
6292
|
-
await runtime.harness.sessions.saveMessages(ctx, sessionId, body.messages);
|
|
6688
|
+
await runtime.harness.sessions.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
|
|
6293
6689
|
}
|
|
6294
6690
|
return reply.code(204).send();
|
|
6295
6691
|
} catch {
|
|
@@ -6344,7 +6740,7 @@ function modelsRoutes(app, _opts, done) {
|
|
|
6344
6740
|
return a.id.localeCompare(b.id);
|
|
6345
6741
|
});
|
|
6346
6742
|
const configuredDefaultModel = readConfiguredDefaultModel();
|
|
6347
|
-
const defaultModel = configuredDefaultModel && models.some((m) => m.available && m.provider === configuredDefaultModel.provider && m.id === configuredDefaultModel.id) ? configuredDefaultModel :
|
|
6743
|
+
const defaultModel = configuredDefaultModel && models.some((m) => m.available && m.provider === configuredDefaultModel.provider && m.id === configuredDefaultModel.id) ? configuredDefaultModel : void 0;
|
|
6348
6744
|
const payload = defaultModel ? { models, defaultModel: { provider: defaultModel.provider, id: defaultModel.id } } : { models };
|
|
6349
6745
|
return reply.code(200).send(payload);
|
|
6350
6746
|
});
|
|
@@ -6352,26 +6748,55 @@ function modelsRoutes(app, _opts, done) {
|
|
|
6352
6748
|
}
|
|
6353
6749
|
|
|
6354
6750
|
// src/server/http/routes/skills.ts
|
|
6355
|
-
import {
|
|
6751
|
+
import {
|
|
6752
|
+
DefaultPackageManager,
|
|
6753
|
+
getAgentDir as getAgentDir3,
|
|
6754
|
+
loadSkills
|
|
6755
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6356
6756
|
var CACHE_TTL_MS2 = 3e4;
|
|
6357
6757
|
function skillsRoutes(app, opts, done) {
|
|
6358
|
-
|
|
6359
|
-
app.get("/api/v1/agent/skills", (
|
|
6360
|
-
const now = Date.now();
|
|
6361
|
-
if (cached && cached.expiresAt > now) {
|
|
6362
|
-
return reply.code(200).send({ skills: cached.skills });
|
|
6363
|
-
}
|
|
6758
|
+
const cached = /* @__PURE__ */ new Map();
|
|
6759
|
+
app.get("/api/v1/agent/skills", async (request, reply) => {
|
|
6364
6760
|
try {
|
|
6365
|
-
const
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6761
|
+
const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
|
|
6762
|
+
const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
|
|
6763
|
+
const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
|
|
6764
|
+
const noSkills = opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills;
|
|
6765
|
+
const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], Boolean(noSkills)]);
|
|
6766
|
+
const now = Date.now();
|
|
6767
|
+
for (const [key, entry] of cached) {
|
|
6768
|
+
if (entry.expiresAt <= now) cached.delete(key);
|
|
6769
|
+
}
|
|
6770
|
+
const cachedEntry = cached.get(cacheKey);
|
|
6771
|
+
if (cachedEntry && cachedEntry.expiresAt > now) {
|
|
6772
|
+
return reply.code(200).send({ skills: cachedEntry.skills });
|
|
6773
|
+
}
|
|
6774
|
+
const agentDir = getAgentDir3();
|
|
6775
|
+
const packageSkillPaths = noSkills ? [] : await (async () => {
|
|
6776
|
+
const settingsManager = createResourceSettingsManager(
|
|
6777
|
+
workspaceRoot,
|
|
6778
|
+
agentDir,
|
|
6779
|
+
piPackages ?? []
|
|
6780
|
+
);
|
|
6781
|
+
const packageManager = new DefaultPackageManager({
|
|
6782
|
+
cwd: workspaceRoot,
|
|
6783
|
+
agentDir,
|
|
6784
|
+
settingsManager
|
|
6785
|
+
});
|
|
6786
|
+
const resolved = await packageManager.resolve();
|
|
6787
|
+
return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
|
|
6788
|
+
})();
|
|
6789
|
+
const result = loadSkills({
|
|
6790
|
+
cwd: workspaceRoot,
|
|
6791
|
+
agentDir,
|
|
6792
|
+
skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
|
|
6793
|
+
includeDefaults: false
|
|
6369
6794
|
});
|
|
6370
6795
|
const skills = result.skills.map((s) => ({
|
|
6371
6796
|
name: s.name,
|
|
6372
6797
|
description: s.description
|
|
6373
6798
|
}));
|
|
6374
|
-
cached
|
|
6799
|
+
cached.set(cacheKey, { skills, expiresAt: now + CACHE_TTL_MS2 });
|
|
6375
6800
|
return reply.code(200).send({ skills });
|
|
6376
6801
|
} catch {
|
|
6377
6802
|
return reply.code(200).send({ skills: [] });
|
|
@@ -7052,12 +7477,10 @@ async function createAgentApp(opts = {}) {
|
|
|
7052
7477
|
const harness = createPiCodingAgentHarness({
|
|
7053
7478
|
tools,
|
|
7054
7479
|
cwd: workspaceRoot,
|
|
7480
|
+
sessionNamespace: opts.sessionNamespace,
|
|
7481
|
+
sessionDir: opts.sessionDir,
|
|
7055
7482
|
systemPromptAppend: opts.systemPromptAppend,
|
|
7056
|
-
resourceLoaderOptions:
|
|
7057
|
-
noContextFiles: true,
|
|
7058
|
-
noSkills: true,
|
|
7059
|
-
...opts.resourceLoaderOptions
|
|
7060
|
-
}
|
|
7483
|
+
resourceLoaderOptions: opts.resourceLoaderOptions
|
|
7061
7484
|
});
|
|
7062
7485
|
const sessionChangesTracker = new InMemorySessionChangesTracker();
|
|
7063
7486
|
const readyTracker = new ReadyStatusTracker({
|
|
@@ -7143,6 +7566,11 @@ function mergeTools(options) {
|
|
|
7143
7566
|
setLastRegistered(merged, tool);
|
|
7144
7567
|
}
|
|
7145
7568
|
for (const tool of options.extraTools ?? []) {
|
|
7569
|
+
if (merged.has(tool.name)) {
|
|
7570
|
+
options.logger?.warn(
|
|
7571
|
+
`[catalog] Tool "${tool.name}" overridden by extraTools`
|
|
7572
|
+
);
|
|
7573
|
+
}
|
|
7146
7574
|
setLastRegistered(merged, tool);
|
|
7147
7575
|
}
|
|
7148
7576
|
for (const plugin of options.pluginTools ?? []) {
|
|
@@ -7298,7 +7726,7 @@ function getRequestWorkspaceId(request) {
|
|
|
7298
7726
|
}
|
|
7299
7727
|
function isWorkspaceAgnosticAgentRequest(request) {
|
|
7300
7728
|
const pathname = request.url.split("?")[0] ?? request.url;
|
|
7301
|
-
return pathname === "/
|
|
7729
|
+
return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models" || pathname === "/api/v1/ready-status";
|
|
7302
7730
|
}
|
|
7303
7731
|
function extractHttpStatus2(error) {
|
|
7304
7732
|
const statusCode = error?.statusCode;
|
|
@@ -7308,6 +7736,11 @@ function extractHttpStatus2(error) {
|
|
|
7308
7736
|
const responseStatus = error?.response?.status;
|
|
7309
7737
|
return typeof responseStatus === "number" ? responseStatus : null;
|
|
7310
7738
|
}
|
|
7739
|
+
function normalizeSessionNamespace(value) {
|
|
7740
|
+
if (typeof value !== "string") return void 0;
|
|
7741
|
+
const trimmed = value.trim();
|
|
7742
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7743
|
+
}
|
|
7311
7744
|
function isExpiredSandboxRuntimeError(error) {
|
|
7312
7745
|
const code = error?.code;
|
|
7313
7746
|
if (code === ErrorCode.enum.SANDBOX_EXPIRED) return true;
|
|
@@ -7325,23 +7758,49 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7325
7758
|
app.addHook("onClose", async () => {
|
|
7326
7759
|
await modeAdapter.dispose?.();
|
|
7327
7760
|
});
|
|
7328
|
-
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function";
|
|
7761
|
+
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getResourceLoaderOptions === "function" || typeof opts.getSessionNamespace === "function";
|
|
7329
7762
|
const sessionChangesTracker = new InMemorySessionChangesTracker();
|
|
7330
7763
|
const runtimeBindings = /* @__PURE__ */ new Map();
|
|
7764
|
+
const MAX_RUNTIME_BINDINGS = 256;
|
|
7765
|
+
function evictRuntimeBindings() {
|
|
7766
|
+
if (runtimeBindings.size <= MAX_RUNTIME_BINDINGS) return;
|
|
7767
|
+
const keys = Array.from(runtimeBindings.keys());
|
|
7768
|
+
for (let i = 0; i < keys.length - MAX_RUNTIME_BINDINGS; i++) {
|
|
7769
|
+
runtimeBindings.delete(keys[i]);
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7331
7772
|
async function resolveRuntimeScope(workspaceId, request) {
|
|
7332
7773
|
const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
|
|
7774
|
+
const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
|
|
7775
|
+
const resourceLoaderOptions = opts.getResourceLoaderOptions ? await opts.getResourceLoaderOptions({ workspaceId, workspaceRoot: root, request }) : opts.resourceLoaderOptions;
|
|
7776
|
+
const sessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot: root, request }) : opts.sessionNamespace);
|
|
7333
7777
|
return {
|
|
7334
7778
|
root,
|
|
7335
|
-
|
|
7779
|
+
templatePath: scopedTemplatePath,
|
|
7780
|
+
resourceLoaderOptions,
|
|
7781
|
+
sessionNamespace,
|
|
7782
|
+
key: JSON.stringify([
|
|
7783
|
+
resolvedMode,
|
|
7784
|
+
workspaceId,
|
|
7785
|
+
root,
|
|
7786
|
+
scopedTemplatePath ?? null,
|
|
7787
|
+
resourceLoaderOptions ?? null,
|
|
7788
|
+
sessionNamespace ?? null
|
|
7789
|
+
])
|
|
7336
7790
|
};
|
|
7337
7791
|
}
|
|
7338
|
-
async function
|
|
7339
|
-
const
|
|
7792
|
+
async function resolveSkillScope(workspaceId, request) {
|
|
7793
|
+
const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
|
|
7794
|
+
const resourceLoaderOptions = opts.getResourceLoaderOptions ? await opts.getResourceLoaderOptions({ workspaceId, workspaceRoot: root, request }) : opts.resourceLoaderOptions;
|
|
7795
|
+
return { root, resourceLoaderOptions };
|
|
7796
|
+
}
|
|
7797
|
+
async function createRuntimeBinding(workspaceId, scope, request) {
|
|
7798
|
+
const root = scope.root;
|
|
7340
7799
|
const runtimeBundle = await modeAdapter.create({
|
|
7341
7800
|
workspaceRoot: root,
|
|
7342
7801
|
sessionId: workspaceId,
|
|
7343
7802
|
workspaceId,
|
|
7344
|
-
templatePath:
|
|
7803
|
+
templatePath: scope.templatePath
|
|
7345
7804
|
});
|
|
7346
7805
|
const standardTools = [
|
|
7347
7806
|
...buildHarnessAgentTools(runtimeBundle),
|
|
@@ -7381,12 +7840,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7381
7840
|
const harness = createPiCodingAgentHarness({
|
|
7382
7841
|
tools,
|
|
7383
7842
|
cwd: root,
|
|
7843
|
+
sessionNamespace: scope.sessionNamespace,
|
|
7384
7844
|
systemPromptAppend: opts.systemPromptAppend,
|
|
7385
|
-
resourceLoaderOptions:
|
|
7386
|
-
noContextFiles: true,
|
|
7387
|
-
noSkills: true,
|
|
7388
|
-
...opts.resourceLoaderOptions
|
|
7389
|
-
}
|
|
7845
|
+
resourceLoaderOptions: scope.resourceLoaderOptions
|
|
7390
7846
|
});
|
|
7391
7847
|
const readyTracker = new ReadyStatusTracker({
|
|
7392
7848
|
sandboxReady: resolvedMode !== "vercel-sandbox",
|
|
@@ -7412,8 +7868,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7412
7868
|
await existing
|
|
7413
7869
|
);
|
|
7414
7870
|
}
|
|
7415
|
-
const created = createRuntimeBinding(workspaceId, scope
|
|
7871
|
+
const created = createRuntimeBinding(workspaceId, scope, request);
|
|
7416
7872
|
runtimeBindings.set(scope.key, created);
|
|
7873
|
+
evictRuntimeBindings();
|
|
7417
7874
|
try {
|
|
7418
7875
|
return await ensureRuntimeBindingReady(
|
|
7419
7876
|
workspaceId,
|
|
@@ -7428,8 +7885,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7428
7885
|
async function recreateRuntimeBinding(workspaceId, scope) {
|
|
7429
7886
|
runtimeBindings.delete(scope.key);
|
|
7430
7887
|
evictSandboxHandleCacheForWorkspace(workspaceId);
|
|
7431
|
-
const created = createRuntimeBinding(workspaceId, scope
|
|
7888
|
+
const created = createRuntimeBinding(workspaceId, scope);
|
|
7432
7889
|
runtimeBindings.set(scope.key, created);
|
|
7890
|
+
evictRuntimeBindings();
|
|
7433
7891
|
try {
|
|
7434
7892
|
const binding = await created;
|
|
7435
7893
|
binding.lastHealthCheckMs = Date.now();
|
|
@@ -7459,6 +7917,15 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7459
7917
|
}
|
|
7460
7918
|
}
|
|
7461
7919
|
const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
|
|
7920
|
+
const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
|
|
7921
|
+
function getSkillsScopeForRequest(request) {
|
|
7922
|
+
let promise = skillsScopeByRequest.get(request);
|
|
7923
|
+
if (!promise) {
|
|
7924
|
+
promise = resolveSkillScope(getRequestWorkspaceId(request), request);
|
|
7925
|
+
skillsScopeByRequest.set(request, promise);
|
|
7926
|
+
}
|
|
7927
|
+
return promise;
|
|
7928
|
+
}
|
|
7462
7929
|
async function getBindingForRequest(request) {
|
|
7463
7930
|
if (staticBinding) return staticBinding;
|
|
7464
7931
|
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
|
|
@@ -7558,7 +8025,13 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7558
8025
|
await app.register(modelsRoutes);
|
|
7559
8026
|
await app.register(skillsRoutes, {
|
|
7560
8027
|
workspaceRoot,
|
|
7561
|
-
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths
|
|
8028
|
+
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths,
|
|
8029
|
+
piPackages: opts.resourceLoaderOptions?.piPackages,
|
|
8030
|
+
noSkills: opts.resourceLoaderOptions?.noSkills,
|
|
8031
|
+
getWorkspaceRoot: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).root,
|
|
8032
|
+
getAdditionalSkillPaths: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.additionalSkillPaths,
|
|
8033
|
+
getPiPackages: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.piPackages,
|
|
8034
|
+
getNoSkills: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.noSkills
|
|
7562
8035
|
});
|
|
7563
8036
|
await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
|
|
7564
8037
|
await app.register(
|
|
@@ -7591,6 +8064,7 @@ export {
|
|
|
7591
8064
|
createNodeWorkspace,
|
|
7592
8065
|
createVercelDeploymentSnapshotProvider,
|
|
7593
8066
|
createVercelSandboxWorkspace,
|
|
8067
|
+
fileRoutes,
|
|
7594
8068
|
hasBwrap,
|
|
7595
8069
|
mergePiPackageSources,
|
|
7596
8070
|
piPackageSourceKey,
|