Compare commits

..

3 Commits

Author SHA1 Message Date
dayuan.jiang
6ed05ed24d chore: always bundle latest draw.io version in Electron builds
Remove pinned v29.3.5 tag so the build always clones the latest draw.io.
This adds the Animated GIF export and other new features to the Electron app.

Closes #770
2026-04-06 10:13:40 +09:00
Dayuan Jiang
31819f413c fix: add 10MB body size limit to MCP HTTP endpoints (#791)
All three POST handlers (/api/state, /api/restore, /api/history-svg)
now use a shared readBody() helper that enforces a 10MB limit and
returns 413 if exceeded, preventing memory exhaustion from oversized
requests.

Bumps @next-ai-drawio/mcp-server to 0.1.19.
2026-04-06 09:14:20 +09:00
Dayuan Jiang
41c410c2ba fix: bind MCP server HTTP to 127.0.0.1 only (#787)
The embedded HTTP sidecar was using server.listen(port) without a host
argument, which defaults to 0.0.0.0 (all interfaces). This exposed the
server to the local network. Now explicitly binds to 127.0.0.1.

Also excludes release/ from tsconfig to fix pre-existing TS errors.

Bumps @next-ai-drawio/mcp-server to 0.1.18.
2026-04-06 09:04:38 +09:00
16 changed files with 70 additions and 799 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Download draw.io static files for offline use
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
@@ -70,7 +70,7 @@ jobs:
shell: bash
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
@@ -105,14 +105,9 @@ jobs:
wait-for-completion: true
output-artifact-directory: release-signed
- name: Fix latest.yml with signed exe hashes
run: node scripts/fix-latest-yml.mjs
- name: Upload signed artifacts and update metadata to release
- name: Upload signed artifacts to release
uses: softprops/action-gh-release@v2
with:
files: |
release-signed/*.exe
release/latest*.yml
files: release-signed/*.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -25,7 +25,6 @@ import Image from "@/components/image-with-basepath"
import { ModelConfigDialog } from "@/components/model-config-dialog"
import { SettingsDialog } from "@/components/settings-dialog"
import { useDiagram } from "@/contexts/diagram-context"
import { useAutoUpdate } from "@/hooks/use-auto-update"
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
import { useDictionary } from "@/hooks/use-dictionary"
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
@@ -1254,22 +1253,10 @@ export default function ChatPanel({
sendChatMessage(newParts, savedXml, previousXml, sessionId)
}
// Auto-update listener (must be before early return so it's always active)
useAutoUpdate()
// Collapsed view (desktop only)
if (!isVisible && !isMobile) {
return (
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
<Toaster
position="bottom-left"
richColors
expand
toastOptions={{
style: { maxWidth: "480px" },
duration: 2000,
}}
/>
<ButtonWithTooltip
tooltipContent={dict.nav.showPanel}
variant="ghost"

View File

@@ -1,124 +0,0 @@
"use client"
import type React from "react"
interface UpdateToastAvailableProps {
variant: "download"
version: string
onDownload: () => void
onDismiss: () => void
}
interface UpdateToastManualProps {
variant: "manual"
version: string
url: string
onDismiss: () => void
}
interface UpdateToastDownloadingProps {
variant: "downloading"
percent: number
onDismiss: () => void
}
type UpdateToastProps =
| UpdateToastAvailableProps
| UpdateToastManualProps
| UpdateToastDownloadingProps
export function UpdateToast(props: UpdateToastProps) {
const { variant, onDismiss } = props
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault()
onDismiss()
}
}
return (
<div
role="alert"
aria-live="polite"
tabIndex={0}
onKeyDown={handleKeyDown}
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm"
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 flex-shrink-0">
<svg
className="w-4 h-4 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</div>
<div className="flex-1 min-w-0">
{variant === "downloading" ? (
<span className="text-sm text-foreground">
Downloading update... {Math.round(props.percent)}%
</span>
) : (
<span className="text-sm text-foreground">
Version {props.version} is available
</span>
)}
</div>
{variant === "download" && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
props.onDownload()
}}
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
>
Download
</button>
)}
{variant === "manual" && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
window.open(props.url, "_blank")
}}
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
>
Download
</button>
)}
<button
type="button"
onClick={onDismiss}
className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label="Dismiss"
>
<svg
className="w-4 h-4"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
)
}

View File

@@ -2,14 +2,6 @@
* Type declarations for Electron API exposed via preload script
*/
/** Update status data sent from main process */
type UpdateStatusData =
| { status: "available"; version: string }
| { status: "available-manual"; version: string; url: string }
| { status: "downloading"; percent: number }
| { status: "downloaded" }
| { status: "error"; message: string }
/** Configuration preset interface */
interface ConfigPreset {
id: string
@@ -82,12 +74,6 @@ declare global {
>
/** Set user's preferred locale */
setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
/** Listen for update status events from main process */
onUpdateStatus: (
callback: (data: UpdateStatusData) => void,
) => () => void
/** Start downloading the available update */
startDownload: () => Promise<void>
}
/** Settings window Electron API */

View File

@@ -6,7 +6,6 @@ import {
type MenuItemConstructorOptions,
shell,
} from "electron"
import { checkForUpdatesManual } from "./auto-updater"
import {
applyPresetToEnv,
getAllPresets,
@@ -157,11 +156,6 @@ function getMenuTemplate(): MenuItemConstructorOptions[] {
template.push({
label: t.help,
submenu: [
{
label: t.checkForUpdates,
click: () => checkForUpdatesManual(),
},
{ type: "separator" },
{
label: t.documentation,
click: async () => {

View File

@@ -1,282 +0,0 @@
import https from "node:https"
import { app, dialog, ipcMain } from "electron"
import electronUpdater from "electron-updater"
import { getMainWindow } from "./window-manager"
const { autoUpdater } = electronUpdater
const CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
const STARTUP_DELAY = 10_000 // 10 seconds
const GITHUB_API_URL =
"https://api.github.com/repos/DayuanJiang/next-ai-draw-io/releases/latest"
let isChecking = false
let updateDownloaded = false
/**
* Whether this platform supports electron-updater auto-update.
* macOS: disabled because builds are ad-hoc signed (no Apple Developer cert).
*/
function supportsAutoUpdate(): boolean {
if (process.platform === "darwin") return false
if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR)
return false
if (process.platform === "linux" && !process.env.APPIMAGE) return false
return true
}
/**
* Compare two semver-like version strings numerically.
* Returns true if remote > local.
* Strips prerelease/build metadata (e.g., "1.2.3-beta.1" → "1.2.3").
*/
function isNewerVersion(remote: string, local: string): boolean {
const strip = (v: string) => v.replace(/^v/, "").split("-")[0].split("+")[0]
const r = strip(remote).split(".").map(Number)
const l = strip(local).split(".").map(Number)
const len = Math.max(r.length, l.length)
for (let i = 0; i < len; i++) {
const rv = r[i] || 0
const lv = l[i] || 0
if (rv > lv) return true
if (rv < lv) return false
}
return false
}
/**
* Send update status to the renderer via IPC
*/
function sendStatus(data: Record<string, unknown>) {
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send("update-status", data)
}
}
/**
* Check GitHub API for latest release (used on macOS and Linux DEB)
*/
function checkGitHubRelease(manual: boolean) {
const req = https.get(
GITHUB_API_URL,
{
headers: { "User-Agent": "next-ai-draw-io" },
timeout: 15000,
},
(res) => {
if (res.statusCode !== 200) {
console.error(`GitHub API returned status ${res.statusCode}`)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message:
"Could not check for updates. Please try again later.",
})
}
isChecking = false
return
}
res.setEncoding("utf8")
let body = ""
res.on("data", (chunk: string) => {
body += chunk
})
res.on("end", () => {
try {
const data = JSON.parse(body)
const remoteVersion = data.tag_name || ""
const localVersion = app.getVersion()
if (isNewerVersion(remoteVersion, localVersion)) {
sendStatus({
status: "available-manual",
version: remoteVersion.replace(/^v/, ""),
url: data.html_url,
})
} else if (manual) {
dialog.showMessageBox({
type: "info",
title: "No Updates",
message: "You're up to date!",
detail: `Version ${localVersion} is the latest version.`,
})
}
} catch (err) {
console.error("Failed to parse GitHub release:", err)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message:
"Could not check for updates. Please try again later.",
})
}
}
isChecking = false
})
},
)
req.on("timeout", () => {
req.destroy()
isChecking = false
})
req.on("error", (err) => {
console.error("GitHub API request failed:", err)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message: "Could not check for updates. Please try again later.",
})
}
isChecking = false
})
req.end()
}
/**
* Set up electron-updater event handlers (Windows NSIS / Linux AppImage)
*/
function setupAutoUpdater() {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on("update-available", (info) => {
console.log("Update available:", info.version)
sendStatus({ status: "available", version: info.version })
isChecking = false
})
autoUpdater.on("update-not-available", () => {
console.log("No update available")
isChecking = false
})
autoUpdater.on("download-progress", (progress) => {
sendStatus({
status: "downloading",
percent: progress.percent,
})
})
autoUpdater.on("update-downloaded", () => {
updateDownloaded = true
console.log("Update downloaded")
sendStatus({ status: "downloaded" })
showRestartDialog()
})
autoUpdater.on("error", (err) => {
console.error("Auto-update error:", err)
sendStatus({ status: "error", message: String(err) })
isChecking = false
})
// IPC: renderer requests download
ipcMain.handle("updater:start-download", () => {
return autoUpdater.downloadUpdate()
})
}
/**
* Show restart dialog after update downloaded
*/
async function showRestartDialog() {
const result = await dialog.showMessageBox({
type: "info",
buttons: ["Restart Now", "Later"],
defaultId: 0,
cancelId: 1,
title: "Update Ready",
message: "A new version has been downloaded.",
detail: "Restart the app to install the update.",
})
if (result.response === 0) {
autoUpdater.quitAndInstall()
}
}
/**
* Run a single update check
*/
function doCheck(manual: boolean) {
if (isChecking) return
isChecking = true
// If update already downloaded, just re-show restart dialog
if (updateDownloaded && supportsAutoUpdate()) {
isChecking = false
showRestartDialog()
return
}
if (supportsAutoUpdate()) {
// For manual check, register listeners BEFORE triggering check
// to avoid race where event fires before listeners are attached
if (manual) {
const onNotAvailable = () => {
dialog.showMessageBox({
type: "info",
title: "No Updates",
message: "You're up to date!",
detail: `Version ${app.getVersion()} is the latest version.`,
})
cleanup()
}
const onAvailable = () => cleanup()
const onError = () => cleanup()
const cleanup = () => {
autoUpdater.off("update-not-available", onNotAvailable)
autoUpdater.off("update-available", onAvailable)
autoUpdater.off("error", onError)
}
autoUpdater.once("update-not-available", onNotAvailable)
autoUpdater.once("update-available", onAvailable)
autoUpdater.once("error", onError)
}
autoUpdater.checkForUpdates().catch((err) => {
console.error("checkForUpdates failed:", err)
isChecking = false
})
} else {
checkGitHubRelease(manual)
}
}
/**
* Initialize auto-updater. Call once after createWindow().
*/
export function initAutoUpdater() {
if (!app.isPackaged) return
if (supportsAutoUpdate()) {
setupAutoUpdater()
} else {
// Register no-op handler so renderer doesn't get an unhandled error
ipcMain.handle("updater:start-download", () => {})
}
// First check after startup delay
setTimeout(() => doCheck(false), STARTUP_DELAY)
// Periodic checks
setInterval(() => doCheck(false), CHECK_INTERVAL)
}
/**
* Manual update check from menu item
*/
export function checkForUpdatesManual() {
if (!app.isPackaged) {
dialog.showMessageBox({
type: "info",
title: "Development Mode",
message: "Auto-update is not available in development mode.",
})
return
}
doCheck(true)
}

