v0.4.0
Part of the OpenKnowledge changelog.
Minor Changes
feat(api): RFC 9457 Problem Details envelope across all HTTP handlers (api-design-hardening)
All 57 handlers in
packages/server/src/api-extension.tsnow share a single canonical wire format:- Errors emit
Content-Type: application/problem+jsonwith a flat body{ type, title, status, instance?, detail? }per RFC 9457. Thetypefield is a closed-enum URN of the formurn:ok:error:<kebab>(per RFC 9457 §3.1.1 — URN form is routing-independent and won't change meaning under reverse-proxy / path-prefix). Thetitleis a required short English summary;instanceis a UUID correlation ID emitted alongside the structured Pino log line for grep-correlated triage;detailis an optional longer explanation. - Success drops the
{ ok: true, ... }wrapper and emits a flat{ ...data }body withContent-Type: application/json. Clients narrow on the HTTP status code (if (!res.ok)) before parsing — the RFC 9457 two-step parse pattern.
This is a wire-format breaking change for any direct in-process consumer of the HTTP API. The
@inkeep/open-knowledgeMCP shim's internalhttpGet/httpPosthelpers wrap the new flat success body with{ok: true, ...body}for in-process MCP tools so existingif (!result.ok) return errorshort-circuits keep working — MCP's own{content, isError?}envelope is unchanged.Structural enforcement:
- Per-handler
XyzRequestSchema+XyzSuccessSchemaZod schemas live in@inkeep/open-knowledge-core/schemas/api. Every schema exportssatisfies StandardSchemaV1<...>. - Request bodies validated through a
withValidation()middleware wrapper atpackages/server/src/http/request-validation.ts(handlers can't be added without going through it). - Errors emit through
errorResponse(res, status, type, title, options)atpackages/server/src/http/error-response.ts— the only sanctioned site (packages/app/tests/integration/error-envelope-coverage.test.tsruns in fail-on-any-occurrence mode and AST-scansapi-extension.tsfor inline{ ok: false, ... }and{ ok: true, ... }literals). - NDJSON streaming endpoints (clone, auth-login, auth-repos) emit pre-stream errors through
errorResponseand mid-stream errors throughstreamingProblemEvent({type: 'error', problem: ProblemDetails})events — typed envelope preserved across the streaming protocol. - Closed-enum
ProblemTypeURNs (~40 tokens) plusassertNeverProblemType/assertNeverLinkTargetexhaustiveness helpers are structurally enforced bypackages/app/tests/integration/exhaustiveness-coverage.test.ts— derived from the schema at test-discovery time so the registry never drifts. - Telemetry:
ok.api.error.count{type, handler}counter increments on every error emit.
Client lockstep: 23 sites in
packages/app/srcreadingdata.ok/body.ok/raw.okmigrated to the two-step parse pattern.class HttpResponseParseErrordistinguishes contract-shape responses (RFC 9457 problem details) from non-contract responses (proxy 502 HTML, network failures).Defense-in-depth: SVG is in
IMAGE_EXTENSIONS(so the editor's<img src=svg>rendering still works — browsers ignore Content-Disposition for embed contexts) but excluded fromINLINE_RENDERABLE_EXTENSIONSso top-level navigation to.svg(web fallbackwindow.openfrom a markdown SVG link) downloads instead of executing embedded<script>under same origin. Aligns with Docmost's posture; cf. GHSA-rcg8-g69v-x23j (Plane SVG XSS).Full spec + decision log (D1–D38, US-001 through US-014):
specs/2026-04-30-api-design-hardening/SPEC.md. Canonical pattern guide:packages/server/src/http/README.md.- Errors emit
feat(editor): asset upload +
![[file.ext]]wiki-embed surfaceAny file drop is accepted by the editor — there is no user-facing byte cap. PDFs, video, audio, archives, and fonts stop hitting the old "Unsupported file type" dead-end. The emit shape is picked by extension: markdown files (
.md/.mdx) emit as[[basename]]wiki-links (link-semantic, navigable on Cmd-click, resolved viafileIndex— markdown is a first-class OK doc, not an opaque asset); images + typed renderable files (PDF, MP4, WebM, MP3, WAV, OGG, M4A, MOV) emit as![[file.ext]]wiki-embeds; opaque files emit as[name](path)markdown links. Uploads stream to disk end-to-end (memory footprint is O(1), not O(fileSize)), so the only rejection axis is disk fullness (storage-full→ HTTP 507). Seereports/streaming-upload-refactor/REPORT.mdfor the refactor rationale.Same-directory sha256 dedup returns existing paths on duplicate drops with a toast (
"Already at <path> — reusing."). Renaming a doc that contains image refs recomputes the relative path; absolute refs and wiki-embed refs are untouched because the basename index resolves them dynamically.New HTTP surface on the server:
POST /api/upload— upload endpoint. Success response (perUploadAssetSuccessSchemain@inkeep/open-knowledge-core): flat{ src, path, deduped, sha?, byteLength? }wheresrcis the asset's basename andpathis the contentDir-relative location (colocated with the referencing doc). Error responses are RFC 9457 problem details (application/problem+json) withtype ∈ {urn:ok:error:malformed-upload, urn:ok:error:storage-full, urn:ok:error:storage-readonly, urn:ok:error:collision-exhaustion, urn:ok:error:storage-error}plustitle,status, andinstancecorrelation UUID. See theapi-design-hardeningchangeset for the cross-handler RFC 9457 envelope.
No user-facing
upload.*config. Attachment placement (co-located), emit shape (![[...]]for supported extensions), same-directory sha256 dedup with a toast notice, and the wiki-embed extension list are fixed defaults. Every value is a module-level constant in@inkeep/open-knowledge-core/constants/upload.ts. One-shot Obsidian-vault migration CLI deferred to a future spec — OK does not read.obsidian/app.jsonat runtime; refugees whose vault uses non-default config shape wait for the future migrator. Legacy configs still carryingupload.*keys parse cleanly (unknown keys are silently stripped).File watcher now emits
asset-create/asset-deleteDiskEvents alongside the existing markdown events; CC1ch:'files'signal coalesces both so file-sidebar and basename-index rebuilds piggyback on one broadcast.sanitizeFilenamepreserves Unicode code points (letters, digits, marks, punctuation, emoji) while stripping path separators and control bytes.Full spec + decision log (D1–D-M):
specs/2026-04-16-editor-asset-and-embed-surface/SPEC.md. Operator-facing guide: Assets and embeds.Asset-click dispatcher + OS-integration surface (2026-04-23 amendment). Click a
![[meeting.pdf]]embed and the PDF opens predictably — a new browser tab in web,shell.openPathin Electron. Previously post-reload clicks routed through the doc-link navigator and failed silently (Gap 3b); Electron drop-time clicks replaced the editor window (Gap 4). Both gaps close.ClassifiedLinkTargetgains a first-class{kind: 'asset', url, ext}variant;resolveAssetProjectPathresolves relative hrefs against the source doc's directory.- Renderer-side dispatcher + empty-at-landing viewer registry at
packages/app/src/editor/asset-dispatch/— future PRs register PDF.js / image lightbox / video-audio viewers as ~40-60 LOC plugins without modifying the dispatch layer. - Three new Electron IPC channels (
ok:shell:open-asset,ok:shell:reveal-asset,ok:shell:show-asset-menu). Main-processopenAssetSafelyenforces path containment (realpath+isPathWithinProject), existence, and an executable-extension blocklist (.exe/.sh/.html/.svg/…) source-verified from Obsidian 1.12.7. Renderer sends project-relative paths; containment fires at the IPC boundary. - Right-click any on-disk reference (asset chip, wiki-link chip, image) → native OS menu with Reveal in Finder / Show in Explorer + Open in default app + Copy link. Gesture-attested (main observes the click directly).
- Defense-in-depth:
setWindowOpenHandler+will-navigateon the editor webContents intercept any asset URL that escapes the renderer dispatcher (pasted<a href>, plugin content, drop-time<a target="_blank">). Same path containment + blocklist enforced on every entry point.
Full amendment (US-A1..A6, FR-A1..A8, NG-A1..A6, D-A1..A12):
specs/2026-04-16-editor-asset-and-embed-surface/SPEC.md§Post-finalization amendment (2026-04-23). Research:reports/electron-os-integration-patterns/+reports/editor-asset-embed-patterns-across-universe/D9.feat: Component Blocks v2 — 5-pack foundation (Callout + Image + Video + Audio + Accordion)
The editor now ships five built-in component primitives —
Callout,Image,Video,Audio, andAccordion— each with a WYSIWYG settings panel, a slash-command insertion menu, and lossless on-disk round-trip for both the MDX form and the markdown form (where one exists). Every primitive is a DIY React component on Open Knowledge's own brand (shadcn / Tailwind); the editor bundle no longer pulls infumadocs-ui's React surface or its CSS variable bridge.What you get out of the box:
- Callout — five GFM alert types (
note/tip/important/warning/caution) plus optionaltitle/icon/color/ and Obsidian-style foldable chrome (> [!NOTE]+/-). Authoring works in any of three forms: GFM alert blockquote, foldable Obsidian opener, or<Callout type="…">…</Callout>MDX JSX. Common alias tokens (success→tip,danger→caution, etc.) fold to the GFM 5 on disk. - Image —
<Image src=… alt=… width=… caption=… />MDX, plus standard CommonMark. Both forms render through the same descriptor with click-to-zoom on by default; the MDX form additionally exposescaption(renders as<figure>+<figcaption>), explicit dimensions, andloading/zoomtoggles. - Video — pure HTML5
<video>wrapper with native controls. No YouTube / Vimeo URL sniffing — embed services with a raw<iframe>in MDX (matches Mintlify's pattern).<track>and<source>children round-trip. - Audio — pure HTML5
<audio>wrapper with native controls always on.<source>and<track>children round-trip. - Accordion — standalone HTML5
<details>/<summary>substrate, no wrapper component required. Cross-browser exclusive grouping via HTML5<details name="…">(Chrome 120+, Safari 17.2+, Firefox 130+). Authors can write either<details><summary>X</summary>Y</details>or<Accordion title="X">Y</Accordion>— both render the same descriptor.
Other improvements:
- Auto-generated settings panel from each component's prop types (string / boolean / number / enum) — no separate component prop docs required.
- Slash-command insertion with sensible defaults; the settings panel auto-opens on insertion so required fields are filled in before you move on.
- Hover chrome with move-up / move-down / delete / settings buttons.
- Keyboard navigation throughout (Tab / Esc / arrow keys with context-aware handling).
- Broken or unrecognized MDX components automatically open in an embedded source-code editor so authored content stays editable — nothing silently disappears.
- Both pristine and dirty save paths preserve the on-disk shape: unedited blocks round-trip byte-for-byte; edited blocks canonicalize to the MDX JSX form.
Breaking changes:
- Both the inline MDX element node (
jsxInline) and the block MDX component node (jsxComponent) changed PM-schema shape in this release.jsxInlinedrops itsattributesandsourceRawattrs — its text content IS the source of truth.jsxComponentwidens from an atom with a raw-content attr to a non-atom block withblock*children and new structured attrs (componentName,kind,attributes,sourceRaw,sourceDirty,props). This is a load-bearing change for collaborative editing — older clients coexisting with this version in the same live session substitute both nodes torawMdxFallback(raw source preserved as editable text) via the y-tiptap schema-throw substitution patch. Upgrade all clients in a session together — both inline JSX authoring and component-block authoring are affected, not just inline. Persisted documents are unaffected; the on-disk MDX is preserved. - Content using component names that are no longer built in
(
Tabs,Card,CardGroup,Steps,Banner,Files,TypeTable,InlineTOC,Mermaid,AudioPlaceholder,ImageZoom) opens as an editable raw-source block. Content is preserved verbatim. Rename<AudioPlaceholder />→<Audio />and<ImageZoom>→<Image>to pick up the new descriptors.
The compound-component tier (Tabs + Tab grouping, Accordion grouping with shared chrome, Steps + Step) is not built in today; it returns when concrete dev-docs / help-center authoring demand surfaces. No public API will change for existing 5-pack consumers when that happens.
Bundle size:
- Main app bundle stays flat (~210 kB gzipped) — the
fumadocs-uidrop and 12-descriptor cut offset the 5-pack prop-surface widening and new selection-chrome plugins. - Total JS across lazy-loaded chunks grows ~100 kB gzipped (~978 kB →
~1.08 MB) to accommodate CB-v2 feature surface (descriptor-dispatch
registry, V2 editor cache, SelectionStatePlugin + Breadcrumb +
SelectionAnnouncer + BlockDragHandle, nested CodeMirror for
rawMdxFallback, slash-command menu, canonical/compat descriptor split with three additional read-only source-form descriptors (GFMCallout, CommonMarkImage, HtmlDetailsAccordion) for round-trip preservation). Theall JS chunks combinedsize-limit ceiling is raised 1050 → 1100 kB (~2% headroom) to match. Delivered via on-demand chunk loading — users don't pay the full bill on first paint.
- Callout — five GFM alert types (
Config Editing Paths — end-to-end UX for editing Open Knowledge configuration:
- Settings pane in the editor area (Cmd-, / App menu / HelpPopover / Command Palette) with
This projectandUserscope tabs. Each field auto-saves; per-field reset; modified-at-scope indicator on cross-scope fields. - Real-time sync — Settings pane is bound to two Y.Text-only synthetic Hocuspocus docs (
__config__/workspace,__user__/config.yml). External edits via CLI, MCP, IDE hand-edit, or anotherok startinstance propagate via a chokidar file watcher into Y.Text and refresh any open pane within ~500ms. - Three-layer defense-in-depth validation — client walker (L1) → fs writer (L2) → persistence-hook (L3). Invalid mutations revert to LKG and surface a toast + brief field flash.
- MCP tools —
get_config,set_folder_rule. fs-direct (no running server required); auto-scope inference via the inspectConfig ladder; mixed-scope rejection. (set_configwas subsequently removed in the schema-simplification changeset; seeconfig-schema-simplify.md.) - CLI —
ok config validate(exits 0/1 with source-located errors) +ok config migrate(idempotent codemod that dropssync.*,persistence.{debounceMs,maxDebounceMs},server.port). ok initscaffolds the workspaceconfig.ymlwith a magic-comment$schemaURL pinned to the schema major (v0) +@latestof the npm package — additive schema changes reach existing users automatically; breaking changes bump the path tov1and old majors stay published forever.- Per-scope JSON Schemas —
dist/schemas/v0/config.workspace.schema.jsonand…/config.user.schema.jsonso VS Code's Red Hat YAML LSP only suggests fields valid AT the file's scope. - Schema cleanup — drops
sync.*(7),persistence.{debounceMs,maxDebounceMs}(2),server.port(1); addsappearance.themeandappearance.editorModeDefault(user-scope, both UNSET by default; chrome<ThemeToggle>writes throughuserBinding.patchso localStorage stays a derived cache).content.*is workspace-scope-only. - OTel — five new
config.*spans (config.bind,config.patch,config.validate,config.persist,config.revert) trace the full edit chain.
- Settings pane in the editor area (Cmd-, / App menu / HelpPopover / Command Palette) with
Config schema simplification — six fields removed, replaced with constants:
- Schema fields removed —
github.oauthAppClientId,server.host,server.openOnAgentEdit,mcp.autoStart,mcp.tools.read_document.historyDepth,mcp.tools.grep.maxResults. None were genuinely user-configurable in practice. Their default values now live as plainexport constin@inkeep/open-knowledge-core(constants/github.ts,constants/mcp.ts,constants/server.ts):DEFAULT_GITHUB_OAUTH_CLIENT_ID,DEFAULT_SERVER_HOST,READ_DOCUMENT_HISTORY_DEPTH,GREP_MAX_RESULTS. Configtype contract — TypeScript consumers readingconfig.server,config.github, orconfig.mcpwill now getnever. The schema's top-level surface shrinks tocontent,preview,appearance,autoSync. Layered config infrastructure (project/user merge, scope inference,__config__/project+__user__/config.ymlY.Text transports) is unchanged for the day a real user-configurable layered field needs it.set_configMCP tool removed — the agent-settable surface was empty after the twomcp.tools.*fields became constants.get_configandset_folder_ruleremain. Re-introduce when an agent-tunable field actually returns.- Runtime overrides preserved —
--hostflag andHOSTenv var (resolved at the start command via the newresolveHost()helper, mirroring the D29 port pattern);OPEN_KNOWLEDGE_GITHUB_CLIENT_IDenv var;OK_MCP_AUTOSTART=0env var. server.openOnAgentEdit— the auto-open-browser-on-first-agent-write feature was deleted entirely (default was off; collapsed dead conditional).- Loose-mode tolerance — existing user/project YAML configs that still set the removed keys parse without throwing; the loader emits a per-key deprecation warn pointing at the right knob (
--hostflag,HOSTenv,OPEN_KNOWLEDGE_GITHUB_CLIENT_ID,OK_MCP_AUTOSTART=0, or "hardcoded in@inkeep/open-knowledge-core" when no override exists). Oldmcp.tools.search.maxResults(pre-#491 rename) is also flagged. - Settings pane — Server, GitHub, and MCP sections removed.
set_configMCP tool description and reference docs cleaned up.
Migration notes for consumers:
- Reading
config.server.host,config.mcp.autoStart, etc. → switch to runtime override (--host/HOST,OK_MCP_AUTOSTART=0) or import the constant directly from@inkeep/open-knowledge-core. - Calling
set_configMCP tool → use the Settings pane or hand-editconfig.yml. Useset_folder_rulefor folder rules;get_configfor reads.
- Schema fields removed —
feat(desktop): replace the "Start fresh" Welcome card with an in-app "Create new project" dialog.
The third Welcome card now opens a shadcn dialog asking for a name and parent location, with checkboxes for which AI editors to wire on first run. A pre-submit cascade surfaces structural conditions before any filesystem write:
- Nested-project block — when the picked parent sits inside an existing Open Knowledge project, the dialog renders a red banner naming the enclosing rootPath with an inline "Open
" action. Create is disabled. - Git-root promote confirm — when the picked parent is inside a git working tree with no enclosing
.ok/, the dialog renders a blue banner explaining that.ok/config.ymlwill land at the git root withcontent.dirscoped to the new folder (one project per git repo). Create stays enabled. - Target-non-empty block — when the resolved
parent/<name>already exists with content, the dialog renders a neutral banner pointing the user at "Open folder on disk" instead.
The IPC handler (
ok:project:create-new) re-runs every check server-side as defense-in-depth — a stale renderer state or a hostile renderer cannot scaffold a project inside an existing one.Breaking (internal-only surfaces):
OkProjectEntryPointvalue'start-fresh'is removed; replaced by'create-new'and the new'create-new-nested-redirect'value (fired by the red banner's inline action).bridge.dialog.createFolder, theok:dialog:create-folderIPC channel, anddialog-helpers.ts'spromptForFolder(createDirectory variant) are deleted. The Pick-existing flow keepspromptForExistingFolderunchanged.- The silent-scaffold branch in
openProject(entryPoint'start-fresh') is gone — every project create now goes through the dialog with explicit user consent on AI-editor wiring.
No public API change —
@inkeep/open-knowledge(the CLI) is unaffected.Telemetry continuity. The
ok.desktop.onboardingConsentspan keeps its shape; theflowKindenum gains'create-new-default'(all editors selected, the canonical happy path) and'create-new-customized'(one or more editors toggled off). The legacy'fresh-silent'variant is no longer emitted; downstream dashboards should treat'create-new-default'as its successor during the transition window.Spec:
specs/2026-05-10-create-new-project-dialog/SPEC.md.- Nested-project block — when the picked parent sits inside an existing Open Knowledge project, the dialog renders a red banner naming the enclosing rootPath with an inline "Open
feat(desktop): per-project onboarding consent dialog + git-root promotion + boot-time config load.
Opening a folder in the macOS desktop app no longer scaffolds
.ok/silently. Two paths now split on the user's Navigator gesture:- Pick Existing Project (and Recents, deep-link, drag-drop) → a per-window consent dialog opens to confirm scaffolding details before any filesystem write. The dialog covers content directory (with a Browse button for sub-folder selection),
.okignorepatterns, AI-tool integration multi-select (all editors checked by default — explicit, not detected), and sensitive-path warnings (/,~,~/Documents,~/Desktop,~/Downloads,/Volumes/<mount>). Git is initialized implicitly when the picked path has no real.git/— no UI toggle. Picking a folder via the dialog == agreeing to scaffold; users who don't want OK in their folder simply Cancel. - Start Fresh → silent path; defaults applied without prompting.
Re-opening a folder that already has
.ok/never re-fires the dialog.One
.ok/per git repo (G11). When the picked path sits inside a git working tree and no ancestor.ok/is present,.ok/materializes at the git working-tree root andcontent.diris pre-filled with the picked sub-path. Eliminates orphan sibling.ok/s within the same repo and aligns OK's project boundary with git's. Applies to desktop (Start Fresh + Pick Existing) AND CLI (ok init).Sub-folders of an OK-managed project promote to the ancestor
.ok/. No nested.ok/ever materializes. The picked sub-path is dropped; the editor opens at the ancestor with a 4 s "Opened existing OK project at <ancestor>" toast.Desktop boot now reads project config. The per-window utility calls
readConfigSafelyagainst.ok/config.ymlBEFOREbootServer, socontent.dirfrom the project file wins over the IPC default. Invalid YAML logs[config] desktop boot config invalidand falls back to schema defaults; the editor still opens.ensureProjectGithardened. Now validates.git/HEAD(not just.git/) and auto-repairs the shell-.git/regression class — folders left with.git/ok/(the shadow subtree) but no.git/HEADget a single silentgit initon next open.git statusworks in OK projects from the moment OK touches them..git/ok/is preserved;Save Version's parent-git path producesok/v<N>tags going forward.CLI parity:
ok initfrom a git sub-folder writes.ok/at the git root via the new sharedresolveProjectRoothelper, withcontent.dirscoped to the sub-path. Surfaces a single disclosure line on first-time scaffold:[ok] Initialized OK at <gitRoot> (scoped to <subPath>/)for git-root promotion,[ok] Opened existing project at <ancestor>for ancestor promotion.ok startoperates against the cwd directly and does not scaffold — single-responsibility per command,initinitializes,startstarts.OkDirMissingErrorfires loud when.ok/is missing.
New OTel span. Each onboarding flow emits one
ok.desktop.onboardingConsentspan with bounded-cardinality attributes (flow_kind,entry_point,git_init_requested,content_dir_changed,warnings_count,ai_integrations_failed_count). No raw paths.New degraded subsystem.
'project-git-shell-only'joinsshadow-repo/head-watcher/file-watcherin the utility'sdegradedarray whendiscoverProjectsaw a shell-only.git/even after auto-repair (defense-in-depth — should not occur in practice).Spec:
specs/2026-05-07-desktop-onboarding-consent/SPEC.md.- Pick Existing Project (and Recents, deep-link, drag-drop) → a per-window consent dialog opens to confirm scaffolding details before any filesystem write. The dialog covers content directory (with a Browse button for sub-folder selection),
feat(frontmatter-editing-ux): top-of-document property panel + per-key
Y.Map('metadata')storage +frontmatter_patchMCP tool. Frontmatter is now editable inline in WYSIWYG mode through typed widgets, and concurrent edits from a human and an agent to different properties merge at the field level instead of clobbering each other through document-level last-write-wins.@inkeep/open-knowledge-core— newpackages/core/src/frontmatter/module exportingFrontmatterValueSchema,FrontmatterPatchSchema,FRONTMATTER_TYPES, and the comment-preserving YAML codec (parseFrontmatterYaml/serializeFrontmatterMapoveryaml@2.x'sparseDocument). Bridge readers/writers inpackages/core/src/bridge/frontmatter-y.tsextended withgetFrontmatterMap,setFrontmatterFromYaml,setFrontmatterProperty, andcomposeFrontmatterForStore.getFrontmatter(doc)now synthesizes from per-key entries when present, falls back to the legacy single-string slot otherwise — existing string-shape callers continue to compile unchanged.@inkeep/open-knowledge-server—Y.Map('metadata')now carries one entry per frontmatter property (Y.Textfor editable strings,Y.Array<Y.Text>for lists, primitives for atomics). NewPOST /api/frontmatter-patchroute +handleFrontmatterPatchhandler applies JSON Merge Patch (RFC 7396) atomically under a per-session, not pairedformOrigin. Observer A's metaMap deep-observer recomposes YAML+body and propagates toY.Textafter settlement.onLoadDocumentruns an eager-on-load migration;applyExternalChange(file watcher) and Observer B reconciliation use per-key diff so undoing a single property reverts only that property.onStoreDocument'scomposeFrontmatterForStorewrites the legacy YAML byte-string verbatim when the per-key map still matches it — comments, blank lines, and scalar styles round-trip losslessly.agent-patch(/api/agent-patch) returns HTTP 400 on FM-intersecting find/replace calls with a migration hint pointing atfrontmatter_patch. New OTel spansfrontmatter.patch+frontmatter.form_write; new counterok.frontmatter.edit_surface_totallabels writes by source (form/mcp-patch/mcp-write/file-watcher/source-mode).@inkeep/open-knowledge— newfrontmatter_patchMCP tool. Set / create / delete frontmatter properties with{patch: {key: value | null}}; optionaltypesmap overrides per-key widget inference (text / number / boolean / date / list); optionalsummarythreads through to the per-contributor attribution journal under the same 80-char cap as the other write tools.@inkeep/open-knowledge-app— new top-of-document Properties panel above the body in WYSIWYG mode. Five widget types (Text / Number / Boolean / Date / List), inline add / delete / rename, type picker dropdown, per-row hover chrome, collapse via chevron, empty-state seeded via the editor toolbar's Add Properties button. All form interactions wire throughPOST /api/frontmatter-patchwithsource: 'form'.
Storage migration is automatic on document load — no user action required. The legacy single-string
metaMap.get('frontmatter')slot is retained as a transitional byte-identical mirror so YAML comments and scalar styles survivedoc-load → no-op-edit → doc-saveround-trips.frontmatter_patchis the only MCP surface for frontmatter edits going forward; the soft-deprecation window foragent-patchFM-touching calls is closed and those now return HTTP 400.Full spec + decision log:
specs/2026-04-24-frontmatter-editing-ux/SPEC.md.Add the Gbrain starter pack — entity-grounded second-brain layout (typed dossiers for
people/,companies/,meetings/,concepts/,originals/,media/) with compiled-truth-above / append-only-timeline-below body convention, per-folder templates, agent-readable folder frontmatter, and five root files (USER.md,SOUL.md,ACCESS_POLICY.md,HEARTBEAT.md,log.md). Selectable asGbrainin the desktop app's pack picker or viaok seed --pack gbrain. Pattern inspired by Garry Tan's gbrain; OK ships the layout natively.Also ships two new docs-site guides under
docs/content/workflows/:karpathy-llm-wiki.mdx— source-grounded knowledge base via the existingknowledge-basestarter pack, mapping 1:1 onto Karpathy's three-layer pattern (external-sources/→research/→articles/) with theingest/research/consolidateMCP tools.gbrain.mdx— entity-grounded second brain via the newgbrainstarter pack, demonstrating the dead-links → triage → WYSIWYG loop and end-to-end agent-driven dossier maintenance using OK's MCP surface.
Both guides land under a new
Workflowssection in the docs IA (docs/content/meta.json+docs/content/workflows/meta.json).Pinned-count test in
starter.test.tsupdated 5 → 6 to include the new pack.feat(mcp):
ok mcpis now an inline stdio MCP server with per-call project routing.The default mode replaces the previous stdio→HTTP shim that bound to a single project at startup. One stdio process can now serve any number of OK projects on the host: each tool call resolves its own
cwdargument, walks up to the nearest.ok/directory (must be a directory; regular files and dangling symlinks named.okare rejected), loads the matching project config, and proxies HTTP traffic to that project's runningok start(auto-spawned whenOK_MCP_AUTOSTARTis unset or non-zero).This makes it safe to register
ok mcponce globally in MCP hosts (Claude Code, Cursor, Codex). The host can call any tool against any local OK project by passing an absolutecwdargument.Breaking change for direct MCP clients: the global path requires an explicit absolute
cwdon every tool call. The previous in-project mode silently defaulted to the configured project root. The global server has no fixed root and throws a clear error whencwdis omitted. Tool-arg descriptions have been tightened to document the new contract. The legacy--port <port>mode (single-backend stdio→HTTP shim) is unchanged.Existing on-disk MCP host registrations from prior
ok initruns continue to register the binary; modern MCP-aware agents will read the updatedcwddescription and pass it per call.MCP search/grep realignment — rename the existing
searchtool togrep, add a new rankedsearchtool that mirrors cmd-K.- Tool removed:
search(the previous tool — fixed-string grep with frontmatter enrichment). - Tool added:
grep— replaces the legacysearchtool with identical behavior. Use it when you need every literal occurrence of a string across content. - Tool added:
search— new ranked workspace retrieval. WrapsPOST /api/searchso results match the cmd-K palette (Orama-backed lexical + body BM25 + recency). Parameters:query,intent('omnibar' | 'full_text', default'full_text'),scopes,limit(default 20, max 100),cwd. Both new tools register MCPannotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }. - Config key: the grep tool's result cap is
mcp.tools.grep.maxResults(default 50). - Server-side instructions (
buildInstructions) now surface the four-way read-tool routing (exec/read_document/search/grep).
Migration:
- Rename any caller using
mcp__open-knowledge__searchfor grep semantics tomcp__open-knowledge__grep. - Switch to the new
mcp__open-knowledge__searchfor relevance-driven retrieval (the same ranking the cmd-K palette uses). - If your
.ok/config.ymlcontainsmcp.tools.search.maxResults, rename the key tomcp.tools.grep.maxResults. The legacy key is silently ignored (loose-object pass-through), so a custom value will not carry over automatically.
Spec:
specs/2026-05-05-mcp-search-grep-rename/SPEC.md. Background research:reports/file-search-tool-mcp-exposure/REPORT.md.- Tool removed:
feat(seed): multi-scaffold templates picker — five Initialize packs + per-folder extra templates + opt-out personal-templates pack
Generalizes the single-scaffold
Initialize LLM brainseed into a five-card picker inSeedDialog:- Knowledge base (existing, renamed from "LLM brain") — Karpathy three-layer source-grounded KB (
external-sources/→research/→articles/). - Software lifecycle —
proposals/+decisions/+specs/+postmortems/+guides/. Industry-current naming perdotnet/designs/withastro/roadmap/ Google Cloud ADR doc /github/spec-kit.specs/shipsspec+spec-plan+spec-taskstemplates so thegithub/spec-kitper-spec triple shape is one click each.guides/shipsguide+onboarding-guide+runbook. - Plain notes —
notes/+daily/. Escape hatch for casual users. - Worldbuilding —
characters/+settings/+themes/+factions/+lore/(Fiction variant).factions/shipsfaction+political-faction+religion;lore/shipslore+magic-system+historical-event. - Writing pipeline —
ideas/+drafts/+published/. Lean three-stage; book pipeline deferred to a future pack.
New public surface:
STARTER_PACKS: Record<PackId, StarterPack>registry (server).PackIdis a closed enum.StarterFolder.extraTemplates?: readonly string[]— additional templates installed alongside the starter in<folder>/.ok/templates/. Picker pre-selects the starter; extras available viaNew from template….resolvePack/coercePackId/isKnownPackId/listStarterPackshelpers — single source of truth for HTTP, IPC, and CLI.GET /api/seed/packs+okDesktop.seed.listPacks()— enumerate available packs (returnsid,name,description,defaultSubfolder?,folders[]with per-folder summaries). Schemas:SeedListPacksSuccessSchema+SeedPackInfoSchema+SeedPackFolderInfoSchema.planSeed/applySeedacceptpackId+includePersonalTemplatesoptions.ScaffoldPlancarries optionalpersonalTemplatespreview;ApplyResultcarries optionalpersonalTemplateswrite summary.- HTTP / IPC / CLI all reject explicit-but-unknown
packIdwith a structured error (trust-boundary symmetry). /api/seed/applyadded toMUTATING_ROUTESfor DNS-rebinding defense-in-depth.- CLI
ok seedgains--pack <id>+--list-packs+--personal-templatesflags. DefaultpackIdstaysknowledge-basefor back-compat.
Personal-templates pack (opt-out, default checked):
- Seven universal templates land at user scope (
~/.ok/templates/):daily-journal,meeting-notes,weekly-review,reading-log-entry,gym-log,recipe,travel-trip. - Idempotent at file level — never overwrites existing user-edited templates.
OK_USER_HOMEenv override is gated onNODE_ENV === 'test'so a stray env var can't misdirect production writes.
Back-compat preserved:
- Legacy
STARTER_FOLDERS,STARTER_TEMPLATES,LOG_MD_TEMPLATEexports are@deprecatedaliases pointing atSTARTER_PACKS['knowledge-base']. - Default
packId = 'knowledge-base'everywhere; callers that don't passpackIdget the same behavior as before. okDesktop.seed.plan()/okDesktop.seed.apply(plan)still callable without options.
Empty-state probe simplified: previously checked only the Knowledge base pack, leaving projects seeded with other packs stuck showing the CTA forever. Now keys off
documentCount === 0— works for all packs.Bridge contract:
OkSeedPlanOptions/OkSeedApplyOptions/OkSeedPackInfo/OkSeedListPacksResultadded; three copies (core, app, desktop) kept in lockstep; drift catcher passes.OkScaffoldPlangains optionalpersonalTemplates;OkSeedApplyResultgains optionalpersonalTemplates.Spec:
specs/2026-05-08-multi-scaffold-templates-experience/SPEC.md(D1–D14 LOCKED). Research:reports/llm-brain-scaffolds-research/{REPORT,PROPOSAL,USE-CASES,PACK-REFINEMENT-2026}.md.- Knowledge base (existing, renamed from "LLM brain") — Karpathy three-layer source-grounded KB (
feat(rename): rename
.open-knowledge/→.ok/everywhere (per-project,~/.ok/, and the shadow repo at.git/ok/); replacecontent.includeandcontent.excludeinconfig.ymlwith a.okignorefile at the project root using gitignore syntax.Hard cutover for the directory rename (pre-release license to break — no auto-rename of
.open-knowledge/→.ok/):- The per-project state directory is now
.ok/instead of.open-knowledge/. Same for the user-global directory (~/.ok/) and the shadow repo (.git/ok/). content.includeandcontent.excludeare removed fromConfigSchema. If they appear in yourconfig.yml, OK rejects the file with a source-located error directing you to move the patterns into a project-root.okignore..okignoreuses gitignore syntax (parsed by theignorenpm library) and is evaluated alongside.gitignorein a single ignore-lib instance — cross-source!overrides work (e.g.!secret.mdin.okignorere-includes a file.gitignoreexcluded). Nested.okignorefiles at any folder depth are honored.ok initnow scaffolds both.ok/and a project-root.okignore(commented header, no example excludes).- The Settings pane's Content section is removed;
content.dirbecomes YAML-only (default.covers the common case). - The MCP
set_configallowlist drops to 3 paths:folders[],mcp.tools.search.maxResults,mcp.tools.read_document.historyDepth.
For pre-existing OK projects:
- Rename your
.open-knowledge/directory to.ok/(manual — no auto-rename shim). - Lift any
content.include/content.excludepatterns into a project-root.okignore(recreate exclusion patterns; remember the.okignoremental model is exclude-only). - Run
ok config migrateto strip the obsoletecontent.{include,exclude}keys fromconfig.yml. The codemod is idempotent and also clears the other deprecated keys (sync.*,persistence.{debounceMs,maxDebounceMs},server.port). - Delete the orphan
.git/open-knowledge/shadow repo. - Re-authenticate.
The dogfood team will see one re-prompt for stored credentials (
~/.ok/auth.yml), first-launch consent (~/.ok/mcp-status.json), and the skill-installed marker on first run after merging — expected behavior for the hard cutover on the user-home rename.The protected identifiers (MCP server name
open-knowledge, writer-IDopenknowledge-service, bundle IDcom.inkeep.open-knowledge, URL schemeopenknowledge://, package names) are unchanged.- The per-project state directory is now
Remove the
preview.baseUrlconfig field and theOPEN_KNOWLEDGE_PREVIEW_BASE_URLenvironment variable. The deployed-wiki use case isn't supported in this greenfield; thepreviewUrlMCP resolver now collapses toelectron-protocol → lockand emits URLs that point at the running UI process only. Migration: start a local UI withok ui(or the desktop app) — preview URLs resolve from theui.lockit writes. Stalepreview: { baseUrl: ... }keys in.ok/config.ymlare accepted silently bylooseObjectbut ignored; the CLI loader now emits a deprecation warn pointing atok ui.feat(rename): consolidate
/api/renameand/api/rename-pathinto a single polymorphic endpoint, lift the link-rewrite spine, extend the recovery journal to v2, and add principal-attribution fallback for rename + rollback handlers.Breaking change —
POST /api/renameis removed. Clients (UI, MCP, scripts) must usePOST /api/rename-pathwith the polymorphic body shape:{ "kind": "file" | "folder", "fromPath": "<path>", "toPath": "<path>", ...identity, "summary": "..." }The MCP
rename_documenttool's outward API is unchanged (parameters, response shape, identity passthrough are all functionally equivalent) — only the internal HTTP target changed.New capabilities:
- Folder rename now rewrites all inbound wiki-links and supported markdown links across linking docs (was previously a CONFIRMED gap — folder rename moved files but left link text untouched).
- Folder rename is now crash-safe — process kill mid-batch is recoverable on next startup with no partial state.
- File rename via the consolidated endpoint now updates the in-memory backlink index (was missing on the file branch of
/api/rename-path). - UI-driven rename and rollback now attribute to the server-loaded principal (
principal-<uuid>) when no agent identity is supplied. Body-suppliedprincipalIdis silently ignored — server'sgetPrincipal()is the only source of principal identity.
The recovery journal schema is bumped from v1 (single source/destination) to v2 (multi-doc
affectedDocs[]). The v1 parser is preserved alongside v2 — legacy v1 journals on disk at startup still recover correctly.Side-effect docs (backlink-rewrite cascades) remain anonymous for both agent-driven and principal-driven renames — only the renamed doc itself is attributed to the actor.
Other behavior changes worth noting:
POST /api/rollback500 responses no longer echo the underlying error message; clients now see a genericFailed to roll back documentstring. Eliminates a path / internal-state leak channel for an unauthenticated body endpoint.ok initaddsstate.jsonto the bootstrapped.ok/.gitignorealongsidesync-state.jsonandprincipal.json— covers newly-added local-runtime state at<contentDir>/.ok/state.json(also emitted as a separateinit-gitignore-consolidationchangeset for the broader scaffold rework, but thestate.jsonline specifically lands here).
feat(server): boot in linked git worktrees + fail-fast on missing
.ok/config.yml.Open Knowledge now boots correctly inside linked git worktrees (
git worktree add <path> <ref>).- Main worktree — shadow at
<projectRoot>/.git/ok/ - Linked worktree — shadow at
<repo>/.git/worktrees/<name>/ok/
ok initworks identically in main and linked worktrees with no special handling — the existingwriteIfMissingsafeguard in the init scaffold prevents clobbering committed configs thatgit checkoutmaterialized.Fail-fast on missing config.
bootServernow runs a pre-listen check for<contentDir>/.ok/config.yml. When absent (whether.ok/is missing entirely or justconfig.yml), boot rejects with the new typedMissingOkConfigError(exported from@inkeep/open-knowledge-server):Open Knowledge config not found at
.ok/config.yml. Runok initto scaffold OK in this directory.When only
.ok/.gitignoreis missing (config present), boot emits a one-time stderr warning recommendingok initand proceeds — per-machine state files in.ok/may show up as untracked changes until the recommended ignore entries are merged in.Desktop GUI: silent scaffold on first open. When you open a folder that doesn't yet have
.ok/config.yml, the Open Knowledge desktop app now scaffolds it silently and opens the editor — same idempotent flow asok init. The user's Navigator / recents / deep-link / drag-drop gesture is treated as consent for the scaffold.writeIfMissingensures committed configs (materialized bygit checkout) are never clobbered. Explicit-consent dialog UX (Yes/No modal before scaffold) is a follow-up.Unusable
.gitpaths surface as typed errors with the right recovery hint. Two distinct failure modes, two distinct typed errors (both exported from@inkeep/open-knowledge-serverand@inkeep/open-knowledge-core/shadow-repo-layout):MalformedGitPointerError—<projectRoot>/.gitis a file but itsgitdir:target is unreadable, has nogitdir:line, or references a missing admin directory. Typical cause: a partialgit worktree removerace orrm -rfof the admin directory withoutgit worktree prune. Recovery hint namesgit worktree prune.GitDirAccessError—statSyncon<projectRoot>/.gitfailed for a reason other thanENOENT(typicallyEACCES/EPERMon a misconfigured ACL or stale mount). The shape of.gitis undetermined. Recovery hint names filesystem permissions and mount state.
Both errors carry the original
errnoexception ascauseso log consumers can branch onEACCESvs parse-failure without parsing strings.Observability. The boot path emits an OTel
ok.bootspan carryingok.worktree.kind(main|linked) and a normalizedok.worktree.gitdirso failure rates can be sliced by worktree shape.Two worktrees on different branches running
ok startconcurrently are fully independent: separate shadows, separate.ok/server.lockfiles, separate ports, separate MCP endpoints. Per-worktree isolation is the intended model — writer history, upstream imports, and checkpoints stay scoped to the worktree they were authored in.- Main worktree — shadow at
Patch Changes
Fix
Cannot find package '@inquirer/checkbox'crash on every CLI invocation in the packaged desktop.app.tsdown.config.tsalwaysBundlelisted@inquirer/passwordbut missed@inquirer/checkbox—cli.tseagerly importsinitCommand, which top-level imports@inquirer/checkbox, so every subcommand (auth status,auth repos,clone, …) crashed before producing any output. Dev mode resolved the dep from the workspacenode_modulesand hid the bug. The packaged.appships nonode_modulesnext todist/cli.mjs, so the bare specifier failed at module-load time. Added the matching pattern toalwaysBundleso the dep is force-inlined intodist/cli.mjs.User-visible symptom: clicking "Clone from GitHub" in the published Open Knowledge desktop app surfaced
auth status exited with code 1because the renderer's IPCauth statusprobe spawnedok.sh auth status --jsonand got a Node module-not-found crash with no JSON output.ok(no positional args) on macOS now launches the desktop Electron app when it's installed in/Applications/Open Knowledge.app(or~/Applications/). Detection is interactive-only — SSH sessions, non-TTY stdout, and missing bundles all fall through to the existingok startbehavior (server + browser). On Linux, Windows, and any other platform without the macOS desktop bundle,okrunsstartexactly as before.Two new escape hatches:
ok start --mode=browser|appis an explicit modal selector.--mode=browser(or omitted) keeps today's server-only behavior bit-for-bit;--mode=appforces the desktop hand-off and errors with a clear message if the bundle isn't found.--opencontinues to mean "open a browser tab against the local server" and is mutually exclusive with--mode=app.OK_FORCE_BROWSER=1env var disables the dispatch entirely for the current process. Use this in scripts/CI/MCP wrappers that need the server even on a workstation with the desktop installed.OK_FORCE_DESKTOP=1does the inverse (force-launch desktop, bypassing the headless gate).
Non-breaking —
ok start(no flag) is unchanged. The dispatch only fires whenokis invoked with no subcommand. All existing subcommands (mcp,init,status,stop, etc.) keep current behavior. Seespecs/2026-05-04-cli-default-desktop-launch/SPEC.md.Fix
TS2559: Type 'ProcessEnv' has no properties in common with type '{ HOST?: string | undefined; }'atpackages/cli/src/commands/start.ts:540. TheresolveHosthelper'senvparameter type was a TypeScript "weak type" (only optional declared properties) that triggers TS2559's weak-type detection when called withprocess.env—NodeJS.ProcessEnvdeclares noHOSTproperty; its index signature alone doesn't satisfy the rule. Widened the parameter to{ HOST?: string | undefined; [key: string]: string | undefined }so production callers passingprocess.envand existing unit-test literals ({},{ HOST: '...' }) all type-check. Function body and runtime behavior unchanged. Restoresbun run typecheckto green at the monorepo level (canonical CI gate; introduced in #492 and slipped through that PR's typecheck job).Fix the "Install for Claude Chat & Cowork" handoff in the packaged Electron app, which previously failed with
build-failed: Could not resolve @inkeep/open-knowledge CLI version. The cause was a heuristic two-path filesystem probe inbuild-skill-zipthat didn't match thenode_modules/@inkeep/open-knowledge/layout shipped inside the .app — and the version it was looking up was not actually used in this code path.build-skill-zipis now a pure ZIP builder:BuildSkillZipOptions.skipVersionCheckandBuildSkillZipResult.cliVersionare removed in favor of a presence-basedBuildSkillZipOptions.expectedSkillVersion?: string(pass it from release builds that need to assert SKILL ↔ CLI alignment, omit otherwise).BuildAndOpenSkillResult.cliVersionis also removed —skillVersionis the canonical artifact version.A new
resolvePackageVersion(packageName, fromUrl)utility is exported from@inkeep/open-knowledge-serverfor any caller that needs to look up an installed package's version at runtime. It uses Node's module resolver plus a directory walk, which is robust across workspace symlinks, hoistednode_modules, pnpm.pnpm/-flat, and asar-packed Electron.The
ok install-skillpost-build message now readsSkill v<x.y.z>(presence-checked) instead ofCLI v<x.y.z>.fix(dev): three blockers in the fresh-checkout
bun run devboot path- Vite config bundling:
vite.config.ts(viahocuspocus-plugin.ts) imports@inkeep/open-knowledge-coreand-server. Vite'snodeResolveWithViteuses conditions['node', module-sync]only, so it fell through todefault→dist/index.mjsand threwFailed to resolve entry for packageon any checkout where the workspace dists weren't built. Add apredevhook inpackages/app/package.jsonthat builds both deps (turbo handles caching) sodefault → distalways resolves. Anode → src/index.tsexports condition would also work for Vite but breaks the packaged Electron main process (which runs Node 22 without--conditions=developmentand would resolve workspace deps to TypeScript source →ERR_UNKNOWN_FILE_EXTENSION); predev side-steps that conflict. - Blank page on first render (
asset-click-dispatch.e2e.tsP9._ regressions): the dev plugin's asset-serve middleware 404 guard intercepted Vite-internal paths (/favicon.svg,/src/foo.png?import,/@vite/_). Keep the synchronous registration (load-bearing — must run BEFORE Vite'sspaFallbackMiddlewareso unknown asset URLs return 404, not the SPA shell — confirmed by P9.22) and add an explicit Vite-internal-path bypass list. A post-hook approach (return () => server.middlewares.use(...)) would defer registration AFTERspaFallbackMiddleware, breaking the asset 404 guard and routing PDF/m4v asset URLs through the SPA fallback. - Backlinks ENOENT on
.mdxfiles:BacklinkIndex.rebuildFromDisklooked up file extensions viagetDocExtension, which reads from the file-watcher's extension registry. Boot order callsrebuildFromDiskBEFOREstartWatcher, so the registry is empty and every docName defaults to.md— ENOENT on every.mdx. Align with the siblingreconcileWithDiskpath: usewalkForPathsto thread the observed on-disk path through to the read.
- Vite config bundling:
Docs accuracy pass — surgical fixes for drift between
docs/content/and the shipped CLI/MCP/integration code.reference/cli.mdx:- Fixed broken markdown table in the
startflags section (missing| --- | --- |separator row meant the table didn't render). - Removed duplicate
--openrow. - Added the
--mode <browser|app>flag onstart(escape hatch for the desktop-dispatch path). - Added the
pscommand to the Lifecycle section (lists running OK servers).
integrations/codex.mdx: clarified thatinitalso installs the Open Knowledge skill globally (vianpx skills add) so Codex can discover the MCP tools — previously the doc said only "registers the MCP server."integrations/claude-desktop.mdx: added a date-stamped callout flagging the Cowork-bug workaround section as upstream-bug-driven, so it gets revisited when Anthropic ships a fix.get-started/quickstart.mdx: corrected the post-initdescription —initregisters MCP for all 6 supported editors (Claude Code, Cursor, Codex, Claude Desktop, VS Code, Windsurf) and installs a skill for Claude Code, Cursor, and Codex. Previous wording omitted Codex from the skill list and named only Claude Code + Cursor for the "every supported editor" claim.reference/configuration.mdxandreference/mcp.mdxwere re-verified during the pass and need no changes.- Fixed broken markdown table in the
fix(open-knowledge/server): serve doc-referenced assets that live in a dedicated
assets/directoryImages (and other media) referenced from markdown via a doc-relative path into a dedicated assets tree —
— failed to load with a 404. In the Electron app the symptom was a console error likeFailed to load resource: 404 (Not Found) http://localhost:<port>/assets/images/characters/aang.png; the same gap affectedok uiandbun run dev.Root cause:
createAssetServeMiddlewaregated serving oncontentFilter.isExcluded(), which applies the D11 sibling-asset heuristic — an asset path is admitted only if its directory also contains an included.mddocument. That heuristic is a file-watcher index-walk concern (don't index loose binaries that nothing references); it is wrong for the serve path, where the request is an explicit reference. Assets organized the standard way — underassets//images//attachments/with no sibling.md— were therefore treated as excluded and 404'd, while an otherwise-identical copy sitting next to a.mdfile served fine.Fix: the serve middleware now uses
contentFilter.isPathIgnored()— the security-boundary-only check (.gitignore/.okignorepatterns,BUILTIN_SKIP_DIRSlikenode_modules//dist//.git/, reserved system-doc names) without the sibling-asset heuristic — plus an explicit "only.md/.mdxand known content-asset extensions stream a contentDir file" gate that restores the jobisExcluded's default-exclude branch was doing (so.html/.exe/ extensionless paths still fall through). This matches whathandleAsset/collectReferencedAssetsinapi-extension.tsalready do for the same reason. User-configured exclusions still apply, so an asset matched by.gitignore/.okignoreis still refused; the/node_modules/...//dist/...Vite-internal fall-through is unchanged.Side effects: (1) missing asset-extension and executable-blocklist URLs now consistently hit the fail-closed 404 guard instead of leaking the editor HTML via the SPA fallback in directories without a sibling
.md(an improvement — that leak is the very anti-pattern the 404 guard exists to prevent). (2).svgfiles in dedicated assets directories are now reachable through this serve path (previously D11-blocked); since.svgis inINLINE_RENDERABLE_EXTENSIONSit serves withContent-Disposition: inline, so the middleware now also sets the sameContent-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'headerhandleAssetapplies — a top-levelGETof a contentDir SVG can no longer execute embedded<script>. (Aligning the two serve paths' SVG handling further — e.g. a shared helper — is left for a follow-up.)This completes the Electron asset-origin work from the prior fix (which routed the request to the utility server's
apiOriginbut left the serve-side admission gate as the remaining gap).Fix CLI npm publish: set repository.url to inkeep/open-knowledge-legacy (matches OIDC build context, satisfies sigstore provenance) and drop leading "./" from bin paths so npm publish stops auto-removing them.
Drop
repositoryfield from CLI package.json so npm sigstore provenance no longer rejects publish with E422. The previous URL pointed at the futureinkeep/open-knowledgerepo while builds run frominkeep/open-knowledge-legacy; with the field removed, npm infers the repo from the OIDC build context and provenance matches by construction. No revert needed after the planned repo rename.chore(init): consolidate Open Knowledge ignore rules into
.open-knowledge/.gitignore. The scaffold now writescache/,server.lock,ui.lock,sync-state.json,principal.json, andlast-spawn-error.log, andok initmerges missing entries into pre-existing files (existing user lines preserved).ok cloneno longer mutates the cloned repo's tracked.gitignore; per-clone protection lives in.git/info/exclude(local-only, never committed) instead.Fix agent-presence icon flickering between MCP tool calls. The
ok mcpshim now forwards its keepalive WSconnectionIdviax-ok-connection-id, and the MCP HTTP session adopts that id asidentity.connectionIdinstead of minting a fresh UUID. With both surfaces sharing one id, the 3sbumpPresenceTsheartbeat keeps the broadcaster entry fresh between writes, and on-closeclearPresencefinds the right key. The icon now stays visible for the lifetime of the keepalive WS instead of disappearing 5s after each tool call. Header values are validated throughvalidateAgentId. Invalid values fall back torandomUUID()so non-shim MCP clients still get a working session.Refuse to run
ok mcpin directories that haven't beenok init'd. Closes a regression where invokingnpx @inkeep/open-knowledge mcpfrom a non-OK directory eagerly scaffolded.ok/,.openknowledge/(legacy bare shadow), and.gitignoreas a side effect of MCP startup — observed when the OK MCP was registered globally (e.g.~/.claude.jsontop-levelmcpServers) and the user opened claude in any directory.ok mcpnow exits cleanly at startup if<cwd>/.ok/doesn't exist, before registering tools or discovering servers.--portbypasses the gate (explicit user intent). The skill description already says "Skip if no .ok/ — not an Open Knowledge project"; this enforces the same contract at the server level.refactor(mcp): route
ok mcpthrough the shared HTTP server. The CLI now uses a thin stdio-to-HTTP shim, removes legacy--pinsetup, and relies on the runningok startserver for MCP tool execution.chore(layout): move per-machine runtime files from
.ok/<name>to.ok/local/<name>.<contentDir>/.ok/now separates committed config (config.yml,frontmatter.yml,templates/) from per-machine runtime state, which moves under.ok/local/:server.lock,ui.lock,state.json,principal.json,sync-state.json,conflicts.json,last-spawn-error.log,managed-rename.json,cache/<branch>/backlinks.json,tmp/upload-<uuid>.OK_GITIGNORE_CONTENTis now a single line (local/) — adding a new runtime file no longer requires a coordinated.gitignoreedit, and.ok/local/documents the contract by name. All in-process consumers resolve the path through the newgetLocalDir(contentDir)helper exported from@inkeep/open-knowledge-core.If you're an OK developer pulling this branch and your project has runtime files at
.ok/root, runrm -rf .ok/thenbun run --filter=@inkeep/open-knowledge ok init. The boot logs a one-shot warning if it finds legacy files at.ok/root; the new code reads/writes only under.ok/local/, so legacy files sit inert until you clean them up. There is no auto-migration — Open Knowledge has not shipped to real users yet, so the developer-only re-init path is the right tradeoff (per spec D2 LOCKED).Spec:
specs/2026-05-05-ok-local-folder-pattern/SPEC.md.ok psandok stopnow surface and act on hostname-drifted servers correctly, label desktop-spawned servers asdesktop, and call out orphaned UI processes:- Foreign-host visible by default in
ok ps. macOS hostname drift (BonjourName ↔ FQDN across DHCP/VPN/sleep) flips lock entries toforeign-host; the default filter now keeps them alongsidealiveso same-machine servers don't vanish from the table.--allis now narrowed to "include stale (dead-pid) entries"; the help text anddocs/content/reference/cli.mdxare updated to match. - Desktop label. Desktop and CLI both write
kind: 'interactive', so the lock alone can't tell them apart.ok psclassifies via the live process command — Electron utility processes carry--type=utilityAND--utility-sub-type=node.mojom.NodeService(the latter is whatutilityProcess.forkspecifically registers, so VS Code / Slack / Discord helpers don't false-match). Renders asdesktop(blue), overridingrunning/foreign. Falls back when the command lookup is unavailable. ok stopaccepts hostname-drifted entries.buildStopPlan,findLockDirByNumber, and thestop allfilter now also stopforeign-hostentries when the PID is locally live (verified via the canonicalisProcessAliveprobe from@inkeep/open-knowledge-server, not a duplicate). Truly cross-host locks fail the liveness probe and are left alone.- Hostname-drifted dead locks classify as
dead-pid.inspectLockpreviously short-circuited toforeign-hoston hostname mismatch before the liveness probe, leaving stale drift locks stuck visible forever. Liveness now runs first;foreign-hostmeans specifically "different hostname AND PID exists locally" (the genuine drift case). Stale drift locks now fall out ofok psby default and become eligible forok cleanpruning. The nextok start/ desktop launch in that dir also auto-replaces them via the existingacquireProcessLockstale-replacement path, so manual cleanup is rarely needed. ui-orphanstatus. When the server lock isdead-pidbut the UI sidekick is alive (orforeign-host-with-live-PID),ok psnow renders the row asui-orphan(magenta) instead of hiding the orphan behind astalelabel or blanking the UI port. Surfaces the lifecycle hole whereok uisurvives an ungraceful server crash; the underlying self-shutdown / boot-time-reap fix is tracked separately.- Foreign-host UI port shown in PORTS. Post the inspectLock reorder,
foreign-hostUIs have a live local PID listening on that port — show it. Hiding it (the prior behavior) made orphan UIs invisible because hostname drift forces them intoforeign-host.
JSON output of
ok psgains anisDesktop: booleanfield on each entry for tooling consumers.- Foreign-host visible by default in
Remove the dead "Folders" section from the Settings pane.
The
folders[]cascade was retired in spec2026-05-01-folder-level-metadata-and-templates(FR8 / D19) — folder defaults moved into nested<folder>/.ok/frontmatter.ymlfiles, edited via theset_folder_ruleMCP tool. TheFoldersSection.tsxSettings UI and itsapplyFolderRulesUpsertwrite path were left behind in that change; entries typed into the section persisted intoconfig.ymlviaz.looseObjectpassthrough but no production code read them.This removes:
FoldersSection.tsx+ its smoke testapply-folder-rules-upsert.ts(the dead UI was the only caller;set_folder_ruleuses its ownapplyNestedFolderRulesUpsert)folder-rules.tslegacy resolver (zero production callers)- the
applyFolderRulesUpsertre-export from@inkeep/open-knowledge-core/server - stale "config.yml
folders:" references inenrichment.ts,exec.ts, andseed.tsJSDoc/CLI descriptions
FolderRuleSchemaandFolderFrontmatterSchemaexports remain in place —set_folder_rulereuses them.fix(rename): preserve file content when renaming via the sidebar.
Renaming a file via the sidebar inline-rename (right-click → Rename, or F2) was erasing the file content on disk: the editor showed an empty placeholder under the new name, and the original content survived only in the renaming tab's IndexedDB cache (which is why renaming back made it "reappear"). Cold reloads or other tabs/clients saw the renamed file as empty.
Two layered fixes:
FileTreeignores selection-change events whose docName isn't yet in the localdocumentslist.@pierre/treesfiresonSelectionChangesynchronously when an inline rename commits — before the rename API has written the file at the new path — and the resulting premature navigation was opening a server-side Y.Doc against a missing file, which the persistence layer subsequently flushed back to disk as 0 bytes.persistence.onStoreDocumentrefuses to materialize a 0-byte file when the Y.Doc was never confirmed to exist on disk AND the serialized markdown is empty. This blocks accidental orphan files from any code path that opens a Y.Doc for a non-existent docName (browser races,/api/document?docName=<missing>, MCP queries on deleted docs, future callers). Legitimate first-write paths (/api/create-page, agent writes via/api/agent-write-md) are unaffected.
Production hardening + test helper disambiguation for the sidebar resize race (#576). Retroactive changeset — this PR landed without one, this triggers the next VP cycle so the changes flow through the beta cadence.
The collab server now caps the number of distinct agent sessions per
Documentat 256 and refuses additionalagent-write/agent-write-md/agent-patchrequests with503 too-many-agent-sessionsonce the cap is reached. Without the cap, an unauthenticated peer (or a runaway agent loop) could mint unlimited freshagentIds, each spawning a new server-side session with its own bounded undo manager and presence entry, exhausting memory and overwhelming the awareness map. The 256 ceiling is well above any legitimate per-document agent fleet and triggers aAgentSessionCapacityErrorlog line includingdocNameandagentIdso operators can identify the offending peer.Bound desktop auth-query IPC subprocess fan-out. The
authStatus/authReposIPC handlers inpackages/desktop/src/main/ipc/local-op.tsnow coalesce concurrent requests for the same host and enforce a per-handler concurrency cap; overflow is reported back to the renderer as a structured response rather than spawning anotheropen-knowledge auth …child process. Closes the unbounded-spawn DoS path where a compromised renderer could exhaust the process table by tight-loopingwindow.okDesktop.localOp.authStatus()/authRepos().The backlink index's wiki-link extractor now linearly skips backtick spans rather than falling into a quadratic regex on long unclosed backtick runs. A doc containing thousands of consecutive backticks (no closing pair) previously sent the parser into pathological backtracking and pinned a CPU core for seconds-to-minutes per re-index. The new path scans backtick boundaries with a single pass and short-circuits when no closing pair is found, keeping per-doc parse time linear in document size regardless of backtick density.
The chokidar fallback file-watcher now refuses to forward events whose path resolves through a symlink to a target outside the project's
contentDir. Previously a symlink planted in the watched tree (viagit clone, an extracted archive, or a co-tenant write) could surface as anadd/changeevent whosereadFilewould dereference the symlink and pull in/etc/passwdor another sensitive target. The neweventEscapesContentDirhelperlstats the event path, resolves the symlink withrealpathSync, and refuses unless the canonical target is withincontentDir. Thelstatandrealpathcatch blocks distinguishENOENT(delete-race, pass through) andELOOP(broken symlink, drop quietly) from other errno codes (EPERM/EACCES/EIO— drop and log) so a hostile symlink with restricted intermediate-path permissions can't bypass the gate by triggering an unexpected error.ok initnow refuses to overwrite project-scope config paths (.mcp.json,.cursor/mcp.json,.claude/skills/open-knowledge) when any segment of the destination is a symlink whose realpath escapes the project cwd. A malicious repo could previously plant a symlink that pointedgit cloned files at/etc/passwdor another sensitive location, then haveok initfollow it and overwrite the target. The newassertProjectPathSafeguard rejects leaf-symlink, ancestor-symlink, and parent-traversal escapes while still allowing in-cwd symlinks. Editor-ID lookups also switched from bracket access toObject.hasOwnso prototype-chain pollution can't surface forged editor labels.Treat lock-file PIDs as untrusted input across every signal-delivery and liveness-probe site. The desktop's collision-recovery path in
packages/desktop/src/main/window-manager.tspreviously read the holder PID straight from<contentDir>/.ok/local/server.lock(user-writable JSON) and calledprocess.kill(pid, 'SIGTERM')after only atypeof pid === 'number'shape check, allowing crafted lock entries with negative PIDs (process-group signaling on Unix),0,1(init), NaN, or values above0x7fffffffto direct signals at unintended processes.The fix layers three defenses through the new shared
isValidLockPidvalidator (packages/server/src/process-alive.ts):- Parse-time validation.
process-lock.ts,lock-state.ts(CLIok stop/ok ps),shadow-lock.ts, and the desktop window manager now all reject hostile PIDs ascorrupt/stalebeforeisProcessAliveorprocess.killever sees them. - TOCTOU re-verification.
WindowManager.verifyHolderStillOwnsLockre-reads the lock right before signaling and confirms the holder PID hasn't changed since the collision report. - Self-PID guard. The auto-kill path refuses to send a signal when
existingLock.pid === process.pid, blocking a self-SIGTERM if PID-recycling ever produces a collision metadata that names the desktop itself.
- Parse-time validation.
The forward-links endpoint now passes every wiki-link target through the project's
ContentFilterbefore reading the target document's frontmatter. Previously a wiki-link to a path excluded from the project (via.gitignoreor.okignore) would still resolve, surface the excluded doc's title in the response, and effectively leak the file's existence + title to anyone with read access to the linking doc. The exclusion gate is now applied per-target so excluded docs render as plain wikilinks (no resolved title) without revealing whether they exist.The
/api/history/:docNameendpoint now rejectsdocNamevalues that contain.., leading slashes, drive letters, or otherwise resolve outside the project's content root. Previously the docName was concatenated into asafeContentPathlookup without anisSafeDocNamegate; a request likeGET /api/history/..%2F..%2Fetc%2Fpasswdcould traverse out ofcontentDirand surface git history (or, on systems where the timeline-query path read file contents, the file itself). The handler now returns400 invalid-docNamewhen the input failsisSafeDocName, matching the contract used by every other docName-keyed endpoint.The local-op path validators (
assertLocalOpPathSafeand friends) now resolve every path throughrealpathSyncand refuse paths whose canonical target lies outside the project's content root. Previously a.local/oppayload with a path likesubdir/symlink-to-etc/passwdonly ran a lexical containment check; ifsubdir/symlink-to-etcwas a symlink pointing outside the project, the local-op handler would happily read or write the target. Symlink loops surface asELOOPand are also rejected. The path-safety helpers are now shared with the seed-walk equivalents so the two security boundaries stay in sync.The
ok uistatic-server/apireverse proxy now enforces the same loopback-only and Host-header-allowlist gates that the upstream server applies before forwarding any request, instead of trusting that requests reaching the proxy are loopback-bound. Without the gate, a non-loopback peer could reach the upstream API by talking to the static server's port directly. The gate refuses with403 {ok:false, error}(andX-Content-Type-Options: nosniffto match the upstream's response posture) when either condition fails, before any proxy handshake.The reconciliation engine's LCS-based diff path now caps the input length and falls back to a whole-doc replace once the cap is exceeded, instead of allocating an
O(n*m)table over arbitrary user input. A multi-megabyte agent write previously sent the LCS path into a memory allocation proportional toprevLength * newLength, exhausting the Node heap and crashing the server. The cap is sized so that legitimate edits (well below 1 MiB per side) continue through the precise LCS diff while pathological inputs get the coarser whole-doc replace path; either way the persistence layer ends up with the user's bytes.POST /api/seed/planandPOST /api/seed/applynow refuse seed entries whose resolved target path lies outside the project directory, either via lexical..-traversal in the entry's relative path or via any symlink in the target's resolved chain whose realpath escapes the project root. The newassertNoSymlinkEscapewalks each existing ancestor of the target, callsrealpathSync, and rejects paths that resolve outsiderealpath(projectDir)with aSeedRootDirError. Symlink loops surface asELOOPand are also rejected. Without these guards a seed plan could write to/etc/, the user's home directory, or any other writable location reachable through a symlink the project happens to ship.The
sourceLiteralmark's mdast-conversion path now refuses to persist asourceRawattribute that doesn't match the visible PM text it's attached to. Previously a caller that could mutate the document (agent API, synced collaborator, crafted clipboard paste) could store one byte sequence as visible text and persist an arbitrarily different sequence on save — a hidden-content injection vector where the editor displays "Hello" but the on-disk file or downstream LLM transcript contains a prompt-injection / commit-message-injection / structural-markdown payload.isValidSourceLiteralRawgates the single-child path; the multi-child / non-text fallback now dropssourceRawentirely (rather than re-using the unvalidated raw as both the synthetic value and the synthetic sourceRaw, which made the downstream defense-in-depth gate vacuous).sourceLiteraldeclaresexcludes: ''so it can co-occur withstrong/emphasis/link/ etc., which is exactly the multi-child path; closing this gap completes the threat-model coverage.Agent and contributor summaries are now stripped of newlines, control characters, and other commit-message-meaningful bytes before they're concatenated into shadow-repo commit messages. Without the strip, an agent or actor could inject
\n\nSigned-off-by: someone-else <fake@example.com>(or any git trailer / multi-line block) into the commit body by embedding the bytes in a summary field, producing forged author attribution or trailer-driven side effects in downstream tooling.normalizeSummaryalready capped length and rejected non-strings; the new step also strips line breaks and control bytes from base + per-element summaries before they reachgit commit -m.The collab server now releases empty Y.Docs that were never confirmed to back an on-disk file once their last connection drops. Without the carve-out, a local peer (browser tab, MCP agent, DNS-rebound origin) could connect to an arbitrary stream of unique non-existent docNames over the loopback
/collabWS and growhocuspocus.documentswithout bound — each allocating aY.Doc+ persistence-store handle that lived until process shutdown. The carve-out only releases phantom docs (nogetReconciledBaseand empty fragment + empty Y.Text); file-backed and content-bearing docs continue to follow the resident-for-lifetime rule the cache-epoch-recovery defense relies on.Also drops a stray
docName/agentIdshorthand reference from threeAgentSessionCapacityErrorlog calls inapi-extension.ts— a typecheck regression that's been onmainsince #449's merge dropped my earlier revert. Tests + typecheck pass with this PR's branch butmainitself does not type-check until this lands.The asset upload endpoint (
POST /api/upload-asset) now refuses to write when any segment of the destination directory is a symbolic link, before performing the recursivemkdirSync(destDir, { recursive: true })that previously could traverse a symlink and place uploaded files outside the project's content root. An attacker who could plant a symlink at<contentDir>/some/dir(viagit cloneof a malicious repo, file-watcher race, or another endpoint that creates uncanonicalized paths) could otherwise have uploads land in/etc/,~/.ssh/, or any other writable location the server process could reach. The handler nowlstats each ancestor segment and refuses withsymlink-escapewhen it finds a symlink, regardless of whether the symlink's realpath would resolve back insidecontentDir.openBrowser(called byok start --open/ok ui --open) now refuses URLs that contain shell-metacharacters (& | < > ^ ( ) ; $ \\" ' [ ]plus newlines and spaces) and limits the scheme tohttp(s)before invokingcmd /c start ""on Windows. Previously a malicious or misconfigured--host/HOSTenv /server.hostconfig value (e.g.localhost&calc) was concatenated into the launcher URL and could reachShellExecuteaftercmd.exeparsed the metacharacters as additional commands. Validation runs on every platform but the practical RCE is Windows-only; macOS / Linux launchers (open/xdg-open) are not vulnerable to the same expansion. Validation tests now run in CI (the innerURL validationdescribe was previously nested inside the CI-skip-gated outer block; it's been hoisted to a top-level describe using_bunDescribedirectly so regressions are caught by the automated pipeline).
0.4.0-beta.36
Patch Changes
fix(dev): three blockers in the fresh-checkout
bun run devboot path- Vite config bundling:
vite.config.ts(viahocuspocus-plugin.ts) imports@inkeep/open-knowledge-coreand-server. Vite'snodeResolveWithViteuses conditions['node', module-sync]only, so it fell through todefault→dist/index.mjsand threwFailed to resolve entry for packageon any checkout where the workspace dists weren't built. Add apredevhook inpackages/app/package.jsonthat builds both deps (turbo handles caching) sodefault → distalways resolves. Anode → src/index.tsexports condition would also work for Vite but breaks the packaged Electron main process (which runs Node 22 without--conditions=developmentand would resolve workspace deps to TypeScript source →ERR_UNKNOWN_FILE_EXTENSION); predev side-steps that conflict. - Blank page on first render (
asset-click-dispatch.e2e.tsP9._ regressions): the dev plugin's asset-serve middleware 404 guard intercepted Vite-internal paths (/favicon.svg,/src/foo.png?import,/@vite/_). Keep the synchronous registration (load-bearing — must run BEFORE Vite'sspaFallbackMiddlewareso unknown asset URLs return 404, not the SPA shell — confirmed by P9.22) and add an explicit Vite-internal-path bypass list. A post-hook approach (return () => server.middlewares.use(...)) would defer registration AFTERspaFallbackMiddleware, breaking the asset 404 guard and routing PDF/m4v asset URLs through the SPA fallback. - Backlinks ENOENT on
.mdxfiles:BacklinkIndex.rebuildFromDisklooked up file extensions viagetDocExtension, which reads from the file-watcher's extension registry. Boot order callsrebuildFromDiskBEFOREstartWatcher, so the registry is empty and every docName defaults to.md— ENOENT on every.mdx. Align with the siblingreconcileWithDiskpath: usewalkForPathsto thread the observed on-disk path through to the read.
- Vite config bundling:
0.4.0-beta.35
0.4.0-beta.34
Minor Changes
Add the Gbrain starter pack — entity-grounded second-brain layout (typed dossiers for
people/,companies/,meetings/,concepts/,originals/,media/) with compiled-truth-above / append-only-timeline-below body convention, per-folder templates, agent-readable folder frontmatter, and five root files (USER.md,SOUL.md,ACCESS_POLICY.md,HEARTBEAT.md,log.md). Selectable asGbrainin the desktop app's pack picker or viaok seed --pack gbrain. Pattern inspired by Garry Tan's gbrain; OK ships the layout natively.Also ships two new docs-site guides under
docs/content/workflows/:karpathy-llm-wiki.mdx— source-grounded knowledge base via the existingknowledge-basestarter pack, mapping 1:1 onto Karpathy's three-layer pattern (external-sources/→research/→articles/) with theingest/research/consolidateMCP tools.gbrain.mdx— entity-grounded second brain via the newgbrainstarter pack, demonstrating the dead-links → triage → WYSIWYG loop and end-to-end agent-driven dossier maintenance using OK's MCP surface.
Both guides land under a new
Workflowssection in the docs IA (docs/content/meta.json+docs/content/workflows/meta.json).Pinned-count test in
starter.test.tsupdated 5 → 6 to include the new pack.
0.4.0-beta.33
Minor Changes
- Remove the
preview.baseUrlconfig field and theOPEN_KNOWLEDGE_PREVIEW_BASE_URLenvironment variable. The deployed-wiki use case isn't supported in this greenfield; thepreviewUrlMCP resolver now collapses toelectron-protocol → lockand emits URLs that point at the running UI process only. Migration: start a local UI withok ui(or the desktop app) — preview URLs resolve from theui.lockit writes. Stalepreview: { baseUrl: ... }keys in.ok/config.ymlare accepted silently bylooseObjectbut ignored; the CLI loader now emits a deprecation warn pointing atok ui.
0.4.0-beta.32
Patch Changes
fix(open-knowledge/server): serve doc-referenced assets that live in a dedicated
assets/directoryImages (and other media) referenced from markdown via a doc-relative path into a dedicated assets tree —
— failed to load with a 404. In the Electron app the symptom was a console error likeFailed to load resource: 404 (Not Found) http://localhost:<port>/assets/images/characters/aang.png; the same gap affectedok uiandbun run dev.Root cause:
createAssetServeMiddlewaregated serving oncontentFilter.isExcluded(), which applies the D11 sibling-asset heuristic — an asset path is admitted only if its directory also contains an included.mddocument. That heuristic is a file-watcher index-walk concern (don't index loose binaries that nothing references); it is wrong for the serve path, where the request is an explicit reference. Assets organized the standard way — underassets//images//attachments/with no sibling.md— were therefore treated as excluded and 404'd, while an otherwise-identical copy sitting next to a.mdfile served fine.Fix: the serve middleware now uses
contentFilter.isPathIgnored()— the security-boundary-only check (.gitignore/.okignorepatterns,BUILTIN_SKIP_DIRSlikenode_modules//dist//.git/, reserved system-doc names) without the sibling-asset heuristic — plus an explicit "only.md/.mdxand known content-asset extensions stream a contentDir file" gate that restores the jobisExcluded's default-exclude branch was doing (so.html/.exe/ extensionless paths still fall through). This matches whathandleAsset/collectReferencedAssetsinapi-extension.tsalready do for the same reason. User-configured exclusions still apply, so an asset matched by.gitignore/.okignoreis still refused; the/node_modules/...//dist/...Vite-internal fall-through is unchanged.Side effects: (1) missing asset-extension and executable-blocklist URLs now consistently hit the fail-closed 404 guard instead of leaking the editor HTML via the SPA fallback in directories without a sibling
.md(an improvement — that leak is the very anti-pattern the 404 guard exists to prevent). (2).svgfiles in dedicated assets directories are now reachable through this serve path (previously D11-blocked); since.svgis inINLINE_RENDERABLE_EXTENSIONSit serves withContent-Disposition: inline, so the middleware now also sets the sameContent-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'headerhandleAssetapplies — a top-levelGETof a contentDir SVG can no longer execute embedded<script>. (Aligning the two serve paths' SVG handling further — e.g. a shared helper — is left for a follow-up.)This completes the Electron asset-origin work from the prior fix (which routed the request to the utility server's
apiOriginbut left the serve-side admission gate as the remaining gap).
0.4.0-beta.31
0.4.0-beta.30
Minor Changes
feat(desktop): replace the "Start fresh" Welcome card with an in-app "Create new project" dialog.
The third Welcome card now opens a shadcn dialog asking for a name and parent location, with checkboxes for which AI editors to wire on first run. A pre-submit cascade surfaces structural conditions before any filesystem write:
- Nested-project block — when the picked parent sits inside an existing Open Knowledge project, the dialog renders a red banner naming the enclosing rootPath with an inline "Open
" action. Create is disabled. - Git-root promote confirm — when the picked parent is inside a git working tree with no enclosing
.ok/, the dialog renders a blue banner explaining that.ok/config.ymlwill land at the git root withcontent.dirscoped to the new folder (one project per git repo). Create stays enabled. - Target-non-empty block — when the resolved
parent/<name>already exists with content, the dialog renders a neutral banner pointing the user at "Open folder on disk" instead.
The IPC handler (
ok:project:create-new) re-runs every check server-side as defense-in-depth — a stale renderer state or a hostile renderer cannot scaffold a project inside an existing one.Breaking (internal-only surfaces):
OkProjectEntryPointvalue'start-fresh'is removed; replaced by'create-new'and the new'create-new-nested-redirect'value (fired by the red banner's inline action).bridge.dialog.createFolder, theok:dialog:create-folderIPC channel, anddialog-helpers.ts'spromptForFolder(createDirectory variant) are deleted. The Pick-existing flow keepspromptForExistingFolderunchanged.- The silent-scaffold branch in
openProject(entryPoint'start-fresh') is gone — every project create now goes through the dialog with explicit user consent on AI-editor wiring.
No public API change —
@inkeep/open-knowledge(the CLI) is unaffected.Telemetry continuity. The
ok.desktop.onboardingConsentspan keeps its shape; theflowKindenum gains'create-new-default'(all editors selected, the canonical happy path) and'create-new-customized'(one or more editors toggled off). The legacy'fresh-silent'variant is no longer emitted; downstream dashboards should treat'create-new-default'as its successor during the transition window.Spec:
specs/2026-05-10-create-new-project-dialog/SPEC.md.- Nested-project block — when the picked parent sits inside an existing Open Knowledge project, the dialog renders a red banner naming the enclosing rootPath with an inline "Open
0.4.0-beta.29
0.4.0-beta.28
Minor Changes
feat(seed): multi-scaffold templates picker — five Initialize packs + per-folder extra templates + opt-out personal-templates pack
Generalizes the single-scaffold
Initialize LLM brainseed into a five-card picker inSeedDialog:- Knowledge base (existing, renamed from "LLM brain") — Karpathy three-layer source-grounded KB (
external-sources/→research/→articles/). - Software lifecycle —
proposals/+decisions/+specs/+postmortems/+guides/. Industry-current naming perdotnet/designs/withastro/roadmap/ Google Cloud ADR doc /github/spec-kit.specs/shipsspec+spec-plan+spec-taskstemplates so thegithub/spec-kitper-spec triple shape is one click each.guides/shipsguide+onboarding-guide+runbook. - Plain notes —
notes/+daily/. Escape hatch for casual users. - Worldbuilding —
characters/+settings/+themes/+factions/+lore/(Fiction variant).factions/shipsfaction+political-faction+religion;lore/shipslore+magic-system+historical-event. - Writing pipeline —
ideas/+drafts/+published/. Lean three-stage; book pipeline deferred to a future pack.
New public surface:
STARTER_PACKS: Record<PackId, StarterPack>registry (server).PackIdis a closed enum.StarterFolder.extraTemplates?: readonly string[]— additional templates installed alongside the starter in<folder>/.ok/templates/. Picker pre-selects the starter; extras available viaNew from template….resolvePack/coercePackId/isKnownPackId/listStarterPackshelpers — single source of truth for HTTP, IPC, and CLI.GET /api/seed/packs+okDesktop.seed.listPacks()— enumerate available packs (returnsid,name,description,defaultSubfolder?,folders[]with per-folder summaries). Schemas:SeedListPacksSuccessSchema+SeedPackInfoSchema+SeedPackFolderInfoSchema.planSeed/applySeedacceptpackId+includePersonalTemplatesoptions.ScaffoldPlancarries optionalpersonalTemplatespreview;ApplyResultcarries optionalpersonalTemplateswrite summary.- HTTP / IPC / CLI all reject explicit-but-unknown
packIdwith a structured error (trust-boundary symmetry). /api/seed/applyadded toMUTATING_ROUTESfor DNS-rebinding defense-in-depth.- CLI
ok seedgains--pack <id>+--list-packs+--personal-templatesflags. DefaultpackIdstaysknowledge-basefor back-compat.
Personal-templates pack (opt-out, default checked):
- Seven universal templates land at user scope (
~/.ok/templates/):daily-journal,meeting-notes,weekly-review,reading-log-entry,gym-log,recipe,travel-trip. - Idempotent at file level — never overwrites existing user-edited templates.
OK_USER_HOMEenv override is gated onNODE_ENV === 'test'so a stray env var can't misdirect production writes.
Back-compat preserved:
- Legacy
STARTER_FOLDERS,STARTER_TEMPLATES,LOG_MD_TEMPLATEexports are@deprecatedaliases pointing atSTARTER_PACKS['knowledge-base']. - Default
packId = 'knowledge-base'everywhere; callers that don't passpackIdget the same behavior as before. okDesktop.seed.plan()/okDesktop.seed.apply(plan)still callable without options.
Empty-state probe simplified: previously checked only the Knowledge base pack, leaving projects seeded with other packs stuck showing the CTA forever. Now keys off
documentCount === 0— works for all packs.Bridge contract:
OkSeedPlanOptions/OkSeedApplyOptions/OkSeedPackInfo/OkSeedListPacksResultadded; three copies (core, app, desktop) kept in lockstep; drift catcher passes.OkScaffoldPlangains optionalpersonalTemplates;OkSeedApplyResultgains optionalpersonalTemplates.Spec:
specs/2026-05-08-multi-scaffold-templates-experience/SPEC.md(D1–D14 LOCKED). Research:reports/llm-brain-scaffolds-research/{REPORT,PROPOSAL,USE-CASES,PACK-REFINEMENT-2026}.md.- Knowledge base (existing, renamed from "LLM brain") — Karpathy three-layer source-grounded KB (
0.4.0-beta.27
0.4.0-beta.26
0.4.0-beta.25
Minor Changes
feat(mcp):
ok mcpis now an inline stdio MCP server with per-call project routing.The default mode replaces the previous stdio→HTTP shim that bound to a single project at startup. One stdio process can now serve any number of OK projects on the host: each tool call resolves its own
cwdargument, walks up to the nearest.ok/directory (must be a directory; regular files and dangling symlinks named.okare rejected), loads the matching project config, and proxies HTTP traffic to that project's runningok start(auto-spawned whenOK_MCP_AUTOSTARTis unset or non-zero).This makes it safe to register
ok mcponce globally in MCP hosts (Claude Code, Cursor, Codex). The host can call any tool against any local OK project by passing an absolutecwdargument.Breaking change for direct MCP clients: the global path requires an explicit absolute
cwdon every tool call. The previous in-project mode silently defaulted to the configured project root. The global server has no fixed root and throws a clear error whencwdis omitted. Tool-arg descriptions have been tightened to document the new contract. The legacy--port <port>mode (single-backend stdio→HTTP shim) is unchanged.Existing on-disk MCP host registrations from prior
ok initruns continue to register the binary; modern MCP-aware agents will read the updatedcwddescription and pass it per call.
0.4.0-beta.24
0.4.0-beta.23
0.4.0-beta.22
Minor Changes
feat(api): RFC 9457 Problem Details envelope across all HTTP handlers (api-design-hardening)
All 57 handlers in
packages/server/src/api-extension.tsnow share a single canonical wire format:- Errors emit
Content-Type: application/problem+jsonwith a flat body{ type, title, status, instance?, detail? }per RFC 9457. Thetypefield is a closed-enum URN of the formurn:ok:error:<kebab>(per RFC 9457 §3.1.1 — URN form is routing-independent and won't change meaning under reverse-proxy / path-prefix). Thetitleis a required short English summary;instanceis a UUID correlation ID emitted alongside the structured Pino log line for grep-correlated triage;detailis an optional longer explanation. - Success drops the
{ ok: true, ... }wrapper and emits a flat{ ...data }body withContent-Type: application/json. Clients narrow on the HTTP status code (if (!res.ok)) before parsing — the RFC 9457 two-step parse pattern.
This is a wire-format breaking change for any direct in-process consumer of the HTTP API. The
@inkeep/open-knowledgeMCP shim's internalhttpGet/httpPosthelpers wrap the new flat success body with{ok: true, ...body}for in-process MCP tools so existingif (!result.ok) return errorshort-circuits keep working — MCP's own{content, isError?}envelope is unchanged.Structural enforcement:
- Per-handler
XyzRequestSchema+XyzSuccessSchemaZod schemas live in@inkeep/open-knowledge-core/schemas/api. Every schema exportssatisfies StandardSchemaV1<...>. - Request bodies validated through a
withValidation()middleware wrapper atpackages/server/src/http/request-validation.ts(handlers can't be added without going through it). - Errors emit through
errorResponse(res, status, type, title, options)atpackages/server/src/http/error-response.ts— the only sanctioned site (packages/app/tests/integration/error-envelope-coverage.test.tsruns in fail-on-any-occurrence mode and AST-scansapi-extension.tsfor inline{ ok: false, ... }and{ ok: true, ... }literals). - NDJSON streaming endpoints (clone, auth-login, auth-repos) emit pre-stream errors through
errorResponseand mid-stream errors throughstreamingProblemEvent({type: 'error', problem: ProblemDetails})events — typed envelope preserved across the streaming protocol. - Closed-enum
ProblemTypeURNs (~40 tokens) plusassertNeverProblemType/assertNeverLinkTargetexhaustiveness helpers are structurally enforced bypackages/app/tests/integration/exhaustiveness-coverage.test.ts— derived from the schema at test-discovery time so the registry never drifts. - Telemetry:
ok.api.error.count{type, handler}counter increments on every error emit.
Client lockstep: 23 sites in
packages/app/srcreadingdata.ok/body.ok/raw.okmigrated to the two-step parse pattern.class HttpResponseParseErrordistinguishes contract-shape responses (RFC 9457 problem details) from non-contract responses (proxy 502 HTML, network failures).Defense-in-depth: SVG is in
IMAGE_EXTENSIONS(so the editor's<img src=svg>rendering still works — browsers ignore Content-Disposition for embed contexts) but excluded fromINLINE_RENDERABLE_EXTENSIONSso top-level navigation to.svg(web fallbackwindow.openfrom a markdown SVG link) downloads instead of executing embedded<script>under same origin. Aligns with Docmost's posture; cf. GHSA-rcg8-g69v-x23j (Plane SVG XSS).Full spec + decision log (D1–D38, US-001 through US-014):
specs/2026-04-30-api-design-hardening/SPEC.md. Canonical pattern guide:packages/server/src/http/README.md.- Errors emit
feat(desktop): per-project onboarding consent dialog + git-root promotion + boot-time config load.
Opening a folder in the macOS desktop app no longer scaffolds
.ok/silently. Two paths now split on the user's Navigator gesture:- Pick Existing Project (and Recents, deep-link, drag-drop) → a per-window consent dialog opens to confirm scaffolding details before any filesystem write. The dialog covers content directory (with a Browse button for sub-folder selection),
.okignorepatterns, AI-tool integration multi-select (all editors checked by default — explicit, not detected), and sensitive-path warnings (/,~,~/Documents,~/Desktop,~/Downloads,/Volumes/<mount>). Git is initialized implicitly when the picked path has no real.git/— no UI toggle. Picking a folder via the dialog == agreeing to scaffold; users who don't want OK in their folder simply Cancel. - Start Fresh → silent path; defaults applied without prompting.
Re-opening a folder that already has
.ok/never re-fires the dialog.One
.ok/per git repo (G11). When the picked path sits inside a git working tree and no ancestor.ok/is present,.ok/materializes at the git working-tree root andcontent.diris pre-filled with the picked sub-path. Eliminates orphan sibling.ok/s within the same repo and aligns OK's project boundary with git's. Applies to desktop (Start Fresh + Pick Existing) AND CLI (ok init).Sub-folders of an OK-managed project promote to the ancestor
.ok/. No nested.ok/ever materializes. The picked sub-path is dropped; the editor opens at the ancestor with a 4 s "Opened existing OK project at <ancestor>" toast.Desktop boot now reads project config. The per-window utility calls
readConfigSafelyagainst.ok/config.ymlBEFOREbootServer, socontent.dirfrom the project file wins over the IPC default. Invalid YAML logs[config] desktop boot config invalidand falls back to schema defaults; the editor still opens.ensureProjectGithardened. Now validates.git/HEAD(not just.git/) and auto-repairs the shell-.git/regression class — folders left with.git/ok/(the shadow subtree) but no.git/HEADget a single silentgit initon next open.git statusworks in OK projects from the moment OK touches them..git/ok/is preserved;Save Version's parent-git path producesok/v<N>tags going forward.CLI parity:
ok initfrom a git sub-folder writes.ok/at the git root via the new sharedresolveProjectRoothelper, withcontent.dirscoped to the sub-path. Surfaces a single disclosure line on first-time scaffold:[ok] Initialized OK at <gitRoot> (scoped to <subPath>/)for git-root promotion,[ok] Opened existing project at <ancestor>for ancestor promotion.ok startoperates against the cwd directly and does not scaffold — single-responsibility per command,initinitializes,startstarts.OkDirMissingErrorfires loud when.ok/is missing.
New OTel span. Each onboarding flow emits one
ok.desktop.onboardingConsentspan with bounded-cardinality attributes (flow_kind,entry_point,git_init_requested,content_dir_changed,warnings_count,ai_integrations_failed_count). No raw paths.New degraded subsystem.
'project-git-shell-only'joinsshadow-repo/head-watcher/file-watcherin the utility'sdegradedarray whendiscoverProjectsaw a shell-only.git/even after auto-repair (defense-in-depth — should not occur in practice).Spec:
specs/2026-05-07-desktop-onboarding-consent/SPEC.md.- Pick Existing Project (and Recents, deep-link, drag-drop) → a per-window consent dialog opens to confirm scaffolding details before any filesystem write. The dialog covers content directory (with a Browse button for sub-folder selection),
Patch Changes
ok psandok stopnow surface and act on hostname-drifted servers correctly, label desktop-spawned servers asdesktop, and call out orphaned UI processes:- Foreign-host visible by default in
ok ps. macOS hostname drift (BonjourName ↔ FQDN across DHCP/VPN/sleep) flips lock entries toforeign-host; the default filter now keeps them alongsidealiveso same-machine servers don't vanish from the table.--allis now narrowed to "include stale (dead-pid) entries"; the help text anddocs/content/reference/cli.mdxare updated to match. - Desktop label. Desktop and CLI both write
kind: 'interactive', so the lock alone can't tell them apart.ok psclassifies via the live process command — Electron utility processes carry--type=utilityAND--utility-sub-type=node.mojom.NodeService(the latter is whatutilityProcess.forkspecifically registers, so VS Code / Slack / Discord helpers don't false-match). Renders asdesktop(blue), overridingrunning/foreign. Falls back when the command lookup is unavailable. ok stopaccepts hostname-drifted entries.buildStopPlan,findLockDirByNumber, and thestop allfilter now also stopforeign-hostentries when the PID is locally live (verified via the canonicalisProcessAliveprobe from@inkeep/open-knowledge-server, not a duplicate). Truly cross-host locks fail the liveness probe and are left alone.- Hostname-drifted dead locks classify as
dead-pid.inspectLockpreviously short-circuited toforeign-hoston hostname mismatch before the liveness probe, leaving stale drift locks stuck visible forever. Liveness now runs first;foreign-hostmeans specifically "different hostname AND PID exists locally" (the genuine drift case). Stale drift locks now fall out ofok psby default and become eligible forok cleanpruning. The nextok start/ desktop launch in that dir also auto-replaces them via the existingacquireProcessLockstale-replacement path, so manual cleanup is rarely needed. ui-orphanstatus. When the server lock isdead-pidbut the UI sidekick is alive (orforeign-host-with-live-PID),ok psnow renders the row asui-orphan(magenta) instead of hiding the orphan behind astalelabel or blanking the UI port. Surfaces the lifecycle hole whereok uisurvives an ungraceful server crash; the underlying self-shutdown / boot-time-reap fix is tracked separately.- Foreign-host UI port shown in PORTS. Post the inspectLock reorder,
foreign-hostUIs have a live local PID listening on that port — show it. Hiding it (the prior behavior) made orphan UIs invisible because hostname drift forces them intoforeign-host.
JSON output of
ok psgains anisDesktop: booleanfield on each entry for tooling consumers.- Foreign-host visible by default in
0.4.0-beta.21
0.4.0-beta.20
0.4.0-beta.19
0.4.0-beta.18
Patch Changes
- Fix agent-presence icon flickering between MCP tool calls. The
ok mcpshim now forwards its keepalive WSconnectionIdviax-ok-connection-id, and the MCP HTTP session adopts that id asidentity.connectionIdinstead of minting a fresh UUID. With both surfaces sharing one id, the 3sbumpPresenceTsheartbeat keeps the broadcaster entry fresh between writes, and on-closeclearPresencefinds the right key. The icon now stays visible for the lifetime of the keepalive WS instead of disappearing 5s after each tool call. Header values are validated throughvalidateAgentId. Invalid values fall back torandomUUID()so non-shim MCP clients still get a working session.
0.4.0-beta.17
0.4.0-beta.16
0.4.0-beta.15
0.4.0-beta.14
0.4.0-beta.13
Patch Changes
- Fix
TS2559: Type 'ProcessEnv' has no properties in common with type '{ HOST?: string | undefined; }'atpackages/cli/src/commands/start.ts:540. TheresolveHosthelper'senvparameter type was a TypeScript "weak type" (only optional declared properties) that triggers TS2559's weak-type detection when called withprocess.env—NodeJS.ProcessEnvdeclares noHOSTproperty; its index signature alone doesn't satisfy the rule. Widened the parameter to{ HOST?: string | undefined; [key: string]: string | undefined }so production callers passingprocess.envand existing unit-test literals ({},{ HOST: '...' }) all type-check. Function body and runtime behavior unchanged. Restoresbun run typecheckto green at the monorepo level (canonical CI gate; introduced in #492 and slipped through that PR's typecheck job).
0.4.0-beta.12
Patch Changes
- Production hardening + test helper disambiguation for the sidebar resize race (#576). Retroactive changeset — this PR landed without one, this triggers the next VP cycle so the changes flow through the beta cadence.
0.4.0-beta.11
Patch Changes
- The
sourceLiteralmark's mdast-conversion path now refuses to persist asourceRawattribute that doesn't match the visible PM text it's attached to. Previously a caller that could mutate the document (agent API, synced collaborator, crafted clipboard paste) could store one byte sequence as visible text and persist an arbitrarily different sequence on save — a hidden-content injection vector where the editor displays "Hello" but the on-disk file or downstream LLM transcript contains a prompt-injection / commit-message-injection / structural-markdown payload.isValidSourceLiteralRawgates the single-child path; the multi-child / non-text fallback now dropssourceRawentirely (rather than re-using the unvalidated raw as both the synthetic value and the synthetic sourceRaw, which made the downstream defense-in-depth gate vacuous).sourceLiteraldeclaresexcludes: ''so it can co-occur withstrong/emphasis/link/ etc., which is exactly the multi-child path; closing this gap completes the threat-model coverage.
0.4.0-beta.10
Patch Changes
openBrowser(called byok start --open/ok ui --open) now refuses URLs that contain shell-metacharacters (& | < > ^ ( ) ; $ \\" ' [ ]plus newlines and spaces) and limits the scheme tohttp(s)before invokingcmd /c start ""on Windows. Previously a malicious or misconfigured--host/HOSTenv /server.hostconfig value (e.g.localhost&calc) was concatenated into the launcher URL and could reachShellExecuteaftercmd.exeparsed the metacharacters as additional commands. Validation runs on every platform but the practical RCE is Windows-only; macOS / Linux launchers (open/xdg-open) are not vulnerable to the same expansion. Validation tests now run in CI (the innerURL validationdescribe was previously nested inside the CI-skip-gated outer block; it's been hoisted to a top-level describe using_bunDescribedirectly so regressions are caught by the automated pipeline).
0.4.0-beta.9
Patch Changes
- Agent and contributor summaries are now stripped of newlines, control characters, and other commit-message-meaningful bytes before they're concatenated into shadow-repo commit messages. Without the strip, an agent or actor could inject
\n\nSigned-off-by: someone-else <fake@example.com>(or any git trailer / multi-line block) into the commit body by embedding the bytes in a summary field, producing forged author attribution or trailer-driven side effects in downstream tooling.normalizeSummaryalready capped length and rejected non-strings; the new step also strips line breaks and control bytes from base + per-element summaries before they reachgit commit -m.
0.4.0-beta.8
0.4.0-beta.7
Patch Changes
- The reconciliation engine's LCS-based diff path now caps the input length and falls back to a whole-doc replace once the cap is exceeded, instead of allocating an
O(n*m)table over arbitrary user input. A multi-megabyte agent write previously sent the LCS path into a memory allocation proportional toprevLength * newLength, exhausting the Node heap and crashing the server. The cap is sized so that legitimate edits (well below 1 MiB per side) continue through the precise LCS diff while pathological inputs get the coarser whole-doc replace path; either way the persistence layer ends up with the user's bytes.
0.4.0-beta.6
Patch Changes
- Fix CLI npm publish: set repository.url to inkeep/open-knowledge-legacy (matches OIDC build context, satisfies sigstore provenance) and drop leading "./" from bin paths so npm publish stops auto-removing them.
- Drop
repositoryfield from CLI package.json so npm sigstore provenance no longer rejects publish with E422. The previous URL pointed at the futureinkeep/open-knowledgerepo while builds run frominkeep/open-knowledge-legacy; with the field removed, npm infers the repo from the OIDC build context and provenance matches by construction. No revert needed after the planned repo rename.
0.4.0-beta.5
Patch Changes
- The local-op path validators (
assertLocalOpPathSafeand friends) now resolve every path throughrealpathSyncand refuse paths whose canonical target lies outside the project's content root. Previously a.local/oppayload with a path likesubdir/symlink-to-etc/passwdonly ran a lexical containment check; ifsubdir/symlink-to-etcwas a symlink pointing outside the project, the local-op handler would happily read or write the target. Symlink loops surface asELOOPand are also rejected. The path-safety helpers are now shared with the seed-walk equivalents so the two security boundaries stay in sync.
0.4.0-beta.4
Patch Changes
- The backlink index's wiki-link extractor now linearly skips backtick spans rather than falling into a quadratic regex on long unclosed backtick runs. A doc containing thousands of consecutive backticks (no closing pair) previously sent the parser into pathological backtracking and pinned a CPU core for seconds-to-minutes per re-index. The new path scans backtick boundaries with a single pass and short-circuits when no closing pair is found, keeping per-doc parse time linear in document size regardless of backtick density.
0.4.0-beta.3
Patch Changes
- The
/api/history/:docNameendpoint now rejectsdocNamevalues that contain.., leading slashes, drive letters, or otherwise resolve outside the project's content root. Previously the docName was concatenated into asafeContentPathlookup without anisSafeDocNamegate; a request likeGET /api/history/..%2F..%2Fetc%2Fpasswdcould traverse out ofcontentDirand surface git history (or, on systems where the timeline-query path read file contents, the file itself). The handler now returns400 invalid-docNamewhen the input failsisSafeDocName, matching the contract used by every other docName-keyed endpoint.
0.4.0-beta.2
Patch Changes
The collab server now releases empty Y.Docs that were never confirmed to back an on-disk file once their last connection drops. Without the carve-out, a local peer (browser tab, MCP agent, DNS-rebound origin) could connect to an arbitrary stream of unique non-existent docNames over the loopback
/collabWS and growhocuspocus.documentswithout bound — each allocating aY.Doc+ persistence-store handle that lived until process shutdown. The carve-out only releases phantom docs (nogetReconciledBaseand empty fragment + empty Y.Text); file-backed and content-bearing docs continue to follow the resident-for-lifetime rule the cache-epoch-recovery defense relies on.Also drops a stray
docName/agentIdshorthand reference from threeAgentSessionCapacityErrorlog calls inapi-extension.ts— a typecheck regression that's been onmainsince #449's merge dropped my earlier revert. Tests + typecheck pass with this PR's branch butmainitself does not type-check until this lands.
0.4.0-beta.1
Patch Changes
- The forward-links endpoint now passes every wiki-link target through the project's
ContentFilterbefore reading the target document's frontmatter. Previously a wiki-link to a path excluded from the project (via.gitignoreor.okignore) would still resolve, surface the excluded doc's title in the response, and effectively leak the file's existence + title to anyone with read access to the linking doc. The exclusion gate is now applied per-target so excluded docs render as plain wikilinks (no resolved title) without revealing whether they exist.
0.4.0-beta.0
Minor Changes
feat(editor): asset upload +
![[file.ext]]wiki-embed surfaceAny file drop is accepted by the editor — there is no user-facing byte cap. PDFs, video, audio, archives, and fonts stop hitting the old "Unsupported file type" dead-end. The emit shape is picked by extension: markdown files (
.md/.mdx) emit as[[basename]]wiki-links (link-semantic, navigable on Cmd-click, resolved viafileIndex— markdown is a first-class OK doc, not an opaque asset); images + typed renderable files (PDF, MP4, WebM, MP3, WAV, OGG, M4A, MOV) emit as![[file.ext]]wiki-embeds; opaque files emit as[name](path)markdown links. Uploads stream to disk end-to-end (memory footprint is O(1), not O(fileSize)), so the only rejection axis is disk fullness (storage-full→ HTTP 507). Seereports/streaming-upload-refactor/REPORT.mdfor the refactor rationale.Same-directory sha256 dedup returns existing paths on duplicate drops with a toast (
"Already at <path> — reusing."). Renaming a doc that contains image refs recomputes the relative path; absolute refs and wiki-embed refs are untouched because the basename index resolves them dynamically.New HTTP surface on the server:
POST /api/upload— upload endpoint. Success response:{ ok, src, path, deduped }wheresrcis the asset's basename andpathis the contentDir-relative location (colocated with the referencing doc). Error responses carry a typederrorreason (malformed-upload/storage-full/storage-readonly/collision-exhaustion/storage-error) plus a human-readablemessage.
No user-facing
upload.*config. Attachment placement (co-located), emit shape (![[...]]for supported extensions), same-directory sha256 dedup with a toast notice, and the wiki-embed extension list are fixed defaults. Every value is a module-level constant in@inkeep/open-knowledge-core/constants/upload.ts. One-shot Obsidian-vault migration CLI deferred to a future spec — OK does not read.obsidian/app.jsonat runtime; refugees whose vault uses non-default config shape wait for the future migrator. Legacy configs still carryingupload.*keys parse cleanly (unknown keys are silently stripped).File watcher now emits
asset-create/asset-deleteDiskEvents alongside the existing markdown events; CC1ch:'files'signal coalesces both so file-sidebar and basename-index rebuilds piggyback on one broadcast.sanitizeFilenamepreserves Unicode code points (letters, digits, marks, punctuation, emoji) while stripping path separators and control bytes.Full spec + decision log (D1–D-M):
specs/2026-04-16-editor-asset-and-embed-surface/SPEC.md. Operator-facing guide: Assets and embeds.Asset-click dispatcher + OS-integration surface (2026-04-23 amendment). Click a
![[meeting.pdf]]embed and the PDF opens predictably — a new browser tab in web,shell.openPathin Electron. Previously post-reload clicks routed through the doc-link navigator and failed silently (Gap 3b); Electron drop-time clicks replaced the editor window (Gap 4). Both gaps close.ClassifiedLinkTargetgains a first-class{kind: 'asset', url, ext}variant;resolveAssetProjectPathresolves relative hrefs against the source doc's directory.- Renderer-side dispatcher + empty-at-landing viewer registry at
packages/app/src/editor/asset-dispatch/— future PRs register PDF.js / image lightbox / video-audio viewers as ~40-60 LOC plugins without modifying the dispatch layer. - Three new Electron IPC channels (
ok:shell:open-asset,ok:shell:reveal-asset,ok:shell:show-asset-menu). Main-processopenAssetSafelyenforces path containment (realpath+isPathWithinProject), existence, and an executable-extension blocklist (.exe/.sh/.html/.svg/…) source-verified from Obsidian 1.12.7. Renderer sends project-relative paths; containment fires at the IPC boundary. - Right-click any on-disk reference (asset chip, wiki-link chip, image) → native OS menu with Reveal in Finder / Show in Explorer + Open in default app + Copy link. Gesture-attested (main observes the click directly).
- Defense-in-depth:
setWindowOpenHandler+will-navigateon the editor webContents intercept any asset URL that escapes the renderer dispatcher (pasted<a href>, plugin content, drop-time<a target="_blank">). Same path containment + blocklist enforced on every entry point.
Full amendment (US-A1..A6, FR-A1..A8, NG-A1..A6, D-A1..A12):
specs/2026-04-16-editor-asset-and-embed-surface/SPEC.md§Post-finalization amendment (2026-04-23). Research:reports/electron-os-integration-patterns/+reports/editor-asset-embed-patterns-across-universe/D9.feat: Component Blocks v2 — 5-pack foundation (Callout + Image + Video + Audio + Accordion)
The editor now ships five built-in component primitives —
Callout,Image,Video,Audio, andAccordion— each with a WYSIWYG settings panel, a slash-command insertion menu, and lossless on-disk round-trip for both the MDX form and the markdown form (where one exists). Every primitive is a DIY React component on Open Knowledge's own brand (shadcn / Tailwind); the editor bundle no longer pulls infumadocs-ui's React surface or its CSS variable bridge.What you get out of the box:
- Callout — five GFM alert types (
note/tip/important/warning/caution) plus optionaltitle/icon/color/ and Obsidian-style foldable chrome (> [!NOTE]+/-). Authoring works in any of three forms: GFM alert blockquote, foldable Obsidian opener, or<Callout type="…">…</Callout>MDX JSX. Common alias tokens (success→tip,danger→caution, etc.) fold to the GFM 5 on disk. - Image —
<Image src=… alt=… width=… caption=… />MDX, plus standard CommonMark. Both forms render through the same descriptor with click-to-zoom on by default; the MDX form additionally exposescaption(renders as<figure>+<figcaption>), explicit dimensions, andloading/zoomtoggles. - Video — pure HTML5
<video>wrapper with native controls. No YouTube / Vimeo URL sniffing — embed services with a raw<iframe>in MDX (matches Mintlify's pattern).<track>and<source>children round-trip. - Audio — pure HTML5
<audio>wrapper with native controls always on.<source>and<track>children round-trip. - Accordion — standalone HTML5
<details>/<summary>substrate, no wrapper component required. Cross-browser exclusive grouping via HTML5<details name="…">(Chrome 120+, Safari 17.2+, Firefox 130+). Authors can write either<details><summary>X</summary>Y</details>or<Accordion title="X">Y</Accordion>— both render the same descriptor.
Other improvements:
- Auto-generated settings panel from each component's prop types (string / boolean / number / enum) — no separate component prop docs required.
- Slash-command insertion with sensible defaults; the settings panel auto-opens on insertion so required fields are filled in before you move on.
- Hover chrome with move-up / move-down / delete / settings buttons.
- Keyboard navigation throughout (Tab / Esc / arrow keys with context-aware handling).
- Broken or unrecognized MDX components automatically open in an embedded source-code editor so authored content stays editable — nothing silently disappears.
- Both pristine and dirty save paths preserve the on-disk shape: unedited blocks round-trip byte-for-byte; edited blocks canonicalize to the MDX JSX form.
Breaking changes:
- Both the inline MDX element node (
jsxInline) and the block MDX component node (jsxComponent) changed PM-schema shape in this release.jsxInlinedrops itsattributesandsourceRawattrs — its text content IS the source of truth.jsxComponentwidens from an atom with a raw-content attr to a non-atom block withblock*children and new structured attrs (componentName,kind,attributes,sourceRaw,sourceDirty,props). This is a load-bearing change for collaborative editing — older clients coexisting with this version in the same live session substitute both nodes torawMdxFallback(raw source preserved as editable text) via the y-tiptap schema-throw substitution patch. Upgrade all clients in a session together — both inline JSX authoring and component-block authoring are affected, not just inline. Persisted documents are unaffected; the on-disk MDX is preserved. - Content using component names that are no longer built in
(
Tabs,Card,CardGroup,Steps,Banner,Files,TypeTable,InlineTOC,Mermaid,AudioPlaceholder,ImageZoom) opens as an editable raw-source block. Content is preserved verbatim. Rename<AudioPlaceholder />→<Audio />and<ImageZoom>→<Image>to pick up the new descriptors.
The compound-component tier (Tabs + Tab grouping, Accordion grouping with shared chrome, Steps + Step) is not built in today; it returns when concrete dev-docs / help-center authoring demand surfaces. No public API will change for existing 5-pack consumers when that happens.
Bundle size:
- Main app bundle stays flat (~210 kB gzipped) — the
fumadocs-uidrop and 12-descriptor cut offset the 5-pack prop-surface widening and new selection-chrome plugins. - Total JS across lazy-loaded chunks grows ~100 kB gzipped (~978 kB →
~1.08 MB) to accommodate CB-v2 feature surface (descriptor-dispatch
registry, V2 editor cache, SelectionStatePlugin + Breadcrumb +
SelectionAnnouncer + BlockDragHandle, nested CodeMirror for
rawMdxFallback, slash-command menu, canonical/compat descriptor split with three additional read-only source-form descriptors (GFMCallout, CommonMarkImage, HtmlDetailsAccordion) for round-trip preservation). Theall JS chunks combinedsize-limit ceiling is raised 1050 → 1100 kB (~2% headroom) to match. Delivered via on-demand chunk loading — users don't pay the full bill on first paint.
- Callout — five GFM alert types (
Config Editing Paths — end-to-end UX for editing Open Knowledge configuration:
- Settings pane in the editor area (Cmd-, / App menu / HelpPopover / Command Palette) with
This projectandUserscope tabs. Each field auto-saves; per-field reset; modified-at-scope indicator on cross-scope fields. - Real-time sync — Settings pane is bound to two Y.Text-only synthetic Hocuspocus docs (
__config__/workspace,__user__/config.yml). External edits via CLI, MCP, IDE hand-edit, or anotherok startinstance propagate via a chokidar file watcher into Y.Text and refresh any open pane within ~500ms. - Three-layer defense-in-depth validation — client walker (L1) → fs writer (L2) → persistence-hook (L3). Invalid mutations revert to LKG and surface a toast + brief field flash.
- MCP tools —
get_config,set_folder_rule. fs-direct (no running server required); auto-scope inference via the inspectConfig ladder; mixed-scope rejection. (set_configwas subsequently removed in the schema-simplification changeset; seeconfig-schema-simplify.md.) - CLI —
ok config validate(exits 0/1 with source-located errors) +ok config migrate(idempotent codemod that dropssync.*,persistence.{debounceMs,maxDebounceMs},server.port). ok initscaffolds the workspaceconfig.ymlwith a magic-comment$schemaURL pinned to the schema major (v0) +@latestof the npm package — additive schema changes reach existing users automatically; breaking changes bump the path tov1and old majors stay published forever.- Per-scope JSON Schemas —
dist/schemas/v0/config.workspace.schema.jsonand…/config.user.schema.jsonso VS Code's Red Hat YAML LSP only suggests fields valid AT the file's scope. - Schema cleanup — drops
sync.*(7),persistence.{debounceMs,maxDebounceMs}(2),server.port(1); addsappearance.themeandappearance.editorModeDefault(user-scope, both UNSET by default; chrome<ThemeToggle>writes throughuserBinding.patchso localStorage stays a derived cache).content.*is workspace-scope-only. - OTel — five new
config.*spans (config.bind,config.patch,config.validate,config.persist,config.revert) trace the full edit chain.
- Settings pane in the editor area (Cmd-, / App menu / HelpPopover / Command Palette) with
Config schema simplification — six fields removed, replaced with constants:
- Schema fields removed —
github.oauthAppClientId,server.host,server.openOnAgentEdit,mcp.autoStart,mcp.tools.read_document.historyDepth,mcp.tools.grep.maxResults. None were genuinely user-configurable in practice. Their default values now live as plainexport constin@inkeep/open-knowledge-core(constants/github.ts,constants/mcp.ts,constants/server.ts):DEFAULT_GITHUB_OAUTH_CLIENT_ID,DEFAULT_SERVER_HOST,READ_DOCUMENT_HISTORY_DEPTH,GREP_MAX_RESULTS. Configtype contract — TypeScript consumers readingconfig.server,config.github, orconfig.mcpwill now getnever. The schema's top-level surface shrinks tocontent,preview,appearance,autoSync. Layered config infrastructure (project/user merge, scope inference,__config__/project+__user__/config.ymlY.Text transports) is unchanged for the day a real user-configurable layered field needs it.set_configMCP tool removed — the agent-settable surface was empty after the twomcp.tools.*fields became constants.get_configandset_folder_ruleremain. Re-introduce when an agent-tunable field actually returns.- Runtime overrides preserved —
--hostflag andHOSTenv var (resolved at the start command via the newresolveHost()helper, mirroring the D29 port pattern);OPEN_KNOWLEDGE_GITHUB_CLIENT_IDenv var;OK_MCP_AUTOSTART=0env var. server.openOnAgentEdit— the auto-open-browser-on-first-agent-write feature was deleted entirely (default was off; collapsed dead conditional).- Loose-mode tolerance — existing user/project YAML configs that still set the removed keys parse without throwing; the loader emits a per-key deprecation warn pointing at the right knob (
--hostflag,HOSTenv,OPEN_KNOWLEDGE_GITHUB_CLIENT_ID,OK_MCP_AUTOSTART=0, or "hardcoded in@inkeep/open-knowledge-core" when no override exists). Oldmcp.tools.search.maxResults(pre-#491 rename) is also flagged. - Settings pane — Server, GitHub, and MCP sections removed.
set_configMCP tool description and reference docs cleaned up.
Migration notes for consumers:
- Reading
config.server.host,config.mcp.autoStart, etc. → switch to runtime override (--host/HOST,OK_MCP_AUTOSTART=0) or import the constant directly from@inkeep/open-knowledge-core. - Calling
set_configMCP tool → use the Settings pane or hand-editconfig.yml. Useset_folder_rulefor folder rules;get_configfor reads.
- Schema fields removed —
feat(frontmatter-editing-ux): top-of-document property panel + per-key
Y.Map('metadata')storage +frontmatter_patchMCP tool. Frontmatter is now editable inline in WYSIWYG mode through typed widgets, and concurrent edits from a human and an agent to different properties merge at the field level instead of clobbering each other through document-level last-write-wins.@inkeep/open-knowledge-core— newpackages/core/src/frontmatter/module exportingFrontmatterValueSchema,FrontmatterPatchSchema,FRONTMATTER_TYPES, and the comment-preserving YAML codec (parseFrontmatterYaml/serializeFrontmatterMapoveryaml@2.x'sparseDocument). Bridge readers/writers inpackages/core/src/bridge/frontmatter-y.tsextended withgetFrontmatterMap,setFrontmatterFromYaml,setFrontmatterProperty, andcomposeFrontmatterForStore.getFrontmatter(doc)now synthesizes from per-key entries when present, falls back to the legacy single-string slot otherwise — existing string-shape callers continue to compile unchanged.@inkeep/open-knowledge-server—Y.Map('metadata')now carries one entry per frontmatter property (Y.Textfor editable strings,Y.Array<Y.Text>for lists, primitives for atomics). NewPOST /api/frontmatter-patchroute +handleFrontmatterPatchhandler applies JSON Merge Patch (RFC 7396) atomically under a per-session, not pairedformOrigin. Observer A's metaMap deep-observer recomposes YAML+body and propagates toY.Textafter settlement.onLoadDocumentruns an eager-on-load migration;applyExternalChange(file watcher) and Observer B reconciliation use per-key diff so undoing a single property reverts only that property.onStoreDocument'scomposeFrontmatterForStorewrites the legacy YAML byte-string verbatim when the per-key map still matches it — comments, blank lines, and scalar styles round-trip losslessly.agent-patch(/api/agent-patch) returns HTTP 400 on FM-intersecting find/replace calls with a migration hint pointing atfrontmatter_patch. New OTel spansfrontmatter.patch+frontmatter.form_write; new counterok.frontmatter.edit_surface_totallabels writes by source (form/mcp-patch/mcp-write/file-watcher/source-mode).@inkeep/open-knowledge— newfrontmatter_patchMCP tool. Set / create / delete frontmatter properties with{patch: {key: value | null}}; optionaltypesmap overrides per-key widget inference (text / number / boolean / date / list); optionalsummarythreads through to the per-contributor attribution journal under the same 80-char cap as the other write tools.@inkeep/open-knowledge-app— new top-of-document Properties panel above the body in WYSIWYG mode. Five widget types (Text / Number / Boolean / Date / List), inline add / delete / rename, type picker dropdown, per-row hover chrome, collapse via chevron, empty-state seeded via the editor toolbar's Add Properties button. All form interactions wire throughPOST /api/frontmatter-patchwithsource: 'form'.
Storage migration is automatic on document load — no user action required. The legacy single-string
metaMap.get('frontmatter')slot is retained as a transitional byte-identical mirror so YAML comments and scalar styles survivedoc-load → no-op-edit → doc-saveround-trips.frontmatter_patchis the only MCP surface for frontmatter edits going forward; the soft-deprecation window foragent-patchFM-touching calls is closed and those now return HTTP 400.Full spec + decision log:
specs/2026-04-24-frontmatter-editing-ux/SPEC.md.MCP search/grep realignment — rename the existing
searchtool togrep, add a new rankedsearchtool that mirrors cmd-K.- Tool removed:
search(the previous tool — fixed-string grep with frontmatter enrichment). - Tool added:
grep— replaces the legacysearchtool with identical behavior. Use it when you need every literal occurrence of a string across content. - Tool added:
search— new ranked workspace retrieval. WrapsPOST /api/searchso results match the cmd-K palette (Orama-backed lexical + body BM25 + recency). Parameters:query,intent('omnibar' | 'full_text', default'full_text'),scopes,limit(default 20, max 100),cwd. Both new tools register MCPannotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }. - Config key: the grep tool's result cap is
mcp.tools.grep.maxResults(default 50). - Server-side instructions (
buildInstructions) now surface the four-way read-tool routing (exec/read_document/search/grep).
Migration:
- Rename any caller using
mcp__open-knowledge__searchfor grep semantics tomcp__open-knowledge__grep. - Switch to the new
mcp__open-knowledge__searchfor relevance-driven retrieval (the same ranking the cmd-K palette uses). - If your
.ok/config.ymlcontainsmcp.tools.search.maxResults, rename the key tomcp.tools.grep.maxResults. The legacy key is silently ignored (loose-object pass-through), so a custom value will not carry over automatically.
Spec:
specs/2026-05-05-mcp-search-grep-rename/SPEC.md. Background research:reports/file-search-tool-mcp-exposure/REPORT.md.- Tool removed:
feat(rename): rename
.open-knowledge/→.ok/everywhere (per-project,~/.ok/, and the shadow repo at.git/ok/); replacecontent.includeandcontent.excludeinconfig.ymlwith a.okignorefile at the project root using gitignore syntax.Hard cutover for the directory rename (pre-release license to break — no auto-rename of
.open-knowledge/→.ok/):- The per-project state directory is now
.ok/instead of.open-knowledge/. Same for the user-global directory (~/.ok/) and the shadow repo (.git/ok/). content.includeandcontent.excludeare removed fromConfigSchema. If they appear in yourconfig.yml, OK rejects the file with a source-located error directing you to move the patterns into a project-root.okignore..okignoreuses gitignore syntax (parsed by theignorenpm library) and is evaluated alongside.gitignorein a single ignore-lib instance — cross-source!overrides work (e.g.!secret.mdin.okignorere-includes a file.gitignoreexcluded). Nested.okignorefiles at any folder depth are honored.ok initnow scaffolds both.ok/and a project-root.okignore(commented header, no example excludes).- The Settings pane's Content section is removed;
content.dirbecomes YAML-only (default.covers the common case). - The MCP
set_configallowlist drops to 3 paths:folders[],mcp.tools.search.maxResults,mcp.tools.read_document.historyDepth.
For pre-existing OK projects:
- Rename your
.open-knowledge/directory to.ok/(manual — no auto-rename shim). - Lift any
content.include/content.excludepatterns into a project-root.okignore(recreate exclusion patterns; remember the.okignoremental model is exclude-only). - Run
ok config migrateto strip the obsoletecontent.{include,exclude}keys fromconfig.yml. The codemod is idempotent and also clears the other deprecated keys (sync.*,persistence.{debounceMs,maxDebounceMs},server.port). - Delete the orphan
.git/open-knowledge/shadow repo. - Re-authenticate.
The dogfood team will see one re-prompt for stored credentials (
~/.ok/auth.yml), first-launch consent (~/.ok/mcp-status.json), and the skill-installed marker on first run after merging — expected behavior for the hard cutover on the user-home rename.The protected identifiers (MCP server name
open-knowledge, writer-IDopenknowledge-service, bundle IDcom.inkeep.open-knowledge, URL schemeopenknowledge://, package names) are unchanged.- The per-project state directory is now
feat(rename): consolidate
/api/renameand/api/rename-pathinto a single polymorphic endpoint, lift the link-rewrite spine, extend the recovery journal to v2, and add principal-attribution fallback for rename + rollback handlers.Breaking change —
POST /api/renameis removed. Clients (UI, MCP, scripts) must usePOST /api/rename-pathwith the polymorphic body shape:{ "kind": "file" | "folder", "fromPath": "<path>", "toPath": "<path>", ...identity, "summary": "..." }The MCP
rename_documenttool's outward API is unchanged (parameters, response shape, identity passthrough are all functionally equivalent) — only the internal HTTP target changed.New capabilities:
- Folder rename now rewrites all inbound wiki-links and supported markdown links across linking docs (was previously a CONFIRMED gap — folder rename moved files but left link text untouched).
- Folder rename is now crash-safe — process kill mid-batch is recoverable on next startup with no partial state.
- File rename via the consolidated endpoint now updates the in-memory backlink index (was missing on the file branch of
/api/rename-path). - UI-driven rename and rollback now attribute to the server-loaded principal (
principal-<uuid>) when no agent identity is supplied. Body-suppliedprincipalIdis silently ignored — server'sgetPrincipal()is the only source of principal identity.
The recovery journal schema is bumped from v1 (single source/destination) to v2 (multi-doc
affectedDocs[]). The v1 parser is preserved alongside v2 — legacy v1 journals on disk at startup still recover correctly.Side-effect docs (backlink-rewrite cascades) remain anonymous for both agent-driven and principal-driven renames — only the renamed doc itself is attributed to the actor.
Other behavior changes worth noting:
POST /api/rollback500 responses no longer echo the underlying error message; clients now see a genericFailed to roll back documentstring. Eliminates a path / internal-state leak channel for an unauthenticated body endpoint.ok initaddsstate.jsonto the bootstrapped.ok/.gitignorealongsidesync-state.jsonandprincipal.json— covers newly-added local-runtime state at<contentDir>/.ok/state.json(also emitted as a separateinit-gitignore-consolidationchangeset for the broader scaffold rework, but thestate.jsonline specifically lands here).
feat(server): boot in linked git worktrees + fail-fast on missing
.ok/config.yml.Open Knowledge now boots correctly inside linked git worktrees (
git worktree add <path> <ref>).- Main worktree — shadow at
<projectRoot>/.git/ok/ - Linked worktree — shadow at
<repo>/.git/worktrees/<name>/ok/
ok initworks identically in main and linked worktrees with no special handling — the existingwriteIfMissingsafeguard in the init scaffold prevents clobbering committed configs thatgit checkoutmaterialized.Fail-fast on missing config.
bootServernow runs a pre-listen check for<contentDir>/.ok/config.yml. When absent (whether.ok/is missing entirely or justconfig.yml), boot rejects with the new typedMissingOkConfigError(exported from@inkeep/open-knowledge-server):Open Knowledge config not found at
.ok/config.yml. Runok initto scaffold OK in this directory.When only
.ok/.gitignoreis missing (config present), boot emits a one-time stderr warning recommendingok initand proceeds — per-machine state files in.ok/may show up as untracked changes until the recommended ignore entries are merged in.Desktop GUI: silent scaffold on first open. When you open a folder that doesn't yet have
.ok/config.yml, the Open Knowledge desktop app now scaffolds it silently and opens the editor — same idempotent flow asok init. The user's Navigator / recents / deep-link / drag-drop gesture is treated as consent for the scaffold.writeIfMissingensures committed configs (materialized bygit checkout) are never clobbered. Explicit-consent dialog UX (Yes/No modal before scaffold) is a follow-up.Unusable
.gitpaths surface as typed errors with the right recovery hint. Two distinct failure modes, two distinct typed errors (both exported from@inkeep/open-knowledge-serverand@inkeep/open-knowledge-core/shadow-repo-layout):MalformedGitPointerError—<projectRoot>/.gitis a file but itsgitdir:target is unreadable, has nogitdir:line, or references a missing admin directory. Typical cause: a partialgit worktree removerace orrm -rfof the admin directory withoutgit worktree prune. Recovery hint namesgit worktree prune.GitDirAccessError—statSyncon<projectRoot>/.gitfailed for a reason other thanENOENT(typicallyEACCES/EPERMon a misconfigured ACL or stale mount). The shape of.gitis undetermined. Recovery hint names filesystem permissions and mount state.
Both errors carry the original
errnoexception ascauseso log consumers can branch onEACCESvs parse-failure without parsing strings.Observability. The boot path emits an OTel
ok.bootspan carryingok.worktree.kind(main|linked) and a normalizedok.worktree.gitdirso failure rates can be sliced by worktree shape.Two worktrees on different branches running
ok startconcurrently are fully independent: separate shadows, separate.ok/server.lockfiles, separate ports, separate MCP endpoints. Per-worktree isolation is the intended model — writer history, upstream imports, and checkpoints stay scoped to the worktree they were authored in.- Main worktree — shadow at
Patch Changes
Fix
Cannot find package '@inquirer/checkbox'crash on every CLI invocation in the packaged desktop.app.tsdown.config.tsalwaysBundlelisted@inquirer/passwordbut missed@inquirer/checkbox—cli.tseagerly importsinitCommand, which top-level imports@inquirer/checkbox, so every subcommand (auth status,auth repos,clone, …) crashed before producing any output. Dev mode resolved the dep from the workspacenode_modulesand hid the bug. The packaged.appships nonode_modulesnext todist/cli.mjs, so the bare specifier failed at module-load time. Added the matching pattern toalwaysBundleso the dep is force-inlined intodist/cli.mjs.User-visible symptom: clicking "Clone from GitHub" in the published Open Knowledge desktop app surfaced
auth status exited with code 1because the renderer's IPCauth statusprobe spawnedok.sh auth status --jsonand got a Node module-not-found crash with no JSON output.ok(no positional args) on macOS now launches the desktop Electron app when it's installed in/Applications/Open Knowledge.app(or~/Applications/). Detection is interactive-only — SSH sessions, non-TTY stdout, and missing bundles all fall through to the existingok startbehavior (server + browser). On Linux, Windows, and any other platform without the macOS desktop bundle,okrunsstartexactly as before.Two new escape hatches:
ok start --mode=browser|appis an explicit modal selector.--mode=browser(or omitted) keeps today's server-only behavior bit-for-bit;--mode=appforces the desktop hand-off and errors with a clear message if the bundle isn't found.--opencontinues to mean "open a browser tab against the local server" and is mutually exclusive with--mode=app.OK_FORCE_BROWSER=1env var disables the dispatch entirely for the current process. Use this in scripts/CI/MCP wrappers that need the server even on a workstation with the desktop installed.OK_FORCE_DESKTOP=1does the inverse (force-launch desktop, bypassing the headless gate).
Non-breaking —
ok start(no flag) is unchanged. The dispatch only fires whenokis invoked with no subcommand. All existing subcommands (mcp,init,status,stop, etc.) keep current behavior. Seespecs/2026-05-04-cli-default-desktop-launch/SPEC.md.Fix the "Install for Claude Chat & Cowork" handoff in the packaged Electron app, which previously failed with
build-failed: Could not resolve @inkeep/open-knowledge CLI version. The cause was a heuristic two-path filesystem probe inbuild-skill-zipthat didn't match thenode_modules/@inkeep/open-knowledge/layout shipped inside the .app — and the version it was looking up was not actually used in this code path.build-skill-zipis now a pure ZIP builder:BuildSkillZipOptions.skipVersionCheckandBuildSkillZipResult.cliVersionare removed in favor of a presence-basedBuildSkillZipOptions.expectedSkillVersion?: string(pass it from release builds that need to assert SKILL ↔ CLI alignment, omit otherwise).BuildAndOpenSkillResult.cliVersionis also removed —skillVersionis the canonical artifact version.A new
resolvePackageVersion(packageName, fromUrl)utility is exported from@inkeep/open-knowledge-serverfor any caller that needs to look up an installed package's version at runtime. It uses Node's module resolver plus a directory walk, which is robust across workspace symlinks, hoistednode_modules, pnpm.pnpm/-flat, and asar-packed Electron.The
ok install-skillpost-build message now readsSkill v<x.y.z>(presence-checked) instead ofCLI v<x.y.z>.Docs accuracy pass — surgical fixes for drift between
docs/content/and the shipped CLI/MCP/integration code.reference/cli.mdx:- Fixed broken markdown table in the
startflags section (missing| --- | --- |separator row meant the table didn't render). - Removed duplicate
--openrow. - Added the
--mode <browser|app>flag onstart(escape hatch for the desktop-dispatch path). - Added the
pscommand to the Lifecycle section (lists running OK servers).
integrations/codex.mdx: clarified thatinitalso installs the Open Knowledge skill globally (vianpx skills add) so Codex can discover the MCP tools — previously the doc said only "registers the MCP server."integrations/claude-desktop.mdx: added a date-stamped callout flagging the Cowork-bug workaround section as upstream-bug-driven, so it gets revisited when Anthropic ships a fix.get-started/quickstart.mdx: corrected the post-initdescription —initregisters MCP for all 6 supported editors (Claude Code, Cursor, Codex, Claude Desktop, VS Code, Windsurf) and installs a skill for Claude Code, Cursor, and Codex. Previous wording omitted Codex from the skill list and named only Claude Code + Cursor for the "every supported editor" claim.reference/configuration.mdxandreference/mcp.mdxwere re-verified during the pass and need no changes.- Fixed broken markdown table in the
chore(init): consolidate Open Knowledge ignore rules into
.open-knowledge/.gitignore. The scaffold now writescache/,server.lock,ui.lock,sync-state.json,principal.json, andlast-spawn-error.log, andok initmerges missing entries into pre-existing files (existing user lines preserved).ok cloneno longer mutates the cloned repo's tracked.gitignore; per-clone protection lives in.git/info/exclude(local-only, never committed) instead.Refuse to run
ok mcpin directories that haven't beenok init'd. Closes a regression where invokingnpx @inkeep/open-knowledge mcpfrom a non-OK directory eagerly scaffolded.ok/,.openknowledge/(legacy bare shadow), and.gitignoreas a side effect of MCP startup — observed when the OK MCP was registered globally (e.g.~/.claude.jsontop-levelmcpServers) and the user opened claude in any directory.ok mcpnow exits cleanly at startup if<cwd>/.ok/doesn't exist, before registering tools or discovering servers.--portbypasses the gate (explicit user intent). The skill description already says "Skip if no .ok/ — not an Open Knowledge project"; this enforces the same contract at the server level.refactor(mcp): route
ok mcpthrough the shared HTTP server. The CLI now uses a thin stdio-to-HTTP shim, removes legacy--pinsetup, and relies on the runningok startserver for MCP tool execution.chore(layout): move per-machine runtime files from
.ok/<name>to.ok/local/<name>.<contentDir>/.ok/now separates committed config (config.yml,frontmatter.yml,templates/) from per-machine runtime state, which moves under.ok/local/:server.lock,ui.lock,state.json,principal.json,sync-state.json,conflicts.json,last-spawn-error.log,managed-rename.json,cache/<branch>/backlinks.json,tmp/upload-<uuid>.OK_GITIGNORE_CONTENTis now a single line (local/) — adding a new runtime file no longer requires a coordinated.gitignoreedit, and.ok/local/documents the contract by name. All in-process consumers resolve the path through the newgetLocalDir(contentDir)helper exported from@inkeep/open-knowledge-core.If you're an OK developer pulling this branch and your project has runtime files at
.ok/root, runrm -rf .ok/thenbun run --filter=@inkeep/open-knowledge ok init. The boot logs a one-shot warning if it finds legacy files at.ok/root; the new code reads/writes only under.ok/local/, so legacy files sit inert until you clean them up. There is no auto-migration — Open Knowledge has not shipped to real users yet, so the developer-only re-init path is the right tradeoff (per spec D2 LOCKED).Spec:
specs/2026-05-05-ok-local-folder-pattern/SPEC.md.Remove the dead "Folders" section from the Settings pane.
The
folders[]cascade was retired in spec2026-05-01-folder-level-metadata-and-templates(FR8 / D19) — folder defaults moved into nested<folder>/.ok/frontmatter.ymlfiles, edited via theset_folder_ruleMCP tool. TheFoldersSection.tsxSettings UI and itsapplyFolderRulesUpsertwrite path were left behind in that change; entries typed into the section persisted intoconfig.ymlviaz.looseObjectpassthrough but no production code read them.This removes:
FoldersSection.tsx+ its smoke testapply-folder-rules-upsert.ts(the dead UI was the only caller;set_folder_ruleuses its ownapplyNestedFolderRulesUpsert)folder-rules.tslegacy resolver (zero production callers)- the
applyFolderRulesUpsertre-export from@inkeep/open-knowledge-core/server - stale "config.yml
folders:" references inenrichment.ts,exec.ts, andseed.tsJSDoc/CLI descriptions
FolderRuleSchemaandFolderFrontmatterSchemaexports remain in place —set_folder_rulereuses them.fix(rename): preserve file content when renaming via the sidebar.
Renaming a file via the sidebar inline-rename (right-click → Rename, or F2) was erasing the file content on disk: the editor showed an empty placeholder under the new name, and the original content survived only in the renaming tab's IndexedDB cache (which is why renaming back made it "reappear"). Cold reloads or other tabs/clients saw the renamed file as empty.
Two layered fixes:
FileTreeignores selection-change events whose docName isn't yet in the localdocumentslist.@pierre/treesfiresonSelectionChangesynchronously when an inline rename commits — before the rename API has written the file at the new path — and the resulting premature navigation was opening a server-side Y.Doc against a missing file, which the persistence layer subsequently flushed back to disk as 0 bytes.persistence.onStoreDocumentrefuses to materialize a 0-byte file when the Y.Doc was never confirmed to exist on disk AND the serialized markdown is empty. This blocks accidental orphan files from any code path that opens a Y.Doc for a non-existent docName (browser races,/api/document?docName=<missing>, MCP queries on deleted docs, future callers). Legitimate first-write paths (/api/create-page, agent writes via/api/agent-write-md) are unaffected.
The collab server now caps the number of distinct agent sessions per
Documentat 256 and refuses additionalagent-write/agent-write-md/agent-patchrequests with503 too-many-agent-sessionsonce the cap is reached. Without the cap, an unauthenticated peer (or a runaway agent loop) could mint unlimited freshagentIds, each spawning a new server-side session with its own bounded undo manager and presence entry, exhausting memory and overwhelming the awareness map. The 256 ceiling is well above any legitimate per-document agent fleet and triggers aAgentSessionCapacityErrorlog line includingdocNameandagentIdso operators can identify the offending peer.Bound desktop auth-query IPC subprocess fan-out. The
authStatus/authReposIPC handlers inpackages/desktop/src/main/ipc/local-op.tsnow coalesce concurrent requests for the same host and enforce a per-handler concurrency cap; overflow is reported back to the renderer as a structured response rather than spawning anotheropen-knowledge auth …child process. Closes the unbounded-spawn DoS path where a compromised renderer could exhaust the process table by tight-loopingwindow.okDesktop.localOp.authStatus()/authRepos().The chokidar fallback file-watcher now refuses to forward events whose path resolves through a symlink to a target outside the project's
contentDir. Previously a symlink planted in the watched tree (viagit clone, an extracted archive, or a co-tenant write) could surface as anadd/changeevent whosereadFilewould dereference the symlink and pull in/etc/passwdor another sensitive target. The neweventEscapesContentDirhelperlstats the event path, resolves the symlink withrealpathSync, and refuses unless the canonical target is withincontentDir. Thelstatandrealpathcatch blocks distinguishENOENT(delete-race, pass through) andELOOP(broken symlink, drop quietly) from other errno codes (EPERM/EACCES/EIO— drop and log) so a hostile symlink with restricted intermediate-path permissions can't bypass the gate by triggering an unexpected error.ok initnow refuses to overwrite project-scope config paths (.mcp.json,.cursor/mcp.json,.claude/skills/open-knowledge) when any segment of the destination is a symlink whose realpath escapes the project cwd. A malicious repo could previously plant a symlink that pointedgit cloned files at/etc/passwdor another sensitive location, then haveok initfollow it and overwrite the target. The newassertProjectPathSafeguard rejects leaf-symlink, ancestor-symlink, and parent-traversal escapes while still allowing in-cwd symlinks. Editor-ID lookups also switched from bracket access toObject.hasOwnso prototype-chain pollution can't surface forged editor labels.Treat lock-file PIDs as untrusted input across every signal-delivery and liveness-probe site. The desktop's collision-recovery path in
packages/desktop/src/main/window-manager.tspreviously read the holder PID straight from<contentDir>/.ok/local/server.lock(user-writable JSON) and calledprocess.kill(pid, 'SIGTERM')after only atypeof pid === 'number'shape check, allowing crafted lock entries with negative PIDs (process-group signaling on Unix),0,1(init), NaN, or values above0x7fffffffto direct signals at unintended processes.The fix layers three defenses through the new shared
isValidLockPidvalidator (packages/server/src/process-alive.ts):- Parse-time validation.
process-lock.ts,lock-state.ts(CLIok stop/ok ps),shadow-lock.ts, and the desktop window manager now all reject hostile PIDs ascorrupt/stalebeforeisProcessAliveorprocess.killever sees them. - TOCTOU re-verification.
WindowManager.verifyHolderStillOwnsLockre-reads the lock right before signaling and confirms the holder PID hasn't changed since the collision report. - Self-PID guard. The auto-kill path refuses to send a signal when
existingLock.pid === process.pid, blocking a self-SIGTERM if PID-recycling ever produces a collision metadata that names the desktop itself.
- Parse-time validation.
The
ok uistatic-server/apireverse proxy now enforces the same loopback-only and Host-header-allowlist gates that the upstream server applies before forwarding any request, instead of trusting that requests reaching the proxy are loopback-bound. Without the gate, a non-loopback peer could reach the upstream API by talking to the static server's port directly. The gate refuses with403 {ok:false, error}(andX-Content-Type-Options: nosniffto match the upstream's response posture) when either condition fails, before any proxy handshake.POST /api/seed/planandPOST /api/seed/applynow refuse seed entries whose resolved target path lies outside the project directory, either via lexical..-traversal in the entry's relative path or via any symlink in the target's resolved chain whose realpath escapes the project root. The newassertNoSymlinkEscapewalks each existing ancestor of the target, callsrealpathSync, and rejects paths that resolve outsiderealpath(projectDir)with aSeedRootDirError. Symlink loops surface asELOOPand are also rejected. Without these guards a seed plan could write to/etc/, the user's home directory, or any other writable location reachable through a symlink the project happens to ship.The asset upload endpoint (
POST /api/upload-asset) now refuses to write when any segment of the destination directory is a symbolic link, before performing the recursivemkdirSync(destDir, { recursive: true })that previously could traverse a symlink and place uploaded files outside the project's content root. An attacker who could plant a symlink at<contentDir>/some/dir(viagit cloneof a malicious repo, file-watcher race, or another endpoint that creates uncanonicalized paths) could otherwise have uploads land in/etc/,~/.ssh/, or any other writable location the server process could reach. The handler nowlstats each ancestor segment and refuses withsymlink-escapewhen it finds a symlink, regardless of whether the symlink's realpath would resolve back insidecontentDir.