Rebuild the workspace AI settings page into card-based "Endpoints"
(Chat / Embeddings / Voice) matching the new design, and split the
single connection test into independent per-endpoint Test buttons.
- server: testConnection(workspaceId, capability) probes only the
requested capability ('chat' | 'embeddings'); add TestAiConnectionDto
and wire it through the /workspace/ai-settings/test controller
- client: testAiConnection(capability) + capability-typed mutation; two
independent test mutation instances so Chat/Embeddings results are isolated
- client: full rewrite of ai-provider-settings into Endpoints section —
drop the provider dropdown (driver is always openai, base URL + key
always shown), move the "AI chat" and surface the "Semantic search"
feature toggles into card headers, system message behind an Edit modal,
pgvector/reindex footer, and a disabled Voice/STT stub
- client: restyle external MCP tools and the MCP server section; collapse
the AI sections in workspace-settings; remove the standalone
ai-chat-settings component
- toggles now surface the server error message (e.g. missing pgvector)
- i18n: add new English strings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This document outlines the removal of non‑functional DOCX, PDF, and Confluence
import options that relied on a private EE module. It records the completed
frontend changes and lists the remaining backend cleanup tasks.
DOCX, PDF and Confluence import relied on a private EE module that was
dropped from this build, so those code paths only threw "enterprise
license" errors (DOCX/PDF) or silently did nothing (Confluence) while the
UI still presented them as working options.
- page-import-modal: drop the Word (DOCX), PDF and Confluence FileButtons
- remove the now-dead icon imports (IconFileTypeDocx, IconFileTypePdf,
ConfluenceIcon), the docx/pdf/confluence file refs and their input-reset
branches in handleFileUpload/handleZipUpload
- delete the orphaned confluence-icon.tsx component (no remaining importers)
Markdown, HTML, Notion and the generic zip upload remain unchanged.
Improve agent RAG quality with three changes, plus a roadmap doc for the rest.
- Indexer: prefix each chunk with its heading path ("Page > H1 > H2"), built by
walking the ProseMirror JSON (heading nodes) so a `#` inside a fenced code block
is never mistaken for a heading. Falls back to plain-text chunking on any error.
buildChunkRows: drop indexOf-against-source offsets (breadcrumb prefixes break
verbatim matching) for a cumulative cursor — offsets are provenance-only.
- Hybrid search: new migration adds a generated `fts` tsvector column + GIN index
to page_embeddings (same english+f_unaccent config as pages.tsv). New
PageEmbeddingRepo.hybridSearch fuses cosine + full-text rankings via Reciprocal
Rank Fusion (k=60, equal weights) in one SQL query at chunk granularity.
- Tools: collapse semanticSearch + searchPages into one hybrid `searchPages` tool
with a query-rewrite-oriented description; gracefully falls back to the REST
full-text path when embeddings are unconfigured. Access control (space scope +
page-permission post-filter) preserved. Add a query-rewrite hint to the default
system prompt.
- docs/rag-improvements-plan.md: record what shipped and the deferred backlog
(reranker, attachment indexing, eval harness, tuning).
Note: requires a corpus reindex to populate breadcrumbs on existing pages.
"Indexed N of M pages" stayed at e.g. "27 of 34" forever even after a
successful full reindex. The numerator counted pages that have embeddings
while the denominator counted ALL non-deleted pages, so empty / text-less
pages (which legitimately store zero embeddings) could never be reached.
Add PageRepo.countEmbeddablePages: counts non-deleted pages that have
non-empty textContent OR already have a stored embedding row, and use it as
the totalPages denominator in AiSettingsService.getMasked. The "has
embeddings" clause covers pages indexed from the content JSON (null
textContent) and guarantees indexedPages <= totalPages. No DB migration.
The floating AI chat window used a hard-coded border-radius of 14px,
larger than any other element and out of line with the rest of the UI.
Switch to the Mantine md radius token (8px) so the window corners blend
with the inner cards and message bubbles.
The bulk embedding reindex could hang on a single page forever
("Indexed 27 of 34 pages") with zero log output:
- all progress logs were debug-level, suppressed in production (pino info);
- embedMany() had no timeout, so a slow/hung embeddings endpoint blocked
the sequential per-page loop indefinitely.
Changes:
- ai.service.embedTexts: bound embedMany with AbortSignal.timeout
(configurable via AI_EMBEDDING_TIMEOUT_MS, default 120000ms); on timeout
throw a clear, greppable message, classified by both signal.aborted and
the error name (TimeoutError/AbortError/ResponseAborted) so a real
provider error racing the timer keeps its diagnostics.
- embedding-indexer.reindexWorkspace: promote lifecycle/progress logs to
info; log "[i/N] indexing page <id>" BEFORE the await so a hang names the
stuck page; warn on slow pages (>30s); add timing + final summary.
- .env.example: document AI_EMBEDDING_TIMEOUT_MS.
A misconfigured embeddings endpoint failed the RAG indexer with an opaque
"Invalid JSON response" and was not caught by "Test connection" (which only
probed the chat model), so it only surfaced silently during background
indexing.
- add describeProviderError(): formats AI SDK errors as
"<statusCode>: <message> | response body: <truncated one-line snippet>"
(statusCode/message/responseBody never carry the API key)
- use it in the bulk-reindex catch and the embedding processor's formatter so
the real cause (e.g. an HTML 404 from a wrong base URL) is visible in logs
- testConnection now probes chat AND embeddings independently: skips a probe
when that capability is unconfigured, returns ok:false with a Chat:/Embeddings:
prefix on real failure, "not configured" when neither is set
The WORKSPACE_CREATE_EMBEDDINGS / WORKSPACE_DELETE_EMBEDDINGS jobs were
enqueued (on AI Search enable/disable) but had no AI_QUEUE handler, so
existing pages were never indexed ("Indexed 0 of N pages") and disabling
never purged embeddings.
- EmbeddingProcessor: handle WORKSPACE_CREATE_EMBEDDINGS (bulk reindex all
live pages) and WORKSPACE_DELETE_EMBEDDINGS (purge workspace embeddings)
- EmbeddingIndexerService: add reindexWorkspace() (skips when embeddings
unconfigured; per-page error isolation) and removeWorkspace()
- PageRepo.getIdsByWorkspace(), PageEmbeddingRepo.deleteByWorkspace()
- AiSettingsService.reindex() + admin-only POST /workspace/ai-settings/reindex
- Frontend: "Reindex now" button, service call and mutation
- Stable per-workspace jobId with remove-before-add so a stale job can't
block future reindexes; cancel the delayed purge on enable/reindex so it
can't wipe freshly-built embeddings
Add docs/mobile-app-plan.md capturing the mobile-app research: current
stack, existing responsive web inventory, why the editor must stay in a
WebView, comparison of native/Capacitor/hybrid paths, the recommended
Capacitor route, required backend changes (token-in-body login, CORS
whitelist, APNs/FCM push, optional Swagger), Android/iOS specifics, and
the offline outlook (referencing docs/offline-sync-plan.md).
Enrich the "Mobile app" roadmap entry in README.md and README.ru.md to
point to the new plan and correct the inaccurate "native" wording
(Capacitor wrapper of the existing web UI, iOS first, Android to follow).
Add docs/voice-dictation-plan.md — a ready-to-implement design covering
server-side Whisper transcription via the existing per-workspace AI provider,
with the mic button in both the AI agent chat and the page editor. The doc
consolidates four parts: STT provider credentials (full parity with the LLM
and embedding creds, incl. the encrypted stt_api_key_enc column and both
provider-field whitelists), the getTranscriptionModel builder + /transcribe
endpoint, the ai.dictation visibility toggle, and the client capture
(useDictation + MicButton). Includes edge cases, security notes, an
implementation order, and the full list of affected files.
Per-workspace AI provider config previously shared a single base URL and
a single API key between the chat model and the embedding model. Add
dedicated, optional embedding endpoint/token that fall back to the chat
values when empty, preserving backward compatibility.
- db: new migration adds nullable `embedding_api_key_enc` to
`ai_provider_credentials`; chat key stays in `api_key_enc`
- repo: add `upsertEmbeddingKey` / `clearEmbeddingKey` (on-conflict
touches only its own column, so chat/embedding keys never overwrite)
- ai-settings.service: store non-secret `embeddingBaseUrl`; resolve()
applies fallback (embeddingBaseUrl || baseUrl; embedding key || chat
key); getMasked() exposes raw `embeddingBaseUrl` + `hasEmbeddingApiKey`,
never the key; update() handles the embedding key write-only
- ai.service: getEmbeddingModel() builds openai/gemini/ollama with the
embedding-specific URL/key; chat path unchanged
- client: new "Embedding base URL" and "Embedding API key" fields with
fallback hints and a clear-key action
Requires running the DB migration on deploy.
The insert_image and replace_image MCP tools previously uploaded only
local files (filePath), which an AI MCP client cannot provide — it has
no access to the server filesystem. Replace filePath with a required
imageUrl and download the image over http(s).
- client.ts: add fetchRemoteImage(url, maxBytes) — http/https-only scheme
allowlist, 20 MiB cap (maxContentLength + post-download length recheck),
30s timeout, Content-Type→MIME resolution with URL-extension fallback,
filename derivation with canonical extension
- client.ts: rewrite uploadImage(pageId, url) as URL-only; drop the
local-file branch, imageMimeFromPath and the fs import; insertImage/
replaceImage now take a url
- index.ts: drop filePath, add required imageUrl to both tools; update
tool descriptions and SERVER_INSTRUCTIONS
- README: document the web-URL behaviour
The EE-frontend removal (a88b3f77) deleted enable-ai-chat.tsx, the only UI
that set the workspace flag settings.ai.chat. The backend gates remained:
the header assistant icon (page-header-menu) and the /ai-chat/stream endpoint
both require settings.ai.chat === true, and the flag is auto-enabled only in
cloud mode. As a result, self-hosted community builds could configure an AI
provider but had no way to turn the chat on, so the assistant icon never
appeared.
Restore a single admin toggle wired to the already-existing backend `aiChat`
field, modeled on the adjacent MCP settings switch:
- add aiChat?: boolean to the client IWorkspace type
- add AiChatSettings component (optimistic update, revert-on-error,
workspaceAtom sync so the icon updates without reload)
- render it (admin-only) in workspace settings between AI / Models and
AI / External tools (MCP)
Generative AI and AI search toggles, removed in the same commit, are left
out of scope.
Replace the bare brand text on pages with the Gitmost logo lockup
(mark + "gitmost" wordmark) and use the mark as the favicon.
- add generated logo lockups (text outlined from Space Grotesk SemiBold)
in dark/light ink variants; add reusable theme-aware <BrandLogo> component
- use BrandLogo in the global header (mark-only on mobile, full lockup on
desktop) and on auth pages, dropping the old Docmost icon + plain text
- point favicon to /brand/gitmost-favicon.svg (SVG primary + PNG fallbacks);
regenerate favicon/app-icon PNGs from the brand SVGs
- rename app name Docmost -> Gitmost (getAppName, index.html title/apple
title, manifest name); use getAppName() in the 404 title
- align theme/background colors to the brand tile (#0E1117)
- move brand guide and logos into docs/brand/ (canonical home) with a README,
and serve runtime copies from apps/client/public/brand/
Document migrating an existing Docmost instance to Gitmost. The DB schema
is a strict superset (additive-only migrations that auto-apply on boot), so
the move is essentially two image swaps; the only hard requirement is the
pgvector DB image for the page_embeddings RAG table (CREATE EXTENSION vector).
- Primary path: clean in-place swap from a Docmost on postgres:18
(app -> ghcr.io/vvzvlad/gitmost, db -> pgvector/pgvector:pg18), no
dump/restore, volume reused as-is.
- Notes: pg_dump backup first, identical PGDATA layout, matching the
Postgres major (pg16 case), managed-Postgres caveat, AI opt-in.
- Added to both README.md and README.ru.md.
Read tools (getPage, getPageJson, getNode, diffPageVersions,
exportPageMarkdown) return whole pages with no size cap. Their outputs
were stored verbatim in metadata.parts and the tool_calls column, and
metadata.parts is replayed to the provider on every later turn via
convertToModelMessages. After reading a couple of large pages the prompt
grew by full page bodies each turn — rising token cost, latency and DB
row size.
Add compactToolOutput(): a pure, recursive, size-bounded compactor used
in assistantParts() and serializeSteps(). It preserves the value's kind
and small scalar fields (id/title/pageId, which the client reads to build
citations on reload) while truncating long strings, capping long arrays
with a marker, and collapsing subtrees past a depth limit. Small outputs
are returned unchanged by identity. Tool inputs are left intact so
replayed tool_use arguments keep their object shape.
Compaction runs only at persistence time (onFinish/onAbort), so the live
stream and the current turn's multi-step reasoning still see full bodies.
Add unit tests for compactToolOutput.
Typing into the composer while the agent was streaming lost the draft once
the turn finished: on a brand-new chat, adopting the freshly created chat id
changes ChatThread's key and remounts it, wiping ChatInput's local state.
Lift the composer draft into a module-level jotai atom (aiChatDraftAtom) so it
survives the remount. Reset it only on deliberate chat switches — startNewChat,
selectChat, and the page-history "AI agent" badge deep-link — so a draft never
leaks between conversations, while adoption (which goes through a useEffect)
preserves it.
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.