OpenKnowledge

v0.5.0

Part of the OpenKnowledge changelog.

Minor Changes

  • Desktop now reclaims project-local MCP files and SKILL files on project open, force-writes user-level SKILL files on every launch, and renames-aside corrupt MCP configs (both project and user scope) so a fresh canonical config can be written.

    Project open (new): every supported editor's project-local MCP config (.mcp.json, .cursor/mcp.json, .codex/config.toml) is checked alongside .claude/launch.json. Existing mcpServers.open-knowledge entries get rewritten to the bundled CLI; absent entries / absent files stay absent (namespace ownership). Existing project-local SKILL.md files under .claude/skills/open-knowledge/ and .cursor/skills/open-knowledge/ get replaced with the bundled version; absent files stay absent.

    Every launch (changed): user-level ~/.agents/skills/open-knowledge/ always force-writes to the bundled version, and per-host copies (~/.claude/skills/open-knowledge/, ~/.cursor/skills/open-knowledge/) force-write when the host directory exists. This replaces the prior npx skills add subprocess path that silently spawn-errored under macOS GUI launches (Dock click, LaunchServices) because npx typically isn't on the GUI process's PATH. ~/.ok/skill-state.yml now advances on every cold launch instead of getting stuck on whichever version a past terminal-invoked ok init recorded.

    Corrupt MCP configs (new): if a project-local or user-level MCP config file (.mcp.json, .cursor/mcp.json, .codex/config.toml, ~/.claude.json, etc.) is unparseable OR blank/whitespace-only, the desktop renames it to <configPath>.broken-<isoTimestamp> and writes a fresh canonical config in its slot. This unblocks recovery from accidentally-truncated configs without forfeiting the original bytes. Valid files without an open-knowledge entry are still left alone (no clobbering of unrelated tools' configs). The CLI gains a new classifyExistingMcpEntry helper exporting the discriminated { kind: 'absent' | 'no-entry' | 'present' | 'corrupt' } outcome that drives the new logic.

    .claude/launch.json force-write (changed): on every project open, the desktop unconditionally invokes scaffoldLaunchJson rather than gating on namespace ownership. scaffoldLaunchJson is already merge-aware — it creates the file when absent, adds the open-knowledge-ui configuration when missing (siblings preserved), replaces the entry when present, and gracefully handles blank/whitespace files by writing a fresh one. This brings launch.json behavior in line with the user-level SKILL force-write posture so blanking the file produces a working launch.json on next project open instead of a no-op. Only genuinely-corrupt JSON (invalid syntax) still fails — siblings the user authored into a broken file outweigh the recovery-on-corrupt case here, so no backup-and-rewrite for launch.json.

  • Packaged macOS Desktop now reclaims existing Open Knowledge integration namespaces to the installed bundle on startup/project open. Existing mcpServers.open-knowledge entries are rewritten to the bundled CLI, existing Claude Code open-knowledge-ui launch configs are rewritten to ok ui via the bundled CLI, and terminal shims are installed at ~/.ok/bin through a managed ~/.ok/env.sh shell-rc block instead of an automatic /usr/local/bin admin prompt.

    If your real interactive shell PATH includes additional writable non-system directories (for example ~/bin or ~/.local/bin), Desktop may also create or refresh OK-owned ok / open-knowledge symlinks there; foreign entries are never overwritten. If you use custom wrappers, register them under a different MCP server or launch-config name. The menu-driven “Install Command-Line Tools…” action remains available for users who explicitly want /usr/local/bin/{ok,open-knowledge} symlinks.

  • Add discover MCP tool — brownfield project convention extraction + link-graph activation.

    Joins ingest / research / consolidate as the fourth workflow-style tool that returns a multi-step instructional body with STOP gates. Where ok seed scaffolds greenfield repos with the Karpathy three-layer, discover handles brownfield: connect OK to a repo with existing markdown content and the agent extracts conventions from sibling docs, sets folder frontmatter + templates, curates .okignore, and activates the link graph (orphan triage, hub identification, untextualized + vague-referential cross-references via suggest_links and search). Seven phases with per-phase user-confirmation gates; idempotent on re-run.

    Composes existing primitives only (set_folder_rule, write_template, get_orphans, get_hubs, get_dead_links, suggest_links, edit_document, list_documents, exec, search) — no new MCP primitives, no schema changes. Phases 1-4 run fs-direct without the Hocuspocus server; Phase 5 (link-graph activation) requires the server.

    Plus seven surgical SKILL.md edits make the tool discoverable from inside the bundled Agent Skill (Workflow tools table row, anti-pattern row, three section additions, two paragraph updates). SKILL.md stays under the 40,000-char hard cap.

    Spec: specs/2026-05-13-discover-tool/SPEC.md.

  • Project creation under a directory whose parent contains .git/ now opens the parent (the git working-tree root) AND defaults the content scope to that same parent — opened folder and content.dir align by default. Previously the parent was opened but content.dir was silently scoped to the picked sub-folder, so the user landed in the git root visually while writes were narrowed beneath it.

    Surfaces the WHY in three places:

    • CLI (ok init): [ok] Initialized OK at <gitRoot> — opened parent of <cwd> because it contains a .git folder (was (scoped to <subPath>/)).
    • Desktop toast on freshly-spawned editor windows: Initialized OK at <gitRoot> — opened parent of <pickedPath> because it contains a .git folder (toast IPC payload's contentDir field replaced with pickedPath).
    • Desktop consent dialog banner: explicit "parent of <pickedPath> because it contains a .git folder (one .ok/ per git repo). The default scope is the entire repo; change Content directory below to narrow it."

    Narrowing back to the originally-picked sub-folder is still a one-edit choice — the consent dialog's Content directory field is pre-filled with . and accepts any sub-path, and config.yml's content.dir remains the post-init knob.

  • feat(mcp): ingest biases toward downloading raw binary files

    Agents calling ingest("<binary-URL>") from a shell-capable host now preserve the raw bytes of the source — PDFs, images, audio, video, Office docs, archives — under external-sources/<slug>.<ext> alongside a markdown wrapper external-sources/<slug>.md whose body is a single ![[<slug>.<ext>]] wiki-embed. Previously the tool's plan body directed agents to "use your available web fetch tool," which on every host is an LLM-mediated text extraction that loses figures, tables, layout, OCR, and embedded metadata.

    The mechanism stays planner-only: the MCP tool returns a procedural plan, and the agent executes it via its shell tool (curl -L --fail --max-redirs 5 --max-time 60 --max-filesize 104857600 -o ...). OK's file-watcher picks up the dropped binary as an asset-create event identically to a manual drop. The wiki-embed renders inline for images / video / audio and as a Notion-style File row for PDFs and opaque attachments (the pdfjs canvas viewer remains opt-in via the explicit <Pdf src="..."> JSX form).

    New STOP gates in the plan body:

    • Streaming-video / DRM media (heuristic — YouTube, Vimeo, Spotify, Twitch, anti-bot signatures) STOPs with a yt-dlp pointer rather than failing into anti-scraping.
    • Executable / scripted-document extensions (EXECUTABLE_BLOCKLIST_EXTENSIONS from @inkeep/open-knowledge-core) hard-block — the Electron openAssetSafely runtime backstop is desktop-only, so plan-level enforcement is the only line of defense in CLI / web / Cowork contexts.
    • Size pre-check via HEAD Content-Length: 100 MB hard cap (GitHub's commit-cap is the binding constraint since external-sources/ is committed to git by default), 50 MB warn tier.
    • Re-ingest with sha256 mismatch: new dated sibling (<slug>.YYYY-MM-DD.{ext,md}) with supersedes: frontmatter pointer. Old wrapper + binary untouched — append-only invariant per external-sources/** layer convention.

    Shell-less hosts (Claude.ai web; possibly Cowork — verify per host) fall back to the existing text-extraction flow and set preservation: text-only in the wrapper's frontmatter, plus prepend an admonition warning that the binary was not preserved. Downstream tooling can grep the flag to detect docs that should be re-ingested from a shell-capable client.

    Wrapper frontmatter declares the full tags: [source, immutable, layer-ingest, binary] list explicitly — the legacy .ok/config.yml folders[] cascade mechanism is no longer honored by the runtime config loader (folder defaults today live in opt-in nested <folder>/.ok/frontmatter.yml), so the wrapper does not rely on a cascade that may not exist.

    Bundled SKILL.md updated in lock-step: §Media now distinguishes free-form image-embed placement (co-located alongside the referencing doc) from ingest's external-sources/ layer-1 convention; §Workflow tools' ingest row references the binary-preservation flow; §Frontmatter conventions document the binary-source wrapper shape.

    Full SPEC + decision log: specs/2026-05-19-ingest-prefer-binary-downloads/SPEC.md. Architecture stays planner-only (NG1) — server-side fetch is documented as a future-work relief valve in §15 if dogfood shows the shell-less degraded path is a meaningful product gap.

  • BREAKING: Mermaid diagrams are now authored exclusively as ```mermaid fenced code blocks. Existing <Mermaid chart="…" /> JSX must be manually converted to a mermaid fence — legacy JSX will display as an editable source block instead of a rendered diagram.

    To migrate: search your content for <Mermaid chart= and rewrite each occurrence as a ```mermaid fence. Untouched legacy content remains visible and editable as an unknown-component source block, but no longer renders as a diagram until converted.

    Mechanism: the canonical descriptor was renamed MermaidMermaidFence so legacy <Mermaid /> JSX no longer matches at parse time and falls through to the unknown-component editable source block (no silent migration on save). The id and theme props were dropped (neither is expressible in fence syntax). Slash menu and PropPanel continue to show "Mermaid" via displayName.

  • feat(ok-mcp-additions): three additive MCP tool surface changes plus a preview-url correction. Closes feedback-driven gaps from the 2026-05-15-ok-feedback-gaps parent spec: per-key frontmatter editing via JSON Merge Patch, build-time generation of the MCP instructions echo from canonical SKILL.md, and workflow-tool DESCRIPTIONs that lead with the "returns procedural guidance, not data" contract. The fix-up commit also drops a misleading openknowledge:// deep-link short-circuit from the MCP previewUrl field that external in-app browsers cannot render.

    • @inkeep/open-knowledge-core — new FrontmatterPatchRequestSchema + FrontmatterPatchSuccessSchema in packages/core/src/schemas/api/agent-write.ts; new urn:ok:error:invalid-frontmatter-patch code in packages/core/src/schemas/api/_envelope.ts. The patch request schema enforces RFC 7396 JSON Merge Patch semantics ({key: value} sets, {key: null} deletes); per-key values flow through the existing FrontmatterValueSchema so atomic rejection with per-key fieldErrors is consistent with the rest of the agent-write surface.

    • @inkeep/open-knowledge-server — three MCP tool surface changes plus a generated artifact:

      • frontmatter_patch (revived, packages/server/src/mcp/tools/frontmatter-patch.ts) — previously parked, now re-enabled with a server-side CRDT path in api-extension.ts. Opens a DirectConnection, applies JSON Merge Patch logic inside one dc.document.transact(fn, session.origin) block (mirrors applyAgentMarkdownWriteInner at agent-sessions.ts:124-184). Atomic rejection on schema failure via editError capture; routes through composeAndWriteRawBody (paired-write enforcement). Concurrent patches to different keys merge via Y.js field-level CRDT; concurrent patches to the same key resolve last-writer-wins per Y.js semantics.
      • Workflow-tool DESCRIPTION rewrites for ingest, research, consolidate, discover — each now leads with "Returns a multi-step plan for ..." framing to fix the cross-round naming-expectation pain (F1, C4, E7 in the parent spec's feedback rounds). Tool names unchanged (DD-5 LOCK B).
      • Build-time generation of instructions.ts — new packages/server/scripts/generate-instructions.ts extracts 4 H2 sections from canonical SKILL.md by exact heading-text match (STOP, Reads-examples, Preview, Scope recap); concatenates with one-line identity prefix and pointer suffix. instructions.ts is now a generated artifact; bun run check gates drift via scripts/check-instructions-drift.sh. Eliminates dual-source-of-truth drift between SKILL.md and the MCP instructions echo.
      • previewUrl resolution simplified (packages/server/src/mcp/tools/preview-url.ts) — OK_ELECTRON_PROTOCOL_HOST short-circuit dropped from the resolver. previewUrl is now always http://localhost:<ui-port>/... or null; the openknowledge:// URL scheme stays load-bearing for OS-level deep-linking (URL-scheme handler, dock drag, sidebar pills, second-instance argv) but is no longer emitted as an MCP previewUrl — external agent in-app browsers (Claude Desktop, Cursor, Codex) cannot render custom URL schemes. Under OK Electron the resolver currently returns null until a follow-on spec exposes the React shell over HTTP from the utility process; once that lands, the lock branch fires universally and previewUrl is non-null for every running topology.
    • @inkeep/open-knowledge — the CLI's bundled MCP server picks up all three tool changes via the existing registerAllTools shim in packages/cli/src/mcp/tools/index.ts. No CLI subcommand changes.

    Full parent spec + decision log: specs/2026-05-15-ok-feedback-gaps/SPEC.md. Sub-spec B: specs/2026-05-15-ok-mcp-additions/SPEC.md.

  • Remove the user-global template tier (~/.ok/templates/) — breaking schema changes.

    Templates now live exclusively at the project + folder tier (<folder>/.ok/templates/<name>.md); the user-global cascade hop is gone. Five breaking changes ship together:

    • MCP write_template / delete_template lose target. The InputSchema is now .strict()-wrapped, so callers passing any target value (including 'project') — or any field not in the schema — get a Zod parse error citing the unknown key instead of a silent drop. Update LLM agents and automation that previously sent target: 'user' or target: 'project' to drop the field entirely; the project-tier path is now the only path.
    • HTTP PUT /api/template rejects legacy target body field. The request schema is .strict(); sending { ..., target: 'user' } returns 400 RFC 9457 with the field named in detail. GET and DELETE accept ?target=... as an unknown query and silently ignore it — same as any other unknown query param — and serve the project-tier resource.
    • Include personal templates checkbox removed from the in-app SeedDialog; ok seed --personal-templates flag removed from the CLI. Commander rejects legacy invocations with an unknown-option error.
    • Personal-templates pack content deleted entirely from source. The 8 templates (daily-journal, meeting-notes, weekly-review, reading-log-entry, gym-log, recipe, travel-trip, morning-pages) and the surrounding plan/write infrastructure are gone. No in-tree stash — recover from git history at the previous beta tag if you ever need the strings.
    • Desktop-bridge contract narrows. OkScaffoldPlan.personalTemplates, OkSeedApplyResult.personalTemplates, OkSeedPlanOptions.includePersonalTemplates, and OkSeedApplyOptions.includePersonalTemplates are removed across packages/core, packages/app, and packages/desktop.

    Existing ~/.ok/templates/* files become inert (no read path references them; no migration ships). Move any templates you want to keep into a project's .ok/templates/.

    Locality principle behind the removal: see PRECEDENTS.md #50 (clone-behavior test).

Patch Changes

  • Hide the View → Reload / Force Reload / Toggle Developer Tools cluster in stable desktop builds. Beta DMGs keep the cluster (they're built from the same legacy commit with version X.Y.Z-beta.N) so we can still debug shipped beta; stable builds (where promotion overrides the version to plain X.Y.Z via --config.extraMetadata.version) no longer expose the dev tools entry points. Dev (bun run dev) is unaffected.

  • Navigator window polish:

    • Set minimum window dimensions on the editor and navigator (Switch Project) windows. Previously both BrowserWindow constructors omitted minWidth / minHeight, so Electron applied its 0 default and users could drag the windows to almost nothing. Editor clamps at 720 × 480; navigator clamps at 640 × 560.
    • Retune the navigator's default size from 800 × 750 (aspect 1.07, awkwardly square) to 840 × 600 (aspect 1.40, list-friendly landscape) — better matches the category convention (Xcode, VS Code, IntelliJ all landscape) and gives the recent-projects list a more proportional shape.
    • Fix the empty / first-launch state: with zero recents, the chrome row (header) and content row (3 cards) now center vertically as a single group instead of leaving the header pinned to the top with the cards floating in a void below it. Trade-off: in the empty state, the macOS title-bar drag region moves with the chrome row to its centered position; users drag the window from the centered header rather than the top edge. Traffic-light buttons remain functional. The 4 chrome-row drag invariants pinned in NavigatorApp.test.ts are preserved.

    Initial editor size (1280 × 800) is unchanged.

  • ok diagnose process <pid> now explains why a pid is rejected (Invalid pid '<value>': must be a positive integer) instead of the bare Invalid pid: <value> — matches the format of the sibling --cpu-profile validation message in the same command.

  • fix(desktop): treat ERR_UPDATER_CHANNEL_FILE_NOT_FOUND as "no update available" instead of an alarming 404 dialog

    A beta build's menu-driven "Check for Updates…" could surface a multi-paragraph HTTP-404 dump (Cannot find latest-mac.yml in the latest release artifacts (…/v0.5.0-beta.22/latest-mac.yml): HttpError: 404 …) when the latest prerelease tag existed but its DMG + channel manifest hadn't been uploaded yet. release.yml creates the prerelease GitHub Release first; desktop-release.yml runs second and takes ~8 min to build, sign, notarize, and upload beta-mac.yml + the DMG. During that race window the release appears in releases.atom (so electron-updater picks it up as the latest matching tag) but the channel manifest 404s. electron-updater's GitHubProvider also falls back from beta-mac.ymllatest-mac.yml when allowPrerelease=true, so the final user-visible URL names latest-mac.yml even on the beta channel.

    onError in auto-updater.ts now special-cases err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' on menu-driven checks: the friendly "You're on the latest version" dialog fires instead, and the classified-warn log still records the code + URL for operator triage. The next periodic check (default ~5 min) re-fetches and surfaces the genuine update once the manifest lands. Other classified codes (HTTP_ERROR_*, ERR_UPDATER_ZIP_FILE_NOT_FOUND, ERR_UPDATER_INVALID_UPDATE_INFO, …) still surface as error dialogs — they describe real failures, not a transient empty-release state.

  • Fix Claude Code preview pane breaking when port 3000 is occupied by an unrelated dev server. ok ui now defaults to binding port 39847 with kernel-allocated fallback on EADDRINUSE, and ok init writes port 39848 plus autoPort: true to .claude/launch.json. The deliberate gap between the two ports routes Claude Code's preview spawn through the lock-collision proxy mode (which empirically works) rather than the same-port "already-running" exit-0 path (which empirically fails the preview pane).

  • fix(desktop): clear stale versionPendingInstall on boot so the relaunch toast doesn't reappear for the version you're already running

    The auto-updater writes AppState.versionPendingInstall when an update finishes downloading and clears it in exactly one place — the ok:update:relaunch-now IPC handler invoked by Toast A's "Relaunch" button. The other install path (autoInstallOnAppQuit = true, which applies the staged update when the user quits the app normally) never touched the field, so the next launch left the app running on the new version with a stale pointer to it. main/index.ts's browser-window-created re-broadcast then surfaced that stale value as a phantom "Version X ready to install" toast for the version the app was already running.

    The boot path now compares app.getVersion() against versionPendingInstall via a small versionAtLeast MMP helper; when the running version has caught up, the field is cleared and the persisted state advances. Malformed inputs fall through to "don't clear" so a parse failure can't drop a genuinely-pending update on garbage.

  • Fix Open Knowledge.app failing to open projects on macOS 26.4.x (Tahoe).

    On macOS 26.4.x, Apple tightened AMFI enforcement of restricted entitlements on Electron helper processes. Helper binaries (Renderer / GPU / Plugin / utilityProcess.fork children) do not carry an embedded provisioning profile — only the main app does. With the prior config sharing one entitlements file across both, helpers claimed com.apple.developer.associated-domains (profile-restricted) without a profile and macOS SIGKILLed them at launch. Parent process saw exit_code=9. Symptom on every project open:

    Unable to open project — utility exited before ready (code=9)

    …plus repeated GPU process exited unexpectedly: exit_code=9 and eventually FATAL: GPU process isn't usable. Goodbye. in stderr. Sibling Electron 41.2.1 apps (Granola, Linear, Notion) were unaffected because their helper plists already followed the standard Electron split.

    This patch splits the entitlements per Electron's canonical pattern:

    • packages/desktop/build/entitlements.mac.plist (unchanged) — main-app entitlements (restricted associated-domains + file-access).
    • packages/desktop/build/entitlements.mac.inherit.plist (new) — helper- only: JIT trio + cs.allow-dyld-environment-variables + inherit.
    • packages/desktop/electron-builder.yml mac.entitlementsInherit now points at the new file.

    A contract test (tests/unit/entitlements-helper-split.test.ts) pins forbidden-keys exclusion + required-keys inclusion + allowlist + symmetric YAML wiring so the shape can't drift again.

    Users on the prior installed build (0.5.0-beta.31 or earlier) on macOS 26.4.x will pick up the fix in the next signed beta DMG. The installed package cannot be patched in place — entitlements are part of the code signature.

  • fix(open-knowledge): mdast→PM dispatcher no longer corrupts documents to literal type-name text when a schema is missing an extension.

    The mdast→PM dispatcher's inlineUnknownHandler fallback emitted node.type (an internal mdast identifier — e.g. "footnoteReference") as a plain text node whenever it ran without a matching schema entry. The emitted text concatenated against neighbouring prose, turning Hello[^1]. into HellofootnoteReference. on disk — indistinguishable from authored content, and re-saved verbatim by every subsequent serialize pass.

    Defense lives at three layers:

    1. inlineUnknownHandler never emits a bare mdast type name as text. When node.value is present (text-shaped inline mdast like inlineCode), use it. Otherwise emit «unknown:${node.type}» — characters bracketed by «…» so the failure mode is visually auditable instead of looking like prose. Any future unhandled inline type degrades to a recognisable sentinel rather than silent corruption.

    2. footnoteReference schema-drift fallback recovers the markdown source. When the FootnoteReference PM extension is missing from the schema, the dispatcher now reconstructs the [^${identifier}] source form as text so the bytes round-trip byte-stable. A drifted surface produces a document that re-parses correctly on the next non-drift surface instead of irreversibly losing the reference.

    3. footnoteDefinition schema-drift fallback surfaces a visible marker. Pre-fix the library's built-in ignore list silently dropped the entire footnoteDefinition node when the matching PM extension was absent — the user lost the body and the marker with no warning. The dispatcher now emits a [^${identifier}]: paragraph so the user can see the definition existed and re-author the body. (Body content is intentionally lossy under drift — children are arbitrary blocks and a faithful reconstruction would re-enter the broken dispatch; surfacing the marker beats silent drop.)

    Adds five regression tests in footnote.precision.test.ts covering the drift surface explicitly: stripped extensions, reference-without-corruption, byte-stable round-trip, definition marker visibility, and a class-level guard that the bare mdast type name never appears as text content for any input.

  • Fix client-side schema validation failures on set_folder_rule, discover, and six sibling MCP tools.

    Strict-validation MCP clients (Claude Code, Claude Desktop) rejected every call to set_folder_rule, discover, write_template, delete_template, get_config, get_components, grep, and search with Structured content does not match the tool's output schema: data must NOT have additional properties. Root cause: textPlusStructured auto-injects a _text field into structuredContent as a client-quirk workaround (Claude Code hides content[] when structuredContent is present and the visible body is mirrored under _text), but no tool's outputSchema declared _text, so the SDK-emitted JSON-schema (additionalProperties: false) rejected it under AJV.

    Fix: single-source outputSchemaWithText(shape) helper that lays down _text: z.string().optional() alongside the caller's shape. Applied to all eight tools that combine registerTool with textPlusStructured. Caller can override _text (mirrors the runtime escape hatch in textPlusStructured).

  • Fix MCP tool input-validation errors to name the missing field and list allowed values (PRD-6659). Calling write_document (or any other MCP tool with a required enum/string argument) without the required argument used to surface as a bare Required — or, on Zod v4, a multi-line JSON dump of the issues array — with no obvious way for an agent to see which field was missing or what values were valid. A new installPrettyZodErrors patch on McpServer re-formats validation failures with z.prettifyError, so a missing position now renders as ✖ Invalid option: expected one of "append"|"prepend"|"replace" → at position. Applies uniformly across every tool with a Zod input schema; saves one round-trip on every agent's first tool call.

  • fix(cli/mcp): detect bundle drag-replace from inside ok mcp children and shut down cleanly so external hosts respawn

    When a user drag-replaces /Applications/Open Knowledge.app to install a new DMG, any external MCP host process (Claude Desktop, Cursor, Codex, Windsurf, VS Code, …) that has already spawned ok mcp children keeps those children running off the previous bundle's executable mappings until the children are explicitly killed (or until each host happens to restart its own MCP transport). The host keeps talking to the previous version's code path; ok --version in a terminal can report the new version while MCP-mediated edits still run on the old version. There is no user-visible signal that this divergence exists.

    This is the cli-side sibling of the desktop-side drag-replace fix — see bundle-replace-detector.ts for the OK desktop app's About-dialog / NSBundle.mainBundle-cache equivalent. The two fixes close the same root cause (drag-replace mutates the inode while live processes hold the previous one open) from opposite ends.

    The cli now exposes a darwin-gated startBundleIdentityWatcher (5-min setInterval, edge-triggered, single-shot disarm) wired in to startGlobalMcpServer. Boot-time identity is captured via fileURLToPath(import.meta.url) — not process.execPath, which is Bun's binary outside the bundle — and the watcher periodically stats the captured path. When the inode changes vs the captured value, the child logs a structured bundle-moved breadcrumb and triggers idempotent shutdown() with a 5-second unref()'d deadline. The host's transport sees EOF, recycles, and respawns a fresh child off the new bundle on its next call.

    Detection is single-shot per session (re-arming would prompt twice when the user drag-replaces over an already-replaced binary); idempotent shutdown (the deadline kills the process even if the MCP server's own close path stalls). No behavior change on non-darwin platforms — Windows/Linux desktop parity is deferred per packages/desktop/README.md.

  • fix(open-knowledge): serve SPA-bundle fonts/sprites/PNGs from dist/ under ok ui instead of 404'ing through the content-asset middleware.

    ok ui runs the content-asset middleware before the SPA static handler. The middleware's fail-closed branch refuses to fall through for any path whose extension is in ASSET_EXTENSIONS (woff/woff2/png/svg/jpg/…) when the file is absent under contentDir — so every Vite-hashed asset Vite emits at /assets/<hash>.<ext> got intercepted before sirv could serve it from packages/cli/dist/public/assets/. Fonts silently fell back to ui-sans-serif / system-ui; bundled icon SVGs and PNGs also 404'd.

    ui.ts now routes /assets/<file> to the static handler first. If sirv misses (the SPA bundle doesn't ship that file), the request falls through to the content-asset middleware so user uploads under <contentDir>/assets/foo.png still serve with the same Content-Disposition policy and fail-closed 404 guard as before. The STOP-rule fail-closed behavior is preserved for asset paths outside /assets/ and for /assets/<missing-file> (sirv miss → middleware miss → 404, not SPA shell).

    Electron + ok start + the Vite dev plugin are unaffected: they don't go through ui.ts's dispatcher, and the renderer in Electron loads SPA assets via file://, not HTTP.

    Two regression tests in packages/cli/src/commands/ui.test.ts:

    • a woff2 from packages/app/dist/assets/ serves 200 with no Content-Disposition (proving the static handler responded)
    • a user-staged PNG at <contentDir>/assets/user-upload.png still serves 200 with Content-Disposition: inline + X-Content-Type- Options: nosniff (proving the content middleware fall-through fires)
  • fix(desktop): deliver post-update Toast B on auto-update relaunch

    The "Updated to Version X" sidebar notice silently dropped on every auto-update relaunch. State.json confirmed the boot logic ran correctly (lastSeenVersion advanced, persistSafely succeeded), but the IPC never reached the renderer.

    Root cause is a race in whenRendererReady's tryFire helper in packages/desktop/src/main/index.ts. On boot, bootAutoUpdater runs before wm.createProjectWindow has constructed the editor window (the project-open path is fire-and-forget with several awaits before the window is created), so whenRendererReady registers app.once('browser-window-created', tryFire). Electron emits that event synchronously inside new BrowserWindow(opts), four lines before await window.loadURL(...) runs. At that instant webContents.isLoading() returns false because no load has been initiated yet, so tryFire interprets it as "already loaded" and fires the broadcast immediately — against a renderer whose main.tsx hasn't run and whose installUpdateNoticesBridge() subscriber isn't attached. Electron drops main→renderer IPC sent to an unloaded page.

    tryFire now treats webContents.getURL() === '' as "never navigated" alongside isLoading(), so a fresh window emerging from browser-window-created waits for did-finish-load before the broadcast fires. Affects all three boot-routed toasts (A/B/C) routed through whenRendererReady, but Toast B was the only one that hit the race deterministically (A's update-downloaded arrives after a network roundtrip when the editor is loaded; C's stuck-hint fires after an initial-check grace period).

  • fix(rename): preserve document body when renaming a file with the editor open

    Renaming a file (sidebar or MCP) while an editor was connected to it could wipe the document body: the editor and every later reader saw an empty document even though the original content was intact on disk.

    The managed-rename spine emitted the connection-close that forces connected clients to reconnect — and be redirected to the new name — before it had moved the file on disk. The redirected client's load ran against a not-yet-existent destination file, leaving an empty in-memory document that nothing re-populated once the move landed. The disk move now completes before the close, so the redirected client always loads the renamed file's real content.

  • fix(cli/auth): plug three token-leak paths in the share-link flow

    Three independent bugs in the CLI's auth handling all surfaced from the share-link flow:

    1. Cloning a share link to a public GitHub repo failed with "Repository not found" whenever the recipient's stored token was a fine-grained PAT or org-restricted token. The credential helper unconditionally injected that token, and GitHub returns 404 for any repo outside the token's scope. The clone command now probes the GitHub API anonymously first; on success, the credential helper is skipped entirely so the request goes through as a normal public read.

    2. ok auth signout could leave a plaintext token at ~/.ok/auth.yml. Storage selection (keyring vs file) is decided per-run by probing the @napi-rs/keyring native binding, and the probe result can flip between sessions. When a token was written under the file fallback and signout ran later under the keyring backend, signout only cleared the empty keychain and never touched the file. Signout now clears every backend it knows about, regardless of which one resolves this run. It does not create an empty auth.yml for keychain-only users.

    3. Users authenticated via the gh CLI were still prompted for device-auth login when running the desktop app on macOS. macOS GUI launches inherit only the launchd PATH (/usr/bin:/bin:/usr/sbin:/sbin), so gh installed under /opt/homebrew/bin was invisible to subprocess spawns and the auth cascade fell through to "no token". detectGh now falls back to a list of known install locations (Homebrew Apple Silicon and Intel, MacPorts, Linux snap, distro packages) when the bare PATH lookup fails.

  • fix(desktop): derive the auto-update channel from the build version, not a runtime preference (fix-update-channel-from-build)

    The desktop app's update channel is now sourced from app.getVersion()0.4.0-beta.36 → beta feed, 0.4.0 → stable feed — so the feed electron-updater checks (and therefore the in-app "release notification" / "What's New" notice) always matches the binary the user installed. Previously a persisted AppState.updateChannel could diverge from the build (e.g. a beta install with updateChannel: 'latest'), causing a beta build to surface stable-channel releases.

    Removed:

    • the Settings → Update channel switcher (ChannelSection) and the useUpdateChannel setter
    • the persisted AppState.updateChannel key — existing state.json files carrying it are migrated by silently dropping it on first read
    • the ok:update:set-channel / ok:update:confirm-downgrade IPC channels and the ok:state:update-channel-changed event, plus the beta→stable downgrade-warning notice (it only ever fired for runtime channel switches)

    state.query() still reports the channel (now build-derived) so the BETA badge and About-panel label keep working.

  • Fix git identity writes from linked worktrees no longer silently mutating the main checkout. POST /api/local-op/auth/set-identity now detects when it's running in a linked git worktree and writes per-checkout via --worktree (enabling extensions.worktreeConfig once, idempotently); main checkouts retain the existing --local behavior. resolveGitIdentity reads the per-worktree scope first, so each linked worktree can carry its own committer identity.

  • ok init now writes .claude/launch.json's open-knowledge-ui entry as {runtimeExecutable: 'npx', runtimeArgs: ['-y', '@inkeep/open-knowledge@latest', 'ui']} instead of the bare 2-arg form. ok start sweeps the project's launch.json at boot and forward-migrates any pre-existing bare-npx entries to the new canonical shape.

    Closes the same silent-downgrade hole as the MCP-canonical pin: the bare-name form routed through npm's range-spec resolver could silently downgrade Claude Code Desktop's preview-pane spawn (preview_start("open-knowledge-ui")) to a years-stale release on older Node versions. Pinning @latest makes the resolver use the literal latest dist-tag and surfaces a Node-version mismatch as a loud EBADENGINE.

    The startup sweep only rewrites bare-managed entries. Version-pinned (@beta, @x.y.z), dev-mode (node /path/cli.mjs ui), foreign-customized, and unrelated configurations co-located in the same file are preserved.

  • ok init now writes MCP host configs as {command: 'npx', args: ['-y', '@inkeep/open-knowledge@latest', 'mcp']} instead of the bare 2-arg form. ok start sweeps user-level and project-level configs at boot and forward-migrates any pre-existing bare-npx entries to the new canonical shape.

    The bare-name form (['@inkeep/open-knowledge', 'mcp']) routes through npm's range-spec resolver, which sorts by engine compatibility and can silently downgrade users on older Node versions to years-stale releases. Pinning @latest makes the registry resolve the literal latest dist-tag and lets a Node-version mismatch surface as a loud EBADENGINE instead.

    The startup sweep only rewrites bare-managed entries. Version-pinned (@beta, @x.y.z), dev-mode (node /path/cli.mjs), desktop-bundled (/.../ok.sh), and custom-command entries are preserved.

  • fix(open-knowledge): MCP tool responses no longer drop their visible body on Claude Code and Claude Desktop.

    Claude Code and Claude Desktop hide the content text stream of an MCP tool result whenever structuredContent is present — the agent sees only the structured payload and the visible body silently vanishes. The shared textPlusStructured helper used by 26 OK MCP tools (read_document, ingest, research, consolidate, search, grep, the get_* tools, every write/edit/rename/rollback handler, etc.) previously put only typed metadata (e.g. { previewUrl: null }) into structuredContent, so on those clients read_document(...) returned {previewUrl: null} with the file body gone and ingest / research / consolidate returned {previewUrl: null} with the workflow prompt gone.

    The helper now mirrors the visible body under structuredContent._text, so the body survives the client quirk for every caller of the helper in one place. The underscore prefix marks the field as internal / auto-generated (matching MCP spec's _meta convention) so existing exact-match tests on structuredContent shape don't trip on the extra key. exec.ts already had its own per-tool duplication for the same reason (structuredContent.stdout); that field stays because it carries the raw command stdout without banners or enrichment — a semantically distinct slice, not a duplicate of the visible content.

    Same change also refactors the three workflow tools (ingest, research, consolidate) to share a single buildWorkflowHandler instead of three near-identical per-file register() bodies, and hoists their byte-identical *Deps interfaces into one WorkflowToolDeps type. Net: 60+ lines of duplicated registration boilerplate collapse into one shared helper, and the { previewUrl: null } workflow-tool contract is now encoded in one place.

    No behavior change on clients that respect both channels — they continue to see the same content array plus the same structured fields, with one extra _text mirror.

  • Fix collab WebSocket connection from ok ui so the React shell can reach Hocuspocus from inside sandboxed agent preview panes (Claude Code, etc.).

    Before this change, /api/config advertised the collab WebSocket as ws://localhost:<collab-port>/collab — a different port from where ok ui served the React shell. The browser opened the WebSocket directly to the collab server. That works in a normal browser tab but fails in agent preview panes that sandbox the renderer to one URL/port (the editor pane shows "Connection dropped — Lost connection to '<doc>'" for every .md / .mdx file).

    Now:

    • /api/config advertises the WebSocket on the same origin the shell loaded from (derived from the request's Host header, with the existing loopback allowlist).
    • ok ui registers a /collab upgrade handler that forwards the WebSocket to the collab server's port (read from server.lock) via raw TCP. Connection / Upgrade / Sec-WebSocket-* headers are preserved verbatim so the upstream Hocuspocus can complete the handshake.

    Result: the same URL serves shell + WebSocket. Works in regular browsers (unchanged behavior — same-origin always succeeds) AND in sandboxed preview panes (the new working case).

  • Forward WebSocket upgrades through ok ui's proxy mode so the editor works in Claude Code's preview pane when OK Desktop is open for the same project.

    When OK Desktop holds ui.lock for a project and Claude Code spawns ok ui for the same project (via its launch.json), ok ui falls into proxy mode — binds the requested port (LAUNCH_JSON_PORT = 39848) and forwards HTTP to the Desktop utility's port. Before this change, the proxy forwarded HTTP fine (file tree rendered) but had no httpServer.on('upgrade') listener, so the per-document WebSocket upgrade was dropped — Claude Code's preview pane showed "Connection dropped" for every .md / .mdx file.

    The proxy now wires the same proxyUpgrade helper that ok ui's main HTTP server uses. The upstream (Desktop utility) handles /collab upgrades natively via Hocuspocus. Live upgrade-pipe sockets are tracked so close() can drain them on shutdown (httpServer.close() does not track upgrade-detached sockets).

    Closes the Desktop + Claude Code-on-the-same-project scenario, completing the WS-upgrade fix landed in the previous PR (which covered the direct-bind path but explicitly left proxy mode out of scope).

  • Sidebar context menus + Open with AI dispatch unification + macOS File/View menu refresh.

    The Open Knowledge editor's file sidebar gets three coherent right-click menus (empty-space, folder, file) with consistent ordering, and the macOS application menu picks up the same affordances in keyboard-reachable form. The Open with AI handoff (previously file-row-only) now dispatches at file / folder / project scope from every surface — right-click menus, the sparkle icon in the editor header, and the new File menu items.

    • Three sidebar right-click menus. Empty-space (new surface — right-click anywhere in the sidebar, not just on rows), folder, and file menus share a five-section ordering: create → act → filter → tree state → destructive. Net additions include Open in Terminal at every scope, Show Hidden Files (Cmd+Shift+.) / Show all files project-local visibility toggles, and Open with AI on folder rows + empty space (previously file-only).
    • Open with AI dispatches at every scope. Right-click menus, the editor-header sparkle icon, and the macOS File menu all use the new label. The dispatch payload is a short text prompt asking the agent to open the target in Open Knowledge's web preview — replacing the prior file-attachment / cwd-set semantics. Three prompt templates: file-scope (Can you open <path> in web view with open knowledge editor.), folder-scope (Let's work on `<folder>` folder using Open Knowledge. Open the OK editor in web view.), project-scope (Let's work on this project using Open Knowledge. Open the OK editor in web view.). Cursor's two-step spawn, Claude's claude.ai web fallback (file scope only), and per-agent install detection are all preserved.
    • Sparkle icon dispatches at 3 scopes. The editor-header sparkle picks the dispatch scope from the active editor target — file when a doc is open, folder when a folder overview is the active view, project when nothing is active. Previously the sparkle only rendered when a doc was open.
    • Delete now uses the system Trash. Recoverable from Finder Trash, with a VS Code-parity confirm modal (Are you sure you want to delete '<name>'? / Move to Trash / Cancel) and a secondary fallback modal (Couldn't move to Trash / <target context> Do you want to permanently delete instead? / [Cancel] [Retry] [Delete Permanently]) when shell.trashItem fails (locked file, network drive without Trash, OneDrive quirks). Multi-target failures aggregate into one modal listing every failed path. Replaces today's hard unlinkSync / rmSync for UI-driven deletes; agent and non-UI HTTP callers continue to use the hard-delete path.
    • Open in Terminal. New menu item across all three sidebar menus and the macOS File menu. Spawns open -a Terminal.app <dir> (argv array, shell: false, 2 s timeout, project-containment check). Targets are scope-appropriate: folder → folder, file → parent dir, empty-space → project root, asset row → parent dir. Terminal.app is hardcoded for v1; user-configurable terminal preference is on the roadmap.
    • Show Hidden Files / Show all files. Two project-local visibility toggles persisted in .ok/local/config.yml under appearance.sidebar.{showHiddenFiles, showAllFiles}. Show Hidden Files (bound to Cmd+Shift+. matching Finder convention) bypasses the client's dot-segment drop (recovers allowed dotfiles like brain/.archived/note.md); server filters still apply. Show all files bypasses every filter — client AND server — and surfaces every file on disk under the content directory except synthetic system docs and a curated BUILTIN_SKIP_DIRS set (.git, .ok, node_modules, framework caches, build output, OS dirs). Mirrored as checkbox items in the macOS View menu so flipping either surface keeps the sidebar and menu bar in sync via the existing CRDT subscription.
    • macOS File menu gets a state-aware item-management section below the project navigation items: New File (Cmd+N), New Folder (Cmd+Shift+N), New from Template…, Rename, Move to Trash (Cmd+Delete), Reveal in Finder, Open in Terminal, Open with AI ▸, Copy Path ▸. Enable / disable per the active editor target. Hide-this-file / Hide-folder stay sidebar-only.
    • macOS View menu gets Show Hidden Files (Cmd+Shift+.), Show All Files, Expand All, Collapse All before the Zoom and Fullscreen items.
    • Accelerator rebinds. Cmd+N = New File (added). Cmd+Shift+N = New Folder (was Switch Project…). Cmd+Delete = Move to Trash (matches Finder / VS Code). Cmd+Shift+P = Switch Project… (new home for the rebound accelerator). Cmd+Shift+. = Show Hidden Files (matches Finder). Cmd+O (Open Folder) and Cmd+, (Settings…) unchanged.

    1-way door — Cmd+Shift+N muscle memory. Existing users see Cmd+Shift+N invoke New Folder instead of Switch Project…; Switch Project is now Cmd+Shift+P. Aligns with macOS HIG defaults, Finder, and VS Code.

    Install-state detection — capability-tier. Open with AI install detection (useInstalledAgents() + GET /api/installed-agents) now applies the per-scheme OS probe only when the browser and server share a host (localhost / 127.0.0.1 / [::1]). On remote-hosted setups (the server's machine differs from the user's browser machine), every agent renders and the browser's OS protocol-dispatch dialog ("Open Cursor?") becomes the truth signal — no silent failure from probing the wrong filesystem.

    New IPC channels. ok:shell:trash-item (Trash via shell.trashItem with reason-union failure shape), ok:shell:open-in-terminal (Terminal.app spawn with argv allowlist + project containment), ok:editor:active-target-changed (renderer → main, drives File menu enable / disable), ok:sidebar:expand-all / ok:sidebar:collapse-all (main → renderer, View menu fan-out). Web mode hides the shell-IPC items entirely (no shell.trashItem / process-spawn in browsers).

    New HTTP endpoint. POST /api/trash/cleanup { kind, path } — runs captureAndCloseDocuments + recentlyRemovedDocs.setDeleted + index update + CC1 emit after the renderer's IPC trash step succeeds. Preserves extractActorIdentity attribution at the new entry point.

    New config fields. appearance.sidebar.showHiddenFiles and appearance.sidebar.showAllFiles (both boolean, default false, project-local scope).

    Legacy directory cleanup. Dropped backwards-compat scanner entries for the pre-rename .open-knowledge / .openknowledge per-project state dirs (greenfield — no users to preserve compat for). Both legacy names added to BUILTIN_SKIP_DIRS so any disk residue from older OK installs stays out of the sidebar.

    Spec: specs/2026-05-16-sidebar-context-menus/SPEC.md.

  • Tighten the bundled Open Knowledge Agent Skill: eleven prose edits to packages/server/assets/skills/open-knowledge/SKILL.md that close cross-round agent-feedback gaps without changing any MCP behavior.

    • New ## TL;DR — the 90% case section between the skill-version note and the STOP block names the four daily-driver moves (Reads / Writes / Preview / Workflow tools) so agents make a correct first write_document call after reading <300 words instead of internalizing the whole skill.
    • Promote the "workflow tools return procedural guidance, not data" framing from a buried trailing line into the section opener — closes the naming-expectation gap where ingest("url") reads as "do the ingest" but returns a multi-step plan.
    • New frontmatter-editing paragraph explains that edit_document rejects frontmatter-intersecting patches with HTTP 400 and routes agents to write_document({ position: "replace" }). Matching row added to the anti-pattern table.
    • Stale-MCP failure-mode signpost names the "Text not found on text that demonstrably exists" symptom and routes agents to the Open Knowledge MCP unavailable: escape hatch instead of silent retry loops.
    • Per-tool autonomy gates (research scoping, consolidate decision-confirmation) clarified to NOT be overridden by session-level "work without stopping" hints.
    • Workflow-tools 2nd+ invocation skim guidance — agents can skim repeat instructional bodies in-session without re-internalizing.
    • ls -la recommended over plain ls for directory listings — OK projects carry dot-prefixed entries (.ok/, nested .ok/, .okignore) that plain ls omits.
    • User-scope template noise — skip scope: "user" entries whose title/description don't match the folder's purpose (e.g. daily-journal in wiki/).
    • ok seed version-floor caveat appended to the skill-version note — heads off the first-time-user wall when an older @inkeep/open-knowledge (< 0.4.0) is globally installed.
    • Seed-pack wiki-link legacy note — seed-pack templates may still emit [[Page]] placeholders; agents are told to replace them with standard markdown links during the {shape}-fill pass.
    • Description-line cleanup — the trailing "Authoritative — MCP server instructions overlap…" sentence (agent-routing meta-commentary that was surfacing in Cowork's permission-preview chip) is dropped from the frontmatter description and relocated to a body callout under the H1 tagline.

    Spec: specs/2026-05-15-ok-skill-content-cleanup/SPEC.md. No MCP, CLI, or server-runtime changes — the only artifact that changed is the bundled SKILL.md.

  • fix(config): rename user-global config from ~/.ok/config.yml to ~/.ok/global.yml

    The user-global config now lives at ~/.ok/global.yml. The project marker stays .ok/config.yml, so the two are no longer name-twins — the ancestor-walk that detects an OK project can never treat the user's home directory as a project root, and users editing one file can't confuse it with the other.

    The CRDT doc name __user__/config.yml is intentionally unchanged — it's an opaque internal identifier and renaming it would be a wire-level change for connected clients.

0.5.0-beta.5

Patch Changes

  • fix(desktop): clear stale versionPendingInstall on boot so the relaunch toast doesn't reappear for the version you're already running

    The auto-updater writes AppState.versionPendingInstall when an update finishes downloading and clears it in exactly one place — the ok:update:relaunch-now IPC handler invoked by Toast A's "Relaunch" button. The other install path (autoInstallOnAppQuit = true, which applies the staged update when the user quits the app normally) never touched the field, so the next launch left the app running on the new version with a stale pointer to it. main/index.ts's browser-window-created re-broadcast then surfaced that stale value as a phantom "Version X ready to install" toast for the version the app was already running.

    The boot path now compares app.getVersion() against versionPendingInstall via a small versionAtLeast MMP helper; when the running version has caught up, the field is cleared and the persisted state advances. Malformed inputs fall through to "don't clear" so a parse failure can't drop a genuinely-pending update on garbage.

0.5.0-beta.4

0.5.0-beta.3

Minor Changes

  • BREAKING: Mermaid diagrams are now authored exclusively as ```mermaid fenced code blocks. Existing <Mermaid chart="…" /> JSX must be manually converted to a mermaid fence — legacy JSX will display as an editable source block instead of a rendered diagram.

    To migrate: search your content for <Mermaid chart= and rewrite each occurrence as a ```mermaid fence. Untouched legacy content remains visible and editable as an unknown-component source block, but no longer renders as a diagram until converted.

    Mechanism: the canonical descriptor was renamed MermaidMermaidFence so legacy <Mermaid /> JSX no longer matches at parse time and falls through to the unknown-component editable source block (no silent migration on save). The id and theme props were dropped (neither is expressible in fence syntax). Slash menu and PropPanel continue to show "Mermaid" via displayName.

0.5.0-beta.2

Minor Changes

  • Project creation under a directory whose parent contains .git/ now opens the parent (the git working-tree root) AND defaults the content scope to that same parent — opened folder and content.dir align by default. Previously the parent was opened but content.dir was silently scoped to the picked sub-folder, so the user landed in the git root visually while writes were narrowed beneath it.

    Surfaces the WHY in three places:

    • CLI (ok init): [ok] Initialized OK at <gitRoot> — opened parent of <cwd> because it contains a .git folder (was (scoped to <subPath>/)).
    • Desktop toast on freshly-spawned editor windows: Initialized OK at <gitRoot> — opened parent of <pickedPath> because it contains a .git folder (toast IPC payload's contentDir field replaced with pickedPath).
    • Desktop consent dialog banner: explicit "parent of <pickedPath> because it contains a .git folder (one .ok/ per git repo). The default scope is the entire repo; change Content directory below to narrow it."

    Narrowing back to the originally-picked sub-folder is still a one-edit choice — the consent dialog's Content directory field is pre-filled with . and accepts any sub-path, and config.yml's content.dir remains the post-init knob.

0.4.1-beta.1

Patch Changes

  • fix(desktop): derive the auto-update channel from the build version, not a runtime preference (fix-update-channel-from-build)

    The desktop app's update channel is now sourced from app.getVersion()0.4.0-beta.36 → beta feed, 0.4.0 → stable feed — so the feed electron-updater checks (and therefore the in-app "release notification" / "What's New" notice) always matches the binary the user installed. Previously a persisted AppState.updateChannel could diverge from the build (e.g. a beta install with updateChannel: 'latest'), causing a beta build to surface stable-channel releases.

    Removed:

    • the Settings → Update channel switcher (ChannelSection) and the useUpdateChannel setter
    • the persisted AppState.updateChannel key — existing state.json files carrying it are migrated by silently dropping it on first read
    • the ok:update:set-channel / ok:update:confirm-downgrade IPC channels and the ok:state:update-channel-changed event, plus the beta→stable downgrade-warning notice (it only ever fired for runtime channel switches)

    state.query() still reports the channel (now build-derived) so the BETA badge and About-panel label keep working.

0.4.1-beta.0

Patch Changes

  • fix(config): rename user-global config from ~/.ok/config.yml to ~/.ok/global.yml

    The user-global config now lives at ~/.ok/global.yml. The project marker stays .ok/config.yml, so the two are no longer name-twins — the ancestor-walk that detects an OK project can never treat the user's home directory as a project root, and users editing one file can't confuse it with the other.

    The CRDT doc name __user__/config.yml is intentionally unchanged — it's an opaque internal identifier and renaming it would be a wire-level change for connected clients.

View v0.5.0 on GitHub