Files
Codewalkers/apps/server/agent/content-serializer.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

127 lines
3.2 KiB
TypeScript

/**
* Content Serializer
*
* Converts Tiptap JSON page tree into markdown for agent prompts.
* Uses @tiptap/markdown's MarkdownManager for standard node serialization,
* with custom handling only for pageLink nodes.
*/
import { Node, type JSONContent } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import { MarkdownManager } from '@tiptap/markdown';
/**
* Minimal page shape needed for serialization.
*/
export interface PageForSerialization {
id: string;
parentPageId: string | null;
title: string;
content: string | null; // JSON string from Tiptap
sortOrder: number;
}
/**
* Server-side pageLink node — only needs schema definition + markdown rendering.
*/
const ServerPageLink = Node.create({
name: 'pageLink',
group: 'block',
atom: true,
addAttributes() {
return {
pageId: { default: null },
};
},
renderMarkdown(node: JSONContent) {
const pageId = (node.attrs?.pageId as string) ?? '';
return `[[page:${pageId}]]\n\n`;
},
});
let _manager: MarkdownManager | null = null;
function getManager(): MarkdownManager {
if (!_manager) {
_manager = new MarkdownManager({
extensions: [StarterKit, Link, ServerPageLink],
});
}
return _manager;
}
/**
* Convert a Tiptap JSON document to markdown.
*/
export function tiptapJsonToMarkdown(json: unknown): string {
if (!json || typeof json !== 'object') return '';
const doc = json as JSONContent;
if (doc.type !== 'doc' || !Array.isArray(doc.content)) return '';
return getManager().serialize(doc).trim();
}
/**
* Serialize an array of pages into a single markdown document.
* Pages are organized as a tree (root first, then children by sortOrder).
*
* Each page is marked with <!-- page:$id --> so the agent can reference them.
*/
export function serializePageTree(pages: PageForSerialization[]): string {
if (pages.length === 0) return '';
// Build parent→children map
const childrenMap = new Map<string | null, PageForSerialization[]>();
for (const page of pages) {
const parentKey = page.parentPageId;
if (!childrenMap.has(parentKey)) {
childrenMap.set(parentKey, []);
}
childrenMap.get(parentKey)!.push(page);
}
// Sort children by sortOrder
for (const children of childrenMap.values()) {
children.sort((a, b) => a.sortOrder - b.sortOrder);
}
// Render tree depth-first
const sections: string[] = [];
function renderPage(page: PageForSerialization, depth: number): void {
const headerPrefix = '#'.repeat(Math.min(depth + 1, 6));
let section = `<!-- page:${page.id} -->\n${headerPrefix} ${page.title}`;
if (page.content) {
try {
const parsed = JSON.parse(page.content);
const md = tiptapJsonToMarkdown(parsed);
if (md.trim()) {
section += `\n\n${md}`;
}
} catch {
// Invalid JSON — skip content
}
}
sections.push(section);
const children = childrenMap.get(page.id) ?? [];
for (const child of children) {
renderPage(child, depth + 1);
}
}
// Start from root pages (parentPageId is null)
const roots = childrenMap.get(null) ?? [];
for (const root of roots) {
renderPage(root, 1);
}
return sections.join('\n\n');
}