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 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
@@ -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. */
@@ -1 +1 @@
1
- {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"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"}
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
- const name = path.basename(wanted);
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
- res.setHeader("Content-Type", contentTypeFor(base));
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