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.jsonalongside content). Users on a personal-machine posture pick local-only, which appends the OK artifact set to<gitdir>/info/excludeso 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-defaultcontent.dirare 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 viareadSharingMode(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 asconfig-sharingto stay clear of the unrelatedok sharePublish-to-GitHub group.)- Desktop consent dialog (Pick-existing) and Create-new-project dialog both surface a
Share with team/Local only on this machineradio. - Settings → Sharing row in the per-project Settings dialog — two-state segmented control; routes through the same
addOkPathsToGitExclude/removeOkPathsFromGitExcludeprimitives as the CLI.
Safety check
Every
shared → local-onlytransition 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 exactgit rm --cachedremediation commands —.git/info/excludecannot hide tracked files, so silently completing the operation would mislead the user. Same probe site forok unshare, the consent dialog, the Create-new dialog, and the Settings toggle.Worktree-correct
All
.git/info/excludewrites route throughresolveGitDirDetailed(projectRoot)and resolve the per-clonecommondirfrom the resolved gitdir, so linked worktrees (where<projectRoot>/.gitis a pointer file) write to the correct shared<repo>/.git/info/exclude— not a non-existent<projectRoot>/.git/info/exclude. The legacyensureOkExcludedFromGitwriter inok cloneis 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/excludeitself. No parallel registry;readSharingModederives the mode from the file's content.feat(open-knowledge): seed project-root
.gitignorewith.DS_Storeon freshgit initok initand the Desktop "Create new project" flow now write a one-line.gitignorecontaining.DS_Storeat the project root, but only whenensureProjectGitactually rangit initduring the invocation (no enclosing repo already existed). Pre-existing.gitignorefiles are never touched — the seed useswriteIfMissingsemantics, so subsequentok initruns and user hand-edits are protected.Why: Open Knowledge is macOS-only today, so every project accumulates Finder's per-folder
.DS_Storemetadata. Without an ignore entry the user's firstgit statuslists 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
writeRootGitignoreForNewRepohelper alongsideinitContentin@inkeep/open-knowledge-server. The CLI summary now includes aSeeded .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 uibuild forwards console output (and uncaught errors) to a newPOST /api/client-logsendpoint that writes to the server log.ok bug-reportnow 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 isok bug-report's redaction pass, which now also strips JWTs and URL-embedded credentials.feat(cli):
ok diagnose bundle --redacthashes doc names and strips content-dirThe
--redactflag now performs end-to-end redaction on the staged bundle:- Doc-name attribute values (the
doc.namekey, in both the OTLP attribute pair shape{key:"doc.name", value:{stringValue}}and the Pino flat-key shape{"doc.name": "..."}) are replaced withdoc:<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, andstate/*.txt. manifest.json.redaction.appliedbecomestrueandmanifest.json.redaction.docNameMapcarries the stable per-bundle inverse map (hashed -> original) for the user's own de-anonymization.manifest.json.contentDir.absolutePathis masked to<CONTENT_DIR>;pathSha256stays 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.- Doc-name attribute values (the
feat(cli): add
ok diagnose bundlefor support bundlesNew
ok diagnose bundlesubcommand 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>runsok diagnose process <pid>into a tmp dir first and places its output underprocess/in the bundle.--out <path>writes to a custom location (parent must exist).--yesskips the y/N confirmation prompt.--redactplumbed 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/yesaccept; bare Enter or anything else declines. Works without a running server — bundles the on-disk telemetry and writesserver-status.txt: not-running.feat(cli): add
ok open <doc>to open a doc in the OK Desktop appNew
ok open <name> [--folder] [--project <dir>]command. A doc focus-or-launches the OK Desktop app via theopenknowledge://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 theget_preview_urltool description no longer claims "Claude Code" usespreview_start(that is the Desktop pane; the CLI usesok open). Cursor and Codex (in-app browser) and Claude Code Desktop (pane) are unaffected: they sit on higher ladder rungs and never reachok 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/*), andok sync/pull/pushsend thex-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 (
.okignoreand 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-defaultcontent.dirgetOkArtifactPathsanchored the.ok/and.okignoreexclude entries to<contentDir>/.ok/. For a project that scopes content to a subdirectory (content.dir!=.), that missed the project-root.ok/(whereconfig.ymlactually lives, read from<projectRoot>/.ok/), so local-only mode left the primary OK config committable.Now
.ok/and.okignoreare 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-unnecessarycontent.dirread from the artifact-path computation.Route Node-side diagnostic
console.logcalls 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 viagetLogger; 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 bundletelemetry/redaction surface — no observable behavior change beyond closing credential / doc-name leak paths and a Windows redaction regression. Follow-up to the originalok diagnose bundleship; review-flagged at merge time.ScrubbingSpanProcessornow 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 forspan.attributes; a caller emittingspan.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 sharedscrubAttributeshelper.- 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--redactwalker now applies a substring scrub for known doc names when a JSON state file (e.g. a tornagent-presence.json) failsJSON.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.initTelemetrynow commits theTracerProvidermodule slot immediately aftertrace.setGlobalTracerProvider— beforeMeterProviderconstruction. 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 vianode:path'sbasenameinstead offilePath.lastIndexOf('/')-based manual slicing. The manual slice returned-1on Windows backslash paths and left the extracted base as the full absolute path, missing theSTATE_JSON_FILESset and routingagent-presence.json/runtime.jsonto the substring-only walker that skips doc-name hashing under--redact.
Three pullfrog-flagged fixes for
ok diagnose bundle:- The CLI now resolves
content.dirviaresolveContentDir(config, cwd)so users with a non-defaultcontent.dirsetting get a bundle of the right tree (previously the command readprocess.cwd()directly and would silently bundle an empty.ok/local/for projects withcontent.dir: docsor similar). --redactno 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 onlyredaction.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--redactis active. The count was always a key-count, not a value-count; the previous phrasing implied otherwise.
- The CLI now resolves
fix: enforce a single docName validation contract before the write path
Malformed
docNamevalues 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: thesafeDocNameFieldrequest-schema field shared by every mutating HTTP handler (andrequiredSafeDocNameFieldfor the legacy/api/renamefrom/to fields, previously a baremin(1)holdout), plus the MCPnormalizeDocName. Malformed names now get a clear400(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:
normalizeDocNameis shared by the read/edit/delete MCP tools (execcat/listing aside, e.g.links,get_history,edit_document,delete_document,version,share_link), not onlywrite_document/rename. So an MCP read/edit/delete that targets a pre-existing structurally-invalid name (e.g. a hidden.fooplaced 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 narrowerisSafeDocNamepath-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
SidebarSearchBaris 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/keyringnative module — it ships only underapp.asar.unpacked/, which the standalone CLI atResources/cli/dist/cannot reach — socreateTokenStoresilently 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.ymltoken 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 startandok start → ok ui) re-invokedprocess.execPathdetached. Inside the packaged Electron appprocess.execPathis the parent.app's main binary, so the detached spawn registered a duplicate.applaunch with LaunchServices, andcoreservicesdparked 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=truehelper 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 resolverresolveHelperBundleBinary— has been hoisted out ofpackages/desktop/src/main/resolve-detached-spawn-args.tsinto@inkeep/open-knowledge-core/helper-bundle.tsso 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.plistCFBundleExecutable,afterPack.mjs,electron-builder.yml'sextraFiles[].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 theexistsprobe that infer "this is a packaged build" without Electron'sapp.isPackaged(unavailable underELECTRON_RUN_AS_NODE=1) — lives in the CLI alongside its sole consumer atpackages/cli/src/commands/self-spawn.ts. The desktop keeps its ownplatform === 'darwin' && isPackagedgate over the same shared resolver; the two predicates are intentionally not unified.Dev mode, non-darwin, and npm-global-install shapes (any
execPaththat is NOT inside a.app/Contents/MacOS/slot) keepprocess.execPathunchanged. 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 uipath.- 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 asok 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 startno longer leave a stray "exec" Dock tile. The bundledok.shwrapper now runs these long-lived in-process servers via the Dock-lessLSUIElementhelper binary when present, falling back to the main binary for older bundles and npm-global installs. The defaultokdesktop launch keeps the main binary.
- Preview deep-links now navigate in desktop mode. The collab server that serves the editor SPA in desktop / worktree-as-project topology now answers
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 honorsnavigator.clipboard.writeTextwhen the document holds transient user activation at call time, and the multi-secondsubmitPublishRequestbetween the original click and the eventual clipboard call consumed that activation.The fix has two parts:
Two-step publish-then-copy UX in the wizard (
packages/app/src/components/PublishToGitHubDialog.tsx). The dialog no longer auto-copies on submit. OnsubmitPublishRequest({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 underlyingnavigator.clipboard.writeTextcall rides cleanly. The submit button label changes from "Publish & copy link" to "Publish" to honestly reflect the two-step flow.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. Detectswindow.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 tonavigator.clipboard.writeTextfor browser contexts. This keeps the OK desktop app's share flow unconditionally reliable regardless of network timing.
Internal contract unchanged:
ShareActionDeps.clipboardWritekeeps its(text: string) => Promise<void>shape. Source-text guards inShareButton.test.tsandPublishToGitHubDialog.test.tsupdated to pin the adapter wiring (callers route throughscheduleClipboardWrite, not a directnavigator.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.gitobject store or thousands ofnode_modulesdirectories would exhaust the heap, crashing the server and dropping the editor's connection). Other normally-hidden content —.gitignored files, build output such asdist/— 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 directoryfollowed byfatal: 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-credentialargument 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 —ghoropen-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.ymloverlaid its values onto every descendant doc's effective frontmatter at read time (scalars replaced, arrays unioned), so a doc's frontmatter surfaced throughexec/searchdiffered from what was on disk. That made<folder>/.ok/frontmatter.ymldo 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.ymldescribes 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_configset-ruleaction andPUT /api/folder-configaccept any key (title/description/tagsare 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-configdrops the cascade-derivedfrontmatter_sourcesfield;DirectoryMeta.frontmatter_defaultsandEnrichedMetacascade 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_documentcreate, 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'saddhandler does), so a.mdxdoc the index never registered resolves to its real file rather thangetDocExtension's.mddefault (which would otherwise read a non-existent.mdpath and throwManagedRenameMissingDocumentError, or write a.mdsibling of the moved.mdx). The folder conflict pre-check inhandleRenamePathwas switched to the same disk walk for consistency. With docs enumerated, the existing spine rewrites inbound links, remaps CRDT identity, and returns an accuraterenamed[]/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
hasChildrenfield toDocumentListEntrySchemaso the depth-1 children variant ofGET /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/HEADwith 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 uiserving 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-placenpm i -gupgraded) build is served without restarting the server — previously the stale listing 404'd the new hashed bundles while still serving a freshindex.htmlthat referenced them. Adds ETag revalidation (304s on unchanged bundles), and returns a plain404for missing static assets instead of the APIproblem+jsonerror 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_startopens the pane at the UI root with no way to pass a target, so the agent armed one viaget_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_startreused 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 uinow redirects a base-openGET /straight to an armed pane target, so arm +preview_startlands 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#/docfragment on the follow-up request) and prevents an in-TTL reload from being yanked back to the armed doc.PaneTargetLandingin the app stays as a client-side backstop.- The bundled project skill now splits the guidance into two modes: arm +
preview_startto land a fresh open on a doc, and a one-steppreview_eval(window.location.hash = '#/<doc>') to move between docs once the pane is open — sincepreview_startcan't move an already-running pane. The no-screenshot rule gains a carve-out clarifying that navigating viapreview_evaldrives the view and is not a forbidden read-back verification loop.
fix(server): reject empty docName instead of silently writing to
test-docA mutating write whose
docNamewas empty or missing previously fell through to a hardcodedtest-docfallback:write_document({ docName: "" })returned "Written successfully" while overwritingtest-doc.md(and, with the defaultposition: "replace", clobbering any existingtest-doc). The response'spreviewUrlwas/#/, 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-docfallback —/api/agent-write,/api/agent-write-md,/api/frontmatter-patch,/api/agent-patch,/api/agent-undo— now rejects an empty/missingdocNamewith400 urn:ok:error:invalid-request("docNamemust 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.ymldeclared-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: descriptivefrontmatter.yml(the folder's owntitle/description/tags) + templates (the starting content and values for new docs).Removed: the
folder_configdeclare-field/remove-fieldactions, theschema.ymlreader/writer, thedeclarationschannel onPUT /api/folder-config, theschema_declarations/schema_local/schema_sourcesfields on the folder-config API and onDirectoryMeta/EnrichedMeta, theDeclarationMap/FolderSchematypes from@inkeep/open-knowledge-core, and the declared-field affordances in the property pane. Thewrite_document/edit_frontmatterprompts 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.ymlkeys 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.excludehard-errored, a hand-maintained list of keys (server.host,mcp.autoStart,upload.maxBytes, …) only warned, and others — including the headlinefoldersandappearance.editorModeDefault— were silently accepted as no-ops despite the docs still teachingfolders. A loose schema meant editors never flagged them either.All removed keys now route through one data-driven registry (
REMOVED_KEYSin@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.ymlfails fast so the user fixes it in place, while a stale user-global~/.ok/global.ymlis sidelined and Open Knowledge boots on defaults rather than bricking every project.ok config migratenow strips every registry key too (sourced from the same table), so the "runok 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/documentsresponse landed after the first selection-mirror commit. The mirror selected against an empty tree, and when the documents arrivedmodel.resetPathsrebuilt the tree without re-running the mirror, so the active row was revealed and its folder expanded but the row was never selected.useSelectionMirrornow re-runs ontreePathsSignature(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 branchGET /api/server-inforeadgetActiveBranch()without awaiting the server'sreadypromise. The branch is module-level state inpersistence.tsinitialized to the hardcoded'main'default and updated byswitchReconciledBaseScope(startupBranch)insideinitAsync. A renderer that fetched server-info during the boot window — betweenhttpServer.listenresolving andinitAsyncreaching the branch reset — observed'main'regardless of the actual HEAD. The client-sidecurrent-branch-storeis fire-once and only updates from CC1branch-switched, so the stale value stuck for the lifetime of the session until a real cross-branch checkout.handleServerInfonow mirrorshandleDocumentListand parks onreadybefore sampling.refreshServerInfoon the renderer also emitsbranch-changedwhen the observed branch advances, so any session running against an older server self-corrects on the next__system__reconnect.User-visible: the sidebar
ProjectSwitcherbranch row and editor footer branch label now match the Project Navigator's.git/HEADread, which was the only correct source before.Bound
GET /api/documents?showAll=truewith a hard entry cap (default 50000, tunable viaOK_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 optionaltruncatedflag 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 gaugeok.server.memory.usage_megabytes.Stream
GET /api/documents?showAll=trueas 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 negotiatesAccept: application/x-ndjsonthe 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 NDJSONAccept(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=truewalks. 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.),
onStoreDocumentthrew, Hocuspocus caught the rethrow and kept the doc in memory ("stays in memory to avoid data loss"), and the/api/agent-write-mdhandler — 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.handleAgentWriteMdforce-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-onlyOK_TEST_STORE_FAULTseam 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 theprocess/subdirectory (cpuprofile metadata, lsof, stacks, process-stats). Previously onlytelemetry/,logs/, andstate/were walked, so the stagedprocess/metadata.jsonstill 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 bundlesubstrate:- Pino file sink drained on shutdown.
bootServer.destroynow awaitsloggerFactory.flushAllFileSinks()aftershutdownTelemetry()so log records enqueued in the final moments beforeprocess.exit()land on disk. The previous shutdown drained theFileSpanExporter's appender chain but missed the separate Pino chain. - Project
enabled: falseis now honored.resolveLocalSinkConfigused schema-defaulted values to compare project vs project-local, which meanttelemetry.localSink.enabled: falsecommitted in the project config was silently overridden by the local file's defaultedtrue— 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.
ScrubbingSpanProcessornow 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 nolocalSinkoptions are supplied. - Pino log records are scrubbed too. When the file sink is wired,
PinoLoggerapplies Pino'sredact: { paths, censor: '[REDACTED]' }for every denylist key (plus*.{key}wildcards) so a caller who accidentally logsauthorization,cookie, etc., never ships the secret to disk or stdout. - Inverse-map hash collisions surfaced.
--redactnow records distinct doc names that hash to the same 8-hex prefix inmanifest.json.redaction.docNameCollisions(omitted when empty) so recipients of large-workspace bundles see when the inverse map is incomplete for a hash.
- Pino file sink drained on shutdown.
feat(telemetry): split file-sink and OTLP push gates so default installs persist spans locally
initTelemetry()now composes two pipelines on a singleBasicTracerProvider: the new file sink (config-gated viatelemetry.localSink.enabled, default-on) and the existing OTLP/HTTP push (env-gated viaOTEL_SDK_DISABLED). Both can run together; either can run alone.bootServerreads project + project-local config once at boot and wires the logger fileSink + file SpanExporter from the same resolved values, so a defaultok startnow captures recent spans + logs on disk under<contentDir>/.ok/local/{telemetry,logs}/forok diagnose bundleto harvest — noOTEL_SDK_DISABLED=falserequired, no LGTM stack required. LGTM stack users still setOTEL_SDK_DISABLED=falseto 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.jsonrecipe runsok start(via a portable# ok-ui-v1bundle→npx shell chain) rather than bareok ui.ok startgains a--ui-portflag 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_documentnow accepts emptymarkdownwithposition: "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. Emptyappend/prependis 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_documenthonors an explicit.mdxextension when authoring a new docwrite_document({ docName: "x.mdx" })stripped the.mdxand always createdx.md, so there was no way to author a.mdxdocument 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 (
normalizeDocNamestill strips it for keying) and, on a create, forwards it to the write path so a.mdxrequest lands a.mdxfile (.mdor no suffix still defaults to.md)./api/agent-write-mdaccepts an optionalextension(.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.mdxrequest 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.