@hachej/boring-agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/front/index.d.ts +26 -3
- package/dist/front/index.js +290 -51
- package/dist/front/styles.css +24 -0
- package/dist/{harness-CMiJ4kok.d.ts → harness-DpMCKbzh.d.ts} +20 -0
- package/dist/{sandbox-handle-store-OPdvRwie.d.ts → sandbox-handle-store-DCNEJJ6b.d.ts} +9 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +711 -149
- package/dist/shared/index.d.ts +3 -3
- package/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -725,6 +725,11 @@ function logSandboxStop(logger, metadata) {
|
|
|
725
725
|
...metadata
|
|
726
726
|
});
|
|
727
727
|
}
|
|
728
|
+
function isSandboxAlreadyExistsError(error) {
|
|
729
|
+
const code = error?.json?.error?.code;
|
|
730
|
+
const message = error?.json?.error?.message;
|
|
731
|
+
return code === "bad_request" && typeof message === "string" && message.includes("already exists");
|
|
732
|
+
}
|
|
728
733
|
async function createFresh(workspaceId, snapshotId, tarballUrl, previous, store, vercel, logger) {
|
|
729
734
|
let sandbox;
|
|
730
735
|
let sourceType = "empty";
|
|
@@ -733,21 +738,25 @@ async function createFresh(workspaceId, snapshotId, tarballUrl, previous, store,
|
|
|
733
738
|
persistent: true,
|
|
734
739
|
snapshotExpiration: 0
|
|
735
740
|
};
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
})
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
741
|
+
const tryCreate = async () => {
|
|
742
|
+
try {
|
|
743
|
+
if (snapshotId) {
|
|
744
|
+
sourceType = "snapshot";
|
|
745
|
+
return await vercel.create({ ...base, source: { type: "snapshot", snapshotId } });
|
|
746
|
+
} else if (tarballUrl) {
|
|
747
|
+
sourceType = "tarball";
|
|
748
|
+
return await vercel.create({ ...base, source: { type: "tarball", url: tarballUrl } });
|
|
749
|
+
} else {
|
|
750
|
+
return await vercel.create(base);
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (isSandboxAlreadyExistsError(error)) {
|
|
754
|
+
return await vercel.get({ name: base.name, sandboxId: base.name, resume: true });
|
|
755
|
+
}
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
sandbox = await tryCreate();
|
|
751
760
|
logSandboxCreate(logger, {
|
|
752
761
|
workspaceId,
|
|
753
762
|
sandboxId: getSandboxIdentifier(sandbox),
|
|
@@ -1380,10 +1389,18 @@ function createNodeWorkspace(root) {
|
|
|
1380
1389
|
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1381
1390
|
return await readFile3(absPath, "utf-8");
|
|
1382
1391
|
},
|
|
1392
|
+
async readBinaryFile(relPath) {
|
|
1393
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1394
|
+
return new Uint8Array(await readFile3(absPath));
|
|
1395
|
+
},
|
|
1383
1396
|
async writeFile(relPath, data) {
|
|
1384
1397
|
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1385
1398
|
await writeFile3(absPath, data, "utf-8");
|
|
1386
1399
|
},
|
|
1400
|
+
async writeBinaryFile(relPath, data) {
|
|
1401
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1402
|
+
await writeFile3(absPath, data);
|
|
1403
|
+
},
|
|
1387
1404
|
async readFileWithStat(relPath) {
|
|
1388
1405
|
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1389
1406
|
const [content, fileStat] = await Promise.all([
|
|
@@ -1409,6 +1426,16 @@ function createNodeWorkspace(root) {
|
|
|
1409
1426
|
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1410
1427
|
};
|
|
1411
1428
|
},
|
|
1429
|
+
async writeBinaryFileWithStat(relPath, data) {
|
|
1430
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1431
|
+
await writeFile3(absPath, data);
|
|
1432
|
+
const fileStat = await stat2(absPath);
|
|
1433
|
+
return {
|
|
1434
|
+
size: fileStat.size,
|
|
1435
|
+
mtimeMs: fileStat.mtimeMs,
|
|
1436
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1437
|
+
};
|
|
1438
|
+
},
|
|
1412
1439
|
async unlink(relPath) {
|
|
1413
1440
|
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1414
1441
|
const pathStat = await lstat2(absPath);
|
|
@@ -1819,6 +1846,20 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
1819
1846
|
}
|
|
1820
1847
|
return Buffer.from(content).toString("utf-8");
|
|
1821
1848
|
},
|
|
1849
|
+
async readBinaryFile(relPath) {
|
|
1850
|
+
const sandboxPath = toSandboxPath(relPath);
|
|
1851
|
+
if (remote.fs?.readFile) {
|
|
1852
|
+
const content2 = await remote.fs.readFile(sandboxPath);
|
|
1853
|
+
return new Uint8Array(Buffer.from(content2));
|
|
1854
|
+
}
|
|
1855
|
+
const content = await remote.readFileToBuffer?.({ path: sandboxPath });
|
|
1856
|
+
if (!content) {
|
|
1857
|
+
const err = new Error(`ENOENT: file not found, open '${sandboxPath}'`);
|
|
1858
|
+
err.code = "ENOENT";
|
|
1859
|
+
throw err;
|
|
1860
|
+
}
|
|
1861
|
+
return new Uint8Array(Buffer.from(content));
|
|
1862
|
+
},
|
|
1822
1863
|
async writeFile(relPath, data) {
|
|
1823
1864
|
const sandboxPath = toSandboxPath(relPath);
|
|
1824
1865
|
await sandbox.writeFiles([
|
|
@@ -1831,6 +1872,18 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
1831
1872
|
workspaceOpts.onMutation?.();
|
|
1832
1873
|
emitChange({ op: "write", path: relPath });
|
|
1833
1874
|
},
|
|
1875
|
+
async writeBinaryFile(relPath, data) {
|
|
1876
|
+
const sandboxPath = toSandboxPath(relPath);
|
|
1877
|
+
await sandbox.writeFiles([
|
|
1878
|
+
{
|
|
1879
|
+
path: sandboxPath,
|
|
1880
|
+
content: Buffer.from(data)
|
|
1881
|
+
}
|
|
1882
|
+
]);
|
|
1883
|
+
invalidateMetadataCache();
|
|
1884
|
+
workspaceOpts.onMutation?.();
|
|
1885
|
+
emitChange({ op: "write", path: relPath });
|
|
1886
|
+
},
|
|
1834
1887
|
async readFileWithStat(relPath) {
|
|
1835
1888
|
const sandboxPath = toSandboxPath(relPath);
|
|
1836
1889
|
const cachedStat = statCache.get(sandboxPath);
|
|
@@ -1905,6 +1958,44 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
1905
1958
|
emitChange({ op: "write", path: relPath, mtimeMs: writtenStat.mtimeMs });
|
|
1906
1959
|
return cloneStat(writtenStat);
|
|
1907
1960
|
},
|
|
1961
|
+
async writeBinaryFileWithStat(relPath, data) {
|
|
1962
|
+
const sandboxPath = toSandboxPath(relPath);
|
|
1963
|
+
const payload = Buffer.from(data);
|
|
1964
|
+
if (payload.byteLength > MAX_INLINE_WRITE_BYTES) {
|
|
1965
|
+
await sandbox.writeFiles([
|
|
1966
|
+
{
|
|
1967
|
+
path: sandboxPath,
|
|
1968
|
+
content: payload
|
|
1969
|
+
}
|
|
1970
|
+
]);
|
|
1971
|
+
invalidateMetadataCache();
|
|
1972
|
+
workspaceOpts.onMutation?.();
|
|
1973
|
+
const writtenStat2 = remote.fs?.stat ? await (async () => {
|
|
1974
|
+
const fileStat = await remote.fs.stat(sandboxPath);
|
|
1975
|
+
return {
|
|
1976
|
+
size: fileStat.size,
|
|
1977
|
+
mtimeMs: fileStat.mtimeMs,
|
|
1978
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1979
|
+
};
|
|
1980
|
+
})() : await runJson(
|
|
1981
|
+
remote,
|
|
1982
|
+
`node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
|
|
1983
|
+
);
|
|
1984
|
+
statCache.set(sandboxPath, writtenStat2);
|
|
1985
|
+
emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
|
|
1986
|
+
return cloneStat(writtenStat2);
|
|
1987
|
+
}
|
|
1988
|
+
const encoded = payload.toString("base64");
|
|
1989
|
+
const writtenStat = await runJson(
|
|
1990
|
+
remote,
|
|
1991
|
+
`node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(encoded)}`
|
|
1992
|
+
);
|
|
1993
|
+
invalidateMetadataCache();
|
|
1994
|
+
statCache.set(sandboxPath, writtenStat);
|
|
1995
|
+
workspaceOpts.onMutation?.();
|
|
1996
|
+
emitChange({ op: "write", path: relPath, mtimeMs: writtenStat.mtimeMs });
|
|
1997
|
+
return cloneStat(writtenStat);
|
|
1998
|
+
},
|
|
1908
1999
|
async unlink(relPath) {
|
|
1909
2000
|
const sandboxPath = toSandboxPath(relPath);
|
|
1910
2001
|
if (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: false, force: false });
|
|
@@ -2410,6 +2501,39 @@ async function ensureVercelWorkspaceRoot(sandbox) {
|
|
|
2410
2501
|
throw new Error(`failed to initialize ${VERCEL_SANDBOX_REMOTE_ROOT} (exit ${result.exitCode ?? "unknown"})`);
|
|
2411
2502
|
}
|
|
2412
2503
|
}
|
|
2504
|
+
async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger) {
|
|
2505
|
+
const files = await collectFiles(templatePath);
|
|
2506
|
+
const hash = computeTemplateHash(files);
|
|
2507
|
+
const markerPath = `.boring-agent/templates/${hash}.json`;
|
|
2508
|
+
try {
|
|
2509
|
+
await workspace.stat(markerPath);
|
|
2510
|
+
logger.info("[vercel-sandbox:mode] template already seeded", {
|
|
2511
|
+
hash,
|
|
2512
|
+
fileCount: files.length
|
|
2513
|
+
});
|
|
2514
|
+
return;
|
|
2515
|
+
} catch {
|
|
2516
|
+
}
|
|
2517
|
+
for (const file of files) {
|
|
2518
|
+
if (workspace.writeBinaryFile) {
|
|
2519
|
+
await workspace.writeBinaryFile(file.rel, new Uint8Array(file.content));
|
|
2520
|
+
} else {
|
|
2521
|
+
await workspace.writeFile(file.rel, file.content.toString("utf-8"));
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
await workspace.writeFile(markerPath, JSON.stringify({ hash, seededAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2525
|
+
logger.info("[vercel-sandbox:mode] template seeded into workspace", {
|
|
2526
|
+
hash,
|
|
2527
|
+
fileCount: files.length
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
async function ensureTemplateExecutables(sandbox) {
|
|
2531
|
+
if (!sandbox.runCommand) return;
|
|
2532
|
+
await sandbox.runCommand({
|
|
2533
|
+
cmd: "sh",
|
|
2534
|
+
args: ["-c", `chmod +x ${VERCEL_SANDBOX_REMOTE_ROOT}/.boring-agent/bin/* 2>/dev/null || true`]
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2413
2537
|
var DEFAULT_MODE_LOGGER = {
|
|
2414
2538
|
info(message, metadata) {
|
|
2415
2539
|
process.stderr.write(`${message} ${JSON.stringify(metadata)}
|
|
@@ -2521,17 +2645,14 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
2521
2645
|
onMutation: markDirty
|
|
2522
2646
|
});
|
|
2523
2647
|
await ensureVercelWorkspaceRoot(sandboxHandle);
|
|
2524
|
-
if (ctx.templatePath
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
for (const file of files) {
|
|
2530
|
-
await workspace.writeFile(file.rel, file.content.toString("utf-8"));
|
|
2648
|
+
if (ctx.templatePath) {
|
|
2649
|
+
if (!tarballUrl) {
|
|
2650
|
+
logger.info("[vercel-sandbox:mode] falling back to writeFiles for template", {
|
|
2651
|
+
templatePath: ctx.templatePath
|
|
2652
|
+
});
|
|
2531
2653
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
});
|
|
2654
|
+
await seedTemplateIntoVercelWorkspace(workspace, ctx.templatePath, logger);
|
|
2655
|
+
await ensureTemplateExecutables(sandboxHandle);
|
|
2535
2656
|
}
|
|
2536
2657
|
const sandbox = createVercelSandboxExec(sandboxHandle, {
|
|
2537
2658
|
onMutation: markDirty
|
|
@@ -2584,6 +2705,8 @@ import Fastify from "fastify";
|
|
|
2584
2705
|
import { basename as basename2 } from "path";
|
|
2585
2706
|
|
|
2586
2707
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
2708
|
+
import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
|
|
2709
|
+
import { extname, join as join6 } from "path";
|
|
2587
2710
|
import {
|
|
2588
2711
|
createAgentSession,
|
|
2589
2712
|
SessionManager,
|
|
@@ -2595,6 +2718,86 @@ import {
|
|
|
2595
2718
|
loadSkills
|
|
2596
2719
|
} from "@mariozechner/pi-coding-agent";
|
|
2597
2720
|
|
|
2721
|
+
// src/server/logging.ts
|
|
2722
|
+
var SENSITIVE_KEYS = new Set([
|
|
2723
|
+
"apiKey",
|
|
2724
|
+
"api_key",
|
|
2725
|
+
"token",
|
|
2726
|
+
"secret",
|
|
2727
|
+
"password",
|
|
2728
|
+
"authorization",
|
|
2729
|
+
"cookie",
|
|
2730
|
+
"oidcToken",
|
|
2731
|
+
"accessToken",
|
|
2732
|
+
"refreshToken",
|
|
2733
|
+
"ANTHROPIC_API_KEY",
|
|
2734
|
+
"OPENAI_API_KEY",
|
|
2735
|
+
"VERCEL_OIDC_TOKEN",
|
|
2736
|
+
"VERCEL_TEAM_ID"
|
|
2737
|
+
].map((key) => key.toLowerCase()));
|
|
2738
|
+
function isSensitiveKey(key) {
|
|
2739
|
+
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
2740
|
+
}
|
|
2741
|
+
function redactValue(key, value, seen) {
|
|
2742
|
+
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
2743
|
+
if (value == null || typeof value !== "object") return value;
|
|
2744
|
+
if (value instanceof Date) return value;
|
|
2745
|
+
if (seen.has(value)) return "[Circular]";
|
|
2746
|
+
seen.add(value);
|
|
2747
|
+
if (Array.isArray(value)) {
|
|
2748
|
+
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
2749
|
+
seen.delete(value);
|
|
2750
|
+
return out2;
|
|
2751
|
+
}
|
|
2752
|
+
const out = {};
|
|
2753
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
2754
|
+
out[childKey] = redactValue(childKey, childValue, seen);
|
|
2755
|
+
}
|
|
2756
|
+
seen.delete(value);
|
|
2757
|
+
return out;
|
|
2758
|
+
}
|
|
2759
|
+
function redact(fields) {
|
|
2760
|
+
const out = {};
|
|
2761
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2762
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
2763
|
+
out[k] = redactValue(k, v, seen);
|
|
2764
|
+
}
|
|
2765
|
+
return out;
|
|
2766
|
+
}
|
|
2767
|
+
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
2768
|
+
function createLogger(prefix) {
|
|
2769
|
+
function emit(level, msg, fields) {
|
|
2770
|
+
const entry = {
|
|
2771
|
+
level,
|
|
2772
|
+
prefix,
|
|
2773
|
+
msg,
|
|
2774
|
+
...fields ? redact(fields) : {},
|
|
2775
|
+
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
2776
|
+
};
|
|
2777
|
+
if (level === "error") {
|
|
2778
|
+
console.error(JSON.stringify(entry));
|
|
2779
|
+
} else if (level === "warn") {
|
|
2780
|
+
console.warn(JSON.stringify(entry));
|
|
2781
|
+
} else {
|
|
2782
|
+
console.log(JSON.stringify(entry));
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return {
|
|
2786
|
+
debug(msg, fields) {
|
|
2787
|
+
if (verbose) emit("debug", msg, fields);
|
|
2788
|
+
},
|
|
2789
|
+
info(msg, fields) {
|
|
2790
|
+
emit("info", msg, fields);
|
|
2791
|
+
},
|
|
2792
|
+
warn(msg, fields) {
|
|
2793
|
+
emit("warn", msg, fields);
|
|
2794
|
+
},
|
|
2795
|
+
error(msg, fields) {
|
|
2796
|
+
emit("error", msg, fields);
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2598
2801
|
// src/server/harness/pi-coding-agent/tool-adapter.ts
|
|
2599
2802
|
function adaptToolForPi(tool) {
|
|
2600
2803
|
return {
|
|
@@ -3159,86 +3362,6 @@ function piMessagesToUIMessages(messages) {
|
|
|
3159
3362
|
return result;
|
|
3160
3363
|
}
|
|
3161
3364
|
|
|
3162
|
-
// src/server/logging.ts
|
|
3163
|
-
var SENSITIVE_KEYS = new Set([
|
|
3164
|
-
"apiKey",
|
|
3165
|
-
"api_key",
|
|
3166
|
-
"token",
|
|
3167
|
-
"secret",
|
|
3168
|
-
"password",
|
|
3169
|
-
"authorization",
|
|
3170
|
-
"cookie",
|
|
3171
|
-
"oidcToken",
|
|
3172
|
-
"accessToken",
|
|
3173
|
-
"refreshToken",
|
|
3174
|
-
"ANTHROPIC_API_KEY",
|
|
3175
|
-
"OPENAI_API_KEY",
|
|
3176
|
-
"VERCEL_OIDC_TOKEN",
|
|
3177
|
-
"VERCEL_TEAM_ID"
|
|
3178
|
-
].map((key) => key.toLowerCase()));
|
|
3179
|
-
function isSensitiveKey(key) {
|
|
3180
|
-
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
3181
|
-
}
|
|
3182
|
-
function redactValue(key, value, seen) {
|
|
3183
|
-
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
3184
|
-
if (value == null || typeof value !== "object") return value;
|
|
3185
|
-
if (value instanceof Date) return value;
|
|
3186
|
-
if (seen.has(value)) return "[Circular]";
|
|
3187
|
-
seen.add(value);
|
|
3188
|
-
if (Array.isArray(value)) {
|
|
3189
|
-
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
3190
|
-
seen.delete(value);
|
|
3191
|
-
return out2;
|
|
3192
|
-
}
|
|
3193
|
-
const out = {};
|
|
3194
|
-
for (const [childKey, childValue] of Object.entries(value)) {
|
|
3195
|
-
out[childKey] = redactValue(childKey, childValue, seen);
|
|
3196
|
-
}
|
|
3197
|
-
seen.delete(value);
|
|
3198
|
-
return out;
|
|
3199
|
-
}
|
|
3200
|
-
function redact(fields) {
|
|
3201
|
-
const out = {};
|
|
3202
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
3203
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
3204
|
-
out[k] = redactValue(k, v, seen);
|
|
3205
|
-
}
|
|
3206
|
-
return out;
|
|
3207
|
-
}
|
|
3208
|
-
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
3209
|
-
function createLogger(prefix) {
|
|
3210
|
-
function emit(level, msg, fields) {
|
|
3211
|
-
const entry = {
|
|
3212
|
-
level,
|
|
3213
|
-
prefix,
|
|
3214
|
-
msg,
|
|
3215
|
-
...fields ? redact(fields) : {},
|
|
3216
|
-
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
3217
|
-
};
|
|
3218
|
-
if (level === "error") {
|
|
3219
|
-
console.error(JSON.stringify(entry));
|
|
3220
|
-
} else if (level === "warn") {
|
|
3221
|
-
console.warn(JSON.stringify(entry));
|
|
3222
|
-
} else {
|
|
3223
|
-
console.log(JSON.stringify(entry));
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
return {
|
|
3227
|
-
debug(msg, fields) {
|
|
3228
|
-
if (verbose) emit("debug", msg, fields);
|
|
3229
|
-
},
|
|
3230
|
-
info(msg, fields) {
|
|
3231
|
-
emit("info", msg, fields);
|
|
3232
|
-
},
|
|
3233
|
-
warn(msg, fields) {
|
|
3234
|
-
emit("warn", msg, fields);
|
|
3235
|
-
},
|
|
3236
|
-
error(msg, fields) {
|
|
3237
|
-
emit("error", msg, fields);
|
|
3238
|
-
}
|
|
3239
|
-
};
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
3365
|
// src/server/harness/pi-coding-agent/sessionTitle.ts
|
|
3243
3366
|
var DEFAULT_SESSION_TITLE = "New session";
|
|
3244
3367
|
var FALLBACK_PREFIX = "New chat";
|
|
@@ -3637,6 +3760,24 @@ async function applyRequestedSessionOptions(handle, input) {
|
|
|
3637
3760
|
handle.piSession.setThinkingLevel(input.thinkingLevel);
|
|
3638
3761
|
}
|
|
3639
3762
|
}
|
|
3763
|
+
var log2 = createLogger("pi-harness");
|
|
3764
|
+
var DEFAULT_ATTACHMENT_DIR = "assets/images";
|
|
3765
|
+
function extForAttachment(filename, contentType) {
|
|
3766
|
+
const fromName = extname(filename).toLowerCase().replace(/^\./, "");
|
|
3767
|
+
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
3768
|
+
if (contentType === "image/jpeg") return "jpg";
|
|
3769
|
+
if (contentType === "image/png") return "png";
|
|
3770
|
+
if (contentType === "image/gif") return "gif";
|
|
3771
|
+
if (contentType === "image/webp") return "webp";
|
|
3772
|
+
if (contentType === "image/svg+xml") return "svg";
|
|
3773
|
+
return "bin";
|
|
3774
|
+
}
|
|
3775
|
+
function basenameForAttachment(filename) {
|
|
3776
|
+
const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
|
|
3777
|
+
const withoutExt = base.replace(/\.[^.]*$/, "");
|
|
3778
|
+
const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
3779
|
+
return safe || "image";
|
|
3780
|
+
}
|
|
3640
3781
|
function createPiCodingAgentHarness(opts) {
|
|
3641
3782
|
const sessionStore = new PiSessionStore(opts.cwd);
|
|
3642
3783
|
const piSessions = /* @__PURE__ */ new Map();
|
|
@@ -3733,10 +3874,17 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3733
3874
|
await originalDelete(ctx, sessionId);
|
|
3734
3875
|
disposePiSession(sessionId);
|
|
3735
3876
|
};
|
|
3877
|
+
const harnessFollowUpQueues = /* @__PURE__ */ new Map();
|
|
3736
3878
|
return {
|
|
3737
3879
|
id: "pi-coding-agent",
|
|
3738
3880
|
placement: "server",
|
|
3739
3881
|
sessions: sessionStore,
|
|
3882
|
+
followUp(sessionId, text, attachments) {
|
|
3883
|
+
harnessFollowUpQueues.set(sessionId, { message: text, attachments });
|
|
3884
|
+
},
|
|
3885
|
+
clearFollowUp(sessionId) {
|
|
3886
|
+
harnessFollowUpQueues.delete(sessionId);
|
|
3887
|
+
},
|
|
3740
3888
|
/**
|
|
3741
3889
|
* Pi exposes the resolved system prompt as a getter on AgentSession.
|
|
3742
3890
|
* Sessions are created lazily on first sendMessage, so callers may see
|
|
@@ -3752,6 +3900,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3752
3900
|
input,
|
|
3753
3901
|
ctx
|
|
3754
3902
|
);
|
|
3903
|
+
harnessFollowUpQueues.delete(input.sessionId);
|
|
3755
3904
|
const chunks = [];
|
|
3756
3905
|
let done = false;
|
|
3757
3906
|
let streamError = null;
|
|
@@ -3800,6 +3949,20 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3800
3949
|
return out;
|
|
3801
3950
|
}
|
|
3802
3951
|
let sawTextChunk = false;
|
|
3952
|
+
let inlineTurnIndex = 0;
|
|
3953
|
+
function namespaceInlinePartIds(input2) {
|
|
3954
|
+
if (inlineTurnIndex === 0) return input2;
|
|
3955
|
+
return input2.map((chunk2) => {
|
|
3956
|
+
const rec = chunk2;
|
|
3957
|
+
if ((rec.type === "text-start" || rec.type === "text-delta" || rec.type === "text-end" || rec.type === "reasoning-start" || rec.type === "reasoning-delta" || rec.type === "reasoning-end") && typeof rec.id === "string") {
|
|
3958
|
+
return { ...rec, id: `turn-${inlineTurnIndex}:${rec.id}` };
|
|
3959
|
+
}
|
|
3960
|
+
return chunk2;
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
let pendingFollowUp;
|
|
3964
|
+
let promptSettled = false;
|
|
3965
|
+
let promptPromise = Promise.resolve();
|
|
3803
3966
|
const unsubscribe = piSession.subscribe((event) => {
|
|
3804
3967
|
if (event.type === "message_update") {
|
|
3805
3968
|
const ame = event.assistantMessageEvent;
|
|
@@ -3819,11 +3982,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3819
3982
|
activeTools.delete(event.toolCallId);
|
|
3820
3983
|
if (activeTools.size === 0) stopHeartbeat();
|
|
3821
3984
|
}
|
|
3822
|
-
let converted = dedupStartChunks(piEventToChunks(event));
|
|
3985
|
+
let converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
3823
3986
|
if (event.type === "message_update") {
|
|
3824
3987
|
const ame = event.assistantMessageEvent;
|
|
3825
3988
|
if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
|
|
3826
|
-
const id = String(ame.contentIndex)
|
|
3989
|
+
const id = inlineTurnIndex === 0 ? String(ame.contentIndex) : `turn-${inlineTurnIndex}:${ame.contentIndex}`;
|
|
3827
3990
|
converted = [
|
|
3828
3991
|
...textStartSeen.has(ame.contentIndex) ? [] : [{ type: "text-start", id }],
|
|
3829
3992
|
{ type: "text-delta", id, delta: ame.content },
|
|
@@ -3848,10 +4011,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3848
4011
|
chunks.push({ type: "error", errorText });
|
|
3849
4012
|
sawTextChunk = true;
|
|
3850
4013
|
} else if (role === "assistant" && text.length > 0) {
|
|
4014
|
+
const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
|
|
3851
4015
|
chunks.push(
|
|
3852
|
-
{ type: "text-start", id
|
|
3853
|
-
{ type: "text-delta", id
|
|
3854
|
-
{ type: "text-end", id
|
|
4016
|
+
{ type: "text-start", id },
|
|
4017
|
+
{ type: "text-delta", id, delta: text },
|
|
4018
|
+
{ type: "text-end", id }
|
|
3855
4019
|
);
|
|
3856
4020
|
sawTextChunk = true;
|
|
3857
4021
|
assistantText += text;
|
|
@@ -3868,30 +4032,95 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3868
4032
|
chunks.push({ type: "error", errorText });
|
|
3869
4033
|
sawTextChunk = true;
|
|
3870
4034
|
} else if (role === "assistant" && text.length > 0) {
|
|
4035
|
+
const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
|
|
3871
4036
|
chunks.push(
|
|
3872
|
-
{ type: "text-start", id
|
|
3873
|
-
{ type: "text-delta", id
|
|
3874
|
-
{ type: "text-end", id
|
|
4037
|
+
{ type: "text-start", id },
|
|
4038
|
+
{ type: "text-delta", id, delta: text },
|
|
4039
|
+
{ type: "text-end", id }
|
|
3875
4040
|
);
|
|
3876
4041
|
sawTextChunk = true;
|
|
3877
4042
|
assistantText += text;
|
|
3878
4043
|
}
|
|
3879
4044
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
4045
|
+
const followUp = harnessFollowUpQueues.get(input.sessionId);
|
|
4046
|
+
if (followUp !== void 0 && !ctx.abortSignal.aborted) {
|
|
4047
|
+
harnessFollowUpQueues.delete(input.sessionId);
|
|
4048
|
+
chunks.push({ type: "data-followup-consumed" });
|
|
4049
|
+
inlineTurnIndex += 1;
|
|
4050
|
+
sawTextChunk = false;
|
|
4051
|
+
textStartSeen.clear();
|
|
4052
|
+
textDeltaSeen.clear();
|
|
4053
|
+
assistantText = "";
|
|
4054
|
+
pendingFollowUp = followUp;
|
|
4055
|
+
} else {
|
|
4056
|
+
done = true;
|
|
4057
|
+
stopHeartbeat();
|
|
4058
|
+
}
|
|
3882
4059
|
}
|
|
3883
4060
|
if (wake) wake();
|
|
3884
4061
|
});
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4062
|
+
async function prepareTurn(message, attachments) {
|
|
4063
|
+
const initialImages = [];
|
|
4064
|
+
const savedPaths = [];
|
|
4065
|
+
const writeErrors = [];
|
|
4066
|
+
for (const a of attachments ?? []) {
|
|
4067
|
+
const match = a.url.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
4068
|
+
if (!match) continue;
|
|
4069
|
+
const [, contentType, b64] = match;
|
|
4070
|
+
initialImages.push({ type: "image", mimeType: contentType, data: b64 });
|
|
4071
|
+
if (!ctx.workdir) {
|
|
4072
|
+
writeErrors.push(`${a.filename ?? "image"}: no workdir`);
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
const bytes = Buffer.from(b64, "base64");
|
|
4077
|
+
const ext = extForAttachment(a.filename ?? "image", contentType);
|
|
4078
|
+
const base = basenameForAttachment(a.filename ?? "image");
|
|
4079
|
+
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4080
|
+
const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
|
|
4081
|
+
await mkdir9(join6(ctx.workdir, DEFAULT_ATTACHMENT_DIR), { recursive: true });
|
|
4082
|
+
await writeFile7(join6(ctx.workdir, relPath), bytes);
|
|
4083
|
+
savedPaths.push(relPath);
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4086
|
+
log2.error("attachment write failed", { workdir: ctx.workdir, error: msg });
|
|
4087
|
+
writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
const attachmentNotes = [];
|
|
4091
|
+
if (savedPaths.length > 0) {
|
|
4092
|
+
attachmentNotes.push(`[Attached file(s) saved to workspace:
|
|
4093
|
+
${savedPaths.map((p) => `- ${p}`).join("\n")}]`);
|
|
4094
|
+
}
|
|
4095
|
+
if (writeErrors.length > 0) {
|
|
4096
|
+
attachmentNotes.push(`[Warning: failed to save attachment(s) to workspace \u2014 ${writeErrors.join("; ")}. The image is available via vision only.]`);
|
|
4097
|
+
}
|
|
4098
|
+
return {
|
|
4099
|
+
message: attachmentNotes.length > 0 ? `${message}
|
|
4100
|
+
|
|
4101
|
+
${attachmentNotes.join("\n")}` : message,
|
|
4102
|
+
promptOpts: initialImages.length > 0 ? { images: initialImages } : void 0
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
async function startTurn(message, attachments) {
|
|
4106
|
+
promptSettled = false;
|
|
4107
|
+
const prepared = await prepareTurn(message, attachments);
|
|
4108
|
+
return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
|
|
4109
|
+
promptSettled = true;
|
|
4110
|
+
if (pendingFollowUp !== void 0 && !ctx.abortSignal.aborted) {
|
|
4111
|
+
const next = pendingFollowUp;
|
|
4112
|
+
pendingFollowUp = void 0;
|
|
4113
|
+
promptPromise = startTurn(next.message, next.attachments);
|
|
4114
|
+
}
|
|
4115
|
+
}).catch((err) => {
|
|
4116
|
+
promptSettled = true;
|
|
4117
|
+
streamError = err;
|
|
4118
|
+
done = true;
|
|
4119
|
+
stopHeartbeat();
|
|
4120
|
+
if (wake) wake();
|
|
4121
|
+
});
|
|
4122
|
+
}
|
|
4123
|
+
promptPromise = startTurn(input.message, input.attachments);
|
|
3895
4124
|
const onAbort = () => {
|
|
3896
4125
|
if (promptSettled) {
|
|
3897
4126
|
streamError = new Error("Aborted");
|
|
@@ -3939,11 +4168,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3939
4168
|
|
|
3940
4169
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
3941
4170
|
import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
|
|
3942
|
-
import { join as
|
|
4171
|
+
import { join as join7, extname as extname2, resolve as resolve4, sep as sep3 } from "path";
|
|
3943
4172
|
import { homedir as homedir4 } from "os";
|
|
3944
4173
|
import { pathToFileURL } from "url";
|
|
3945
4174
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
3946
|
-
var GLOBAL_DIR =
|
|
4175
|
+
var GLOBAL_DIR = join7(homedir4(), ".pi", "agent", "extensions");
|
|
3947
4176
|
var LOCAL_DIR = ".pi/extensions";
|
|
3948
4177
|
var EXTENSIONS_JSON = ".pi/extensions.json";
|
|
3949
4178
|
async function dirExists(path4) {
|
|
@@ -3965,7 +4194,7 @@ async function fileExists2(path4) {
|
|
|
3965
4194
|
async function discoverFromDir(dir, source) {
|
|
3966
4195
|
if (!await dirExists(dir)) return [];
|
|
3967
4196
|
const entries = await readdir5(dir);
|
|
3968
|
-
return entries.filter((e) => VALID_EXTENSIONS.has(
|
|
4197
|
+
return entries.filter((e) => VALID_EXTENSIONS.has(extname2(e))).map((e) => ({ path: join7(dir, e), source }));
|
|
3969
4198
|
}
|
|
3970
4199
|
function extractTools(mod) {
|
|
3971
4200
|
const tools = [];
|
|
@@ -3995,17 +4224,17 @@ async function loadModule(filePath, importFn) {
|
|
|
3995
4224
|
return extractTools(mod);
|
|
3996
4225
|
}
|
|
3997
4226
|
async function discoverNpmPlugins(cwd) {
|
|
3998
|
-
const nodeModulesDir =
|
|
4227
|
+
const nodeModulesDir = join7(cwd, "node_modules");
|
|
3999
4228
|
if (!await dirExists(nodeModulesDir)) return [];
|
|
4000
4229
|
try {
|
|
4001
4230
|
const entries = await readdir5(nodeModulesDir);
|
|
4002
|
-
return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) =>
|
|
4231
|
+
return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join7(nodeModulesDir, e));
|
|
4003
4232
|
} catch {
|
|
4004
4233
|
return [];
|
|
4005
4234
|
}
|
|
4006
4235
|
}
|
|
4007
4236
|
async function loadNpmPlugin(pkgDir, importFn) {
|
|
4008
|
-
const pkgJsonPath =
|
|
4237
|
+
const pkgJsonPath = join7(pkgDir, "package.json");
|
|
4009
4238
|
if (!await fileExists2(pkgJsonPath)) return [];
|
|
4010
4239
|
const pkgJson = JSON.parse(await readFile7(pkgJsonPath, "utf-8"));
|
|
4011
4240
|
const main = pkgJson.main ?? "index.js";
|
|
@@ -4017,7 +4246,7 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
4017
4246
|
return loadModule(resolvedMain, importFn);
|
|
4018
4247
|
}
|
|
4019
4248
|
async function loadExtensionsJson(cwd) {
|
|
4020
|
-
const configPath =
|
|
4249
|
+
const configPath = join7(cwd, EXTENSIONS_JSON);
|
|
4021
4250
|
if (!await fileExists2(configPath)) return null;
|
|
4022
4251
|
try {
|
|
4023
4252
|
const raw = await readFile7(configPath, "utf-8");
|
|
@@ -4035,7 +4264,7 @@ async function loadPlugins(options) {
|
|
|
4035
4264
|
const globals = await discoverFromDir(GLOBAL_DIR, "global");
|
|
4036
4265
|
candidates.push(...globals);
|
|
4037
4266
|
}
|
|
4038
|
-
const localDir =
|
|
4267
|
+
const localDir = join7(options.cwd, LOCAL_DIR);
|
|
4039
4268
|
const locals = await discoverFromDir(localDir, "local");
|
|
4040
4269
|
candidates.push(...locals);
|
|
4041
4270
|
const npmDirs = await discoverNpmPlugins(options.cwd);
|
|
@@ -4045,7 +4274,7 @@ async function loadPlugins(options) {
|
|
|
4045
4274
|
const config = await loadExtensionsJson(options.cwd);
|
|
4046
4275
|
if (config?.npm) {
|
|
4047
4276
|
for (const pkg of config.npm) {
|
|
4048
|
-
const pkgDir =
|
|
4277
|
+
const pkgDir = join7(options.cwd, "node_modules", pkg);
|
|
4049
4278
|
if (await dirExists(pkgDir)) {
|
|
4050
4279
|
const already = candidates.some((c) => c.path === pkgDir);
|
|
4051
4280
|
if (!already) {
|
|
@@ -4094,7 +4323,7 @@ import {
|
|
|
4094
4323
|
|
|
4095
4324
|
// src/server/tools/operations/bound.ts
|
|
4096
4325
|
import { constants as constants3 } from "fs";
|
|
4097
|
-
import { access as access3, lstat as lstat3, mkdir as
|
|
4326
|
+
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";
|
|
4098
4327
|
import { dirname as dirname6, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5 } from "path";
|
|
4099
4328
|
function toPosixPath(value) {
|
|
4100
4329
|
return value.split("\\").join("/");
|
|
@@ -4232,12 +4461,12 @@ function boundFs(workspaceRoot) {
|
|
|
4232
4461
|
const write = {
|
|
4233
4462
|
async writeFile(absolutePath, content) {
|
|
4234
4463
|
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4235
|
-
await
|
|
4236
|
-
await
|
|
4464
|
+
await mkdir10(dirname6(absolutePath), { recursive: true });
|
|
4465
|
+
await writeFile8(absolutePath, content);
|
|
4237
4466
|
},
|
|
4238
4467
|
async mkdir(dir) {
|
|
4239
4468
|
await assertWithinWorkspace(workspaceRoot, dir);
|
|
4240
|
-
await
|
|
4469
|
+
await mkdir10(dir, { recursive: true });
|
|
4241
4470
|
}
|
|
4242
4471
|
};
|
|
4243
4472
|
const edit = {
|
|
@@ -4247,7 +4476,7 @@ function boundFs(workspaceRoot) {
|
|
|
4247
4476
|
},
|
|
4248
4477
|
async writeFile(absolutePath, content) {
|
|
4249
4478
|
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4250
|
-
await
|
|
4479
|
+
await writeFile8(absolutePath, content);
|
|
4251
4480
|
},
|
|
4252
4481
|
async access(absolutePath) {
|
|
4253
4482
|
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
@@ -4920,6 +5149,13 @@ function healthRoutes(app, opts, done) {
|
|
|
4920
5149
|
}
|
|
4921
5150
|
|
|
4922
5151
|
// src/server/http/routes/file.ts
|
|
5152
|
+
import { dirname as dirname7, extname as extname3, relative as relative7 } from "path/posix";
|
|
5153
|
+
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
5154
|
+
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
5155
|
+
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
5156
|
+
function defaultWorkspaceSettings() {
|
|
5157
|
+
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
5158
|
+
}
|
|
4923
5159
|
function isPathValidationError(err) {
|
|
4924
5160
|
return err instanceof Error && typeof err.reason === "string";
|
|
4925
5161
|
}
|
|
@@ -4965,12 +5201,108 @@ function requireStringParam(value, field, reply) {
|
|
|
4965
5201
|
}
|
|
4966
5202
|
return value;
|
|
4967
5203
|
}
|
|
5204
|
+
function parseWorkspaceSettings(raw) {
|
|
5205
|
+
try {
|
|
5206
|
+
const parsed = JSON.parse(raw);
|
|
5207
|
+
const dir = parsed?.markdown?.imageUploadDir;
|
|
5208
|
+
return {
|
|
5209
|
+
...parsed,
|
|
5210
|
+
markdown: {
|
|
5211
|
+
...parsed.markdown ?? {},
|
|
5212
|
+
imageUploadDir: typeof dir === "string" && dir.trim() ? dir.trim() : DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR
|
|
5213
|
+
}
|
|
5214
|
+
};
|
|
5215
|
+
} catch {
|
|
5216
|
+
return defaultWorkspaceSettings();
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
async function readWorkspaceSettings(workspace) {
|
|
5220
|
+
try {
|
|
5221
|
+
return parseWorkspaceSettings(await workspace.readFile(BORING_SETTINGS_PATH));
|
|
5222
|
+
} catch (error) {
|
|
5223
|
+
const code = error?.code;
|
|
5224
|
+
if (code === "ENOENT") return defaultWorkspaceSettings();
|
|
5225
|
+
throw error;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
function normalizeUploadDir(value) {
|
|
5229
|
+
if (typeof value !== "string") return null;
|
|
5230
|
+
const dir = value.trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
5231
|
+
if (!dir || dir.includes("\0") || dir.startsWith("/") || dir.split("/").includes("..")) return null;
|
|
5232
|
+
return dir.replace(/\/+$/, "");
|
|
5233
|
+
}
|
|
5234
|
+
function extForUpload(filename, contentType) {
|
|
5235
|
+
const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
|
|
5236
|
+
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
5237
|
+
if (contentType === "image/jpeg") return "jpg";
|
|
5238
|
+
if (contentType === "image/png") return "png";
|
|
5239
|
+
if (contentType === "image/gif") return "gif";
|
|
5240
|
+
if (contentType === "image/webp") return "webp";
|
|
5241
|
+
if (contentType === "image/svg+xml") return "svg";
|
|
5242
|
+
return "bin";
|
|
5243
|
+
}
|
|
5244
|
+
function basenameForUpload(filename) {
|
|
5245
|
+
const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
|
|
5246
|
+
const withoutExt = base.replace(/\.[^.]*$/, "");
|
|
5247
|
+
const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
5248
|
+
return safe || "image";
|
|
5249
|
+
}
|
|
5250
|
+
function markdownUrlFor(sourcePath, assetPath) {
|
|
5251
|
+
if (!sourcePath) return assetPath;
|
|
5252
|
+
const fromDir = dirname7(sourcePath.replace(/\\/g, "/"));
|
|
5253
|
+
const rel = relative7(fromDir === "." ? "" : fromDir, assetPath);
|
|
5254
|
+
return rel && !rel.startsWith(".") ? rel : rel || assetPath;
|
|
5255
|
+
}
|
|
5256
|
+
function contentTypeForPath(path4) {
|
|
5257
|
+
switch (extname3(path4).toLowerCase()) {
|
|
5258
|
+
case ".avif":
|
|
5259
|
+
return "image/avif";
|
|
5260
|
+
case ".gif":
|
|
5261
|
+
return "image/gif";
|
|
5262
|
+
case ".jpg":
|
|
5263
|
+
case ".jpeg":
|
|
5264
|
+
return "image/jpeg";
|
|
5265
|
+
case ".png":
|
|
5266
|
+
return "image/png";
|
|
5267
|
+
case ".svg":
|
|
5268
|
+
return "image/svg+xml";
|
|
5269
|
+
case ".webp":
|
|
5270
|
+
return "image/webp";
|
|
5271
|
+
case ".pdf":
|
|
5272
|
+
return "application/pdf";
|
|
5273
|
+
default:
|
|
5274
|
+
return "application/octet-stream";
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
4968
5277
|
function fileRoutes(app, opts, done) {
|
|
4969
5278
|
async function resolveWorkspace(request) {
|
|
4970
5279
|
if (opts.getWorkspace) return await opts.getWorkspace(request);
|
|
4971
5280
|
if (opts.workspace) return opts.workspace;
|
|
4972
5281
|
throw new Error("file route requires workspace or getWorkspace");
|
|
4973
5282
|
}
|
|
5283
|
+
app.get("/api/v1/files/raw", async (request, reply) => {
|
|
5284
|
+
const query = request.query;
|
|
5285
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
5286
|
+
if (path4 === null) return;
|
|
5287
|
+
try {
|
|
5288
|
+
const workspace = await resolveWorkspace(request);
|
|
5289
|
+
if (!workspace.readBinaryFile) {
|
|
5290
|
+
return reply.code(501).send({
|
|
5291
|
+
error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
|
|
5292
|
+
});
|
|
5293
|
+
}
|
|
5294
|
+
const stat7 = await workspace.stat(path4);
|
|
5295
|
+
if (stat7.kind !== "file") {
|
|
5296
|
+
return reply.code(400).send({
|
|
5297
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
5298
|
+
});
|
|
5299
|
+
}
|
|
5300
|
+
const bytes = await workspace.readBinaryFile(path4);
|
|
5301
|
+
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
5302
|
+
} catch (err) {
|
|
5303
|
+
return classifyError(err, reply, "file");
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
4974
5306
|
app.get("/api/v1/files", async (request, reply) => {
|
|
4975
5307
|
const query = request.query;
|
|
4976
5308
|
const path4 = requireStringParam(query.path, "path", reply);
|
|
@@ -5041,6 +5373,87 @@ function fileRoutes(app, opts, done) {
|
|
|
5041
5373
|
return classifyError(err, reply, "file");
|
|
5042
5374
|
}
|
|
5043
5375
|
});
|
|
5376
|
+
app.post("/api/v1/files/upload", async (request, reply) => {
|
|
5377
|
+
const body = request.body;
|
|
5378
|
+
const filename = requireStringParam(body?.filename, "filename", reply);
|
|
5379
|
+
if (filename === null) return;
|
|
5380
|
+
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
5381
|
+
if (contentBase64 === null) return;
|
|
5382
|
+
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
5383
|
+
if (!contentType.startsWith("image/")) {
|
|
5384
|
+
return reply.code(400).send({
|
|
5385
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
5386
|
+
});
|
|
5387
|
+
}
|
|
5388
|
+
try {
|
|
5389
|
+
const workspace = await resolveWorkspace(request);
|
|
5390
|
+
const settings = await readWorkspaceSettings(workspace);
|
|
5391
|
+
const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
|
|
5392
|
+
const ext = extForUpload(filename, contentType);
|
|
5393
|
+
const base = basenameForUpload(filename);
|
|
5394
|
+
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5395
|
+
const path4 = `${dir}/${base}-${unique}.${ext}`;
|
|
5396
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
5397
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
5398
|
+
return reply.code(400).send({
|
|
5399
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `upload must be 1 byte to ${MAX_UPLOAD_BYTES} bytes`, field: "contentBase64" }
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
await workspace.mkdir(dir, { recursive: true });
|
|
5403
|
+
const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
5404
|
+
if (!workspace.writeBinaryFile) {
|
|
5405
|
+
throw new Error("workspace does not support binary uploads");
|
|
5406
|
+
}
|
|
5407
|
+
await workspace.writeBinaryFile(path4, bytes);
|
|
5408
|
+
return await workspace.stat(path4);
|
|
5409
|
+
})();
|
|
5410
|
+
const sourcePath = typeof body.sourcePath === "string" && !body.sourcePath.includes("\0") ? body.sourcePath : null;
|
|
5411
|
+
return {
|
|
5412
|
+
ok: true,
|
|
5413
|
+
path: path4,
|
|
5414
|
+
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
5415
|
+
mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
|
|
5416
|
+
};
|
|
5417
|
+
} catch (err) {
|
|
5418
|
+
return classifyError(err, reply, "upload");
|
|
5419
|
+
}
|
|
5420
|
+
});
|
|
5421
|
+
app.get("/api/v1/workspace-settings", async (request, reply) => {
|
|
5422
|
+
try {
|
|
5423
|
+
const workspace = await resolveWorkspace(request);
|
|
5424
|
+
return { settings: await readWorkspaceSettings(workspace) };
|
|
5425
|
+
} catch (err) {
|
|
5426
|
+
return classifyError(err, reply, "settings");
|
|
5427
|
+
}
|
|
5428
|
+
});
|
|
5429
|
+
app.put("/api/v1/workspace-settings", async (request, reply) => {
|
|
5430
|
+
const body = request.body;
|
|
5431
|
+
const incoming = body?.settings ?? body;
|
|
5432
|
+
const markdown = incoming.markdown ?? {};
|
|
5433
|
+
const imageUploadDir = normalizeUploadDir(markdown.imageUploadDir);
|
|
5434
|
+
if (!imageUploadDir) {
|
|
5435
|
+
return reply.code(400).send({
|
|
5436
|
+
error: { code: ERROR_CODE_INVALID_PATH, message: "markdown.imageUploadDir must be a relative workspace path", field: "markdown.imageUploadDir" }
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5439
|
+
try {
|
|
5440
|
+
const workspace = await resolveWorkspace(request);
|
|
5441
|
+
const current = await readWorkspaceSettings(workspace);
|
|
5442
|
+
const next = {
|
|
5443
|
+
...current,
|
|
5444
|
+
markdown: {
|
|
5445
|
+
...current.markdown ?? {},
|
|
5446
|
+
imageUploadDir
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
5449
|
+
await workspace.mkdir(".boring", { recursive: true });
|
|
5450
|
+
await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
|
|
5451
|
+
`);
|
|
5452
|
+
return { settings: next };
|
|
5453
|
+
} catch (err) {
|
|
5454
|
+
return classifyError(err, reply, "settings");
|
|
5455
|
+
}
|
|
5456
|
+
});
|
|
5044
5457
|
app.delete("/api/v1/files", async (request, reply) => {
|
|
5045
5458
|
const query = request.query;
|
|
5046
5459
|
const path4 = requireStringParam(query.path, "path", reply);
|
|
@@ -5568,7 +5981,7 @@ function chatRoutes(app, opts, done) {
|
|
|
5568
5981
|
"/api/v1/agent/chat",
|
|
5569
5982
|
{ preHandler: validateBody },
|
|
5570
5983
|
async (request, reply) => {
|
|
5571
|
-
const { sessionId, message, model, thinkingLevel } = request.body;
|
|
5984
|
+
const { sessionId, message, model, thinkingLevel, attachments } = request.body;
|
|
5572
5985
|
const turnId = randomUUID3();
|
|
5573
5986
|
request.log.info({ sessionId, turnId, model, thinkingLevel }, "[chat] start");
|
|
5574
5987
|
const runtime = await resolveRuntime(request);
|
|
@@ -5587,7 +6000,7 @@ function chatRoutes(app, opts, done) {
|
|
|
5587
6000
|
const buf = buffers.create(sessionId, turnId);
|
|
5588
6001
|
try {
|
|
5589
6002
|
const chunks = runtime.harness.sendMessage(
|
|
5590
|
-
{ sessionId, message, model, thinkingLevel },
|
|
6003
|
+
{ sessionId, message, model, thinkingLevel, attachments },
|
|
5591
6004
|
ctx
|
|
5592
6005
|
);
|
|
5593
6006
|
const stream = createUIMessageStream({
|
|
@@ -5719,6 +6132,36 @@ function chatRoutes(app, opts, done) {
|
|
|
5719
6132
|
}
|
|
5720
6133
|
}
|
|
5721
6134
|
);
|
|
6135
|
+
app.post(
|
|
6136
|
+
"/api/v1/agent/chat/:sessionId/followup",
|
|
6137
|
+
async (request, reply) => {
|
|
6138
|
+
const { sessionId } = request.params;
|
|
6139
|
+
const body = request.body;
|
|
6140
|
+
if (typeof body?.message !== "string" || body.message.length === 0) {
|
|
6141
|
+
return reply.code(400).send({
|
|
6142
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "message is required" }
|
|
6143
|
+
});
|
|
6144
|
+
}
|
|
6145
|
+
const parsedAttachments = chatBodySchema.shape.attachments.safeParse(body.attachments);
|
|
6146
|
+
if (!parsedAttachments.success) {
|
|
6147
|
+
return reply.code(400).send({
|
|
6148
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "attachments is invalid" }
|
|
6149
|
+
});
|
|
6150
|
+
}
|
|
6151
|
+
const runtime = await resolveRuntime(request);
|
|
6152
|
+
runtime.harness.followUp?.(sessionId, body.message, parsedAttachments.data);
|
|
6153
|
+
return reply.code(202).send({ queued: true });
|
|
6154
|
+
}
|
|
6155
|
+
);
|
|
6156
|
+
app.delete(
|
|
6157
|
+
"/api/v1/agent/chat/:sessionId/followup",
|
|
6158
|
+
async (request, reply) => {
|
|
6159
|
+
const { sessionId } = request.params;
|
|
6160
|
+
const runtime = await resolveRuntime(request);
|
|
6161
|
+
runtime.harness.clearFollowUp?.(sessionId);
|
|
6162
|
+
return reply.code(204).send();
|
|
6163
|
+
}
|
|
6164
|
+
);
|
|
5722
6165
|
app.put(
|
|
5723
6166
|
"/api/v1/agent/chat/:sessionId/messages",
|
|
5724
6167
|
async (request, reply) => {
|
|
@@ -5806,7 +6249,11 @@ function skillsRoutes(app, opts, done) {
|
|
|
5806
6249
|
return reply.code(200).send({ skills: cached.skills });
|
|
5807
6250
|
}
|
|
5808
6251
|
try {
|
|
5809
|
-
const result = loadSkills2({
|
|
6252
|
+
const result = loadSkills2({
|
|
6253
|
+
cwd: opts.workspaceRoot,
|
|
6254
|
+
skillPaths: opts.additionalSkillPaths ?? [],
|
|
6255
|
+
includeDefaults: true
|
|
6256
|
+
});
|
|
5810
6257
|
const skills = result.skills.map((s) => ({
|
|
5811
6258
|
name: s.name,
|
|
5812
6259
|
description: s.description
|
|
@@ -6221,7 +6668,7 @@ async function createAgentApp(opts = {}) {
|
|
|
6221
6668
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
6222
6669
|
const sessionId = opts.sessionId ?? DEFAULT_SESSION_ID;
|
|
6223
6670
|
const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
|
|
6224
|
-
const app = Fastify({ logger: opts.logger ?? true });
|
|
6671
|
+
const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 16 * 1024 * 1024 });
|
|
6225
6672
|
const resolvedMode = opts.mode ?? autoDetectMode();
|
|
6226
6673
|
const runtimeBundle = await resolveMode(resolvedMode).create({
|
|
6227
6674
|
workspaceRoot,
|
|
@@ -6289,7 +6736,10 @@ async function createAgentApp(opts = {}) {
|
|
|
6289
6736
|
});
|
|
6290
6737
|
await app.register(systemPromptRoutes, { harness });
|
|
6291
6738
|
await app.register(modelsRoutes);
|
|
6292
|
-
await app.register(skillsRoutes, {
|
|
6739
|
+
await app.register(skillsRoutes, {
|
|
6740
|
+
workspaceRoot,
|
|
6741
|
+
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths
|
|
6742
|
+
});
|
|
6293
6743
|
await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
|
|
6294
6744
|
await app.register(catalogRoutes, { tools });
|
|
6295
6745
|
await app.register(readyStatusRoutes, { tracker: readyTracker });
|
|
@@ -6350,6 +6800,113 @@ function mergeTools(options) {
|
|
|
6350
6800
|
return [...merged.values()];
|
|
6351
6801
|
}
|
|
6352
6802
|
|
|
6803
|
+
// src/server/tools/upload/index.ts
|
|
6804
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
6805
|
+
import { extname as extname4, join as join8 } from "path";
|
|
6806
|
+
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
6807
|
+
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
6808
|
+
function contentTypeFromExt(path4) {
|
|
6809
|
+
switch (extname4(path4).toLowerCase()) {
|
|
6810
|
+
case ".jpg":
|
|
6811
|
+
case ".jpeg":
|
|
6812
|
+
return "image/jpeg";
|
|
6813
|
+
case ".png":
|
|
6814
|
+
return "image/png";
|
|
6815
|
+
case ".gif":
|
|
6816
|
+
return "image/gif";
|
|
6817
|
+
case ".webp":
|
|
6818
|
+
return "image/webp";
|
|
6819
|
+
case ".svg":
|
|
6820
|
+
return "image/svg+xml";
|
|
6821
|
+
case ".avif":
|
|
6822
|
+
return "image/avif";
|
|
6823
|
+
default:
|
|
6824
|
+
return "application/octet-stream";
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
function extForUpload2(filename, contentType) {
|
|
6828
|
+
const fromName = extname4(filename).toLowerCase().replace(/^\./, "");
|
|
6829
|
+
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
6830
|
+
if (contentType === "image/jpeg") return "jpg";
|
|
6831
|
+
if (contentType === "image/png") return "png";
|
|
6832
|
+
if (contentType === "image/gif") return "gif";
|
|
6833
|
+
if (contentType === "image/webp") return "webp";
|
|
6834
|
+
if (contentType === "image/svg+xml") return "svg";
|
|
6835
|
+
return "bin";
|
|
6836
|
+
}
|
|
6837
|
+
function basenameForUpload2(filename) {
|
|
6838
|
+
const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
|
|
6839
|
+
const withoutExt = base.replace(/\.[^.]*$/, "");
|
|
6840
|
+
const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
6841
|
+
return safe || "image";
|
|
6842
|
+
}
|
|
6843
|
+
function buildUploadAgentTools(bundle) {
|
|
6844
|
+
const { workspace } = bundle;
|
|
6845
|
+
const cwd = workspace.root;
|
|
6846
|
+
return [
|
|
6847
|
+
{
|
|
6848
|
+
name: "upload_file",
|
|
6849
|
+
description: "Copy a workspace file into artifact storage (assets/images by default) and return its workspace-relative path. Use this when you want to embed an image in markdown or make a generated file accessible as a stable artifact. The returned path can be used directly in markdown image syntax: ",
|
|
6850
|
+
parameters: {
|
|
6851
|
+
type: "object",
|
|
6852
|
+
properties: {
|
|
6853
|
+
path: {
|
|
6854
|
+
type: "string",
|
|
6855
|
+
description: 'Workspace-relative path of the source file (e.g. "chart.png" or "output/plot.png")'
|
|
6856
|
+
},
|
|
6857
|
+
directory: {
|
|
6858
|
+
type: "string",
|
|
6859
|
+
description: `Destination directory inside the workspace. Defaults to "${DEFAULT_UPLOAD_DIR}".`
|
|
6860
|
+
}
|
|
6861
|
+
},
|
|
6862
|
+
required: ["path"],
|
|
6863
|
+
additionalProperties: false
|
|
6864
|
+
},
|
|
6865
|
+
async execute(input) {
|
|
6866
|
+
const filePath = typeof input.path === "string" ? input.path.trim() : "";
|
|
6867
|
+
if (!filePath || filePath.includes("\0") || filePath.startsWith("/") || filePath.split("/").includes("..")) {
|
|
6868
|
+
return { content: [{ type: "text", text: "invalid path" }], isError: true };
|
|
6869
|
+
}
|
|
6870
|
+
const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
|
|
6871
|
+
const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
|
|
6872
|
+
try {
|
|
6873
|
+
const bytes = await readFile9(join8(cwd, filePath));
|
|
6874
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
|
|
6875
|
+
return {
|
|
6876
|
+
content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
|
|
6877
|
+
isError: true
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
const contentType = contentTypeFromExt(filePath);
|
|
6881
|
+
const ext = extForUpload2(filePath, contentType);
|
|
6882
|
+
const base = basenameForUpload2(filePath);
|
|
6883
|
+
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6884
|
+
const destPath = `${dir}/${base}-${unique}.${ext}`;
|
|
6885
|
+
await workspace.mkdir(dir, { recursive: true });
|
|
6886
|
+
if (workspace.writeBinaryFile) {
|
|
6887
|
+
await workspace.writeBinaryFile(destPath, bytes);
|
|
6888
|
+
} else {
|
|
6889
|
+
return {
|
|
6890
|
+
content: [{ type: "text", text: "workspace does not support binary file writes" }],
|
|
6891
|
+
isError: true
|
|
6892
|
+
};
|
|
6893
|
+
}
|
|
6894
|
+
return {
|
|
6895
|
+
content: [{ type: "text", text: `Uploaded to ${destPath}` }],
|
|
6896
|
+
isError: false,
|
|
6897
|
+
details: { path: destPath }
|
|
6898
|
+
};
|
|
6899
|
+
} catch (err) {
|
|
6900
|
+
return {
|
|
6901
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : "upload failed" }],
|
|
6902
|
+
isError: true
|
|
6903
|
+
};
|
|
6904
|
+
}
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
];
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6353
6910
|
// src/server/registerAgentRoutes.ts
|
|
6354
6911
|
var DEFAULT_VERSION2 = "0.1.0-dev";
|
|
6355
6912
|
var DEFAULT_WORKSPACE_ID3 = "default";
|
|
@@ -6420,16 +6977,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
6420
6977
|
key: `${resolvedMode}:${workspaceId}:${root}`
|
|
6421
6978
|
};
|
|
6422
6979
|
}
|
|
6423
|
-
async function createRuntimeBinding(workspaceId, root) {
|
|
6980
|
+
async function createRuntimeBinding(workspaceId, root, request) {
|
|
6981
|
+
const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
|
|
6424
6982
|
const runtimeBundle = await modeAdapter.create({
|
|
6425
6983
|
workspaceRoot: root,
|
|
6426
6984
|
sessionId: workspaceId,
|
|
6427
6985
|
workspaceId,
|
|
6428
|
-
templatePath
|
|
6986
|
+
templatePath: scopedTemplatePath
|
|
6429
6987
|
});
|
|
6430
6988
|
const standardTools = [
|
|
6431
6989
|
...buildHarnessAgentTools(runtimeBundle),
|
|
6432
|
-
...buildFilesystemAgentTools(runtimeBundle)
|
|
6990
|
+
...buildFilesystemAgentTools(runtimeBundle),
|
|
6991
|
+
...buildUploadAgentTools(runtimeBundle)
|
|
6433
6992
|
];
|
|
6434
6993
|
const pluginTools = [];
|
|
6435
6994
|
if (resolvedMode !== "vercel-sandbox") {
|
|
@@ -6494,7 +7053,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
6494
7053
|
await existing
|
|
6495
7054
|
);
|
|
6496
7055
|
}
|
|
6497
|
-
const created = createRuntimeBinding(workspaceId, scope.root);
|
|
7056
|
+
const created = createRuntimeBinding(workspaceId, scope.root, request);
|
|
6498
7057
|
runtimeBindings.set(scope.key, created);
|
|
6499
7058
|
try {
|
|
6500
7059
|
return await ensureRuntimeBindingReady(
|
|
@@ -6631,7 +7190,10 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
6631
7190
|
getHarness: async (request) => (await getBindingForRequest(request)).harness
|
|
6632
7191
|
});
|
|
6633
7192
|
await app.register(modelsRoutes);
|
|
6634
|
-
await app.register(skillsRoutes, {
|
|
7193
|
+
await app.register(skillsRoutes, {
|
|
7194
|
+
workspaceRoot,
|
|
7195
|
+
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths
|
|
7196
|
+
});
|
|
6635
7197
|
await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
|
|
6636
7198
|
await app.register(
|
|
6637
7199
|
catalogRoutes,
|