shadok-ai 0.16.27 → 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
@@ -228,7 +228,9 @@ Enter to skip; you can add it later from the web UI).
228
228
  remove it. On send, an image shows **inline in your own message bubble**;
229
229
  under the hood the agent still receives it as an `[Attached …: path]` line,
230
230
  the same format Telegram uses, so nothing about how the agent reads it
231
- changes. A message can be attachments alone, with no words. The files are
231
+ changes. If the agent is mirrored into Telegram, the file is **posted into its
232
+ topic too** — a photo as a photo, anything else as a document — so whoever is
233
+ reading there sees what you sent instead of a path. A message can be attachments alone, with no words. The files are
232
234
  served back to the browser by `GET /media/<name>` from the same directory
233
235
  Telegram attachments land in.
234
236
  - **Files an agent hands you** — when an agent's result IS a file (a report, an
@@ -240,6 +242,13 @@ Enter to skip; you can add it later from the web UI).
240
242
  otherwise just print a local path you cannot open. **Artifacts are denied**
241
243
  cockpit-wide for the same reason — a page on claude.ai is not the file you
242
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.
243
252
  - **Compaction summaries, folded away** — when Claude Code runs out of context
244
253
  it compacts the conversation and writes a long "This session is being
245
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