vibeman 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/runtime/api/.tsbuildinfo +1 -1
  2. package/dist/runtime/api/agent/agent-service.d.ts +7 -6
  3. package/dist/runtime/api/agent/agent-service.js +36 -27
  4. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +2 -0
  5. package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +62 -30
  6. package/dist/runtime/api/agent/codex-cli-provider.test.js +47 -2
  7. package/dist/runtime/api/agent/routing-policy.d.ts +13 -30
  8. package/dist/runtime/api/agent/routing-policy.js +82 -132
  9. package/dist/runtime/api/agent/routing-policy.test.js +63 -0
  10. package/dist/runtime/api/api/routers/ai.d.ts +15 -3
  11. package/dist/runtime/api/api/routers/ai.js +7 -6
  12. package/dist/runtime/api/api/routers/executions.d.ts +1 -1
  13. package/dist/runtime/api/api/routers/tasks.d.ts +3 -3
  14. package/dist/runtime/api/api/routers/workflows.d.ts +8 -0
  15. package/dist/runtime/api/api/routers/workflows.js +2 -1
  16. package/dist/runtime/api/api/trpc.d.ts +6 -6
  17. package/dist/runtime/api/lib/trpc/server.d.ts +27 -7
  18. package/dist/runtime/api/router.d.ts +27 -7
  19. package/dist/runtime/api/settings-service.js +49 -1
  20. package/dist/runtime/api/types/index.d.ts +8 -1
  21. package/dist/runtime/api/types/settings.d.ts +15 -2
  22. package/dist/runtime/api/workflows/vibing-orchestrator.js +32 -1
  23. package/dist/runtime/web/.next/BUILD_ID +1 -1
  24. package/dist/runtime/web/.next/app-build-manifest.json +18 -11
  25. package/dist/runtime/web/.next/app-path-routes-manifest.json +2 -1
  26. package/dist/runtime/web/.next/build-manifest.json +2 -2
  27. package/dist/runtime/web/.next/prerender-manifest.json +10 -10
  28. package/dist/runtime/web/.next/routes-manifest.json +8 -0
  29. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -0
  30. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -0
  31. package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -0
  32. package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  33. package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
  34. package/dist/runtime/web/.next/server/app/_not-found.rsc +1 -1
  35. package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  36. package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
  37. package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
  38. package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
  39. package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  40. package/dist/runtime/web/.next/server/app/index.html +2 -2
  41. package/dist/runtime/web/.next/server/app/index.rsc +2 -2
  42. package/dist/runtime/web/.next/server/app/page.js +21 -21
  43. package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/dist/runtime/web/.next/server/app-paths-manifest.json +2 -1
  45. package/dist/runtime/web/.next/server/pages/404.html +2 -2
  46. package/dist/runtime/web/.next/server/pages/500.html +1 -1
  47. package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
  48. package/dist/runtime/web/.next/static/5_15u1WQCxN1_eHZpldCv/_buildManifest.js +1 -0
  49. package/dist/runtime/web/.next/static/chunks/{277-0142a939f08738c3.js → 823-6f371a6e829adbba.js} +1 -1
  50. package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-751c9265a65409e5.js +1 -0
  51. package/dist/runtime/web/.next/static/chunks/app/api/health/route-751c9265a65409e5.js +1 -0
  52. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-751c9265a65409e5.js +1 -0
  53. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-751c9265a65409e5.js +1 -0
  54. package/dist/runtime/web/.next/static/chunks/app/page-9fe7d75095b4ccec.js +1 -0
  55. package/package.json +1 -1
  56. package/dist/runtime/api/lib/image-paste-drop-extension.d.ts +0 -26
  57. package/dist/runtime/api/lib/image-paste-drop-extension.js +0 -125
  58. package/dist/runtime/api/lib/markdown-utils.d.ts +0 -8
  59. package/dist/runtime/api/lib/markdown-utils.js +0 -282
  60. package/dist/runtime/api/lib/markdown-utils.test.js +0 -348
  61. package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.d.ts +0 -1
  62. package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.js +0 -27
  63. package/dist/runtime/api/lib/tiptap-utils.d.ts +0 -130
  64. package/dist/runtime/api/lib/tiptap-utils.js +0 -327
  65. package/dist/runtime/web/.next/static/chunks/app/api/health/route-105a61ae865ba536.js +0 -1
  66. package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-105a61ae865ba536.js +0 -1
  67. package/dist/runtime/web/.next/static/chunks/app/api/upload/route-105a61ae865ba536.js +0 -1
  68. package/dist/runtime/web/.next/static/chunks/app/page-8c3ba579efc6f918.js +0 -1
  69. package/dist/runtime/web/.next/static/mRpNgPfbYR_0wrODzlg_4/_buildManifest.js +0 -1
  70. /package/dist/runtime/api/{lib/markdown-utils.test.d.ts → agent/routing-policy.test.d.ts} +0 -0
  71. /package/dist/runtime/web/.next/static/{mRpNgPfbYR_0wrODzlg_4 → 5_15u1WQCxN1_eHZpldCv}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeman",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "Vibeman CLI - Command-line interface for starting the Vibeman development environment",
