shadok-ai 0.16.28 → 0.16.29
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 +7 -0
- package/dist/download.d.ts +18 -0
- package/dist/download.js +48 -1
- package/dist/download.js.map +1 -1
- package/dist/server.js +35 -20
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +116 -36
package/README.md
CHANGED
|
@@ -242,6 +242,13 @@ Enter to skip; you can add it later from the web UI).
|
|
|
242
242
|
otherwise just print a local path you cannot open. **Artifacts are denied**
|
|
243
243
|
cockpit-wide for the same reason — a page on claude.ai is not the file you
|
|
244
244
|
asked for (`SHADOK_ALLOW_ARTIFACTS=1` lifts it).
|
|
245
|
+
- **Photos and videos, viewable** — an image or a **video** (mp4/webm/mov…) in
|
|
246
|
+
the chat renders inline (a video gets a real player), and clicking a photo
|
|
247
|
+
opens it full-size in a lightbox. The 🖼 button in an agent's bar opens a
|
|
248
|
+
**media gallery**: a grid of every photo and video in that conversation —
|
|
249
|
+
click a thumbnail to view or play it full-size, then back to the grid. Videos
|
|
250
|
+
are served with HTTP range requests so they seek and stream instead of
|
|
251
|
+
downloading whole.
|
|
245
252
|
- **Compaction summaries, folded away** — when Claude Code runs out of context
|
|
246
253
|
it compacts the conversation and writes a long "This session is being
|
|
247
254
|
continued…" summary as the first message of what follows. Shown in full it
|
package/dist/download.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare const SEND_FILE_TOOL = "SendUserFile";
|
|
3
3
|
/** Would the web render this inline as an <img>? */
|
|
4
4
|
export declare function isImageFile(name: string): boolean;
|
|
5
|
+
/** Would the web render this inline as a <video>? */
|
|
6
|
+
export declare function isVideoFile(name: string): boolean;
|
|
5
7
|
/** Should Telegram send this as a photo (inline) rather than a document? */
|
|
6
8
|
export declare function telegramPhotoable(name: string): boolean;
|
|
7
9
|
/** Best-effort Content-Type for GET /download. `image/svg+xml` is served with a
|
|
@@ -13,8 +15,24 @@ export interface FileCard {
|
|
|
13
15
|
path: string;
|
|
14
16
|
name: string;
|
|
15
17
|
image: boolean;
|
|
18
|
+
video: boolean;
|
|
16
19
|
}
|
|
17
20
|
export declare function fileCard(p: string): FileCard;
|
|
21
|
+
/**
|
|
22
|
+
* Pure: parse an HTTP `Range` header against a known file size, for the single
|
|
23
|
+
* `bytes=start-end` form a <video> issues (browsers never send multipart ranges
|
|
24
|
+
* to a plain file server, so one range is all we honour).
|
|
25
|
+
*
|
|
26
|
+
* Returns the inclusive byte slice, `null` when there is no range to honour (no
|
|
27
|
+
* header, a non-bytes unit, a syntactically broken value → serve the whole file,
|
|
28
|
+
* a 200), or `"unsatisfiable"` when the range is well-formed but out of bounds
|
|
29
|
+
* (→ the caller answers 416). `bytes=N-` is "from N to the end"; `bytes=-N` is
|
|
30
|
+
* "the last N bytes".
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseByteRange(header: string | undefined, size: number): {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
} | null | "unsatisfiable";
|
|
18
36
|
/** The `files` a SendUserFile tool_use delivered — its `input.files`, kept only
|
|
19
37
|
* when they are real string paths. Any other tool → none. */
|
|
20
38
|
export declare function toolFiles(name: unknown, input: unknown): string[];
|
package/dist/download.js
CHANGED
|
@@ -9,6 +9,10 @@ import path from "node:path";
|
|
|
9
9
|
/** The tool that delivers a file to the user. */
|
|
10
10
|
export const SEND_FILE_TOOL = "SendUserFile";
|
|
11
11
|
const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif", ".bmp", ".ico"]);
|
|
12
|
+
// What the web renders inline as a <video>. Kept to the formats a browser can
|
|
13
|
+
// actually play (mp4/webm/ogg, plus mov which Safari plays and Chrome often
|
|
14
|
+
// does too); an unplayable container still downloads via the octet-stream path.
|
|
15
|
+
const VIDEO_EXT = new Set([".mp4", ".m4v", ".webm", ".ogv", ".mov"]);
|
|
12
16
|
// What Telegram will accept as a PHOTO (inline preview). Narrower than the web's
|
|
13
17
|
// <img>: svg/gif/bmp/ico are not photos there, so they go as documents.
|
|
14
18
|
const TG_PHOTO_EXT = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
@@ -18,12 +22,18 @@ const CONTENT_TYPE = {
|
|
|
18
22
|
".ico": "image/x-icon", ".pdf": "application/pdf", ".html": "text/html", ".htm": "text/html",
|
|
19
23
|
".css": "text/css", ".js": "text/javascript", ".json": "application/json", ".csv": "text/csv",
|
|
20
24
|
".txt": "text/plain", ".md": "text/markdown", ".xml": "application/xml", ".zip": "application/zip",
|
|
25
|
+
".mp4": "video/mp4", ".m4v": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg",
|
|
26
|
+
".mov": "video/quicktime",
|
|
21
27
|
};
|
|
22
28
|
const ext = (name) => path.extname(String(name || "")).toLowerCase();
|
|
23
29
|
/** Would the web render this inline as an <img>? */
|
|
24
30
|
export function isImageFile(name) {
|
|
25
31
|
return IMAGE_EXT.has(ext(name));
|
|
26
32
|
}
|
|
33
|
+
/** Would the web render this inline as a <video>? */
|
|
34
|
+
export function isVideoFile(name) {
|
|
35
|
+
return VIDEO_EXT.has(ext(name));
|
|
36
|
+
}
|
|
27
37
|
/** Should Telegram send this as a photo (inline) rather than a document? */
|
|
28
38
|
export function telegramPhotoable(name) {
|
|
29
39
|
return TG_PHOTO_EXT.has(ext(name));
|
|
@@ -35,7 +45,44 @@ export function contentTypeFor(name) {
|
|
|
35
45
|
}
|
|
36
46
|
export function fileCard(p) {
|
|
37
47
|
const name = path.basename(String(p || ""));
|
|
38
|
-
return { path: String(p || ""), name, image: isImageFile(name) };
|
|
48
|
+
return { path: String(p || ""), name, image: isImageFile(name), video: isVideoFile(name) };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Pure: parse an HTTP `Range` header against a known file size, for the single
|
|
52
|
+
* `bytes=start-end` form a <video> issues (browsers never send multipart ranges
|
|
53
|
+
* to a plain file server, so one range is all we honour).
|
|
54
|
+
*
|
|
55
|
+
* Returns the inclusive byte slice, `null` when there is no range to honour (no
|
|
56
|
+
* header, a non-bytes unit, a syntactically broken value → serve the whole file,
|
|
57
|
+
* a 200), or `"unsatisfiable"` when the range is well-formed but out of bounds
|
|
58
|
+
* (→ the caller answers 416). `bytes=N-` is "from N to the end"; `bytes=-N` is
|
|
59
|
+
* "the last N bytes".
|
|
60
|
+
*/
|
|
61
|
+
export function parseByteRange(header, size) {
|
|
62
|
+
if (!header || size <= 0)
|
|
63
|
+
return null;
|
|
64
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
65
|
+
if (!m)
|
|
66
|
+
return null;
|
|
67
|
+
const [, rawStart, rawEnd] = m;
|
|
68
|
+
if (rawStart === "" && rawEnd === "")
|
|
69
|
+
return null;
|
|
70
|
+
let start, end;
|
|
71
|
+
if (rawStart === "") {
|
|
72
|
+
// Suffix: the last N bytes. N === 0 is unsatisfiable (no "last zero bytes").
|
|
73
|
+
const n = Number(rawEnd);
|
|
74
|
+
if (n === 0)
|
|
75
|
+
return "unsatisfiable";
|
|
76
|
+
start = Math.max(0, size - n);
|
|
77
|
+
end = size - 1;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
start = Number(rawStart);
|
|
81
|
+
end = rawEnd === "" ? size - 1 : Math.min(Number(rawEnd), size - 1);
|
|
82
|
+
}
|
|
83
|
+
if (start > end || start >= size)
|
|
84
|
+
return "unsatisfiable";
|
|
85
|
+
return { start, end };
|
|
39
86
|
}
|
|
40
87
|
/** The `files` a SendUserFile tool_use delivered — its `input.files`, kept only
|
|
41
88
|
* when they are real string paths. Any other tool → none. */
|
package/dist/download.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,uEAAuE;AACvE,gFAAgF;AAChF,kFAAkF;AAClF,EAAE;AACF,iFAAiF;AAEjF,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,iDAAiD;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,cAAc,CAAC;AAE7C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AACvG,iFAAiF;AACjF,wEAAwE;AACxE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjE,MAAM,YAAY,GAA2B;IAC3C,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW;IACrF,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW;IAC1F,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW;IAC5F,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,UAAU;IAC7F,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB;
|
|
1
|
+
{"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import express from "express";
|
|
12
12
|
import { WebSocketServer, WebSocket } from "ws";
|
|
13
13
|
import { detectDialog, dialogKey, isResumeSummaryDialog, findSessionId, lastPromptAt, listSessions, loadHistory, resumedTurnStart, transcriptFilePath, } from "./extract.js";
|
|
14
|
-
import { fileCard, sentFilePaths, contentTypeFor, isImageFile } from "./download.js";
|
|
14
|
+
import { fileCard, sentFilePaths, contentTypeFor, isImageFile, isVideoFile, parseByteRange } from "./download.js";
|
|
15
15
|
import { OFFER_TOOL, filesFileFor, loadOffers, saveOffers, offerVerdict, withOffer, offeredPaths, refusalMessage, statForOffer, } from "./files.js";
|
|
16
16
|
// Same implementation as the web client's preview (plain JS, loaded as is by
|
|
17
17
|
// the browser): one source for reading the in-flight text off the screen.
|
|
@@ -3030,6 +3030,38 @@ function historyWithOffers(turns, sessionId) {
|
|
|
3030
3030
|
out.push(fileTurns[oi++]);
|
|
3031
3031
|
return out;
|
|
3032
3032
|
}
|
|
3033
|
+
// Stream a vetted file back to the browser, honouring a single `Range` so a
|
|
3034
|
+
// <video> can seek and so Safari plays one at all (it demands a 206 before it
|
|
3035
|
+
// will start). Images/PDF/etc. get the same path — Range is harmless there and
|
|
3036
|
+
// helps a resumed download. A raster image or a video is served `inline` (the
|
|
3037
|
+
// chat's <img>/<video> renders it, and opening the URL shows it); everything
|
|
3038
|
+
// else — including SVG/HTML — downloads, under the same nosniff + sandbox CSP.
|
|
3039
|
+
function serveStaticFile(req, res, filePath, stat, name) {
|
|
3040
|
+
const ct = contentTypeFor(name);
|
|
3041
|
+
const inline = (isImageFile(name) && ct !== "image/svg+xml") || isVideoFile(name);
|
|
3042
|
+
res.setHeader("Content-Type", ct);
|
|
3043
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
3044
|
+
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
3045
|
+
res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename="${name.replace(/["\\]/g, "_")}"`);
|
|
3046
|
+
res.setHeader("Accept-Ranges", "bytes");
|
|
3047
|
+
const range = parseByteRange(req.headers.range, stat.size);
|
|
3048
|
+
if (range === "unsatisfiable") {
|
|
3049
|
+
res.setHeader("Content-Range", `bytes */${stat.size}`);
|
|
3050
|
+
res.status(416).end();
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const onErr = () => { if (!res.headersSent)
|
|
3054
|
+
res.status(500); res.end(); };
|
|
3055
|
+
if (range) {
|
|
3056
|
+
res.status(206);
|
|
3057
|
+
res.setHeader("Content-Range", `bytes ${range.start}-${range.end}/${stat.size}`);
|
|
3058
|
+
res.setHeader("Content-Length", String(range.end - range.start + 1));
|
|
3059
|
+
fs.createReadStream(filePath, { start: range.start, end: range.end }).on("error", onErr).pipe(res);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
res.setHeader("Content-Length", String(stat.size));
|
|
3063
|
+
fs.createReadStream(filePath).on("error", onErr).pipe(res);
|
|
3064
|
+
}
|
|
3033
3065
|
app.get("/download", (req, res) => {
|
|
3034
3066
|
const session = String(req.query.session ?? "");
|
|
3035
3067
|
const wanted = String(req.query.path ?? "");
|
|
@@ -3066,17 +3098,7 @@ app.get("/download", (req, res) => {
|
|
|
3066
3098
|
catch {
|
|
3067
3099
|
return res.status(404).type("text").send("file no longer on disk");
|
|
3068
3100
|
}
|
|
3069
|
-
|
|
3070
|
-
res.setHeader("Content-Type", contentTypeFor(name));
|
|
3071
|
-
res.setHeader("Content-Length", String(stat.size));
|
|
3072
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
3073
|
-
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
3074
|
-
// A raster image is served inline (so opening it shows the image); everything
|
|
3075
|
-
// else — including SVG/HTML — downloads. The <img> in the chat renders either.
|
|
3076
|
-
const inline = isImageFile(name) && contentTypeFor(name) !== "image/svg+xml";
|
|
3077
|
-
res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename="${name.replace(/["\\]/g, "_")}"`);
|
|
3078
|
-
fs.createReadStream(wanted).on("error", () => { if (!res.headersSent)
|
|
3079
|
-
res.status(500); res.end(); }).pipe(res);
|
|
3101
|
+
serveStaticFile(req, res, wanted, stat, path.basename(wanted));
|
|
3080
3102
|
});
|
|
3081
3103
|
// Serve a file the USER dropped / pasted (or a Telegram attachment) — they all
|
|
3082
3104
|
// land in MEDIA_DIR, referenced in the chat as `[Attached image: <path>]`. The
|
|
@@ -3100,14 +3122,7 @@ app.get("/media/:name", (req, res) => {
|
|
|
3100
3122
|
catch {
|
|
3101
3123
|
return res.status(404).type("text").send("no such media");
|
|
3102
3124
|
}
|
|
3103
|
-
|
|
3104
|
-
res.setHeader("Content-Length", String(stat.size));
|
|
3105
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
3106
|
-
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
3107
|
-
const inline = isImageFile(base) && contentTypeFor(base) !== "image/svg+xml";
|
|
3108
|
-
res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename="${base.replace(/["\\]/g, "_")}"`);
|
|
3109
|
-
fs.createReadStream(file).on("error", () => { if (!res.headersSent)
|
|
3110
|
-
res.status(500); res.end(); }).pipe(res);
|
|
3125
|
+
serveStaticFile(req, res, file, stat, base);
|
|
3111
3126
|
});
|
|
3112
3127
|
// Install (or repair) the global Claude Code CLI on demand — `npm i -g
|
|
3113
3128
|
// @anthropic-ai/claude-code`. The cockpit shows this button when an agent's TUI
|