teapot-coding-agent 0.15.3 → 0.15.5
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/server/api.js +80 -3
- package/package.json +1 -1
- package/public/assets/{index-xuHe-Iu0.js → index-4i2VuS2k.js} +9 -9
- package/public/assets/index-4i2VuS2k.js.map +1 -0
- package/public/assets/index-mKmKGRRj.css +1 -0
- package/public/assets/{shiki-gYjW0Ev7.js → shiki-C0VENFA4.js} +2 -2
- package/public/assets/{shiki-gYjW0Ev7.js.map → shiki-C0VENFA4.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-BQv6XAjp.css +0 -1
- package/public/assets/index-xuHe-Iu0.js.map +0 -1
package/dist/server/api.js
CHANGED
|
@@ -45,6 +45,43 @@ async function listModels(baseUrl, apiKey) {
|
|
|
45
45
|
import path from "node:path";
|
|
46
46
|
import { bus } from "../bus.js";
|
|
47
47
|
import { readEvents } from "../log/events.js";
|
|
48
|
+
/** media categories the file tree can preview inline */
|
|
49
|
+
const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif"]);
|
|
50
|
+
const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv", "avi", "m4v"]);
|
|
51
|
+
const AUDIO_EXT = new Set(["mp3", "wav", "ogg", "m4a", "flac", "aac", "opus"]);
|
|
52
|
+
function mediaKind(p) {
|
|
53
|
+
const ext = p.split(".").pop()?.toLowerCase() ?? "";
|
|
54
|
+
if (IMAGE_EXT.has(ext))
|
|
55
|
+
return "image";
|
|
56
|
+
if (VIDEO_EXT.has(ext))
|
|
57
|
+
return "video";
|
|
58
|
+
if (AUDIO_EXT.has(ext))
|
|
59
|
+
return "audio";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** extension → MIME (used for the media preview endpoint) */
|
|
63
|
+
export const MEDIA_MIME = (() => {
|
|
64
|
+
const m = {};
|
|
65
|
+
for (const e of IMAGE_EXT)
|
|
66
|
+
m[e] = `image/${e === "jpg" ? "jpeg" : e}`;
|
|
67
|
+
Object.assign(m, { svg: "image/svg+xml", ico: "image/x-icon", avif: "image/avif" });
|
|
68
|
+
for (const e of VIDEO_EXT)
|
|
69
|
+
m[e] = `video/${e === "mov" ? "quicktime" : e === "mkv" ? "x-matroska" : e}`;
|
|
70
|
+
for (const e of AUDIO_EXT)
|
|
71
|
+
m[e] = `audio/${e}`;
|
|
72
|
+
Object.assign(m, { m4a: "audio/mp4", mp3: "audio/mpeg" });
|
|
73
|
+
return m;
|
|
74
|
+
})();
|
|
75
|
+
/** category → generic fallback when no specific extension match exists */
|
|
76
|
+
const KIND_MIME = {
|
|
77
|
+
image: "application/octet-stream",
|
|
78
|
+
video: "application/octet-stream",
|
|
79
|
+
audio: "application/octet-stream",
|
|
80
|
+
};
|
|
81
|
+
function mimeFor(rel, kind) {
|
|
82
|
+
const ext = rel.split(".").pop()?.toLowerCase() ?? "";
|
|
83
|
+
return MEDIA_MIME[ext] ?? KIND_MIME[kind];
|
|
84
|
+
}
|
|
48
85
|
export function buildApp(master) {
|
|
49
86
|
const app = new Hono();
|
|
50
87
|
// Optional bearer auth for LAN exposure — TEAPOT_API_TOKEN env wins, else
|
|
@@ -140,8 +177,9 @@ export function buildApp(master) {
|
|
|
140
177
|
}
|
|
141
178
|
};
|
|
142
179
|
const cur = termCounts.get(agentId) ?? 0;
|
|
143
|
-
if (cur >=
|
|
144
|
-
|
|
180
|
+
if (cur >= 10) {
|
|
181
|
+
// soft safeguard only — the UI allows up to 10 tabs per agent
|
|
182
|
+
send({ kind: "exit", error: "too many terminals for this agent (max 10)" });
|
|
145
183
|
return;
|
|
146
184
|
}
|
|
147
185
|
termCounts.set(agentId, cur + 1);
|
|
@@ -328,6 +366,44 @@ export function buildApp(master) {
|
|
|
328
366
|
path: rel,
|
|
329
367
|
truncated: buf.length > 100_000,
|
|
330
368
|
content: buf.subarray(0, 100_000).toString("utf8"),
|
|
369
|
+
media: mediaKind(rel),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
return c.json({ error: err.message }, 400);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// raw bytes for media previews (images / video / audio) — auth-gated like
|
|
377
|
+
// every other agent route, workspace-confined, served with the right MIME
|
|
378
|
+
app.get("/api/agents/:id/raw", async (c) => {
|
|
379
|
+
const a = master.agents.get(c.req.param("id"));
|
|
380
|
+
if (!a)
|
|
381
|
+
return c.json({ error: "not found" }, 404);
|
|
382
|
+
const rel = c.req.query("path") ?? "";
|
|
383
|
+
if (!rel.trim())
|
|
384
|
+
return c.json({ error: "path required" }, 400);
|
|
385
|
+
let abs;
|
|
386
|
+
try {
|
|
387
|
+
abs = safeJoin(a.workspace, rel);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
return c.json({ error: err.message }, 400);
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const buf = await fs.readFile(abs);
|
|
394
|
+
const kind = mediaKind(rel);
|
|
395
|
+
if (!kind)
|
|
396
|
+
return c.json({ error: "not a previewable media file" }, 400);
|
|
397
|
+
if (buf.length > 200 * 1024 * 1024)
|
|
398
|
+
return c.json({ error: "file too large to preview (>200MB)" }, 413);
|
|
399
|
+
// hono-native body API — a raw `new Response(...)` returned here lost
|
|
400
|
+
// its headers somewhere in the middleware chain (content-type arrived
|
|
401
|
+
// as undefined), while c.body() passes them through correctly
|
|
402
|
+
return c.body(new Uint8Array(buf), 200, {
|
|
403
|
+
"content-type": mimeFor(rel, kind),
|
|
404
|
+
// previews only; the browser shouldn't cache stale workspace files
|
|
405
|
+
"cache-control": "no-store",
|
|
406
|
+
"content-disposition": `inline; filename="${encodeURIComponent(path.basename(rel))}"`,
|
|
331
407
|
});
|
|
332
408
|
}
|
|
333
409
|
catch (err) {
|
|
@@ -781,7 +857,8 @@ export function buildApp(master) {
|
|
|
781
857
|
const a = master.agents.get(c.req.param("id"));
|
|
782
858
|
if (!a)
|
|
783
859
|
return c.json({ error: "not found" }, 404);
|
|
784
|
-
|
|
860
|
+
// cached like /events — this used to bypass it and re-read the whole log
|
|
861
|
+
const events = await readEventsCached(a.log.filePath);
|
|
785
862
|
const branches = new Map();
|
|
786
863
|
for (const e of events) {
|
|
787
864
|
const b = branches.get(e.branch) ?? { branch: e.branch, events: 0 };
|