OpenKnowledge

v0.7.0

Part of the OpenKnowledge changelog.

Minor Changes

  • feat(desktop): decouple the Open Knowledge server's lifecycle from the Electron app

    Packaged desktop builds now spawn the OK server as a fully-detached child_process.spawn of the bundled dist/cli.mjs start under ELECTRON_RUN_AS_NODE=1. The server runs in its own process group (detached: true, stdio: 'ignore', .unref()) and survives Electron parent exit — closing the editor window OR quitting the desktop no longer affects the server. MCP-connected agents (Claude Code, Cursor, Codex) keep working through both events.

    Server lifecycle. The desktop becomes one client of the project server, not its owner. Every desktop window is effectively in attach mode — the production detached-spawn path bootstraps the server then delegates to the existing attach-mode codepath. tryAttachExistingServer now accepts ANY valid server.lock regardless of kind (the previous refusal of mcp-spawned locks + the SIGTERM-and-replace branch are removed — lock kind is provenance-only, not a capability gate). Opening the desktop on a project with a live MCP-spawned server attaches instead of destroying the agent's session.

    Auto-update. A new stopAllOwnedServers() replaces the previous synchronous killAllUtilities(). Before quitAndInstall, the desktop SIGTERMs each spawned detached pid in parallel, polls each lock for release every 200 ms for up to 10 s (sharing DEFAULT_SIGTERM_GRACE_MS with ok start's idle-shutdown UI-sibling termination), and escalates to SIGKILL per-pid on timeout. The hook is awaited before quitAndInstall so ShipIt's pre-swap pgrep check sees a clean process tree.

    Keepalive. The desktop main process holds a presence-invisible /collab/keepalive WS per open project — counts toward the server's idle-shutdown WS-client tally so a brief MCP disconnect (agent restart, IDE reload) can't trip idle-shutdown mid-session. Identity fields are omitted intentionally so the desktop is not redundantly rendered as a peer in the agent-presence bar.

    Process visibility. ok start now sets process.title = 'open-knowledge-server <projectName>' early in its action. The detached server is findable in Activity Monitor and ps -ax | grep open-knowledge-server — the primary user-facing surface for orphan management, since there is no in-app stop UX (idle reaper handles routine cleanup; Activity Monitor handles wedged servers).

    CLI. Two new flags on ok start enable the single-origin renderer model the desktop relies on:

    • --serve-content-assets — serves content-directory assets (images, PDFs, file attachments) via createAssetServeMiddleware on the same HTTP port as /api/* and /collab*.
    • --react-shell-dist-dir <path> — serves a bundled React shell on / with SPA fallback (and auto-suppresses the ok ui sibling — when this flag is set, the server is self-sufficient for in-app-browser preview).

    Terminal-launched ok start without these flags retains today's two-process behavior (server + ok ui sibling).

    Dev path unchanged. bun run dev in packages/app/ keeps the in-process hocuspocus-plugin.ts model (HMR + log capture ergonomics). Electron dev keeps utilityProcess.fork — the spawnDetachedServer dep is omitted in dev wiring; the WindowManager falls back to forkUtility.

    Shared primitives. The MCP shim's startKeepalive is lifted from packages/cli/src/mcp/keepalive.ts to @inkeep/open-knowledge-core/keepalive/ (its McpLogger type reference is generalized to a structural KeepaliveLogger interface). Both the MCP shim and the desktop main process now consume from the shared location. The CLI's DEFAULT_SIGTERM_GRACE_MS + DEFAULT_SIGTERM_POLL_MS are lifted to @inkeep/open-knowledge-core/lifecycle-constants so the desktop's auto-update teardown and the CLI's idle-shutdown UI-sibling termination stay in lockstep.

    Closes PRD-6665. Unblocks PRD-6664 (agent-side failure when server is down) and PRD-6513 (MCP tool completeness + agent harness integration) — both presupposed a server that outlives a single GUI session.

    Spec: specs/2026-05-21-desktop-detached-server-lifecycle/SPEC.md.

  • MCP host entries are now written as npx -y @inkeep/open-knowledge@latest mcp everywhere, and existing Open Knowledge-managed entries are reclaimed to that canonical shape on Desktop boot or ok start. The Desktop File-menu Command-Line Tools install surface was removed; use npm install -g @inkeep/open-knowledge or npx @inkeep/open-knowledge for terminal access. On macOS, the published ok mcp now proxies to the installed Desktop bundle when available; suppress with ok mcp --no-bundle-proxy or OK_BUNDLE_PROXY=0.

Patch Changes

  • fix(open-knowledge): keep the desktop beta channel sticky across stable promotions

    A machine running a beta desktop build was a one-way door onto stable. The auto-updater derived its channel purely from the running binary's version (channelFromVersion(app.getVersion())), and electron-updater's GitHub provider treats the beta channel as a superset of stable — a beta client walks the repo's single releases.atom feed, picks the newest release of either channel, and cascades beta-mac.ymllatest-mac.yml when that release is a stable one. So every stable promotion auto-updated beta machines onto the stable build. From that point channelFromVersion returned latest, allowPrerelease went false, and no future beta was ever visible again — there was no persisted channel preference and no way back.

    The auto-update channel is now sticky. AppState.updateChannel is a persisted preference that latches to beta the first time a beta build runs on a machine. The effective channel (resolveEffectiveChannel) is beta whenever either the build OR the persisted preference is beta, so allowPrerelease stays true after a cascade-promotion onto a stable build and the machine re-boards the next beta release instead of being stranded on stable. Beta testers still ride onto stable releases (the intended cascade) — they just no longer fall off the beta track when they do.

    No new UI: the latch is automatic. There is no in-app beta→stable opt-out in this change; reverting to stable means installing a stable DMG and clearing the updateChannel key in state.json. A Settings channel toggle is future work.

  • fix(open-knowledge): serialize concurrent writes to MCP host config files

    writeEditorMcpConfig (the shared primitive that writes OK's MCP server entry into Claude Desktop / Cursor / Codex / VS Code / Windsurf config files) previously used a naked fs.writeFileSync inside a read-modify-write loop with no advisory lock and no atomic-write guarantee. Concurrent OK writers — CLI ok init running while OK Desktop's startup-repair sweep is firing, two desktop instances launching against the same project, a double-clicked Add button in the consent dialog — could each observe the same pre-state, each compute pre-state + my-entry, and the second write would clobber the first writer's addition. In the worst case, writeFileSync interleaves stripped every pre-existing MCP server entry from other tools (Cursor's, hand-edited, Codex's) — leaving only the last writer's single entry as the entire server map.

    A 20-process race reproduction (now committed as a regression test under packages/cli/tests/integration/mcp-host-config-race.test.ts) showed lost updates in every trial, JSON corruption in one of five trials, and destruction of pre-existing entries in two of five. The bug class is the same one Anthropic's own claude-code#28966 identified for .claude.json and recommends fixing with atomic writes plus a write lock.

    The fix wraps the read-modify-write block in a per-config-file advisory lock (<configPath>.lock via withFileLockSync, new in @inkeep/open-knowledge-core/server) and switches the underlying writes to a tmp+rename atomic pattern. The lock serializes OK writers across processes; the atomic write prevents external readers (Claude Desktop's config-load on launch) from observing a torn file mid-write.

    writeEditorMcpConfig keeps its synchronous signature — the sync lock variant uses a bounded busy-wait sleep during retry rather than forcing an async cascade through applyProjectIntegrations, writeProjectAiIntegrations, repairMcpConfigs, and Desktop's writeProjectMcpConfig arrow function. Typical lock-hold windows are sub-10 ms; the 5 s acquire timeout bounds the worst-case CPU spin.

  • Fix Move-to-Trash for newly-created files in the sidebar (PRD-6769). Right-click → Delete on an Untitled file now correctly moves it to OS Trash instead of failing with a TrashFailureModal. Same fix applies to any freshly-renamed file whose Pierre tree path is still in the post-rename-strip extensionless state.

  • fix(open-knowledge/app): inline markdown images get click-to-zoom

    Markdown ![alt](src) images sitting mid-prose (inline-position — image inside a paragraph that has other content) rendered as bare <img> tags with no lightbox affordance. Block-context ![]() (image on its own line) already had zoom because imagePromoterPlugin promotes single-image paragraphs to mdxJsxFlowElement(CommonMarkImage), which routes through the img descriptor → React Image.tsx (with react-medium-image-zoom's always-on <Zoom> wrap). Inline-position images deliberately stay as bare PM image nodes (the promoter skips them — promoting an inline image to a flow-level component would split the paragraph), so they bypassed that surface entirely.

    The app layer now overrides ImageSrcFidelity with a React NodeView that wraps the inline <img> in the same <Zoom> lightbox, matching the descriptor side's configuration (wrapElement="span" for inline-flow fit; zoomMargin={20}; zoomImg.sizes cleared so the modal isn't constrained by a thumbnail-scoped sizes attribute). Block-context images, markdown round-trip, and PM-to-HTML serialization (clipboard, export) are unchanged — only the live editor render of the bare PM image node is replaced.

    Inline alignment is intentionally not surfaced: inline images live inside flow paragraphs, and align="right" mid-paragraph would have to float out of the text-run to mean anything. Authors who want alignment move the image to its own line (which already gets the full block-context CommonMarkImage controls).

View v0.7.0 on GitHub