Adds a draft design document outlining the challenges, security considerations, and possible implementation approaches for inserting arbitrary HTML, CSS, and JavaScript into Docmost pages. Includes analysis of ProseMirror schema limitations, node creation steps, and isolation model options.
Display "Indexed N of M pages" on the AI provider settings page so admins
can see how much of the wiki is covered by vector-RAG semantic search.
- page-embedding.repo: add countIndexedPages() — distinct non-deleted pages
that have stored embeddings in the workspace
- page.repo: add countByWorkspace() — total non-deleted pages
- ai-settings.service: compute both counts in getMasked() (Promise.all) and
return them with the masked settings; inject PageEmbeddingRepo + PageRepo
- MaskedAiSettings / IAiSettings: add indexedPages + totalPages
- ai-provider-settings: render a dimmed coverage line under "Embedding model"
- i18n: add the "Indexed {{indexed}} of {{total}} pages" key (en-US, ru-RU)
The built-in AI agent chat over wiki content is fully implemented
(server module, ~40 tools, RAG search, provider settings, external MCP),
so showcase it in both READMEs:
- list AI agent chat among the from-scratch community replacements
- add a "What's different" table row and a dedicated feature section
- move AI chat from the "In progress" roadmap bucket to "Done"
- add it to the Features list
- add docs/screenshots/ai-chat.png and show it in the Screenshots section
Updated in sync in README.md and README.ru.md.
The default system prompt introduced the assistant as embedded in
"Docmost". Update DEFAULT_PROMPT to say "Gitmost" so the agent
presents itself with the correct product name.
Surfacing the stream error via useChat().error alone was not enough: on a
brand-new chat the errored turn still fires onFinish -> onTurnFinished, which
adopts the freshly-created chat id and changes the <ChatThread> key, remounting
it with a fresh useChat whose transient `error` is gone. The thread re-seeds
from persisted history, where the assistant row has empty parts and the error
lives only in metadata.error — which was never rendered. Result: an empty
"AI agent" row and no visible error.
- Render the persisted metadata.error inline in MessageItem, so the error
survives the remount and is also shown in reopened chat history.
- Carry metadata.error onto the rebuilt UIMessage in rowToUiMessage.
- Extract the error formatter into utils/error-message.ts (describeChatError)
and reuse it for both the live Alert and the persisted error.
- Add metadata.error to the IAiChatMessageRow type.
Client-only; the server already persists metadata.error. No new i18n keys.
The AI chat UI previously collapsed every non-403/503 failure into a
generic "could not respond" message, hiding real provider errors such as
OpenRouter HTTP 402 "requires more credits". The backend already forwards
the real "<status>: <message>" via pipeUIMessageStreamToResponse onError,
so the fix is client-side.
- describeError now returns the provider message verbatim for any error
that is not one of our own gating responses, so 402 (credits), 429
(rate limit) and similar causes are visible to the user.
- Match gating responses by the NestJS JSON "statusCode" field instead of
loose substring/word checks, so a provider message that merely contains
"403"/"503"/"disabled" is no longer misclassified and hidden.
- Add a providerDetail() helper that filters empty text and the opaque
"An error occurred." / "Internal server error" placeholders, falling
back to the generic message only then.
No backend changes; no new i18n keys.
BullMQ rejects custom job IDs containing ':' (Redis key separator),
throwing "Custom Id cannot contain :" inside the onStoreDocument hook
for every agent edit. This broke agent-driven page saves (MCP
create_page runs as actor='agent') with HTTP 400.
Switch the agent dedup suffix from `${page.id}:agent` to
`${page.id}-agent`. The jobId is only used as a BullMQ dedup key and is
never parsed by the history processor; page.id is a UUID, so the
hyphenated id cannot collide with a human job whose id is a bare page.id.
Replace the docked right-aside AI chat with a floating, draggable,
resizable, minimizable window per the GitmostAgent design. The "AI chat"
entry points (page header menu, page-history item) now open the window
instead of the aside tab.
- Add ai-chat-window.tsx + ai-chat-window.module.css: fixed-position
window with viewport-clamped drag, CSS resize, minimize (hides body
via CSS so ChatThread/useChat stays mounted and streaming is not
aborted), and geometry kept in state (survives close/reopen, re-clamped
on open via useLayoutEffect, size tracked with a ResizeObserver).
- Reuse ChatThread, ConversationList and the transcript components
unchanged; move all orchestration (active chat, adopt-new-chat,
openPage, queries) into the window.
- Header shows a tokens-only badge: sum of persisted metadata.usage for
the active chat (no cost/context-total data available), hidden at 0.
- Add aiChatWindowOpenAtom; mount the window once in global-app-shell.
- Remove the aside "ai-chat" tab handling and delete ai-chat-panel.tsx.
- Type IAiChatMessageRow.metadata.usage; add "Minimize" and
"Tokens used in this chat" i18n keys.
Locators (edit_page_text `find`, insert_node `anchorText`) are matched
against the document's plain text, so a model-supplied locator carrying
markdown wrappers (**bold**, *italic*, `code`, [t](url)) or trailing emoji
never matched and the edit/insert failed. Add stripInlineMarkdown() and a
fallback: try the locator verbatim first (exact match wins, so literal
asterisks/underscores still work), and only on zero matches retry with a
markdown-stripped form. The ambiguity guard runs on the post-fallback count,
and `replace` / inserted node content are never stripped, so no formatting is
lost. Failed edits gain an atom-aware reason plus a bounded "closest block
text" hint; the insert_node "anchor not found" error now points at plain-text
anchors / anchorNodeId.
New packages/mcp/src/lib/text-normalize.ts (+ unit tests); wired into
json-edit.ts and node-ops.ts; tool descriptions updated. Tests: 212 pass.
edit_page_text (applyTextEdits) now matches at the inline-block level instead of
per text node, so a find/replace may cross bold/italic/link boundaries; the
replacement inherits marks from the unchanged common prefix/suffix via a diff
splice. Atom (non-text inline) slots can never be part of a match, making the
U+FFFC placeholder collision-safe, and inserted text never inherits an atom's
marks.
The edit batch is no longer all-or-nothing: applyTextEdits returns
{ doc, results, failed } and applies what it can; editPageText writes only on a
real change (no spurious history version for a no-op) and throws an aggregated,
actionable error only when nothing applied.
The AI-chat insert_node / patch_node / update_page_json tools now JSON.parse a
node/content argument that arrives as a string, matching the standalone MCP
server (this is what made insert_node fail under OpenAI tool calls).
Tool descriptions gain concrete ProseMirror examples and reflect the new
edit_page_text behavior. Adds/updates json-edit unit tests (183 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Page-history snapshots are debounced/coalesced (one per 1–5 min window,
jobId=page.id). A human edit followed by an agent edit in the same window
collapsed into a single snapshot, losing both the pre-agent human state and
a deterministic record of the agent's result.
Two provenance-aware boundaries now bracket an agent intervention:
- Before: on a user->agent transition, onStoreDocument synchronously pins the
current (pre-agent) human content as its own history version tagged 'user',
inside the page-write transaction, before the agent overwrites it.
- After: agent stores enqueue an immediate (delay 0), source-keyed history job
(jobId=`${pageId}:agent`) so the agent's result snapshots deterministically
as 'agent' and a later human edit (jobId=page.id) cannot coalesce/retag it.
Also add an `id desc` tie-break to findPageLastHistory so "last history" stays
deterministic when two snapshots share a created_at, consistent with
findPageHistoryByPageId.
Known trade-offs (Variant 1): the delay-0 worker re-reads the row, leaving a
millisecond mis-tag window; multiple agent edits in one turn may yield multiple
versions. The reverse agent->human boundary is intentionally out of scope.
Editing an existing comment's text is irreversible (not version-tracked),
which breaks the agent's "only reversible operations" invariant. Remove the
updateComment tool that was added in the toolset-expansion change, leaving the
agent at 40 tools (comments: create/resolve only).
- Remove the updateComment tool from forUser().
- Remove updateComment from the DocmostClientLike interface.
- Reword SAFETY_FRAMEWORK: comments are create/resolve only; drop the
comment-text-edit exception (keep the public-sharing one); keep the
no-permanent-deletion guarantee and anti-prompt-injection rules.
- Tests: assert updateComment is NOT exposed (mirrors the deleteComment guard).
- docs(ai-agent-chat-plan): move updateComment to the "not exposed" list.
external MCP servers, per-server enable toggle and Test (lists the server's
tools), write-only auth headers (never shown), tool allowlist, and a Tavily
preset (key in the Authorization header, not the URL). Consumes the existing
admin /workspace/ai-mcp-servers endpoints. Fixes a discriminated-union narrowing
type error in the (previously untracked) server form.
Grow the agent tool registry in forUser() from 10 to 41 tools, wiring all
remaining @docmost/mcp client capabilities: reads (workspace/spaces/pages/
sidebar/outline/json/node/table/comments/shares/history/diff/export) and
reversible writes (editPageText, patch/insert/delete node, updatePageJson,
table ops, copy/import content, share/unshare, restorePageVersion,
updateComment, transformPage).
Deliberately NOT exposed: deleteComment (irreversible hard delete) and the
filePath-based image tools (uploadImage/insertImage/replaceImage — useless
and unsafe for a server-side agent). transformPage omits the deleteComments
option from its schema and never passes it, so the comment-deletion path is
unreachable from the agent.
- Extend DocmostClientLike with the new method signatures.
- Update SAFETY_FRAMEWORK to describe the broader toolset while keeping the
no-permanent-deletion guarantee and anti-prompt-injection rules; flag that
comment-text edits are not version-tracked and sharing is public.
- Add guardrail tests: no deleteComment tool; transformPage schema rejects
deleteComments.
- docs(ai-agent-chat-plan): record the toolset expansion and a backlog item
to support image insertion by URL via the existing SSRF guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce the default font size of chat messages to create a smaller, denser view. This adjustment cascades into user bubbles, assistant markdown, and tool cards while preserving the size of explicit Mantine labels. Improves readability and allows more content to fit within the chat window.
so the v6 hook stops re-creating its store every render on a new chat
(which wiped the optimistic user message + streamed deltas, so nothing
showed until the turn finished). Also send X-Accel-Buffering:no + flushHeaders.
- context: client sends the currently-open page {id,title}; the system prompt
tells the agent which page 'this page' refers to (it reads it via its
CASL-scoped getPage tool; id is prompt-context only, no server-side fetch).
- embeddings: make page_embeddings.embedding dimension-agnostic (drop the
HNSW index + ALTER to vector), remove the hard 1536 guard, filter search by
model_dimensions — so 3072-dim (and any) models index instead of being
skipped. Seq-scan <=> search (wiki scale); existing pages reindex on next edit.
- openai provider: use .chat() (Chat Completions) instead of the default callable
(Responses API), which gateways reject on multi-turn -> 400.
- updateAiProviderSettings: assemble settings.ai.provider via jsonb_build_object
with ::text-cast bound params + jsonb_typeof self-heal (postgres.js was
double-encoding it into an array; the ::text cast avoids 'could not determine
data type of parameter').
- chat agent: drop the hard maxOutputTokens cap (truncated complex tool calls);
keep a tiny cap only on the test-connection ping.
- testConnection + chat stream: surface the real provider error (statusCode+message)
to logs and the UI instead of generic masks; never log the API key.
- chat UI: typing indicator, incremental streaming render, tool 'running' status, Stop.
Also bundled (prior uncommitted ai-chat work):
- history 'AI agent' provenance badge; vector RAG (pgvector image + page_embeddings
+ AI_QUEUE indexer + space-scoped semanticSearch); external MCP servers backend
(@ai-sdk/mcp client, SSRF IP-pinning, encrypted headers, admin CRUD/Test);
yjs duplicate-instance fix via pnpm patch (single CJS instance server-side).
- Add reversible write tools to the per-user agent toolset (page create/update/
move/soft-delete; comment reply + resolve), exposed under the user's JWT and
enforced by Docmost CASL; no permanent/force delete (D3).
- Non-spoofable agent provenance: sign actor/aiChatId into the access and collab
tokens (TokenService), propagate via jwt.strategy onto the request, and set
pages.last_updated_source/last_updated_ai_chat_id on REST create/update/move and
comments.created_source/resolved_source/ai_chat_id.
- packages/mcp: add an optional getCollabToken provider (content-edit provenance)
and guard against empty tokens; service-account /mcp path unchanged.
Frontend:
- Admin 'AI / Models' settings section: provider/model/embedding/base URL, a
write-only API key field, system prompt, and Test connection.
- AI chat panel (useChat + DefaultChatTransport): conversation list, streamed
messages, tool-call action log and page citations; header entry point gated on
settings.ai.chat.
Compile-verified (server nest build + client tsc/vite); not yet live-tested.
Known gaps: history 'AI agent' badge (C3), vector RAG (D), external MCP (E);
chat tool-card citation links pending a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WIP checkpoint of the gitmost AI-chat backend (plan stages A + B1 + B3a).
The agent acts under the requesting user's JWT (Docmost CASL enforces page
access); the external service-account /mcp endpoint is untouched.
LLM provider config (A2-A4):
- integrations/crypto: AES-256-GCM SecretBoxService (key derived from APP_SECRET,
per-record salt/iv; clear error on rotation instead of crashing).
- ai_provider_credentials table/repo/types: encrypted API key stored outside
workspace settings/baseFields, write-only (never returned by any endpoint).
- integrations/ai: per-workspace AI SDK v6 provider driver (openai/gemini/ollama),
admin-gated GET(masked)/PATCH(write-only key)/Test endpoints; settings.ai.provider
holds non-secret config incl. systemPrompt. Removed unused AI_* env getters (DB is
the single source of truth).
Chat module (A1, A5-A8):
- ai_chats/ai_chat_messages repos (workspace-scoped, soft-delete, tsv never selected).
- core/ai-chat: CRUD + POST /ai-chat/stream (Fastify hijack + AI SDK v6
pipeUIMessageStreamToResponse, abort on disconnect, persist user/assistant msgs).
- Agent loop: streamText + stepCountIs(8); read tools searchPages/getPage via a
per-request DocmostClient over loopback REST under the user's minted access token.
- Gate settings.ai.chat (+ 503 when provider unconfigured); buildSystemPrompt with a
non-removable safety/anti-prompt-injection framework. Per-user rate limit.
Per-user auth (B1):
- @docmost/mcp DocmostClient gains an additive getToken variant (carry a user JWT,
re-fetch on 401) and exports DocmostClient; the email/password service-account path
(external /mcp, stdio) is unchanged.
Agent-edit provenance backbone (B3a):
- Migration: pages/page_history (last_updated_source, last_updated_ai_chat_id) and
comments (created_source, ai_chat_id, resolved_source).
- Signed actor/aiChatId claim in the collab token; onAuthenticate propagates it,
onStoreDocument writes it with a sticky agent marker, saveHistory copies it.
Migrations auto-run on boot (additive). Write tools, frontend, RAG and external MCP
servers are not in this checkpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite README around the community fork story:
- state Gitmost is an AGPL-only fork of Docmost with NO Enterprise-Edition
code (ee dirs removed) and replacements written from scratch
- add a "What's different from Docmost" table (EE removal, comment
resolution, embedded MCP, rebranding, compact tree, GHCR CI)
- reframe the MCP section: it is our own docmost-mcp built in, better than
Docmost's coarse EE MCP (agent-native tools), and the same server as the
standalone one — just bundled (Docmost + docmost-mcp in one package)
- group the roadmap into Done / In progress / Planned; done: MCP, macOS
app; in progress: AI chat, git sync; planned: templates, viewer
comments, password-protected pages, Windows/Linux app, mobile app,
offline/PWA, and a batch of editor & UX improvements
- update the License section to reflect the absence of EE directories
- add README.ru.md (full Russian translation) and an English/Russian
language switcher to both files
Add `ignore-error=true` to the `cache-to` settings in both develop and release GitHub Actions workflows to prevent build failures when cache upload encounters errors.
Introduce a GitHub Actions workflow for the `main` branch that builds and pushes a Docker image to GitHub Container Registry. Includes concurrency control, permissions, Docker Buildx setup, and caching configuration.
Added sections 14 and 15 to the AI‑agent chat plan documenting review
findings, identified blockers (C1‑C3) and their resolutions, high/medium
issues, and verification steps. This provides clear guidance before
starting implementation.
Add documentation for external MCP server support, covering architecture,
configuration, security (SSRF protection, secret handling), system prompt
management, UI updates, and the new @ai-sdk/mcp dependency. This clarifies the
expanded three‑axis authorization model and migration steps.
Replace the removed enterprise EE MCP (private apps/server/src/ee submodule,
license-gated /mcp route) with our docmost-mcp, vendored as an isolated ESM
workspace package and served by the server over HTTP — no enterprise license.
Backend:
- Add packages/mcp (@docmost/mcp): vendored docmost-mcp refactored into a
side-effect-free createDocmostMcpServer() factory (38 tools preserved),
stdio entry kept in stdio.ts, Streamable-HTTP session manager in http.ts.
- Add apps/server McpModule: @Post/@Get/@Delete('mcp') (served at /mcp via the
existing global-prefix exclude), @SkipTransform + reply.hijack to bridge raw
Fastify req/res into the SDK transport. The module dynamically imports the
ESM-only package from CommonJS via a Function-indirected import resolved with
require.resolve + file:// URL. Gated by the workspace ai.mcp toggle, a
service-account (MCP_DOCMOST_EMAIL/PASSWORD/API_URL) and optional MCP_TOKEN;
per-session idle eviction (MCP_SESSION_IDLE_MS).
- Drop the enterprise license check on mcpEnabled in workspace.service.
- Dockerfile: copy packages/mcp into the production image.
- .env.example: document MCP_DOCMOST_*, MCP_TOKEN, MCP_SESSION_IDLE_MS.
Frontend:
- Recreate the community "AI & MCP" workspace-settings panel (mcp-settings.tsx):
admin-only toggle on settings.ai.mcp with optimistic update, copyable
${APP_URL}/mcp URL; wired into workspace-settings page. Reuses existing i18n.
Fixes:
- Pin packages/mcp tiptap deps to 3.20.4 (matching the client) and inline
getStyleProperty, preventing a duplicate @tiptap/core@3.26.1 from leaking into
the client editor via pnpm shamefully-hoist (was breaking apps/client tsc).
Add comment resolve/re-open as a community feature, written from scratch on top
of the infrastructure already present in the community codebase: the
resolved_at/resolved_by_id columns, the COMMENT_RESOLVED notification job, the
resolveCommentMark collaboration handler, the commentResolved websocket event,
the comment service/types and the Open/Resolved tabs. No Enterprise-Edition code
is reused and there is no EE feature gating — resolving is available to anyone
who can comment.
Backend:
- add POST /comments/resolve (ResolveCommentDto) guarded by validateCanComment;
reject resolving replies
- add CommentService.resolveComment: set/clear resolvedAt/resolvedById, sync the
inline comment mark via collaboration handleYjsEvent, queue
COMMENT_RESOLVED_NOTIFICATION (only when another user resolves), emit the
commentResolved websocket event and write a resolve/reopen audit log
Frontend:
- add useResolveCommentMutation with optimistic update + rollback
- add ResolveComment toggle button
- wire the resolve button and menu item into comment-list-item / comment-menu,
gated on canComment for parent comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add docs/offline-sync-plan.md — a ready-to-implement design document for
offline editing and synchronization in gitmost.
- Describes current state: Yjs/Hocuspocus + y-indexeddb for document body
(CRDT, offline-ready) vs REST-based structural data (online-only).
- Clarifies that PWA installability already exists (inherited from Docmost);
the missing piece is a service worker for offline app-shell.
- Defines two sync contours (CRDT body / outbox+LWW for REST) and a staged
plan M0..M4 with per-step files, acceptance criteria and risks.
- Includes conflict-resolution rules, pitfalls, npm deps, open questions
and an implementation checklist.
Port the compact page-tree indentation tweak from the docmost-app
WebKit wrapper (previously applied via injected CSS) into the source.
- doc-tree.tsx: change the default `indentPerLevel` prop from 16 to 8,
giving an 8px step per nesting level for a more compact page tree.
No callers override the value, so the single default change applies
everywhere DocTree is rendered. EE-hiding injections from docmost-app
were skipped: those surfaces are already removed in this community fork.
Strip the proprietary client EE so the fork ships a clean community/AGPL
edition, mirroring Forkmost. Delete apps/client/src/ee (201 files) and
packages/ee, and patch every consumer that imported from @/ee/*.
- gate-out EE features (useHasFeature -> false): API keys, SSO, MFA, SCIM,
audit logs, AI / AI-chat, templates, page permissions, page verification,
comment resolution, trash retention, viewer comments
- drop cloud/billing/trial/entitlement/posthog flows; sign-in is now
email+password only (no SSO/LDAP/cloud)
- remove EE routes from App.tsx and EE entries from sidebars/settings nav
- restore the community page-share button (ShareModal) that the EE
PageShareModal used to provide
- remove the dead "Attachments" search filter, dead MFA navigation and
orphaned route constants
Client type-checks clean; full `pnpm build` is green for all three projects.
Remove the private apps/server/src/ee git submodule (github.com/docmost/ee)
and the now-empty .gitmodules so that `git clone --recurse-submodules` and CI
checkout no longer fail with 404. The server loads EE only via guarded runtime
require(), so the build succeeds without it (community edition).
Rewrite .github/workflows/release.yml for the fork:
- drop the GitHub App token step and `submodules: recursive` checkout
- publish to GHCR (ghcr.io/vvzvlad/gitmost) via the built-in GITHUB_TOKEN
instead of Docker Hub (docmost/docmost) — no extra secrets required
- add `packages: write` permission and an IMAGE env var
- log in as github.repository_owner; rename release tarballs to gitmost-*
Repoint the Dockerfile image source label to the fork.