6
6
  "main": "dist/index.js",
@@ -1,26 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- export interface ImagePasteDropOptions {
3
- /**
4
- * Function that handles the image upload process
5
- */
6
- upload?: (file: File, onProgress?: (event: {
7
- progress: number;
8
- }) => void, abortSignal?: AbortSignal) => Promise<string>;
9
- /**
10
- * Callback for upload errors
11
- */
12
- onError?: (error: Error) => void;
13
- /**
14
- * Callback for successful uploads
15
- */
16
- onSuccess?: (url: string) => void;
17
- /**
18
- * Maximum file size in bytes
19
- */
20
- maxSize?: number;
21
- /**
22
- * Allowed MIME types
23
- */
24
- allowedTypes?: string[];
25
- }
26
- export declare const ImagePasteDrop: Extension<ImagePasteDropOptions, any>;
@@ -1,125 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- import { Plugin, PluginKey } from '@tiptap/pm/state';
3
- import { handleImageUpload } from './tiptap-utils.js';
4
- export const ImagePasteDrop = Extension.create({
5
- name: 'imagePasteDrop',
6
- addOptions() {
7
- return {
8
- upload: handleImageUpload,
9
- onError: undefined,
10
- onSuccess: undefined,
11
- maxSize: 5 * 1024 * 1024, // 5MB
12
- allowedTypes: [
13
- 'image/jpeg',
14
- 'image/jpg',
15
- 'image/png',
16
- 'image/gif',
17
- 'image/webp',
18
- 'image/svg+xml',
19
- ],
20
- };
21
- },
22
- addProseMirrorPlugins() {
23
- const uploadAndInsertImages = (files, view, position) => {
24
- files.forEach(async (file) => {
25
- try {
26
- // Validate file size
27
- if (this.options.maxSize && file.size > this.options.maxSize) {
28
- const error = new Error(`File size exceeds maximum allowed (${this.options.maxSize / (1024 * 1024)}MB)`);
29
- this.options.onError?.(error);
30
- return;
31
- }
32
- // Insert placeholder first
33
- const placeholderTransaction = view.state.tr.insert(position, view.state.schema.text('🔄 Uploading...'));
34
- view.dispatch(placeholderTransaction);
35
- // Upload the image
36
- const uploadFunction = this.options.upload || handleImageUpload;
37
- const imageUrl = await uploadFunction(file);
38
- // Replace placeholder with actual image
39
- const filename = file.name.replace(/\.[^/.]+$/, '') || 'image';
40
- const imageNode = view.state.schema.nodes.image.create({
41
- src: imageUrl,
42
- alt: filename,
43
- title: filename,
44
- });
45
- // Find the placeholder text and replace it
46
- const currentState = view.state;
47
- let found = false;
48
- currentState.doc.descendants((node, pos) => {
49
- if (!found && node.isText && node.text === '🔄 Uploading...') {
50
- const replaceTransaction = currentState.tr.replaceRangeWith(pos, pos + node.nodeSize, imageNode);
51
- view.dispatch(replaceTransaction);
52
- found = true;
53
- return false;
54
- }
55
- return true;
56
- });
57
- this.options.onSuccess?.(imageUrl);
58
- }
59
- catch (error) {
60
- // Remove placeholder on error
61
- const currentState = view.state;
62
- currentState.doc.descendants((node, pos) => {
63
- if (node.isText && node.text === '🔄 Uploading...') {
64
- const deleteTransaction = currentState.tr.delete(pos, pos + node.nodeSize);
65
- view.dispatch(deleteTransaction);
66
- return false;
67
- }
68
- return true;
69
- });
70
- const errorMessage = error instanceof Error ? error : new Error('Upload failed');
71
- this.options.onError?.(errorMessage);
72
- }
73
- });
74
- };
75
- return [
76
- new Plugin({
77
- key: new PluginKey('imagePasteDrop'),
78
- props: {
79
- handleDOMEvents: {
80
- // Handle drag and drop
81
- drop: (view, event) => {
82
- event.preventDefault();
83
- const { files } = event.dataTransfer;
84
- const imageFiles = Array.from(files).filter((file) => this.options.allowedTypes?.includes(file.type));
85
- if (imageFiles.length === 0) {
86
- return false;
87
- }
88
- // Get drop position
89
- const pos = view.posAtCoords({
90
- left: event.clientX,
91
- top: event.clientY,
92
- });
93
- if (!pos)
94
- return false;
95
- uploadAndInsertImages(imageFiles, view, pos.pos);
96
- return true;
97
- },
98
- // Handle paste
99
- paste: (view, event) => {
100
- const { files } = event.clipboardData;
101
- const imageFiles = Array.from(files).filter((file) => this.options.allowedTypes?.includes(file.type));
102
- if (imageFiles.length === 0) {
103
- return false;
104
- }
105
- event.preventDefault();
106
- // Insert at current cursor position
107
- const { selection } = view.state;
108
- uploadAndInsertImages(imageFiles, view, selection.from);
109
- return true;
110
- },
111
- // Prevent default drag behaviors
112
- dragover: (view, event) => {
113
- event.preventDefault();
114
- return false;
115
- },
116
- dragenter: (view, event) => {
117
- event.preventDefault();
118
- return false;
119
- },
120
- },
121
- },
122
- }),
123
- ];
124
- },
125
- });
@@ -1,8 +0,0 @@
1
- /**
2
- * Convert markdown to HTML for Tiptap editor
3
- */
4
- export declare function markdownToHtml(markdown: string): string;
5
- /**
6
- * Convert HTML to markdown for storage
7
- */
8
- export declare function htmlToMarkdown(html: string): string;
@@ -1,282 +0,0 @@
1
- import { marked } from 'marked';
2
- import TurndownService from 'turndown';
3
- import { gfm } from 'turndown-plugin-gfm';
4
- marked.setOptions({
5
- gfm: true,
6
- breaks: false,
7
- });
8
- const turndownService = new TurndownService({
9
- headingStyle: 'atx',
10
- hr: '---',
11
- bulletListMarker: '-',
12
- codeBlockStyle: 'fenced',
13
- fence: '```',
14
- });
15
- // Use GitHub Flavored Markdown plugin
16
- turndownService.use(gfm);
17
- // Custom rule for regular list items to use single space after dash
18
- turndownService.addRule('listItem', {
19
- filter: (node) => {
20
- return (node.nodeName === 'LI' && !node.getAttribute('data-type') // Not a task item
21
- );
22
- },
23
- replacement: (content, node, _options) => {
24
- content = content
25
- .replace(/^\n+/, '') // Remove leading newlines
26
- .replace(/\n+$/, '\n') // Replace trailing newlines with just one
27
- .replace(/\n/gm, '\n '); // Indent subsequent lines
28
- let prefix = '- '; // Use single space after dash
29
- const parent = node.parentNode;
30
- if (parent && parent.nodeName === 'OL') {
31
- const index = Array.prototype.indexOf.call(parent.children, node);
32
- const start = parent.getAttribute('start');
33
- const startIndex = start ? parseInt(start, 10) - 1 : 0;
34
- prefix = startIndex + index + 1 + '. ';
35
- }
36
- return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
37
- },
38
- });
39
- // Custom rule for TipTap TaskItems
40
- turndownService.addRule('tiptapTaskItem', {
41
- filter: (node, _options) => {
42
- return (node.nodeName === 'LI' &&
43
- node.getAttribute('data-type') === 'taskItem' &&
44
- node.getAttribute('data-checked') !== null);
45
- },
46
- replacement: (content, node) => {
47
- const element = node;
48
- const isChecked = element.getAttribute('data-checked') === 'true';
49
- const checkbox = isChecked ? '[x]' : '[ ]';
50
- // Extract text content from the div > p structure
51
- const contentDiv = element.querySelector('div');
52
- const textContent = contentDiv ? contentDiv.textContent?.trim() || '' : content.trim();
53
- return `- ${checkbox} ${textContent}`;
54
- },
55
- });
56
- // Custom rule for TipTap TaskLists - handles nested structure properly
57
- turndownService.addRule('tiptapTaskList', {
58
- filter: (node) => {
59
- return node.nodeName === 'UL' && node.getAttribute('data-type') === 'taskList';
60
- },
61
- replacement: (content, node) => {
62
- // Process only direct child li elements to preserve nesting
63
- const directItems = Array.from(node.children).filter((child) => child.nodeName === 'LI' && child.getAttribute('data-type') === 'taskItem');
64
- if (directItems.length === 0)
65
- return content;
66
- const processTaskItem = (li, indent = '') => {
67
- const isChecked = li.getAttribute('data-checked') === 'true';
68
- const checkbox = isChecked ? '[x]' : '[ ]';
69
- // Get text content from the div, excluding nested lists
70
- const contentDiv = li.querySelector('div');
71
- let textContent = '';
72
- if (contentDiv) {
73
- // Clone the div to avoid modifying the original
74
- const tempDiv = contentDiv.cloneNode(true);
75
- // Remove any nested ul elements from the clone to get just the text
76
- const nestedLists = Array.from(tempDiv.querySelectorAll('ul'));
77
- nestedLists.forEach((list) => {
78
- if (list instanceof Element) {
79
- list.remove();
80
- }
81
- });
82
- textContent = tempDiv.textContent?.trim() || '';
83
- }
84
- let result = `${indent}- ${checkbox} ${textContent}`;
85
- // Process nested task lists
86
- const nestedList = li.querySelector('ul[data-type="taskList"]');
87
- if (nestedList) {
88
- const nestedItems = Array.from(nestedList.children).filter((child) => child.nodeName === 'LI' && child.getAttribute('data-type') === 'taskItem');
89
- nestedItems.forEach((nestedLi) => {
90
- if (nestedLi instanceof Element) {
91
- result += '\n' + processTaskItem(nestedLi, indent + ' ');
92
- }
93
- });
94
- }
95
- return result;
96
- };
97
- return '\n' + directItems.map((li) => processTaskItem(li)).join('\n') + '\n';
98
- },
99
- });
100
- // Override paragraph handling to preserve paragraph breaks
101
- // Ensure there is a blank line between paragraphs in markdown
102
- turndownService.addRule('paragraph', {
103
- filter: 'p',
104
- replacement: (content) => {
105
- return '\n\n' + content.trim() + '\n\n';
106
- },
107
- });
108
- /**
109
- * Clean up HTML by removing <p> tags within <li> elements under <ul> or <ol> lists
110
- */
111
- function cleanupListParagraphs(html) {
112
- if (!html?.trim())
113
- return html;
114
- // Use DOM parsing to handle nested structures properly
115
- if (typeof window !== 'undefined') {
116
- const parser = new DOMParser();
117
- const doc = parser.parseFromString(html, 'text/html');
118
- // Find all <li> elements within <ul> or <ol>
119
- const listItems = doc.querySelectorAll('ul > li, ol > li');
120
- listItems.forEach((li) => {
121
- // Find all <p> tags directly within this <li>
122
- const paragraphs = Array.from(li.querySelectorAll('p'));
123
- paragraphs.forEach((p) => {
124
- // Only remove <p> if it's a direct child or within the list item structure
125
- if (p.parentElement === li ||
126
- (p.parentElement && ['UL', 'OL', 'LI'].includes(p.parentElement.nodeName))) {
127
- // Replace <p> with its content
128
- const content = p.innerHTML;
129
- const textNode = doc.createDocumentFragment();
130
- textNode.appendChild(doc.createTextNode(content));
131
- // For better HTML structure, we'll replace with the content directly
132
- p.outerHTML = content;
133
- }
134
- });
135
- });
136
- return doc.body.innerHTML;
137
- }
138
- // Fallback for server-side: use regex approach (less robust but works)
139
- // Remove <p> and </p> tags within <li> elements
140
- return html
141
- .replace(/<li([^>]*)><p>/g, '<li$1>')
142
- .replace(/<\/p><\/li>/g, '</li>')
143
- .replace(/<\/p><ul>/g, '<ul>')
144
- .replace(/<\/p><ol>/g, '<ol>')
145
- .replace(/<\/ul><p>/g, '</ul>')
146
- .replace(/<\/ol><p>/g, '</ol>');
147
- }
148
- /**
149
- * Convert GFM checkbox HTML to Tiptap TaskItem format
150
- */
151
- function convertToTiptapTaskItems(html) {
152
- if (!html?.trim())
153
- return html;
154
- // Use DOM parsing for better accuracy
155
- if (typeof window !== 'undefined') {
156
- const parser = new DOMParser();
157
- const doc = parser.parseFromString(html, 'text/html');
158
- // Process all ul elements, starting from the deepest nested ones
159
- const lists = Array.from(doc.querySelectorAll('ul')).reverse();
160
- lists.forEach((ul) => {
161
- let hasTaskItems = false;
162
- // Only process direct children li elements to preserve nesting
163
- const directListItems = Array.from(ul.children).filter((child) => child.nodeName === 'LI');
164
- directListItems.forEach((li) => {
165
- const checkbox = li.querySelector('input[type="checkbox"]');
166
- if (checkbox) {
167
- hasTaskItems = true;
168
- // Get the checkbox state
169
- const isChecked = checkbox.hasAttribute('checked');
170
- // Set TaskItem attributes
171
- li.setAttribute('data-type', 'taskItem');
172
- li.setAttribute('data-checked', isChecked ? 'true' : 'false');
173
- // Get the text content, preserving nested structure
174
- const clonedLi = li.cloneNode(true);
175
- // Remove the checkbox from the clone
176
- const clonedCheckbox = clonedLi.querySelector('input[type="checkbox"]');
177
- if (clonedCheckbox) {
178
- clonedCheckbox.remove();
179
- }
180
- // Extract nested lists to preserve them
181
- const nestedLists = Array.from(clonedLi.querySelectorAll('ul'));
182
- const nestedListsHTML = nestedLists.map((list) => list.outerHTML);
183
- // Remove nested lists temporarily to get just the text content
184
- nestedLists.forEach((list) => {
185
- if (list instanceof Element) {
186
- list.remove();
187
- }
188
- });
189
- // Get the text content
190
- const textContent = clonedLi.textContent?.trim() || '';
191
- // Create the proper TipTap structure
192
- let innerHTML = `
193
- <label contenteditable="false">
194
- <input type="checkbox" ${isChecked ? 'checked' : ''}>
195
- <span></span>
196
- </label>
197
- <div>
198
- <p>${textContent}</p>`;
199
- // Add back any nested lists, ensuring they have the correct data-type
200
- if (nestedListsHTML.length > 0) {
201
- nestedListsHTML.forEach((nestedHTML) => {
202
- // Make sure nested task lists have the correct data-type attribute
203
- let processedHTML = nestedHTML;
204
- if (nestedHTML.includes('data-type="taskItem"') &&
205
- !nestedHTML.includes('data-type="taskList"')) {
206
- processedHTML = nestedHTML.replace('<ul>', '<ul data-type="taskList">');
207
- }
208
- innerHTML += processedHTML;
209
- });
210
- }
211
- innerHTML += '</div>';
212
- li.innerHTML = innerHTML.trim();
213
- }
214
- });
215
- // If this ul contains task items, mark it as a taskList
216
- if (hasTaskItems) {
217
- ul.setAttribute('data-type', 'taskList');
218
- }
219
- });
220
- return doc.body.innerHTML;
221
- }
222
- // Fallback for server-side: use regex approach (less robust)
223
- return (html
224
- // First pass: Convert list items with checkboxes
225
- .replace(/<li>(\s*)<input\s+([^>]*?)type="checkbox"([^>]*?)>([^<]*)/gi, (match, leadingSpace, beforeType, afterType, textContent) => {
226
- const isChecked = beforeType.includes('checked') || afterType.includes('checked');
227
- const cleanText = textContent.trim();
228
- return `<li data-type="taskItem" data-checked="${isChecked ? 'true' : 'false'}">
229
- <label contenteditable="false">
230
- <input type="checkbox" ${isChecked ? 'checked' : ''}>
231
- <span></span>
232
- </label>
233
- <div>
234
- <p>${cleanText}</p>
235
- </div>`;
236
- })
237
- // Second pass: Mark parent ul as taskList if it contains taskItems
238
- .replace(/<ul>(\s*(?:<li[^>]*data-type="taskItem"[^>]*>[\s\S]*?<\/li>\s*)+)<\/ul>/gi, '<ul data-type="taskList">$1</ul>'));
239
- }
240
- /**
241
- * Pre-process markdown to ensure proper task list item boundaries
242
- */
243
- function preprocessMarkdownTaskItems(markdown) {
244
- if (!markdown?.trim())
245
- return markdown;
246
- // Split markdown into lines
247
- const lines = markdown.split('\n');
248
- const processedLines = [];
249
- for (let i = 0; i < lines.length; i++) {
250
- const line = lines[i];
251
- const nextLine = i + 1 < lines.length ? lines[i + 1] : '';
252
- processedLines.push(line);
253
- // If current line is a task item and next line is not empty and not indented
254
- // and not another list item, add a blank line to separate them
255
- if (line.trim().match(/^-\s+\[[ x]\]/) &&
256
- nextLine.trim() &&
257
- !nextLine.match(/^-\s+/) &&
258
- !nextLine.match(/^\s{2,}/)) {
259
- processedLines.push(''); // Add blank line
260
- }
261
- }
262
- return processedLines.join('\n');
263
- }
264
- /**
265
- * Convert markdown to HTML for Tiptap editor
266
- */
267
- export function markdownToHtml(markdown) {
268
- if (!markdown?.trim())
269
- return '';
270
- const preprocessedMarkdown = preprocessMarkdownTaskItems(markdown);
271
- const html = marked(preprocessedMarkdown);
272
- return convertToTiptapTaskItems(html);
273
- }
274
- /**
275
- * Convert HTML to markdown for storage
276
- */
277
- export function htmlToMarkdown(html) {
278
- if (!html?.trim())
279
- return '';
280
- const cleanedHtml = cleanupListParagraphs(html);
281
- return turndownService.turndown(cleanedHtml);
282
- }