OpenKnowledge

v0.9.0

Part of the OpenKnowledge changelog.

Minor Changes

  • OK config sharing mode — opt out of committing OK artifacts.

    Adds a first-class shared / local-only sharing posture for every OK artifact set the CLI and Desktop scaffold. Default is shared (commit .ok/, .okignore, .mcp.json, project skills, .claude/launch.json alongside content). Users on a personal-machine posture pick local-only, which appends the OK artifact set to <gitdir>/info/exclude so the files stay out of git without touching .gitignore (per-clone, never committed). The content-relative artifacts (.ok/ and .okignore) are matched at every folder depth, so folder-scoped configs nested under a non-default content.dir are excluded too.

    Surfaces

    • ok init --shared / ok init --local-only (mutually exclusive flags) + interactive TTY prompt; the pre-selected default mirrors the project's current state via readSharingMode(projectRoot) so idempotent re-runs preserve the user's earlier choice.
    • ok config-sharing share, ok config-sharing unshare, ok config-sharing status — post-init toggle + read. (Namespaced as config-sharing to stay clear of the unrelated ok share Publish-to-GitHub group.)
    • Desktop consent dialog (Pick-existing) and Create-new-project dialog both surface a Share with team / Local only on this machine radio.
    • Settings → Sharing row in the per-project Settings dialog — two-state segmented control; routes through the same addOkPathsToGitExclude / removeOkPathsFromGitExclude primitives as the CLI.

    Safety check

    Every shared → local-only transition runs a single-site tracked-files probe (git ls-files --error-unmatch). When a teammate has already committed an OK artifact upstream, the transition refuses with a multi-line diagnostic naming the exact git rm --cached remediation commands — .git/info/exclude cannot hide tracked files, so silently completing the operation would mislead the user. Same probe site for ok unshare, the consent dialog, the Create-new dialog, and the Settings toggle.

    Worktree-correct

    All .git/info/exclude writes route through resolveGitDirDetailed(projectRoot) and resolve the per-clone commondir from the resolved gitdir, so linked worktrees (where <projectRoot>/.git is a pointer file) write to the correct shared <repo>/.git/info/exclude — not a non-existent <projectRoot>/.git/info/exclude. The legacy ensureOkExcludedFromGit writer in ok clone is migrated onto the new module and picks up the worktree correctness as a side benefit.

    State of the current sharing mode lives in .git/info/exclude itself. No parallel registry; readSharingMode derives the mode from the file's content.

  • feat(open-knowledge): seed project-root .gitignore with .DS_Store on fresh git init

    ok init and the Desktop "Create new project" flow now write a one-line .gitignore containing .DS_Store at the project root, but only when ensureProjectGit actually ran git init during the invocation (no enclosing repo already existed). Pre-existing .gitignore files are never touched — the seed uses writeIfMissing semantics, so subsequent ok init runs and user hand-edits are protected.

    Why: Open Knowledge is macOS-only today, so every project accumulates Finder's per-folder .DS_Store metadata. Without an ignore entry the user's first git status lists them as untracked — confusing for users new to git, noisy for everyone else. The seed is a quality-of-life convenience for the first-run path; the file becomes user-owned the moment it lands.

    Surfaces a new writeRootGitignoreForNewRepo helper alongside initContent in @inkeep/open-knowledge-server. The CLI summary now includes a Seeded .gitignore at <path> (.DS_Store) line when the seed fires.

