v0.10.0-beta.9
Pre-releaseFirst beta of the cycle — 26 changesets.
Minor Changes
-
Adopt GPL-3.0-or-later. Open Knowledge is now licensed under the GNU General Public License v3.0 or later: every package declares the
licensefield, the GPLv3 LICENSE text ships in the npm tarball and the desktop app bundle,ok --versionprints the standard GPL notice (copyright, free software, no warranty), and the desktop About panel carries the same notice. -
Add
ok <file>— open a single markdown file in the editor with zero project setup. Runok notes.md(orok open notes.md) on any.md/.mdxfile and it opens in the WYSIWYG ↔ source editor.- Project-aware. When the file lives inside an existing Open Knowledge project (an ancestor
.ok/), it opens that project focused on the file — the path is realpath-resolved first, so a symlink into a project routes correctly. - No-project mode. For a loose file, it opens an ephemeral single-file session: a throwaway server scoped to just that one file, with git, MCP, and agents off. Edits save straight back to your original file. No
.ok/or other state is written into your directory — all session state lives in a temporary directory that's removed when you close the window. - Opening never reformats. A file you open but don't edit is left byte-for-byte identical, even when it has an unstable markdown round-trip.
- Desktop-first with a browser fallback. Opens in the Open Knowledge desktop app when it's installed; otherwise serves the editor in your browser (Ctrl-C to end the session). Launching from a closed app goes straight to the file in a single window — it doesn't reopen your last project alongside it.
- Focused chrome. No-project sessions hide the file sidebar, tabs, project switcher, and Settings — just the file and the editor.
Discoverable via
ok --help. - Project-aware. When the file lives inside an existing Open Knowledge project (an ancestor
-
Preview embeds can now reach the network. The code-block
html previewiframe runs with an open network CSP, so embeds that load external stylesheets,fetchlive data, pull map tiles / remote images, use web fonts, or embed third-party iframes (overhttps:/wss:/data:/blob:) render — Leaflet maps, live-data charts, and the like work out of the box.'unsafe-eval'is not granted (the common libraries don't need it), and*/ plaintexthttp:/ws:schemes are excluded.The iframe stays a sandboxed null-origin frame (
sandbox="allow-scripts", noallow-same-origin), so an embed can reach the network but can never read the knowledge base, cookies, or auth — it widens network reach, not document access.Breaking: the
preview.scriptSrcconfig field is removed — the preview network policy is no longer configurable. A stalepreview.scriptSrckey is rejected loudly on config load with a migration message (and handled byok config migrate), not a silent no-op. A future multi-tenant host that needs to lock the preview network down will do so with an operator-level control (an env / build flag the tenant can't edit), not a content-editable config field.
Patch Changes
-
Rename the
gbrainstarter pack to Entity vault (GBrain-compatible) and reposition it as a human cockpit for GBrain-style Markdown brains rather than a GBrain reimplementation. This is a breaking pre-release rename: the canonical pack ID is nowentity-vaultwith nogbrainalias, sook seed --pack gbrainno longer resolves — useok seed --pack entity-vault. The pack picker, seed dialog, toast, and CLI list now show "Entity vault (GBrain-compatible)". Generated dossier templates are tightened for GBrain-compatible parsing — document-leveltitle:frontmatter, an explicit--- timeline ---sentinel, and parseable- **YYYY-MM-DD** | source | @author — … Confidence: …timeline bullets with path-qualified[[folder/slug|Label]]links. The workflow doc moved from/workflows/gbrainto/workflows/entity-vault. OK edits and reviews the Markdown; Garry Tan'sgbrain, if installed, can still import/sync the same vault — interop is Markdown + Git, with no deep integration implied. -
Restore the
repositoryfield on the published CLI package, pointing atinkeep/open-knowledge. It was dropped when the mirror was re-pointed offopen-knowledge-legacy, which left npm Trusted Publishing unable to verify provenance and failed every release publish withE422 ... "repository.url" is "". -
Desktop: the main process no longer crashes with a recurring "A JavaScript error occurred in the main process — Error: write EPIPE" dialog after every window is closed. On macOS the main process outlives its windows, and the auto-updater's periodic timer keeps writing to the stdout/stderr inherited from the launching terminal; once that terminal closes, each write throws
EPIPE, which — with no streamerrorlistener — escalated to an uncaught exception and Electron's fatal-error modal, reappearing every few minutes until the process was killed. A broken-pipe guard installed at the earliest point of main-process boot now handles these benign writes at the stream boundary so they never become an uncaught exception, while genuine (non-broken-pipe) stream errors are still surfaced to the file log. The guard is logger-agnostic, so it covers every stdout/stderr writer in the main process, not just the updater. -
Fix the editor toolbar's "Open with AI" menu freezing the rest of the app in the macOS desktop app. Once the menu became openable on the desktop host, its default modal behavior disabled pointer events on everything outside the menu while it was open. Because the menu lives in the macOS title-bar drag region — where the outside-click that normally dismisses a modal doesn't reliably reach the menu — the only way to close it was to pick an agent, and meanwhile the rest of the chrome (notably the bottom-left project switcher) couldn't be clicked. The menu is now non-modal: opening it no longer blocks the rest of the UI, and clicking anywhere outside dismisses it. Browsers (
ok ui) are unaffected. -
Fix the sidebar project switcher not opening, and not dismissing on outside click, in the macOS desktop app. Radix's DropdownMenu opens on
pointerdown, but the desktop renderer does not receive realpointerdownevents (onlymousedown/click), so clicking the trigger did nothing; and because the menu was modal, clicking outside could not dismiss it either. The trigger now also opens from theclickevent on the desktop host (mirroring the editor's "Open with AI" menu), and the menu is non-modal so outside-click dismissal works and the rest of the UI stays interactive while it's open — matching the Cloud/Sync popover. Browsers are unaffected. -
Fix the project switcher in the sidebar footer not opening in the macOS desktop app. Clicking it showed a press state but the dropdown never opened. A Radix tooltip recently added to the trigger (to show the project path on hover) interfered with the menu on the desktop host: once the tooltip had opened on hover, clicking the trigger raced the tooltip's teardown against the menu's open, so the menu never stayed open. The trigger no longer uses a Radix tooltip — the full project path is shown via a native tooltip instead — and the menu opens reliably.
-
fix(open-knowledge): source-view tables no longer render indented left of surrounding text
GFM table rows in the Markdown source view rendered roughly 2ch to the left of the
surrounding prose, headings, and lists. The source-polish view-plugin set the
.cm-table-row/.cm-table-headerline classes but never set the--list-hang
variable that the base.cm-linerule consumes, so the base!important
padding-inline-startoverrode the standalone tablepaddingwhile the table's
negativetext-indentstill applied, pulling table lines into the gutter. Tables
now participate in the same hanging-indent mechanism as lists and fenced code, so
they line up with body text. (PRD-6922) -
fix(sync): track push and pull errors separately so a sync error stops flashing in the popover
The sync status popover would show a sync error (e.g. a failed push) for only a split second before it vanished. The engine stored the last failure in a single shared
errorfield, so a successful fetch on the pull leg cleared a still-unresolved push error — andtrigger('sync')runs push then pull, so the push error was wiped by the immediately-following fetch within the same "Sync now". The same alternation happened continuously under auto-sync.Push and pull errors are now tracked independently (
pushError/pushErrorCodeandpullError/pullErrorCode). A success on one leg only clears that leg's error, so a real push failure stays visible until a push actually succeeds (or sync is toggled).When both legs fail with the same root cause (e.g. an auth failure that blocks fetch and push alike) the popover collapses them into a single neutral line instead of repeating two near-identical messages. When the two legs fail for different reasons, each line is labeled "Push:" / "Pull:" so it's clear which direction failed. A lone failure renders unlabeled, with pull-specific copy for read-side auth failures.
-
Remove the dismiss X from the "Relaunching to install the update…" banner. That card is a terminal in-progress state shown the instant Relaunch is clicked — there is nothing to dismiss, and the card already disappears when the app restarts, so a manual close would only hide live progress. The armed "Version X ready to install" card keeps its X (the user can still close it and relaunch later); only the post-click in-progress swap is now non-dismissible, via a new
dismissibleflag on the update-notice shape. -
Fix the log and telemetry file-sinks writing into a second
.ok/whencontent.diris a sub-folder. Both per-machine sinks (.ok/local/logs/server-current.jsonl,.ok/local/telemetry/spans-current.jsonl) now anchor on the project root likeserver.lock/principal.json/state.json, so a project with e.g.content.dir: docskeeps a single.ok/at the root instead of growing a second one buried inside the content sub-folder where backups, git, sync, and visual inspection wouldn't expect it.ok diagnose bundlereads the sinks andserver.lockfrom the same project-root-anchored.ok/local/so it keeps harvesting them after the move. -
Open
.baseand.canvasfiles (Obsidian Bases / Canvas) directly in the read-only text viewer. Previously, clicking a[[file.base]]or[[file.canvas]]wiki-link opened a chooser pane; clicking "Open file" from the chooser then replaced the editor view with a raw415JSON error envelope from the API. Now both file types open immediately in the built-in text viewer (.canvaswith JSON syntax highlighting) — no chooser, no broken "Open file". The "Open file" affordance for all other downloadable types (.docx,.zip, etc.) is also hardened: it now routes through the sanctioned asset-dispatch path (OS-handoff on desktop, new tab on web) instead of a same-frame navigation to the asset API. -
Fix the editor toolbar's "Open with AI" menu not opening in the macOS desktop app. The header sits in a macOS title-bar drag region (
-webkit-app-region: drag), and macOS swallows thepointerdownevent on its children — even on theno-dragbutton — before the DOM sees it. Radix's dropdown opens onpointerdown, so clicking the button did nothing. The synthesizedclickstill fires, so on the desktop host the menu now opens from the click instead. Browsers (ok ui) are unaffected and keep Radix's default behavior. This regressed when the button's redundant hover tooltip was removed, which had incidentally kept pointer events flowing to the trigger. -
fix(open-knowledge): MCP writes reflect on-disk truth — reconcile out-of-band edits instead of silently clobbering them (PRD-6832)
An OK MCP read could authoritatively return content older than the bytes on disk, with no warning. The root cause was upstream of the read: a doc loaded in the server holds stale in-memory CRDT state, an out-of-band edit (a script,
git pull, manual edit) makes disk newer, and the next MCP write serializes the stale CRDT over the newer file — making disk itself stale, so the next read faithfully returns bad bytes. In one incident this gave an agent a stale spec scope and cost a multi-file revert.This change makes disk authoritative on the write/reconcile path:
- Reconcile-before-apply (L1). Before a content write (
write_document/edit_document/edit_frontmatter) or arenameapplies, the server compares the on-disk bytes to the last-synced base and, on divergence, ingests the disk edit first (through the existing sanctioned file-watcher path) so the agent's edit lands on top of current reality. Both edits survive. - Store-time backstop (L3). For the residual few-millisecond window where disk changes between the reconcile check and the store, the store re-checks disk before overwriting. On divergence it aborts the overwrite (disk wins) and the handler returns a hard
urn:ok:error:disk-divergence(409) — the agent's edit was NOT applied; re-read and retry (a retry re-applies exactly once). This is the only guard forundo/rollback, which have no L1. - Heads-up on reconcile. When a write reconciles an out-of-band edit, the success response carries a
disk-edit-reconciledwarning (structuredContent.contentDivergence) so the agent re-reads to see the combined result. Observational — the write still landed and both edits are on disk.
Human-editor (browser) writes are unaffected: the backstop only fires on agent-triggered stores, so in-progress typing is never reverted.
- Reconcile-before-apply (L1). Before a content write (
-
fix(markdown): stop over-escaping phrasing-boundary whitespace character references
A space or tab at a phrasing boundary (the edge of a paragraph, list item, or blockquote's inline content) was serializing to a backslash-escaped character reference (
\ /\	) instead of a bare /	. Per CommonMark the escaped form renders as the literal visible text in GitHub, the docs site, and Open Knowledge's own re-parsed editor.escapeEntityAmpersandsnow leaves character references thatmdast-util-to-markdownsynthesizes for boundary-whitespace preservation un-escaped, while continuing to escape user-authored entities. -
Allow digit-leading tags (like the year
2026) in a document's frontmattertags:field. The property panel previously rejected them with "Tags must start with a letter…", even though a year is a legitimate tag. The frontmatter tag grammar is now more permissive than the inline#taggrammar: a frontmatter tag may start with a letter or a digit, while inline#tagin prose still requires a leading letter (so#123stays plain text and does not collide with issue-reference conventions). -
Fix
execMCP results overflowing the client's per-result token limit on mid-size reads. The visible body was serialized up to 3× per result —content[].text,structuredContent.text(the load-bearing cross-client mirror that Claude-class clients read when they dropcontent[].text), and a redundantstructuredContent.stdoutraw copy — while the soft cap budgeted only one copy. A markdown file comfortably under the cap (soexecreportedstdoutTruncated: false) could still produce a result large enough that the client rejected it and spilled to a file, silently degrading an in-budget read.Two changes: (1) drop the
structuredContent.stdoutfield. It was the original Claude-Desktopcontent[].text-drop workaround, fully superseded by thestructuredContent.textmirror it predates; its bytes are a strict subset oftextand nothing consumed it. (2) Recalibrate the soft cap to budget the realized 2× body duplication (content[].text+structuredContent.text) rather than a single copy — the per-string cap is now ~25 KB (RESULT_BODY_BUDGET_BYTES / WIRE_BODY_COPIES), so the doubled payload stays well under the client ceiling. Oversized reads now truncate with the existing<truncated: … re-run with a more-specific query>marker instead of silently overflowing. The cross-clientcontent[].text+structuredContent.textmirror is unchanged. -
Fix telemetry shutdown losing local spans when the OTLP collector is unreachable.
shutdownTelemetry()now drains the local file-sink exporter on its own awaited promise before the provider-wide teardown, instead of relying ontracerProvider.shutdown(). The provider fan-out isPromise.all(...).then(resolve, reject), which short-circuits the moment the OTLP push processor rejects (a fast ECONNREFUSED when no collector is listening) — pre-empting the file pipeline's still-in-flight disk write. That dropped the very spansok diagnose bundleexists to harvest and caused the intermittentTelemetry > Gate combination 4failure. Draining the file sink first makes on-disk span capture independent of the push pipeline's fate; the whole sequence still rides the existing shutdown-timeout race, so a pathological filesystem stall cannot deadlock teardown. -
Add a "New project" command to the Cmd+K command palette. It opens the same create-new-project dialog reachable from the File → Create New Project… menu and the project switcher. Desktop-only — hidden in the web host.
-
Fix inline markdown images with a doc-relative path (
mid-prose) resolving against the SPA root instead of the document's folder. The inline image node-view now applies the same doc-folder base resolution the block image render path already uses, so a relativesrcno longer 404s until a hard reload. -
feat(open-knowledge): editor footer counts scope to the selection
The footer word / character / token counts now reflect the current text
selection. When you select a passage in either edit mode, the counts switch to
that passage and gain a "Selected" indicator; collapsing the selection reverts
to the whole-document counts. Selection counts use the same visible-text
semantics as the document counter (markdown syntax stripped), so the same
passage counts identically whether selected in WYSIWYG or source mode. -
Sidebar + share-dialog polish:
- Custom Markdown sidebar icon (M↓) for
.mdand.mdxfiles, replacing Pierre's default glyph. Colored via the existing--trees-file-icon-color-markdownCSS variable so it tracks light/dark and selected-row treatment. - ShareBranchSwitchDialog: align Cancel button styling with the other dialog Cancel buttons (outline + font-mono uppercase). Give the secondary "Open in current branch" button the same uppercase font-mono treatment as the primary "Open in " button.
- ShareBranchSwitchDialog: move the repository/file/branch metadata block from the dialog header into the body so it groups visually with the explanation paragraph.
- ShareBranchSwitchDialog: fix a spurious scrollbar caused by inline
<code>pills whosepy-0.5extends past the surrounding line-box (leading-6on the variant paragraphs absorbs the inline padding). - ShareMetadataRows: bump value rows to
text-1smand label cells totext-xsfor tighter hierarchy (affects both share dialogs that use this component). - ShareReceiveDialog: tighten the inline-
<code>corner radius torounded-sm.
- Custom Markdown sidebar icon (M↓) for
-
Custom DMG install window: the mounted disk image now shows the standard "drag the app onto Applications" layout, with the app icon and the
/Applicationsalias positioned over a background image.Configured via electron-builder's
dmgblock (installer-window size, icon coordinates,background). The committed background is a schematic placeholder; final branded artwork drops intopackages/desktop/build/dmg-background.png+dmg-background@2x.png(540x380 / 1080x760). Iterate on the layout locally withbun run build:mac:unsigned. -
feat(ok-desktop): show the release-notes update notice on every open project window
The "Updated to Version X" release-notes notice previously appeared on only one project window when several projects were open together, so a user looking at a different project never saw it. It now appears on every open window, and the "Relaunch now" update banner does too.
Dismissing the release-notes notice on one window (or letting it auto-expire after its one-minute window) now clears it on all of them, so the same FYI no longer needs swatting once per window. A project opened shortly after an update, while the notice is still live, also shows it.
When both a "new version ready to install" banner and the release-notes notice are armed at once, the single-card behavior is unchanged: the relaunch banner takes precedence and the release-notes notice waits behind it.