View File

@@ -1,6 +1,5 @@
import { app, BrowserWindow, dialog, shell } from "electron"
import { buildAppMenu } from "./app-menu"
import { initAutoUpdater } from "./auto-updater"
import { getCurrentPresetEnv } from "./config-manager"
import { loadEnvFile } from "./env-loader"
import { registerIpcHandlers } from "./ipc-handlers"
@@ -59,11 +58,6 @@ if (!gotTheLock) {
// Create main window
createWindow(serverUrl)
// Initialize auto-updater (production only)
if (!isDev) {
initAutoUpdater()
}
} catch (error) {
console.error("Failed to start application:", error)
dialog.showErrorBox(

View File

@@ -33,7 +33,6 @@ export interface MenuTranslations {
help: string
documentation: string
reportIssue: string
checkForUpdates: string
}
const translations: Record<MenuLocale, MenuTranslations> = {
@@ -63,7 +62,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "Help",
documentation: "Documentation",
reportIssue: "Report Issue",
checkForUpdates: "Check for Updates...",
},
zh: {
@@ -92,7 +90,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "帮助",
documentation: "文档",
reportIssue: "报告问题",
checkForUpdates: "检查更新...",
},
ja: {
@@ -121,7 +118,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "ヘルプ",
documentation: "ドキュメント",
reportIssue: "問題を報告",
checkForUpdates: "アップデートを確認...",
},
"zh-Hant": {
@@ -150,7 +146,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "說明",
documentation: "文件",
reportIssue: "回報問題",
checkForUpdates: "檢查更新...",
},
}

View File

@@ -1,12 +1,4 @@
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron"
// Locally defined to avoid dependency on electron.d.ts compilation scope
type UpdateStatus =
| { status: "available"; version: string }
| { status: "available-manual"; version: string; url: string }
| { status: "downloading"; percent: number }
| { status: "downloaded" }
| { status: "error"; message: string }
import { contextBridge, ipcRenderer } from "electron"
/**
* Expose safe APIs to the renderer process
@@ -39,13 +31,4 @@ contextBridge.exposeInMainWorld("electronAPI", {
getUserLocale: () => ipcRenderer.invoke("get-user-locale"),
setUserLocale: (locale: string) =>
ipcRenderer.invoke("set-user-locale", locale),
// Auto-update
onUpdateStatus: (callback: (data: UpdateStatus) => void) => {
const handler = (_event: IpcRendererEvent, data: UpdateStatus) =>
callback(data)
ipcRenderer.on("update-status", handler)
return () => ipcRenderer.removeListener("update-status", handler)
},
startDownload: () => ipcRenderer.invoke("updater:start-download"),
})

View File

@@ -1,86 +0,0 @@
import { createElement, useEffect, useRef } from "react"
import { toast } from "sonner"
import { UpdateToast } from "@/components/update-toast"
export function useAutoUpdate() {
const downloadToastId = useRef<string | number | null>(null)
useEffect(() => {
const api = (window as Window).electronAPI
if (!api?.onUpdateStatus) return
const cleanup = api.onUpdateStatus((data) => {
switch (data.status) {
case "available":
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "download",
version: data.version,
onDownload: () => {
toast.dismiss(t)
api.startDownload().catch(() => {
// Error will come through update-status channel
})
},
onDismiss: () => toast.dismiss(t),
}),
{ duration: 15000 },
)
break
case "available-manual":
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "manual",
version: data.version,
url: data.url,
onDismiss: () => toast.dismiss(t),
}),
{ duration: Number.POSITIVE_INFINITY },
)
break
case "downloading": {
const id =
downloadToastId.current ??
`download-progress-${Date.now()}`
downloadToastId.current = id
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "downloading",
percent: data.percent,
onDismiss: () => toast.dismiss(t),
}),
{ id, duration: Number.POSITIVE_INFINITY },
)
break
}
case "downloaded":
// Dismiss progress toast — native restart dialog handles the rest
if (downloadToastId.current) {
toast.dismiss(downloadToastId.current)
downloadToastId.current = null
}
break
case "error":
if (downloadToastId.current) {
// Error during active download — inform the user
toast.dismiss(downloadToastId.current)
downloadToastId.current = null
toast.error(
"Update download failed. Please try again later.",
)
}
// Otherwise: silent — background check failure, logged in main process
break
}
})
return cleanup
}, [])
}

118
package-lock.json generated
View File

@@ -48,9 +48,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"electron-updater": "^6.8.3",
"idb": "^8.0.3",
"jsonrepair": "^3.13.1",
"lightningcss": "^1.32.0",
"lightningcss-linux-x64-gnu": "^1.32.0",
"lucide-react": "^0.577.0",
"motion": "^12.23.25",
"nanoid": "^5.0.0",
@@ -2225,6 +2226,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
@@ -2242,6 +2246,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
@@ -2259,6 +2266,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
@@ -2276,6 +2286,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
@@ -8246,6 +8259,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10046,6 +10062,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -10681,6 +10698,7 @@
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
@@ -12779,69 +12797,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/electron-updater": {
"version": "6.8.3",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
"license": "MIT",
"dependencies": {
"builder-util-runtime": "9.5.1",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "~7.7.3",
"tiny-typed-emitter": "^2.1.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-updater/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/electron-updater/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-winstaller": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
@@ -14960,6 +14915,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/gzip-size": {
@@ -16192,6 +16148,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -16409,6 +16366,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/levn": {
@@ -16609,6 +16567,9 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16792,6 +16753,9 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16812,6 +16776,9 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16832,6 +16799,9 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -17099,19 +17069,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -21098,6 +21055,7 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
@@ -22517,12 +22475,6 @@
"semver": "bin/semver"
}
},
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",

View File

@@ -70,7 +70,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"electron-updater": "^6.8.3",
"idb": "^8.0.3",
"jsonrepair": "^3.13.1",
"lucide-react": "^0.577.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.17",
"version": "0.1.19",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module",
"main": "dist/index.js",

View File

@@ -4,6 +4,29 @@
*/
import http from "node:http"
const MAX_BODY_BYTES = 10 * 1024 * 1024 // 10 MiB
function readBody(
req: http.IncomingMessage,
res: http.ServerResponse,
cb: (body: string) => void,
): void {
let body = ""
let size = 0
req.on("data", (chunk: Buffer) => {
size += chunk.length
if (size > MAX_BODY_BYTES) {
res.writeHead(413, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "Payload too large" }))
req.destroy()
return
}
body += chunk
})
req.on("end", () => cb(body))
}
import {
addHistory,
clearHistory,
@@ -155,7 +178,7 @@ export function startHttpServer(port = 6002): Promise<number> {
}
})
server.listen(port, () => {
server.listen(port, "127.0.0.1", () => {
serverPort = port
log.info(`HTTP server running on http://localhost:${port}`)
resolve(port)
@@ -266,11 +289,7 @@ function handleStateApi(
}),
)
} else if (req.method === "POST") {
let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
readBody(req, res, (body) => {
try {
const data = JSON.parse(body)
const { sessionId } = data
@@ -347,11 +366,7 @@ function handleRestoreApi(
return
}
let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
readBody(req, res, (body) => {
try {
const { sessionId, index } = JSON.parse(body)
if (!sessionId || index === undefined) {
@@ -393,11 +408,7 @@ function handleHistorySvgApi(
return
}
let body = ""
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
readBody(req, res, (body) => {
try {
const { sessionId, svg } = JSON.parse(body)
if (!sessionId || !svg) {

View File

@@ -1,134 +0,0 @@
/**
* Fix latest.yml metadata after Windows code signing.
*
* electron-builder generates latest.yml with hashes of unsigned executables.
* After SignPath signs them, the hashes change. This script reads each
* latest*.yml in release/, finds the matching signed exe in release-signed/,
* and rewrites the sha512 and size fields.
*
* Usage: node scripts/fix-latest-yml.mjs
*/
import { createHash } from "node:crypto"
import {
createReadStream,
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs"
import { join } from "node:path"
const RELEASE_DIR = "release"
const SIGNED_DIR = "release-signed"
// Verify directories exist
if (!existsSync(RELEASE_DIR)) {
console.error(`Error: ${RELEASE_DIR}/ directory does not exist`)
process.exit(1)
}
if (!existsSync(SIGNED_DIR)) {
console.error(`Error: ${SIGNED_DIR}/ directory does not exist`)
process.exit(1)
}
// Find all latest*.yml files
const ymlFiles = readdirSync(RELEASE_DIR).filter(
(f) => f.startsWith("latest") && f.endsWith(".yml"),
)
if (ymlFiles.length === 0) {
console.log("No latest*.yml files found in release/")
process.exit(0)
}
/**
* Compute SHA-512 hash of a file using streaming (avoids loading large exe into memory)
*/
function hashFile(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash("sha512")
const stream = createReadStream(filePath)
stream.on("data", (chunk) => hash.update(chunk))
stream.on("end", () => resolve(hash.digest("base64")))
stream.on("error", reject)
})
}
// Build a map of signed exe filenames to their hash and size
const signedFiles = new Map()
for (const f of readdirSync(SIGNED_DIR)) {
if (!f.endsWith(".exe")) continue
const filePath = join(SIGNED_DIR, f)
const sha512 = await hashFile(filePath)
const size = statSync(filePath).size
signedFiles.set(f, { sha512, size })
}
if (signedFiles.size === 0) {
console.error("No signed .exe files found in release-signed/")
process.exit(1)
}
console.log(
`Found ${signedFiles.size} signed exe(s):`,
[...signedFiles.keys()].join(", "),
)
for (const ymlFile of ymlFiles) {
const ymlPath = join(RELEASE_DIR, ymlFile)
const content = readFileSync(ymlPath, "utf-8")
const lines = content.split("\n")
const outputLines = []
// Track the current file entry being processed (by url field)
let currentExeName = null
for (const line of lines) {
// Match " url: SomeFile.exe" inside the files array
const urlMatch = line.match(/^\s+url:\s+(.+\.exe)\s*$/)
if (urlMatch) {
currentExeName = urlMatch[1]
outputLines.push(line)
continue
}
// Match top-level "path: SomeFile.exe"
const pathMatch = line.match(/^path:\s+(.+\.exe)\s*$/)
if (pathMatch) {
currentExeName = pathMatch[1]
outputLines.push(line)
continue
}
// Replace sha512 lines
const sha512Match = line.match(/^(\s*)sha512:\s+/)
if (sha512Match && currentExeName && signedFiles.has(currentExeName)) {
const indent = sha512Match[1]
outputLines.push(
`${indent}sha512: ${signedFiles.get(currentExeName).sha512}`,
)
continue
}
// Replace size lines
const sizeMatch = line.match(/^(\s*)size:\s+\d+/)
if (sizeMatch && currentExeName && signedFiles.has(currentExeName)) {
const indent = sizeMatch[1]
outputLines.push(
`${indent}size: ${signedFiles.get(currentExeName).size}`,
)
// Reset current exe after processing both sha512 and size
currentExeName = null
continue
}
outputLines.push(line)
}
writeFileSync(ymlPath, outputLines.join("\n"))
console.log(`Updated ${ymlFile}`)
}
console.log("Done!")

View File

@@ -35,6 +35,7 @@
"packages",
"electron",
"electron-standalone",
"dist-electron"
"dist-electron",
"release"
]
}