Patch Changes

  • Support rename, move, delete, and Finder reveal flows for non-markdown files in the project tree, including automatic markdown reference rewrites when assets move.

  • Capture client-side (renderer/browser) console output in the on-disk diagnostics logs. Previously only Node-side logs were persisted, so client events — including the collab provider's "Failed to connect" messages — were lost. The Electron app now routes each window's renderer console through the desktop pino log (~/.ok/logs); the web/ok ui build forwards console output (and uncaught errors) to a new POST /api/client-logs endpoint that writes to the server log. ok bug-report now also bundles the server local-sink logs (<contentDir>/.ok/local/logs/), so both client and server diagnostics travel with a report.

    Privacy note: capturing all renderer console output is a deliberate local-diagnostics tradeoff — raw console.* message strings are logged verbatim (pino field redaction only masks structured fields, not the message). The logs are local; the ship-path backstop is ok bug-report's redaction pass, which now also strips JWTs and URL-embedded credentials.

  • feat(cli): ok diagnose bundle --redact hashes doc names and strips content-dir

    The --redact flag now performs end-to-end redaction on the staged bundle:

    • Doc-name attribute values (the doc.name key, in both the OTLP attribute pair shape {key:"doc.name", value:{stringValue}} and the Pino flat-key shape {"doc.name": "..."}) are replaced with doc:<8 hex> derived from BLAKE2b-256 truncated to 8 hex chars.
    • The absolute content-dir prefix in any string field is replaced with the literal token <CONTENT_DIR> — applied to telemetry JSONLs, log JSONLs, state/runtime.json, state/agent-presence.json, and state/*.txt.
    • manifest.json.redaction.applied becomes true and manifest.json.redaction.docNameMap carries the stable per-bundle inverse map (hashed -> original) for the user's own de-anonymization.
    • manifest.json.contentDir.absolutePath is masked to <CONTENT_DIR>; pathSha256 stays as the SHA-256 of the original path for cross-bundle correlation.

    Original on-disk files under <contentDir>/.ok/local/telemetry/ and <contentDir>/.ok/local/logs/ are NOT modified - redaction only mutates the staged copies inside the bundle.

  • feat(cli): add ok diagnose bundle for support bundles

    New ok diagnose bundle subcommand zips the on-disk telemetry sink, server logs, and live server state (lock, agent-presence, shadow-repo head, runtime info) into a single bug-report-ready zip. Default location: <contentDir>/.ok/local/diagnostics/bundle-<ISO ts>.zip.

    Flags:

    • --pid <pid> runs ok diagnose process <pid> into a tmp dir first and places its output under process/ in the bundle.
    • --out <path> writes to a custom location (parent must exist).
    • --yes skips the y/N confirmation prompt.
    • --redact plumbed through to the collector and dependency-injectable so the redactor can hook in (see the companion changeset for the redactor implementation).

    Before writing, the CLI prints a content summary (file count, uncompressed size, doc-name occurrences visible in telemetry, content-dir path visibility, server status) and prompts Write bundle? [y/N]. y / yes accept; bare Enter or anything else declines. Works without a running server — bundles the on-disk telemetry and writes server-status.txt: not-running.

  • feat(cli): add ok open <doc> to open a doc in the OK Desktop app

    New ok open <name> [--folder] [--project <dir>] command. A doc focus-or-launches the OK Desktop app via the openknowledge://open?project=&doc= deep link, with a system-browser fallback when no desktop bundle is installed. A folder opens the /#/<folder>/ browser route. This gives the Claude Code CLI (a pure-stdio host with no preview pane and no in-app browser) a one-step way to open a doc in the app instead of printing a URL for the user to click.

    The bundled project skill's preview capability ladder now routes the no-browser-tool rung to ok open, and the get_preview_url tool description no longer claims "Claude Code" uses preview_start (that is the Desktop pane; the CLI uses ok open). Cursor and Codex (in-app browser) and Claude Code Desktop (pane) are unaffected: they sit on higher ladder rungs and never reach ok open.

    Folder deep-linking, an installed-version check, and branch/worktree open are intentionally deferred.

  • Every client now sends its own version metadata (protocol version, runtime semver, and a client-kind discriminator) on its requests to the local server. The web app and desktop renderer send it as x-ok-client-* HTTP headers and Hocuspocus auth-token fields; the MCP shim (/mcp), the desktop main process (HTTP /api/*), and ok sync/pull/push send the x-ok-client-* headers. The server does not read this metadata yet, so current servers are unaffected. It lets a follow-up release detect an incompatible client and tell it to update or respawn.

  • Fix duplicate ignore patterns (and silently-reverted settings) after the collaboration server restarts while the app is open. The config documents (.okignore and the project/user/local settings) now claim the server instance in their sync handshake, so a stale reconnect after a respawn is rejected before it can merge — the same protection the editor documents already had.

  • fix(open-knowledge): config-sharing local-only now excludes the project-root .ok/ for a non-default content.dir

    getOkArtifactPaths anchored the .ok/ and .okignore exclude entries to <contentDir>/.ok/. For a project that scopes content to a subdirectory (content.dir != .), that missed the project-root .ok/ (where config.yml actually lives, read from <projectRoot>/.ok/), so local-only mode left the primary OK config committable.

    Now .ok/ and .okignore are emitted unanchored: a slash-free gitignore entry matches at every depth, so one entry each covers the project-root config dir, the content-dir copy, and folder-nested copies. Also drops the now-unnecessary content.dir read from the artifact-path computation.

  • Route Node-side diagnostic console.log calls through the pino logger so operational logs are structured and captured in the on-disk diagnostics bundle. Server-side sites (file-watcher, head-watcher, project-git, share, history/timeline/rollback) now log via getLogger; the desktop main process and the dev Hocuspocus plugin do the same. CLI user-facing stdout and renderer-side React are intentionally unchanged.

  • feat(desktop): notify and offer to restart when attached to a server of a different version

    When OK Desktop opens a project and attaches to an already-running server whose version differs from the app's — most often a prior version's detached server still alive after an auto-update — it now surfaces a cancelable notification ("This project is running an older version…"). The notification's action terminates that server and recreates the window against a fresh server matching the app. The kill is always user-initiated, and the copy warns that connected agents (Claude Code, Codex, Cursor) will see their Open Knowledge MCP connection drop and may need to be restarted. If termination fails because the server runs under another account, the notification points the user at a reboot rather than a command that would hit the same permission wall.

  • Five hardening fixes to the ok diagnose bundle telemetry/redaction surface — no observable behavior change beyond closing credential / doc-name leak paths and a Windows redaction regression. Follow-up to the original ok diagnose bundle ship; review-flagged at merge time.

    • ScrubbingSpanProcessor now masks credential-shaped attributes on span events and links in addition to top-level span attributes. The processor's stated invariant ("no credential bytes reach a persistent surface") previously held only for span.attributes; a caller emitting span.addEvent('auth-check', { authorization: token }) would write the credential verbatim to the file sink and the OTLP collector. Coverage is now uniform across the three OTel attribute surfaces via a shared scrubAttributes helper.
    • Pino's denylist-derived redact paths now enumerate one wildcard segment per nesting depth (up to 5), so the dominant HTTP-logging shape { req: { headers: { authorization: "Bearer X" } } } (depth 2) is masked on disk. Pino's redact engine treats ** as a literal property name — the previous *.{key}-only composition missed everything deeper than one level.
    • bundle-redact's --redact walker now applies a substring scrub for known doc names when a JSON state file (e.g. a torn agent-presence.json) fails JSON.parse. The fallback used to apply only the content-dir prefix substitution, so a corrupt state file could leak doc names the flag promised to hide. The substring scrub composes with the existing content-dir substitution and is keyed off the inverse map that the structural walker already populates from earlier-walked telemetry/log files.
    • initTelemetry now commits the TracerProvider module slot immediately after trace.setGlobalTracerProvider — before MeterProvider construction. A metrics-only failure (invalid OTLP endpoint, collector unreachable at construct time) used to throw inside the same try/catch and clobber both providers, silently disabling all span capture. The catch reshapes to a warn when the tracer has already committed: metrics degrade independently of traces, and the file sink keeps writing for bug-report bundles.
    • redactStagedBundle's state-file dispatch now resolves the basename via node:path's basename instead of filePath.lastIndexOf('/')-based manual slicing. The manual slice returned -1 on Windows backslash paths and left the extracted base as the full absolute path, missing the STATE_JSON_FILES set and routing agent-presence.json / runtime.json to the substring-only walker that skips doc-name hashing under --redact.
  • Three pullfrog-flagged fixes for ok diagnose bundle:

    • The CLI now resolves content.dir via resolveContentDir(config, cwd) so users with a non-default content.dir setting get a bundle of the right tree (previously the command read process.cwd() directly and would silently bundle an empty .ok/local/ for projects with content.dir: docs or similar).
    • --redact no longer ships the { hashed → original } inverse map inside the zip. The map is now written to a sidecar file (<basename>.docnames.json) next to the zip on the user's machine; the manifest carries only redaction.docNameMapSidecar: "<filename>" so the recipient sees a pointer rather than the data. This restores the SPEC §5 privacy contract ("downstream consumers see only hashes").
    • The pre-zip summary now reads doc.name attributes: N occurrence(s) in telemetry (values hashed) when --redact is active. The count was always a key-count, not a value-count; the previous phrasing implied otherwise.
  • fix: enforce a single docName validation contract before the write path

    Malformed docName values were handled inconsistently: a whitespace-only name (" ") passed request validation and then threw a 500 deep in the doc layer ("Document name must not be empty"); ".", "a/" (trailing slash), ".foo" (leading dot), and tab-bearing names were silently accepted and created junk, hidden, or unaddressable files; only ".." / "../escape" were correctly rejected.

    A new shared validateDocName (exported from @inkeep/open-knowledge-core) defines the structural contract — non-empty, no leading/trailing whitespace, no control characters, no path traversal, no absolute/backslash paths, no empty or hidden-dot (.-leading) path segments — and is enforced at the write entry points: the safeDocNameField request-schema field shared by every mutating HTTP handler (and requiredSafeDocNameField for the legacy /api/rename from/to fields, previously a bare min(1) holdout), plus the MCP normalizeDocName. Malformed names now get a clear 400 (or a clear MCP error) consistent with the already-correct .. rejection. Whitespace is rejected rather than silently trimmed, matching the project's preference for loud, explicit failures over silent normalization.

    Scope note: normalizeDocName is shared by the read/edit/delete MCP tools (exec cat/listing aside, e.g. links, get_history, edit_document, delete_document, version, share_link), not only write_document / rename. So an MCP read/edit/delete that targets a pre-existing structurally-invalid name (e.g. a hidden .foo placed out-of-band) now returns a clear error instead of resolving — those docs were already excluded from the KB. The HTTP read/delete endpoints (guarded by the narrower isSafeDocName path-traversal check, left unchanged so listings/backlink counts are unaffected) remain a recovery path.

  • Show a search icon in the editor header when the sidebar is collapsed. The sidebar's SidebarSearchBar is hidden in collapsed state, leaving the ⌘K / Ctrl+K shortcut as the only discoverable way to open the command palette. The new icon sits to the right of the sidebar toggle, opens the same command palette, and disappears once the sidebar is expanded again.

  • Fix GitHub credentials being stored in plaintext (~/.ok/auth.yml) instead of the macOS keychain. The bundled desktop CLI could not resolve the @napi-rs/keyring native module — it ships only under app.asar.unpacked/, which the standalone CLI at Resources/cli/dist/ cannot reach — so createTokenStore silently fell back to the file backend on every CLI-spawned auth operation (clone, login, status, git-credential). The keyring module is now shipped onto the CLI's own resolution path (cli/node_modules/). A keyring load failure now logs the underlying reason instead of downgrading silently, and the keychain-backed store transparently migrates any existing ~/.ok/auth.yml token into the keychain on first read.

  • fix(open-knowledge): redirect CLI self-spawn through the LSUIElement helper bundle to stop the packaged-macOS "exec" Dock-tile leak

    In the packaged macOS app, the CLI's self-spawn paths (ok mcp → ok start and ok start → ok ui) re-invoked process.execPath detached. Inside the packaged Electron app process.execPath is the parent .app's main binary, so the detached spawn registered a duplicate .app launch with LaunchServices, and coreservicesd parked a stuck generic "exec" Dock tile for the lifetime of every auto-spawned server — one tile per worktree, since each worktree gets its own server.

    The desktop's own detached-server spawn already solves this by routing through an LSUIElement=true helper bundle at <.app>/Contents/Frameworks/Open Knowledge Server.app/Contents/MacOS/Open Knowledge Helper. The shared mechanism — bundle name (HELPER_BUNDLE_NAME), executable basename (HELPER_EXECUTABLE_NAME), and the pure path-arithmetic resolver resolveHelperBundleBinary — has been hoisted out of packages/desktop/src/main/resolve-detached-spawn-args.ts into @inkeep/open-knowledge-core/helper-bundle.ts so both the desktop spawn site and the CLI self-spawn site compose a single source of truth. The contract is four independent encodings that must agree (TS resolver, Info.plist CFBundleExecutable, afterPack.mjs, electron-builder.yml's extraFiles[].to); the desktop spawn site and the CLI self-spawn redirect are the two consumers of the resolver, not additional encodings — they import its return value and cannot drift independently.

    The CLI-specific redirect policy — the .app/Contents/MacOS/<exe> shape predicate plus the exists probe that infer "this is a packaged build" without Electron's app.isPackaged (unavailable under ELECTRON_RUN_AS_NODE=1) — lives in the CLI alongside its sole consumer at packages/cli/src/commands/self-spawn.ts. The desktop keeps its own platform === 'darwin' && isPackaged gate over the same shared resolver; the two predicates are intentionally not unified.

    Dev mode, non-darwin, and npm-global-install shapes (any execPath that is NOT inside a .app/Contents/MacOS/ slot) keep process.execPath unchanged. The per-worktree-server topology is unchanged — one server per worktree is correct behavior — but the Dock no longer accumulates one stuck "exec" tile per worktree.

  • Fix two desktop / worktree-as-project-server regressions where deep-link preview and Dock presentation only worked on the standalone ok ui path.

    • Preview deep-links now navigate in desktop mode. The collab server that serves the editor SPA in desktop / worktree-as-project topology now answers GET/DELETE /api/config, returning the same { collabUrl, previewUrl, port, paneTarget } shape as ok ui. Previously the endpoint 404'd there, so an armed pane target (get_preview_url({ armPaneTarget: true })) was never read and the preview landed on a blank base page instead of the requested route.
    • ok mcp / ok start no longer leave a stray "exec" Dock tile. The bundled ok.sh wrapper now runs these long-lived in-process servers via the Dock-less LSUIElement helper binary when present, falling back to the main binary for older bundles and npm-global installs. The default ok desktop launch keeps the main binary.
  • fix(open-knowledge): share-and-copy reliably writes to the clipboard

    Users clicking "Publish and copy link" in the Publish-to-GitHub modal saw the error toast Link ready but could not copy to clipboard. — the publish itself succeeded, but the clipboard write was rejected. Root cause: the browser Clipboard API only honors navigator.clipboard.writeText when the document holds transient user activation at call time, and the multi-second submitPublishRequest between the original click and the eventual clipboard call consumed that activation.

    The fix has two parts:

    1. Two-step publish-then-copy UX in the wizard (packages/app/src/components/PublishToGitHubDialog.tsx). The dialog no longer auto-copies on submit. On submitPublishRequest({ok: true}) it transitions to a Published success view with an explicit Copy share link button. The user's click on that button installs a fresh user activation that the underlying navigator.clipboard.writeText call rides cleanly. The submit button label changes from "Publish & copy link" to "Publish" to honestly reflect the two-step flow.

    2. Clipboard adapter that prefers the Electron IPC bridge (packages/app/src/lib/share/clipboard-adapter.ts). A small single-responsibility helper (scheduleClipboardWrite(text)) used by the Share button and the new Copy share link button. Detects window.okDesktop.clipboard.writeText (the existing Electron preload bridge to the main process — not gated on transient activation) and routes the write through it when present; falls back to navigator.clipboard.writeText for browser contexts. This keeps the OK desktop app's share flow unconditionally reliable regardless of network timing.

    Internal contract unchanged: ShareActionDeps.clipboardWrite keeps its (text: string) => Promise<void> shape. Source-text guards in ShareButton.test.ts and PublishToGitHubDialog.test.ts updated to pin the adapter wiring (callers route through scheduleClipboardWrite, not a direct navigator.clipboard.writeText).

  • Fix an out-of-memory crash that could take down the local server when Show All Files is enabled on a large repository. In Show All Files mode the file listing no longer descends into .git/, node_modules/, or .ok/ — directories that never hold knowledge-base content and could grow the walk without bound (a multi-GB .git object store or thousands of node_modules directories would exhaust the heap, crashing the server and dropping the editor's connection). Other normally-hidden content — .gitignored files, build output such as dist/ — still surfaces as before.

  • fix(server): shell-quote the sync credential helper so auto-sync works from the packaged macOS app

    Auto-sync (and "Sync now") failed in the installed desktop app with a red flash like … No such file or directory followed by fatal: could not read Username for 'https://github.com': Device not configured, even though cloning the same repo had just succeeded.

    The sync engine builds a git credential.helper=!<cli> auth git-credential argument from the bundled CLI path. In the packaged app that path lives under /Applications/Open Knowledge.app/…, which contains a space. Git runs a !-prefixed credential helper through the shell, so the unquoted space split the path: the shell tried to exec /Applications/Open, the helper returned no credentials, and git fell back to an interactive username prompt with no TTY. (Clone was unaffected because it builds its helper from a bare command name — gh or open-knowledge — with no spaces.)

    The credential-helper argv elements are now shell-escaped, so paths with spaces (or other shell metacharacters) survive intact as a single token.

  • Fix a crash when navigating to another document while a table cell-handle dropdown is open. The column/row handle menus no longer mount into editor-owned DOM that the editor tears down on navigation, which previously surfaced as an uncaught Failed to execute 'removeChild' on 'Node' error.

  • Fix: clicking the close (×) or Cancel button on the New template form no longer flashes "required" validation errors or needs a second click to dismiss. Dialog dismiss controls now keep focus on the active field instead of blurring it, so dismissing never triggers blur-driven validation (which had grown the centered dialog and slid the close button out from under the pointer mid-click).

  • Folder frontmatter no longer cascades into child docs — a doc's frontmatter is exactly its own on-disk YAML.

    Previously, each <folder>/.ok/frontmatter.yml overlaid its values onto every descendant doc's effective frontmatter at read time (scalars replaced, arrays unioned), so a doc's frontmatter surfaced through exec / search differed from what was on disk. That made <folder>/.ok/frontmatter.yml do double duty — describe the folder AND inject default values into children — which duplicated templates and was surprising for a "plain markdown files" product.

    Folder frontmatter is now SELF-ONLY: <folder>/.ok/frontmatter.yml describes only its own folder, with no inheritance up or down the tree. A doc's effective frontmatter equals its on-disk frontmatter. The single mechanism for "what new docs in a folder start with" is templates (<folder>/.ok/templates/).

    Folder frontmatter is open-shape, exactly like a doc's — the folder_config set-rule action and PUT /api/folder-config accept any key (title / description / tags are conventional keys the UI surfaces, not a hard whitelist). The property panel shows a doc's own frontmatter only (no inherited rows), and the folder-properties editor edits the folder's own frontmatter with the same widgets a doc uses. GET /api/folder-config drops the cascade-derived frontmatter_sources field; DirectoryMeta.frontmatter_defaults and EnrichedMeta cascade fields are removed. The cascade machinery (mergeCascade, the root→leaf resolver) is deleted.

  • Reduce jargon in the folder overview UI for first-time users. The "Folder defaults" panel is now "Folder properties" with a plain-language description; its actions read "Set a shared value" and "Add a blank field" (was "Add default value" / "Declare field"), declared properties on a doc are marked "to fill in" (was "declared"), and the templates and new-file empty states drop the "resolve here" / "seed" wording. The collision and malformed-frontmatter error messages were reworded to match (no more "already resolves here" / "Frontmatter is malformed").

    Docs: adds an Advanced section with a combined Folders and templates page — folder properties (shared values vs. blank fields), templates, and the shared folder cascade, with frontmatter explained in plain terms before the word is used. The standalone Features → Templates page is folded into it, and the Editor page now defines properties/frontmatter as the canonical reference.

  • fix(server): folder rename enumerates descendant docs from disk, not the lagging file index

    Folder rename derived its affected-docs set from the in-memory file index, which the chokidar watcher populates asynchronously. Right after a fresh write_document create, that index lags on-disk truth — so renaming the enclosing folder found zero managed docs, reported a no-op (renamed: [], rewrittenDocs: [], previewUrls: {}, "nothing to rename"), and skipped inbound-link rewriting while STILL moving the directory on disk. Every wiki / inline link pointing into the folder was silently orphaned, even though single-doc rename (which trusts the caller's path, not the index) rewrites the same links correctly.

    Folder rename now enumerates descendant docs by walking the source directory on disk — the authoritative set of what the move carries — instead of the index. The walk also registerDocExtensions each descendant (as the watcher's add handler does), so a .mdx doc the index never registered resolves to its real file rather than getDocExtension's .md default (which would otherwise read a non-existent .md path and throw ManagedRenameMissingDocumentError, or write a .md sibling of the moved .mdx). The folder conflict pre-check in handleRenamePath was switched to the same disk walk for consistency. With docs enumerated, the existing spine rewrites inbound links, remaps CRDT identity, and returns an accurate renamed[] / rewrittenDocs[] / previewUrls{}.

  • Add a depth-1 children mode to GET /api/documents (?dir=&depth=1) for lazy per-directory FileTree expansion. The Show All Files walk now yields only the immediate children of the scoped directory, stamping each child folder with a hasChildren flag so the sidebar can render expand affordances without walking the subtree. This is the server half of collapsing the file-tree retained set from the whole content tree to the on-screen working set; the client lazy-expansion rework is tracked separately.

  • fix(open-knowledge): folder hasChildren in document-list schema for depth-1 FileTree expansion

    Add an optional folder-only hasChildren field to DocumentListEntrySchema so the depth-1 children variant of GET /api/documents (?dir=<rel>&depth=1) can mark which child folders are expandable without walking the subtree. The .refine() document and asset branches reject the field; the folder branch permits it. This is the schema foundation for collapsing the FileTree retained set from the whole content tree to the on-screen working set (PRD-6857).

  • Project Navigator's recent-projects list now shows the current git branch alongside each entry (where the Missing badge sits, for non-missing projects). Powered by a new bridge.project.readHeadBranch(projectPath) IPC — a pure filesystem read of .git/HEAD with no server boot, so the labels appear without slowing the Navigator's first paint. Non-git projects and detached HEAD render no label; missing projects continue to show the Missing badge.

  • Fix ok ui serving a blank page after an in-place build update. The preview web server now resolves SPA bundles from disk on each request instead of caching the dist file listing at startup, so a rebuilt (or in-place npm i -g upgraded) build is served without restarting the server — previously the stale listing 404'd the new hashed bundles while still serving a fresh index.html that referenced them. Adds ETag revalidation (304s on unchanged bundles), and returns a plain 404 for missing static assets instead of the API problem+json error envelope.

  • fix(preview): land the Claude pane directly on a doc, and document one-step re-navigation

    The Claude Code Desktop preview flow could not reliably open a doc. preview_start opens the pane at the UI root with no way to pass a target, so the agent armed one via get_preview_url({ armPaneTarget }) for the page to read on load. That worked only on a genuine fresh load: when the UI was already running, preview_start reused the live process without reloading, the armed target was never read, and the pane silently stayed put while both tool calls returned success.

    Two changes, both scoped to the Claude Code Desktop flow (other hosts navigate straight to the deep URL and never arm a target, so they are unaffected):

    • ok ui now redirects a base-open GET / straight to an armed pane target, so arm + preview_start lands the pane directly on the doc on a fresh open instead of rendering root and then client-navigating. The target is consumed as the 302 is emitted, which breaks the hash-fragment redirect loop (the server never sees the #/doc fragment on the follow-up request) and prevents an in-TTL reload from being yanked back to the armed doc. PaneTargetLanding in the app stays as a client-side backstop.
    • The bundled project skill now splits the guidance into two modes: arm + preview_start to land a fresh open on a doc, and a one-step preview_eval (window.location.hash = '#/<doc>') to move between docs once the pane is open — since preview_start can't move an already-running pane. The no-screenshot rule gains a carve-out clarifying that navigating via preview_eval drives the view and is not a forbidden read-back verification loop.
  • fix(server): reject empty docName instead of silently writing to test-doc

    A mutating write whose docName was empty or missing previously fell through to a hardcoded test-doc fallback: write_document({ docName: "" }) returned "Written successfully" while overwriting test-doc.md (and, with the default position: "replace", clobbering any existing test-doc). The response's previewUrl was /#/, which didn't even match where the content landed — a silent wrong-target write in the data-loss class.

    Every mutating API handler that read the test-doc fallback — /api/agent-write, /api/agent-write-md, /api/frontmatter-patch, /api/agent-patch, /api/agent-undo — now rejects an empty/missing docName with 400 urn:ok:error:invalid-request ("docName must be a non-empty document name.") before any session is opened or content is written. Read-only GET fallbacks are unchanged.

  • Removed the <folder>/.ok/schema.yml declared-fields mechanism — folders now have two mechanisms, not three.

    The declared-fields channel (.ok/schema.yml) let a folder declare named blank fields that every doc "should have." With the value cascade gone, OK had three overlapping folder mechanisms; this collapses to two with a clean story: descriptive frontmatter.yml (the folder's own title / description / tags) + templates (the starting content and values for new docs).

    Removed: the folder_config declare-field / remove-field actions, the schema.yml reader/writer, the declarations channel on PUT /api/folder-config, the schema_declarations / schema_local / schema_sources fields on the folder-config API and on DirectoryMeta / EnrichedMeta, the DeclarationMap / FolderSchema types from @inkeep/open-knowledge-core, and the declared-field affordances in the property pane. The write_document / edit_frontmatter prompts no longer mention declared fields.

    This is a pre-launch cleanup; there is no migration tooling. A folder that wants every doc to start with a field should ship a template carrying that field.

  • Removed config.yml keys now fail loudly through a single registry instead of an inconsistent three-tier contract.

    Previously, stale keys were handled three different ways: content.include/content.exclude hard-errored, a hand-maintained list of keys (server.host, mcp.autoStart, upload.maxBytes, …) only warned, and others — including the headline folders and appearance.editorModeDefault — were silently accepted as no-ops despite the docs still teaching folders. A loose schema meant editors never flagged them either.

    All removed keys now route through one data-driven registry (REMOVED_KEYS in @inkeep/open-knowledge-core) and a single detector. Severity is uniform — every removed key is rejected with a source-located error (file:line:col + snippet) whose message names the replacement (env var, flag, .okignore, or nested <folder>/.ok/). Where that rejection surfaces preserves the existing user-vs-project split: a project .ok/config.yml fails fast so the user fixes it in place, while a stale user-global ~/.ok/global.yml is sidelined and Open Knowledge boots on defaults rather than bricking every project.

    ok config migrate now strips every registry key too (sourced from the same table), so the "run ok config migrate" hint in each error is always actionable. No config migration tooling beyond this codemod is added — pre-launch, hand-rolled stale configs simply get a clear, source-located error.

  • fix(open-knowledge): select the active sidebar row when documents arrive after first paint

    Opening a nested doc by direct URL or hash navigation (graph/wikilink click) could leave the sidebar with no selected row when the /api/documents response landed after the first selection-mirror commit. The mirror selected against an empty tree, and when the documents arrived model.resetPaths rebuilt the tree without re-running the mirror, so the active row was revealed and its folder expanded but the row was never selected. useSelectionMirror now re-runs on treePathsSignature (the same trigger the reveal-active-row effect already uses), re-asserting the selection after the tree repopulates.

  • fix(open-knowledge): in-app branch label stuck on 'main' when project opens on a non-main branch

    GET /api/server-info read getActiveBranch() without awaiting the server's ready promise. The branch is module-level state in persistence.ts initialized to the hardcoded 'main' default and updated by switchReconciledBaseScope(startupBranch) inside initAsync. A renderer that fetched server-info during the boot window — between httpServer.listen resolving and initAsync reaching the branch reset — observed 'main' regardless of the actual HEAD. The client-side current-branch-store is fire-once and only updates from CC1 branch-switched, so the stale value stuck for the lifetime of the session until a real cross-branch checkout.

    handleServerInfo now mirrors handleDocumentList and parks on ready before sampling. refreshServerInfo on the renderer also emits branch-changed when the observed branch advances, so any session running against an older server self-corrects on the next __system__ reconnect.

    User-visible: the sidebar ProjectSwitcher branch row and editor footer branch label now match the Project Navigator's .git/HEAD read, which was the only correct source before.

  • Bound GET /api/documents?showAll=true with a hard entry cap (default 50000, tunable via OK_SHOWALL_MAX_ENTRIES) so a content directory pointed at a large repository can no longer drive the server toward out-of-memory. The response now carries an optional truncated flag and the sidebar shows a "showing first N (truncated)" notice when the list is partial. Server process memory (heap used/total, RSS) is now exposed as a bounded OpenTelemetry observable gauge ok.server.memory.usage_megabytes.

  • Stream GET /api/documents?showAll=true as NDJSON via a generator walk. The Show All Files walk is now an async generator that yields one entry at a time; when the sidebar negotiates Accept: application/x-ndjson the server writes each entry to the socket (honoring backpressure) and ends with a {type:"complete",truncated,count} line, instead of buffering the whole listing, validating it as one array, and serializing it to a single string. That collapses the serialization heap peak from three live copies of the listing to one entry plus the traversal cursors, the durable fix on top of the entry cap floor. The sidebar consumes the stream incrementally and validates each entry as it arrives. Requests without the NDJSON Accept (and the index-backed non-showAll response) keep the buffered single-flight JSON path unchanged, so streaming is opt-in and back-compatible.

  • Dedupe overlapping GET /api/documents?showAll=true walks. Concurrent identical Show All Files requests now share a single in-flight disk walk and its sorted result instead of each starting its own, collapsing the per-walk heap cost when the sidebar fires bursts of refreshes. The sidebar also cancels a superseded refresh, and the server aborts a walk once every waiting caller has disconnected, so abandoned walks stop instead of running to completion.

  • Surface the current git branch in the editor UI. The sidebar's project switcher shows the branch as a muted second line under the project name, and the editor footer renders the project name + branch (with a tooltip over the project name showing the full project path) whenever the project switcher isn't visible — sidebar collapsed or non-Electron host.

  • fix(server): surface disk-persistence failures instead of reporting a false success

    When the disk-persistence step of an agent write failed (ENOSPC, EACCES, read-only FS, etc.), onStoreDocument threw, Hocuspocus caught the rethrow and kept the doc in memory ("stays in memory to avoid data loss"), and the /api/agent-write-md handler — which had already returned 200 — told the caller "Written successfully". The content never reached disk and was lost on restart: silent data loss with a success response, the highest-trust failure mode for an agent that routes all writes through OK MCP.

    The persistence layer now records the store failure out-of-band (Hocuspocus swallows the throw without signaling the caller) and exposes it via takeStoreFailure. handleAgentWriteMd force-flushes the L1 store, then — on a recorded failure — responds with the matching storage problem type (storage-full / storage-readonly / storage-error, 507/500) and a message making clear the content was not persisted, rather than a false success. The CRDT copy is still retained in memory. A test-only OK_TEST_STORE_FAULT seam injects a synthetic store failure for deterministic coverage.

    This is a concrete down payment on the broader write-durability half of the "stale snapshot / debounced-writes-lost" investigation (PRD-6832); the read-staleness half is tracked separately.

  • Hide the table cell-handle dropdown pills when you aren't actively interacting with the table. Previously they stayed visible whenever the cursor sat in a cell, including after navigating to another document and back (the warm editor cache preserves the selection). They now appear only when the editor is focused, a handle pill is focused, or a handle's dropdown menu is open.

  • Two post-QA review fixes for the telemetry bundle feature:

    • ok diagnose bundle --redact --pid <pid> now also redacts the process/ subdirectory (cpuprofile metadata, lsof, stacks, process-stats). Previously only telemetry/, logs/, and state/ were walked, so the staged process/metadata.json still carried the absolute content-dir path even with --redact.
    • shutdownTelemetry() now clears and unrefs its 5s timeout timer on fast-path resolve, so a caller awaiting destroy can observe normal event-loop drain instead of a 5s linger.
  • fix(telemetry+bundle): close credential leak gaps and drain log sink on shutdown

    Local-review hardening on the ok diagnose bundle substrate:

    • Pino file sink drained on shutdown. bootServer.destroy now awaits loggerFactory.flushAllFileSinks() after shutdownTelemetry() so log records enqueued in the final moments before process.exit() land on disk. The previous shutdown drained the FileSpanExporter's appender chain but missed the separate Pino chain.
    • Project enabled: false is now honored. resolveLocalSinkConfig used schema-defaulted values to compare project vs project-local, which meant telemetry.localSink.enabled: false committed in the project config was silently overridden by the local file's defaulted true — violating the AC5 contract. The resolver now reads raw YAML to detect explicit presence and falls through to schema defaults only when no layer set the key.
    • OTLP push is scrubbed. ScrubbingSpanProcessor now registers whenever ANY pipeline is active (file sink or OTLP push) so toggling the file sink off does not silently re-enable credential leaks to the remote collector. The default denylist applies when no localSink options are supplied.
    • Pino log records are scrubbed too. When the file sink is wired, PinoLogger applies Pino's redact: { paths, censor: '[REDACTED]' } for every denylist key (plus *.{key} wildcards) so a caller who accidentally logs authorization, cookie, etc., never ships the secret to disk or stdout.
    • Inverse-map hash collisions surfaced. --redact now records distinct doc names that hash to the same 8-hex prefix in manifest.json.redaction.docNameCollisions (omitted when empty) so recipients of large-workspace bundles see when the inverse map is incomplete for a hash.
  • feat(telemetry): split file-sink and OTLP push gates so default installs persist spans locally

    initTelemetry() now composes two pipelines on a single BasicTracerProvider: the new file sink (config-gated via telemetry.localSink.enabled, default-on) and the existing OTLP/HTTP push (env-gated via OTEL_SDK_DISABLED). Both can run together; either can run alone. bootServer reads project + project-local config once at boot and wires the logger fileSink + file SpanExporter from the same resolved values, so a default ok start now captures recent spans + logs on disk under <contentDir>/.ok/local/{telemetry,logs}/ for ok diagnose bundle to harvest — no OTEL_SDK_DISABLED=false required, no LGTM stack required. LGTM stack users still set OTEL_SDK_DISABLED=false to push to their collector; behavior is unchanged for them.

  • Treat a git worktree as its own Open Knowledge project so its editor preview no longer hangs on "Connecting — waiting for collab server".

    Opening a worktree's preview now starts that worktree's own collab server instead of only a UI: the .claude/launch.json recipe runs ok start (via a portable # ok-ui-v1 bundle→npx shell chain) rather than bare ok ui. ok start gains a --ui-port flag that pins its UI sibling to the preview pane's port, and connects-instead-of-erroring when a server already runs in the folder — so one committed recipe is correct on both the main checkout and a fresh worktree. Deleting a worktree (Remove Git Folder) now stops that worktree's server first. When a folder genuinely has no collab server, the editor shows an actionable message instead of an indefinite spinner.

  • write_document now accepts empty markdown with position: "replace" to clear a document's body — frontmatter, document identity, and history are preserved — instead of rejecting it with a generic "Request body is invalid." error. Empty append/prepend is a reported no-op (no more stray blank-line injection), and validation errors now surface the server's specific field-level reason rather than the bare title. Creating a brand-new document with empty or whitespace-only content is rejected with an actionable message instead of silently writing no file.

  • fix: write_document honors an explicit .mdx extension when authoring a new doc

    write_document({ docName: "x.mdx" }) stripped the .mdx and always created x.md, so there was no way to author a .mdx document through the MCP — even though the knowledge base renders MDX/JSX components. The only workaround was native file creation, which the project guidance discourages.

    The tool now recovers the caller's explicit suffix (normalizeDocName still strips it for keying) and, on a create, forwards it to the write path so a .mdx request lands a .mdx file (.md or no suffix still defaults to .md). /api/agent-write-md accepts an optional extension (.md | .mdx) and pre-registers it before the persistence flush — the same synchronous extension-registration the rename path uses for an extension change.

    The hint is honored only when the doc does not yet exist. For an existing doc the recorded on-disk extension wins (switching it would write a sibling file and orphan the original); the gate is enforced both in the tool and in the handler (docNameExistsWithAnySupportedExtension). When a .mdx request hits a doc already on disk as .md (or vice-versa) the write proceeds to the existing file and the response surfaces a one-line note that the on-disk extension was preserved — so the coercion is no longer silent.

View v0.9.0 on GitHub