v0.3.0
Part of the OpenKnowledge changelog.
Minor Changes
feat(agent-writes): optional
summaryon all four MCP write tools — renders as collapsible bullets on the Timeline row so readers can scan agent intent without opening every diff.Agents calling
write_document,edit_document,rename_document, orrollback_to_versioncan now pass an optional one-linesummarydescribing the outcome of the edit (e.g."Fixed token-refresh race"). Summaries persist per-contributor to the shadow-repook-contributors:JSON line and render under the author on the [[timeline]] WIP row — first bullet inline, the rest collapsed behind a "Show N more" expander matching the existingWipGrouppattern. The doc-list stays visible as ground truth alongside the bullets.@inkeep/open-knowledge-core—ShadowContributorgainssummaries?: string[](flat per-contributor array, oldest-first).parseContributorsaccepts both legacy (no field) and new shapes; malformedsummariesvalues drop just that field while preserving the contributor entry — a deliberate divergence from the whole-entry-skip convention so decorative loss (no bullets) never escalates to attribution loss.@inkeep/open-knowledge-server— newagent-write-summary.tsexportsnormalizeSummaryas the single API-boundary truncation point (80-char cap, U+2026 suffix when truncated; whitespace-only and empty strings classify asabsent).recordContributorthreads through the optional 5th-arg summary;formatContributorsFromemitssummarieson theok-contributors:line only when non-empty so summary-less writes stay byte-identical to today. Five API handlers (/api/agent-write,/api/agent-write-md,/api/agent-patch,/api/rename,/api/rollback) accept the optional body field and returnsummary: {value, truncatedFrom?}+ a human-readable hint when truncation fires. Three new metrics counters (agentWriteCalls,summariesProvided,summariesTruncated) track M1 adoption and M2 cap efficacy.handleRenameandhandleRollbacknow callextractAgentIdentity+recordContributor— but only when the request body carries an explicitagentId(D22 LOCKED), so the in-editor Restore button (which posts with no identity) stays anonymous on the timeline as it always has. MCP-driven rename and rollback calls get a server-generated default summary ("Renamed <from> → <to>"/"Restored to <sha-short>") when the agent omits one.@inkeep/open-knowledge— the four write MCP tools exposesummaryin their Zod schemas (Zod hard-cap of 200 chars as a transport-safety bound separate from the 80-char rendering cap);rename_documentandrollback_to_versionalso thread agent identity (agentId/agentName/clientName/colorSeed) matching the pattern fromwrite-document.tsso summary attribution lands correctly. Tool descriptions include the cap, the rename/rollback defaults, and a no-PII/secrets hint.@inkeep/open-knowledge-app—TimelinePanelEntryRowrenders the collapsible bullet list when any contributor on the row hassummaries; zero regression for legacy rows without the field. The doc-list line stays as ground truth alongside the bullets.
The
ok-contributors:JSON line stays atv: 1—summariesis purely additive (precedent #9). Legacy commits (no field) and summary-less writes (field omitted) both remain byte-identical to pre-feature behavior.exec/read_documentenrichment carries the field through automatically viahistory.contributors[*].summaries.Full spec + decision log (D1–D27, US-001–US-007):
specs/2026-04-21-agent-write-summaries/SPEC.md.feat(desktop): Electron desktop M1 — native macOS app with persistent Navigator launcher, per-project editor windows, and attach-to-existing-server.
New private package
@inkeep/open-knowledge-desktoplaunches Open Knowledge as a native macOS Electron app (dev loop only — signing, notarization, DMG, auto-update, URL scheme, keyring, MCP wiring, CLI-on-PATH menu are M2–M7).bun run dev --filter=@inkeep/open-knowledge-desktopopens a Navigator window with three cards (Clone from GitHub, Open folder on disk, Start fresh) + Recent list; every project pick spawns a new editor window per D3/D24 revised (no switch-in-place).Process model: one BrowserWindow ↔ one
utilityProcess.fork↔ onecreateServer↔ onecontentDir(D6), with a second branch that attaches to a live same-hostserver.lockinstead of colliding — so a runningnpx open-knowledge startCLI and the desktop app cooperate on the same project. Typed IPC channel map (D14), hand-rolled preload bridge with contextBridge listener wrappers (D38 + electron/electron#33328),utilityProcess.forkwithwindowLifecycleBound: true(D39), macOS poll-based parent-death detection (D49),shell.openExternalscheme allowlist (D47), sandbox-compatible CommonJS preload.Server + core refactors that landed alongside:
@inkeep/open-knowledge-serverexportsbootServer(opts)— the shared wrapper that composescreateServer()+ HTTP listener + server-lock port-write + optionalok uisibling + idle-shutdown. CLI'sok startis now a thin wrapper over it; Electron's utility process calls it with{ attachUiSibling: false, idleShutdownMs: null }. Also emits permissive CORS headers for/api/*so cross-origin renderer fetches (Electron dev server → utility process) work.@inkeep/open-knowledge-coregainsOK_DIR(moved from CLI) and the canonicalOkDesktopBridgeinterface.@inkeep/open-knowledge-appshipsNavigatorApp.tsx(Electron-only launcher),WorkspaceSwitcher.tsx,CommandPalette.tsx(Cmd+K), anddesktop-fetch.ts— a renderer-side/api/*fetch rewriter that targetswindow.okDesktop.config.apiOriginwhen present.useCollabUrlshort-circuits on the same bridge config in Electron.@inkeep/open-knowledge(CLI) is unchanged externally; internallybootStartServerdelegates tobootServer.
Web and CLI distributions are unaffected —
window.okDesktopis undefined outside Electron, and every desktop-specific surface is gated on it.Full spec + decision log (D1–D52):
specs/2026-04-11-electron-desktop-app/SPEC.md.feat(desktop): M3 — Auto-update (electron-updater + install-on-quit).
Wires
electron-updater@6.8.4(exact-pinned, paired withelectron-builder@^26.9.0via sharedbuilder-util-runtime@9.6.0) into the Electron main process behind theapp.isPackagedgate. Adds.ziptomac.targetinelectron-builder.ymlso Squirrel.Mac's ZIP-based swap path has the artifact it needs (MacUpdater.ts:89downloads.zip, not.dmg).Main-process module at
packages/desktop/src/main/auto-updater.tssubscribes sixautoUpdaterevents (checking-for-update,update-available,update-not-available,download-progressdebug-only,update-downloaded,error) and explicitly skipslogin,update-cancelled,appimage-filename-updated. Classified errors (ERR_UPDATER_*/HTTP_ERROR_*) and bare Squirrel.Mac Errors both log silently and retry on the next launch per parent J7a — no dialog, no nag.Three renderer toasts via the existing sonner mount in
packages/app/src/main.tsx(allduration: Infinity, user-dismissable):- Toast A —
"Update downloaded"+"Relaunch now"action button, fires once per pending-update version. - Toast B —
"Updated to v${VERSION} — see what's new"with a link to the GitHub Releases tag, once per version transition. - Toast C — D12 stuck-update escape hatch: after 7 consecutive calendar days without a successful update check, fires once per installation with a link to the manual-download page.
Four new
AppStatefields persist the toast gates (versionPendingInstall,lastSeenVersion,lastSuccessfulCheckAt,stuckHintShown), backwards-compatible with pre-M3state.jsonvia the existing defensive-coercion pattern inparseAppState.Periodic check every 1 hour (matching Obsidian UX per D10 revised), singleton-per-launch, cleared on
app.on('will-quit'). Relaunch-now button invokesautoUpdater.quitAndInstall(); if the user dismisses the toast instead,autoInstallOnAppQuit = truestill installs at next natural quit.Release pipeline: new
.github/workflows/desktop-release.ymltriggers onrelease: published(fired byrelease.yml'sgh release create) and runselectron-builder --mac --publish alwaysonmacos-14to upload.dmg,.dmg.blockmap,.zip,.zip.blockmap, andlatest-mac.ymlto the existing GitHub Release. Workflow lints + parses but its real-world execution is gated on M2's Apple-creds procurement and the universal-merge SHA-parity fix (M2 FU-1 / FU-2).Dev-mode smoke:
packages/desktop/scripts/smoke-mock-update.mjsspins a local HTTP server with a hand-craftedlatest-mac.yml+ fake.zipso the wiring can be exercised end-to-end (short of the signature-verified Squirrel.Mac swap) before signed DMGs exist.Version bootstrap is governed by the
changesets fixedgroup —@inkeep/open-knowledge-desktop@0.0.0bumps lockstep with its peers at nextrelease.ymlrun; no hand-edit topackage.json.Full spec + decision log (D1–D12):
specs/2026-04-21-m3-electron-updater/SPEC.md.- Toast A —
feat(desktop): M4
openknowledge://URL scheme end-to-end.Closes M4's DOD in the Electron desktop app. Clicking
openknowledge://open?project=<abs-path>&doc=<name>from any surface (Terminalopen, Mail/Slack hyperlinks, MCP tool responses in Claude Desktop) routes the user to the right project window with the renderer navigated to the target doc. Unblocks M6: MCPpreview-url.tsnow emitsopenknowledge://URLs when running inside Electron (gated onOK_ELECTRON_PROTOCOL_HOST=1, set at utility fork), and falls through tohttp://localhost/...for CLI/bunx consumers.Implementation details: synchronous top-level
open-urllistener registration (per electron/electron#32600 —open-urlcan fire beforewill-finish-launchingORreadyon macOS); VS Code-style queue-then-flush with 10 × 500ms retries;second-instanceargv scan for CLI-style launches;realpathSync-canonicalizedwindowsByPathkeys;dom-ready-gatedsendDeepLinkon cold spawns to defeat subscriber-mount races; dev-modesetAsDefaultProtocolClient('openknowledge')withbefore-quitcleanup viaremoveAsDefaultProtocolClient(prevents stale Launch Services bindings to deleted worktrees). Path-traversal defense rejects null bytes (pre-decode + post-decode for layered%2500shapes), URL-decodes, then checks the raw decoded string for..segments before normalization —path.resolve's silent-flatten behavior makes equality-style gates insufficient.shell.openExternalscheme allowlist (https,http,mailto,openknowledge) enforced at the main-process boundary per D47. Nested doc names (notes/meeting-2026) round-trip correctly viaencodeURIComponenton both producer (preview-url) and consumer (renderer hash nav) sides.macOS-only v0 per D51. Windows/Linux NG1/NG2 paths remain
NOT NOW. Cold-start Apple-Event delivery requires signed DMG + Launch Services binding — deferred to M3/M7 and captured as a namedtest.skipindeep-link.e2e.tsfor explicit CI visibility.See
specs/2026-04-21-m4-url-scheme/SPEC.mdand parentspecs/2026-04-11-electron-desktop-app/SPEC.md§14.feat(desktop): M6 CLI-on-PATH install + first-launch MCP consent.
Closes M6's DOD on the Electron desktop app. Two phased deliverables on one branch:
- M6a — Install Command-Line Tools… menu item (macOS only). Click → admin prompt →
/usr/local/bin/ok+/usr/local/bin/open-knowledgesymlinks pointing at the bundled wrapper. Wrapper usesELECTRON_RUN_AS_NODE=1to host the CLI under the bundled Electron binary — no separate Node install required (D17 amendment flipsRunAsNodefuse totrue; VS Code / Atom precedent). Translocation guard refuses install from DMG mount; collision guard prompts before stomping foreign files; G5 launch-time repair offers re-link when symlinks point at a removed bundle. - M6b — first-launch MCP consent dialog (host-agnostic; renders in whichever window opens first per D-M6-R10). User-scoped marker at
~/.open-knowledge/mcp-status.jsonfires the dialog exactly once per user per Mac. Detected editors preselected (Claude Code, Claude Desktop, Cursor, VS Code, Codex, Windsurf —detectInstalledEditorsalready global-scoped). Add → per-editor MCP config writes via NEW CLI exportwriteUserMcpConfigs(opts)(NOTrunInit—runInitrunsensureProjectGit+initContent+scaffoldLaunchJson+upsertRootInstructionswhich wouldgit init /from a packaged Electron app'sprocess.cwd()). HybridcliPathper D-M6-R9:/usr/local/bin/okwhen M6a installed AND ownership-checked, bundle-absoluteResources/cli/bin/ok.shotherwise.
Public-surface additions to
@inkeep/open-knowledge(the published CLI):writeUserMcpConfigs(opts: UserMcpConfigsOptions): Promise<EditorMcpResult[]>— new export. Writes per-editor MCP entries withoutrunInit's project-scoped side effects. Surface for Electron main; CLIok initfrom terminal continues to userunInitunchanged.UserMcpConfigsOptionsinterface —{editors, force?:boolean|Set<EditorId>, cliPath?:string, home?:string}.readExistingMcpEntry(target, cwd, home?)— new export. Format-aware (JSON / TOML) tolerant reader. Never throws — returnsnullon absent / unparseable / shape-incompatible config.writeEditorMcpConfig— promoted from private to exported (was already declared atinit.ts, now available via the public surface).McpInstallOptions.cliPath?: string— new optional field. Highest-precedence branch inbuildManagedServerEntry:{command: cliPath, args: ['mcp']}. Backward-compatible — existingok initcalls withoutcliPathcontinue to produce the canonical{command:'npx', args:['@inkeep/open-knowledge','mcp']}shape.
Notable implementation decisions:
confirmHandlerreturns{ok:false, error: <user-readable>}on partial-write failure — sonner toast surfaces the failure to the user since the dialog itself unmounts on result resolution. Marker stays absent (deferred-marker per OQ-19) so the dialog re-fires next launch for retry.- Renderer-ready handshake (D-M6-R10) registers a one-shot
ok:mcp-wiring:renderer-readyinvoke handler; main responds withok:mcp-wiring:showto the same WebContents and removes the handler ONLY on successful dispatch — so a failed first dispatch keeps the handler armed for the next renderer's mount-ack. - POSIX-safe shell escape on the install-script command builders — apostrophes in bundle paths (renamed
.app, account name with', etc.) cannot inject root commands through theosascript ... do shell script "..." with administrator privilegeschain. - Atomic marker writes via
tmp+rename— mirrorsstate-store.saveAppStateToDirso power-loss between write and fsync can't leave a truncated marker. - macOS-only v0 per D51. Windows/Linux NG4 paths remain
NOT NOW.
See
specs/2026-04-21-m6-cli-and-mcp-wiring/SPEC.mdand parentspecs/2026-04-11-electron-desktop-app/SPEC.md§14.- M6a — Install Command-Line Tools… menu item (macOS only). Click → admin prompt →
feat(init): scope selection for MCP config — user, project, or both
ok initnow supports writing MCP server config at the user level, the project level, or both.- Interactive (TTY): checkbox prompt, both scopes pre-selected
- Non-interactive (piped/CI): defaults to
both --scope <user|project|both>: bypasses the prompt
Project-level paths:
.mcp.json(Claude Code),.cursor/mcp.json(Cursor),.vscode/mcp.json(VS Code),.codex/config.toml(Codex). Windsurf and Claude Desktop are skipped (no project-local config format).feat(presence): unify agent presence on
__system__awareness (multi-agent)N concurrent agents (Claude + Cursor, two Claudes, etc.) now coexist in the presence bar as distinct badges. The previous per-content-doc awareness surface stomped because every Hocuspocus
Documenthas one sharedAwarenessclientID — every agent'ssetLocalStateoverwrote the prior. Presence now lives on the__system__Y.Doc's awareness as a map-valuedagentPresencefield keyed byagentId.Breaking (core):
AwarenessUser.typenarrowed from'human' | 'agent'to'human'. Agents no longer publish per-doc awareness — constructAgentPresenceEntryinstead and call the server-sideAgentPresenceBroadcaster. If you were readinguser.type === 'agent'on per-doc awareness, that path is gone; readagentPresence?on__system__.awarenessinstead.AgentFocusEntrytype export removed. UseAgentPresenceEntryfrom@inkeep/open-knowledge-core.AwarenessState.agentFocus?field removed. Replaced byagentPresence?: Record<string, AgentPresenceEntry>on the same type.
Breaking (server):
AgentFocusBroadcasterrenamed toAgentPresenceBroadcaster. API replaced:setFocus/clearFocus/getFocusMap→setPresence(agentId, entry),clearPresence(agentId),touchMode(agentId, mode),getPresenceMap(). Entry shape now{displayName, icon, color, currentDoc, mode, ts}(was{agentName, currentDoc, writeKind, ts}).ServerInstance.agentFocusBroadcasterrenamed toagentPresenceBroadcaster.ApiExtensionOptions.agentFocusBroadcasterrenamed toagentPresenceBroadcaster.- New endpoint
GET /api/metrics/agent-presencereturns the presence map for operator diagnostics (not polled by the browser).
CLI:
- MCP keepalive URL now carries
&agentId=${connectionId}so the server can deterministically clear presence on process exit. Older MCP clients without the param fall back gracefully to the 5s TTL filter.
Client:
PresenceBarrenders sectioned: current-doc agents + humans | divider | cross-doc agents (dimmed). Cross-doc agents are now keyboard-accessible — the avatar itself is a button with an aria-label describing the target doc; clicking navigates.-space-x-1.5(overlapping avatars) replaced withgap-1.5so 2+ agents render side-by-side cleanly (triage #1 fix).
chore: complete the Node.js 24 floor rollout —
@types/nodealigned to^24.7.0for the CLI.Follows #296 which raised
engines.nodefrom>=22to>=24on the CLI. This change finishes the rollout by bumping the CLI's@types/nodedev type definitions so TypeScript sees the same Node 24 API surface the declaredengines.nodeadvertises. Companion desktop/root engine additions and the user-facingNode.js >= 24prerequisite indocs/content/guides/getting-started.mdxship in the same PR but are private-package changes, so they do not emit their own changeset entries.CI already pins
node-version: "24"across every workflow (ci.yml,release.yml,nightly-e2e-stability.yml,desktop-build.yml,desktop-release.yml,bundle-size.yml). The Bun-first development path (bun install,bun run dev,bun run check) is unchanged —engines.nodeonly gates consumers who run the published CLI under Node directly.feat(cli): register
okas a short bin alias alongsideopen-knowledge. Both bins point to the same CLI entrypoint (./dist/cli.mjs); existingopen-knowledgeinvocations are unchanged. Users installing globally (bun i -g,npm i -g,pnpm add -g) now haveok init/ok start/ok mcpavailable as the short form. One-shot runners (bunx,npx,pnpm dlx) continue to default to the package-name-matchingopen-knowledgebin — usenpx -p @inkeep/open-knowledge ok <cmd>(or the bunx / pnpm equivalents) to select the short bin from an ephemeral install.README install-path matrix expanded to cover all three package managers (bun / npm / pnpm) for both global installs and dlx runners, and the lifecycle-commands table now shows long / short pairs.
Decision rationale and peer precedent (Mastra + Speakeasy neither register a short alias because their product names are already short; open-knowledge at 14 chars justifies the alias) are captured in the companion research report at
reports/mastra-speakeasy-cli-install-recommendations/and codified asspecs/2026-04-20-cli-distribution-and-install-ux/D1.feat(handoff): "Open in Agent Desktop" — one-click handoff from Open Knowledge to Claude Cowork / Claude Code / OpenAI Codex Desktop / Cursor.
A new "Open in…" dropdown surfaces from three places — the editor header action strip, the
Cmd+Kcommand palette ("Open in agent" group), and the file-tree right-click menu — routing every click through a singledispatchHandoffentry point (AC9 asserts no other dispatch sites). Each enabled row fires the target's canonical URL scheme through the existingshell.openExternalIPC (Electron host) or an anchor-click (web host), with a minimal auto-composed prompt that points the target agent at the doc plus a hint to use theopen-knowledgeMCP for backlinks + related context. Disabled rows render with a keyboard-reachable submenu — install link +Open in claude.ai →secondary affordance on Claude rows — instead of a non-interactive tooltip.Built on four pure URL builders in the new
packages/core/src/handoff/(claude-url.ts,codex-url.ts,cursor-url.ts,web-fallback-url.ts) with an encoding discipline pinned against Cursor's two-pass-decode behavior (text=double-encoded,workspace=single-encoded basename,mode=agentliteral). The Cursor two-step dispatcher (cursor-two-step.ts, Electron only per E4 DIRECTED) spawns the workspace first through a dedicatedok:shell:spawn-cursorIPC — distinct from the URL-scheme allowlist because the threat model is a command allowlist — then fires thecursor://prompt after a 1000–1500 ms settle. On macOS the spawn routes through/usr/bin/open -a <bundle>becauseapp.getApplicationInfoForProtocol('cursor://')returns the.appbundle (a directory), not an executable.Install detection is unified across hosts: Electron uses
app.getApplicationInfoForProtocol(scheme)per probe (with anxdg-mime query default x-scheme-handler/<name>fallback on Linux); web uses a newGET /api/installed-agentsendpoint with a per-scheme 60 s server-side cache, a 10 s per-client refresh throttle, and the standardcheckLocalOpSecurityloopback + Host-header gate. Windows probes the mergedHKCRview so machine-scope (HKLM) installers are detected alongside user-scope. Web-host Cursor is always disabled-with-tooltip regardless of probe result (E4 DIRECTED — local-use-case only; the/api/handoff/open-foldercross-machine primitive is deferred).Security:
packages/desktop/src/main/shell-allowlist.ts(D47) extended withclaude:,codex:,cursor:behind per-scheme JSDoc and an exact-set test. A drift-detector inshell-allowlist.test.tsreadsKNOWN_TARGETSand fails if any future target lands without an allowlist row. Every outbound URL is built by a typed pure function — never from user-supplied raw URL strings.Observability:
~/.open-knowledge/stats.jsonlappend-only per dispatch (zero phone-home per XQ3 LOCKED). Success/failure sonner toasts close the DC3/DC4/vendor-drift silent-failure gap, with a bounded retry (2–3 attempts; distinct copy on the final failure) per review M5. Full spec with decision log + test plan atspecs/2026-04-21-open-in-agent-desktop/SPEC.md; end-user guide atdocs/content/guides/open-in-agent-desktop.mdx.feat(presence): use git-config name for the human presence avatar; dedupe tabs of the same checkout
The presence bar now shows the user's actual name (from
git config user.name) and a deterministic per-principal color, instead of a randomAdjective Animalnickname. Multi-tab users see ONE avatar with a tooltip like"Ada Lovelace-King · 2 tabs"instead of N copies. Users on a fresh box without git config keep the existing animal-fallback experience — no regression.Cursor labels and tooltips polish Unix-style names:
ada-kt-lovelacefloatsAda Kt Lovelacenext to selections, matching theAKinitials the avatar already shows.The data plumbing reuses an existing fetch —
DocumentContextalready callsGET /api/principalfor the auth-token claim — and threads the resolved principal into a new optionalprincipalId?: stringfield onAwarenessUser.usePresence()dedupes humans whoseprincipalIdmatches; cursors stay per-clientId so N tabs editing still render N cursors in the editor.API surface:
- New optional wire field
AwarenessUser.principalIdon per-doc awareness (loopback-only trust today; non-loopback connections must switch to server-authoritative attribution atonAuthenticate). - New public exports from
@inkeep/open-knowledge-core:Principal(now an alias of the schema-inferredPrincipalResponse),PrincipalResponseSchema,PrincipalResponse,computeInitials,formatPresenceLabel,HUMAN_COLORS. colorFromSeednow accepts an optionalpaletteparameter; the default remainsAGENT_COLORSso existing single-arg callers are byte-equivalent.HumanParticipantfrom@inkeep/open-knowledge-app(internal) gainstabCount: number.localStoragecache keys for the random-fallback identity move fromok-user-{name,color}-v2to-v3. No migration — pre-launch state.
Hardening:
GET /api/principalnow requires loopback + Host-header gates so PII (display_name,display_email) doesn't leak under--host 0.0.0.0deployments. Matches the gate/api/metrics/agent-presenceand/api/workspacealready enforce.PrincipalResponseSchema.display_nameanddisplay_emailuse.min(1)so an empty git-config value routes through the silent random-identity fallback rather than rendering an empty initial / blank tooltip / blank cursor label.
- New optional wire field
feat(shadow-repo): collapse dual-mode to single-mode at
<projectRoot>/.git/open-knowledge/, auto-git initon first run when no parent repo exists, and rename legacy.git/openknowledge/shadows in place.The shadow repo (OK's attribution journal for WIP refs, upstream imports, checkpoints, and the rescue timeline) previously branched between
integratedmode at<root>/.git/openknowledge/andstandalonemode at<root>/.openknowledge/. Standalone mode had semantically distinct behavior — no parent.git/HEADfor the HEAD watcher, no real project branch for therefs/wip/<branch>/<writer-id>namespace, no upstream-import path — which forced every shadow-touching change through a two-mode test matrix for zero user-facing payoff. The dual-mode split is now gone: the shadow always lives at<projectRoot>/.git/open-knowledge/, projects without.git/get auto-git init'd by the newensureProjectGithelper (fail-fast on missing git — no degraded fallback), and legacy.git/openknowledge/shadows are silentlyrenameSync-migrated on first run so pre-spec users keep their attribution history.@inkeep/open-knowledge-core—resolveShadowDir(projectRoot: string): string— return type collapses from{ path, mode }to a plain string;ShadowRepoModeandResolvedShadowDirtypes are deleted.OkDesktopBridgegainsonGitInitNotice(cb)alongside the existingonProjectSwitched/onMenuActionpush-event surfaces.@inkeep/open-knowledge-server— newensureProjectGit+ProjectGitInitErrorexports (pre-listen fail-fast hook).BootServerOptionsgainsensureProjectGitFn;BootedServergainsdidGitInit.initShadowRepocarries a ~5-line R9 rename shim for legacy layouts.skipAutoInitnow gates bothensureProjectGitFnandautoInitFn.@inkeep/open-knowledge—ok startandok initcallensureProjectGit(cwd)in the fresh-directory path; the CLI preview-block gate extends todidAutoInit || didGitInitand emitsInitialized git repo at <cwd>/.git/ (default branch: main).ok mcpis unchanged directly but inherits the side effect transitively when it auto-spawnsok start(opt out withOK_MCP_AUTOSTART=0or configmcp.autoStart: false)..gitignoreauto-append of.openknowledge/is deleted;.openknowledgeis removed fromenrichment.ts/mtime-scan.tsscan-exclusion sets.@inkeep/open-knowledge-desktop— utility process passesensureProjectGitFntobootServer;UtilityReadyMessagecarriesdidGitInit. Newgit-init-noticepush event on the preload bridge; main-side dispatch deferred untilwebContents.once('dom-ready', ...)to defeat the subscriber-mount race.@inkeep/open-knowledge-app— renderer subscriber (lib/install-git-init-toast.ts, wired imperatively inmain.tsx) routesonGitInitNoticetotoast.info(\Initialized git repo at ${gitDir}`)`. No-op outside Electron.
Legacy
.openknowledge/standalone-mode directories are silent orphans (no detection, no warning, no migration) — OK carries zero runtime reference to that path per D5/NG5. Worktree-specific semantics are out of scope for this change; they remain owned by a separate spec (NG6).Full spec + decision log (D1–D14, R1–R9):
specs/2026-04-21-shadow-repo-single-mode/SPEC.md.feat(skill, ingest): closed-loop grounding, broadened
ingesttrigger, log-discipline rule, and project-shape-neutral terminology.Three behavioral additions to the bundled
open-knowledgeAgent Skill and theingestMCP tool, driven by a wiki author's diagnostic of two recurring lapses (citing web sources inline instead of ingesting them; not appending to the project log after KB-changing turns).- Closed-loop grounding. External sources don't get cited out to the live web — they get pulled in via
ingest, then cited locally. A bare[source](https://...)URL inside a knowledge-base doc is now explicitly a TODO, not a finished citation. Self-fetched URLs (WebFetch/WebSearchfrom the agent itself) triggeringestexactly like a user share does. - Broadened
ingesttrigger. Both the SKILL.md workflow-tools row and the MCP tool's discoverableDESCRIPTIONnow name agent-initiated fetches as a first-class trigger. Prior framing was user-share-only, which let agents downgrade to inline-URL citation when they did the fetch themselves. - Log-discipline rule. New SKILL.md section: after any turn that creates / edits / restructures KB content, check for a project
log.md(project root or seedrootDir) and follow whatever its frontmatterdescription:and in-file comment say. The skill carries the trigger; the seeded file owns the policy (cadence, entry shape, categories) — so projects that don't runok seedcan opt out by simply not having alog.md. The seededLOG_MD_TEMPLATE(packages/server/src/seed/starter.ts) now spells the contract out in its frontmatter description so it surfaces in everyexec("ls")enrichment, and the example entry shape uses real markdown links ([path](./path.md)) instead of bare path strings — so log entries register inget_backlinksfor the docs they reference and the audit trail compounds inside the doc graph. - Project-shape-neutral terminology. Open Knowledge knowledge bases serve multiple shapes — wiki, LLM brain, spec collection, research log, project notes. Replaced "wiki" with "knowledge base" / "KB doc" everywhere it had been used as a project-shape claim (skill grounding section, workflow-tools layer column, hub-candidates JSDoc). Kept the term where it's a legitimate technical reference (the
[[Page]]"wiki-link" syntax,ARCHITECTURE.mdcompetitive-landscape rows naming Notion/Confluence/Wiki.js).
The change is additive on the install side: the skill
metadata.versionand@inkeep/open-knowledge-serverpackage version both control the install gate (~/.open-knowledge/skill-installed-versionsidecar), so this version bump triggers a fresh skill install in environments where 0.2.0 was previously cached.- Closed-loop grounding. External sources don't get cited out to the live web — they get pulled in via
feat(cli): require explicit project routing for MCP tool calls
MCP tool calls now route by explicit
cwdfirst, otherwise by the client's only advertised root, and fail clearly instead of guessing the startup project.
Patch Changes
feat(desktop): M5 —
@napi-rs/keyringend-to-end verification in packaged build.Adds the verification layer that proves
@napi-rs/keyringloads and round-trips insideutilityProcess.fork()in the packaged Electron app. The PR #166 auth substrate itself is unchanged — this milestone ships infrastructure for observing the substrate from outside the app (driver script) and from inside the renderer (gated debug IPC), so R15 (utilityProcess compat) and R16 (CFBundleDisplayNameprompt + bundle-ID stability + upsert semantics) become empirically verifiable.New surfaces:
packages/desktop/src/utility/keyring-smoke.ts—runKeyringSmoke(deps?)primitive. Namespace-scoped round-trip (open-knowledge-smoke/test-user) via@napi-rs/keyring; cleans up on success. Injectabledepsparameter allows AC3 YAML-fallback unit coverage without touching the production substrate (SPEC §9 SCOPE lock). ReturnsKeyringSmokeResult = { ok, backend, durationMs, timestamp, error? }.packages/desktop/src/main/debug-ipc.ts— renderer↔main↔utility relay. Correlation-IDMap<id, {resolve,reject,timer}>with 5 s default timeout;clearTimeoutfires on both resolve and timeout paths so the Map stays bounded.packages/desktop/src/utility/server-entry.ts— extends the IPC protocol with{ kind: 'debug-request' }dispatch. Also adds a boot-time auto-smoke mode gated onOK_DEBUG_KEYRING_SMOKE=1: writesKeyringSmokeResultJSON toOK_DEBUG_KEYRING_SMOKE_OUT, exits0post-write whenOK_DEBUG_KEYRING_SMOKE_EXIT=1. This is the only creds-free path that exercises the hardened-runtime + fuses + signed-binary loader on packaged builds.packages/desktop/src/shared/{ipc-channels.ts,bridge-contract.ts}+packages/core/src/desktop-bridge.ts+packages/app/src/lib/desktop-bridge-types.ts— add theok:debug:keyring-smokechannel and the optionaldebug?: { keyringSmoke(): Promise<KeyringSmokeResult> }bridge namespace. The namespace is gated at preload time:!app.isPackaged || process.env.OK_DEBUG_KEYRING_SMOKE === '1'. In normal packaged runs,window.okDesktop.debugisundefinedand typos surface at TypeScript compile time, not runtime.scripts/verify-keyring-in-packaged-dmg.mjs— driver for creds-free pre-flight. Accepts an.appor.dmg, launches the packaged app with theOK_DEBUG_KEYRING_SMOKE*env triplet, parses the result JSON, exits0on ok /1on smoke failure /2on 30 s boot timeout /3on pre-smoke crash.packages/cli/src/auth/token-store.test.ts— extended with upsert-semantics characterization tests + YAML-fallback mocking strategy. The productiontoken-store.tssubstrate is unchanged per SPEC §9 SCOPE lock; the new tests document and guard the substrate's already-correct behavior.packages/desktop/tests/smoke/keyring-e2e.md— 11-step creds-gated manual runbook covering AC4 (CFBundleDisplayName prompt), AC5 (relaunch persistence), AC6 (v0.1.0→v0.1.1 upgrade persistence), AC7 (log showcaller-attribution). Executable once Apple Developer credentials are on the test machine.
Web and CLI distributions are unaffected — the debug namespace and env-var auto-smoke only fire in the Electron utility process, and the token-store test changes don't touch runtime behavior.
Creds-free ACs (AC1–AC3, AC8–AC10) land green in this changeset. Creds-gated ACs (AC4–AC7) execute manually via the runbook and will attach screenshots +
log showoutput to a follow-up status update once Apple credentials are available on the test machine (same external dependency that gates M2's end-state DOD).Full spec:
specs/2026-04-21-m5-keyring-packaged-e2e/SPEC.md; design decisions (D-M5-1 through D-M5-8):specs/2026-04-21-m5-keyring-packaged-e2e/meta/investigation-findings.md; parent milestone plan:specs/2026-04-11-electron-desktop-app/SPEC.md§14.fix(cli):
ok startbanner URL now tracks the portok uiactually bound.Post-D-033 the auto-spawned
ok uidefaults to port 0 (kernel-allocated), but the banner had hardcodedhttp://localhost:3000on the spawn branch. Users runningbun run packages/cli/dist/cli.mjs startsaw the banner URL, got connection-refused, and no documents loaded.bootStartServernow pollsui.lockafter spawn and exposesresolvedUiPortonBootedStartServer; the banner uses that, falling back to the API URL on timeout.bun run devis unaffected (Vite serves everything same-origin on one port).chore(licensing): ship
THIRD_PARTY_NOTICES.mdin npm tarball + Electron.appAdds a reproducible attribution pipeline for the published
@inkeep/open-knowledgeCLI tarball and the@inkeep/open-knowledge-desktopElectron app. Both bundle source from MIT/ISC/BSD/Apache-2.0 deps and OFL-1.1 fonts; the newTHIRD_PARTY_NOTICES.mdat repo root is the committed source-of-truth and ships under each artifact:- npm CLI tarball — copied to
packages/cli/dist/THIRD_PARTY_NOTICES.mdvia the existingbuild:assetsstep (already covered byfiles: ["dist", …]). - Electron desktop —
electron-builder.ymlextraResourcesplaces it atOpen Knowledge.app/Contents/Resources/THIRD_PARTY_NOTICES.md(alongside electron-builder's auto-generatedLICENSE+LICENSES.chromium.html).
The closure walker (
scripts/generate-third-party-notices.mjs) is deterministic (byte-stable sort, no timestamps) and the committed file is drift-checked against the resolved dep tree bybun run check,bun run check:full:parallel, and thelintjob in.github/workflows/ci.yml.- npm CLI tarball — copied to
Handle final chunks from github stream buffer and bypass simple git auth check.