chore: v2.8.13 — Artifacts reinstall/edit/installs, agent steps in chat, pick a local file to attach, Windows paths as links

- Chat: an Agent step shows its task, status and tool calls and opens its conversation in place; Windows paths (E:\…, C:/…) in replies open as files
- Prompt: attach a file on this computer by path (native picker), clearer too-large messages
- Artifacts: edit title/description and a skill's files (new snapshot), Reinstall, install history, Hub-only rows, skills installed into ~/.claude/skills and ~/.agents/skills
- Files: batch upload of small files in one extract
- AI Tools: npm install falls back to --prefix ~/.local when the global prefix is not writable
This commit is contained in:
2026-10-20 16:44:55 +07:00
parent 49f87c4da3
commit 0547488f13
59 changed files with 4285 additions and 690 deletions
+63
View File
@@ -332,6 +332,69 @@ describe("POST /api/run/drop-image", () => {
});
assert.equal(r.status, 400);
});
it("refuses a file declared over the limit before its body arrives", async () => {
// 4 GB announced, 1 KB sent: the answer must come without the rest.
const { status, body } = await new Promise((resolve, reject) => {
const url = new URL("/api/run/drop-file?name=big.mp4", BASE);
const req = http.request(url, {
method: "POST",
headers: { "Content-Type": "video/mp4", "Content-Length": 4 * 1024 ** 3 },
});
req.on("response", (res) => {
let text = "";
res.on("data", (c) => (text += c));
res.on("end", () => {
req.destroy();
resolve({ status: res.statusCode, body: JSON.parse(text) });
});
});
req.on("error", reject);
req.write(Buffer.alloc(1024));
});
assert.equal(status, 413);
assert.equal(body.error.code, "ETOOBIG");
assert.equal(body.error.max_bytes, 20 * 1024 * 1024); // the UI shows the server's limit
});
});
describe("POST /api/run/local-paths", () => {
let srv;
before(async () => {
srv = http.createServer(createApp());
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
BASE = `http://127.0.0.1:${srv.address().port}`;
});
after(() => new Promise((r) => srv.close(r)));
it("answers the size of each regular file, null for anything else", async () => {
const file = path.join(TMP, "clip.mp4");
fs.writeFileSync(file, "12345");
const r = await fetchJson("/api/run/local-paths", {
method: "POST",
body: { paths: [file, TMP, path.join(TMP, "missing.bin")] },
});
assert.equal(r.status, 200);
assert.deepEqual(r.body.sizes, [5, null, null]);
});
it("refuses a relative path", async () => {
const r = await fetchJson("/api/run/local-paths", {
method: "POST",
body: { paths: ["clip.mp4"] },
});
assert.equal(r.status, 400);
});
it("refuses a request a proxy forwarded: that browser is elsewhere", async () => {
const r = await fetchJson("/api/run/local-paths", {
method: "POST",
headers: { "X-Forwarded-For": "10.0.0.5" },
body: { paths: [TMP] },
});
assert.equal(r.status, 403);
assert.equal(r.body.error.code, "EREMOTE");
});
});
describe("GET /api/run/drop/:name", () => {