Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes

- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas,
  login rate limiting, audit log and backups
- Ghidra headless decompilation (ARM64 natives built from source), binary_info tool,
  clean room / deep workflow per session
- Recreate skill and commands for analysing and re-building old software
- File checkpoints with rewind/restore, background shells (bash_output, kill_shell)
- web_search tool (DuckDuckGo, SearXNG, Brave, Tavily)
- In-app dialogs and toasts instead of browser dialogs
- 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One,
  Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 19:12:30 +01:00
1 parent fab9add5d5
commit 5b569b6835
108 files changed
+6785 -279

No files matched your search

+100 -3
View File
@@ -1,18 +1,115 @@
import { memo } from "react";
import { memo, useState, type ReactNode } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
export const Markdown = memo(function Markdown({ text }: { text: string }) {
export const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)$/i;
/** URL for a file in the project (absolute or relative to baseDir). */
export function projectFileUrl(src: string, baseDir?: string): string | undefined {
if (/^(https?:|data:|blob:)/i.test(src)) return src;
if (!baseDir && !src.startsWith("/")) return undefined;
const parts = (src.startsWith("/") ? src : `${baseDir}/${src}`).split("/");
const out: string[] = [];
for (const p of parts) {
if (p === "..") out.pop();
else if (p && p !== ".") out.push(p);
}
return `/api/files/raw?path=${encodeURIComponent("/" + out.join("/"))}`;
}
function textOf(node: ReactNode): string {
if (typeof node === "string") return node;
if (Array.isArray(node)) return node.map(textOf).join("");
if (node && typeof node === "object" && "props" in node) return textOf((node as any).props.children);
return "";
}
/** SVG code shown as an image (img context: scripts never run), code on demand. */
export function SvgBlock({ code, children }: { code: string; children?: ReactNode }) {
const [showCode, setShowCode] = useState(false);
const url = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(code)))}`;
return (
<div className="svg-block">
<img src={url} alt="SVG" />
<button className="small ghost" onClick={() => setShowCode(!showCode)}>
{showCode ? "▾ SVG" : "▸ SVG"}
</button>
{showCode && children}
</div>
);
}
export const Markdown = memo(function Markdown({ text, baseDir }: { text: string; baseDir?: string }) {
return (
<div className="markdown">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
components={{ a: (props) => <a {...props} target="_blank" rel="noreferrer" /> }}
components={{
a: (props) => <a {...props} target="_blank" rel="noreferrer" />,
img: ({ src, alt }) => {
const url = typeof src === "string" ? projectFileUrl(src, baseDir) : undefined;
if (!url) return <span>{alt}</span>;
// Non-image files (3D models, documents) become links.
if (typeof src === "string" && !/^(data:|https?:)/i.test(src) && !IMAGE_EXT.test(src)) {
return (
<a href={url} target="_blank" rel="noreferrer">
📎 {alt || src}
</a>
);
}
return <img className="inline-image" src={url} alt={alt ?? ""} loading="lazy" />;
},
pre: ({ children }) => {
const code = textOf(children);
const child: any = Array.isArray(children) ? children[0] : children;
const cls = String(child?.props?.className ?? "");
if (/language-svg/.test(cls) || (/language-(xml|html)/.test(cls) && code.trim().startsWith("<svg"))) {
return (
<SvgBlock code={code}>
<pre>{children}</pre>
</SvgBlock>
);
}
return <pre>{children}</pre>;
},
}}
>
{text}
</ReactMarkdown>
</div>
);
});
/** Image files mentioned in a text (paths in backticks or plain). */
export function imagePathsIn(text: string): string[] {
const found = new Set<string>();
const re = /(?:^|[\s`(\["'])((?:\.{0,2}\/)?[\w\-./]+\.(?:png|jpe?g|gif|webp|bmp|svg))(?=$|[\s`)\]"',.:;!?])/gim;
for (const m of text.matchAll(re)) {
// Skip URLs and markdown images (rendered anyway).
const idx = m.index ?? 0;
const before = text.slice(Math.max(0, idx - 2), idx + 1);
if (m[1].includes("://") || before.includes("](") || text.slice(Math.max(0, idx - 10), idx + 1).includes("http")) continue;
found.add(m[1]);
}
return [...found].slice(0, 8);
}
/** Thumbnails for image files mentioned in a message; missing files hide themselves. */
export function ImageRefs({ text, baseDir }: { text: string; baseDir?: string }) {
const paths = imagePathsIn(text.replace(/!\[[^\]]*\]\([^)]*\)/g, ""));
if (paths.length === 0 || !baseDir) return null;
return (
<div className="image-refs">
{paths.map((p) => {
const url = projectFileUrl(p, baseDir)!;
return (
<a key={p} href={url} target="_blank" rel="noreferrer" title={p}>
<img src={url} alt={p} loading="lazy" onError={(e) => ((e.currentTarget.parentElement as HTMLElement).style.display = "none")} />
</a>
);
})}
</div>
);
}