@sandagent/daemon 0.9.4 → 0.9.6
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 +49 -0
- package/dist/bundle.mjs +93 -2
- package/dist/index.js +92 -0
- package/dist/multipart.d.ts +13 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/nextjs.d.ts.map +1 -1
- package/dist/nextjs.js +80 -0
- package/dist/routes/fs.d.ts +22 -0
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -325,6 +325,55 @@ Response: `application/x-ndjson` chunked stream — each line is an AI SDK UI me
|
|
|
325
325
|
| POST | `/api/fs/remove` | `{"path":"tmp","recursive":true}` |
|
|
326
326
|
| POST | `/api/fs/move` | `{"from":"a.txt","to":"b.txt"}` |
|
|
327
327
|
| POST | `/api/fs/copy` | `{"from":"a.txt","to":"b.txt"}` |
|
|
328
|
+
| POST | `/api/fs/upload` | `multipart/form-data` — see below |
|
|
329
|
+
|
|
330
|
+
#### `POST /api/fs/upload`
|
|
331
|
+
|
|
332
|
+
Upload one or more files via `multipart/form-data`.
|
|
333
|
+
|
|
334
|
+
Form fields:
|
|
335
|
+
|
|
336
|
+
| Field | Type | Required | Description |
|
|
337
|
+
|-------|------|----------|-------------|
|
|
338
|
+
| `path` | string | No | Target directory (default: `.`) |
|
|
339
|
+
| `volume` | string | No | Volume name for multi-tenant isolation |
|
|
340
|
+
| `create_dirs` | string | No | Create parent dirs (default: `"true"`) |
|
|
341
|
+
| `file` | file | Yes | One or more files to upload |
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Upload a single file
|
|
347
|
+
curl -X POST http://localhost:3080/api/fs/upload \
|
|
348
|
+
-F "path=uploads" \
|
|
349
|
+
-F "file=@local-file.txt"
|
|
350
|
+
|
|
351
|
+
# Upload multiple files
|
|
352
|
+
curl -X POST http://localhost:3080/api/fs/upload \
|
|
353
|
+
-F "path=data" \
|
|
354
|
+
-F "file=@report.csv" \
|
|
355
|
+
-F "file=@image.png"
|
|
356
|
+
|
|
357
|
+
# Upload to a specific volume
|
|
358
|
+
curl -X POST http://localhost:3080/api/fs/upload \
|
|
359
|
+
-F "path=docs" \
|
|
360
|
+
-F "volume=vol-001" \
|
|
361
|
+
-F "file=@readme.md"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Response:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"ok": true,
|
|
369
|
+
"data": {
|
|
370
|
+
"files": [
|
|
371
|
+
{ "fieldname": "file", "filename": "report.csv", "path": "/workspace/data/report.csv", "size": 1234 }
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
"error": null
|
|
375
|
+
}
|
|
376
|
+
```
|
|
328
377
|
|
|
329
378
|
All fs endpoints accept optional `volume` for multi-tenant isolation.
|
|
330
379
|
|
package/dist/bundle.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __banner_cjsRequire } from "module"; const require = __banner_cjsRequire(import.meta.url);
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -206419,6 +206420,42 @@ var require_lib4 = __commonJS({
|
|
|
206419
206420
|
import * as http3 from "node:http";
|
|
206420
206421
|
import { URL as URL2 } from "node:url";
|
|
206421
206422
|
|
|
206423
|
+
// src/multipart.ts
|
|
206424
|
+
function parseMultipart(contentType, body) {
|
|
206425
|
+
const match2 = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
|
|
206426
|
+
if (!match2) throw new Error("missing multipart boundary");
|
|
206427
|
+
const boundary = match2[1] ?? match2[2];
|
|
206428
|
+
const delimiter2 = Buffer.from(`--${boundary}`);
|
|
206429
|
+
const fields = {};
|
|
206430
|
+
const files = [];
|
|
206431
|
+
let start = body.indexOf(delimiter2) + delimiter2.length;
|
|
206432
|
+
while (start < body.length) {
|
|
206433
|
+
const end = body.indexOf(delimiter2, start);
|
|
206434
|
+
if (end === -1) break;
|
|
206435
|
+
const part = body.subarray(start, end);
|
|
206436
|
+
const partStart = part[0] === 13 && part[1] === 10 ? 2 : 0;
|
|
206437
|
+
const headerEnd = part.indexOf("\r\n\r\n", partStart);
|
|
206438
|
+
if (headerEnd === -1) {
|
|
206439
|
+
start = end + delimiter2.length;
|
|
206440
|
+
continue;
|
|
206441
|
+
}
|
|
206442
|
+
const headerStr = part.subarray(partStart, headerEnd).toString("utf-8");
|
|
206443
|
+
let data = part.subarray(headerEnd + 4);
|
|
206444
|
+
if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
|
|
206445
|
+
data = data.subarray(0, data.length - 2);
|
|
206446
|
+
}
|
|
206447
|
+
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
|
206448
|
+
const filenameMatch = headerStr.match(/filename="([^"]+)"/);
|
|
206449
|
+
if (filenameMatch && nameMatch) {
|
|
206450
|
+
files.push({ filename: filenameMatch[1], data: Buffer.from(data) });
|
|
206451
|
+
} else if (nameMatch) {
|
|
206452
|
+
fields[nameMatch[1]] = data.toString("utf-8");
|
|
206453
|
+
}
|
|
206454
|
+
start = end + delimiter2.length;
|
|
206455
|
+
}
|
|
206456
|
+
return { fields, files };
|
|
206457
|
+
}
|
|
206458
|
+
|
|
206422
206459
|
// src/routes/fs.ts
|
|
206423
206460
|
import * as fs2 from "node:fs/promises";
|
|
206424
206461
|
import * as path2 from "node:path";
|
|
@@ -206596,6 +206633,26 @@ async function fsCopy(state, body) {
|
|
|
206596
206633
|
await fs2.copyFile(from, to);
|
|
206597
206634
|
return ok({ path: to });
|
|
206598
206635
|
}
|
|
206636
|
+
async function fsUpload(state, parts) {
|
|
206637
|
+
const volume = parts.fields.volume;
|
|
206638
|
+
const targetDir = parts.fields.path ?? ".";
|
|
206639
|
+
const createDirs = parts.fields.create_dirs !== "false";
|
|
206640
|
+
const root2 = resolveVolumeRoot(state, volume);
|
|
206641
|
+
const dir = resolveUnderRoot(root2, targetDir);
|
|
206642
|
+
if (createDirs) await ensureDir(dir);
|
|
206643
|
+
const results = [];
|
|
206644
|
+
for (const file of parts.files) {
|
|
206645
|
+
const target = resolveUnderRoot(root2, path2.join(targetDir, file.filename));
|
|
206646
|
+
await fs2.writeFile(target, file.data);
|
|
206647
|
+
results.push({
|
|
206648
|
+
fieldname: "file",
|
|
206649
|
+
filename: file.filename,
|
|
206650
|
+
path: target,
|
|
206651
|
+
size: file.data.length
|
|
206652
|
+
});
|
|
206653
|
+
}
|
|
206654
|
+
return ok({ files: results });
|
|
206655
|
+
}
|
|
206599
206656
|
|
|
206600
206657
|
// src/routes/git.ts
|
|
206601
206658
|
import { execFile } from "node:child_process";
|
|
@@ -262410,6 +262467,10 @@ async function sandagentRun(req, res, env2) {
|
|
|
262410
262467
|
function createDaemon(config) {
|
|
262411
262468
|
const router = new DaemonRouter({ root: config.root });
|
|
262412
262469
|
const env2 = process.env;
|
|
262470
|
+
const state = {
|
|
262471
|
+
root: config.root,
|
|
262472
|
+
volumesRoot: `${config.root}/volumes`
|
|
262473
|
+
};
|
|
262413
262474
|
return http3.createServer(async (req, res) => {
|
|
262414
262475
|
const method = req.method ?? "GET";
|
|
262415
262476
|
const url = new URL2(
|
|
@@ -262421,6 +262482,29 @@ function createDaemon(config) {
|
|
|
262421
262482
|
const body2 = JSON.parse(await readBody(req) || "{}");
|
|
262422
262483
|
return sandagentRun(body2, res, env2);
|
|
262423
262484
|
}
|
|
262485
|
+
if (method === "POST" && pathname === "/api/fs/upload") {
|
|
262486
|
+
try {
|
|
262487
|
+
const ct2 = req.headers["content-type"] ?? "";
|
|
262488
|
+
if (!ct2.includes("multipart/form-data")) {
|
|
262489
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
262490
|
+
res.end(
|
|
262491
|
+
JSON.stringify(fail("content-type must be multipart/form-data"))
|
|
262492
|
+
);
|
|
262493
|
+
return;
|
|
262494
|
+
}
|
|
262495
|
+
const raw = await readBodyRaw(req);
|
|
262496
|
+
const parts = parseMultipart(ct2, raw);
|
|
262497
|
+
const result2 = await fsUpload(state, parts);
|
|
262498
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
262499
|
+
res.end(JSON.stringify(result2));
|
|
262500
|
+
} catch (err) {
|
|
262501
|
+
const status2 = err instanceof AppError ? err.status : 500;
|
|
262502
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
262503
|
+
res.writeHead(status2, { "Content-Type": "application/json" });
|
|
262504
|
+
res.end(JSON.stringify(fail(msg)));
|
|
262505
|
+
}
|
|
262506
|
+
return;
|
|
262507
|
+
}
|
|
262424
262508
|
const params = method === "GET" ? Object.fromEntries(url.searchParams) : JSON.parse(await readBody(req) || "{}");
|
|
262425
262509
|
const result = await router.handle(method, pathname, params);
|
|
262426
262510
|
const status = result?.status ?? 404;
|
|
@@ -262437,14 +262521,21 @@ function readBody(req) {
|
|
|
262437
262521
|
req.on("error", reject);
|
|
262438
262522
|
});
|
|
262439
262523
|
}
|
|
262524
|
+
function readBodyRaw(req) {
|
|
262525
|
+
return new Promise((resolve14, reject) => {
|
|
262526
|
+
const chunks = [];
|
|
262527
|
+
req.on("data", (c) => chunks.push(c));
|
|
262528
|
+
req.on("end", () => resolve14(Buffer.concat(chunks)));
|
|
262529
|
+
req.on("error", reject);
|
|
262530
|
+
});
|
|
262531
|
+
}
|
|
262440
262532
|
|
|
262441
262533
|
// src/cli.ts
|
|
262442
262534
|
var host = process.env.SANDAGENT_DAEMON_HOST ?? "0.0.0.0";
|
|
262443
262535
|
var port = Number(process.env.SANDAGENT_DAEMON_PORT ?? "3080");
|
|
262444
|
-
var root = process.env.SANDAGENT_ROOT ?? "/
|
|
262536
|
+
var root = process.env.SANDAGENT_ROOT ?? "/agent";
|
|
262445
262537
|
async function main2() {
|
|
262446
262538
|
await ensureDir(root);
|
|
262447
|
-
await ensureDir(`${root}/volumes`);
|
|
262448
262539
|
const server = createDaemon({ host, port, root });
|
|
262449
262540
|
server.listen(port, host, () => {
|
|
262450
262541
|
console.log(`sandagent-daemon listening on http://${host}:${port}`);
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire as __banner_cjsRequire } from "module"; const require = __banner_cjsRequire(import.meta.url);
|
|
1
2
|
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -206591,6 +206592,26 @@ async function fsCopy(state, body) {
|
|
|
206591
206592
|
await fs2.copyFile(from, to);
|
|
206592
206593
|
return ok({ path: to });
|
|
206593
206594
|
}
|
|
206595
|
+
async function fsUpload(state, parts) {
|
|
206596
|
+
const volume = parts.fields.volume;
|
|
206597
|
+
const targetDir = parts.fields.path ?? ".";
|
|
206598
|
+
const createDirs = parts.fields.create_dirs !== "false";
|
|
206599
|
+
const root = resolveVolumeRoot(state, volume);
|
|
206600
|
+
const dir = resolveUnderRoot(root, targetDir);
|
|
206601
|
+
if (createDirs) await ensureDir(dir);
|
|
206602
|
+
const results = [];
|
|
206603
|
+
for (const file of parts.files) {
|
|
206604
|
+
const target = resolveUnderRoot(root, path2.join(targetDir, file.filename));
|
|
206605
|
+
await fs2.writeFile(target, file.data);
|
|
206606
|
+
results.push({
|
|
206607
|
+
fieldname: "file",
|
|
206608
|
+
filename: file.filename,
|
|
206609
|
+
path: target,
|
|
206610
|
+
size: file.data.length
|
|
206611
|
+
});
|
|
206612
|
+
}
|
|
206613
|
+
return ok({ files: results });
|
|
206614
|
+
}
|
|
206594
206615
|
|
|
206595
206616
|
// src/routes/git.ts
|
|
206596
206617
|
import { execFile } from "node:child_process";
|
|
@@ -206764,6 +206785,42 @@ var DaemonRouter = class {
|
|
|
206764
206785
|
import * as http3 from "node:http";
|
|
206765
206786
|
import { URL as URL2 } from "node:url";
|
|
206766
206787
|
|
|
206788
|
+
// src/multipart.ts
|
|
206789
|
+
function parseMultipart(contentType, body) {
|
|
206790
|
+
const match2 = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
|
|
206791
|
+
if (!match2) throw new Error("missing multipart boundary");
|
|
206792
|
+
const boundary = match2[1] ?? match2[2];
|
|
206793
|
+
const delimiter2 = Buffer.from(`--${boundary}`);
|
|
206794
|
+
const fields = {};
|
|
206795
|
+
const files = [];
|
|
206796
|
+
let start = body.indexOf(delimiter2) + delimiter2.length;
|
|
206797
|
+
while (start < body.length) {
|
|
206798
|
+
const end = body.indexOf(delimiter2, start);
|
|
206799
|
+
if (end === -1) break;
|
|
206800
|
+
const part = body.subarray(start, end);
|
|
206801
|
+
const partStart = part[0] === 13 && part[1] === 10 ? 2 : 0;
|
|
206802
|
+
const headerEnd = part.indexOf("\r\n\r\n", partStart);
|
|
206803
|
+
if (headerEnd === -1) {
|
|
206804
|
+
start = end + delimiter2.length;
|
|
206805
|
+
continue;
|
|
206806
|
+
}
|
|
206807
|
+
const headerStr = part.subarray(partStart, headerEnd).toString("utf-8");
|
|
206808
|
+
let data = part.subarray(headerEnd + 4);
|
|
206809
|
+
if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
|
|
206810
|
+
data = data.subarray(0, data.length - 2);
|
|
206811
|
+
}
|
|
206812
|
+
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
|
206813
|
+
const filenameMatch = headerStr.match(/filename="([^"]+)"/);
|
|
206814
|
+
if (filenameMatch && nameMatch) {
|
|
206815
|
+
files.push({ filename: filenameMatch[1], data: Buffer.from(data) });
|
|
206816
|
+
} else if (nameMatch) {
|
|
206817
|
+
fields[nameMatch[1]] = data.toString("utf-8");
|
|
206818
|
+
}
|
|
206819
|
+
start = end + delimiter2.length;
|
|
206820
|
+
}
|
|
206821
|
+
return { fields, files };
|
|
206822
|
+
}
|
|
206823
|
+
|
|
206767
206824
|
// ../../packages/runner-claude/dist/ai-sdk-stream.js
|
|
206768
206825
|
import { appendFileSync, existsSync, unlinkSync } from "node:fs";
|
|
206769
206826
|
import { join as join3 } from "node:path";
|
|
@@ -262409,6 +262466,10 @@ async function sandagentRun(req, res, env2) {
|
|
|
262409
262466
|
function createDaemon(config) {
|
|
262410
262467
|
const router = new DaemonRouter({ root: config.root });
|
|
262411
262468
|
const env2 = process.env;
|
|
262469
|
+
const state = {
|
|
262470
|
+
root: config.root,
|
|
262471
|
+
volumesRoot: `${config.root}/volumes`
|
|
262472
|
+
};
|
|
262412
262473
|
return http3.createServer(async (req, res) => {
|
|
262413
262474
|
const method = req.method ?? "GET";
|
|
262414
262475
|
const url = new URL2(
|
|
@@ -262420,6 +262481,29 @@ function createDaemon(config) {
|
|
|
262420
262481
|
const body2 = JSON.parse(await readBody(req) || "{}");
|
|
262421
262482
|
return sandagentRun(body2, res, env2);
|
|
262422
262483
|
}
|
|
262484
|
+
if (method === "POST" && pathname === "/api/fs/upload") {
|
|
262485
|
+
try {
|
|
262486
|
+
const ct2 = req.headers["content-type"] ?? "";
|
|
262487
|
+
if (!ct2.includes("multipart/form-data")) {
|
|
262488
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
262489
|
+
res.end(
|
|
262490
|
+
JSON.stringify(fail("content-type must be multipart/form-data"))
|
|
262491
|
+
);
|
|
262492
|
+
return;
|
|
262493
|
+
}
|
|
262494
|
+
const raw = await readBodyRaw(req);
|
|
262495
|
+
const parts = parseMultipart(ct2, raw);
|
|
262496
|
+
const result2 = await fsUpload(state, parts);
|
|
262497
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
262498
|
+
res.end(JSON.stringify(result2));
|
|
262499
|
+
} catch (err) {
|
|
262500
|
+
const status2 = err instanceof AppError ? err.status : 500;
|
|
262501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
262502
|
+
res.writeHead(status2, { "Content-Type": "application/json" });
|
|
262503
|
+
res.end(JSON.stringify(fail(msg)));
|
|
262504
|
+
}
|
|
262505
|
+
return;
|
|
262506
|
+
}
|
|
262423
262507
|
const params = method === "GET" ? Object.fromEntries(url.searchParams) : JSON.parse(await readBody(req) || "{}");
|
|
262424
262508
|
const result = await router.handle(method, pathname, params);
|
|
262425
262509
|
const status = result?.status ?? 404;
|
|
@@ -262436,6 +262520,14 @@ function readBody(req) {
|
|
|
262436
262520
|
req.on("error", reject);
|
|
262437
262521
|
});
|
|
262438
262522
|
}
|
|
262523
|
+
function readBodyRaw(req) {
|
|
262524
|
+
return new Promise((resolve14, reject) => {
|
|
262525
|
+
const chunks = [];
|
|
262526
|
+
req.on("data", (c) => chunks.push(c));
|
|
262527
|
+
req.on("end", () => resolve14(Buffer.concat(chunks)));
|
|
262528
|
+
req.on("error", reject);
|
|
262529
|
+
});
|
|
262530
|
+
}
|
|
262439
262531
|
export {
|
|
262440
262532
|
DaemonRouter,
|
|
262441
262533
|
createDaemon
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal multipart/form-data parser — zero external dependencies.
|
|
3
|
+
* Parses raw body buffer using the boundary from Content-Type header.
|
|
4
|
+
*/
|
|
5
|
+
export interface ParsedMultipart {
|
|
6
|
+
fields: Record<string, string>;
|
|
7
|
+
files: Array<{
|
|
8
|
+
filename: string;
|
|
9
|
+
data: Buffer;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseMultipart(contentType: string, body: Buffer): ParsedMultipart;
|
|
13
|
+
//# sourceMappingURL=multipart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../src/multipart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GACX,eAAe,CAiDjB"}
|
package/dist/nextjs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../src/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../src/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,IASzD,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAkD/C"}
|
package/dist/nextjs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire as __banner_cjsRequire } from "module"; const require = __banner_cjsRequire(import.meta.url);
|
|
1
2
|
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -206414,6 +206415,42 @@ var require_lib4 = __commonJS({
|
|
|
206414
206415
|
}
|
|
206415
206416
|
});
|
|
206416
206417
|
|
|
206418
|
+
// src/multipart.ts
|
|
206419
|
+
function parseMultipart(contentType, body) {
|
|
206420
|
+
const match2 = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
|
|
206421
|
+
if (!match2) throw new Error("missing multipart boundary");
|
|
206422
|
+
const boundary = match2[1] ?? match2[2];
|
|
206423
|
+
const delimiter2 = Buffer.from(`--${boundary}`);
|
|
206424
|
+
const fields = {};
|
|
206425
|
+
const files = [];
|
|
206426
|
+
let start = body.indexOf(delimiter2) + delimiter2.length;
|
|
206427
|
+
while (start < body.length) {
|
|
206428
|
+
const end = body.indexOf(delimiter2, start);
|
|
206429
|
+
if (end === -1) break;
|
|
206430
|
+
const part = body.subarray(start, end);
|
|
206431
|
+
const partStart = part[0] === 13 && part[1] === 10 ? 2 : 0;
|
|
206432
|
+
const headerEnd = part.indexOf("\r\n\r\n", partStart);
|
|
206433
|
+
if (headerEnd === -1) {
|
|
206434
|
+
start = end + delimiter2.length;
|
|
206435
|
+
continue;
|
|
206436
|
+
}
|
|
206437
|
+
const headerStr = part.subarray(partStart, headerEnd).toString("utf-8");
|
|
206438
|
+
let data = part.subarray(headerEnd + 4);
|
|
206439
|
+
if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
|
|
206440
|
+
data = data.subarray(0, data.length - 2);
|
|
206441
|
+
}
|
|
206442
|
+
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
|
206443
|
+
const filenameMatch = headerStr.match(/filename="([^"]+)"/);
|
|
206444
|
+
if (filenameMatch && nameMatch) {
|
|
206445
|
+
files.push({ filename: filenameMatch[1], data: Buffer.from(data) });
|
|
206446
|
+
} else if (nameMatch) {
|
|
206447
|
+
fields[nameMatch[1]] = data.toString("utf-8");
|
|
206448
|
+
}
|
|
206449
|
+
start = end + delimiter2.length;
|
|
206450
|
+
}
|
|
206451
|
+
return { fields, files };
|
|
206452
|
+
}
|
|
206453
|
+
|
|
206417
206454
|
// src/routes/fs.ts
|
|
206418
206455
|
import * as fs2 from "node:fs/promises";
|
|
206419
206456
|
import * as path2 from "node:path";
|
|
@@ -206591,6 +206628,26 @@ async function fsCopy(state, body) {
|
|
|
206591
206628
|
await fs2.copyFile(from, to);
|
|
206592
206629
|
return ok({ path: to });
|
|
206593
206630
|
}
|
|
206631
|
+
async function fsUpload(state, parts) {
|
|
206632
|
+
const volume = parts.fields.volume;
|
|
206633
|
+
const targetDir = parts.fields.path ?? ".";
|
|
206634
|
+
const createDirs = parts.fields.create_dirs !== "false";
|
|
206635
|
+
const root = resolveVolumeRoot(state, volume);
|
|
206636
|
+
const dir = resolveUnderRoot(root, targetDir);
|
|
206637
|
+
if (createDirs) await ensureDir(dir);
|
|
206638
|
+
const results = [];
|
|
206639
|
+
for (const file of parts.files) {
|
|
206640
|
+
const target = resolveUnderRoot(root, path2.join(targetDir, file.filename));
|
|
206641
|
+
await fs2.writeFile(target, file.data);
|
|
206642
|
+
results.push({
|
|
206643
|
+
fieldname: "file",
|
|
206644
|
+
filename: file.filename,
|
|
206645
|
+
path: target,
|
|
206646
|
+
size: file.data.length
|
|
206647
|
+
});
|
|
206648
|
+
}
|
|
206649
|
+
return ok({ files: results });
|
|
206650
|
+
}
|
|
206594
206651
|
|
|
206595
206652
|
// src/routes/git.ts
|
|
206596
206653
|
import { execFile } from "node:child_process";
|
|
@@ -262415,6 +262472,10 @@ function createNextHandler(opts) {
|
|
|
262415
262472
|
const router = new DaemonRouter({ root: opts.root });
|
|
262416
262473
|
const env2 = process.env;
|
|
262417
262474
|
const prefix = opts.prefix ?? "/api/daemon";
|
|
262475
|
+
const state = {
|
|
262476
|
+
root: opts.root,
|
|
262477
|
+
volumesRoot: `${opts.root}/volumes`
|
|
262478
|
+
};
|
|
262418
262479
|
return async (req) => {
|
|
262419
262480
|
const url = new URL(req.url);
|
|
262420
262481
|
const pathname = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
|
|
@@ -262423,6 +262484,25 @@ function createNextHandler(opts) {
|
|
|
262423
262484
|
const body = await req.json().catch(() => ({}));
|
|
262424
262485
|
return codingRunStream(body, env2);
|
|
262425
262486
|
}
|
|
262487
|
+
if (method === "POST" && pathname === "/api/fs/upload") {
|
|
262488
|
+
try {
|
|
262489
|
+
const ct2 = req.headers.get("content-type") ?? "";
|
|
262490
|
+
if (!ct2.includes("multipart/form-data")) {
|
|
262491
|
+
return Response.json(
|
|
262492
|
+
fail("content-type must be multipart/form-data"),
|
|
262493
|
+
{ status: 400 }
|
|
262494
|
+
);
|
|
262495
|
+
}
|
|
262496
|
+
const raw = Buffer.from(await req.arrayBuffer());
|
|
262497
|
+
const parts = parseMultipart(ct2, raw);
|
|
262498
|
+
const result2 = await fsUpload(state, parts);
|
|
262499
|
+
return Response.json(result2);
|
|
262500
|
+
} catch (err) {
|
|
262501
|
+
const status = err instanceof AppError ? err.status : 500;
|
|
262502
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
262503
|
+
return Response.json(fail(msg), { status });
|
|
262504
|
+
}
|
|
262505
|
+
}
|
|
262426
262506
|
const params = method === "GET" ? Object.fromEntries(url.searchParams) : await req.json().catch(() => ({}));
|
|
262427
262507
|
const result = await router.handle(method, pathname, params);
|
|
262428
262508
|
if (!result) {
|
package/dist/routes/fs.d.ts
CHANGED
|
@@ -79,5 +79,27 @@ export declare function fsMove(state: AppState, body: MoveCopyBody): Promise<imp
|
|
|
79
79
|
export declare function fsCopy(state: AppState, body: MoveCopyBody): Promise<import("../utils.js").ApiEnvelope<{
|
|
80
80
|
path: string;
|
|
81
81
|
}>>;
|
|
82
|
+
export interface UploadedFile {
|
|
83
|
+
fieldname: string;
|
|
84
|
+
filename: string;
|
|
85
|
+
path: string;
|
|
86
|
+
size: number;
|
|
87
|
+
}
|
|
88
|
+
export interface UploadResult {
|
|
89
|
+
files: UploadedFile[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Handle multipart/form-data file upload.
|
|
93
|
+
* Expects form field "path" for target directory and one or more "file" parts.
|
|
94
|
+
*/
|
|
95
|
+
export declare function fsUpload(state: AppState, parts: {
|
|
96
|
+
fields: Record<string, string>;
|
|
97
|
+
files: Array<{
|
|
98
|
+
filename: string;
|
|
99
|
+
data: Buffer;
|
|
100
|
+
}>;
|
|
101
|
+
}): Promise<import("../utils.js").ApiEnvelope<{
|
|
102
|
+
files: UploadedFile[];
|
|
103
|
+
}>>;
|
|
82
104
|
export {};
|
|
83
105
|
//# sourceMappingURL=fs.d.ts.map
|
package/dist/routes/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAU5C,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,UAAU;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAID,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;;;MAkBzD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;IAKzD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;;IASzD;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;IAQ3D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;UAMhD,MAAM;UACN,MAAM;YACJ,OAAO;UACT,MAAM;MA4Bf;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;;IAQ7D;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;IAQ9D;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;IAK7D;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU;;IAU/D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;;IAS/D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;;IAS/D"}
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAU5C,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,UAAU;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAID,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;;;MAkBzD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;IAKzD;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;;IASzD;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;;;IAQ3D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS;UAMhD,MAAM;UACN,MAAM;YACJ,OAAO;UACT,MAAM;MA4Bf;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;;IAQ7D;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;IAQ9D;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS;;IAK7D;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU;;IAU/D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;;IAS/D;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;;IAS/D;AAID,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE;IACL,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;;IAuBF"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAQlC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC,MAAM,CA2D9D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandagent/daemon",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "SandAgent Daemon - Unified API gateway for sandbox services (file, git, volumes)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "pnpm clean && tsc --emitDeclarationOnly && pnpm bundle",
|
|
45
|
-
"bundle": "
|
|
46
|
-
"dev": "
|
|
45
|
+
"bundle": "node scripts/build.mjs",
|
|
46
|
+
"dev": "pnpm build && node dist/bundle.mjs",
|
|
47
|
+
"dev:watch": "tsc --watch",
|
|
47
48
|
"clean": "rm -rf dist",
|
|
48
49
|
"typecheck": "tsc --noEmit",
|
|
49
50
|
"lint": "echo 'no lint configured'",
|