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

+153
View File
@@ -0,0 +1,153 @@
// 3D preview for GLB/GLTF, STL, OBJ and STEP/IGES (via OpenCascade WASM).
// Loaded on demand only when such a file is opened.
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
import occtScriptUrl from "occt-import-js/dist/occt-import-js.js?url";
import occtWasmUrl from "occt-import-js/dist/occt-import-js.wasm?url";
let occtPromise: Promise<any> | undefined;
/** Load the OpenCascade importer once (script tag + wasm URL). */
function loadOcct(): Promise<any> {
occtPromise ??= new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = occtScriptUrl;
s.onload = () => (window as any).occtimportjs({ locateFile: () => occtWasmUrl }).then(resolve, reject);
s.onerror = () => reject(new Error("could not load the STEP importer"));
document.head.appendChild(s);
});
return occtPromise;
}
function cssColor(name: string, fallback: string) {
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return v || fallback;
}
async function loadObject(url: string, ext: string): Promise<THREE.Object3D> {
const material = new THREE.MeshStandardMaterial({ color: cssColor("--blue", "#83a598"), metalness: 0.1, roughness: 0.6, side: THREE.DoubleSide });
switch (ext) {
case "glb":
case "gltf":
return (await new GLTFLoader().loadAsync(url)).scene;
case "stl": {
const geo = await new STLLoader().loadAsync(url);
geo.computeVertexNormals();
return new THREE.Mesh(geo, material);
}
case "obj": {
const obj = await new OBJLoader().loadAsync(url);
obj.traverse((c) => c instanceof THREE.Mesh && (c.material = material));
return obj;
}
default: {
// STEP / IGES: tessellate with OpenCascade in the browser.
const [occt, buf] = await Promise.all([loadOcct(), fetch(url, { credentials: "same-origin" }).then((r) => r.arrayBuffer())]);
const data = new Uint8Array(buf);
const res = ext === "igs" || ext === "iges" ? occt.ReadIgesFile(data, null) : occt.ReadStepFile(data, null);
if (!res.success) throw new Error("the file could not be read");
const group = new THREE.Group();
for (const m of res.meshes) {
const geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.Float32BufferAttribute(m.attributes.position.array, 3));
if (m.attributes.normal) geo.setAttribute("normal", new THREE.Float32BufferAttribute(m.attributes.normal.array, 3));
else geo.computeVertexNormals();
if (m.index) geo.setIndex(new THREE.Uint32BufferAttribute(m.index.array, 1));
const mat = m.color ? material.clone() : material;
if (m.color) (mat as THREE.MeshStandardMaterial).color = new THREE.Color(m.color[0], m.color[1], m.color[2]);
group.add(new THREE.Mesh(geo, mat));
}
return group;
}
}
}
export function ModelViewer({ url, ext }: { url: string; ext: string }) {
const host = useRef<HTMLDivElement>(null);
const [state, setState] = useState<{ loading: boolean; error?: string; info?: string }>({ loading: true });
useEffect(() => {
const el = host.current!;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
el.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(cssColor("--bg0", "#282828"));
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.6));
const sun = new THREE.DirectionalLight(0xffffff, 1.8);
sun.position.set(3, 5, 4);
scene.add(sun);
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 1e6);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const resize = () => {
const w = el.clientWidth || 300;
const h = el.clientHeight || 300;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
};
const ro = new ResizeObserver(resize);
ro.observe(el);
resize();
let alive = true;
let frame = 0;
const loop = () => {
if (!alive) return;
controls.update();
renderer.render(scene, camera);
frame = requestAnimationFrame(loop);
};
loop();
loadObject(url, ext)
.then((obj) => {
if (!alive) return;
scene.add(obj);
// Frame the model.
const box = new THREE.Box3().setFromObject(obj);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
const radius = Math.max(size.length() / 2, 1e-3);
camera.position.copy(center).add(new THREE.Vector3(radius * 1.4, radius * 1.0, radius * 1.8));
camera.near = radius / 100;
camera.far = radius * 100;
camera.updateProjectionMatrix();
controls.target.copy(center);
let tris = 0;
obj.traverse((c) => {
if (c instanceof THREE.Mesh) tris += (c.geometry.index ? c.geometry.index.count : c.geometry.attributes.position.count) / 3;
});
setState({ loading: false, info: `${Math.round(tris).toLocaleString()} triangles · ${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}` });
})
.catch((e) => alive && setState({ loading: false, error: String(e?.message ?? e) }));
return () => {
alive = false;
cancelAnimationFrame(frame);
ro.disconnect();
controls.dispose();
scene.traverse((c) => {
if (c instanceof THREE.Mesh) {
c.geometry.dispose();
(Array.isArray(c.material) ? c.material : [c.material]).forEach((m) => m.dispose());
}
});
renderer.dispose();
el.removeChild(renderer.domElement);
};
}, [url, ext]);
return (
<div className="model-viewer">
<div ref={host} className="model-canvas" />
<div className="model-info">{state.loading ? "…" : state.error ? `⚠ ${state.error}` : state.info}</div>
</div>
);
}